GDB核心命令:启动、暂停、继续、单步执行与程序状态查看
调试器这东西,说白了就是让你能「看」进程序内部。GDB 作为嵌入式调试的标配,它的核心命令并不多,但每个都值得你花时间吃透。我刚开始用 GDB 时,就靠这几个命令走天下,后来发现很多同事连 step 和 next 的区别都说不清楚——嗯,这其实是个大坑。
2.1 启动调试:让程序跑起来
启动调试的方式主要有三种,我根据项目场景不同会换着用。
| 启动方式 | 命令示例 | 适用场景 |
|---|---|---|
| 直接调试可执行文件 | gdb ./app.elf |
本地调试,最常见 |
| 附加到运行中的进程 | gdb attach <pid> |
调试已跑起来的服务,不重启 |
| 远程调试(嵌入式常用) | target remote :2331 |
板子上跑 gdbserver,PC 端连过来 |
gdb ./app.elf 加载符号表,再通过 target remote 连目标板。这样即使板子还没启动,也能先看源码、设断点。
启动后,GDB 会停在 _start 或 main 之前。如果你想直接跑到 main 再停,可以用:
(gdb) break main
(gdb) run
嵌入式场景下,run 通常不好使——因为程序已经在板子上跑了。这时候用 continue 代替。
2.2 暂停与继续:掌控执行节奏
暂停程序靠断点,继续执行靠 continue。这俩是调试的「油门」和「刹车」。
2.2.1 设置断点
(gdb) break main // 在函数入口设断点
(gdb) break file.c:42 // 在源码行号设断点
(gdb) break *0x80002000 // 在地址设断点(硬件断点常用)
(gdb) info breakpoints // 查看所有断点
我曾经在调试一个 UART 驱动时,发现中断服务函数总是不触发。折腾了半天,最后用 break *0x80002000 直接在中断向量表地址设断点,才发现是中断向量配置错了。嗯,硬件断点在这种场景下特别好用。
2.2.2 继续执行
(gdb) continue // 简写 c,继续跑直到下一个断点
(gdb) continue 5 // 忽略当前断点 5 次,第 6 次才停
continue 后程序会全速运行。如果断点设得太频繁,可能会影响实时性。我建议只在关键路径上设断点。
2.3 单步执行:逐行看程序怎么走
单步执行是调试的「显微镜」。GDB 提供了四个核心命令,我列个表对比一下:
| 命令 | 简写 | 行为 | 遇到函数调用时 |
|---|---|---|---|
step |
s |
执行一行源码 | 进入函数内部 |
next |
n |
执行一行源码 | 不进入函数,直接跳过 |
finish |
fin |
一直执行到当前函数返回 | 跳出函数 |
until |
u |
执行到指定行或地址 | 灵活跳出循环 |
为什么会这样设计?你想想看:
- 如果你在
main里调了uart_send(),用next会直接跳过这个函数,不关心内部细节。 - 用
step则会钻进uart_send()里,一行一行看它怎么发数据的。 - 如果已经钻进去了,想快速出来,就用
finish。
step 进入了调度器函数,结果 GDB 卡住了——因为调度器会切换上下文,单步执行时 PC 指针乱跳。后来我改用 next 跳过调度器,只在任务函数内部设断点,问题就解决了。
2.4 程序状态查看:看变量、寄存器、内存
暂停下来之后,你得知道程序「现在是什么状态」。GDB 提供了三组查看命令:
2.4.1 查看变量
(gdb) print var_name // 打印变量值
(gdb) print/x var_name // 十六进制显示
(gdb) display var_name // 每次暂停都自动打印
(gdb) info locals // 查看当前函数所有局部变量
我个人习惯用 display 来监控关键变量。比如调试一个环形缓冲区时,我会把 head、tail、count 都设成自动显示,这样每次断点停下就能看到队列状态。
2.4.2 查看寄存器
(gdb) info registers // 查看所有寄存器
(gdb) info registers r0 r1 // 只看 r0 和 r1
(gdb) print $pc // 查看程序计数器
(gdb) print $sp // 查看栈指针
嵌入式调试中,寄存器查看特别重要。我记得有一次调试 I2C 通信,从设备一直没应答。我用 info registers 看了状态寄存器,发现 ACK 位始终为 0,这才定位到是硬件上拉电阻没焊好。
2.4.3 查看内存
(gdb) x/10x 0x20000000 // 查看地址 0x20000000 开始的 10 个十六进制字
(gdb) x/20b 0x20000000 // 按字节显示
(gdb) x/5i $pc // 反汇编当前 PC 附近的 5 条指令
x 命令的格式是 x/<数量><格式> <地址>。格式可以是:
x十六进制d十进制c字符s字符串i指令
x/64x $sp 查看栈顶附近的数据。如果看到栈空间被写入了大量 0xDEADBEEF 之类的填充值,基本就能确认溢出了。
2.5 知识体系总览
下面这张图把本章的核心命令和它们的关系串起来了。你可以把它当作调试时的「快捷键地图」。
2.6 实战组合拳:一个典型调试流程
说了这么多,不如看一个实际例子。假设你在调试一个串口发送函数 uart_send(),发现数据发出去不对:
- 启动并设断点:
gdb ./app.elf→break uart_send - 跑起来:
continue(或target remote后直接c) - 暂停到断点:程序停在
uart_send入口 - 查看参数:
print buf、print len,确认传入的数据对不对 - 单步执行:
next跳过函数内部,或者step钻进函数看细节 - 查看寄存器:
info registers看 UART 状态寄存器 - 查看内存:
x/16x buf看缓冲区实际内容 - 继续或跳出:
continue跑下一个断点,或finish跳出当前函数
print 和 info registers 收集证据,再用 step 或 next 缩小范围,最后用 continue 验证修复效果。