本文主要分析 Linux 0.11 内核引导程序setup.s
关键部分。《Linux 内核完全注释》和《Linux 内核设计的艺术》两书将作为主要参考资料。
setup 模块已由 bootsect 模块加载至 0x90200 处,并且指令指针已指向该位置(boot/bootsect.s -> jmpi 0,SETUPSEG)。需要注意的是,setup 模块目前仍工作在实模式。
加载机器系统数据
setup 模块首先是将机器系统数据加载到内存 0x90000~0x901FD
区域(510B)即原 bootsect 模块所在地方(0x90000~0x901FF,共 512B)。这样就覆盖了原 bootsect 模块,并且只有 2 个字节没有使用到(512B - 510B)。具体代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
以上代码执行结果可用表表示如下:
图片来源:《Linux 内核设计的艺术》
注:关于上述代码的几个知识点
- lds (Load pointer using DS 或 Load Data Segment Register):传送目标指针,把指针内容装入 ds 和 si。如 lds si,string 把 string
段地址:偏移地址
存到ds:si
。类似的还有 les、lfs 等。 - 硬盘参数表
在计算机 BIOS 设定的中断向量表中,int 0x41 的中断向量位置(4*0x41=0x0000:0x0104)存放的并不是中断服务程序的地址,而是第一个硬盘的基本参数列表。第二个硬盘的基本参数列表入口地址存放于 int 0x46 中断向量位置处(4*0x46=0x0000:0x0118)。硬盘参数表内容如下表所示:
图片来源:《Linux 内核完全注释》
关中断并复制 system 模块
关中断
在加载完机器系统数据后、进入保护模式前,首先要做的就是关中断:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
这意味着,在接下来的程序执行当中,无论是否发生中断,系统都不会对中断进行响应,除非中断重新开启(sti)。
对于 cli 和 sti,《Linux 内核设计的艺术》一书有一个详细的阐述:
关中断(cli)和开中断(sti)操作将在操作系统代码中频繁出现,其意义深刻。慢慢的你会发现,cli、sti 总是在一个完整操作过程的两头出现,目的是避免中断在此期间的介入。接下来的代码将为操作系统进入保护模式做准备。此处即将进行实模式下中断向量表(IVT)和保护模式下中断描述符表(IDT)的交接工作。试想,如果没有 cli,又恰好发生中断,如用户不小心碰了一下键盘,中断就要切进来,就不得不面对实模式的中断机制已经废除、保护模式的中断机制尚未完成的尴尬局面,结果就是系统崩溃。cli、sti 保证了这个过程中,IDT 能够完整创建,以避免不可预料中断的进入造成 IDT 创建不完整或新老中断机制混用。
复制 system 模块
接下来,setup 模块将 system 模块从 0x10000 复制到内存起始位置 0x00000 处:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
其中,每次拷贝 0x8000 个字,也即 0x8000 * 2 = 0x10000 个字节。
这个复制操作将 BIOS 中断向量表和 BIOS 数据区完全覆盖,使它们不复存在。直到新的中断服务体系构建完毕之前,操作系统不再具备响应并处理中断的能力。
设置中断描述符表和全局描述符表
关于中断描述符表和全局描述符表基础知识,可先参考之前两篇博文:
设置中断描述符表和全局描述符表代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
其中,重点解释如下:
- lidt 和 lgdt:这两个命令分别用于加载中断描述符表寄存器(IDTR)和全局描述符表寄存器(GDTR),操作数(idt_48 和 gdt_48)均为 6 个字节(即 48 位,因 IDTR 和 GDTR 长度均为 48 位)。操作数的 0
1 字节用于表示描述符表的长度,25 字节用于表示描述符表的 32 位线性基地址。 - gdt:表示全局描述符表的内容,每项描述符均长 8 字节(即 64 位)。在这里,全局描述符表的第 0 项(dummy)不用,第 1 项是内核代码段描述符,第 2 项是内核数据段描述符。
设置中断描述符表和全局描述符表过程可用图表示如下:
图片来源:《Linux 内核设计的艺术》
上图 GDTR 的基地址”512+GDT”中的 GDT 表示的是 gdt 在 setup 模块中的偏移地址(在 setup.s 中即 205)。这里很容易误解为新建的全局描述符表会“覆盖” setup 模块从 gdt 开始的内容。其实,由 setup.s 编译而成的 setup 模块从 gdt 开始处本就存放着全局描述符表,而 GDTR 只是指向该处而已。可见于本文最后的 setup 模块执行完后内存映像图。
打开 A20
打开 A20,意味着 CPU 可以进行 32 位寻址,最大寻址空间为 4GB。这部分代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
虽然 Linux 0.11 只能支持 16MB 的物理内存,但其线性寻址空间已经是不折不扣的 4GB。
重新编程中断控制器 8259A
这部分代码为:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
对这部分,《Linux 内核设计的艺术》有一个比较详细的说法:
CPU 在保护模式下,int 0x00
0x1F 被 Intel 保留作为内部(不可屏蔽)中断和异常中断。如果不对 8259A 进行重新编程,int 0x000x1F 将被覆盖。例如,IRQ0 (时钟中断)为 8 号(int 0x08)中断,但在保护模式下此中断号是 Intel 保留的“Double Fault”(双重故障)。因此,必须对 8259A 编程将原来的 IRQ0x00IRQ0x0F 对应的中断号重新分布,即在保护模式下,IRQ0x00IRQ0x0F 的中断号是 int 0x20~int 0x2F。
这一过程可用图表示如下:
图片来源:《Linux 内核设计的艺术》
设置处理器工作方式为保护模式并跳转至内存 0x0000 0000 处开始执行
在之前博文 Linux 内核学习笔记:预备知识之“寄存器模型”我们有介绍了 CR0 寄存器。要让处理器工作模式从实模式转变为保护模式,需要将 CR0 的 PE 置 1。在处理器工作模式转变为保护模式后,指令指针需指向至内存 0x0000 0000 (32 位)处,以从 head 程序(system 模块最前边部分)开始执行。这部分代码如下:
1 | ! It is written in Intel 8086 Assembly. Added by MAX. |
其中,重点解释如下:
- lmsw (Load Machine Status Word):将 CR0 的 PE 置 1。
- jmpi 0,8:因为处理器已经工作在保护模式,所以 jmpi 中的“段基址” 8 不再表示实模式下的段基址,而是保护模式下的
段选择子
。按照段选择子数据格式,8 (二进制形式为 0000 0000 0000 1000)表示该段选择子请求特权级为 0 (RPL=00)、所指向的描述符存放在 GDT (TI=0)、所指向的描述符索引为 1(DI=0000 0000 0000 1)。所以,这一句汇编表示的是“选择 GDT 中的 1 号描述符,并根据该描述符内容和偏移(jmpi0
,8)跳转至内存某一位置”。这个过程可用图表示如下:
图片来源:《Linux 内核设计的艺术》
而 GDT 第一项描述符的内容(如下图所示)说明了代码是从段基址 0x0000 0000、偏移为 0 处,也就是 head 程序的开始位置开始执行的。这意味着执行 head 程序。
图片来源:《Linux 内核设计的艺术》
setup 模块执行完后内存映像
setup 模块执行完后内存映像图如下所示:
图片来源:《Linux 内核完全注释》