本文主要分析 Linux 0.11 内核引导程序bootsect.s
关键部分。《Linux 内核完全注释》和《Linux 内核设计的艺术》两书将作为主要参考资料。
从上一博文 Linux 内核学习笔记:内核引导程序概况及 BIOS 我们知道,bootsect 模块需要由 BIOS 来将其拷贝至内存。下边我们先来看这一过程。
加载 bootsect 模块
这一部分仍属于 BIOS 部分,不属于 Linux 引导部分。
bootsect 模块(由 bootsect.s 汇编而成)放在磁盘(启动设备)的第 1 扇区(0 盘面 0 磁道 1 扇区)。第 1 扇区也称为启动扇区(boot sector)。在 1.44MB 磁盘上,其跟 setup 模块和 system 模块的分布情况如下图所示:
图片来源:《Linux 内核完全注释》
(bootsect 模块占用了第 1 个扇区,setup 模块占用了随后的 4 个扇区,Linux 0.11 内核 system 模块占用了接下的约 240 个扇区。1.44MB 磁盘总共有 2880 个扇区,除了被以上 3 个模块占用,还有 2630 多个扇区未被使用。这些未用空间可用来存放一个基本的根文件系统
,从而创建出使用单张磁盘就能让系统运转起来的集成盘来。)
BIOS 通过int 0x19
中断将软盘第 1 扇区内容(也即 bootsect 模块)拷贝至内存 0x07C00 处。需要注意的是,该中断的服务程序是由 BIOS 提前设计好的,需要跟操作系统的区分开来。就如《Linux 内核设计的艺术》所说的:
无论 Linux 0.11 的内核是如何设计的,这段 BIOS 程序(即 int 0x19 的服务程序)所要做的就是“找到软盘”并“加载第一扇区”,其余的它什么都不知道,也不必知道。
bootsect 模块的加载可用图表示如下:
图片来源:《Linux 内核设计的艺术》
bootsect 模块的加载非常重要。这意味着计算机自开机以来,内存中第一次有了 Linux 操作系统自己的代码(虽然只是启动代码)。接下来 setup 模块和
system 模块(包括了 head、main、kenel、mm、fs、lib 模块)
的加载都由 bootsect 模块完成。
关于硬盘(或软盘)的基础知识可参考之前博文 Linux 内核学习笔记:预备知识之“硬盘基础知识”。
实模式下内存规划
内存规划部分源代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
用图可表示如下:
图片来源:《Linux 内核设计的艺术》
其中,
SYSSIZE = 0x3000
:这个参数(196KB)限定了即将加载的 system 模块的最大长度。实际上,system 模块在磁盘只占用了约 240 个扇区(240 * 512B = 120KB)。512 KB
:该参数说明 system 模块的最大长度为 512KB,保证了 bootsect 模块在复制自身和加载 system 模块至新的位置时,模块之间不会互相重叠。详细请见上一博文 Linux 内核学习笔记:内核引导程序概况及 BIOS “Linux 0.11 内核引导程序概况”部分。ROOT_DEV = 0x306
指明了根文件系统设备是第 2 个硬盘个的第 1 个分区。
复制 bootsect
bootsect 模块将其自身(512B)从内存物理地址0x07C00(BOOTSEG)
处复制到0x90000(INITSEG)
。代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
其中,重点解释如下:
复制过程
这部分核心代码是:
1 | mov ax,#BOOTSEG |
ds (0x07C0) 和 si (0x0000) 联合使用,构成了源地址 0x07C00;es (0x9000) 和 di (0x0000) 联合使用,构成了目的地址 0x90000;cx (256) 指出了需要复制的“字”(movw)数(1 个字 2 个字节,共 512 个字节,也即第 1 扇区的字节数)。
这个过程其实还隐藏了一个点,即方向标志位(Direction Flag) DF
的值。在这里,方向标志位为 0,表示在执行串操作,地址按递增的方向变化。
关于这部分,百度知道有一个不错的解答:
- 先说说 MOVSB(MOVe String Byte):即字符串传送指令,这条指令按字节传送数据。通过 SI 和 DI 这两个寄存器控制字符串的源地址和目标地址,比如
DS:SI
这段地址的 N 个字节复制到ES:DI
指向的地址,复制后 DS:SI 的内容保持不变。- 而
REP
(REPeat)指令就是“重复”的意思,术语叫做“重复前缀指令”,因为既然是传递字符串,则不可能一个字(节)一个字(节)地传送,所以需要有一个寄存器来控制串长度。这个寄存器就是CX
,指令每次执行前都会判断 CX 的值是否为 0(为 0 结束重复,不为 0,CX 的值减 1),以此来设定重复执行的次数。因此设置好 CX 的值之后就可以用 REP MOVSB 了。CLD
(CLear Direction flag)则是清方向标志位,也就是使DF
的值为0,在执行串操作时,使地址按递增的方式变化,这样便于调整相关段的的当前指针。这条指令与STD
(SeT Direction flag)的执行结果相反,后者置 DF 的值为 1。
跳转至新位置
这部分代码如下:
1 | jmpi go,INITSEG |
jmpi 的语法是jmpi offset:segment
,表示跳转到 segment * 16 + offset 处。所以这部分代码的意思就是指令指针跳转到新位置(INITSEG)后接着原来的执行顺序(go)继续执行下去。可用图表示如下:
图片来源:《Linux 内核设计的艺术》
es 的作用
通过上边的分析,当指令指针跳转到新位置后,cs 的值位 0x9000,通过赋值,es 的值也变为 0x9000。
为 es 赋值是为接下来加载 setup 模块做准备。且看下文。
加载 setup 模块
bootsect 模块借助 BIOS 的int 0x13
中断加载 setup 模块。核心代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
这里需要重点解释的是int 0x13
中断用于从磁盘读数据到内存中:
- 入口参数
- (ah)=int 0x13 的功能号(2 表示读扇区)
- (al)=读取的扇区数
- (ch)=磁道号
- (cl)=扇区号
- (dh)=磁头号
- (dl)=驱动器号(软驱从 0 开始,0:软驱 A,1:软驱 B;硬盘从 80h 开始,80h:硬盘 C,81h:硬盘 D)
es:bx
-> 指向接收数据的内存区
- 返回参数
- 操作成功:(ah)=0,(al)=读入的扇区数
- 操作失败:(ah)=出错代码
所以,上述代码就是将磁盘中紧接着 bootsect 模块后的 4 个扇区(setup 模块)读取到 0x90200 处(紧接 bootsect 模块在内存中的新位置的末端)。加载结果如下图所示:
图片来源:《Linux 内核设计的艺术》
加载 system 模块
相对于加载 setup 模块,加载 system 模块需要加载大约 240 个扇区,所需时间自然更长。由于是长时间操作软盘,所以需要对软盘设备进行更多的监控,对读盘结果不断地进行检测。因此,加载 system 模块的代码也相对复杂:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
加载结果如下图所示:
图片来源:《Linux 内核设计的艺术》
确认根设备号
在加载完 system 模块后,还需确认根设备号:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
上述代码可用图表述如下:
图片来源:《Linux 内核设计的艺术》
最终,通过执行下边这一语句,指令指针跳转至 0x90200 处,开始执行 setup 模块:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |