中断与异常追踪——在 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 | 键盘服务(读按键) |
| 0x1A | RTC 时间服务 |
在 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
如果断点没触发:
- 检查内核是不是正确设置了 IDT(
info sreg看 IDTR) - 检查 IRQ1 是不是被 PIC 屏蔽了(
xp /2bx 0x21看 PIC IMR) - 检查内核是不是在运行前开了中断(EFLAGS.IF = 1)
异常处理:#PF page fault 是常客
内核开分页后经常遇到 page fault——地址翻译失败,CPU 自动触发 #PF(异常号 0x0E)。异常比硬件中断多一个 error code(压在栈上),告诉你失败原因:
| error code 位 | 含义 |
|---|---|
| bit 0 | 0 = 页面不存在,1 = 权限错误 |
| bit 1 | 0 = 读,1 = 写 |
| bit 2 | 0 = 内核态,1 = 用户态 |
| bit 3 | 0 = 未保留位,1 = reserved bit |
| bit 4 | 0 = 外部,1 = page fault during instruction fetch |
| bit 5 | 0 = 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 地址)