GDB 入门:调试器中的瑞士军刀
说实话,我第一次接触 GDB 是在大学做操作系统实验的时候。那时候写了个内核模块,一运行就死机,连个错误提示都没有。急得我团团转。后来师兄甩过来一句:「用 GDB 啊。」嗯,从那天起,我就再也没离开过这个工具。
GDB,全称 GNU Debugger。说白了,就是一个让你能「看」进程序内部的神器。你写的 C/C++ 代码跑起来出了奇怪的问题,不用靠猜,不用靠 printf 大法,直接用 GDB 把程序停下来,看看变量值、看看堆栈、看看内存——一切清清楚楚。
GDB 是什么?
GDB 是一个可调试多种语言的源码级调试器。它支持 C、C++、Go、Rust、Fortran 等等。我个人的习惯是,只要编译时加了 -g 选项,GDB 就能把机器码和源代码对应起来。你想想看,这意味着什么?意味着你可以在源代码行上设断点,单步执行,甚至修改变量的值。
它的核心能力其实就三条:
- 控制执行:让程序跑、停下来、一步一步走
- 观察状态:看变量、寄存器、内存、堆栈
- 修改状态:改变量值、改内存、甚至改指令
我在项目中遇到过最典型的场景:一个服务跑着跑着就 segfault 了,日志里啥也没有。用 GDB 挂上去,等它崩,然后 bt 一下,调用链清清楚楚——原来是某个指针被 free 了两次。这种问题,靠肉眼读代码,读三天也未必找得到。
一句话总结:GDB 就是程序员的「CT 扫描仪」。你不需要拆开程序,就能看到它内部的一切。
GDB 的历史:一个四十年的故事
GDB 是 Richard Stallman 在 1986 年开始写的。没错,就是那个 GNU 项目的发起人。他当时写 GCC 编译器,发现缺一个配套的调试器,于是自己动手写了 GDB。
你可能觉得,一个四十年前的工具,现在还能用?嗯,不仅能用,而且依然是业界标准。为什么?因为它的设计太扎实了。GDB 采用了一种「目标-前端」分离的架构:
- 前端:处理用户交互(命令行、GUI、IDE 集成)
- 目标:处理底层调试逻辑(ptrace、硬件调试寄存器)
这种设计让它能适应各种平台。从 x86 到 ARM,从 Linux 到 macOS,甚至嵌入式系统,GDB 都能搞定。我记得有一次调试一个跑在 MIPS 架构路由器上的程序,GDB 照样稳稳地工作。
| 版本 | 发布时间 | 重要特性 |
|---|---|---|
| GDB 1.0 | 1986 | 基本调试功能 |
| GDB 4.0 | 1990 | 支持 C++ |
| GDB 7.0 | 2009 | Python 脚本扩展 |
| GDB 10.0 | 2020 | 支持 Rust、改进的调试信息 |
安装 GDB:三分钟搞定
安装 GDB 其实很简单。不同系统的方法我列一下:
# Ubuntu / Debian
sudo apt-get install gdb
# CentOS / RHEL
sudo yum install gdb
# macOS(用 Homebrew)
brew install gdb
# 从源码编译(如果你非要这么干)
wget https://ftp.gnu.org/gnu/gdb/gdb-14.1.tar.gz
tar -xzf gdb-14.1.tar.gz
cd gdb-14.1
./configure
make -j$(nproc)
sudo make install
我个人建议用包管理器安装,省事。从源码编译虽然能拿到最新版,但编译过程挺耗时的。除非你需要某些特定补丁,否则没必要。
小提示:安装完后,在终端输入 gdb --version,如果看到版本号,就说明装好了。我每次在新机器上干活,第一件事就是敲这个命令。
基本使用流程:从编译到调试
好,现在 GDB 装好了。怎么用?我带你走一遍最基础的流程。
第一步:编译带调试信息的程序
这是关键。编译时必须加 -g 选项:
gcc -g -o myprogram myprogram.c
不加 -g,GDB 就看不到源代码信息,只能看到汇编。我曾经犯过这个错,调试了半天全是地址,那叫一个痛苦。
第二步:启动 GDB
gdb ./myprogram
你会看到 GDB 的提示符 (gdb)。这时候程序还没跑,只是被加载进来了。
第三步:设置断点
(gdb) break main
(gdb) break 42 # 在第42行设断点
(gdb) break my_function # 在函数入口设断点
第四步:运行程序
(gdb) run
程序会一直跑到第一个断点处停下来。这时候你可以:
print x— 看变量 x 的值next— 执行下一行(不进入函数)step— 执行下一行(进入函数)continue— 继续运行到下一个断点list— 查看当前附近的源代码
第五步:退出
(gdb) quit
如果程序还在运行,GDB 会问你是否要终止。输入 y 就行。
注意:如果程序崩溃了(比如 segfault),GDB 会自动停在崩溃的位置。这时候千万别急着退出,先敲 bt 看调用栈,这是定位崩溃最直接的方法。
一个完整的例子
假设我们有这么个程序:
// bug.c
#include <stdio.h>
void crash_me() {
int *p = NULL;
*p = 42; // 这里会崩
}
int main() {
printf("Hello\n");
crash_me();
printf("World\n");
return 0;
}
编译并调试:
$ gcc -g -o bug bug.c
$ gdb ./bug
(gdb) run
Starting program: ./bug
Hello
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555159 in crash_me () at bug.c:5
5 *p = 42;
(gdb) bt
#0 0x0000555555555159 in crash_me () at bug.c:5
#1 0x0000555555555173 in main () at bug.c:10
(gdb) print p
$1 = (int *) 0x0
(gdb) quit
看到了吗?GDB 直接告诉我们:p 是空指针,在第 5 行解引用时崩了。调用栈显示是从 main 的 10 行调过来的。问题一目了然。
知识体系总览
下面这张图,是我自己整理的 GDB 入门知识结构。你跟着这个脉络学,不会乱:
这张图把 GDB 入门要掌握的东西都串起来了。你照着这个结构学,先搞定「控制执行」,再学「观察状态」,最后才是「修改状态」。别一上来就想着改内存,先把断点和单步走顺了再说。
我的建议:刚开始用 GDB,别急着记命令。先记住三个:break、run、print。这三个能解决 80% 的问题。剩下的命令,遇到具体场景再查,用多了自然就记住了。
好了,GDB 入门就讲到这里。记住,调试不是魔法,是技术。GDB 就是你手里最趁手的工具。多用、多练,慢慢你就会发现——那些曾经让你抓狂的 bug,其实都有迹可循。