3. 断点进阶
断点这东西,很多人觉得就是「在行号上停一下」。说实话,我刚入行那会儿也这么想。直到有一次,我在调试一个多线程的死锁问题,普通断点根本没法用——每次停下来,线程上下文都变了。那时候我才意识到,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 个观察点,定位到问题后就删掉。
可可精品资料库