bochs 中文文档 下载 App

中断与异常追踪——在 Bochs 里观察 PIC/APIC 和中断处理完整路径

x86 中断/异常的完整路径

硬件中断 / CPU 异常
    │
    ▼
[1] PIC / APIC 接收中断信号
    │
    ▼
[2] PIC/APIC 检查 IMR(Interrupt Mask Register)
    │    └─ 如果没屏蔽 → 向 CPU 发送 INTR 信号 + 中断号
    │
    ▼
[3] CPU 响应:
    │    a. 如果是硬件中断:发 INTA 周期,读 PIC/APIC 的 ISR 拿到中断号
    │    b. 如果是异常:CPU 内部自己知道异常号
    │
    ▼
[4] CPU 查 IDTR → 定位 IDT 基址
    │
    ▼
[5] CPU 查 IDT 里对应门描述符(按中断号偏移)
    │    └─ 拿到 ISR 入口地址、段选择子、特权级
    │
    ▼
[6] CPU 切换栈(如果是跨特权级中断)
    │    └─ 从 TSS 加载新 SS:ESP
    │
    ▼
[7] CPU 压入旧状态到新栈:
    │    └─ 旧 SS, 旧 ESP, EFLAGS, 旧 CS, 旧 EIP
    │    └─ (某些异常还要压入 error code)
    │
    ▼
[8] CPU 跳转到 ISR(中断服务例程)执行
    │
    ▼
[9] ISR 处理中断 → 发 EOI(End of Interrupt)给 PIC/APIC
    │
    ▼
[10] ISR 执行 IRET → CPU 从栈弹出状态 → 返回被中断的代码

Bochs 完整模拟了这个路径——OS 内核开发者可以在每一步设断点观察行为。

配置:bochsrc 里中断相关

PIC/APIC 在 Bochs 里默认启用(不需要额外配置)。相关关键字:

# bochsrc 里
cpu: ... reset_on_triple_fault=0    # triple fault 时停住调试
...
# 网络/声卡如果开了,会产生硬件中断
ne2k: enabled=1, mac=00:40:3c:00:00:01, ethmod=slirp
sb16: enabled=1

OS 开发建议不开网络和声卡——避免它们产生不可控的中断干扰调试。如果需要一个可控的中断源,键盘(IRQ 1)是最好的选择——按一下键盘就触发一次中断,节奏完全由你控制。

断点位置:从 PIC 到 ISR

位置一:PIC 收到中断信号

Bochs 调试器可以在 PIO 端口设 watchpoint:

bochs:1> watch write 0x20       # PIC 8259 主片命令端口
bochs:2> watch write 0x21       # PIC 8259 主片数据端口

当内核初始化 PIC(写 0x20/0x21 发送初始化命令)时,调试器会停住。

位置二:CPU 进入中断向量号对应的处理

最实用的断点——IDT 里 ISR 的入口地址:

bochs:3> vb <你的 ISR 入口地址>   # 比如 0xc0102000(内核里的 IRQ1 handler)
bochs:4> c                       # 继续
# 当硬件触发中断(比如按键盘)→ 调试器停在 ISR 入口

位置三:IRET 返回

bochs:5> # ISR 末尾是 IRET 指令
bochs:6> s                       # 单步到 IRET
bochs:7> u                       # 确认是 IRET
bochs:8> s                       # 执行 IRET
bochs:9> info registers          # 检查返回后的状态:CS/EIP/EFLAGS 是否正确恢复

实模式下的中断(BIOS)

实模式下不用 IDT——用 IVT(Interrupt Vector Table)在物理地址 0x0 ~ 0x3FF。每个中断号占 4 字节(IP + CS)。

Bootloader 里常用的 BIOS 中断:

中断号功能
0x10视频服务(写屏、设置显示模式)
0x13磁盘服务(读扇区、写扇区)
0x15系统服务(查询内存、A20 控制)
0x16键盘服务(读按键)
0x1ARTC 时间服务

在 Bochs 里追踪 BIOS 中断:

bochs:1> pb 0x10                 # 追踪 int 0x10(不精确,因为 int 指令的断点是 CPU 进入中断处理时触发)
# 更好的方式:在 IVT 里看 int 0x10 的 ISR 地址
bochs:2> xp /4bx 0x10*4          # 中断向量 0x10 在 IVT 里的位置(物理 0x40)
# 读取 IVT 得到 ISR 地址(低字 IP + 高字 CS)
bochs:3> pb <得到的 ISR 物理地址>

BIOS 中断处理程序在 ROM BIOS 里(0xF0000 ~ 0xFFFFF),调试器里可以 u 反汇编 BIOS 代码。

OS 开发场景:调试 keyboard handler

内核写好 keyboard IRQ handler 后,想验证它能被正确触发:

bochs:1> vb <keyboard handler 入口>   # 比如 IRQ1 handler
bochs:2> c                             # 继续运行内核
# 在 Bochs 模拟窗口里按任意键
# → 调试器停在 IRQ1 handler 入口
bochs:3> info registers                # 看 CS/EIP/ESP/EFLAGS
bochs:4> u                             # 反汇编 handler 第一条指令
bochs:5> s                             # 单步执行
...
bochs:N> u                             # 到 handler 末尾看 IRET

如果断点没触发:

  1. 检查内核是不是正确设置了 IDT(info sreg 看 IDTR)
  2. 检查 IRQ1 是不是被 PIC 屏蔽了(xp /2bx 0x21 看 PIC IMR)
  3. 检查内核是不是在运行前开了中断(EFLAGS.IF = 1)

异常处理:#PF page fault 是常客

内核开分页后经常遇到 page fault——地址翻译失败,CPU 自动触发 #PF(异常号 0x0E)。异常比硬件中断多一个 error code(压在栈上),告诉你失败原因:

error code 位含义
bit 00 = 页面不存在,1 = 权限错误
bit 10 = 读,1 = 写
bit 20 = 内核态,1 = 用户态
bit 30 = 未保留位,1 = reserved bit
bit 40 = 外部,1 = page fault during instruction fetch
bit 50 = not 1G页,1 = 1G页

调试 #PF:

bochs:1> vb <page fault handler 入口>
bochs:2> c
# page fault 触发
bochs:3> info registers
# CR2 里是引起 page fault 的虚拟地址!
bochs:4> info cr
cr2=0xc0100008
bochs:5> x /16lx 0xc0100000    # 检查这个地址的页表

CR2(Page Fault Linear Address Register)专门记录引起 page fault 的那个虚拟地址——这是 x86 硬件给内核的免费信息,调试时直接看 info cr 就能知道是哪个地址的翻译出了问题。

triple fault:中断链最终冒泡

如果内核的 page fault handler 又触发了 page fault(或 double fault handler 也炸了),会冒泡成 triple fault。默认 Bochs 会重启——改成停住调试:

cpu: ... reset_on_triple_fault=0

triple fault 停住后:

bochs:1> info registers   # 看 EIP/EAX/CR2/CR3——崩溃瞬间的现场
bochs:2> info cr          # 特别注意 CR2(page fault 地址)