《80X86汇编语言程序设计教程》十一 32位代码段和16位代码段切换实例

时间:2022-01-03 12:11:40

1、  演示32位代码段与16位代码段之间的切换。实现的功能是以十六进制和ASCII码字符两种形式显示从内存地址100000H开始的16个字节的内容。

 

 

2、  源代码如下:

 

  1 ;DosTest.Asm
2 ;16位偏移的段间转移指令的宏定义
3 ;使用于16位段,用于跳转到32位目的段
4 ;注意:标号偏移必须在16位二进制符号数数能表示的范围之内
5 JUMP16 macro selector,offsetv
6 db 0eah ;操作码
7 dw offsetv ;16位偏移
8 dw selector ;段值或者选择子
9 endm
10
11 ;32位偏移的段间转移指令的宏定义
12 ;使用于32位段,用于跳转到16位目的段
13 JUMP32 macro selector,offsetv
14 db 0eah ;操作码
15 dw offsetv ;32位偏移
16 dw 0
17 dw selector ;选择子
18 endm
19
20 ;存储段描述符结构类型的定义
21 DESCRIPTOR struc
22 LimitL dw 0 ;段界限(0~15)
23 BaseL dw 0 ;段基地址(0~15)
24 BaseM db 0 ;段基地址(16~23)
25 Attributes dw 0 ;段属性
26 BaseH db 0 ;段基地址(24~31)
27 DESCRIPTOR ends
28
29
30 ;伪描述符结构类型的定义
31 PDESC struct
32 Limit dw 0 ;16位界限
33 Base dd 0 ;基地址
34 PDESC ends
35
36 ;7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
37 ;G D 0 AVL Limit(19…16) P DPL DT TYPE
38 ;常量定义
39 ATDR = 0090h ;存在的只读数据段属性值(用于描述源数据段)
40 ATDW = 0092h ;存在的可读写数据段属性值(用于描述目的数据段)
41 ATDWA = 0093h ;存在的已访问可读写数据段属性值
42 ATCE = 0098h ;存在的只执行16位代码段属性值
43 ATCE32 = 4098h ;存在的只执行32位代码段属性值
44 DATALEN = 16 ;源数据段长度
45
46
47 ;须使用386特权指令
48 .386P
49
50 ;-----------------------------------
51 ;数据段
52 dseg segment use16 ;16位段
53 ;GDT表
54 GDT label byte
55 DUMMY DESCRIPTOR<> ;空描述符
56 CODE32_SEL = 08h ;32位代码段描述符选择子
57 CODE32 DESCRIPTOR<CODE32LEN-1,,,ATCE32,>
58 CODE16_SEL = 10h ;16位代码段描述符选择子
59 CODE16 DESCRIPTOR<0ffffh,,,ATCE,>
60 DATAS_SEL = 18h ;源数据段描述符选择子
61 DATAS DESCRIPTOR<DATALEN-1,,10h,ATDR,>;段基地址100000h
62 DATAD_SEL = 20h ;目的数据段描述符选择子
63 DATAD DESCRIPTOR<DATALEN*8-1,80a0h,0bh,ATDW,0>;段基地址0b80a0h
64 STACKS_SEL = 28h ;堆栈段描述符选择子
65 STACKS DESCRIPTOR<0ffffh,,,ATDWA,>;段基地址0000h,栈顶基址0ffffh
66 NORMAL_SEL = 30h ;规范段描述符选择子
67 NORMAL DESCRIPTOR<0ffffh,0,0,ATDW,>;段基地址0000h,栈顶基址0ffffh
68 GDTLEN = $ - GDT ;GDT表长度
69 ;
70 VGDTR PDESC<GDTLEN-1,> ;GDT伪描述符
71 VARSS dw ? ;用于保存SS变量
72 dseg ends
73
74 ;-----------------------------------
75 ;实模式下代码段
76 csegr segment use16 'real'
77 assume cs:csegr,ds:dseg
78 start: ;程序入口
79 mov ax,dseg
80 mov ds,ax
81 ;
82 mov bx,16 ;写VGDTR(GDT首地址转线性地址)
83 mul bx
84 add ax,offset GDT
85 adc dx,0
86 mov word ptr VGDTR.Base,ax
87 mov word ptr VGDTR.Base + 2,dx
88 ;
89 mov ax,cseg32 ;写32位代码段段基址
90 mul bx
91 mov CODE32.BaseL,ax
92 mov CODE32.BaseM,dl
93 mov CODE32.BaseH,dh
94 ;
95 mov ax,cseg16 ;写16位代码段段基址
96 mul bx
97 mov CODE16.BaseL,ax
98 mov CODE16.BaseM,dl
99 mov CODE16.BaseH,dh
100 ;
101 mov ax,ss ;写堆栈段段基址
102 mul bx
103 mov STACKS.BaseL,ax
104 mov STACKS.BaseM,dl
105 mov STACKS.BaseH,dh
106 mov VARSS,ss ;保存实模式下段基址
107 ;
108 lgdt fword ptr VGDTR ;装载VGDTR到GDTR
109 ;
110 cli ;关中断
111 call ENABLEA20 ;开地址线A20
112 ;
113 mov eax,cr0 ;CR0的PE位置1
114 or eax,1
115 mov cr0,eax
116 ;进入32位代码段
117 JUMP16 <CODE32_SEL>,<low offset SPM32>;切换到保护模式
118 ;此时已经回到实模式
119 TOREAL:
120 mov ax,dseg
121 mov ds,ax
122 mov ss,VARSS ;恢复实模式下的SS
123 call DISABLEA20 ;关闭地址线A20
124 sti ;开中断
125 mov ah,07h ;等待按键终止程序
126 int 21h
127 mov ah,4ch
128 int 21h
129
130
131 ;打开地址线A20号
132 ENABLEA20 proc
133 push ax
134 in al,92h
135 or al,2
136 out 92h,al
137 pop ax
138 ret
139 ENABLEA20 endp
140
141 ;关闭地址线A20号
142 DISABLEA20 proc
143 push ax
144 in al,92h
145 and al,0fdh
146 out 92h,al
147 pop ax
148 ret
149 DISABLEA20 endp
150
151 csegr ends
152
153 ;-----------------------------------
154 ;32位代码段
155 cseg32 segment use32 'pm32'
156 assume cs:cseg32
157 SPM32:
158 mov ax,STACKS_SEL
159 mov ss,ax ;装载堆栈段描述符选择子
160 mov ax,DATAS_SEL
161 mov ds,ax ;装载源数据段描述符选择子
162 mov ax,DATAD_SEL
163 mov es,ax ;装载目的数据段描述符选择子
164 ;以下开始以ASCII码形式显示源16个字节
165 ;目的数据段需要16 * (4 + 2) = 96个字节
166 xor esi,esi ;设置指针和计数器
167 xor edi,edi
168 mov ecx,DATALEN ;16个数据,循环16次
169 cld ;清方向标志位
170 NEXT:
171 lodsb ;从源数据段装载一个byte数据到al并移动指针
172 push   ax
173 call   TOASCII ;低4位转ASCII码(一个byte)
174 mov ah,7 ;显示属性为黑底白字(再一个byte)
175 shl eax,16 ;暂存在eax高16位
176 pop ax
177 shr al,4 ;高4位转ASCII码
178 call TOASCII
179 mov ah,7
180 stosd ;eax的4个byte(dword)存入目的数据段并移动指针
181 mov al,' ' ;显示空格,属性为黑底白字,2个字节包含字符ASCII码和字符属性
182 stosw ;ax的2个byte(word)入目的数据段并移动指针
183 loop NEXT
184 ;变化到16位代码段
185 JUMP32 <CODE16_SEL>,<offset SPM16>
186 ;jmp far ptr SPM16 ;这里取代完全没有问题
187
188 ;把AL低4位的十六进制数转换成对应的ASCII码,保存在AL中
189 TOASCII proc
190 and al,0fh
191 add al,90h
192 daa
193 adc al,40h
194 daa
195 ret
196 TOASCII endp
197
198 CODE32LEN = $ - SPM32
199 cseg32 ends
200
201 ;-----------------------------------
202 ;16位代码段
203
204 cseg16 segment use16 'pm16'
205 assume cs:cseg16
206 SPM16: ;跳转过来时ss、es的值都没有改变,实际上还是在保护模式
207 ;以下开始以十六进制数形式显示源16个字节
208 ;目的数据段需要16 * 2 = 32个字节
209 xor si,si ;源数据段指针归位
210 mov di,DATALEN * 3 * 2 ;这个语句是多余的,这里重新设置di没有意义
211 mov ah,7 ;显示属性为黑底白字
212 mov cx,DATALEN
213 AGAIN:
214 lodsb ;从源数据段装载一个byte数据到al并移动指针
215 stosw ;ax的2个byte(word)入目的数据段并移动指针
216 loop   AGAIN
217 ;
218 mov ax,NORMAL_SEL ;装载规范段描述符选择子到ds和es
219 mov ds,ax ;这将引起高速缓存寄存器的刷新
220 mov es,ax
221 ;
222 mov eax,cr0 ;切换到实模式下
223 and eax,0fffffffeh
224 mov cr0,eax
225 ;切换回实模式
226 jmp far ptr TOREAL
227 cseg16 ends
228 end start

 

 

3、  源代码有几处要说明的地方

  1)  原书中的“JUMP16 CODE32_SEL,<offset SPM32>”语句须改为“JUMP16     <CODE32_SEL>,<low offset SPM32> ”语句,原因是16位段不支持32位偏移(offset SPM32为32位立即数)

  2)  原书中的“CODE32LEN  = $”需要修改为“CODE32LEN     = $ - SPM32”,如果不是原书印刷等之类的错误,那么绝对是作者逻辑错误

 

 

4、  运行效果与相关说明

  1)  使用DiskGenuis复制.exe目标文件到DOS虚拟系统

  2)  打开虚拟机进入DOS7.1,使用“cls”指令清屏(否则将影响输出的视觉效果)

  3)  执行目标程序,将看到输出结果

《80X86汇编语言程序设计教程》十一 32位代码段和16位代码段切换实例 

  4)  使用adu.exe验证一下输出是正确的

《80X86汇编语言程序设计教程》十一 32位代码段和16位代码段切换实例 

 

 

5、  实现步骤的简单阐述

  1)  作切换到保护方式的准备

    2个16位数据段描述符、1个16位代码段描述符、1个16位堆栈段描述符、1个32位代码段描述符和1个规范段描述符。这里没必要再说了,只是注意下,32位代码段描述符在设置界限时采用的方法。另外,这个界限值不是长度,在以字节为粒度时是偏移量。

  2)  切换到保护方式一个32位代码段

    在上一个实例中已经提到过,这个JUMP宏实际上就是一条特殊的远跳转指令,这里要关注的一个东西是,跳转时的地址偏移问题。我简单说下:

    JUMP16用于16位段中,实现跳入32位段。由段间绝对跳转的性质可知,最大偏移是0FFFFH,也就是说,JUMP16实现的从16位段到32位段的跳转是有条件的:目标地址标号在32位段的段内偏移必须不大于0FFFFH

    JUMP32用于32位段中,实现跳入16位段。这个也只需要注意同一个地方,那就是16位段最大偏移为0FFFFH,所以跳转时必须保证高16位为0,作者在这里使用了宏定义把双字类型拆分成两个字类型的域,并把高字强行设置为0,对安全性有一定的提高

    从上面来看,跳转时不需要关注段寄存器的内容,此外,16位段与32位段之间的相互跳转与实模式还是保护模式没有半点关系,从这里也可以看到,实模式到保护模式的切换,在把VGDTR装载到GDTR寄存器以及将CR0的PE位置1后,就是一个简单的跳转指令,所以,也支持在模式切换的同时进行段类型的切换。

    如果能保证足够安全,可以完全不用作者的宏来实现这些跳转,就像在本例中最后由保护模式切换为实模式的“jmp far ptr TOREAL”那样,直接使用标号进行远跳,也是一样的,当然,这里要注意一些问题,这个问题也正体现之前一直说的这个远跳还是特殊的地方:这个宏中远跳指令可以重置自己设定的代码段值/代码段选择子和代码段内偏移分别到CS、IP/EIP。在保护模式下,装入段CS的是段选择子而不是段基地址,使用该指令来跳转是必须的,凡是装入的是段值而不是段选择子的情况,都可以使用远跳指令取代

  3)  把源数据转十六进制数码的ASCII码,并直接填入显存

    采用的是直接写屏的方式(参考“《80X86汇编语言程序设计教程》四 输入输出与中断”)。显存开始地址为0B8000H,这里的0b80a0h表示在3号显示方式下,屏幕第2行开头的位置。

  4)  切换到16位段代码

  5)  把源数据直接作为ASCII码填入显存

  6)  切回实模式

 

 

6、  特别说明

  1)  本例没有建立专用堆栈,但是在原堆栈上建立了堆栈段,所以保护模式下可进行堆栈操作

  2)  同上个实例一样,大量简化处理,没有IDT和LDT,DPL都设置为了0。

  3)  关于远跳的特殊之处的说明,各个段寄存器所配的高速缓冲寄存器在实模式下依然起作用。实模式下要求它的内容应该如下表:

 

 

 

段基地址

 

 

段界限

(固定)

其它段属性

G

R

W

E

CS

当前CS*16

0000FFFFH

Y

0

Y

B

U

Y

Y

Y

-

N

SS

当前SS*16

0000FFFFH

Y

0

Y

B

U

Y

Y

N

W

-

DS

当前DS*16

0000FFFFH

Y

0

Y

B

U

Y

Y

N

-

-

ES

当前ES*16

0000FFFFH

Y

0

Y

B

U

Y

Y

N

-

-

FS

当前FS*16

0000FFFFH

Y

0

Y

B

U

Y

Y

N

-

-

GS

当前GS*16

0000FFFFH

Y

0

Y

B

U

Y

Y

N

-

-

 

    其中:“Y”表示“是”,“N”表示“否”,“B”表示字节,“U”表示向高扩展段,“W”表示字操作堆栈。由于实模式下不可以设置高速缓存寄存器(也就是说,即使改变段寄存器中的段值,也不会引起高速缓存寄存器中内容的刷新),所以我们必须在保护模式下提前刷新它们到符合要求的值,再切换回保护模式。源代码中的“规范段描述符选择子”做的就是这样一个事情。我测试过,如果将它们去掉,程序在切换回实模式后将死机。此外,需要说明的是,源代码中的JMP32完全没必要用,这个纯粹就是跳转,根本不需要刷新高速缓存寄存器,这里也没有进行模式切换,我不知道作者放这是为了什么,感觉容易误导到读者