进入保护模式-代码详解
MBR --> Loader --> Kernel
本节代码只涉及 MBR 和 Loader 部分,暂未考虑内核代码。同时,为规范操作,我们使用 [加载器-用户程序] 方式将 Loader 从硬盘载入内存。这种方式非常漂亮,同时能让你理解重定位的本质,详细请阅读程序加载器-重定位 (本节前置要求,务必阅读)。
本节代码对应分支
protected-mode
。
配置文件
本节的 MBR 可以直接引用 程序加载器 一文中的 MBR 代码,并在文件头引入配置文件:
1 | %include "loader.inc" |
其中 loader.inc
文件内容如下:
1 | ;文件说明:loader.inc |
接着,定义保护模式的配置文件 boot.inc
。将此图和以下代码对比阅读:
1 | ;文件说明:boot.inc ,包含保护模式中要用到的段选择子,描述符等内容 |
下面对以上宏定义进行说明:
- 将各个子属性进行宏定义,最后相加组成段选择子的高四字节(低四字节后续定义)。相比一大串莫名奇妙的数字,这样更加直观。
- 第 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
1 | ;文件说明:loader.s |
以上代码的解释:
-
为什么此 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
。
执行结果如下: