第三章 引导加载程序(Bootloader):BIOS 启动流程、MBR 与分区引导扇区
好,咱们今天聊聊 MINIX 3 启动的第一步——引导加载程序。说白了,就是计算机从按下电源键到操作系统内核跑起来之间,那段「谁都不认识谁」的尴尬期怎么过渡的。
我个人习惯把引导过程比作「传接力棒」。BIOS 是第一棒,MBR 是第二棒,分区引导扇区是第三棒,最后才交到 MINIX 3 内核手上。每一棒都有严格的交接协议,出一点差错,系统就起不来。
BIOS 启动流程:硬件自检与启动盘选择
按下电源键那一刻,CPU 还处于实模式,寄存器里全是默认值。它做的第一件事,就是去物理地址 0xFFFF0 读取指令——那里是 BIOS 的入口。嗯,这里要注意,这个地址离 1MB 边界只差 16 个字节,所以 BIOS 的第一条指令通常是个跳转。
BIOS 先跑 POST(加电自检),检查内存、键盘、磁盘这些基本硬件。我刚开始做底层开发时,总觉得 POST 就是走个过场。直到有一次,我在一台老机器上调试引导程序,内存条松了,POST 卡在「长鸣三声」那里死活过不去——从那以后,我再也不敢小看这一步了。
POST 通过后,BIOS 会按照 CMOS 里配置的启动顺序,逐个检查启动设备。对于硬盘,它读取第一个扇区(LBA 0)的最后两个字节:
偏移 0x1FE: 0x55
偏移 0x1FF: 0xAA
如果这两个字节是 0x55 0xAA,BIOS 就认为这是个可引导的磁盘,然后把该扇区加载到内存 0x7C00 处,跳转过去执行。这就是 MBR 的由来。
MBR 的结构:446 字节代码 + 64 字节分区表 + 2 字节签名
MBR 只有 512 字节,空间极其有限。它的布局是这样的:
| 偏移 | 大小 | 内容 |
|---|---|---|
| 0x000 - 0x1BD | 446 字节 | 引导代码(Boot Code) |
| 0x1BE - 0x1FD | 64 字节 | 分区表(4个条目,每个16字节) |
| 0x1FE - 0x1FF | 2 字节 | 魔数 0x55AA |
你想想看,446 字节能干什么?连个像样的 C 函数都写不了。所以 MBR 的引导代码必须用汇编写,而且得精打细算。MINIX 3 的 MBR 代码就在 boot/boot.s 里,我截取关键部分给你看:
! MBR 入口点
.org 0x7C00
start:
cli ! 关中断
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ! 栈指针设在 MBR 下方
! 读取活动分区引导扇区
mov si, partition_table
mov cx, 4 ! 检查4个分区条目
check_partition:
mov al, [si] ! 分区状态字节
test al, 0x80 ! 是否为活动分区?
jnz found_active
add si, 16 ! 下一个分区条目
loop check_partition
int 0x18 ! 没找到活动分区,调用 ROM BASIC
found_active:
! 读取该分区的引导扇区到 0x7C00
mov dx, [si+2] ! CHS 参数
mov cx, [si+4]
mov ax, 0x0201 ! BIOS 读扇区功能
mov bx, 0x7C00
int 0x13
jc disk_error
jmp 0x7C00 ! 跳转到分区引导扇区
这段代码做了三件事:关中断、设置栈、扫描分区表找活动分区。找到后,把该分区的第一个扇区读到 0x7C00,然后跳过去。注意,它把自己覆盖了——因为分区引导扇区也要加载到同一个地址。
jmp 0x7C00 这样的远跳转,确保 CS:IP 正确。
分区引导扇区:从 MBR 到内核的桥梁
分区引导扇区(VBR,Volume Boot Record)是每个分区自己的引导扇区。MINIX 3 的分区引导扇区比 MBR 稍微复杂一点,因为它需要知道文件系统的布局,才能找到内核文件。
MINIX 3 的分区引导扇区结构如下:
- 跳转指令(3 字节):跳过 BPB 到实际代码
- BPB(BIOS Parameter Block,59 字节):描述文件系统参数
- 引导代码(约 400 字节):加载内核到内存
- 魔数(2 字节):0x55AA
BPB 里记录了每扇区字节数、每簇扇区数、根目录起始位置等。MINIX 3 的 VBR 通过这些信息,在文件系统中找到 /boot/image 或 /boot/kernel 文件,然后加载到内存。
这里有个关键点:VBR 代码必须知道内核文件在磁盘上的具体位置。MINIX 3 的做法是,在安装系统时,把内核文件的起始扇区号写入 VBR 的某个固定偏移处。这样 VBR 就不需要解析完整的文件系统了——说白了,就是走个捷径。
引导流程的完整接力
把整个流程串起来,就是这样的:
- BIOS:POST → 读取 MBR 到 0x7C00 → 跳转
- MBR:扫描分区表 → 找到活动分区 → 读取 VBR 到 0x7C00 → 跳转
- VBR:解析 BPB → 找到内核文件 → 加载到内存(通常是 0x100000 或更高)→ 跳转
- 内核:初始化页表、中断、设备驱动 → 启动第一个进程
每一步的代码都运行在实模式下,地址空间只有 1MB。直到内核启动后,才会切换到保护模式或长模式。所以引导代码必须非常精简,不能依赖任何操作系统服务。
我记得有一次,我在 MINIX 3 的 VBR 里加了个调试用的串口输出,结果代码膨胀了 30 字节,超过了 512 字节的限制。最后不得不把一些初始化操作挪到内核里,才勉强塞下。这让我深刻体会到:引导代码的每一字节都是宝贵的。
MINIX 3 的特殊之处
MINIX 3 的引导加载程序跟 Linux 的 GRUB 不太一样。GRUB 是个完整的引导管理器,支持菜单、配置文件、模块加载。而 MINIX 3 的引导加载程序走的是「极简路线」:
- MBR 只有 446 字节,不做任何用户交互
- VBR 直接加载内核,不提供选择菜单
- 不支持多引导(但可以通过链式加载实现)
为什么这么设计?因为 MINIX 3 的设计哲学是「微内核 + 高可靠性」。引导代码越简单,出错的概率就越低。你想想看,如果引导加载程序本身就有 bug,那系统连启动都启动不了,还谈什么可靠性?
不过,MINIX 3 也提供了一个可选的引导管理器 bootmon,它位于 MBR 和 VBR 之间,提供菜单选择功能。但默认安装是不带的——保持简单。
-s 选项可以启动 GDB 调试,让你单步跟踪引导代码。我当年调试 MBR 时,就是靠 QEMU + GDB 组合,一步步找出分区表解析的 bug 的。
好了,这一章的内容就到这里。下一章我们会深入 MINIX 3 的内核初始化过程,看看从实模式切换到保护模式时,那些让人头疼的页表、GDT、IDT 是怎么设置的。到时候我会分享一个我踩过的坑——关于 A20 地址线的,保证让你印象深刻。