2、GDB核心概念:断点、观察点、捕获点、单步执行、继续执行、查看变量
调试器这东西,说白了就是让你能「看穿」程序运行时的内部状态。GDB 的核心能力,就围绕这几个概念展开。我刚开始用 GDB 时,觉得断点就是「停住程序」,后来才发现,停在哪、怎么停、停了之后看什么,这里面的门道深着呢。
2.1 断点(Breakpoint)—— 最常用的「暂停键」
断点就是告诉 GDB:「执行到某一行,给我停下来」。这是最基础,也是最常用的功能。
设置断点的几种方式:
# 按行号设置
(gdb) break main.c:42
# 按函数名设置
(gdb) break my_function
# 按条件设置(只有满足条件才触发)
(gdb) break main.c:42 if x > 10
# 临时断点(触发一次后自动删除)
(gdb) tbreak main.c:42
我个人习惯用函数名设置断点,尤其是调试第三方库时。你想想看,你根本不知道人家的源码行号,但函数名总归是知道的。
小技巧: 用 info breakpoints 查看所有断点,用 delete 1 删除编号为1的断点。
我在项目中遇到过一个问题:一个循环跑了100万次,在第999,999次才崩溃。如果不用条件断点,你得手动按「继续」按到手指抽筋。条件断点 break loop.c:10 if i == 999998 直接搞定。
2.2 观察点(Watchpoint)—— 盯住变量的「监视器」
断点是盯住代码位置,观察点是盯住变量本身。只要变量的值发生变化,程序就会停下来。
# 观察变量 x 的变化
(gdb) watch x
# 观察 x 被读取(不常用,但有时很有用)
(gdb) rwatch x
# 观察 x 被读写
(gdb) awatch x
嗯,这里要注意:观察点对硬件有要求。如果变量是局部变量,函数返回后观察点会自动失效。我曾经踩过这个坑——观察一个局部变量,函数返回后观察点消失了,我还以为是 GDB 的 bug。
避坑指南: 我曾经调试一个多线程程序,用 watch 观察一个全局变量,结果发现观察点只对当前线程有效。其他线程修改了这个变量,GDB 根本不会停下来。解决方案是用硬件观察点,或者加线程限定。
2.3 捕获点(Catchpoint)—— 捕捉「事件」的触发器
捕获点不是盯代码,也不是盯变量,而是盯事件。比如 C++ 异常抛出、系统调用、库加载等。
# 捕获 C++ 异常
(gdb) catch throw
# 捕获系统调用(需要指定系统调用号)
(gdb) catch syscall write
# 捕获 fork 事件
(gdb) catch fork
为什么需要捕获点?你想想看,有时候程序莫名其妙崩溃,你根本不知道异常在哪抛出的。用 catch throw 一抓,程序在抛出异常的那一刻就停住了,你再用 backtrace 看调用栈,问题一目了然。
我记得有一次调试一个服务器程序,它老是莫名其妙地 fork 出子进程。我用了 catch fork,发现是某个第三方库在初始化时偷偷调了 fork。嗯,这种问题,不用捕获点根本查不出来。
2.4 单步执行(Step / Next)—— 一行一行「走」过去
程序停下来了,然后呢?你得一步一步地看它怎么执行。
| 命令 | 作用 | 区别 |
|---|---|---|
step 或 s |
单步进入 | 遇到函数调用,会进入函数内部 |
next 或 n |
单步跳过 | 遇到函数调用,直接执行完整个函数 |
finish |
执行到函数返回 | 跳出当前函数 |
until |
执行到指定行 | 相当于「快速单步」 |
我个人习惯:调试自己的代码用 next,调试别人的代码用 step。为什么?自己的代码,函数内部逻辑我清楚,没必要进去看;别人的代码,鬼知道里面写了什么,得进去瞅一眼才放心。
小技巧: 如果你不小心 step 进了一个不想进的函数,别慌,按 finish 直接跳出来。
2.5 继续执行(Continue)—— 从暂停到「跑起来」
单步执行太慢了,有时候你想让程序继续跑,直到下一个断点或观察点触发。
# 继续执行
(gdb) continue
(gdb) c
# 跳过当前断点,继续执行 N 次
(gdb) continue N
# 继续执行,但忽略当前断点一次
(gdb) continue 1
这里有个坑:如果你在循环里设了断点,每次 continue 都会停在同一个地方。这时候用 continue 5 可以跳过前5次触发,直接停在第6次。我调试一个排序算法时用过这个技巧,省了不少时间。
2.6 查看变量(Print / Display)—— 调试的「眼睛」
程序停下来了,你得看看变量值对不对。这是调试的核心目的。
# 打印变量
(gdb) print x
(gdb) p x
# 打印表达式
(gdb) print x + y
# 打印数组
(gdb) print *array@10
# 打印结构体
(gdb) print my_struct
# 自动显示(每次停顿时自动打印)
(gdb) display x
(gdb) display /x y # 以十六进制显示
我建议你多用 display,尤其是调试复杂数据结构时。每次单步执行,GDB 自动把关键变量打印出来,省得你手动敲 print。
格式化输出:
/x— 十六进制/d— 十进制/s— 字符串/f— 浮点数
我曾经调试一个网络协议栈,里面有个指针指向的数据结构特别复杂。每次 print 出来一大坨,看得眼花。后来我用 display /x 只看十六进制,配合 ptype 看类型定义,效率高了很多。
2.7 知识体系总览
下面这张图,是我自己总结的 GDB 核心概念关系图。你看一眼,就能明白这些概念是怎么配合的。
这张图的核心逻辑很简单:断点、观察点、捕获点是三种「触发暂停」的方式。程序暂停后,你可以选择单步执行、继续执行,或者查看变量。最终目的只有一个——找到 bug,理解程序到底在干什么。
最后提醒一句: 别把 GDB 当成「万能药」。有时候程序崩溃得太快,断点根本来不及设。这时候可以试试 catch syscall 或者用 core dump 事后分析。嗯,这些内容后面的章节会详细讲。