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

调试用户态驱动,说白了就是跟内核态抢时间。你想想看,驱动跑在用户空间,虽然崩溃了不会直接蓝屏,但数据错乱、死锁、竞态条件这些坑,一个都不少。GDB 的基础用法大家都会,但真正能快速定位问题的,往往是那些高级技巧。今天我就把压箱底的东西掏出来,聊聊条件断点、观察点、捕获点,还有多线程调试的那些事儿。

3.1 条件断点:别让断点烦你

普通断点有个毛病——每次执行到那行都停。循环 10 万次,你手动按 10 万次 continue?我刚开始干这行时就这么干过,后来发现头发就是这么掉的。

条件断点就聪明多了。只有满足条件时,它才触发。比如你只关心某个变量等于特定值的情况:

(gdb) break driver_ioctl if cmd == 0xDEAD
(gdb) break packet_handler if packet->len > 1500

我个人习惯在调试 DMA 缓冲区时这么用:

(gdb) break dma_complete if transfer_size > 4096

为什么?因为小传输大概率没问题,大传输才容易踩到边界。条件断点能帮你过滤掉 99% 的噪音。

小技巧:条件表达式里可以用函数调用,但别调太复杂的。GDB 每次停下来都要算一遍条件,太慢的话调试体验会崩。

3.2 观察点:盯着变量不放

有时候你根本不知道变量在哪被改的。断点?你总不能每行都打一个吧。这时候观察点就派上用场了。

观察点监控内存地址。只要那个地址的值变了,GDB 就停:

(gdb) watch my_shared_flag
(gdb) watch *(int*)0x7ffff7a0b000

我记得有一次调试一个用户态驱动,某个全局状态标志莫名其妙被清零。我翻遍了所有写它的地方,都没发现问题。最后用 watch 一盯,发现是另一个线程通过指针越界写到了这个地址。嗯,这种问题,靠肉眼是看不出来的。

观察点分三种:

类型 命令 触发时机
写观察点 watch 变量被写入时
读观察点 rwatch 变量被读取时
读写观察点 awatch 变量被读或写时
注意:观察点依赖硬件调试寄存器,数量有限(通常 4 个)。别一口气设太多,否则 GDB 会退化成单步模拟,慢到你怀疑人生。

3.3 捕获点:抓异常和系统调用

捕获点是个冷门但好用的东西。它不盯代码行,也不盯变量,而是盯事件。比如 C++ 异常、系统调用、信号等。

调试用户态驱动时,我经常用它来抓 SIGSEGV 或 SIGBUS:

(gdb) catch signal SIGSEGV
(gdb) catch syscall mmap

为什么要抓 mmap?因为用户态驱动经常通过 mmap 映射设备内存或 DMA 缓冲区。如果映射参数错了,后面所有操作都是白搭。我曾经遇到一个 bug,驱动在 mmap 时传了个错误的偏移量,导致后续所有读写都跑偏。用捕获点一抓,当场就看到了问题参数。

捕获点还能跟条件组合:

(gdb) catch syscall write if fd == 3

这样只抓文件描述符 3 上的写操作,省得被其他日志刷屏。

3.4 调试多线程程序:别让竞态坑了你

多线程调试是用户态驱动的大头。驱动里一堆 worker 线程、中断模拟线程、主控制线程,稍不注意就死锁或数据错乱。

GDB 默认是 all-stop 模式——一个线程断住,所有线程都停。这其实挺坑的,因为停住的那一瞬间,其他线程的状态可能已经变了。你看到的「现场」其实是伪造的。

我个人更推荐用 non-stop 模式:

(gdb) set non-stop on
(gdb) set target-async on

这样每个线程独立运行。你可以在一个线程里设断点,其他线程继续跑。对于调试死锁特别有用——你可以让一个线程停在锁前,看看其他线程是不是已经占了锁不放。

常用多线程调试命令:

  • info threads —— 列出所有线程
  • thread N —— 切换到线程 N
  • thread apply all bt —— 打印所有线程的调用栈
  • set scheduler-locking on —— 单步时只跑当前线程

举个例子,假设你怀疑某个共享变量被多个线程乱写:

(gdb) thread apply all bt
(gdb) thread 2
(gdb) print shared_var
(gdb) thread 3
(gdb) print shared_var

对比不同线程看到的变量值,往往能发现谁在搞鬼。

避坑指南:我曾经调试一个驱动,发现某个线程总是读不到最新的共享变量值。一开始以为是缓存一致性问题,折腾了半天。后来用 thread apply all p shared_var 一查,发现有个线程根本没加锁就直接写了。嗯,这种问题,GDB 帮不了你写锁,但能帮你找到谁没写锁。

3.5 实战组合拳:一个典型场景

假设你在调试一个用户态网络驱动,收包线程和发包线程共享一个环形缓冲区。收包线程偶尔读到脏数据。你会怎么查?

我的套路是这样的:

  1. 先用 watch 盯住缓冲区头指针,看谁在改。
  2. 如果发现多个线程都在写,用 thread apply all bt 看调用栈。
  3. 在写操作上加条件断点,只抓特定条件(比如写入了非法索引)。
  4. 用 catch syscall 抓 futex 调用,看锁的竞争情况。

这一套下来,90% 的多线程问题都能现原形。剩下的 10%,嗯,那可能是硬件问题,得拿逻辑分析仪了。

最后说一句:GDB 再强也只是工具。真正重要的是你对驱动逻辑的理解。工具帮你看到现象,但根因还得靠脑子想。别过度依赖调试器,有时候加个日志打印反而更快。