计算机的启动

本文是对 Gustavo Duarte 两篇文章的笔记:

目标: 了解从按下电源按钮开始到内核开始正常工作的主要过程

关键字: 启动,Linux,boot loader

第一部分:内核之前的故事

说明: 以下步骤忽略了很多细节。

主板初始化,确保上面的芯片能正常运行。

CPU开始运行,对于多核的CPU,会选择其中的一个作为 bootstrap processor(BSP) 来运行。此时CPU处于所谓的 real mode ,不能对内存进行分页管理,一共只有1M内 存可用。

CPU的大多数寄存器都是实现定义好的,包括执行当前要运行的指令在内存中的地址, 即 instruction pointer(EIP)。主板确保这个指令的工作就是 jump 到BIOS的入口的 那条指令。

CPU开始执行BIOS的指令,这些指令固定在内存的第640K-1M的区间内。

BIOS程序会对机器的硬件做一些初始化,然后做一下 Power-on Self Test(POST),对 各个组件进行测试。

BIOS试图按照用户设置好的顺序从某种介质启动操作系统,可能是磁盘、光驱或者网 络等等,我们假设从磁盘启动。

BIOS读取磁盘最开始的512字节的数据,这部分数据叫做 boot sector 或者 boot block, 即引导扇区。Boot sector一般包含一段程序,这样启动程序就能能够从同一个设备上加载 头512字节的内容,把这部分内容放到物理内存的0x7c00的位置,然后执行它,从而引导 操作系统或者做其它事情。

对于已经分区的设备,其boot sector又叫做 Master Boot Record(MBR)。 MBR包含两部分,一开始是和OS相关的启动程序,后面接着的是磁盘的分区表。但事 实上BIOS并不关心MBR具体的构成,它只是简单地把MBR读取到内存的 0x7c00 地址处 ,然后跳到此处开始执行,即所谓的 boot loader.

Boot loader包含两部分,第一部分就是在MBR中的代码,叫做 Stage 1.

由于MBR很小,它所做的事情就是从硬盘中再读入另外一段程序,然后执行它。

读入的程序就是 Boot loader 的第二部分,在GRUB中就叫做 “GRUB Stage 2” ,对于 Windows就是 NTLDR ,这部分代码从磁盘主读取启动配置文件(比如grub.conf或者 boot.ini),根据配置进行启动。

Boot Loader根据启动配置文件,使用BOIS的硬盘IO服务从磁盘中读取内核镜像文件到 内存,跳到这部分代码并开始执行,至此内核开始启动。需要注意的是,此时CPU仍然 是运行在 real mode中的,只用用头1M内存,并且第640K-1M的部分已经被BIOS用了, 于是只剩640K可用。内核镜像一般都大于640K,于是 boot loader 使用了一些技术, 使其能够访问1M以上的内存。

第二部分:内核的启动

第一部分结束后,现在CPU还处于 real-mode ,只能处理1M以内的内存,供内核使用 的更是只有640K,而内核镜像一般都比1M大。于是,内核镜像被分做两部分分别加载 到内存中:第一部分较小,包含了 real-mode 的内核代码,这部分加载到640K以下的 位置,剩下的部分是 protect-mode 的代码,加载到从1M开始的位置,如下图所示:

0-640K的内存区域中,boot loader会读取一些信息,包括可读的内核版本的字符串,以及 real-mode 内核部分的位置等。另外boot loader也会写一些东西到这部分内存,比如用户 在boot loader菜单中输入的命令参数等。当boot loader程序结束时,它会填充所有内核 需要的参数,然后jump到内核的入口。

内核镜像 real-mode 部分的头512字节,事实上就是一个boot sector(引导扇区),在没 有boot loader的时代,这部分代码就负责对Linux的引导。现在,这部分代码仅仅打印一 行信息。

在头512字节后,偏移量为0x200的位置,即是 real-mode 内核的入口,也就是前面所说 的boot loader需要jump到的位置。接下来会执行一段程序,为 real-mode 的内核设置一 个栈(Stack),把BSS设置为0。以上的代码都是汇编,这部分完成后,就会jump到位于 arch/X86/boot/main.c的C程序。

main.c主要做一些初始化设置,还检测一下内存、设置一下video等,然后调用 go_to_protected_mode(),进入 protected mode。real-mode 和 protected mode之间 有两个最主要的区别:中断和内存。在real-mode,中断描述符表总是在地址0处,而在 protected mode,中断向量表的位置储存在IDTR寄存器中。另外,protected mode 需要 对逻辑地址进行和物理地址进行转换,需要一个GDTR的寄存器来存储 Global Descriptor Table 。real-mode到protected mode的这些转换工作都是在go_to_protected_mode()中完成 的。

对protected mode的进入是通过一段名叫protected_mode_jump的汇编完成的,具体是通 过设置CPU的CR0寄存器的PE位。对于32位系统,现在已经可以使用4G的内存了。接下来就 是一些寄存器的初始化,然后对压缩的内核进行解压。

内核解压完成后就会覆盖之前压缩的内容,于是它也是从内存的1M位置处开始。解压完成 后,就jump到protected mode的接入点,开始执行内核代码。

内核通过汇编对protected mode 的BSS进行初始化,设置最终的GDTR的值,创建页表( page tables)使得分页机制能被启动,初始化一个Stack,创建最终的中断描述符表,最 后jump到start_kernel()的C代码。

start_kernel()主要就是对一系列内核子系统和数据结构的初始化,接下来调用 rest_init()

rest_init()启动一个内核线程运行kernel_init(),然后调用schedule()开始启动 进程调度,然后调用cpu_idle()cpu_idle()是使CPU保持idle状态的线程,它一直循 环直到有其它工作要做。

在这之前,所有代码都只使用了一个CPU,而kernel_init()就负责对剩下的CPU进行初始 化,最后调用init_post,在用户空间启动一个进程,即PID为1的init进程。init进程 再根据配置启动一系列的程序。

到此,系统启动完毕。


参考资料: