4. GDB进阶:条件断点、观察点、捕获点、多线程调试、core dump分析
说实话,GDB 这工具,很多人只会用个 b main 和 n。但真正到了线上排查问题,或者调试多线程死锁的时候,那点基本功根本不够用。我这些年带团队,发现一个规律:能用好条件断点和观察点的人,定位 bug 的速度至少快三倍。
今天咱们就把 GDB 的进阶玩法捋一遍。这些东西,说白了就是让你在代码的海洋里精准「捞针」。
4.1 条件断点:别让程序停在不该停的地方
你想想看,一个循环跑 10 万次,你只关心第 99999 次的情况。要是每次都用 c 继续,手都得按抽筋。条件断点就是干这个的。
核心语法:
(gdb) break [文件名:行号] if [条件表达式]
(gdb) condition [断点编号] [条件表达式]
举个例子,我调试过一个内存池的分配器,有个 bug 只在分配大小等于 1024 字节时触发:
(gdb) break pool_alloc if size == 1024
(gdb) run
Breakpoint 1, pool_alloc (size=1024) at pool.c:42
42 block = find_free_block(size);
嗯,这里要注意:条件表达式里可以用函数的参数、全局变量,甚至复杂的逻辑运算。但别写太复杂的条件,否则每次断点触发时 GDB 都要解释执行,程序会慢得像蜗牛。
我的小技巧: 如果你不确定条件写对了没,可以先不加条件打上断点,然后用 condition 1 size == 1024 动态加上去。这样万一写错了,还能改。
4.2 观察点:变量变了,立刻停
条件断点解决的是「什么时候停」,观察点解决的是「谁动了我的变量」。我曾经遇到过一个全局变量被莫名其妙改掉的问题,查了两天没头绪。后来用观察点,一下子就逮住了那个野指针。
观察点有三种:
| 命令 | 作用 | 适用场景 |
|---|---|---|
watch [变量] |
变量被写入时停 | 变量值被意外修改 |
rwatch [变量] |
变量被读取时停 | 想追踪谁在读这个变量 |
awatch [变量] |
读写都停 | 全面监控 |
(gdb) watch g_shared_counter
Hardware watchpoint 2: g_shared_counter
(gdb) continue
Continuing.
Hardware watchpoint 2: g_shared_counter
Old value = 42
New value = 43
thread_func (arg=0x0) at thread.c:28
28 g_shared_counter++;
注意: 观察点依赖硬件调试寄存器,数量有限(通常 4 个)。如果 GDB 提示「Could not insert hardware watchpoint」,说明用完了。另外,观察点对局部变量不友好——变量出作用域后,观察点自动失效。
4.3 捕获点:不打断代码也能抓事件
捕获点这东西,用的人不多,但真的香。它不打断程序执行,而是当特定事件发生时(比如抛异常、加载动态库、fork 子进程),GDB 自动停下来。
我个人习惯在调试 C++ 异常时用它:
(gdb) catch throw
Catchpoint 1 (throw)
(gdb) run
Catchpoint 1 (exception thrown), 0x7ffff7a8e5c0 in __cxa_throw ()
(gdb) bt
#0 0x7ffff7a8e5c0 in __cxa_throw ()
#1 0x00005555555551a0 in divide (a=10, b=0) at main.cpp:12
#2 0x00005555555551e0 in main () at main.cpp:18
其他常用捕获点:
catch syscall [名称]— 监控系统调用,比如catch syscall writecatch fork— 监控 fork 事件,调试多进程利器catch load/catch unload— 动态库加载/卸载
4.4 多线程调试:别被线程切换搞晕
多线程调试是 GDB 进阶的硬骨头。我刚开始搞多线程时,经常被线程切换搞晕——明明在调试线程 A,一执行 n,结果跑到了线程 B 里。
记住这几个命令,基本够用:
| 命令 | 作用 |
|---|---|
info threads |
查看所有线程及其状态 |
thread [编号] |
切换到指定线程 |
thread apply [编号] [命令] |
对指定线程执行命令 |
set scheduler-locking on |
锁定调度,只调试当前线程 |
举个例子,调试死锁时,我一般这么干:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fc1740 (LWP 12345) "myapp" 0x7ffff7bc4d2d in __lll_lock_wait ()
2 Thread 0x7ffff6ffb700 (LWP 12346) "myapp" 0x7ffff7bc4d2d in __lll_lock_wait ()
(gdb) thread 1
(gdb) bt
#0 0x7ffff7bc4d2d in __lll_lock_wait ()
#1 0x7ffff7bc4617 in _L_lock_812 ()
#2 0x7ffff7bc44f8 in pthread_mutex_lock ()
#3 0x00005555555551a0 in worker_a () at deadlock.c:15
(gdb) thread 2
(gdb) bt
#0 0x7ffff7bc4d2d in __lll_lock_wait ()
#1 0x7ffff7bc4617 in _L_lock_812 ()
#2 0x7ffff7bc44f8 in pthread_mutex_lock ()
#3 0x00005555555551c0 in worker_b () at deadlock.c:25
看到没?两个线程都在等锁,而且 worker_a 和 worker_b 各持有一把对方需要的锁。这就是典型的死锁。
避坑指南: 调试多线程时,记得先 set scheduler-locking on。否则你单步执行时,GDB 可能会切换到其他线程,让你摸不着头脑。我曾经因为这个浪费了一整个下午。
4.5 Core Dump 分析:程序挂了,别慌
程序崩溃了,没有日志,没有复现步骤。怎么办?Core dump 就是你的救命稻草。
首先,确保系统允许生成 core 文件:
# 查看当前限制
ulimit -c
# 设置为无限制
ulimit -c unlimited
# 设置 core 文件命名格式(可选)
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern
然后,用 GDB 加载 core 文件:
gdb ./myapp /tmp/core.myapp.12345
进去之后,第一件事就是看程序死在哪里:
(gdb) bt
#0 0x00007ffff7a8e5c0 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007ffff7a8e5c0 in __GI_abort () at abort.c:79
#2 0x00007ffff7a8e5c0 in __assert_fail_base () at assert.c:92
#3 0x00007ffff7a8e5c0 in __assert_fail () at assert.c:101
#4 0x00005555555551a0 in process_data (data=0x0) at main.c:42
#5 0x00005555555551e0 in main () at main.c:55
看到 __assert_fail 了吗?说明是断言失败。再看 process_data (data=0x0),传了个空指针进来。问题就出在这里。
我常用的 core dump 分析三板斧:
- bt(backtrace) — 看调用栈,找到崩溃位置
- frame [编号] — 切换到指定栈帧,查看局部变量
- info locals — 查看当前函数的局部变量值
核心思路: Core dump 分析的本质是「还原案发现场」。你不需要重新运行程序,只需要分析那一瞬间的内存快照。所以,编译时一定要加 -g 选项保留调试信息,否则你看到的全是地址,没有符号。
4.6 本章知识体系
下面这张图,是我自己总结的 GDB 进阶调试思维导图。你可以把它当作一个检查清单——遇到问题,先看属于哪一类,再选对应的工具。
说实话,GDB 进阶技巧这东西,光看文档是学不会的。你得真的遇到一个死锁、一个段错误、一个莫名其妙的内存被改,然后用这些工具亲手把它揪出来。经历过一次,你就再也不会忘了。
嗯,今天就先聊到这儿。下一节咱们会深入讲讲性能剖析工具,到时候用 perf 和火焰图来抓性能瓶颈,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321