第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 后面只有那么点空间可用。
加载过程是这样的:
- BIOS 读取 MBR(512 字节)到内存 0x7C00 处
- MBR 中的代码(其实就是 bootmon 的第一阶段)被 BIOS 执行
- 这段代码再去读取磁盘上后续的 bootmon 代码(第二阶段)
- 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 必须在跳转到内核之前,完成模式切换。
切换过程大致如下:
- 关闭中断(CLI)
- 加载全局描述符表(GDT)
- 设置控制寄存器 CR0 的保护模式位(PE 位)
- 执行远跳转(far jump)刷新指令流水线
- 设置段寄存器(CS、DS、ES、FS、GS、SS)
- 跳转到内核入口点
代码示例(简化版):
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 的内核在获得控制权后,是如何一步步建立起完整的系统环境的。