保护模式(Protect Mode) (1)
到底什么是保护模式?谁又在保护谁?是通过什么方式达到保护目的的? 为什么要保护呢?带着这些疑问和几分好奇,我们开始了第三章的学习,开始随着作者一步一步地体会“保护”二字的内在含义。那个连胚胎都还不是的“操作系统” 究竟能否正常发育,这一章的“保护”很关键!
3.1 认识保护模式
在好奇心的驱使下,人总是很勤劳的,作者如是说。
接着作者马上就给我们泼来了一头雾水:
;==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
;==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 07c00h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT 长度
GdtPtr dw GdtLen - 1 ; GDT 界限
dd 0 ; GDT 基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .s16]
[BITS 16]
L ABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 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
; 为加载 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 处
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子( 目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
---------------------------------------%include "pm.inc"-----------------------------------------------
;
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
如果你是第一次读到这里,不知道你对这段代码有怎么的感觉,可能对开头的耐心到第二页就消失的差不多了,然后很快的扫视,很短时间之后,你已经到达了代码末尾。
知我者作者也!我确确实实就是这样的感受。我只记住了一句话:这段代码实现了从实模式到保护模式的转换!
我们看到,整个程序有很多个类似[SECTION .xxx] 的东西,这个是用来把程序大卸八块的;我们还看到,[SECTION .gdt] 后面有3 个带 Descriptor 字样的行。
先来看看这个Descriptor 是什么东东,看字面意思,我们知道它好像描述了一些什么东西……
是的,它是一个宏,而且还带了3 个参数,不过它代表的不是一段可执行的代码,而是一个数据结构!我们暂且把它看成是结构体好了,汇编器处理的时候会直接把这个结构体替换进来。我们知道,一个dw 两个字节,一个db 一个字节,总共有3 个dw 和2 个db ,所以这个结构体总共有3*2+2*1 = 8 个字节,跟注释说的是一致的。
其中,%1 表示第一个参数,%2 代表第二个参数,%3 代表第三个参数。我们又看到,这些个参数被支离破碎地填充到了Descriptor 的8 个字节里面。
怎么搞这么麻烦啊! ?你问我,呵呵,我也不知道,反正这个叫GDT 的结构体有点变态 是不是,更可恶的是还不止一个。
相比GDT 这个变态的家伙,紧挨着它屁股后面的这个数据结构GdtPtr 就要单纯多了。
GdtPtr dw GdtLen - 1 ; GDT 界限
dd 0 ; GDT 基地址
还真是GDT 的跟屁虫!你看到了的:里面的东西全都是跟GDT 有关的。 头2 个字节描述了GDT 的身高--- 界限;后4 个字节想要透露出GDT 的藏身地--- 基地址。( 其实刚开始GdtPtr 自己也不知道,所以是0)
然后是两行Selector 打头的常量 (equ 定义出来是常量哦,像上面的dw 、dd 定义的才是变量) ,这里好像是选择了GDT 中的某个符号到某个符号差值,一下子搞不明白了,我选择暂时离开。
再往下走吧,在[SECTION .s16] 后面有个[BITS 16] ,下面应该就是一些16 位的代码了吧?这些代码做了什么事?
它把ds (数据段)、es( 扩展段) 、ss( 堆栈段) 全搞成跟cs( 代码段) 一样值了( 这个值就由org 指定的0x7c00) ,还把sp( 堆栈指针) 指向0x100h (256) 处。然后eax 自身异或清零,代码段指针被传进去,左移4 位后与LABEL_SEG_CODE32 相加。
LABEL_SEG_CODE32 在哪里?
LABEL_SEG_CODE32 在 [SECTION .s32] [BITS 32] 后面,是一段32 位代码段的头标签。根据NASM 的规则我们知道,任何不带[] 标签或变量是代表一个地址,所以LABEL_SEG_CODE32 就是那段32 位代码段的偏移地址。Cs 是16 位的,左移4 位再加上一个偏移地址不就是一个20 位的物理地址吗!
这个20 位的物理地址又被分成了3 部分,被填入变态的GDT 的LABEL_DESC_CODE32 的第2 、4 、7 字节处,果真是初始化了一个GDT 。
eax 又改过自新了一次,与上面类似地,通过ds 竟然把变态的GDT 老大的地址弄到了,而且还把它存到了跟屁虫GdtPtr 的第2 字节开始处. 。这下GdtPtr 什么都没做就有了那些GDT 的藏身基地,真是踏破铁鞋无觅处,得来全不费工夫。
Lgdt 是个探子----- 一旦GdtPrt 有了GDT 们的基地消息,就向GDTR 报告。这样一来,GDTR 就可以顺滕摸瓜把GDT 们一个一个给揪出来了。究竟GDTR 是如何找到GDT 们的,下一节会有分解。
再看,in/out 做了一些端口操作,打通了地址线A20 ,然后又对控制器寄存器cr0 的第0 位做了些手脚,此时此刻,我们终于走到了保护模式的大门口!
鲤鱼跳龙门,华丽转身。
jmp 之后它就开始进入保护模式的天地了,在这片天地里,它向人们展现了一个红色的’P ’ ,最终死在了jmp $ 上。
百说不如一做,让我们来见证一下这段辉煌。
第二章已经搭建好的bochs 和nasm 可以派上用场了:
nasm pmtest1.asm -o pmtest1.bin
dd if=pmtest1.bin of=a.img bs=512 count=1 conv=notrunc
bochs
注意:a.img 一定要用上次做好的那个,为什么?想一想。
现在,我们已经隐隐约约知道到了:
- 程序定义了一个叫做GDT 的数据结构。
- 后面的16 位代码进行了一些与GDT 有关的操作。
- 程序最后跳到32 位代码做了一点操作显存的工作。
下一节,让我们跟保护模式走得更近一些。