4. 观察点与捕获点

调试内存问题,说白了就是跟「谁改了我的数据」作斗争。我早年做嵌入式驱动时,有个全局变量莫名其妙被改掉,查了三天没结果。后来用了一个工具,十分钟定位——就是 GDB 的观察点(watchpoint)。

这一章,我们就聊聊观察点和捕获点。这两个东西用好了,能让你少掉不少头发。

4.1 观察点:盯住变量的「一举一动」

断点(breakpoint)是停在某一行代码。观察点(watchpoint)是停在某个变量被访问的那一刻。你想想看,如果你想知道一个变量什么时候被改,难道要一行行设断点?太累了。观察点就是干这个的。

4.1.1 watch:监视写入

watch 是最常用的观察点。它监视一个变量或内存地址,一旦有写入操作,GDB 就会停下来。

(gdb) watch my_var
Hardware watchpoint 1: my_var
(gdb) continue
Continuing.
Hardware watchpoint 1: my_var

Old value = 0
New value = 42
main () at test.c:10
10	    printf("my_var is now %d\n", my_var);

嗯,这里要注意:GDB 会告诉你旧值和新值。这个信息在排查「谁把值改错了」时特别有用。

我的习惯:调试指针或数组越界时,我经常用 watch *(int*)0x7fffffffde34 这种形式,直接监视某个地址。不过要确保地址有效,否则 GDB 会报错。

4.1.2 rwatch:监视读取

rwatch 监视变量的读取操作。什么时候用?比如你怀疑某个变量被频繁读取导致性能问题,或者你想知道哪些代码路径访问了这个变量。

(gdb) rwatch my_flag
Hardware read watchpoint 2: my_flag
(gdb) continue
Continuing.
Hardware read watchpoint 2: my_flag

Value = 1
0x000055555555515a in check_flag () at test.c:22
22	    if (my_flag) {

我个人项目中用得不多,但在分析锁竞争或共享变量时,rwatch 能帮你画出「谁在读」的完整地图。

4.1.3 awatch:监视读写

awatch 是读写通杀。说白了就是 watch + rwatch 的合体。一旦变量被读或写,都会触发。

(gdb) awatch shared_data
Hardware access (read/write) watchpoint 3: shared_data
我曾经踩过的坑:awatch 在嵌入式平台上容易触发硬件观察点数量限制。ARM Cortex-M 系列通常只有 2-4 个硬件观察点寄存器。一旦用完,GDB 会回退到软件模拟,调试速度会慢到让你怀疑人生。

4.2 观察点的底层原理与限制

观察点依赖硬件调试寄存器。x86 有 DR0-DR3 四个调试寄存器,ARM 有 DBGBCR 等。说白了,硬件观察点数量有限,而且不能监视局部变量(因为局部变量在栈上,地址会变)。

特性 硬件观察点 软件观察点
速度 快(硬件级) 慢(单步模拟)
数量限制 通常 2-4 个 无限制
支持范围 全局/静态变量 任意地址
跨平台兼容 依赖芯片 通用
核心要点:能用硬件观察点就用硬件观察点。如果 GDB 提示「Could not insert hardware watchpoint」,说明资源用完了。这时候可以删除一些不用的观察点,或者改用条件观察点减少触发次数。

4.3 捕获点:异常与系统调用的「守门员」

捕获点(catchpoint)跟观察点不同。它不盯变量,而是盯事件。比如异常、系统调用、信号、C++ 异常等。

4.3.1 catch syscall:跟踪系统调用

调试底层代码时,我经常用 catch syscall 来监控某个系统调用。比如你想知道程序什么时候调用了 open:

(gdb) catch syscall open
Catchpoint 4 (syscall 'open' [2])
(gdb) continue
Continuing.
Catchpoint 4 (call to syscall open), 0x00007ffff7af1d7a in open () at ../sysdeps/unix/syscall-template.S:120

为什么会用到这个?我记得有一次调试一个文件描述符泄漏的问题,就是靠 catch syscall 抓到了所有 open 和 close 的调用栈,一对比就发现少了一次 close。

4.3.2 catch throw / catch catch:C++ 异常调试

C++ 异常调试是很多人的噩梦。用 catch throw 可以在异常被抛出的那一刻停下来:

(gdb) catch throw
Catchpoint 5 (exception thrown)
(gdb) run
Starting program: ./a.out
Catchpoint 5 (exception thrown), __cxa_throw () at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:78
78	{
(gdb) bt
#0  __cxa_throw () at ...
#1  0x00005555555551f4 in divide (a=10, b=0) at test.cpp:15
#2  0x0000555555555230 in main () at test.cpp:22

你看,直接定位到了除零异常的位置。不用再满世界找 try-catch 了。

4.3.3 catch signal:捕获信号

信号处理也是调试难点。用 catch signal 可以捕获 SIGSEGV、SIGABRT 等:

(gdb) catch signal SIGSEGV
Catchpoint 6 (signal SIGSEGV)
(gdb) continue
Continuing.
Catchpoint 6 (signal SIGSEGV), 0x00005555555551a6 in crash_here () at test.c:30
30	    *ptr = 42;
我的建议:在嵌入式裸机调试中,catch signal 特别适合捕获 HardFault 或 BusFault。配合 monitor reset 和 catch signal,可以快速定位硬件异常触发点。

4.4 实战:观察点 + 捕获点组合拳

我分享一个真实案例。当时调试一个多线程程序,某个全局链表被破坏。我用了三步:

  1. 先设观察点:watch list_head,看谁改了链表头指针。
  2. 再设捕获点:catch syscall pthread_mutex_lock,看加锁顺序。
  3. 最后条件观察点:watch list_head if list_head == 0,只在链表头为空时触发。

结果发现是一个线程在释放锁之后,另一个线程抢占了 CPU 并修改了链表。说白了,就是锁的粒度不够细。

4.5 本章知识体系

下面这张图总结了观察点和捕获点的核心逻辑:

GDB 观察点与捕获点知识体系 观察点(Watchpoint) watch(写监视) rwatch(读监视) awatch(读写监视) 依赖硬件调试寄存器 数量有限(2-4个) 捕获点(Catchpoint) catch syscall catch throw/catch catch signal 监视事件而非变量 无数量限制 组合使用技巧 观察点 + 捕获点 + 条件断点 = 高效定位内存与异常问题

4.6 避坑指南

  • 我曾经犯过的错:在循环里设 watch 一个局部变量,结果 GDB 一直报错。后来才意识到局部变量地址不固定,应该用全局变量或静态变量。
  • 硬件观察点用完怎么办?用 info watchpoints 查看当前列表,删除不需要的。或者改用条件观察点减少占用。
  • catch syscall 在嵌入式上可能不支持:有些 RTOS 没有完整的系统调用层,这时候 catch syscall 会失效。可以用 catch signal 替代。
  • 多线程调试注意:观察点默认对所有线程生效。如果只想监视某个线程,用 thread apply 1 watch my_var。

好了,观察点和捕获点就聊到这里。这两个工具用熟了,调试效率能翻一倍。下一章我们聊聊条件断点和断点命令,那又是另一片天地。

可可精品资料库