其实上面的那个实验即使在实模式下也是可以实现的,并没有体现出保护模式的优势。我们知道实模式下寻址能力只有1M(因为能用的地址线只有20条),而保护模式下可以达到4G。而且前面的实验只是让程序进入了保护模式,我们可以在程序的末尾又再次跳转回实模式。
在进行这些美妙的跨越之前我们不得不再一次更新自己的调试环境。你可能想问为什么要更新环境,oh,Sweety,前面的方法都是把我们的程序写到引导扇区去执行的,但是我们知道那个“倒霉鬼”只有512个字节的容量,我们的程序一旦超过了这个限度它就无力再支持我们的程序正确运行了!所以我们得借助一些外力来达到目的,比如DOS。我们先把程序编译成COM文件,然后让DOS来替我们执行它。
那就开始动手吧。
1.下一个FreeDos,我已经为大家准备好了:http://pan.baidu.com/s/1jHVIYou 下载后将其解压,其中有一个a.img文件,将该文件复制到Bochs的安装目录下,并改名为freedos.img。
2.用bximage工具生成一个软盘映像(不会请参考第一章的内容),取名为pm.img。
3.修改Bochs的配置文件,原先为:floppya: 1_44=a.img,status=inserted;现在将其替换为如下三句:
floppya: 1_44=freedos.img, status=inserted
floppyb: 1_44=pm.img,status=inserted
boot:a #该配置用于启动FreeDos
4.启动Bochs,等待FreeDos启动完毕后格式化B:盘(有floppyb指定的pm.img),如下图示(使用指令:format b:):
有时候由于种种原因,你第一次格式化失败了,那么以后成功的时候画面是下面这样的:
不管是哪种,反正都说明格式化成功了。
5.将前面的代码org处的07C00h改为0100h,并重新编译。注意,这次是编译为COM文件,使用命令nasm pmtest1.asm -o pmtest1.com 。
6.在Linux环境下(没有的话只有装个虚拟机了,在下也是这样解决的)将pmtest1.com复制到虚拟软盘pm.img上。最后把pm.img考回Bochs安装目录。
7. 启动Bochs,进入到FreeDos界面后运行如下命令,效果如图:
看见那个运行后的红色“P”就说明我们的环境升级成功了。
好了,那我们就运行如下代码来体验一下访问大于1M地址空间的感受吧(说不定那边的空气更清新),并且实现由实模式跳入保护模式再由保护模式跳回(我又进去了,我又出来了,你来打我啊)。
整片的代码就不贴了,可以在网上下到整本书中相关的完整代码(下面对应的代码是chapter3/b/pmtest2.asm中的内容),后面就主要的部分大家鉴赏一下。基本的解释都已经有了,有个别指令不认识的可以在网上查一下,个人觉得看懂还是不难的。
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 ;每个字节都设置了属性ah
add edi, 2
jmp .1
.2: ; 显示完毕
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 结束---------------------------------------------------------
上面这个代码段实现了所有和读写以及显示相关的操作。需要注意的是上面代码中的寻址方式,基本都是以偏移的形式来给出的。例如其中的offsetStrTest和offsetPMMessage,我们在保护模式下需要用到的正是这个偏移量,而不再是实模式下的地址。代码中的section的妙用也可在此体现出来,配合$$标识符可以使我们的代码思路更加清晰,一目了然。关于这两个模式下的寻址我会在后面的章节详细描述,这里暂时就不解释了,因为篇幅可能会比较大。
[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
;置cr0的PE位
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]
以上代码主要实现从保护模式返回实模式的功能。从实模式进入保护模式时可以直接跳转来达到目标,但是返回的时候回稍微复杂一些。因为在准备结束保护模式回到实模式之前,需要加载一个合适的描述符选择子到有关段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性。而且,我们不能从32位代码段返回实模式,只能从16位代码段返回。这是因为无法实现从32位代码段返回时CS高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。
着重看一下返回实模式的跳转:
jmp 0:LABEL_REAL_ENTRY
看起来好像它的段地址为0,其实前面有这样的操作:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
实模式下长跳转指令的示意图如下:
即刚好将进入保护模式前的实模式下的CS设置为了该跳转指令的段地址。
最后,跳回实模式下,程序重新设置各个段寄存器的值,恢复sp,然后关闭A20地址线,开中断,恢复原来的样子。具体代码如下:
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode];恢复sp,进入保护模式前改制已被保存
in al, 92h ; \
and al, 11111101b ; | 关闭 A20 地址线
out 92h, al ; /
sti ; 开中断
mov ax, 4c00h ; \
int 21h ; / 回到 DOS
程序大致就是这个样子了,可以使用我们新搭建的环境运行一下,其运行结果如下图所示(可以将pm.img再次格式化后再将pmtest2.com拷贝到其中去):