计算机的启动过程

计算机的启动,实际上是一个很复杂的过程。本文将会介绍启动过程的每个步骤、每个细节。本文的叙述主要针对使用 X86 系列 CPU 的 IBM-PC 及其兼容机器,目前主流的台式机和笔记本都是这种架构。另外,操作系统方面,本文主要针对装有 GRUB 引导器的 Linux系统。

整体流程

在进入细节之前,我们先来对启动的过程做一个大体介绍,看一下从按下开机键到 OS 正常运行都经历了哪些过程。

按下开机键之后,CPU 开始通电,首先执行的是 BIOS 中的程序,这个程序首先会检查计算机的硬件,这个过程称作通电自检(POST)。然后 BIOS 根据预先设置的启动顺序检查每一个存储器,如果发现某个存储器第一个扇区(512 字节)的最后两个字节内容是 0x55 和 0xaa,BIOS 就认为这个设备的第一个扇区是一个引导扇区,并把这个扇区内容复制到内存地址 0x7c000 处,并跳转到那里,开始执行。

引导扇区只有 512 个字节,显然装不下一个操作系统。因此,引导扇区中存放的内容通常是一个简单的加载器,负责将操作系统加载到内存并跳转到操作系统开始执行。现代 Linux 发行版采用 GRUB 作为引导程序,这样可以实现多系统的完美共存(当然,Windows 除外)。

GRUB 会对软硬件环境进行基本设置,将 CPU 从实模式切换到保护模式,然后装入内核。内核继续设置分段、分页、初始化中断、内存管理,以及相关设备的驱动程序。之后内核启动 init 进程,第一个用户级进程开始执行。

init 进程启动后,会读取一系列脚本文件,来启动一系列服务程序,包括图形界面,最后执行 /bin/login,显示登陆界面。至此计算机启动完成。

下面将详细探讨启动过程的每一个步骤是如何实现的。


通电

CPU 是计算机的核心组件,因此开机过程也从 CPU 说起。所谓 CPU,无论其多么复杂,本质上都是一块电路。电路要工作,就需要电源,CPU 不会自带电源,而是由主板上一块叫做 ACPI(Advanced Configurable and Power Interface)的模块进行供电的。实际上, ACPI 的作用不仅是供电,OS 实现关机重启也是通过它实现的,不过这和要讨论的启动过程无关。当按下开机键的时候,CPU 这块电路就被加上了电压,这个步骤称作“加电”。CPU 加电之后,其内部的各种寄存器的初始值是不确定的,不仅如此,刚通电的时候,电压也是不稳定的。为了让 CPU 的状态确定下来,主板上的芯片组会想 CPU 持续发送 RESET 信号。当芯片组检测到 CPU 的供电电压已经稳定,便会撤去 RESET 信号,于是 CPU 开始工作。

CPU、引脚

CPU 作为计算机的核心,能够值执行运算、访问内存、控制IO,似乎无所不能。实际上,CPU 作为一块电路,只会做它应该做的事情,至于所做的事情是运算还是读取内存、访问外设,CPU 不会去关心,也无法关心。CPU 与计算机其他模块相对独立,当 CPU 插到主板上,它就通过引脚和外界进行沟通。这些引脚就是 CPU 产品上那一根根金色的针,通常有上百个之多。这些引脚有不同的作用,有的是电源,有的是地线,有的连接时钟脉冲信号,有的连接地址总线,用于表示内存地址。

前面说道“向 CPU 发送 RESET 信号”,其实就是通过引脚实现的。CPU 有一根引脚命名为 RESET,当该引脚为高电平时,CPU 就会把内部各个寄存器的值重置,重置的取值是规定好的,这就保证每次 RESET 引脚变为高电平的时候,CPU 总会回到一个完全相同的状态。

对于 X86 系列的 CPU 来说,RESET 会把 CPU 操作模式设为实模式(Real Mode),将 eip 寄存器设为 0xffff0。eip 表示即将执行的指令的所在内存地址,因此 eip 取值设为 0xffff0 的结果就是,CPU 会读取内存地址 0xffff0 处的指令并开始执行。

内存

现在遇到一个问题,既然要从内存地址 0xffff0 处读取指令并执行,而且内存不属于 CPU,那么 CPU 应该如何读取内存?或者说,什么是内存?

简单来说,内存即“内部存储”,是一种存储数据的设备。内存对于 CPU 来说属于外部存储,所以 CPU 只能通过引脚和内存交互,使用某些引脚传递内存单元的地址,另外一些引脚表示数据。对 CPU 而言,内存就是一个大数组,存储单元连续排列(实际上内存地址空洞是存在的,这里指的是物理地址空间连续)。每一个内存单元都可以使用它的“地址”来标识,对于 32 位 CPU 来说,这个地址就是一个 32 位无符号整数。

但是,在 CPU 的外部,内存的结构却十分复杂。首先,内存的来源十分广泛,RAM、ROM 是两种不同的存储技术,但它们都属于内存。某些外部设备也会占据一部分内存空间,例如显卡中的存储设备(即俗称的“显存”)也作为内存的一部分而存在。因此,必须有一种机制来管理这些内存的来源,使得对于一个内存地址,能够确定这个内存地址对应内存条,还是 BIOS,还是显存,等等。在 IBM-PC 上,这个机制是由内存控制器实现的。

内存控制器通常内置在北桥芯片里(现代的 Intel CPU 已经内置南桥和北桥,AMD 则已经淘汰南北桥的架构),这个内存控制器决定了那种类型的内存条能够使用(Rambus、DRAM、SDRAM等)。对于 X86 PC,1MB 以下的内存是实模式下能使用的全部内存,这部分内存的高地址来自 BIOS,而 CPU 重置之后 eip 的值 0xffff0 正是 1MB 以内的最高端地址。1MB 以内内存的分布如下表显示。

起始地址 结束地址 大小 类型 介绍
0x00000000 0x0009ffff 1KB RAM 部分范围会被 BIOS 使用
0x000a0000 0x000bffff 128KB video RAM VGA 显存
0x000c0000 0x000c7fff 32KB ROM Video BIOS
0x000c8000 0x000effff 160KB ROM 内存映射设备
0x000f0000 0x000fffff 64KB ROM BIOS

可以看到,1MB 内最高 64KB 范围都是 BIOS,对于大多数 BIOS 来说,内存地址 0xffff0 处存放的是一个跳转指令,使 CPU 跳转到 0x000f0000 处开始执行真正的 BIOS 程序。

BIOS 与 POST

BIOS 的全称是基本输入输出系统(Basic Input/Output System),这是一段固化在 ROM 中的程序。理论上讲,BIOS 属于软件,但它和 CPU、芯片组等硬件的关系非常密切,基本上需要为特定的主板订制,因此也称为“固件”(Firmware)。

POST 表示 Power-On Self-Test,是计算机启动过程中 BIOS 会自动进行的一系列操作,实现对硬件的检测和初始化。POST 阶段所检测的硬件包括 CPU、数学协处理器(现已集成到 CPU 内部)、时钟、DMA 控制器,以及中断控制器等。检测的顺序在不同 BIOS 上会有一些差别。接下来,BIOS 会在内存地址 0x000c0000 到 0x000c7800 的范围内寻找 video BIOS。如果找到了 video BIOS,检查其校验和,如果校验通过,BIOS 会将跳转到 video BIOS,后者会初始化显示适配器,并在初始化完成后将控制权还给 BIOS。这个时候,屏幕上会显示出 BIOS 生产商的 Logo 和相关硬件信息,这个界面称作 POST 界面。

除了显示适配器,还有许多设备带有自身的初始化程序 ROM,这些 ROM 映射到了内存 0x000c8000 至 0x000df800 的区域内。BIOS 接下来的工作就是检查这段内存,每隔 2KB 检查一次。如果发现相应设备的 ROM 并且校验吻合,就执行其中的代码以初始化设备。如果某个设备初始化失败,BIOS 就会在屏幕上显示 “XXX ROM Error”,其中的 “XXX” 为出错的 ROM 所在内存地址。

所有设备初始化完毕,BIOS 会读取内存 0x00000472 处的两字节内容,如果内容是 0x1234(小端法),那么说明计算机是热启动开启的,这时 BIOS 就会跳过接下来的 POST

在这些步骤中,如果发现错误,BIOS 会将错误信息显示在屏幕上,有些还会让机器蜂鸣器发生。如果发生的错误不是致命的,计算机会继续运行。

CMOS

计算机 POST 完毕,BIOS 会读取 CMOS 中的配置信息。CMOS 原本表示互补金属氧化物半导体(Complementary Metal-Oxide Semiconductor),但在 PC 机主板上,CMOS 表示一块 RAM 芯片。CMOS 由主板上一块电池供电,因此即使机器断电,CMOS 中的内容也不会丢失。CMOS 的容量很小,只有 128KB,但是存储了许多重要的配置数据,例如引导顺序、相关硬件功能的启用/禁用、开机密码等。

大多数 BIOS 都提供了 CMOS 设置的方法。在 Post Screen 的下方,通常会写着类似 “Press DEL to enter SETUP” 的文字,表示这时按下 DEL 键就会进入 CMOS 设置的界面。除此之外,进入 CMOS 设置的按键还有 F12、F2、ESC 等。

一个比较重要的配置就是引导顺序。BIOS 会根据 CMOS 中设置的引导顺序逐个检查设备,读取设备的第一个扇区(512 字节)。然后检查这个扇区的最后两个字节的内容。如果内容是 0x55 和 0xaa,那么 BIOS 就认为这个扇区是一个引导扇区,并将该扇区的内容加载到内存地址 0x7c000 处并跳转到那里,开始执行引导扇区中的代码。如果扇区的最后两个字节内容不是 0x55 和 0xaa,BIOS 就认为这个设备不可引导,然后继续寻找检测可引导设备。

硬盘引导

最典型的情况是计算机从硬盘引导操作系统,因此会把硬盘的第一个扇区读入内存地址 0x7c000 处并开始执行。然而,硬盘的特殊之处在于它具有分区,因此需要一种数据结构来记录分区的方式。传统分区使用主引导记录(Master Boot Record, MBR)记录分区,MBR 写在硬盘的第一个扇区的末尾,和引导扇区代码处在同一个扇区,但是只占 64 字节,每 16 字节为一个分区条目,也就是最多 4 个分区。MBR 的结构经历了许多变化,有若干标准,但都和经典 MBR 结构兼容。经典 MBR 结构如下表所示:

起始地址 长度 描述
0 446 引导扇区代码
446 16 分区条目1
462 16 分区条目2
478 16 分区条目3
494 16 分区条目4
510 2 0xAA55

在 Linux 系统下,可以通过下面的方法查看硬盘的 MBR 内容:

sudo xxd -l512 /dev/sda

在我的电脑上,输出是这样的:

0000000: eb63 9010 8ed0 bc00 b0b8 0000 8ed8 8ec0  .c..............
0000010: fbbe 007c bf00 06b9 0002 f3a4 ea21 0600  ...|.........!..
0000020: 00be be07 3804 750b 83c6 1081 fefe 0775  ....8.u........u
0000030: f3eb 16b4 02b0 01bb 007c b280 8a74 018b  .........|...t..
0000040: 4c02 cd13 ea00 7c00 00eb fe00 0000 0000  L.....|.........
0000050: 0000 0000 0000 0000 0000 0080 0100 0000  ................
0000060: 0000 0000 fffa 9090 f6c2 8074 05f6 c270  ...........t...p
0000070: 7402 b280 ea79 7c00 0031 c08e d88e d0bc  t....y|..1......
0000080: 0020 fba0 647c 3cff 7402 88c2 52bb 1704  . ..d|<.t...R...
0000090: f607 0374 06be 887d e817 01be 057c b441  ...t...}.....|.A
00000a0: bbaa 55cd 135a 5272 3d81 fb55 aa75 3783  ..U..ZRr=..U.u7.
00000b0: e101 7432 31c0 8944 0440 8844 ff89 4402  ..t21..D.@.D..D.
00000c0: c704 1000 668b 1e5c 7c66 895c 0866 8b1e  ....f..\|f.\.f..
00000d0: 607c 6689 5c0c c744 0600 70b4 42cd 1372  `|f.\..D..p.B..r
00000e0: 05bb 0070 eb76 b408 cd13 730d 5a84 d20f  ...p.v....s.Z...
00000f0: 83d0 00be 937d e982 0066 0fb6 c688 64ff  .....}...f....d.
0000100: 4066 8944 040f b6d1 c1e2 0288 e888 f440  @f.D...........@
0000110: 8944 080f b6c2 c0e8 0266 8904 66a1 607c  .D.......f..f.`|
0000120: 6609 c075 4e66 a15c 7c66 31d2 66f7 3488  f..uNf.\|f1.f.4.
0000130: d131 d266 f774 043b 4408 7d37 fec1 88c5  .1.f.t.;D.}7....
0000140: 30c0 c1e8 0208 c188 d05a 88c6 bb00 708e  0........Z....p.
0000150: c331 dbb8 0102 cd13 721e 8cc3 601e b900  .1......r...`...
0000160: 018e db31 f6bf 0080 8ec6 fcf3 a51f 61ff  ...1..........a.
0000170: 265a 7cbe 8e7d eb03 be9d 7de8 3400 bea2  &Z|..}....}.4...
0000180: 7de8 2e00 cd18 ebfe 4752 5542 2000 4765  }.......GRUB .Ge
0000190: 6f6d 0048 6172 6420 4469 736b 0052 6561  om.Hard Disk.Rea
00001a0: 6400 2045 7272 6f72 0d0a 00bb 0100 b40e  d. Error........
00001b0: cd10 ac3c 0075 f4c3 4134 0400 0000 0020  ...<.u..A4.....
00001c0: 2100 83fe ffff 0008 0000 0000 093d 00fe  !............=..
00001d0: ffff 05fe ffff fe0f 093d 0248 f915 0000  .........=.H....
00001e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001f0: 0000 0000 0000 0000 0000 0000 0000 55aa  ..............U.

可以看到最后的两字节标志 0xAA55,也可以看到 GRUB 引导器的错误提示字符串。在我电脑的硬盘上,只有一个主分区,其余都是逻辑分区,而逻辑分区都处于同一个扩展分区之内,因此 MBR 中只能看到两个分区条目有效。

虽说 MBR 即包含引导代码又包含分区信息,但 BIOS 却不会关心这些。BIOS 只会将这 512 字节的内容读到内存 0x7c000 处并执行,因此需要引导区代码来保证不会造成混乱。

早期的操作系统都是从引导扇区开始的,也就是说,当引导扇区被放入内存 0x7c000,并且 eip 寄存器指向这里的时候,操作系统就开始工作了。但这时的 CPU 仍处于十分原始的阶段。

实模式、保护模式、长模式

CPU 在不断发展,不仅体现在晶体管数量的增加,还体现在体系设计的优化,指令集的增长。然而,为了保证现有程序能够与新 CPU 兼容,在 CPU 设计的时候必须保留许多令人不爽的,过时的特性,其目标仅仅是为了让现有程序能够不用重新编译就能运行在新硬件上。

一开始,这种向后兼容的代价还能为人们接受。但后来,人们感觉旧模式兼容性成了制约发展的主要因素,迫切希望能够打破向后兼容的束缚。为此,80286 CPU 的设计者们机智地创造了“模式”这个概念。实模式(Real Mode)是默认的,也就是 CPU 重置之后就处在这个模式下,与之前的 CPU 完全兼容。与此相对,一种全新的保护模式(Protected Mode)出现了,保护模式与传统的实模式不兼容,原有代码不能直接运行,但是保护模式在硬件层面提供了访问控制、段页式内存管理、中断异常处理等能力,并且能够使用超过 1M 的内存,使得现代操作系统的出现成为可能。总之,保护模式非常先进非常好,只是因为和以前不兼容,所以默认不开启,需要程序打开 CPU 中的一个开关,保护模式才会激活,这个动作是由操作系统完成的。

类似地,当 CPU 由 32 位向 64 位转变的时候,AMD 创造了一种新的模式——长模式(Long Mode),该模式下,CPU 能进行 64 位算术运算,能使用 64 位虚拟地址。但保护模式到长模式的转换并没有实模式到保护模式那么剧烈。

引导器 GRUB

回到计算机启动过程的话题,既然现代操作系统需要保护模式,而保护模式又不是默认开启的,因此操作系统首先要做的就是启用保护模式。

然而,开启保护模式要用许多条指令,512 字节的引导扇区装不下,更不要说 MBR 还要放分区表。因此,多数操作系统的引导扇区做的事情都很简单,加载后面几个扇区到内存,然后由后面扇区中的程序来干活。

多数操作系统都是这样,由引导扇区加载更多扇区,然后切换保护模式,获取机器内存量和外设,初始化,执行内核。既然大多数操作系统启动的初始阶段都是相同的步骤,为何不使用统一的代码和工具实现呢?GRUB 引导器实现的就是这个功能。GRUB 引导分为若干阶段,最早的阶段在引导扇区,后续阶段完成模式切换,搜集硬件信息,寻找/加载内核等工作。最重要的是,GRUB 能识别文件系统,可以用脚本进行配置,而且是操作系统无关的,只要内核遵循 Multiboot 规范,就能够被 GRUB 识别并引导。而且,通过 GRUB,多系统并存也很容易实现。


Linux 内核的启动流程

早期 Linux 没有引导器,具有自己的引导扇区代码。采用 GRUB 之后,原来的 Linux 引导扇区代码尽管保留了下来,但是不会被执行。如果强制执行这段遗留的引导扇区代码,程序会在屏幕上显示一行错误信息,然后让计算机重启。

这是为了适应 Linux 现有引导方式,GRUB 引导 Linux 的方式却不是 Multiboot 协议,而是 Linux Boot Protocol。这种协议下,内核开始执行时仍然处在实模式,进行完实模式下的初始化工作之后,由内核完成到保护模式的跳转。

这里需要提一下 Linux 的内核镜像,也就是 /boot 目录下那个以 vmlinuz 开头的文件(代指 Virtual Memory LINUx gZip)。这个文件的构成十分复杂,由许多部分拼接起来。

镜像文件开头的 512 字节是已经弃用的旧引导扇区代码,之后是 arch/x86/boot/head.S 汇编而成的代码,这也是引导器之后最先运行的部分。这段代码会初始化代码执行环境,清空堆栈段,然后调用 arch/x86/boot/main.c 中的 main 函数。

虽然 C 语言中 main 函数是程序执行的入口,但对于内核来说就是一个普通的函数。该函数会依次完成以下工作:

进入保护模式之后,执行 init/main.c 中的 start_kernel 函数,这个函数是与特定体系架构无关的,但做的事情同样是各种初始化。包括 CPU、SMP、分页、内存管理器、进程调度器、中断、时钟。

Linux 内核通常是压缩过的,用来节省体积。因此内核头部专门含有一段代码,用来解压内核,并将内核放入正确的位置。

sudo xxd -l512 /boot/vmlinuz-3.16.0-4-amd64

具体的文件名可能因内核版本而不同,在我的电脑上,输出的结果如下:

0000000: 4d5a ea07 00c0 078c c88e d88e c08e d031  MZ.............1
0000010: e4fb fcbe 4000 ac20 c074 09b4 0ebb 0700  ....@.. .t......
0000020: cd10 ebf2 31c0 cd16 cd19 eaf0 ff00 f000  ....1...........
0000030: 0000 0000 0000 0000 0000 0000 8200 0000  ................
0000040: 5573 6520 6120 626f 6f74 206c 6f61 6465  Use a boot loade
0000050: 722e 0d0a 0a52 656d 6f76 6520 6469 736b  r....Remove disk
0000060: 2061 6e64 2070 7265 7373 2061 6e79 206b   and press any k
0000070: 6579 2074 6f20 7265 626f 6f74 2e2e 2e0d  ey to reboot....
0000080: 0a00 5045 0000 6486 0400 0000 0000 0000  ..PE..d.........
0000090: 0000 0100 0000 a000 0602 0b02 0214 307d  ..............0}
00000a0: 2f00 0000 0000 d000 c200 1046 0000 0002  /..........F....
00000b0: 0000 0000 0000 0000 0000 2000 0000 2000  .......... ... .
00000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000d0: 0000 0080 f100 0002 0000 0000 0000 0a00  ................
00000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000100: 0000 0000 0000 0600 0000 0000 0000 0000  ................
0000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000130: 0000 0000 0000 0000 0000 2e73 6574 7570  ...........setup
0000140: 0000 e041 0000 0002 0000 e041 0000 0002  ...A.......A....
0000150: 0000 0000 0000 0000 0000 0000 0000 2000  .............. .
0000160: 5060 2e72 656c 6f63 0000 2000 0000 e043  P`.reloc.. ....C
0000170: 0000 2000 0000 e043 0000 0000 0000 0000  .. ....C........
0000180: 0000 0000 0000 4000 1042 2e74 6578 7400  ......@..B.text.
0000190: 0000 303b 2f00 0044 0000 303b 2f00 0044  ..0;/..D..0;/..D
00001a0: 0000 0000 0000 0000 0000 0000 0000 2000  .............. .
00001b0: 5060 2e62 7373 0000 0000 d000 c200 307f  P`.bss........0.
00001c0: 2f00 0000 0000 0000 0000 0000 0000 0000  /...............
00001d0: 0000 0000 0000 8000 00c8 0000 0000 0000  ................
00001e0: 0000 0000 0000 0000 0000 0000 0000 00ff  ................
00001f0: ff21 0100 b3f3 0200 0000 ffff 0000 55aa  .!............U.

参考资料