3. 断点进阶:条件断点、硬件断点、观察点(watchpoints)、捕获点(catchpoints)

断点这东西,很多人觉得就是「在行号上停一下」。说实话,我刚入行那会儿也这么想。直到有一次,我在调试一个多线程的死锁问题,普通断点根本没法用——每次停下来,线程上下文都变了。那时候我才意识到,GDB 的断点远不止「停一下」这么简单。

今天咱们聊聊断点的进阶玩法。说白了,就是让 GDB 在特定条件下才停,或者数据变化时自动停,甚至捕获事件时停。这些技巧,我在项目重构中几乎天天用。

3.1 条件断点:只在需要的时候停

普通断点有个毛病:每次执行到那一行都会停。如果循环 10 万次,你得按 10 万次 continue,手都按酸了。

条件断点就是解决这个问题的。你告诉 GDB:「当某个条件成立时,再停下来。」

核心语法:

break [行号] if [条件表达式]

举个例子。假设我在重构一个网络库,里面有个循环处理数据包:

for (int i = 0; i < MAX_PACKETS; i++) {
    process_packet(&packets[i]);
}

我只想在第 100 个包出问题时停下来。普通做法?设个断点,然后手动数到 100。我当年就这么干过,数到 50 就忘了……

用条件断点就优雅多了:

(gdb) break 42 if i == 100
(gdb) run

GDB 会自动帮你数,只有 i == 100 时才停。你想想看,这省了多少事。

我的小技巧:条件表达式里可以用任何 C 语言的合法表达式。比如检查指针是否为空:break process_packet if packet == NULL。我在调试内存泄漏时经常这么干。

3.2 硬件断点:对付代码自修改和 Flash 调试

硬件断点,说白了就是利用 CPU 的调试寄存器来实现断点。它跟软件断点(int 3 指令)不一样,不需要修改内存中的指令。

什么时候必须用硬件断点?

  • 代码在 Flash 或 ROM 中:不能写,软件断点插不进去
  • 代码自修改:比如 JIT 编译器,软件断点会被覆盖
  • 调试 Bootloader 或内核早期代码:内存还没初始化好

用法很简单:

(gdb) hbreak main
(gdb) hbreak process_packet if error_count > 5

注意,硬件断点数量有限。x86 架构一般只有 4 个调试寄存器,所以最多同时设 4 个硬件断点。ARM 也类似。

我曾经踩过的坑:有一次在嵌入式设备上调试,设了 5 个硬件断点,结果 GDB 没报错,但第 5 个断点根本不生效。排查了半天才发现是硬件限制。所以,用 info break 检查一下断点类型,确认是不是 hw breakpoint。

3.3 观察点(watchpoints):数据变了就停

观察点是我个人最喜欢的调试手段。它不关心代码执行到哪一行,只关心某个内存地址的值是否被修改。

想象一下这个场景:你有一个全局变量 g_config,不知道被谁改了。用普通断点?你得在所有可能修改它的地方都设断点,太累了。

用观察点:

(gdb) watch g_config

只要 g_config 的值发生变化,GDB 就会自动停下来,并告诉你哪行代码改的。

观察点有三种:

命令 含义 适用场景
watch expr 值被写入时停 变量被意外修改
rwatch expr 值被读取时停 谁在读这个变量
awatch expr 读写都停 全面监控

举个例子。我在重构一个内存池时,发现某个块被重复释放了。用 watch pool->blocks[i].in_use,当 in_use 从 1 变成 0 时自动停,一下子就抓到了那个多调了一次 free 的代码。

注意:观察点本质上是硬件断点的一种实现。所以数量也有限,而且会拖慢程序执行速度(每条指令都要检查)。我一般只在定位关键 bug 时才用。

3.4 捕获点(catchpoints):捕获事件而非位置

捕获点跟断点不同。断点是「停在某一行代码」,捕获点是「停在某个事件发生时」。

GDB 支持捕获的事件包括:

  • C++ 异常:catch throw、catch catch
  • 系统调用:catch syscall [名称或编号]
  • 信号:catch signal [信号名]
  • fork/exec:catch fork、catch exec
  • 加载动态库:catch load [库名]

我举个实际例子。有一次在调试一个服务器程序,它莫名其妙地调用了 exit()。我不知道谁调的,也不知道为什么。用普通断点?exit 是库函数,没法直接设断点。

用捕获点:

(gdb) catch syscall exit_group
(gdb) run

程序一调用 exit_group 系统调用,GDB 就停了。我看了调用栈,发现是某个错误处理路径里多写了一个 exit(1)。嗯,三分钟定位问题。

捕获点 vs 断点:

  • 断点:停在代码的位置
  • 捕获点:停在事件发生的那一刻

两者可以配合使用。比如先设一个 catch throw 捕获所有异常,再设条件断点过滤掉不关心的异常类型。

3.5 实战:用观察点定位野指针

讲个我亲身经历的故事。有一次,一个 C 语言项目在重构后频繁崩溃。崩溃位置不固定,有时在字符串处理,有时在内存释放。典型的野指针症状。

我怀疑是某个全局指针被意外修改了。于是:

(gdb) watch *((void**)0x7ffff7a0b040)
# 假设这个地址是全局指针变量的地址

程序跑起来后,很快就停了。GDB 告诉我,是 memset 把这块内存给清零了。我一看代码,原来有个结构体初始化时,不小心把指针变量也覆盖了。

修复方法很简单:调整结构体成员的初始化顺序,或者用 memcpy 只复制需要的字段。

你看,如果没有观察点,这种 bug 可能要排查好几天。有了它,几分钟就搞定了。

3.6 小结

断点进阶的核心就四个字:精准停住。

  • 条件断点:在特定条件下停,适合循环和大量重复调用
  • 硬件断点:不能改代码时用,比如 Flash 调试
  • 观察点:数据变化时停,适合定位内存被意外修改
  • 捕获点:事件发生时停,适合异常、系统调用等场景

我个人建议,把这四种断点都记在脑子里。调试时,先想想「我想在什么条件下停」,然后选最合适的工具。别一上来就设普通断点,那是最笨的办法。

最后提醒一句:断点设多了,程序会跑得很慢。尤其是观察点,每条指令都要检查。我一般只同时开 1-2 个观察点,定位到问题后就删掉。

GDB 断点进阶知识体系 断点进阶 条件断点 硬件断点 观察点 捕获点 break if 条件 循环/重复调用 hbreak 命令 Flash/ROM 调试 watch/rwatch/awatch 数据变化监控 catch throw/syscall 事件驱动调试 核心思想:精准停住,而非盲目打断

可可精品资料库