第三章 引导加载程序(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 的由来。

小提示: 0x7C00 这个地址不是随便选的。当年 IBM PC 设计时,为了给 BIOS 数据区和中断向量表留空间,同时保证 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,然后跳过去。注意,它把自己覆盖了——因为分区引导扇区也要加载到同一个地址。

避坑指南: 我曾经在调试时发现,某些 BIOS 的 int 0x13 调用会修改寄存器值,导致跳转地址出错。解决方案是在调用前保存关键寄存器,或者在调用后重新设置段寄存器。MINIX 3 的代码里就做了 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 就不需要解析完整的文件系统了——说白了,就是走个捷径。

核心要点: MBR 只负责找到活动分区,VBR 才负责找到内核。MBR 是「分区选择器」,VBR 是「内核加载器」。两者分工明确,缺一不可。

引导流程的完整接力

把整个流程串起来,就是这样的:

  1. BIOS:POST → 读取 MBR 到 0x7C00 → 跳转
  2. MBR:扫描分区表 → 找到活动分区 → 读取 VBR 到 0x7C00 → 跳转
  3. VBR:解析 BPB → 找到内核文件 → 加载到内存(通常是 0x100000 或更高)→ 跳转
  4. 内核:初始化页表、中断、设备驱动 → 启动第一个进程

每一步的代码都运行在实模式下,地址空间只有 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 之间,提供菜单选择功能。但默认安装是不带的——保持简单。

调试技巧: 如果你在开发自己的引导加载程序,建议先用 QEMU 模拟器调试。QEMU 的 -s 选项可以启动 GDB 调试,让你单步跟踪引导代码。我当年调试 MBR 时,就是靠 QEMU + GDB 组合,一步步找出分区表解析的 bug 的。

好了,这一章的内容就到这里。下一章我们会深入 MINIX 3 的内核初始化过程,看看从实模式切换到保护模式时,那些让人头疼的页表、GDT、IDT 是怎么设置的。到时候我会分享一个我踩过的坑——关于 A20 地址线的,保证让你印象深刻。