4. GDB 高级调试技巧:断点管理、观察点、条件断点、Core Dump 分析
嵌入式调试,说白了就是跟 Bug 斗智斗勇。GDB 这工具,很多人只会用个 b main 和 n,遇到复杂问题就抓瞎。我做了十几年嵌入式,见过太多人拿着 GDB 当玩具用,其实它的高级功能才是真正的杀手锏。
这一章,咱们就聊聊那些真正能救命的技巧。断点怎么管、观察点怎么设、条件断点怎么用、Core Dump 怎么分析。嗯,都是实战中摸爬滚打出来的经验。
4.1 断点管理:别只会设,要学会管
断点不是设得越多越好。我见过有人一口气设了 20 个断点,结果程序跑起来跟蜗牛一样,自己都忘了断点在哪。断点管理,核心就四个字:精准、高效。
4.1.1 断点的分类与设置
| 断点类型 | 命令 | 适用场景 |
|---|---|---|
| 普通断点 | b function_name |
在函数入口处停下 |
| 行号断点 | b file.c:123 |
在指定行停下 |
| 临时断点 | tb function_name |
只停一次,自动删除 |
| 硬件断点 | hbreak addr |
调试 ROM/Flash 代码 |
我个人习惯,调试循环或中断服务程序时,优先用 临时断点。为什么呢?因为普通断点每次进循环都停,你按 c 按到手抽筋。临时断点停一次就消失,干净利落。
info breakpoints 查看所有断点状态。用 disable 和 enable 临时开关断点,别动不动就删。
4.1.2 断点的高级管理
我在项目中遇到过一种情况:一个函数被多个地方调用,我只想在某次特定调用时停下。怎么办?
用 断点命令列表(commands)!
(gdb) b motor_control
(gdb) commands
> silent
> if call_count == 5
> bt
> continue
> end
> end
这段代码的意思是:每次停在 motor_control 时,先静默执行,检查 call_count 是否等于 5。如果是,打印调用栈并继续。否则直接继续,你甚至感觉不到断点的存在。
hbreak。
4.2 观察点:盯住变量的变化
有时候,你不需要知道程序停在哪一行,而是想知道某个变量什么时候被改了。比如,一个全局变量莫名其妙被篡改,你设断点根本抓不到。这时候,观察点就派上用场了。
4.2.1 观察点的三种模式
| 模式 | 命令 | 触发条件 |
|---|---|---|
| 写观察点 | watch var |
变量被写入时触发 |
| 读观察点 | rwatch var |
变量被读取时触发 |
| 读写观察点 | awatch var |
变量被读或写时触发 |
我曾经调试过一个电机控制项目,电流环的 current_ref 变量总是莫名其妙变成 0。设断点根本抓不到,因为修改它的代码路径太多了。后来我用了一个写观察点:
(gdb) watch current_ref
Hardware watchpoint 1: current_ref
(gdb) continue
程序一跑,GDB 立刻停在了修改 current_ref 的那一行。原来是一个中断服务程序里的指针越界,把数据写到了隔壁的地址上。这种问题,不用观察点,你排查三天都未必找得到。
4.2.2 观察点的局限性
观察点不是万能的。有几点你得注意:
- 数量有限:硬件观察点通常只有 2-4 个。
- 不能监控局部变量:函数返回后,局部变量的地址会被回收,观察点失效。
- 对结构体成员的支持:有些 GDB 版本不支持
watch struct.member,需要先取地址。
嗯,这里有个坑。如果你要监控一个结构体成员,最好这样写:
(gdb) watch *(int*)&motor.status
直接取地址,告诉 GDB 监控那个内存地址。这样最可靠。
4.3 条件断点:精准打击
条件断点,说白了就是带 if 的断点。它只在条件满足时才停下,其他时候直接放行。这玩意儿用好了,调试效率能翻倍。
4.3.1 基本用法
(gdb) b pid_controller if error > 100
这条命令的意思是:只在 error 大于 100 时,停在 pid_controller 函数入口。其他时候,程序照常运行。
我建议你在调试控制算法时,多用条件断点。比如:
- 监控 超调量:
b update_output if output > target * 1.2 - 监控 积分饱和:
b integrator if integral > INTEGRAL_LIMIT - 监控 异常状态:
b state_machine if current_state == FAULT
4.3.2 条件断点的性能问题
条件断点有个大坑:慢。每次经过断点时,GDB 都要停下来评估条件。如果条件表达式很复杂,或者断点在一个高频调用的函数里,程序会慢得像蜗牛。
怎么优化?两个思路:
- 用硬件条件断点:部分调试器支持硬件条件断点,不依赖 GDB 软件评估。
- 用临时变量 + 普通断点:在代码里加一个
if (condition) { dummy = 1; },然后在dummy上设普通断点。
4.4 Core Dump 分析:事后诸葛亮
有些 Bug 是偶发的,跑几个小时才出现一次。你总不能一直盯着屏幕吧?这时候,Core Dump 就是你的救命稻草。
Core Dump 是程序崩溃时,操作系统把内存、寄存器、堆栈等信息保存下来的快照。你可以事后用 GDB 加载它,像调试活程序一样分析死因。
4.4.1 生成 Core Dump
嵌入式 Linux 下,默认可能不生成 Core Dump。你需要先设置:
# 设置 Core Dump 文件大小不限
ulimit -c unlimited
# 设置 Core Dump 文件名格式(可选)
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
如果是裸机或 RTOS 环境,你需要自己实现一个 异常捕获机制。比如在 HardFault_Handler 里,把寄存器压栈,然后通过串口或 Flash 把数据导出来。
4.4.2 分析 Core Dump
拿到 Core Dump 文件后,用 GDB 加载:
arm-none-eabi-gdb my_program.elf -c core.1234
然后,用以下命令快速定位问题:
| 命令 | 作用 |
|---|---|
bt |
查看崩溃时的调用栈 |
info registers |
查看寄存器值 |
frame N |
切换到第 N 层栈帧 |
list |
查看崩溃附近的源代码 |
print var |
打印变量的值 |
我记得有一次,客户反馈设备运行 3 小时后死机。我拿到 Core Dump 后,先用 bt 看调用栈,发现程序死在了 malloc 里。再 info registers 一看,堆栈指针 SP 指向了一个非法地址。嗯,典型的堆栈溢出。
顺着调用栈往上查,发现是一个递归函数没有终止条件,把堆栈撑爆了。这种问题,没有 Core Dump,你根本无从下手。
4.4.3 Core Dump 分析的常见陷阱
- 优化后的代码:如果编译时开了
-O2或-O3,GDB 显示的代码行号可能不准确。建议调试版用-O0 -g。 - 栈被破坏:如果堆栈被严重破坏,
bt可能显示不全。这时候可以手动查看$lr(链接寄存器)的值。 - 多线程/多任务:每个线程/任务都有自己的栈。你需要先切换到对应的线程,再分析。
4.5 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
这四种技巧,不是孤立的。实际调试时,我经常组合使用。比如:先用 Core Dump 定位崩溃位置,再用条件断点缩小范围,最后用观察点锁定罪魁祸首。嗯,这才是高级调试的正确姿势。