单步执行与继续——step、next、fin 和控制流
四条控制命令
| 命令 | 含义 | 类比 GDB | 行为 |
|---|---|---|---|
s(或 step) | 单步 | si(step instruction) | 执行一条指令,停住 |
n(或 next) | 单步跳过 | ni(next instruction) | 执行一条指令,但如果是 CALL/INT 就跳过整个被调过程 |
fin(或 step out) | 跳出 | finish | 从当前 CALL 中返回 |
c(或 continue) | 继续 | c | 一直跑到下一个断点或 Ctrl+C |
单步 vs 单步跳过:区别在哪
假设你在调试一个 Bootloader,里面有一个 call print_msg:
用 s(step)
bochs:1> s
# 进入 print_msg 的第一条指令
bochs:2> u
0000000000007c50: push bp
0000000000007c51: mov bp, sp
...
你会一步一步跟随 CALL 进入被调函数内部,可以调试被调函数里的每一步。
用 n(next)
bochs:1> n
# 直接跨过 call,停在 call 后面那条指令
bochs:2> u
0000000000007c40: mov ax, 0x7c00
0000000000007c43: ret
call 里面的代码被完整执行完了(包括 print_msg 的 push/mov/pop/ret),但你停在 call 返回后的位置。适合"我不想进这个被调函数,只想看 call 返回后发生了什么"。
类比
| 场景 | 用 |
|---|---|
| 想深入理解某个函数内部做了什么 | s 跟踪进入 |
| 不想进某个库函数(比如 BIOS interrupt) | n 跳过 |
| 调试中断处理 | s(因为 int 不能被 n 跳过) |
fin:跳出当前函数
当你用 s 跟踪进入一个 CALL 后,如果想快速返回到 CALL 发起者那里:
bochs:1> fin
# Bochs 会在栈上找返回地址,执行完当前函数后停住
fin 的实现原理:Bochs 会在当前栈帧的返回地址处设一个临时断点,然后 continue。当被调函数执行 ret 时,返回地址就是断点位置——自动停住并删除临时断点。
Ctrl+C:随时打断执行
c 继续执行时,按 Ctrl+C 可以随时中断执行、回到调试器提示:
bochs:1> c
# ... Bochs 正在运行内核 ...
# Ctrl+C
bochs:2>
Bochs 会在当前指令执行完后停住(不是在指令中间)。你可以用 u 看看刚才 CPU 在跑什么,用 info registers 看看当时的状态。
如果 Bochs 不响应 Ctrl+C(模拟卡死掉了),关闭模拟窗口是终极手段——bochsrc 里设了 panic: action=ask 的话,panic 事件也会自动进入调试器。
单步陷阱(TF 标志位)
CPU 的 TF(Trap Flag,EFLAGS bit 8)= 1 时,每条指令执行完后都会自动触发一个 #DB(Debug Exception)。Bochs 调试器用这个机制实现了单步功能——s 命令本质上是设置 TF 后 continue,#DB 异常触发时停住。
OS 内核也可能用 TF 标志做自己的事情——比如调试子系统、性能分析。如果内核启动后 TF 被设置,每条指令都会触发异常,Bochs 单步会"每次跑一条异常"。这种情况:
bochs:1> info flags
iopl=0 nv up ei pl nz na po nc
↑ TF 位为 1
bochs:2> set $tf = 0 # 关掉 TF
一条经典的单步调试流程
OS 开发者调试 Bootloader 的标准流程:
bochs:1> pb 0x7c00 # 在引导扇区入口下断点
bochs:2> c # 继续执行,BOIS 加载引导扇区后跳转
# 断点触发:
bochs:3> u # 反汇编看看是什么指令
bochs:4> info registers # 看 CS/IP/SS/SP/FLAGS 是否符合预期
bochs:5> s # 单步执行第一条指令
bochs:6> s # 再单步... 逐步理解 Bootloader 逻辑
bochs:7> ...
# 遇到 int 0x10(BIOS 中断)时,用 s 还是 n?
bochs:20> n # 用 n 跳过 BIOS 内部(BIOS 内部几千行指令,不关心)
这条流程让你能逐步"看到"Bootloader 从第一条 jmp 到最后的进入内核,每一步 CPU 寄存器、内存的变化都在你眼前——这是 OS 课程实验的核心。
小结
| 想做什么 | 命令 |
|---|---|
| 跑起来到下一个断点 | c |
| 执行一条指令(进入 CALL) | s |
| 执行一条指令(跳过 CALL) | n |
| 从当前 CALL 返回 | fin |
| 随时中断 | Ctrl+C |