bochs 中文文档 下载 App

调试 Bootloader——从 BIOS 加载到 0x7c00 逐步追踪

背景:BIOS 到 Bootloader 发生了什么

真实 PC 上,从上电到 Bootloader 执行,发生了这些事:

  1. 上电:CPU 从物理地址 0xFFFFFFF0 开始执行(ROM BIOS 区域)
  2. POST:BIOS 自检(内存大小、键盘、磁盘、显卡)
  3. 找引导设备:按启动顺序尝试磁盘,读引导扇区
  4. 加载引导扇区:读设备首扇区到物理地址 0x7C00(512 字节)
  5. 验证签名:检查引导扇区最后两字节是否是 0x55 0xAA
  6. 跳转:设置 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 能实时看到:

遇到 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"

最常见的问题。检查清单:

  1. 引导扇区最后两字节(0x7DFE, 0x7DFF)是不是 0x55AA
   bochs: xp /2bx 0x7dfe
  1. 磁盘类型声明对不对(floppyA 还是 ata0-master)
  2. bochsrc 里 boot: 是否设了对应设备
  3. 磁盘镜像文件是否真的写入了 512 字节(用 HxD 或 dd 检查)

MIT 6.828 / COS318 等课程的标准起步

OS 课程的实验第一周几乎都是"用 Bochs 跑起一个 Bootloader"。标准流程:

  1. 写 Bootloader(512 字节,最后两字节 0x55AA)
  2. 用 bximage -q -create -fd 创建软盘
  3. 用 dd conv=notrunc 把 Bootloader 写入软盘
  4. bochsrc 里 floppyA + boot: floppy
  5. bochsdbg 启动 → pb 0x7c00 → c → 开始逐条追踪

这条流程在 Bochs 里能跑通,你就完成了 OS 课程实验一。