调试 Bootloader——从 BIOS 加载到 0x7c00 逐步追踪
背景:BIOS 到 Bootloader 发生了什么
真实 PC 上,从上电到 Bootloader 执行,发生了这些事:
- 上电:CPU 从物理地址 0xFFFFFFF0 开始执行(ROM BIOS 区域)
- POST:BIOS 自检(内存大小、键盘、磁盘、显卡)
- 找引导设备:按启动顺序尝试磁盘,读引导扇区
- 加载引导扇区:读设备首扇区到物理地址 0x7C00(512 字节)
- 验证签名:检查引导扇区最后两字节是否是 0x55 0xAA
- 跳转:设置 CS=0x0000、IP=0x7C00,跳转到 Bootloader 第一条指令
Bochs 完整模拟这个过程——你可以在每一步下断点、观察寄存器状态。
Bochs 里追踪这个过程
准备
bochsrc(软盘启动场景):
romimage: file=BIOS-bochs-latest, address=0xfffc0000
vgaromimage: file=VGABIOS-lgpl-latest.bin
cpu: model=pentium, count=1, ips=5000000
memory: guest=4, host=4
floppyA: type=1_44, path="boot.img", status=inserted
boot: floppy
keyboard: keymap=us
mouse: enabled=0
display_library: wx
log: bochsout.txt
panic: action=ask
boot.img 是你写的 512 字节 Bootloader(最后两字节必须是 0x55AA)。
启动 bochsdbg 并下断点
bochs:1> pb 0x7c00 # 物理断点——BIOS 加载 Bootloader 后跳转就停
bochs:2> c
调试器输出:
(0) Breakpoint 1, 0x0000000000007c00 in ???
Next at t=12345
t=12345 是 Bochs 模拟的指令执行计数——你可以通过 t 值的差距估算 BIOS 从启动到加载 Bootloader 跑了多少条指令(通常几万到几十万条,取决于 BIOS 复杂度)。
断点触发后的标准流程
bochs:3> info registers
# 检查关键寄存器:
# CS 应该是 0x0000(BIOS 设置)
# EIP 应该是 0x7c00(BIOS 刚跳过来)
# EAX 应该是 0x0000(实模式下)
# SS:SP 可能是 0x0000:0x7c00(BIOS 通常把栈设在 Bootloader 下方)
bochs:4> u # 反汇编当前指令——这就是 Bootloader 的第一条指令
bochs:5> u 0x7c00 16 # 看看 Bootloader 整体前 16 条指令是什么
bochs:6> xp /512bx 0x7c00 # 看完整引导扇区内容,确认 0x55AA 签名在末尾
bochs:7> xp /2bx 0x7dfe # 直接看最后两字节:应该是 55 aa
逐条追踪 Bootloader 执行
bochs:8> s # 单步执行第一条指令
bochs:9> u # 反汇编新的 EIP 位置
bochs:10> info registers # 看寄存器变化
bochs:11> s # 继续单步...
每次单步后都看 info registers 能实时看到:
- CS/DS/SS/ES 是否被正确设置(Bootloader 通常先把所有段寄存器清零)
- SP 栈指针是否合理
- 如果 Bootloader 有循环,是否正常退出循环
遇到 int 0x10 怎么处理
Bootloader 常用 int 0x10 调 BIOS 中断来写屏(打印字符串)。s 会进入 BIOS 内部(几千行指令),用 n 跳过:
bochs:50> u
0000000000007c3a: int 0x10 # Bootloader 调 BIOS 中断写屏
bochs:51> n # 跳过整个 BIOS 中断处理(几千行内部指令)
bochs:52> u # 看返回后的下一条指令
常见 Bootloader 调试问题
Bootloader 里设置了 DS=0 但访问数据段还是错
实模式下 DS 是段基址(左移 4 位 + 偏移 = 物理地址)。如果你在 DS=0 时写 mov ax, [0x1234],物理地址应该是 0x1234。
但如果 Bootloader 不是从物理 0 开始编译的(比如用 NASM 的 ORG 0x7c00),那数据段偏移要注意:
[ORG 0x7c00] ; NASM 假设代码从 0x7c00 开始
mov ax, cs ; 用 CS 设置 DS
mov ds, ax ; 这样 DS 基址正确 = CS 基址(0x0000)
mov si, hello_msg ; SI 里是相对偏移(比如 0x7c20)
; 实际物理地址 = CS<<4 + SI = 0x0000<<4 + 0x7c20 = 0x7c20
在 Bochs 里验证:info registers cs ds + xp 0x7c20 看是不是你期望的数据。
Bootloader 最后一条 ret/iret 跳错了
Bootloader 调完内核后用 jmp 0x10000 或类似指令跳转。如果跳错地方:
bochs:90> u # 看当前指令
bochs:91> s # 单步执行跳转
bochs:92> u # 看跳到了哪里——是正确的内核入口吗?
如果跳到了 0x0000(NULL 段)或者 BIOS 区域,说明 Bootloader 里的内核加载地址写错了——回去检查磁盘镜像里内核实际写到了什么位置。
BIOS 报 "Boot failed: not a bootable disk"
最常见的问题。检查清单:
- 引导扇区最后两字节(0x7DFE, 0x7DFF)是不是 0x55AA
bochs: xp /2bx 0x7dfe
- 磁盘类型声明对不对(floppyA 还是 ata0-master)
- bochsrc 里 boot: 是否设了对应设备
- 磁盘镜像文件是否真的写入了 512 字节(用 HxD 或 dd 检查)
MIT 6.828 / COS318 等课程的标准起步
OS 课程的实验第一周几乎都是"用 Bochs 跑起一个 Bootloader"。标准流程:
- 写 Bootloader(512 字节,最后两字节 0x55AA)
- 用
bximage -q -create -fd创建软盘 - 用
dd conv=notrunc把 Bootloader 写入软盘 - bochsrc 里 floppyA + boot: floppy
- bochsdbg 启动 →
pb 0x7c00→c→ 开始逐条追踪
这条流程在 Bochs 里能跑通,你就完成了 OS 课程实验一。