进入保护模式-代码详解
MBR --> Loader --> Kernel
本节代码只涉及 MBR 和 Loader 部分,暂未考虑内核代码。同时,为规范操作,我们使用 [加载器-用户程序] 方式将 Loader 从硬盘载入内存。这种方式非常漂亮,同时能让你理解重定位的本质,详细请阅读程序加载器-重定位 (本节前置要求,务必阅读)。
本节代码对应分支
protected-mode
。
配置文件
本节的 MBR 可以直接引用 程序加载器 一文中的 MBR 代码,并在文件头引入配置文件:
%include "loader.inc"
SECTION mbr align=16 vstart=0x7c00
;....................以下省略....................
其中 loader.inc
文件内容如下:
;文件说明:loader.inc
BASE_ADDR equ 0x900 ;最好不超过0xFFFF,原因在下文解释
START_SECTOR equ 2 ;从硬盘的第2(lba)扇区将加载器载入内存
接着,定义保护模式的配置文件 boot.inc
。将此图和以下代码对比阅读:
;文件说明:boot.inc ,包含保护模式中要用到的段选择子,描述符等内容
;以下为段描述符的子属性
DESC_G_4K equ 1000_0000_0000_0000_0000_0000B
DESC_DB_32 equ 100_0000_0000_0000_0000_0000B
DESC_L equ 00_0000_0000_0000_0000_0000B
DESC_AVL equ 0_0000_0000_0000_0000_0000B
DESC_LIMIT_CODE2 equ 1111_0000_0000_0000_0000B
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_0000_0000_0000_0000B
DESC_P equ 1000_0000_0000_0000B
DESC_DPL_0 equ 000_0000_0000_0000B
DESC_DPL_1 equ 010_0000_0000_0000B
DESC_DPL_2 equ 100_0000_0000_0000B
DESC_DPL_3 equ 110_0000_0000_0000B
DESC_S_SYS equ 0_0000_0000_0000B
DESC_S_DATA equ 1_0000_0000_0000B
DESC_TYPE_CODE equ 1000_0000_0000B;只执行1000
DESC_TYPE_DATA equ 0010_0000_0000B;可读写0010
;以下为段描述符的高四位(低四位在loader.s中定义)
DESC_CODE_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
DESC_TYPE_CODE + 0X00
DESC_DATA_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
DESC_TYPE_DATA + 0X00
DESC_VIDEO_HIGH4 equ (0x00<<24) + DESC_G_4K + DESC_DB_32 + DESC_L + DESC_AVL +\
DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA +\
DESC_TYPE_DATA + 0X0b
;========段选择子属性=============
RPL0 equ 00B
RPL1 equ 01B
RPL2 equ 10B
RPL3 equ 11B
TI_GDT equ 000B
TI_LDT equ 100B
下面对以上宏定义进行说明:
- 将各个子属性进行宏定义,最后相加组成段选择子的高四字节(低四字节后续定义)。相比一大串莫名奇妙的数字,这样更加直观。
- 第 7, 8 行,将 DATA 和 CODE 的 4 位段界限全设置为 1(后面会将 DATA 和 CODE 的另外16位段界限全设为1),由于 G=1,粒度为 4KB,所以实际段大小为 4GB。第 21,24 行
0x00<<24
以及末尾加上 0x00,这是在将高4字节中的段基址设为 0(后面会将 DATA 和 CODE 的另外16位段基址全设为0),所以段基址为 0。将段基址设为 0,段界限设为 4GB,这样做是为了形成平坦模型 ,即整个内存都在一个段中。平坦模型使用起来很方便,后期我们会慢慢体会到。 - 第 29 行,为啥最后加的 0x0b?之前说过,文本显示适配器的内存地址为
0xb8000~0xbffff
,为了方便显存的操作,显存段不使用平坦模型 ,所以将段基址设置为 0xb8000,其中的 b 在段描述符的高 4 字节上,这就是为啥 26 行末尾加 0x0b;显存的段大小为0xbffff-0xb8000=0x7fff
,粒度为 4KB,因此段界限为0x7fff÷4KB=7
,这将在段描述符的低 4 位设置,高 4 位直接设 0 即可。 - 第 15,16 行,SYS 表明该段是系统段;DATA 不是指数据段,而是相对于 SYS 而言的,代码段/数据段/栈段都属于 DATA 。
- 以上宏定义并未定义栈段,这是因为此处将栈段和数据段定义在了一起,即 DATA 段。关于为什么栈段和数据段能够放在一个段中,参见内存段与段寄存器保护 。
_
仅作分隔符,方便阅读,编译时会自动忽略。
loader.s
;文件说明:loader.s
%include "boot.inc"
%include "loader.inc"
SECTION loader vstart=BASE_ADDR ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start-BASE_ADDR ;偏移地址[0x04]
dd section.loader.start ;段地址[0x06]
realloc_tbl_len dw 0 ;段重定位表项个数为0
;=========================================================
;GDT
;第0描述符不可用
GDT_BASE dd 0x00000000
dd 0x00000000
;第1描述符CODE
DESC_CODE dd 0x0000FFFF
dd DESC_CODE_HIGH4
;第2描述符DATA
DESC_DATA dd 0x0000FFFF
dd DESC_DATA_HIGH4
;第3描述符VIDEO
DESC_VIDEO dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
;GDT指针
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
SELECTOR_CODE equ ((DESC_CODE - GDT_BASE)/8)<<3 + TI_GDT + RPL0
SELECTOR_DATA equ ((DESC_DATA - GDT_BASE)/8)<<3 + TI_GDT + RPL0
SELECTOR_VIDEO equ ((DESC_VIDEO- GDT_BASE)/8)<<3 + TI_GDT + RPL0
loader_msg db 'r',11000010b,'e',11000010b,'a',11000010b,'l',11000010b,'-',11000010b
db 'm',11000010b,'o',11000010b,'d',11000010b,'e',11000010b
;=======================================================
start: ;程序入口
mov ax,0 ;转移到loader代码后,
mov ds,ax ;将各段寄存器清0是头等大事
mov es,ax
mov ss,ax
mov gs,ax
mov fs,ax
print:
mov ax,0xb800 ;彩色字符模式视频缓冲区
mov es,ax
mov si,loader_msg ;ds:si
mov di,0 ;es:di
mov cx,18 ;9个字符,占18字节
cld
rep movsb
;========================================================
;1.打开A20
;2.加载GDT
;3.置PE=1
prepare:
;关中断
cli
;打开A20
in al,0x92
or al,0000_0010B
out 0x92,al
;加载GDT
mov ax,0
mov ds,ax
lgdt [gdt_ptr]
;CR0的第0位置1
mov eax,cr0
or eax,0x0000_0001
mov cr0,eax
;此后进入保护模式
jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线,装载CODE选择子
;======================================================
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,BASE_ADDR ;可以找其他合适的地方作为栈顶,这里使用BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax
mov byte [gs:160],'p'
mov byte [gs:161],11000010b
jmp $
;========================================================
program_end equ $-BASE_ADDR
以上代码的解释:
-
为什么此 loader 段的
vstart
不能像程序加载器中的 loader 段一样设为 0 ?有以下两个原因:-
注意第 75 行代码,该代码执行后,代码段的段基址为 0(因为CODE段描述符的段基址之前被全设为0了),进入了平坦模式,所以在内存中寻址并取得指令就全靠偏移地址啦!该行代码将直接跳转至
p_mode_start
处。那么,p_mode_start
的值为多少呢?这个问题至关重要。如果我们将 loader 段的vstart
设为 0,那么标号p_mode_start
的值就为 79 行代码相对于文件头的偏移量,本文件编译后所得二进制文件大小大概为 172 字节,所以p_mode_start
相对于文件头的偏移大概为 140(0x8C) 字节,即p_mode_start=0x8C
。问题在于,我们已经将此 loader 载入到内存 0x900 处,如果跳转到0x8C
处,显然将执行错误的代码。实际应该跳转到0x98C
处,而vstart=BASE_ADDR
便能将p_mode_start
以 0x900 开始计算偏移,这样就能跳转到正确位置啦!说清楚真不容易。。 -
再注意第 30 行的 GDT_BASE。要知道,GDT_BASE 为 32 位段基地址,CPU 是直接在内存中的
GDT_BASE
处来找到 GDT 的 。说到这读者就应该懂了吧?原因和上点相同。vstart 不好理解,具体参见 程序加载器 。
-
-
整个 loader 自成一段,这是为了方便。你也可以将以上代码分成数据段和代码段,但务必注意,除 loader 段外的其他段不能用 vstart 修饰 !
-
第 8 行,偏移地址为什么是
start-BASE_ADDR
,因为在 MBR 最后的跳转指令(71行)jmp far [0x04]
的效果是jmp 0x900:偏移地址
,所以要此处放置的必须是 start 标号相对于本文件开头的偏移量。 -
第 29 行,注意 GDT 的 LIMIT 等于 SIZE-1(因为偏移从0开始算)。
-
第 23 行,
(DESC_CODE - GDT_BASE)/8
得到索引值(段描述符为8字节),<<3
将索引值移到正确的位置上。
-
第 61 行,关闭中断。保护模式下的中断机制和实模式不同,原有的中断向量表不再适用,BIOS 中断无法继续使用。
-
第 87 行,之前我们说过,为了方便显存操作,对 VIDEO 段仍使用分段模型而非平坦模型。
-
注意!最后
program_end equ $-BASE_ADDR
得到整个文件二进制代码的大小。
最后需要单独强调的是,第 41~46 行代码并非必须要执行。这主要针对的是打印信息,即后面要用到的 ds 寄存器。如果 ds 不清零,则后续寻找字符时,ds:loader_msg
就是错误的地址,原因见上面第 1、2 点。然而如果按照我们上面的这种方式,则该 loader 必须加载到内存 0xFFFF 以内!否则打印无法正常进行(保护模式仍然能够正确进入)。这是因为打印时还在实模式,有效地址最大还是 16 位,如果 loader_msg 的标号超过 0xFFFF,那么有效地址就无法容纳 loader_msg。如果想把这 loader 加载到内存任意位置,则无需清零段寄存器,且第 50 行代码需要改为:mov si,loader_msg-BASE_ADDR
。
执行结果如下: