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 |
| 打印变量 | p | |
| backtrace | 查看调用栈 | bt |
| continue | 继续运行 | c |
| quit | 退出GDB | q |
2.4 核心调试流程总结
整个流程其实就五步:
- 编译:加
-g选项 - 启动:三种方式选一种
- 设断点:告诉GDB在哪停
- 运行:让程序跑起来
- 分析:看变量、看栈、单步跟踪
我曾经带过一个新人,他每次调试都从 main 函数第一行开始单步,几百行代码一步步走下来。我说你干嘛不直接在可疑函数设个断点?他恍然大悟。说白了,断点就是你的「快进键」,别傻傻从头跑到尾。
下面这张图帮你把整个流程串起来:
嗯,这一章的内容就到这儿。记住,调试不是玄学,是有章可循的流程。把编译、启动、断点、运行、分析这五步练熟,你就能解决大部分问题了。