《80X86汇编语言程序设计教程》十 实模式与保护模式的切换实例

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《80X86汇编语言程序设计教程》十 实模式与保护模式的切换实例相关的知识,希望对你有一定的参考价值。

1、  再次声明,需要纯DOS系统才能看到满意测试效果。内容是演示实模式与保护模式切换实例,实现功能是16进制显示从110000H开始的256个字节的值

 

 

2、  源代码如下:

 

  1 ;功能:演示实模式与保护模式的切换,16进制显示从110000H开始的256个字节的值
  2 ;16位偏移的段间直接转移指令的宏定义,这是一个JMP指令到所描述的地址
  3 
  4 JUMP    macro    selector,offsetv
  5     db        0eah                    ;操作码
  6     dw        offsetv                 ;16位偏移
  7     dw        selector                ;段值或者选择子
  8     endm
  9     
 10 ;字符显示宏指令的定义ECHOCH      
 11 ECHOCH    macro    ascii
 12     mov        ah,2
 13     mov        dl,ascii
 14     int        21h
 15 endm
 16 
 17 ;存储段描述符结构类型的定义(8字节)
 18 DESCRIPTOR    struct
 19     LimitL       dw    0                ;段界限(0-15)
 20     BaseL        dw    0                ;段基地址(0-15)
 21     BaseM        db    0                ;段基地址(16-23)
 22     Attributes   dw    0                ;段属性(其中包含高4位段界限)
 23     BaseH        db    0                ;段基地址(24-31)
 24 DESCRIPTOR ends
 25 
 26 ;GDT表的伪描述符结构类型定义PDESC
 27 PDESC    struc
 28     Limit    dw    0                    ;16 bit界限
 29     Base     dd    0                    ;32 bit基地址
 30 PDESC ends
 31 
 32 ;常量定义(属性段定义)
 33 ;92H=1001 0010->表示这是一个可读写的数据段
 34 ;98H=1001 1000->这是一个只执行的代码段
 35 
 36 ATDW    =    92h                        ;存在的可读写数据段属性值(参考8-15bit定义)
 37 ATCE    =    98h                        ;存在的只执行代码段属性值
 38 
 39     .386P
 40 ;--------------------------------------------------------------------------
 41 ;数据段    
 42 dseg    segment    use16                                    ;16位段
 43     ;GDT表
 44     GDT            label    byte                            ;全局描述符表GDT
 45     DUMMY          DESCRIPTOR<>                             ;空描述符
 46     CODE           DESCRIPTOR<0ffffh,,,ATCE,>                
 47     CODE_SEL       = CODE - GDT                             ;代码段描述符的选择子,此语句不生成代码,故不占用内存
 48     DATAS          DESCRIPTOR<0ffffh,0h,11h,ATDW,0>         ;这里描述的是一个base=110000h,limit=0FFFFh的段
 49     DATAS_SEL      = DATAS - GDT                            ;源数据段描述符的选择子
 50     DATAD          DESCRIPTOR<0ffffh,,,ATDW,>
 51     DATAD_SEL      = DATAD - GDT                            ;目标数据段描述符的选择子
 52     GDTLEN         = $ - GDT
 53     ;GDT表的伪描述符
 54     VGDTR          PDESC<GDTLEN-1,>                        
 55     ;缓冲区
 56     BUFFERLEN      = 256                                    ;缓冲区字节长度
 57     BUFFER         db    BUFFERLEN    dup(0)                ;缓存区
 58 dseg    ends
 59 
 60 ;---------------------------------------------------------------------------------
 61 ;代码段    
 62 cseg    segment    use16                                ;16位段
 63     assume    cs:cseg,ds:dseg
 64 start:
 65     mov        ax,dseg
 66     mov        ds,ax
 67     
 68     ;准备要加载到GDTR的伪描述符,把GDT的地址按照实模式方式装载
 69     mov        bx,16                                    ;计算并设置GDT地址
 70     mul        bx                                       ;DX:AX = AX * BX = dseg * 16 
 71     add        ax,offset GDT                            ;DX:AX = dseg * 16 + (offset GDT)
 72     adc        dx,0                                     ;此时DX:AX表示了GDT的实地址        
 73     mov        word ptr VGDTR.Base,ax
 74     mov        word ptr VGDTR.Base + 2,dx               
 75                     
 76     ;设置代码段描述符
 77     mov        ax,cs                                                                        
 78     mul        bx                                        ;DX:AX = cseg * 16 + 0
 79     mov        CODE.BaseL,ax                             ;代码段开始偏移为0
 80     mov        CODE.BaseM,dl                            
 81     mov        CODE.BaseH,dh
 82     
 83     ;设置目标数据段描述符,定义好目标数据段的地址
 84     mov        ax,ds
 85     mul        bx                                        ;DX:AX = AX * BX = dseg * 16 
 86     add        ax,offset BUFFER
 87     adc        dx,0                                      ;DX:AX = dseg * 16 + (offset BUFFER)
 88     mov        DATAD.BaseL,ax
 89     mov        DATAD.BaseM,dl
 90     mov        DATAD.BaseH,dh
 91     
 92     ;加载GDTR
 93     LGDT      fword ptr VGDTR
 94     
 95     ;关中断
 96     cli
 97     call      ENABLEA20                                 ;打开地址线A20
 98     
 99     ;切换到保护模式
100     mov        eax,cr0
101     or         eax,1                                      ;置CR0的PE位,设置为保护模式,不启用分页管理机制,线性地址即为物理地址
102     mov        cr0,eax
103     ;清指令预取队列,并真正进入保护模式
104     JUMP       <CODE_SEL>,<offset VIRTUAL>                ;<代码段描述符的选择子>,<16位偏移地址>
105                                                           ;这里不能用远跳指令,否则段选择子怎么装入?
106     
107     ;现在开始在保护模式下
108 VIRTUAL:
109     mov        ax,DATAS_SEL
110     mov        ds,ax                                      ;加载源数据段描述符选择子 
111     mov        ax,DATAD_SEL
112     mov        es,ax                                      ;加载目标数据段描述符选择子
113     cld
114     xor        si,si                                      ;设置指针初值
115     xor        di,di
116     mov        cx,BUFFERLEN/4                             ;设置4字节为单位的缓冲区长度
117     rep        movsd                            
118     
119     ;切回实模式 
120     mov        eax,cr0
121     and        eax,0fffffffeh                             ;清CR0的PE位,设置为实模式
122     mov        cr0,eax
123     ;清指令预取队列,进入实模式
124     JUMP       <seg REAL>,<offset REAL>                   ;<16位段值>,<16位偏移地址>
125     ;jmp       far ptr REAL                               ;这里完全可以用远跳指令取代
126     
127     ;现在回到实模式
128 REAL:
129     call       DISABLEA20                                 ;关闭地址线A20
130     sti                                                   ;开中断
131     ;重置数据段寄存器
132     mov        ax,dseg                        
133     mov        ds,ax
134     mov        si,offset BUFFER
135     cld
136     ;显示缓冲区内容
137     mov        bp,BUFFERLEN/16
138 NEXTLINE:
139     mov        cx,16
140 NEXTCH:
141     lodsb                                                ;读入ES:DI的内容到AL中
142     push       ax                                        ;实际上是为了压入AL
143     shr        al,4                                      ;取高半字节
144     call       TOASCII
145     ECHOCH     al
146     pop        ax
147     call       TOASCII
148     ECHOCH     al
149     ECHOCH      
150     loop       NEXTCH
151     ECHOCH     0dH
152     ECHOCH     0ah
153     dec        bp
154     jnz        NEXTLINE
155     ;结束程序
156     mov        ax,0700h
157     int        21h
158     mov        ax,4c00h
159     int        21h
160 
161 ;把AL低4位的十六进制数转换成对应的ASCII码,保存在AL中       
162 TOASCII    proc
163     and        al,0fh
164     add        al,90h
165     daa
166     adc        al,40h
167     daa    
168     ret
169 TOASCII endp
170 
171 ;打开地址线A20号
172 ENABLEA20    proc
173     push      ax
174     in        al,92h
175     or        al,2
176     out       92h,al
177     pop       ax
178     ret
179 ENABLEA20 endp
180 
181 ;关闭地址线A20号    
182 DISABLEA20    proc
183     push      ax
184     in        al,92h
185     and       al,0fdh
186     out       92h,al
187     pop       ax
188     ret
189 DISABLEA20 endp    
190 
191 cseg    ends
192     end        start

 

  这个代码有以下几个地方需要说明:

    1)  加载GDTR时,原书语句错误。把qword替换为fword。类型:6(FWORD),  8(QWORD), 10(TBYTE(有人说是因为原作者使用MASM编译器的版本过低)。

    2)   数据复制语句是“rep  movsd”而不是“repz  movsd”,这个绝对是语法错误,mov系列的指令是不影响标志位的,这里repz对ZF的判断毫无意义。

 

 

3、  下面是测试过程与效果对比:

  1)  启动DiskGenius,将编译后的exe文件(我的是DosTest.exe)和adu.exe复制到DOS系统内。

技术分享图片

  2)  启动虚拟机的DOS系统,运行DosTest.exe,将看到输出的236个字节内容:

 技术分享图片

  3)  启动adu.exe,选择flat模式的内存查看,选择地址为110000H,可查看内存:

技术分享图片 

  4)  比对发现,我们编写的程序测试输出是没有任何问题的。这里需要说明一个地方:网络上我发现有人说载入GDT以后系统就会重启,我测试了下这种情况,根据提示,发现是EMM386的一个报错。其实原书作者一开始就强调了,为了保证不发生冲突,不要安装任何高端内存管理与驱动程序。有两个解决方案:第一个是按照上节那样,重新安装DOS操作系统,保证不加载EMM386高端内存管理软件;第二个是“edit config.sys”将“DEVICE = C:\\DOS71\\EMM386.EXE NOEMS”前添加“REM”,即注释掉这一行,不让EMM386加载,然后重启虚拟DOS系统即可。

 

 

4、  然后,运用前面的理论知识,重点解读一下这段程序中的步骤:

  1)  切换到保护模式前的准备

    a)建立VGDTR、GDT。须注意,转入保护模式后,GDT被当成独立段对待,实际上它和数据段出现了重叠。

    b)使VGDTR指向GDT

    c)填写GDT表,需注意,第一个描述符必须为空描述符,这点之前已经提过。经测试,编程时选择子按字节偏移来计算,在运行装载时,会自动转换为索引载入段寄存器

    d)使用LGDT指令把表VGDTR装载到寄存器GDTR

  2)  由实模式切换到保护模式

    a)置CR0的PE位(位0)

    b)通过段间转移指令(JUMP宏)真正进入实模式。这条指令在CR0置位完成之后被预取(IP指向了它),但是在保护模式下执行(CR0已经被置位),实际上,只有执行了它才算是真正进入了保护模式的代码。它的作用是把保护模式下的选择子装入CS,同时刷新指令预取队列。

      从“《80X86汇编语言程序设计教程》九 分段管理机制及纯DOS环境搭建”的段描述符高速缓冲寄存器一节中,已经提到过,在CS被重置时将刷新段描述符高速缓冲寄存器,之后对段描述符的访问全部转为对段描述符高速缓冲寄存器的访问。从这里可以看到,IP在CR0置位以后一直没有跳转(但此时CS的意义已经不是段值,而是段选择子,只是它还没有被修改;类似的IP变为EIP,模式已经切换为保护模式,只是代码执行指针没有切换过来),程序顺序继续向下执行,当执行取JUMP指令时,EIP被置为指向后一条指令,在JUMP执行时,CS段选择子被重置,引起CS的段描述符高速缓冲区执行刷新,EIP被重置,指向了新的段内偏移,这个过程就是原作者所说的所谓“把保护模式下的选择子装入CS,同时刷新指令预取队列”

      下面看下JUMP这条指令怎么做到的:宏展开以后的格式是“0eah CODE_SEL offset VIRTUAL”,其中0eah是操作码,这个是一个机器码形式的操作码,实际上是一个特殊一点的远跳转jmp far ptr。CODE_SEL是代码段段选择子,可见,在CS段选择子被重置时被设置的肯定就是它了,即CS = CODE_SEL,第2个参数是“offset VIRTUAL”,这个实际上是下一条指令的偏移地址,那么,所谓的EIP被重置重置的就是它了,即EIP = offset VIRTUAL。因此,这只不过是一条特殊一点的跳转指令,附带的cs和ip或者eip变换引起的一系列保护模式下CPU自动的一些行为,除此之外没有任何神秘的地方。从这里又可以看到,实质上,执行这条指令以后IP中的内容并没有变,它还是与模式切换之前一样的值,只不过可能EIP高16位被清零了,这样做的目的是让程序继续顺序往下执行

  3)  传送数据

    与上面说的类似,在重置段选择子DS和ES寄存器时,段基址等相关信息被装载进段描述符高速缓冲寄存器。装载以后用数据传送指令进行数据传送。

  4)  由保护模式切换到实模式

    这个过程和“由保护模式切换到实模式”是类似的,只不过是逆向而已。

  5)  显示缓冲区内容

    感觉没什么好讲的

 

 

5、  需要额外说明的地方:

  1)  由实模式切换到保护模式的准备工作时,没有建立IDTR,所以不允许保护模式下有中断存在,这里采用了简单的关中断处理,这就是开关中断的原因

  2) 没有定义保护模式下的堆栈段描述符,所以保护模式下的指令不能有与堆栈相关的PUSH、POP等指令。

  3)  特权等级一律为R0。

  4)  没有采用分页机制,线性地址就是物理地址

  打开和关闭地址线A20:实模式下只使用1MB存储空间,在切换进保护模式,虽然寻址空间可达4GB,但是要使用1MB以上内存,还是需要开启该地址线(当然,不开启也能进入保护模式)。程序中的代码具有一定通用性。

 

以上是关于《80X86汇编语言程序设计教程》十 实模式与保护模式的切换实例的主要内容,如果未能解决你的问题,请参考以下文章

《80X86汇编语言程序设计教程》二十三 分页管理机制实例

《80X86汇编语言程序设计教程》二十四 进入与离开V86模式实例

《80X86汇编语言程序设计教程》二十五 结语(读后感:这本书怎么样)

80x86的保护模式

80X86保护模式及其编程

操作系统学习80x86保护模式内存管理