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。如果是,打印调用栈并继续。否则直接继续,你甚至感觉不到断点的存在。

注意: 硬件断点数量有限(通常 4-8 个),别滥用。调试 Flash 中的代码时,软件断点写不进去,必须用 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 的那一行。原来是一个中断服务程序里的指针越界,把数据写到了隔壁的地址上。这种问题,不用观察点,你排查三天都未必找得到。

核心要点: 观察点依赖硬件调试模块(如 ARM 的 DWT)。如果硬件不支持,GDB 会用软件模拟,但速度会慢 100 倍以上。嵌入式调试时,优先用硬件观察点。

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 都要停下来评估条件。如果条件表达式很复杂,或者断点在一个高频调用的函数里,程序会慢得像蜗牛。

避坑指南: 我曾经在一个 10kHz 的中断服务程序里设了条件断点,结果程序直接跑飞了——因为 GDB 根本来不及处理。后来我改用硬件断点 + 条件触发,才解决问题。

怎么优化?两个思路:

  1. 用硬件条件断点:部分调试器支持硬件条件断点,不依赖 GDB 软件评估。
  2. 用临时变量 + 普通断点:在代码里加一个 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 把数据导出来。

小技巧: 我习惯在 Core Dump 里包含一个 Magic Number,用来验证数据的完整性。如果 Magic Number 不对,说明 Core Dump 已经损坏,分析结果不可信。

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(链接寄存器)的值。
  • 多线程/多任务:每个线程/任务都有自己的栈。你需要先切换到对应的线程,再分析。
一句话总结: 断点管位置,观察点管变量,条件断点管时机,Core Dump 管事后。四者结合,没有调不了的 Bug。

4.5 本章知识体系

下面这张图,帮你理清本章的核心逻辑:

GDB 高级调试技巧知识体系 GDB 高级调试技巧知识体系 断点管理 观察点 条件断点 Core Dump 普通/临时/硬件断点 写/读/读写观察点 条件表达式/性能优化 生成/加载/分析 核心原则 断点管位置 → 观察点管变量 → 条件断点管时机 → Core Dump 管事后

这四种技巧,不是孤立的。实际调试时,我经常组合使用。比如:先用 Core Dump 定位崩溃位置,再用条件断点缩小范围,最后用观察点锁定罪魁祸首。嗯,这才是高级调试的正确姿势。