首页
论坛
课程
招聘
[原创][保护模式编程、三]
2008-4-7 23:28 6182

[原创][保护模式编程、三]

2008-4-7 23:28
6182
【继续80386编程】

在一、到二、我们了解386基本寻址机制,没错就是这么简单!!!

接下来我们谈谈 对上一个386进行扩展:

大家在第二节已经知道了进入386的基本步骤了,那么我们来具体设计吧.

编程首先当然是【声明】与【定义】:

一、【声明】:

在386.inc 头文件里定义好需要的宏信息(好东西直接拿来用了呵呵)

;---------------------------------386.inc-----------------------------------------------------

DA_32    EQU   4000h   ;32位段      
DA_DPL0   EQU   00h ; DPL = 0
DA_DPL1   EQU   20h ; DPL = 1    (表示描述符特权级 Ring0-Ring3级)
DA_DPL2   EQU   40h ; DPL = 2
DA_DPL3   EQU   60h ; DPL = 3
;----------------------------------------------------------------------------
; 存储段描述符类型值说明
;----------------------------------------------------------------------------
DA_DR    EQU 90h ; 存在的只读数据段类型值
DA_DRW   EQU 92h ; 存在的可读写数据段属性值
DA_DRWA   EQU 93h ; 存在的已访问可读写数据段类型值
DA_C    EQU 98h ; 存在的只执行代码段属性值
DA_CR    EQU 9Ah ; 存在的可执行可读代码段属性值
DA_CCO   EQU 9Ch ; 存在的只执行一致代码段属性值
DA_CCOR   EQU 9Eh ; 存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------

%macro Descriptor 3     ;3表示宏的参数有3个 %1表示是第一个参数的标识 >>右移位
dw %2 & 0FFFFh     ; 段界限 1     (2 字节)
dw %1 & 0FFFFh     ; 段基址 1     (2 字节)
db (%1 >> 16) & 0FFh    ; 段基址 2     (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2   (2 字节)
db (%1 >> 24) & 0FFh    ; 段基址 3     (1 字节)
%endmacro ; 共 8 字节

%macro CA20 0   ; 打开地址线A20
   
in al, 92h
or al, 00000010b
out 92h, al
%endmacro

%macro DA20 0        ;关地址线

in   al,92h
and al,11111101b
out 92h,al

%endmacro

二、【定义】:

;在定义模块之前我们首先要有个概念,那就是整体的雏形。

可以简单划分出来也就是2个主要步骤:

1、定义GDT数据段:

{

        1、定义Descriptor即段描述符: (通常是以一个全为零的Descriptor开始)。

       2、定义GdtPtr 信息结构体(再加载gdtr时候要用到)。

       3、定义每个段描述符对应的索引位置(即定义选择子)。

}

2、定义如上描述的具体段:

{

     1、实模式入口段 (这是个入口段:用于跳转到386的段,并不属于386段所以没有描述符)

    2、剩下的段就全是386模式的段。

}

三、【具体编码】

还是要拿实实在在的能运行的代码来讲:

先说一下代码的主要功能:

1、从8086跳到386模式(Protect Mode)

2、在386模式对大地址的寻址测试(超过1MB)

3、测试完毕后回到8086模式(Real Mode)

具体细节上,就看代码吧!!!

在386.asm 文件里实行具体模块的编写(代码比较多,刚开始阅读有点复杂,因为是汇编可读性不是很好

不过这是照上面的方法定义的,可以先从宏观入手!!!):

;========================386.asm===============================

%include "386.inc" ; 常量, 宏, 以及一些说明
%define   _DEBUG_B0OT_
%ifdef   _DEBUG_B0OT_
    org 0100h
%else
    org 07c00h
%endif

jmp LABEL_BEGIN
;===========================;GDT全局描述符数据段==============================

[SECTION .gdt]
LABEL_GDT      Descriptor    0,0,0    ;以空开头

;这个段描述符描述的段有点特殊,因为在下面并没有实际的定义它。它的作用是从保护模式跳转到8086时要用到的,是用来初始化段寄存器的。保护模式与实模式段界限与段属性是不同的, 而这个段描述符的段基址是0、段界限是0FFFFH ,段属性是读加写,与8086 的标准是一样,所以在回到8086模式之前,CPU在对所有段寄存器进行实模式的转换就能正确安排界限与属性了,!!! ),
LABEL_DESC_NORMAL    Descriptor     0,0ffffh,DA_DRW        

LABEL_DESC_DATA    Descriptor    0,SegDataLen - 1 ,DA_DRW | DA_32    ;段属性是非一致的32位读写数据段
LABEL_DESC_STACK    Descriptor   0,TopOfStack - 1,DA_DRWA | DA_32    ;存在的已访问可读写的 32位stack段 在保护模式下的Call命令需要堆栈
LABEL_DESC_CODE32    Descriptor   0,SegCode32Len -1 ,DA_C | DA_32    ;Protect mode的32位代码区

;这个段是保护模式的段,但是它是以16位形式存放的,它是用来从保护模式跳回到8086模式。,。。。。因为直接用32位代码段跳转到8086模式是不行,必需从16位保护模式段跳转到16位8086模式。就像先前说的Normal 描述符,它也是一个具有8086属性的描述符。,所以CS段寄存器的状态也需要先转换成与8086模式相同段界限与段属性。这样才能正确的转换到 8086模式,由此可见:全部的段寄存器都需要对应8086模式的描述符状态。才能正常的进入8086模式。

LABEL_DESC_CODE16    Descriptor   0,0ffffh ,DA_C           ;

;以下两个段描述符是内存中的段不需要自己定义
LABEL_DESC_TEST    Descriptor   0500000h,0ffffh,DA_DRW        ;用于测试线性空间
LABEL_DESC_VIDEO    Descriptor   0B8000h,0ffffh,DA_DRW       ;显示器内存

GdtLen     equ   $ - LABEL_GDT
GdtPtr     dw   GdtLen - 1     ;段界限与段基地址
       dd     0   
;----------------选择子---------------------
SelectorNormal   equ    LABEL_DESC_NORMAL - LABEL_GDT
SelectorData   equ    LABEL_DESC_DATA - LABEL_GDT
SelectorStack   equ    LABEL_DESC_STACK - LABEL_GDT
SelectorCode32   equ    LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16   equ    LABEL_DESC_CODE16 - LABEL_GDT
SelectorTest   equ    LABEL_DESC_TEST - LABEL_GDT
SelectorVideo   equ    LABEL_DESC_VIDEO - LABEL_GDT

;为了便于区分实模式代码与保护模式代码,

我就就把16位的实模式段先写前面:(一般的编程规范下 数据段是写前面的)

;=============================-8086的16位实模式起始段============================

[SECTION .s16]     ;16位代码段
[BITS 16]     ;BITS指出处理器的模式 是16位

LABEL_BEGIN:               ;从100h处跳进来的
     mov   ax,cs
     mov ds,ax
     mov es,ax
     mov ss,ax
     mov sp,100h   ;在实模式下并没有用到sp,这个100h 只是形象表示需要保存实模式的SP值.
     mov [LABEL_GO_BACK_TO_REAL+3], ax   ; 请看[LABEL_GO...]+3标号处的注释。是一个指令参数
     mov [SPValueInRealMode],sp            ;在这里保存实模式的sp值

     ;-----------全局数据段描述符初始化-----------
     xor eax,eax
     mov ax,ds
     shl eax,4                             ;ds * 16 代表这DS原来的基地址
     add eax,LABEL_DATA1       ;得到物理地址
     mov WORD   [LABEL_DESC_DATA + 2],ax
     shr eax,16
     mov BYTE [LABEL_DESC_DATA + 4],al
     mov BYTE [LABEL_DESC_DATA + 7],ah       ;此时数据段描述符已经有基址了也就是可以访问了
   
     ;-----------全局堆栈段描述符初始化-----------
     xor eax,eax
     mov ax,ds
     shl eax,4
     add eax,LABEL_STACK
     mov WORD   [LABEL_DESC_STACK + 2],ax
     shr eax,16
     mov BYTE [LABEL_DESC_STACK + 4],al
     mov BYTE [LABEL_DESC_STACK + 7],ah;此时堆栈段描述符已经有基址了也就是可以访问了
   
     ;-----------32位代码段描述符初始化-----------
     xor eax,eax
     mov ax,cs
     shl eax,4
     add eax,LABEL_SEG_CODE32
     mov WORD   [LABEL_DESC_CODE32 + 2],ax
     shr eax,16
     mov BYTE [LABEL_DESC_CODE32 + 4],al
     mov BYTE [LABEL_DESC_CODE32 + 7],ah     ;进入保护模式后开始执行的代码段
     ;-----------16位代码段描述符初始化-----------
     xor eax,eax
     mov ax,cs
     shl eax,4
     add eax,LABEL_SEG_CODE16
     mov WORD   [LABEL_DESC_CODE16 + 2],ax
     shr eax,16
     mov BYTE [LABEL_DESC_CODE16 + 4],al
     mov BYTE [LABEL_DESC_CODE16 + 7],ah ;用来跳转到实模式的386代码段
   
     ;----------GDTR Ready -----------
     xor eax,eax
     mov ax,ds
     shl eax,4
     add eax,LABEL_GDT
     mov [GdtPtr + 2],eax
     lgdt [GdtPtr]   ;loader gdtr
      ;----------打开A20--------
      CA20
       cli
      ;--------置CR0 PE位-----
      mov eax,cr0
      or   eax,1
      mov cr0,eax
      ;---------------跳到386-----------
     jmp dword SelectorCode32:0             ;以上3个步骤无需多讲了

;这个是迎接保护模式跳回来的时候,执行的代码,,欢迎386回来啊!!!
LABEL_REAL_ENTRY:
     mov ax,cs
     mov ds,ax
     mov es,ax
     mov ss,ax
   
     mov sp,[SPValueInRealMode]    ;恢复到Real原来的堆栈 注意这里要用[标号]
   
     DA20     ;关闭20地址线
     sti
     mov ax,4c00h
     int 21h

;===================================386保护模式段==================================

;-------------------数据段--------------------------

[SECTION .data1]   
align 32
[BITS 32]
LABEL_DATA1:
   SPValueInRealMode     dw     0               ; 这个变量用来保存实模式跳入到保护模式前的SP值

   ;---------字符串-------------
   PMMessage:   db "welcome to Protect Mode ", 0 ; 进入保护模式后显示此字符串
   OffsetPMMessage   equ PMMessage - $$             ;保护模式寻址方式是按段偏移         

   StrTest:   db "This is 5MB", 0
   OffsetStrTest    equ StrTest - $$                        ;测试5MB空间所用的字符串
SegDataLen   equ   $ - LABEL_DATA1    ;数据段长

;-----------------------------------386全局堆栈段------------------------------------
[SECTION .stack32]
align 32
[BITS 32]
LABEL_STACK:
times 512 db 0                                           ;堆栈大小是512byte

TopOfStack equ $ - LABEL_STACK - 1                     ;栈顶的值

;-----------------------------------进入保护模式后的起始代码段------------------------------------
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]

LABEL_SEG_CODE32:
     mov ax,SelectorData
     mov ds,ax
     mov ax,SelectorTest
     mov es,ax
     mov ax,SelectorVideo
     mov gs,ax                                      ;以上3个也不用多说了吧,段的选择子也就是GDT的索引
     ;---堆栈--
     mov ax,SelectorStack
     mov ss,ax
     mov esp,TopOfStack                   ;当然堆栈段也是段的选择子咯

     ;-----------显示缓冲--------
     mov ah, 0Ch    ; 0000: 黑底    1100: 红字
     mov esi,OffsetPMMessage                           ;这个是字符串相对于它的段偏移值
     mov edi,(80*10+0)*2     ;第10行
     cld
   .1:
   
     lodsb
     test al,al
     jz   .2   
     mov [gs:edi],ax       ;要用ax做为参数传进缓冲区
     add edi,2
     jmp .1
   .2:                                      ;OffsetPMMessage   字符串显示完成:
     ;------------------------测试5MB空间的读------------------
     call DispReturn    ;显示回车,也就是改变edi的位置(edi 是显缓冲区段的偏移值)

    call ReadTest
     call WriteTest
     call ReadTest     ;当在执行Call命令的时候     会将eip + 1压栈,然后跳转
     
     jmp SelectorCode16:0      ;跳到最后那个16位代码段去了 前面有16位段描述符的讲解。。

;--------------显示回车---------------
DispReturn:
     push ebx                         ;临时用ebx eax 所以先保存一下
     push eax
     mov bl,160                     
     mov eax,edi
     div bl
     inc eax   ;得到回车后的行数
     mov bl,160
     mul bl
     mov edi,eax ;取得当前的位置
   
     pop eax
     pop ebx
     ret      ;ret 指令会恢复eip + 1

;--------------读取我们定义的大地址段---------------
ReadTest:
   
             xor        esi,esi
                mov        ecx,11
                mov        ah, 0Ch                        ; 0000: 黑底    1100: 红字
                .loop:
                mov        al,[es:esi]          ;这个是SelectorTest的段,也就是我们测试的大地址段
                call        DispAL                ;显示从test段取出来的字符
                inc        esi
                loop        .loop               ;cx--
                                call    DispReturn
                                ret
;--------------写入我们定义的大地址段---------------

WriteTest:
       push esi                         ;借这两个寄存器来传字符串
       push edi
      xor esi, esi
       xor edi, edi
        mov esi, OffsetStrTest ; 数据段的字符串

         cld
.1:
       lodsb
       test al, al
        jz .2
       mov [es:edi], al    ;把数据段的字符串传给测试段
        inc edi
      jmp .1
.2:

   pop edi
    pop esi

ret

;--------------显示AL的内容---------------
DispAL:                         ;对传来的Al字符进行处理
    test al,al
    jnz   .next
    mov al,'0'
   .next:
    mov ah, 0Ch    ; 0000: 黑底    1100: 红字
    mov [gs:edi],ax
     add edi,2
    ret
SegCode32Len   equ   $ - LABEL_SEG_CODE32

;-----------------------------------准备8086模式的16位段------------------------------------
[SECTION .code16]
align 32
[BITS 16]
LABEL_SEG_CODE16:
   
     mov ax,SelectorNormal        ;     保护8086标准段属性的描述符选择子

    mov ds, ax
     mov es, ax
     mov fs, ax
     mov gs, ax
     mov ss, ax       ;使所有的段选择子都达到8086标准   

     mov eax,cr0
     and al,0feh
     mov cr0,eax    ;CR0 PE位为0 回到Real mode
LABEL_GO_BACK_TO_REAL:   
     jmp 0:LABEL_REAL_ENTRY            ;还记得上面的[LABEL_GO..]+3吗,指的就是0这个段值
   
Code16Len   equ   $ - LABEL_SEG_CODE16

呵呵、那么就完工了代码虽然比较繁杂,但是还不是特别复杂:

有几个要注意的地方:

1、堆栈(新加了堆栈这个段,其寻址方式也是选择子。)

在入口的时候是实模式,应该把当前的sp值保存下来,因为在返回系统时我们需要基本恢复原来的样子。

就在保护模式的全局数据段定义一个变量用来保存SP。在返回DOS时候要记得恢复。

2、保护模式到实模式

实模式到保护模式 比较简单就几个固定步骤不需要考虑太多,而从保护模式到实模式就比较复杂:

首先需要定义两个8086标准属性的段描述符.它们分别是Normal数据段与16位代码段描述符。

在16位代码段中 cs的属性跟8086模式的CS段界限与段属性一致,并且把Normal的段属性分别付给全部的数据段寄存器.这样就让所有保护模式的数据段寄存器全部跟8086段界限与段属性一致!!!

在cr0 的PE位为0后 就可以转换成实模式寻址了!!!

接下来继续学习:

补充一下.com要在纯DOS下运行,,,因为用到了很对特权指令!!!

Day Day Up

[看雪官方]《安卓高级研修班》线下班,网课(12月)班开始同步招生!!

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (1)
雪    币: 441
活跃值: 活跃值 (35)
能力值: ( LV12,RANK:600 )
在线值:
发帖
回帖
粉丝
Sysnap 活跃值 14 2008-4-8 07:58
2
0
一上来就见到长长的汇编啊.......................顶个....呵呵
游客
登录 | 注册 方可回帖
返回