Skip to content

GDB 进阶:条件断点、watchpoint 与 core dump

引言:基础够用,但有些 bug 要更趁手的兵器

第 14 章那套「断点 + 单步 + 查看」是地基,绝大多数调试都靠它。但有三种场景,光靠基础命令会把你累死:第一,一个循环跑到第 1000 次才出 bug,你用无条件断点停在循环体里,得按 999 次 continue 才熬到出错那一次;第二,某个变量不知被代码哪个角落改坏了,你不知道该在哪儿设断点;第三,程序在 CI 或用户机器上崩了,你没法「当场」开 gdb 复现。这一章的真跑兵器分别对付它们:条件断点watchpointcore dump

我们准备两个靶子。一个是会段错误的 crash.c(第 14 章那个,算完阶乘再 NULL 解引用),用来讲 core dump;另一个是 loop.c,一个简单的累加循环,用来讲条件断点和 watchpoint:

c
#include <stdio.h>

int main(void) {
    int sum = 0;
    for (int i = 1; i <= 10; i++) {
        sum += i;
    }
    printf("sum = %d\n", sum);
    return 0;
}

都照例 -g -O0 编译。

条件断点:只在满足条件时停

假设你怀疑 sum += i 这行在第 7 次循环时算错了。如果用普通断点 break loop.c:6,前 6 次循环每次都会停,你得手动 continue 六次。条件断点让你给断点挂一个条件,只在条件成立时才停

text
(gdb) break loop.c:6 if i==7
Breakpoint 1 at 0x1151: file loop.c, line 6.
(gdb) run
Breakpoint 1, main () at loop.c:6
6	        sum += i;
(gdb) print i
$1 = 7
(gdb) print sum
$2 = 21
(gdb) continue
sum = 55
[Inferior 1 (process ...) exited normally]

break loop.c:6 if i==7 的意思是「在第 6 行设断点,但只有当 i==7 时才真正停下来」。于是 run 之后程序一路跑,跳过前 6 次,精确在第 7 次循环停在第 6 行——print i 确认是 7,print sum 是 21(此时 sum 还是 1 到 6 的和、还没加上 7)。这下你直接就站在了「出问题的那一次」上,不用按 999 次 continue。条件断点在调试「循环/递归第 N 次才出错」时几乎是必备的,条件可以是任意合法的 C 表达式(if i>5 && sum>10 都行)。

watchpoint:变量一被改就停

断点是「执行到某一行时停」,但有时你不知道该在哪行停——你只知道「某个变量被改坏了,想知道是谁改的」。这就是 watchpoint 的主场:你盯住一个变量,它一被修改就自动停下来,还告诉你改成什么了。我们在循环开始前对 sum 下一个 watch:

text
(gdb) break main
(gdb) run
Breakpoint 1, main () at loop.c:4
(gdb) watch sum
Hardware watchpoint 2: sum
(gdb) continue
Hardware watchpoint 2: sum

Old value = 0
New value = 1
main () at loop.c:5
(gdb) print sum
$1 = 1

break main + run 是为了让程序进到 main、让 sum 这个局部变量先存在(在它还没进作用域时没法 watch)。watch sum 设好后,每次 continue,只要 sum 的值发生变化,GDB 就自动停下,并显示 Old value = 0 / New value = 1——你立刻知道「这次它从 0 被改成了 1」,而且停在了导致这个改动的那条语句附近。在一个大工程里,如果 sum 被某个意想不到的函数改了,watchpoint 会把你直接带到「案发现场」,不用你猜该在哪打断点。注意 watchpoint 有硬件和软件两种:GDB 优先用硬件 watchpoint(快,靠 CPU 的调试寄存器),盯的变量太多或表达式太复杂时会退化成软件 watchpoint(每条指令都检查一次,很慢)——所以 watchpoint 适合定位「值被改」,定位完就 delete 掉,别一直挂着。

core dump:崩溃后留下现场,事后分析

前两个是「程序还活着、你主动控制」。但现实里很多崩溃发生在你没法当场开 gdb 的场合:CI 跑挂了、用户机器上报错、或者那个崩溃只在高负载下偶发、你重新跑它就是不复现。这时候你需要 core dump——程序崩溃那一刻的「内存快照」,存进一个文件,事后用 gdb 读取,就像回到崩溃现场一样查。

先说一个现代 Linux 的坑。你可能会想「段错误会自动生成 core 文件吧」——以前是,但现在很多发行版(包括我们这台 WSL2 机器)把 core 交给 systemd-coredump 统一接管了:

text
$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t ...

这个 core_pattern| 开头,意思是「core 不写成普通文件、而是通过管道交给后面那个程序(systemd-coredump)」,它会存到 /var/lib/systemd/coredump/ 之类的地方,当前目录下不会出现 core 文件。所以与其跟系统的 core 处理斗,不如直接在 GDB 里用 generate-core-file 自己生成一份干净可控的 core:

text
$ gdb -q ./crash
(gdb) run
...
Program received signal SIGSEGV, Segmentation fault.
0x... in main () at crash.c:15
15	    *p = x;
(gdb) generate-core-file crash.core
Saved corefile crash.core

程序在 GDB 里崩了之后(停在崩溃点、还没退出),generate-core-file crash.core 把当前进程的完整内存映像存成一个文件。现在关键来了——你不用重新跑程序,直接拿这个 core 文件让 GDB 事后分析:

text
$ gdb -q ./crash crash.core
Core was generated by `/tmp/cj/ch14/crash'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x... in main () at crash.c:15
15	    *p = x;
(gdb) bt
#0  0x... in main () at crash.c:15
(gdb) print p
$1 = (int *) 0x0
(gdb) print x
$2 = 120

gdb 可执行文件 core文件 这种用法,GDB 一加载就告诉你「这个 core 是哪个程序产生的、死于什么信号」,然后 btprint pprint x 全都能用——和第 14 章「当场调试」得到的信息一模一样,但程序根本没重跑。这就是 core dump 的价值:崩溃那一刻的现场被冻结进了文件,你可以把它从 CI、从用户机器拷回来,慢慢分析。生产环境里,给程序开好 core(或配合 systemd-coredump 的 coredumpctl)是事后排障的关键基础设施。

其他几把趁手的小刀

再顺手介绍几个调试时常省事的小命令。set var sum = 100 可以在调试时强行改变量的值——你想「如果 sum 是 100,后面会怎样」,不必改源码重编,直接改完 continue 看效果,试假设特别快。finish 让程序一直跑到当前函数返回并停在返回点(钻进一个函数看完了想跳出来用它)。until(简写 u)跑到指定行,常用来「跳出当前循环的剩余迭代」。最后是 TUI(终端界面):layout src 把屏幕分成「源码 + 命令」两栏,layout split 还能加一栏汇编,断点/单步时源码会高亮当前行,对照着看比 list 一次次翻舒服得多(退出 TUI 用 tui disableCtrl-X A)。这些加上第 14 章的基础命令,你的调试工具箱就相当齐全了。

小结

第 14 章的断点/单步是地基,这一章补三件对付疑难场景的兵器。条件断点 break 位置 if 条件 只在条件成立时停,专治「循环/递归第 N 次才出错」(我们真跑让循环只在 i==7 停,print 确认 sum=21),省去按几百次 continue。watchpoint watch 变量 在变量被修改时自动停并显示 Old/New 值,专治「值被不知道哪里改坏」(要在变量进入作用域后才能 watch,定位完赶紧 delete,软件 watchpoint 很慢)。core dump 把崩溃那一刻的内存冻结成文件,事后 gdb 可执行文件 core 不重跑程序就能 bt/print 看现场(我们用 generate-core-file 生成、gdb crash crash.core 读出 SIGSEGV 在 crash.c:15、p=0x0/x=120),专治「没法当场复现的崩溃」(CI、用户机器)。注意现代 Linux 的 core 多被 systemd-coredump 接管(core_pattern| 开头)、当前目录不落 core 文件,用 generate-core-file 自己生成最省心。再加 set var(调试时改变量试假设)、finish/until(控制执行)、TUI(layout src 分屏看源码),日常调试就够用了。下一章我们离开调试,回到工程协作——看 Git 工作流怎么管住代码的版本和多人改动。

参考资源

  • GDB 手册:break ... if(条件断点)、watch/rwatch/awatch(watchpoint)、generate-core-file/gcore、core 文件用法、set varfinish/until、TUI(layout
  • core(5)手册页、/proc/sys/kernel/core_patternsystemd-coredump / coredumpctl(现代发行版的 core 处理)
  • 第 14 章:GDB 基础单步(断点/单步/查看的地基)
  • 第 11 章:Sanitizer 门禁(ASan 报 use-after-free 给出的栈,和 core dump 是「自动报 vs 事后查」两条互补的路)