第4章:MINIX 3 的引导加载程序:bootmon 的设计与功能

4.1 bootmon 是什么?

好,咱们直接切入正题。bootmon 这个名字,你拆开看——boot + monitor。它既是引导加载程序,又是一个监控程序。说白了,它负责把 MINIX 3 的内核从磁盘上捞起来,加载到内存里,然后跳转过去执行。

但它的功能远不止这些。我个人习惯把 bootmon 看作是 MINIX 3 整个启动流程的「交通指挥员」。它不仅要保证内核能正确启动,还要在启动过程中提供一些调试和诊断的能力。嗯,这里要注意,bootmon 和 Linux 下的 GRUB 或者 LILO 不太一样,它更轻量,也更贴近 MINIX 的设计哲学——简单、可靠、透明。

核心要点: bootmon 是 MINIX 3 启动流程的第一段用户可见的代码(其实它运行在实模式下),它负责从磁盘读取内核映像,并完成从实模式到保护模式的切换准备。

4.2 bootmon 的存储位置与加载过程

bootmon 存放在哪里?它不在文件系统里。它被写在磁盘的主引导记录(MBR)之后、第一个分区之前的那段空隙里。我记得在早期的 MINIX 3 版本中,bootmon 的大小被严格限制在 8KB 以内,因为 MBR 后面只有那么点空间可用。

加载过程是这样的:

  1. BIOS 读取 MBR(512 字节)到内存 0x7C00 处
  2. MBR 中的代码(其实就是 bootmon 的第一阶段)被 BIOS 执行
  3. 这段代码再去读取磁盘上后续的 bootmon 代码(第二阶段)
  4. bootmon 完整加载后,开始解析启动菜单,等待用户选择

你想想看,这个过程其实挺脆弱的。如果磁盘的前几个扇区坏了,整个系统就起不来了。我曾经在一个老旧的 IDE 硬盘上遇到过这种情况——MBR 读出来了,但后续扇区有坏道,bootmon 加载到一半就卡死了。那会儿排查了好久才发现是硬件问题。

4.3 bootmon 的核心功能模块

bootmon 虽然小,但五脏俱全。我把它拆成几个功能模块来看:

模块名称 功能描述 代码位置(典型)
磁盘读取模块 通过 BIOS 中断 INT 13h 读取磁盘扇区 bootmon.s 中的 read_sector 例程
内存检测模块 调用 BIOS INT 15h 获取内存布局信息 bootmon.s 中的 mem_detect 例程
启动菜单模块 显示菜单,等待用户按键选择 bootmon.c 中的 menu 函数
内核加载模块 解析内核映像格式,加载到指定内存地址 bootmon.c 中的 load_kernel 函数
模式切换模块 从实模式切换到保护模式,跳转到内核入口 bootmon.s 中的 switch_protected 例程

这里我想多说一句,内存检测这个模块特别重要。MINIX 3 需要知道系统到底有多少物理内存,才能正确设置页表和内存管理。我见过有人移植 MINIX 3 到一台老机器上,结果因为 BIOS 返回的内存信息格式不太标准,bootmon 解析错了,导致内核只识别到 64MB 内存(实际有 256MB)。

4.4 启动菜单与用户交互

bootmon 启动后,会显示一个简单的菜单。你可能会问:「就几行文字,有什么好讲的?」其实这里面的设计挺讲究的。

典型的 bootmon 菜单长这样:

MINIX 3 Boot Monitor

1. Boot MINIX 3
2. Boot MINIX 3 (safe mode)
3. Boot MINIX 3 (verbose)
4. Boot from second partition
5. Boot from floppy
6. Command line

Enter choice:

每个选项背后都对应一组启动参数。比如 safe mode 会禁用某些硬件检测,verbose 模式会输出详细的启动日志。我个人习惯在调试内核时选 verbose 模式,这样能看到每一行初始化信息,定位问题快很多。

另外,bootmon 还支持命令行模式。你可以在启动前输入一些底层命令,比如:

  • load — 手动加载某个内核映像
  • boot — 启动已加载的内核
  • mem — 显示内存布局
  • help — 显示帮助信息

小技巧: 如果你在开发自己的内核模块,可以用 bootmon 的命令行模式手动加载测试,不用每次都重启机器。我曾经用这个方式调试了一个磁盘驱动,省了不少重启的时间。

4.5 内核映像的加载与解析

bootmon 加载内核映像时,不是简单地把二进制数据从磁盘读到内存就完事了。它需要解析内核映像的头部信息。MINIX 3 的内核映像有一个固定的格式,头部包含:

struct exec_header {
    uint32_t magic;        // 魔数,用于校验
    uint32_t text_size;    // 代码段大小
    uint32_t data_size;    // 数据段大小
    uint32_t bss_size;     // BSS 段大小
    uint32_t entry_point;  // 入口地址
    uint32_t total_size;   // 总大小
};

bootmon 会先检查魔数是否正确,然后根据这些信息计算出需要加载的扇区数,最后调用 BIOS 中断把数据读到内存中。这里有一个坑:BIOS 的 INT 13h 在读取大块数据时可能会因为 DMA 边界问题出错。我曾经在开发时遇到过,读取超过 64KB 的数据时,某些 BIOS 会莫名其妙地返回错误。解决方案是分多次读取,每次不超过 64KB。

4.6 从实模式到保护模式的切换

这是 bootmon 最关键的一步。MINIX 3 的内核运行在保护模式下,但 bootmon 本身运行在实模式下。所以 bootmon 必须在跳转到内核之前,完成模式切换。

切换过程大致如下:

  1. 关闭中断(CLI)
  2. 加载全局描述符表(GDT)
  3. 设置控制寄存器 CR0 的保护模式位(PE 位)
  4. 执行远跳转(far jump)刷新指令流水线
  5. 设置段寄存器(CS、DS、ES、FS、GS、SS)
  6. 跳转到内核入口点

代码示例(简化版):

switch_to_protected:
    cli                     ; 关中断
    lgdt [gdt_ptr]          ; 加载 GDT
    mov eax, cr0
    or eax, 1               ; 设置 PE 位
    mov cr0, eax
    jmp 0x08:protected_mode ; 远跳转刷新流水线

protected_mode:
    mov ax, 0x10            ; 数据段选择子
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    ; 设置栈指针
    mov esp, 0x1000
    ; 跳转到内核 C 语言入口
    jmp kernel_main

警告: 模式切换时,中断必须保持关闭状态,直到内核重新设置好 IDT 和中断控制器。否则,在切换过程中发生中断,CPU 会以实模式的方式去查中断向量表,但此时已经处于保护模式了,结果就是 triple fault,系统直接重启。

4.7 bootmon 的局限性

说了这么多 bootmon 的优点,也得提提它的不足。毕竟没有完美的设计:

  • 不支持多文件系统: bootmon 只能从 MINIX 3 的原生文件系统(或直接读取磁盘扇区)加载内核。你不能像 GRUB 那样从 ext4、NTFS 等文件系统启动。
  • 不支持网络启动: 没有 PXE 支持,只能从本地磁盘启动。
  • 不支持图形界面: 全程文本模式,没有花哨的启动画面。
  • 调试能力有限: 虽然提供了命令行,但远不如 GRUB 的 rescue shell 强大。

但话说回来,MINIX 3 的设计目标本来就不是通用操作系统。它更偏向教学和研究场景。bootmon 的简洁性反而让它更容易理解和修改。我当年学操作系统时,就是通过阅读 bootmon 的源码,才真正搞懂了计算机从加电到操作系统运行的全过程。

4.8 小结

bootmon 虽然只有几千行代码,但它完成了从磁盘读取内核、解析映像、切换保护模式等一系列关键任务。它是 MINIX 3 启动流程中承上启下的环节——上接 BIOS,下启内核。

下一章,我们会深入内核的初始化过程,看看 MINIX 3 的内核在获得控制权后,是如何一步步建立起完整的系统环境的。