2. GDB快速上手

说实话,很多初学者一上来就急着敲GDB命令,结果发现根本停不到想要的位置,或者变量值全是乱的。为什么?因为程序压根没带调试信息编译。这就像你拿着张模糊的地图去找宝藏——能找到才怪。

这一章,咱们就把最基础的东西捋清楚。我会带你从编译开始,一步步走完一个完整的调试流程。嗯,都是我在项目中踩过坑之后总结出来的经验。

2.1 编译带调试信息的程序

GDB能读懂你的代码,靠的是编译器生成的调试符号表。这个表里记录了变量名、函数名、行号对应关系等等。没有它,GDB看到的只是一堆机器码。

怎么生成?加个 -g 选项就行。

gcc -g -o myprogram myprogram.c
g++ -g -o myprogram myprogram.cpp

我个人习惯在开发阶段用 -g -O0。为什么?-O0 告诉编译器别优化,这样代码执行顺序和源码完全一致,调试起来最直观。你想想看,如果编译器把变量优化没了,你打印它的时候GDB告诉你「optimized out」,多闹心。

关键点:

  • -g:生成调试信息
  • -O0:关闭优化(推荐调试时使用)
  • 不要用 -s 或 strip 命令,它们会删掉调试符号

小技巧:我在项目中经常用 file myprogram 命令检查是否包含调试信息。如果输出里有 not stripped,说明符号还在;如果显示 stripped,那就得重新编译了。

2.2 启动GDB的三种方式

GDB的启动方式不止一种,我根据场景不同会换着用。下面这三种是最常用的。

方式一:直接启动并加载程序

gdb myprogram

这是最标准的方式。启动后你会看到GDB的提示符 (gdb),然后就可以敲命令了。我平时90%的情况都用这个。

方式二:先启动GDB,再加载程序

gdb
(gdb) file myprogram

有时候你忘了程序名,或者想调试多个程序,可以用这个方式。先启动GDB,然后用 file 命令加载。我记得有一次在服务器上调试,程序路径特别长,我就是先启动GDB再粘贴路径的。

方式三:附加到正在运行的进程

gdb -p 12345
# 或者
gdb myprogram 12345

这个厉害了。程序已经跑起来了,突然卡死或者出异常,你不想重启它,那就用 -p 参数加上进程PID。我曾经在生产环境上用过这个——当然,是经过授权的——直接定位到了一个死循环问题。

注意:附加到进程需要相应权限。如果提示 Permission denied,试试 sudo gdb -p PID。另外,附加后程序会暂停,记得调试完用 continue 让它继续跑。

2.3 基本调试流程演示

光说不练假把式。咱们拿一个简单的例子走一遍完整流程。

先写个有bug的程序:

// bug.c
#include <stdio.h>

int divide(int a, int b) {
    return a / b;  // 如果b为0,这里会崩溃
}

int main() {
    int x = 10;
    int y = 0;
    int result = divide(x, y);
    printf("Result: %d\n", result);
    return 0;
}

编译它:

gcc -g -O0 -o bug bug.c

好,现在开始调试。我建议你跟着敲一遍。

第一步:启动GDB

gdb ./bug

第二步:设置断点

我想在 divide 函数入口停住:

(gdb) break divide
Breakpoint 1 at 0x4004f6: file bug.c, line 4.

GDB告诉我断点设在了第4行。你也可以用行号:break bug.c:8。

第三步:运行程序

(gdb) run
Starting program: /home/user/bug

Breakpoint 1, divide (a=10, b=0) at bug.c:4
4           return a / b;

程序停在了断点处。注意看,GDB自动打印了当前执行的代码行和参数值。a=10,b=0——问题已经很明显了。

第四步:查看变量

(gdb) print a
$1 = 10
(gdb) print b
$2 = 0
(gdb) info locals
a = 10
b = 0

print 打印单个变量,info locals 打印所有局部变量。我个人更常用 print,因为可以打印表达式,比如 print a+b。

第五步:单步执行

(gdb) step
Program received signal SIGFPE, Arithmetic exception.
0x00000000004004fa in divide (a=10, b=0) at bug.c:4
4           return a / b;

用 step 执行当前行,结果直接触发了除零异常。GDB很贴心地告诉我们收到了 SIGFPE 信号。

第六步:查看调用栈

(gdb) backtrace
#0  0x00000000004004fa in divide (a=10, b=0) at bug.c:4
#1  0x000000000040052b in main () at bug.c:9

调用栈显示:main 在第9行调用了 divide,然后 divide 在第4行崩溃。信息一目了然。

第七步:退出GDB

(gdb) quit

如果程序还在运行,GDB会问你是否要退出,输入 y 确认就行。

核心命令速记表:

命令 作用 我常用的简写
break 设置断点 b
run 运行程序 r
step 单步进入 s
next 单步跳过 n
print 打印变量 p
backtrace 查看调用栈 bt
continue 继续运行 c
quit 退出GDB q

2.4 核心调试流程总结

整个流程其实就五步:

  1. 编译:加 -g 选项
  2. 启动:三种方式选一种
  3. 设断点:告诉GDB在哪停
  4. 运行:让程序跑起来
  5. 分析:看变量、看栈、单步跟踪

我曾经带过一个新人,他每次调试都从 main 函数第一行开始单步,几百行代码一步步走下来。我说你干嘛不直接在可疑函数设个断点?他恍然大悟。说白了,断点就是你的「快进键」,别傻傻从头跑到尾。

下面这张图帮你把整个流程串起来:

GDB调试核心流程 1. 编译 gcc -g -O0 -o prog prog.c 2. 启动GDB gdb prog / gdb -p PID 3. 设断点 break func 4. 运行 run / r 5. 分析 print / backtrace / step 找到 问题? 是 修复 & 退出 quit / q 否 继续分析 next / step 循环 核心命令:break → run → print/backtrace/step → quit

嗯,这一章的内容就到这儿。记住,调试不是玄学,是有章可循的流程。把编译、启动、断点、运行、分析这五步练熟,你就能解决大部分问题了。