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% 的噪音。
3.2 观察点:盯着变量不放
有时候你根本不知道变量在哪被改的。断点?你总不能每行都打一个吧。这时候观察点就派上用场了。
观察点监控内存地址。只要那个地址的值变了,GDB 就停:
(gdb) watch my_shared_flag
(gdb) watch *(int*)0x7ffff7a0b000
我记得有一次调试一个用户态驱动,某个全局状态标志莫名其妙被清零。我翻遍了所有写它的地方,都没发现问题。最后用 watch 一盯,发现是另一个线程通过指针越界写到了这个地址。嗯,这种问题,靠肉眼是看不出来的。
观察点分三种:
| 类型 | 命令 | 触发时机 |
|---|---|---|
| 写观察点 | watch |
变量被写入时 |
| 读观察点 | rwatch |
变量被读取时 |
| 读写观察点 | awatch |
变量被读或写时 |
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—— 切换到线程 Nthread 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 实战组合拳:一个典型场景
假设你在调试一个用户态网络驱动,收包线程和发包线程共享一个环形缓冲区。收包线程偶尔读到脏数据。你会怎么查?
我的套路是这样的:
- 先用
watch盯住缓冲区头指针,看谁在改。 - 如果发现多个线程都在写,用
thread apply all bt看调用栈。 - 在写操作上加条件断点,只抓特定条件(比如写入了非法索引)。
- 用
catch syscall抓futex调用,看锁的竞争情况。
这一套下来,90% 的多线程问题都能现原形。剩下的 10%,嗯,那可能是硬件问题,得拿逻辑分析仪了。