3、GDB高级调试:条件断点、观察点、捕获点、多线程调试、远程调试

GDB 这东西,说实话,很多工程师用了好几年还停留在「设个断点、跑一下、看看变量」的阶段。但真正遇到棘手问题——比如偶发崩溃、多线程死锁、或者嵌入式板子上没法直接跑 GDB 的时候——你就得掏出高级功能了。

我个人习惯是,先把 GDB 的这几个高级特性练熟,再上战场。今天咱们就聊聊条件断点、观察点、捕获点、多线程调试和远程调试。嗯,每个都是实战利器。

GDB 高级调试知识体系 GDB 高级调试 条件断点 观察点 捕获点 多线程调试 远程调试 break if / condition watch / rwatch / awatch catch / tcatch thread apply / scheduler-locking gdbserver / target remote

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,一下子就逮住了。要是没这个功能,我估计得调试到半夜。

小技巧: 条件表达式里可以用 GDB 支持的任意 C 表达式,包括函数调用。但注意,频繁调用函数会拖慢执行速度。

你也可以用 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 在第一次修改时停下来了——结果发现是一个野指针越界写到了这里。

注意: 观察点依赖硬件调试寄存器,数量有限(通常 4 个)。如果观察点太多,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 接口进行远程调试。

基本流程是这样的:

  1. 在目标板上运行 gdbserver
  2. 在主机上启动 GDB 并连接
  3. 像本地调试一样操作

目标板端:

$ 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 偶尔会变成负数。你怀疑是某个线程在错误条件下修改了它。你会怎么做?

我的做法是:

  1. 设一个观察点:watch counter
  2. 设一个条件断点:break update_counter if counter < 0
  3. 用 thread apply all bt 查看所有线程状态
  4. 如果怀疑是竞态,用 set scheduler-locking on 单步跟踪

你看,这几个高级功能组合起来,威力就出来了。单独用哪个都差点意思,但放在一起,大部分疑难杂症都能搞定。

总结一下: 条件断点帮你精准定位,观察点帮你盯住变量,捕获点抓住系统事件,多线程调试理清并发逻辑,远程调试解决嵌入式难题。这五个工具,是我调试工具箱里的常客。

可可精品资料库