9.3 引导程序与规范
引导程序(Bootloader)是介于底层主板固件与操作系统内核之间的承上启下之作。它的核心使命在于初始化关键总线与执行模式,探查并收集硬件拓扑与物理内存布局,继而将操作系统的内核映像从持久化存储介质安全地装载入物理内存,最终平稳转交处理器的控制权。
随计算机体系结构的演进,固件与硬件引导方式经历了从传统 BIOS 向现代 UEFI 的结构性升级,由此派生出截然不同的启动路径与磁盘组织规范。
1. 硬件固件与引导模型
1.1 Legacy BIOS 与 MBR 分区结构
Legacy BIOS 是 x86 个人计算机长期沿用的传统固件接口,广泛存在于早期机型以及现代主板的兼容支持模块(CSM)中。
在 Legacy BIOS 启动模型下,主板固件在完成基础的加电自检后,会按照启动设备序列依次轮询外部存储介质。对于每一个候选设备,BIOS 会读取其第 0 扇区(逻辑 LBA 0,大小为 512 字节)至内存物理地址 0x7C00 处。如果该扇区末尾两个字节包含有效的引导签名 0x55 0xAA,固件便判定该扇区包含可执行的主引导记录(Master Boot Record,简称 MBR),随即跳转到 0x7C00 开始执行其中的引导代码。由于 MBR 需同时容纳引导代码、4 个主分区项(每项 16 字节)与有效签名,留给引导代码的空间不足 446 字节,通常需要多阶段链式跳转才能完成内核的最终载入。
1.2 UEFI 固件与 GPT 分区体系
为了打破 MBR 寻址扇区上限(最大仅支持 2 TiB 寻址)以及实模式 16 位寻址的陈旧束缚,现代计算机普遍转向统一可扩展固件接口(UEFI)。与之配套的全局唯一标识分区表(GUID Partition Table,简称 GPT)将磁盘容量寻址上限拓展至 18 EB,并提供了主备双分区表与 CRC32 完整性校验机制。
UEFI 固件摆脱了对固定磁盘物理扇区代码的依赖,转而原生解析 FAT32 文件系统。固件从外部存储器的 EFI 分区中的以下路径 EFI\BOOT 根据计算机的架构去加载指定引导文件:
- 64 位计算机加载
BOOTX64.efi - 32 位计算机加载
BOOTIA32.efi
UEFI 运行在处理器原生位宽下,提供丰富的引导期服务(Boot Services)与运行时服务(Runtime Services),在安全性上支持安全启动(Secure Boot),杜绝了传统 MBR 扇区易被恶意软件静默篡改的隐患。
2. 选择引导方式
在实际内核开发中,手动从实模式汇编编写一套支持复杂文件系统解析与内核重定位的引导程序耗时耗力。借助成熟的通用引导规范与现成引导程序,能够将注意力聚焦于内核本身的架构设计与核心组件实现。
2.1 Multiboot
Multiboot 规范是一个开放标准,它为内核提供了一种统一的方式,可以由符合 Multiboot 的引导加载程序引导。符合 Multiboot 规范的内核只需在编译出的映像前 8192 字节内嵌入一段特定的 Multiboot 头部结构,便能由 GRUB 等通用引导器直接识别。Multiboot 规范的参考实现由 GRUB 提供,著名的 Xen 虚拟机监视器就是使用的 Multiboot 引导。
在引导过程中,GRUB 会自动将 CPU 从实模式切换到 32 位保护模式,屏蔽底层 A20 地址线的细微差异,将内存信息、模块地址和命令行参数以标准结构体传入 EBX 寄存器。
2.2 Limine
Limine 是一个先进的、可移植的、多协议的引导加载程序,支持 Linux、原生 Limine 引导协议以及 multiboot1 和 2。Limine 非常适合作为一些自制操作系统的引导程序。
相比传统协议,Limine 原生协议专为 x86_64 等现代 64 位平台量身定制,通过结构体请求(Feature Request)机制,按需为内核提供高级特性:如直接建立高半核(Higher-half Kernel)虚拟映射、提供线性帧缓冲区(Framebuffer)信息、解析 ACPI RSDP 指针等,极大简化了 64 位长模式内核的自举负担。
开发者可依据目标平台的位宽与引导形态灵活选择。
现成的模板
详见 项目搭建
3. 习题
(1) 用本页 1 节的两段说明填一张对照表,四列分别是:引导方式、引导扇区或分区表的位置、固件按什么路径找到引导程序、引导文件名的决定因素。两行分别是 Legacy BIOS (MBR) 与 UEFI BIOS (GPT)。
(2) 在 UEFI 一栏补上 BOOTX64.efi 与 BOOTIA32.efi 各自对应的计算机架构,并按 1.2 节的路径写出完整路径(用反斜杠)。
(3) 回答:把 BOOTX64.efi 改名成 BOOTX86.efi 放到同一个目录,64 位机器上还能不能引导?依据是 1.2 节的哪句话?
验收:四列都填满;第 (3) 问给结论并引用本页原文,不凭印象回答。
1.1 节说 BIOS 把引导设备的第一个扇区读到内存并运行,这个扇区只有 512 字节,最后两字节必须是 55 AA。用 nasm 写一个最小的引导扇区 boot.asm,逐步验证这条约束。骨架如下:
BITS 16
ORG 0x7C00
start:
xor ax, ax
mov ds, ax
mov si, msg
next:
lodsb
test al, al
jz halt
mov ah, 0x0E
mov bx, 0x0007
int 0x10
jmp next
halt:
hlt
jmp halt
msg: db "boot ok", 0
times 510 - ($ - $$) db 0
dw 0xAA552
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(1) nasm -f bin boot.asm -o boot.bin 之后用 wc -c boot.bin 确认是 512 字节,用 od -A x -t x1 -j 0x1F0 -N 16 boot.bin 确认末两字节是 55 aa。
(2) 删掉 ORG 0x7C00 重新汇编,用 ndisasm -b 16 boot.bin 看 mov si, ... 那条指令。两次的立即数分别是 0x7C18 和 0x0018。说明哪一个是对的,为什么错了的那一个在真机上会打印乱码或什么都不打印。提示:BIOS 把扇区装到物理地址 0x7C00,而 ORG 只影响汇编器算出的地址。
(3) 在 start 后面插入 times 600 db 0x90 再汇编。记录报错原文,并说明为什么 times 510 - ($ - $$) 会变成负数。
(4) 没有真机或虚拟机时,怎么确认这段代码的指令序列是对的?给出一种只靠 ndisasm,或 objdump -D -b binary -m i8086 -M intel --adjust-vma=0x7C00 的检查办法,并写出你实际看到的头四条指令。
验收:三组产物的字节数、末两字节、反汇编立即数都贴出来;第 (3) 问的报错是原文,不是转述。
2.1 节提到 Multiboot,2.2 节提到 Limine。这一题把「引导器认不认这份内核」变成一条可以自动检查的规则。
(1) 读 Multiboot 1 规范里关于首部的部分(OSDEV 的 Multiboot 页面或 GNU GRUB 手册),回答:
- 首部由哪三个 32 位字开头?第三个字为什么取那个值,而不是随便一个常数?
- 规范要求首部落在这块映像的前多少个字节之内?
- GRUB 找到首部之后,交给内核的是哪一个寄存器里的什么值?内核用它做什么?
- Limine 除了 Multiboot 1,还支持哪几种引导协议(按 2.2 节列出的说)?
(2) 写 mbscan.c,用法 mbscan <image>:只读前 64 KiB,按 4 字节步长扫描 0x1BADB002;命中后读紧随的 flags、checksum,计算三字之和,打印偏移、三个字、和,并额外判断该偏移是否小于 8192。找不到时打印 no Multiboot 1 header found in N bytes 并以 1 退出;参数错误以 2 退出;校验通过以 0 退出。
(3) 用两份映像测试:一份把 .multiboot 段放在链接脚本最前面;一份把 .text 放前面、在 .rodata 里塞 12 KiB 数据,把 .multiboot 挤到后面。参考输出分别是:
offset 0x1000: flags=0x00000003 checksum=0xE4524FFB sum=0x00000000 checksum ok, inside first 8192offset 0x4098: flags=0x00000003 checksum=0xE4524FFB sum=0x00000000 checksum ok, OUTSIDE first 8192第二份映像链接成功,readelf -S 也能看到 .multiboot 段,但 GRUB 会报找不到首部。用扫描结果解释原因,并写出把首部挪回前 8192 字节的两种做法。
(4) 为什么扫描步长是 4 而不是 1?printf 打印 size_t 时为什么不能用 %X?
验收:两份映像的输出与上面一致;第 (3) 问给出可执行的具体修改;扫描器在 -Wall -Wextra 下无警告。
这一题把三种引导映像的「头部自检」做齐:MBR 分区表、GPT 头、EFI 应用的 PE 头。三份活都在只读文件头的前提下完成,不需要真机。
(1) [4.3] 写 mbrparse.c 解析 512 字节的 ex_mbr.bin,十六进制内容如下(前 0x1B0 字节除前 12 字节的代码外全为 0):
0000: FA 31 C0 8E D0 BC 00 7C FB F4 EB FD 00 00 00 00
01B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00
01C0: 00 00 83 00 00 00 00 08 00 00 00 00 10 00 00 00
01D0: 00 00 EF 00 00 00 00 08 10 00 00 20 00 00 00 00
01E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
01F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA2
3
4
5
6
先校验 55 AA;再从 0x1BE 起按每项 16 字节读出四项,字段为状态、起始 CHS(3 字节)、类型、结束 CHS(3 字节)、起始 LBA(4 字节小端)、扇区数(4 字节小端)。状态 0x80 打印 yes;类型按表翻译:0x00 空、0x83 Linux、0xEE GPT 保护、0xEF EFI 系统、0x07 NTFS/exFAT,其余 unknown。前两行应为:
entry 0: boot=yes type=0x83 (Linux) lba=2048 sectors=1048576
entry 1: boot=no type=0xEF (EFI system) lba=1050624 sectors=81922
结构体用 __attribute__((packed)) 还是位域,选一种并说明理由。再回答:0xEE 为什么叫「GPT 保护」,它防的是哪一类工具;起始 LBA 2048 前面留了多少字节,分区工具为什么偏爱这个值。
(2) [4.4] 写一个程序生成 1536 字节的 disk.img:第 0 扇区是保护性 MBR,第 1 扇区是 GPT 头,第 2 扇区留空。保护性 MBR 在 0x1BE 的类型填 0xEE、起始 LBA 填 1、扇区数填 0x200000,末两字节 55 AA。GPT 头按规范填:偏移 0 写 EFI PART,偏移 8 写 0x00010000,偏移 12 写头长度 92,偏移 24 写当前 LBA 1,偏移 32 写备份 LBA 0x1FFFFF,偏移 40 写首个可用 LBA 34,偏移 48 写末个可用 LBA 0x1FFFDE(总扇区数减 34),偏移 72 写分区项数组起始 LBA 2,偏移 80 写项数 128,偏移 84 写单项大小 128,其余为 0。
再写 gptcheck.c 读回这个映像,输出四行:
MBR: signature AA55 ok
MBR: entry 0 type 0xEE, first LBA 1, sectors 2097152
GPT: signature EFI PART at LBA 1
GPT: header size 92, usable LBA 34..20971182
3
4
用 cmp 确认两次生成的映像逐字节相同。验算 0x1FFFDE + 34 = 0x200000(末个可用 LBA 之后还要留 33 个扇区的分区项数组与 1 个扇区的备份头),说明备份头正好落在最后一个扇区;再把备份 LBA 改成 0x200000,说明为什么这时的磁盘不合法,而只查头的程序看不出来。最后说明 GPT 头的 CRC32 覆盖哪两部分、固件为什么必须先校验它。此问只需文字,不必实现 CRC。
(3) [5.3] 写一个最小的 EFI 应用 efi_main.c,签名 unsigned long long efi_main(void *image_handle, void *system_table),返回 0,用 MinGW 链接:
gcc -m32 -std=c17 -Wall -Wextra -ffreestanding -nostdlib -Wl,--subsystem,10 -Wl,-e,_efi_main -o BOOTIA32.EFI efi_main.c再编一份 64 位版本(去掉 -m32,入口写 efi_main)。用 objdump -p 记录两份产物的 Subsystem、Magic、AddressOfEntryPoint,说明 --subsystem,10 里的 10 是什么,去掉它之后 PE 头会变成什么。再写 pecheck.c:查 MZ,从 0x3C 读 PE 头偏移,查 PE\0\0,读可选头的 Magic(0x10B 为 PE32、0x20B 为 PE32+)与偏移 68 处的 Subsystem,输出:
machine=0x014C magic=0x10B subsystem=10
PE32, EFI application2
machine=0x8664 magic=0x20B subsystem=10
PE32+, EFI application2
把 32 位那份改名为 BOOTX64.EFI 放进 EFI\BOOT,64 位机器上会发生什么?结论要落到 machine 字段。最后说明为什么内核映像用 ELF、UEFI 引导文件却必须是 PE,两者各由谁解析。
验收:三段输出与题面一致;pecheck 对非 PE 文件返回非 0;第 (1) 问的字节数与 1 MiB 对齐的理由齐全;第 (3) 问分别指出固件与引导器各自的角色。