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
4.2 观察点的底层原理与限制
观察点依赖硬件调试寄存器。x86 有 DR0-DR3 四个调试寄存器,ARM 有 DBGBCR 等。说白了,硬件观察点数量有限,而且不能监视局部变量(因为局部变量在栈上,地址会变)。
| 特性 | 硬件观察点 | 软件观察点 |
|---|---|---|
| 速度 | 快(硬件级) | 慢(单步模拟) |
| 数量限制 | 通常 2-4 个 | 无限制 |
| 支持范围 | 全局/静态变量 | 任意地址 |
| 跨平台兼容 | 依赖芯片 | 通用 |
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;
monitor reset 和 catch signal,可以快速定位硬件异常触发点。
4.4 实战:观察点 + 捕获点组合拳
我分享一个真实案例。当时调试一个多线程程序,某个全局链表被破坏。我用了三步:
- 先设观察点:
watch list_head,看谁改了链表头指针。 - 再设捕获点:
catch syscall pthread_mutex_lock,看加锁顺序。 - 最后条件观察点:
watch list_head if list_head == 0,只在链表头为空时触发。
结果发现是一个线程在释放锁之后,另一个线程抢占了 CPU 并修改了链表。说白了,就是锁的粒度不够细。
4.5 本章知识体系
下面这张图总结了观察点和捕获点的核心逻辑:
4.6 避坑指南
- 我曾经犯过的错:在循环里设 watch 一个局部变量,结果 GDB 一直报错。后来才意识到局部变量地址不固定,应该用全局变量或静态变量。
- 硬件观察点用完怎么办?用
info watchpoints查看当前列表,删除不需要的。或者改用条件观察点减少占用。 - catch syscall 在嵌入式上可能不支持:有些 RTOS 没有完整的系统调用层,这时候 catch syscall 会失效。可以用 catch signal 替代。
- 多线程调试注意:观察点默认对所有线程生效。如果只想监视某个线程,用
thread apply 1 watch my_var。
好了,观察点和捕获点就聊到这里。这两个工具用熟了,调试效率能翻一倍。下一章我们聊聊条件断点和断点命令,那又是另一片天地。
可可精品资料库