3、GDB高级调试:条件断点、观察点、捕获点、多线程调试、远程调试
GDB 这东西,说实话,很多工程师用了好几年还停留在「设个断点、跑一下、看看变量」的阶段。但真正遇到棘手问题——比如偶发崩溃、多线程死锁、或者嵌入式板子上没法直接跑 GDB 的时候——你就得掏出高级功能了。
我个人习惯是,先把 GDB 的这几个高级特性练熟,再上战场。今天咱们就聊聊条件断点、观察点、捕获点、多线程调试和远程调试。嗯,每个都是实战利器。
3.1 条件断点:别让断点烦你
你有没有遇到过这种情况?一个循环跑了 10000 次,但只有第 9999 次才出问题。普通断点你得按「continue」按到手抽筋。条件断点就是干这个的。
说白了,就是让 GDB 在满足某个条件时才停下来。比如:
(gdb) break main.c:42 if i == 9999
(gdb) break func if x > 100 && y < 0
我在项目中遇到过一个问题:一个缓冲区溢出只在特定输入长度下触发。我直接设了个条件断点 break memcpy if len > 512,一下子就逮住了。要是没这个功能,我估计得调试到半夜。
你也可以用 condition 命令给已有断点加条件:
(gdb) break main.c:42
(gdb) condition 1 i == 9999
嗯,这里要注意:条件断点只在命中该断点时才会求值。如果条件本身有副作用(比如修改了全局变量),那可就麻烦了。我曾经见过有人条件里写了 i++,结果程序逻辑全乱了——别问我怎么知道的。
3.2 观察点:盯着变量不放
观察点(Watchpoint)是另一种断点。它不关心代码执行到哪一行,只关心某个变量或内存地址的值是否变化。
你想想看,有时候你根本不知道变量是在哪里被改坏的。用观察点,GDB 会在每次写入(或读取)该变量时停下来。
(gdb) watch my_var # 写时触发
(gdb) rwatch my_var # 读时触发
(gdb) awatch my_var # 读写都触发
我记得有一次调试一个全局结构体被意外篡改的问题。代码量太大,逐行看根本不可能。我直接 watch my_struct,然后继续跑程序。GDB 在第一次修改时停下来了——结果发现是一个野指针越界写到了这里。
3.3 捕获点:抓住系统事件
捕获点(Catchpoint)用来捕获程序中的特定事件,比如 C++ 异常、系统调用、fork/exec 等。这东西在分析复杂行为时特别好用。
(gdb) catch throw # 捕获 C++ 异常抛出
(gdb) catch syscall write # 捕获 write 系统调用
(gdb) catch fork # 捕获 fork 调用
我曾经调试过一个诡异的「程序突然退出」问题。用 catch syscall exit_group 一下子就抓住了调用点——结果发现是某个库在错误路径下调了 exit()。要是没有捕获点,我可能还在傻傻地设断点呢。
catch syscall write if fd == 2,只捕获向 stderr 的写入。
3.4 多线程调试:别让线程乱跑
多线程调试是很多人的噩梦。线程切换、竞态条件、死锁……GDB 提供了一整套工具来应对。
首先,你得知道当前有哪些线程:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fc1700 (LWP 12345) "main" main () at main.c:10
2 Thread 0x7ffff7fc2700 (LWP 12346) "worker" worker_func () at worker.c:22
切换线程用 thread N:
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff7fc2700)]
#0 worker_func () at worker.c:22
我个人习惯用 thread apply all bt 一次性看所有线程的调用栈。这招在分析死锁时特别管用——一眼就能看出每个线程卡在哪里。
(gdb) thread apply all bt
Thread 2 (Thread 0x7ffff7fc2700):
#0 __lll_lock_wait () at ...
#1 __pthread_mutex_lock (mutex=0x601060) at ...
#2 worker_func () at worker.c:25
Thread 1 (Thread 0x7ffff7fc1700):
#0 __lll_lock_wait () at ...
#1 __pthread_mutex_lock (mutex=0x601080) at ...
#2 main () at main.c:15
看到没?两个线程各持一把锁,都在等对方释放——典型的死锁场景。
set scheduler-locking on 可以让 GDB 在单步调试时只运行当前线程,其他线程被冻结。这在分析竞态条件时非常有用。
3.5 远程调试:板子上也能调
嵌入式开发中,目标板往往没有完整的开发环境。这时候远程调试就派上用场了。GDB 支持通过 gdbserver 或直接通过 JTAG/SWD 接口进行远程调试。
基本流程是这样的:
- 在目标板上运行 gdbserver
- 在主机上启动 GDB 并连接
- 像本地调试一样操作
目标板端:
$ gdbserver :1234 ./my_program
Process ./my_program created; pid = 1234
Listening on port 1234
主机端:
$ gdb ./my_program
(gdb) target remote 192.168.1.100:1234
Remote debugging using 192.168.1.100:1234
(gdb) break main
(gdb) continue
我在项目中遇到过最头疼的一次:板子上的程序跑着跑着就挂了,但本地模拟器死活复现不了。后来用 gdbserver 挂上去,远程设了条件断点,跑了两个小时终于抓住了那个只在特定硬件时序下才出现的 bug。
对于没有网络接口的嵌入式设备,可以用串口或 JTAG:
(gdb) target remote /dev/ttyUSB0:115200
嗯,这里要注意波特率设置。我曾经因为波特率不匹配,调试了半小时才发现是串口参数没设对——低级错误,但谁没犯过呢?
3.6 综合实战:一个典型场景
假设你有一个多线程程序,某个全局变量 counter 偶尔会变成负数。你怀疑是某个线程在错误条件下修改了它。你会怎么做?
我的做法是:
- 设一个观察点:
watch counter - 设一个条件断点:
break update_counter if counter < 0 - 用
thread apply all bt查看所有线程状态 - 如果怀疑是竞态,用
set scheduler-locking on单步跟踪
你看,这几个高级功能组合起来,威力就出来了。单独用哪个都差点意思,但放在一起,大部分疑难杂症都能搞定。
可可精品资料库