jmp_to32 MACRO selector,offsetv
db 0EAH ;操作码
dw offsetv ;16位偏移
dw selector ;段值或者选择子
endm
jmp_to16 MACRO selector,offsetv
db 0EAH ;操作码
dw offsetv ;32位偏移(低16位)
dw 0 ;32位偏移(高16位)
dw selector ;选择子
endm
;存储段描述符结构类型的定义
Descriptor struc
limitl dw 0 ;段界限(0-15)
basel dw 0 ;段基地址(0-15)
basem db 0 ;段基地址(16-23)
attributes dw 0 ;段属性
baseh db 0 ;段基地址(24-31)
Descriptor ends
;伪描述符结构类型的定义
PDESC struc
limit dw 0 ;16界限
base dd 0 ;32位基地址
PDESC ends
;常量定义
ATDR =90h ;存在的只读数据段属性
ATDW =92h ;存在的可读写数据段属性
ATDWA =93h ;存在的已访问可读写数据段属性值
ATCE =98h ;存在的只执行代码段属性值
ATCE32 = 4098h ;存在的只执行32位代码段属性值
DATALEN =16
.386p
DSEG SEGMENT USE16 ;16位段
GDT label byte ;全局描述符表
DUMMY Descriptor<> ;空描述符
CODE32_SEL =08h ;32位代码段描述符选择子
CODE32 Descriptor <CODE32LEN-1,,,ATCE32,>
CODE16_SEL =10h ;16位代码段描述符选择子
CODE16 Descriptor <0ffffh,,,ATCE,>
DATAS_SEL=18h ;源数据段描述符选择子
DATAS Descriptor <DATALEN-1,0fff0h,0fh,ATDR,0>
DATAD_SEL=20h ;目标数据段描述符选择子
DATAD Descriptor <DATALEN*20-1,80a0h,0bh,ATDW,0> ;显存偏移0B80A0h(第二行开始),注意段界限
STACKS_SEL=28h ;堆栈段描述符选择子
STACKS Descriptor<0ffffh,,,ATDWA,>
NORMAL_SEL =30h ;规范段描述符选择子
NORMAL Descriptor <0ffffh,0,0,ATDW,0>
GDTLEN=$-GDT
VGDTR PDESC<GDTLEN-1,0> ;GDT伪描述符
VARSS dw ? ;用于保存实模式下SS的变量
DSEG ends
CSEG1 SEGMENT USE16 'REAL'
ASSUME CS:CSEG1,DS:DSEG
START:
;初始化GDT表
mov ax,DSEG
mov ds,ax
mov bx,16
mul bx ;段值*16
add ax,offset GDT ;GDT表偏移
adc dx,0
mov word ptr VGDTR.base,ax ;32位基地址偏移低16位
mov word ptr VGDTR.base+2,dx ;32位基地址偏移高16位
;16位界限在定义是已设置完毕
;初始化32位代码段描述符
mov ax,CSEG2
mul bx ;段值*16
mov CODE32.basel,ax
mov CODE32.basem,dl
mov CODE32.baseh,dh
;初始化32位代码段描述符
mov ax,CSEG3
mul bx
mov CODE16.basel,ax
mov CODE16.basem,dl
mov CODE16.baseh,dh
;初始化32位堆栈段描述符
mov ax,ss ;设置堆栈基地址
mul bx ;段值*16
mov STACKS.basel,ax
mov STACKS.basem,dl
mov STACKS.baseh,dh
mov VARSS,ss ;保存实模式下的SS
lgdt fword ptr VGDTR
cli ;关中断
call EA20 ;打开地址线A20
;准备切换到保护模式
mov eax,cr0
or eax,1
mov cr0,eax
;进入32位代码段
jmp_to32 <CODE32_SEL>,<offset SPM32>
TOREAL:
mov ax,DSEG
mov ds,ax
mov ss,VARSS ;恢复实模式下的SS
call DA20 ;关闭地址线A20
sti ;开中断
mov ax,4c00h
int 21h
;打开地址线A20
EA20 proc
push ax
in al,92h
or al,2
out 92h,al
pop ax
ret
EA20 endp
;关闭地址线A20
DA20 proc
push ax
in al,92h
and al,0fdh ;0fdh=not 20h
out 92h,al
pop ax
ret
DA20 endp
CSEG1 ends
CSEG2 SEGMENT USE32 'PM32'
ASSUME CS:CSEG2
SPM32:
mov ax,STACKS_SEL
mov ss,ax ;装载堆栈段寄存器SS
mov ax,DATAS_SEL
mov ds,ax ;装载源数据段寄存器DS
mov ax,DATAD_SEL
mov es,ax ;装载目标数据段寄存器ES
xor esi,esi ;设置指针和计数器
xor edi,edi
mov ecx,DATALEN
cld
NEXT:
lodsb ;取一字节
push ax
call TOASCII ;低4位转换成ASCII
mov ah,7 ;显示属性为黑底白字
shl eax,16
pop ax
shr al,4
call TOASCII ;高4位转换成ASCII
mov ah,7
stosd ;显示
mov al,' '
stosw ;显示空格
loop NEXT
jmp_to16 CODE16_SEL,<offset SPM16>
;子程序功能:
;把al低4位的十六进制数转换成对应的ASCII,保存在AL
TOASCII proc
and al,0fh
add al,'0'
cmp al,'9'
seta dl ;如果大于'9',则设置dl=1,否则dl=0
movzx dx,dl
imul dx,7
add al,dl ;如果dl=1则al+37,否则+30
ret
TOASCII endp
CODE32LEN =$-CSEG2
CSEG2 ends
CSEG3 SEGMENT USE16 'PM16'
ASSUME CS:CSEG3
SPM16:
xor si,si ;设置指针和计数器
call DispReturn
mov ah,7
mov cx,DATALEN
AGAIN:
lodsb ;把指定区域内容直接作为ASCII码显示
stosw
loop AGAIN
mov ax,NORMAL_SEL
mov ds,ax ;把NORMAL段选择子装入DS和ES
mov es,ax
mov eax,cr0 ;切换到实模式
and eax,0fffffffeh
mov cr0 ,eax
;回到实模式
jmp far ptr TOREAL
;换行函数
DispReturn proc
push ax
push bx
mov ax, di ;获取当前光标的显存偏移
mov bl, 160 ;每行80字符,每个字符占两个字节(字符ASCII码+字符属性),所以一行共80*2=160个字节
div bl ;获取当前行数(在显存中行数从0开始编号的)
and ax, 0FFh ;取低8位(在当前页,否则有可能输出到其它页了,显示器不会显示的)
inc ax ;下一行
mov bl, 160
mul bl ;确定下一行开始的字节数
mov di, ax ;更新edi寄存器
pop bx
pop ax
ret
DispReturn endp
; DispReturn 结束---------------------------------------------------------
CSEG3 ends
end START
从本实例的 GDT 中可见,两个数据段的界限都是根据实际大小而设置的。从源程序代码段 CSEG3 可见,在切换到实模式之前,把
一个指向似乎没有用的数据段的描述符 Normal 的选择子装载到 DS 和 ES。这是为什么呢?
在分段管理机制一文中已介绍过,每个段寄存器都配有段描述符高速缓冲寄存器,这些高速缓冲寄存器在实方式下仍发挥作用,只是内容上与保护模式下有所不同。如上表所示,其中“Y”表示“是”; “N”表示“否”;“B”表示字节;“U”表示向上扩展,“W”表示以字方式操作堆栈。段基地址仍是 32 位,其值是相应段寄存器值(段值)乘以 16,在把段值装载到段寄存器时刷新。由于其值是 16 位段值乘上 16,所以在实模式下基地址实际上有效位只有 20 位。每个段的 32 位段界限都固定为 0FFFFH,段属性的许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值,只能继续沿用保护方式下所设置的值。因此,在准备结束保护模式回到实模式之前,要通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。本实例 GDT 中的描述符 Normal就是这样一个描述符,在返回实模式之前把对应选择子 Normal_Sel 加载到 DS 和 ES 就是此目的。由于 SS 段描述符中的内容已符合实模式的需要,所以尽管也改变了 SS,但不需要重新加载 SS(本实例中重新加载了 SS,这除了稍增加运行时间外,并没有什么坏处)。16 位代码段描述符中的内容也符合实模式的需要,所以在通过 16 位代码段返回实模式时,CS 段描述符中的内容也符合实模式的要求。需要注意的是,不能从 32 位代码段返回实模式,这是因为无法实现从 32 位代码段返回时 CS 高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)
CODE32_SEL =08h ;32位代码段描述符选择子
CODE16_SEL =10h ;16位代码段描述符选择子
DATAS_SEL=18h ;源数据段描述符选择子
DATAD_SEL=20h ;目标数据段描述符选择子
DATAD Descriptor <DATALEN*20-1,80a0h,0bh,ATDW,0> ;显存偏移0B80A0h(第二行开始),注意段界限
STACKS_SEL=28h ;堆栈段描述符选择子
NORMAL_SEL =30h ;规范段描述符选择子
DSEG SEGMENT USE16 ;16位段
GDT label byte ;全局描述符表
DUMMY Descriptor<> ;空描述符
;CODE32_SEL =08h ;32位代码段描述符选择子
CODE32 Descriptor <CODE32LEN-1,,,ATCE32,>
CODE32_SEL =CODE32-GDT
;CODE16_SEL =$-offset CODE ;16位代码段描述符选择子
CODE16 Descriptor <0ffffh,,,ATCE,>
CODE16_SEL =CODE16-GDT
;DATAS_SEL=18h ;源数据段描述符选择子
DATAS Descriptor <DATALEN-1,0fff0h,0fh,ATDR,0>
DATAS_SEL=DATAS-GDT
;DATAD_SEL=20h ;目标数据段描述符选择子
DATAD Descriptor <DATALEN*20-1,80a0h,0bh,ATDW,0> ;显存偏移0B80A0h(第二行开始),注意段界限
DATAD_SEL=DATAD-GDT
;STACKS_SEL=28h ;堆栈段描述符选择子
STACKS Descriptor<0ffffh,,,ATDWA,>
STACKS_SEL= STACKS-GDT
;NORMAL_SEL =30h ;规范段描述符选择子
NORMAL Descriptor <0ffffh,0,0,ATDW,0>
NORMAL_SEL=NORMAL-GDT
GDTLEN=$-GDT
VGDTR PDESC<GDTLEN-1,0> ;GDT伪描述符
VARSS dw ? ;用于保存实模式下SS的变量
DSEG ends
我说一下我开始忽略的细节问题:
DATAD Descriptor <DATALEN*20-1,80a0h,0bh,ATDW,0> ;显存偏移0B80A0h(第二行开始),注意段界限
最开始的时候DATAD段界限不是DATALEN*20-1而是
DATALEN*8-1,代码中也没有换行函数,但是两次输出是一起的,为了好看,我增加了换行函数,但是这一增加就出问题了,在纯DOS 下直接崩溃了,被处理器重置了,反复来了几次,后来发现原来是DATAD段界限的问题,当在显存中输入数据时,因为我换行了,所以超过了段界限,导致了执行错误,后来改成了现在这个样子,才可以正确换行,当时换两次行却错误,还是超出了段界限(每一行有160个字节,每个字符占2个字节,故每行可以输出80个字符,16个字节+每个字节后一个空格=32个字符+直接输出16个字节,范围是在一行内的)。