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 核心概念关系图。你看一眼,就能明白这些概念是怎么配合的。

GDB 核心概念关系图 断点 (Breakpoint) 观察点 (Watchpoint) 捕获点 (Catchpoint) 程序暂停 (Program Stopped) 单步执行 (Step/Next) 继续执行 (Continue) 查看变量 (Print/Display) 定位 Bug / 理解程序行为

这张图的核心逻辑很简单:断点、观察点、捕获点是三种「触发暂停」的方式。程序暂停后,你可以选择单步执行、继续执行,或者查看变量。最终目的只有一个——找到 bug,理解程序到底在干什么。

最后提醒一句: 别把 GDB 当成「万能药」。有时候程序崩溃得太快,断点根本来不及设。这时候可以试试 catch syscall 或者用 core dump 事后分析。嗯,这些内容后面的章节会详细讲。

可可精品资料库