GDB 调试指南(第 1 章):GDB 快速上手

← 目录 | 下一章:断点与观察点 →

第 1 章 GDB 快速上手:从编译到第一次调试

1.1 GDB 是什么,什么时候该用它

GDB(GNU Debugger)是 Linux/Unix 下最常用的命令行调试器,能让你在程序运行的过程中“暂停”下来,查看此刻内存里每个变量的真实值、调用栈长什么样、代码到底走到了哪一行。

什么时候该打开 GDB,而不是继续用 printf 满世界加日志?

  • 程序崩溃(段错误、断言失败),但不知道崩在哪一行、当时的变量值是什么;
  • 程序逻辑“看起来”没错,但结果不对,需要一步步看执行路径和变量变化;
  • 需要在不重新编译的前提下,临时改一个变量的值看看后续行为会不会不一样;
  • 面对别人的代码/库,想摸清楚它内部到底调用了什么。

【新手提示】printf 调试法不是不能用,但每加一行都要重新编译、重新跑一遍,效率很低,而且发布前还得把这些调试输出删掉。GDB 不需要改代码、不需要重新编译,随时下断点随时看。

1.2 准备工作:编译时必须带上调试信息

GDB 能看到多少东西,取决于编译时有没有带调试信息,以及带了多少。这是新手最容易忽略、也是后续所有问题里最常见的“地基问题”——如果这一步没做对,后面所有命令都会表现得很奇怪(看不到变量、看不到源码行号)。

1.2.1 -g 系列选项

GCC/G++ 通过 -g 系列选项控制写入可执行文件的调试信息多少,级别越高,文件越大,GDB 能看到的东西也越多。

g++ main.cpp -o cmd1 -g1
g++ main.cpp -o cmd2 -g2   # 等价于直接写 -g
g++ main.cpp -o cmd3 -g3
g++ main.cpp -o cmd        # 不带 -g,完全不含调试信息
选项 调试信息详细程度 二进制体积 包含的关键信息
(不加 -g) 无 最小 无调试段,file 命令会提示 not stripped 但没有 with debug_info
-g1 最低 较小 仅行号、函数名;不含局部变量/参数的调试信息
-g2(默认,即直接写 -g) 中等 中等 完整的行号、函数、局部变量、参数、结构体布局
-g3 最高 最大 在 -g2 基础上,额外包含宏定义、内联函数等信息

【新手提示】日常开发直接用 -g(相当于 -g2)就够了。只有当你需要在 GDB 里展开宏(macro expand)辅助排查时,才需要 -g3。正式发布的生产版本不要带 -g,一是体积变大,二是会把函数名、变量名等内部信息暴露给反编译者。

1.2.2 怎么验证一个可执行文件到底有没有调试信息

拿到一个别人编译好的程序,不确定它有没有调试信息?两个办法:

$ file cmd1
cmd1: ELF 64-bit LSB pie executable ... with debug_info, not stripped
$ file cmd
cmd: ELF 64-bit LSB pie executable ... not stripped        # 没有 with debug_info

出现 with debug_info 就说明带了调试信息。更细致地,可以用 readelf -S cmd1 -W 看段表,带 -g 编译的文件会多出一批 .debug_* 段(.debug_info、.debug_abbrev、.debug_line、.debug_str 等),这些段就是调试信息的实际载体。

1.2.3 不同级别对调试体验的实际影响

用同一个断点 b main 分别调试 -g1、-g2、-g3 编译出的程序,差异非常直观:

查看局部变量(i locals)

(gdb) # 使用 -g1 编译的 cmd1
(gdb) i locals
No locals.                      # -g1 没有变量调试信息,即使局部变量真实存在也看不到

(gdb) # 使用 -g2 编译的 cmd2
(gdb) i locals
a = {-8704, 32767, -137661553, 32767, -135812528}
p = 0x6c6f6f705f68652e          # -g2 开始能看到真实的局部变量值

查看宏定义(macro expand / info macro)

(gdb) # -g1 / -g2 均无宏调试信息
(gdb) macro expand MAX
expands to: MAX                 # 无法展开,原样返回,说明 GDB 根本不知道这个宏

(gdb) # -g3 编译的 cmd3
(gdb) macro expand MAX
expands to: (9988)              # 正确展开为宏的真实值

1.2.4 【常见误区】改了代码忘记重新编译

调试信息里记录的是“编译那一刻”源码的行号和内容。如果你改了 .cpp 文件但忘了重新 g++ 编译,GDB 会发现源码文件的修改时间比可执行文件新,提示:

warning: Source file is more recent than executable.

这时候你看到的行号、断点位置,对应的其实是改动之前的旧代码,跟你正在看的源码对不上,会造成很大的困惑。看到这条警告,第一反应应该是回去重新编译,而不是怀疑 GDB 出 bug 了。

1.3 第一次启动 GDB:完整走一遍流程

准备好一个带调试信息的可执行文件后,正式开始。

1.3.1 启动 GDB

gdb ./cmd        # 直接加载可执行文件
gdb -q ./cmd      # -q 静默模式,不打印版权横幅(推荐日常使用)

首次启动时 GDB 可能会询问是否启用 debuginfod(自动从网络下载调试符号):

Enable debuginfod for this session? (y or [n]) n

内网/离线环境建议选 n,并按提示把下面这行写进 ~/.gdbinit,一劳永逸地关掉这个提示:

set debuginfod enabled off

【新手提示】~/.gdbinit 是 GDB 的启动配置文件,每次启动都会自动读取执行里面的命令,适合放一些你每次都要设置的选项(比如关掉 debuginfod 提示、设置 set listsize 30 等)。不需要专门去学复杂语法,先知道有这个文件、可以往里面扑普通 GDB 命令即可。

若程序没有调试信息,GDB 会在启动时提示:

(No debugging symbols found in ./cmd)

此时仍可以反汇编、看寄存器,但不能用 break 行号、i locals、list 这类依赖源码/符号信息的命令——这也是前面反复强调“一定要带 -g 编译”的原因。

1.3.2 走一遍最小闭环:下断点 → 运行 → 单步 → 看变量 → 继续

下面这组命令是你之后几乎每次调试都会重复的基本动作,先建立整体感觉,细节在后面几章展开:

$ gdb -q ./cmd
Reading symbols from ./cmd...
(gdb) b main                 # 在 main 函数入口下一个断点
Breakpoint 1 at 0x11e4: file main.cpp, line 91.

(gdb) r                      # 运行程序,会在刚才的断点处停下
Starting program: /path/to/cmd
Breakpoint 1, main () at main.cpp:91
91      int main() {

(gdb) n                      # 单步执行一行(不进入被调用函数内部)
92          int a = 123;

(gdb) n
93          char buf[128] = "456";

(gdb) p a                    # 查看变量 a 当前的值
$1 = 123

(gdb) c                      # 继续运行,直到下一个断点或程序结束
Continuing.
...
[Inferior 1 (process 12345) exited normally]

(gdb) q                      # 退出 GDB

逐条解释一下这几个命令在做什么:

  • b main(break 的缩写):在 main 函数第一行有效代码处打一个断点,程序跑到这里会自动暂停。
  • r(run 的缩写):真正启动程序,程序会一直跑到第一个断点才停下来,而不是跑完。
  • n(next 的缩写):执行当前这一行,然后停在下一行,不会“钻进”这一行里调用的函数内部。
  • p a(print 的缩写):打印表达式 a 当前的值。a 可以是变量名,也可以是更复杂的表达式,比如 a + 1、arr[2]。
  • c(continue 的缩写):从当前暂停的地方继续往下跑,直到遇到下一个断点,或者程序自然结束。
  • q(quit 的缩写):退出 GDB。如果程序还在运行中,会询问是否要连带结束它。

【新手提示】GDB 的命令基本都支持“输入前缀唯一即可”的缩写规则,所以 break/b、run/r、next/n、print/p、continue/c 可以混用,不需要每次都打全称。具体每个命令的标准缩写见第 5 章速查表。

跑完这一遍,你已经摸到了 GDB 最核心的工作模式:用断点把程序“钉”在某个位置,然后用单步 + 查看变量去验证你对代码行为的假设。后面几章分别把“断点”“运行参数”“单步与查看状态”这三块展开讲透。

1.4 小结

  • 调试信息是一切的前提:忘记 -g 或忘记重新编译,是新手最容易踩、但一旦意识到就很容易避免的坑。
  • file / readelf -S 可以在不打开 GDB 的情况下,先确认一个可执行文件到底有没有调试信息。
  • GDB 的基本工作循环就是:下断点(break)→ 运行(run)→ 单步(next/step)→ 查看状态(print 等)→ 继续(continue),后面所有章节都是在这个循环上做细化。

← 目录 | 下一章:断点与观察点 →

posted @ 2026-10-10 08:26  BlackSnow  阅读(18)  评论(0)    收藏  举报