3. GDB入门:GDB的安装与启动、基本命令
调试器这东西,说白了就是程序员的「后悔药」。你写了个bug,程序跑飞了,core dump了,这时候GDB就是你的救命稻草。我刚开始做C/C++开发那会儿,总觉得printf大法好,直到有一次线上问题,日志根本打不出来——嗯,从那以后我老老实实学起了GDB。
3.1 GDB是什么?为什么需要它?
GDB,全称GNU Debugger,是Linux下最经典的调试工具。它让你能「钻进」程序内部,看变量值、看调用栈、看内存布局。你想想看,没有GDB的时候,你只能靠猜;有了GDB,你就能亲眼看到程序是怎么一步步走到崩溃的。
我个人习惯把GDB比作「程序员的CT机」——它能给运行中的程序做断层扫描。不管是段错误、死循环,还是逻辑错误,GDB都能帮你定位。
核心能力一览:
- 启动程序并控制执行流程
- 在指定位置暂停程序(断点)
- 单步执行,逐行观察代码行为
- 查看和修改变量、寄存器、内存
- 分析崩溃时的调用栈(backtrace)
3.2 安装GDB
安装GDB其实很简单。不同发行版命令略有不同,但核心思路一样。
# Ubuntu/Debian
sudo apt-get install gdb
# CentOS/RHEL/Fedora
sudo yum install gdb
# 或者
sudo dnf install gdb
# macOS (通过Homebrew)
brew install gdb
# 验证安装
gdb --version
装完之后,跑一下 gdb --version,看到版本号就说明成功了。我遇到过有人装了半天发现没装对——其实就一条命令的事。
小提示:如果你在macOS上遇到签名问题,记得给GDB做代码签名。具体步骤网上有,这里不展开。Windows用户可以用MinGW或WSL里的GDB。
3.3 启动GDB
启动GDB有三种常见方式。我平时用得最多的是第一种。
# 方式一:直接调试可执行文件
gdb ./my_program
# 方式二:调试core dump文件
gdb ./my_program core
# 方式三:附加到正在运行的进程
gdb -p <PID>
举个例子,假设你有个程序叫 hello,编译时记得加 -g 选项:
gcc -g -o hello hello.c
gdb ./hello
进入GDB后,你会看到 (gdb) 提示符。这时候就可以输入命令了。
注意:编译时一定要加 -g 选项!不加的话,GDB就看不到源码和符号信息,调试体验大打折扣。我曾经帮同事排查问题,发现他编译时没加 -g,结果GDB里全是地址,根本没法看。
3.4 核心命令详解
GDB命令很多,但入门只需要掌握7个。我把它们分成三组来讲。
3.4.1 运行与控制:run, continue
run 命令启动程序。如果程序需要命令行参数,直接跟在后面:
(gdb) run arg1 arg2
(gdb) r # 简写形式
continue 命令让暂停的程序继续执行,直到遇到下一个断点或程序结束:
(gdb) continue
(gdb) c # 简写形式
我有个习惯:第一次启动程序时,先用 run 看看能不能正常跑起来。如果直接崩了,那就省事了——直接看backtrace。
3.4.2 断点与单步:break, next, step
break 设置断点。可以按函数名、行号、文件名+行号来设:
(gdb) break main # 在main函数入口
(gdb) break 42 # 在当前文件的第42行
(gdb) break hello.c:15 # 在hello.c的第15行
(gdb) b main # 简写形式
next 和 step 都是单步执行,但区别很大:
- next (n):逐过程执行。遇到函数调用,直接执行完,不进入函数内部。
- step (s):逐语句执行。遇到函数调用,会进入函数内部。
举个例子:
// 假设有这段代码
int result = add(3, 5); // 第10行
printf("%d\n", result); // 第11行
// 在第10行停下后:
(gdb) next # 直接执行完add函数,停在11行
(gdb) step # 进入add函数内部
什么时候用next,什么时候用step?我的经验是:如果你怀疑某个函数内部有问题,就用step钻进去看;如果只是想跳过它,就用next。说白了,step是「深挖」,next是「跳过」。
3.4.3 查看状态:print, backtrace
print 命令查看变量值。这是最常用的命令之一:
(gdb) print x # 查看变量x的值
(gdb) p x # 简写
(gdb) p &x # 查看x的地址
(gdb) p *ptr # 查看指针指向的值
(gdb) p array[3] # 查看数组元素
(gdb) p func() # 甚至可以调用函数!
backtrace 查看函数调用栈。程序崩溃时,这是第一个要用的命令:
(gdb) backtrace
(gdb) bt # 简写
(gdb) bt full # 显示每个栈帧的局部变量
输出大概长这样:
#0 0x00005555555546a7 in divide (a=10, b=0) at calc.c:8
#1 0x00005555555546d4 in main () at calc.c:14
看到 b=0 了吗?这就是除零错误的根源。我曾经帮一个实习生排查段错误,他盯着代码看了半小时没找到问题。我让他跑一下 bt,三秒钟就定位到了——空指针解引用。
调试黄金流程:
- 程序崩溃 → 先跑
bt看调用栈 - 找到崩溃位置 → 用
list看附近源码 - 用
print查看相关变量值 - 分析变量为什么不对 → 设置断点重新跑
- 用
next/step跟踪变量变化
3.5 实战演练:调试一个除零错误
光说不练假把式。我们拿一个简单的例子走一遍流程。
// calc.c
#include <stdio.h>
int divide(int a, int b) {
return a / b; // 第4行
}
int main() {
int x = 10;
int y = 0;
int z = divide(x, y); // 第9行
printf("Result: %d\n", z);
return 0;
}
编译并启动GDB:
gcc -g -o calc calc.c
gdb ./calc
调试过程:
(gdb) run
Starting program: /tmp/calc
Program received signal SIGFPE, Arithmetic exception.
0x00005555555546a7 in divide (a=10, b=0) at calc.c:4
4 return a / b;
(gdb) bt
#0 0x00005555555546a7 in divide (a=10, b=0) at calc.c:4
#1 0x00005555555546d4 in main () at calc.c:9
(gdb) print b
$1 = 0
(gdb) print a
$2 = 10
(gdb) list
1 #include <stdio.h>
2
3 int divide(int a, int b) {
4 return a / b;
5 }
6
7 int main() {
8 int x = 10;
9 int y = 0;
10 int z = divide(x, y);
看到了吧?b=0,除零错误。修复方法就是在 divide 函数里加个判零检查。
我的习惯:调试时我会在 main 函数入口设个断点,然后一步步走。这样能完整看到程序从启动到崩溃的全过程。有时候bug不在崩溃点,而在某个变量被错误修改的地方。
3.6 常用命令速查表
| 命令 | 简写 | 作用 |
|---|---|---|
| run | r | 启动程序 |
| break | b | 设置断点 |
| next | n | 单步执行(跳过函数) |
| step | s | 单步执行(进入函数) |
| continue | c | 继续执行 |
| p | 打印变量值 | |
| backtrace | bt | 查看调用栈 |
| list | l | 显示源码 |
| quit | q | 退出GDB |
3.7 知识体系图
下面这张图帮你理清GDB入门阶段的核心脉络:
嗯,这张图把GDB入门的核心脉络梳理清楚了。你从安装开始,掌握7个核心命令,然后按照实战流程走一遍,基本就能上手了。记住:调试不是玄学,是有方法论的。
避坑指南:我曾经在调试多线程程序时,用 next 跳过一个线程创建函数,结果程序直接跑飞了。后来才意识到——多线程调试要用 thread 命令切换线程上下文。这个后面章节会细讲,但你现在要知道:GDB默认只跟踪主线程。
好了,GDB入门就讲到这里。这些命令看着简单,但用熟了之后,调试效率能翻好几倍。下一节我们聊GDB的高级断点技巧——条件断点、硬件断点、数据断点,这些才是真正拉开差距的东西。
公众号:蓝海资料掘金营,微信deep3321