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以上内存,还是需要开启该地址线(当然,不开启也能进入保护模式)。程序中的代码具有一定通用性。