上篇文章 描述了基于 Intel 平台电脑的主板和内存映射,以便为系统引导启动(Boot)的初始阶段做准备。引导启动是一个复杂的、讨技的、多阶段的事务。下面是对它的整个过程的概述:
一切始于你按下计算机的电源键(纳尼!不会吧!)。主板一旦通电后,它就开始初始化它的固件——芯片组以及其他小部件,并尝试让 CPU 跑起来。如果这个时候失败了(比如 CPU 已损坏或缺失),那么除了旋转的风扇外,你的系统可能看起来就像完全僵死在那了。少数主板在 CPU 缺失或故障时会发出蜂鸣声,但根据我的经验,僵死与风扇齐飞的状态是最常见的情况。有时 USB 或其他设备也会导致这种情况的发生:对于一个曾在正常工作,突然就像这样死机的系统,拔掉所有非必要设备有可能会让问题得到解决。然后,我们再逐一排查出罪魁祸首。
如果一切顺利的话,CPU 现在已经跑起来了。在一个多处理器或多核系统中,某个 CPU 会被选中作为引导处理器(Bootstrap Processor, BSP),它负责运行所有的 BIOS 与内核初始化代码。此时被称为 AP (Application Processors) 的其余处理器保持关闭状态直到它们被内核明确激活。Intel CPU 已经发展数年,但仍保持良好的向后兼容性,因此,现代的 CPU 在开机后的行为能够与早在 1978 年问世的 x86 架构的开山鼻祖 Intel 8086 表现一致。在这种原始的开机状态下,处理器处于 实模式,内存分页 被禁用。这就像“远古”的 MS-DOS 那样,只有 1MB 的内存可以被寻址,任何代码都可以写到内存的任何地方——没有保护或特权的概念。
CPU 中的大多数 寄存器 在计算机上电后都有明确定义的值,包括保存着将要被 CPU 执行的指令地址的指令指针寄存器(EIP)。Intel CPU 使用了一个“黑科技”,从而做到即便在开机后只有 1MB 内存可以访问的情况下,也能执行位于 0xFFFFFFF0 (距离 4GB 内存的顶端 16 个字节,远高于 1MB) 的第一条指令,这是通过一个隐藏的基址(其实就是偏移量)加上 EIP 实现的。这个魔法地址被称为 复位向量,它已经成为了现代 Intel 处理器的标准。
译者注:指令指针寄存器同程序计数器 PC,EIP 是 Intel 的习惯叫法。
主板确保复位向量处的指令是 跳转 到映射了 BIOS 入口点的内存位置,这个跳转同时也默默清除了上电时的隐藏基址。得益于芯片组保存的 内存映射,所有这些内存位置都填充了 CPU 需要的正确内容。它们都被映射到了包含 BIOS 代码的闪存中,因为此时的 RAM 中全是随机的垃圾数据。如下图所示是一个相关内存区域的例子:
然后,CPU 开始执行 BIOS 代码,初始化机器的一些硬件。紧接着,BIOS 启动 开机自检(POST),以检测计算机中的各种组件。缺少可以正常工作的显卡会使得 POST 以失败告终,停止运行 BIOS 并发出哔哔声,以便让你晓得问题之所在,这是因为此时的屏幕并不能显示相关信息。工作正常的显卡带我们来到计算机看起来仍在活跃的阶段:制造商的 Logo,开始测试内存,“天使们吹响了它们的号角”。其他像键盘缺失这样的 POST 失败情况会导致程序停止并在屏幕上显示错误信息。POST 的检测和初始化交相运行,包括整理所有的资源——中断、内存范围、PCI 设备的 I/O 端口。遵循 ACPI 接口的现代 BIOS 会建立多个描述计算机设备的数据表,这些数据表会被后来的内核使用。
POST 完毕后,BIOS 想要启动的操作系统必须能够在某处找得到:硬盘、CD-ROM、软盘等等。用户可以配置 BIOS 查找引导设备的实际顺序。如果没有找到适当的启动设备,BIOS 就会停止,并显示“非系统磁盘或磁盘错误”等信息。故障硬盘可能会导致这种状况的出现。祈愿一切顺利吧,BIOS 找到能正常工作的磁盘,启动工作继续开展。
BIOS 读取硬盘的前 512 字节的数据,即 0 号 扇区,这也称 主引导记录(MBR),它通常包含两个重要部分:MBR 头部特定于操作系统的微型启动程序,后跟磁盘分区表。然而,BIOS 对此并不关心:它只是将 MBR 的内容加载到内存的 0x7c00 处,并跳转到该位置开始执行 MBR 中的代码。
MBR 中的具体代码可能是 Windows MBR 加载器,也可能是来自 Linux 加载器,如 LILO 或 GRUB 中的代码,甚至是病毒。相比之下,分区表是标准化的:它是一个 64 字节的区域,有 4 个 16 字节的条目,描述了磁盘是如何被划分的(所以你可以在同一磁盘上运行多个操作系统或拥有独立的宗卷)。传统上来说,微软的 MBR 代码会查看分区表,找到标记为活动的(唯一)分区,加载该分区的启动扇区,然后运行该代码。启动扇区是分区的第一个扇区,而不是整个磁盘的第一个扇区。如果分区表出了问题,你会得到“无效分区表”或“缺少操作系统”这样的信息。这种消息并非来自 BIOS,而是来自从磁盘加载的 MBR 代码。因此,具体的信息取决于特定的 MBR。
随着时间的推移,引导加载已经变得越来越复杂,越来越灵活。LILO 和 GRUB 可以处理各种各样的操作系统、文件系统和启动配置。它们的 MBR 代码不一定遵循上面所说的“引导活动(active)分区”的方式。但从功能上来说,其过程如下所述:
- MBR 本身包含引导加载程序的第一阶段,GRUB 将此阶段称为阶段 1。
- 由于空间狭小,MBR 中的代码只能够从磁盘加载包含了额外启动代码的另一个扇区。这个扇区可能是一个分区的启动扇区,也可能是安装 MBR 时硬编码到 MBR 代码中的一个扇区。
- 然后,MBR 代码,加上第 2 步加载的代码开始读取一个包含了 BootLoader 第 2 阶段的文件。在 GRUB 中,这是阶段 2 的内容;在 Windows Server 中,这个文件是
c:\NTLDR。在 Windows 中,如果第 2 步失败了,你可能会得到“NTLDR 丢失”的错误信息。第 2 阶段的代码然后读取一个启动配置文件(比如 GRUB 的grub.conf或 Windows 的boot.ini)。然后,它向用户提供引导选项,或者在单引导系统中继续行进。 - 这时,BootLoader 代码需要启动一个内核。它必须对文件系统有足够的了解,才能从启动分区中读取内核。在 Linux 中,这意味着要从像
vmlinuz-2.6.22-14-server这样的包含内核的文件中读取内容,使之加载到内存中并跳转到内核启动代码处。在 Windows Server 2003 中,一些内核启动代码与内核镜像本身是分开的,实际上是嵌入到 NTLDR 中的。在执行了数次初始化之后,NTLDR 会从文件c:\Windows\System32\ntoskrnl.exe中加载内核镜像,然后像 GRUB 一样,跳转到内核入口点。
有个复杂的问题值得一提(即前文我所说的“黑科技”)。对于当前的 Linux 内核,即便是压缩过后,也不能“塞进”实模式下 640KB 的可用内存中,我的 Ubuntu 内核压缩后是 1.7MB。但是,为了能够调用 BIOS 例程来读取磁盘,BootLoader 必须运行在实模式下,因为此时的内核显然是不可用的。解决方案就是让人钦佩的 unreal mode,这并不是一个真正的处理器模式(我希望英特尔的工程师能够像这样来称呼它),而是一种技术。在这种技术中,程序在实模式和保护模式之间来回切换,以便既可以使用 BIOS,又可以访问 1MB 以上的内存。如果你读过 GRUB 源码的话,你会看到这种切换到处都是(在 stage2/ 目录下查找对 real_to_prot 和 prot_to_real 的调用)。当这个拖沓的过程结束时,加载器已经把内核放进了内存,无论采取什么方式,最后它都会让处理器处于实模式状态下。
正如本文第一张图所示,我们现在正从“引导加载程序”过渡到“早期内核初始化”。此时,随着内核开始“放飞自我”,场子逐渐热了起来。下一篇文章是 Linux 内核初始化指北,并附有 Linux Cross Reference 上的源码链接。对于 Windows,我不能一以贯之 ;) 但我会指出重点。