摘要:本节,通过代码解析,帮你解决如下问题:保护模式和实模式下面的偏移量有什么不同?保护膜是和实模式下段基地址是一个意思吗?长跳转指令jmp 0:entry为什么能够在一个代码段中更改另一个代码段的代码?
一、总体思路剖析:
pmtest2,其实实现的是从实模式到保护模式,然后从保护模式回到实模式,最后回归到dos。其中,一开始就进入了实模式,然后在实模式下初始化段描述符,处理GDT等,进入保护模式,在保护模式下完成了一些显示字符串和拷贝读取字符串的功能;最后通过一个normal段,回到实模式,然后通过中断,回到dos程序中。这个过程具体代码如下:
; ==========================================
; pmtest2.asm
; 编译方法:nasm pmtest2.asm -o pmtest2.com
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen - 1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位
LABEL_DESC_TEST: Descriptor 0500000h, 0ffffh, DA_DRW
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorTest equ LABEL_DESC_TEST - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .data1] ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ^-^", 0 ; 进入保护模式后显示此字符串
OffsetPMMessage equ PMMessage - $$
StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest equ StrTest - $$
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
; END of [SECTION .gs]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
; 初始化 16 位代码段描述符
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
in al, 92h ; ┓
and al, 11111101b ; ┣ 关闭 A20 地址线
out 92h, al ; ┛
sti ; 开中断
mov ax, 4c00h ; ┓
int 21h ; ┛回到 DOS
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorTest
mov es, ax ; 测试段选择子
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
; 下面显示一个字符串
mov ah, 0Ch ; 0000: 黑底 1100: 红字
xor esi, esi
xor edi, edi
mov esi, OffsetPMMessage ; 源数据偏移
mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕第 10 行, 第 0 列。
cld
.1:
lodsb
test al, al
jz .2
mov [gs:edi], ax
add edi, 2
jmp .1
.2: ; 显示完毕
xchg bx,bx
call DispReturn
call TestRead
call TestWrite
call TestRead
; 到此停止
jmp SelectorCode16:0
; ------------------------------------------------------------------------
TestRead:
xor esi, esi
mov ecx, 8
.loop:
mov al, [es:esi]
call DispAL
inc esi
loop .loop
call DispReturn
ret
; TestRead 结束-----------------------------------------------------------
; ------------------------------------------------------------------------
TestWrite:
push esi
push edi
xor esi, esi
xor edi, edi
mov esi, OffsetStrTest ; 源数据偏移
cld
.1:
lodsb
test al, al
jz .2
mov [es:edi], al
inc edi
jmp .1
.2:
pop edi
pop esi
ret
; TestWrite 结束----------------------------------------------------------
; ------------------------------------------------------------------------
; 显示 AL 中的数字
; 默认地:
; 数字已经存在 AL 中
; edi 始终指向要显示的下一个字符的位置
; 被改变的寄存器:
; ax, edi
; ------------------------------------------------------------------------
DispAL:
push ecx
push edx
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov dl, al
shr al, 4
mov ecx, 2
.begin:
and al, 01111b
cmp al, 9
ja .1
add al, '0'
jmp .2
.1:
sub al, 0Ah
add al, 'A'
.2:
mov [gs:edi], ax
add edi, 2
mov al, dl
loop .begin
add edi, 2
pop edx
pop ecx
ret
; DispAL 结束-------------------------------------------------------------
; ------------------------------------------------------------------------
DispReturn:
push eax
push ebx
mov eax, edi
mov bl, 160
div bl
and eax, 0FFh
inc eax
mov bl, 160
mul bl
mov edi, eax
pop ebx
pop eax
ret
; DispReturn 结束---------------------------------------------------------
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and al, 11111110b
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
二、代码解惑
1.align 32是什么意思?为什么要写align 32?
答:align是一个让数据对齐的宏。通常align的对象是1、4、8等。这里的align 32是没有意义的,本来就是只有32b的地址总线宽度,怎么还32对齐?不可能。
2.既然我们说PMMessage是表示段内offset,那么我们为什么还要定义一个变量OffsetPMMessage呢?
$、$$、offsetpmmessage、pmmessage的地址分别是什么?
$:当前行被汇编之后的地址,是实际的线性地址
$$:一个section的开始地方被汇编以后的地址,也是实际的线性地址
pmmessage:偏移地址(相对段的首地址)
既然offset是偏移地址,为什么不直接用offset呢?
要解答这些疑惑,我们首先去寻找段基址:你有没有注意到一个问题,在“初始化段描述符”的部分,我们用来初始化段基地址首地址的寄存器,都是cs和ds(实际上二者的数值相等);从某种角度上说,他们处在“同一个段”中。不过分属于不同的offset对应的部分,我们通过段描述符,实现了不同区域的不同权限管理。因为现在程序很小,我们可以在20b的offset之内包括所有的代码和数据,所以这样做是没有任何问题的。
我们再看看保护模式下发生的变化:本来在实模式下,它们属于同一个段,但是最后段基地显然在初始化段描述符的时候悄然发生了变化(都采用了base*16+offset,而offset是不同的)。好了,再来看看“pmmessage-$$”的真实含义,pmmessage和$$都表示实模式下相对与段基地址的offset;但后来随着段基地址的漂移,$$变成了首地址,所以pmmessage对应于保护模式下的偏移自然也就发生了变化,需要减去$$对应的地址才行。
大部分时候,我们在编程的时候,只需要关注offset就可以了。
3.section和段之间有什么区别和联系?程序代码段执行的第一句,mov ax,cs对应的offset会是0吗? section和段之间没有必然的联系,一般我们习惯将一个section放在一个段里面,不过这是用户习惯,不是语法要求——我们可以把两个section放在段里面。mov ax,cs在这里,实模式下offset一般不等于0,保护模式下,这一句的偏移一般是0.
4.这里,我们需要根据打印字符串部分总结一下字符串操作的原理、技巧?汇编中判断和循环语句都有哪些?
参见汇编语言语法简要总结。
5.换行的原理是什么?
实际上就是靠操作edi来实现,edi=[edi/160]+1
6.保护模式下长跳转指令的原理?jmp 0:xx是如何跳转的。
从指令结构上来讲,书上的解释已经很明确了——但是jmp 0:xx中的0是如何被改变的呢?因为jmp指令的机器码在编译的时候已经产生了,但是运行之后,它的机器码被改掉了。这样看起来解释得通,但是再仔细想想,是不是有什么地方不对劲?——代码段的内容怎么会可以更改呢?仔细看看代码,又发现了猫腻——原来对代码的改动发生在实模式,这时分段机制还没对代码起到保护作用。
7.PMMessage后面定义的dd,但是内容不止4b,该如何处理?
dd还是db,表示的是后面的一个单位,而不是所有的内容。
例如dd 3,2,4,6,5:这样定义的就是20个字节。
8.movzx指令
mov eax,bx是非法的,所以要显示的高位补0
9.带有.的loop和一般的label有什么不同
这是本地label的意思:
NASM对于那些以一个句点开始的符号会作特殊处理,一个以单个句点开始的
Label会被处理成本地label, 这意味着它会跟前面一个非本地label相关联.
比如:
label1 ; some code
.loop
; some more code
jne .loop
ret
label2 ; some code
.loop
; some more code
jne .loop
ret
上面的代码片断中,每一个'JNE'指令跳至离它较近的前面的一行上,因为'.loop'
的两个定义通过与它们前面的非本地Label相关联而被分离开来了。
10.32b数据段和16b数据段有什么区别?32b的堆栈段呢
对于stack,位数不同将导致压栈和出战的位数不同;对于数据段,没有什么区别;对于代码段,将决定是ecs还是cs等信息;所以,在32b的物理机器上,你可以都定义成16b的段;但是你不能在16b的机器上,定义32b的段
11.保护模式和实模式段地址有什么区别?
保护模式下32b,不用进行偏移就直接和offset相加;实模式下16b,需要左移四位然后在+offset。
三、调试情况:
打印read和write位置和内容都出现异常,无法回到dos程序之下。
解决方法:调整test段的地址,使得它的地址范围降低,然后像其他段描述符一样进行初始化。
遗留问题:没有解决test段在原文情况下不可写的情况,最后证明,这是一个随机会出现问题的地方。