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) 执行目标程序,将看到输出结果
4) 使用adu.exe验证一下输出是正确的
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完全没必要用,这个纯粹就是跳转,根本不需要刷新高速缓存寄存器,这里也没有进行模式切换,我不知道作者放这是为了什么,感觉容易误导到读者。