4. GDB进阶:条件断点、观察点、捕获点、多线程调试、core dump分析

说实话,GDB 这工具,很多人只会用个 b mainn。但真正到了线上排查问题,或者调试多线程死锁的时候,那点基本功根本不够用。我这些年带团队,发现一个规律:能用好条件断点和观察点的人,定位 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 write
  • catch 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_aworker_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 分析三板斧:

  1. bt(backtrace) — 看调用栈,找到崩溃位置
  2. frame [编号] — 切换到指定栈帧,查看局部变量
  3. info locals — 查看当前函数的局部变量值

核心思路: Core dump 分析的本质是「还原案发现场」。你不需要重新运行程序,只需要分析那一瞬间的内存快照。所以,编译时一定要加 -g 选项保留调试信息,否则你看到的全是地址,没有符号。

4.6 本章知识体系

下面这张图,是我自己总结的 GDB 进阶调试思维导图。你可以把它当作一个检查清单——遇到问题,先看属于哪一类,再选对应的工具。

GDB 进阶调试 条件断点 break if 条件 condition 编号 条件 观察点 watch / rwatch / awatch 硬件调试寄存器 捕获点 catch throw / syscall catch fork / load 多线程调试 info threads thread apply 编号 cmd scheduler-locking on Core Dump 分析 ulimit -c unlimited gdb ./app core bt / frame / info locals 核心原则:先定位问题类型,再选对应工具

说实话,GDB 进阶技巧这东西,光看文档是学不会的。你得真的遇到一个死锁、一个段错误、一个莫名其妙的内存被改,然后用这些工具亲手把它揪出来。经历过一次,你就再也不会忘了。

嗯,今天就先聊到这儿。下一节咱们会深入讲讲性能剖析工具,到时候用 perf 和火焰图来抓性能瓶颈,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321