1. GDB入门

1.1 GDB从哪里来?一段关于调试器的故事

说起GDB,得先聊聊它的出身。GDB的全称是GNU Project Debugger,由Richard Stallman在1986年发起。那时候,Unix世界已经有了adb、sdb这些调试工具,但Stallman觉得它们不够自由,也不够强大。于是,GDB作为GNU计划的一部分诞生了。

我个人觉得,GDB最了不起的地方在于它的设计哲学——给你最大的控制权,但绝不替你做决定。它不像某些IDE调试器那样,帮你隐藏底层细节。GDB把内存、寄存器、堆栈这些赤裸裸地摆在你面前。你想想看,这其实是一种信任:它相信你有能力理解这些底层信息。

核心哲学: GDB不只是一个工具,更是一种思维方式。它教会你如何与程序对话,而不是简单地按F5。

我在项目中遇到过一位同事,他习惯用IDE调试,遇到段错误就一脸懵。我让他试试GDB,用bt命令看堆栈,再用frame切到出错的那一层。他看完之后说:「原来程序死在这里了,之前完全不知道。」嗯,这就是GDB的魅力——它让你看到真相。

1.2 为什么系统编程离不开GDB?

系统编程,说白了就是跟操作系统、硬件打交道。你写的代码可能是一个驱动程序、一个网络协议栈,或者一个内存管理器。这些程序一旦出问题,后果往往是段错误、死锁、内存泄漏。普通的printf调试法在这种场景下,基本就是杯水车薪。

为什么会这样?因为系统编程的bug往往具有非确定性。比如多线程竞争,你加一行printf可能就改变了时序,bug反而不出现了。这时候,GDB的断点、观察点、条件断点就派上用场了。

我记得有一次,我在调试一个共享内存的同步问题。两个进程通过信号量通信,但偶尔会出现死锁。用GDB attach到两个进程上,分别查看它们的信号量状态,一下子就找到了问题——一个进程在等待一个永远不会被释放的信号量。这种问题,靠看代码是看不出来的。

调试场景GDB的优势
段错误直接定位到崩溃的代码行和调用栈
内存泄漏结合watchpoint监控内存变化
多线程死锁查看每个线程的堆栈和锁状态
条件断点只在特定条件下暂停,避免大量无用中断

我的建议: 如果你刚开始学系统编程,别急着用IDE。先学会GDB的基本操作,这对你理解程序的执行流程大有帮助。

1.3 安装GDB:比你想象的简单

安装GDB其实没什么好说的,但有几个细节我想提一下。

在Linux上,用包管理器安装就行:

# Ubuntu/Debian
sudo apt-get install gdb

# CentOS/RHEL
sudo yum install gdb

# macOS (通过Homebrew)
brew install gdb

嗯,这里要注意:macOS上安装GDB需要额外签名,否则无法调试其他进程。我曾经在这上面踩过坑——装好了GDB,结果attach不上进程,折腾了半天才发现是签名问题。

避坑指南: macOS用户安装GDB后,需要执行codesign签名。具体步骤可以查看GDB官方文档。我曾经因为跳过这一步,浪费了两个小时。

验证安装是否成功:

gdb --version

如果看到版本号,恭喜你,GDB已经就绪。

1.4 启动GDB:三种方式,各有千秋

启动GDB的方式有三种,我分别说说它们的适用场景。

方式一:直接调试可执行文件

gdb ./my_program

这是最常用的方式。GDB会加载程序,但不会立即执行。你可以先设置断点,再运行。

方式二:调试core dump文件

gdb ./my_program core.12345

程序崩溃后,系统会生成core dump文件。用GDB加载它,可以直接看到崩溃时的现场。我在项目中遇到过一个问题:程序在客户现场崩溃了,但无法复现。客户把core dump发过来,我用GDB一看,发现是一个空指针解引用。嗯,这种场景下,core dump就是救命稻草。

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

gdb -p 12345

这种方式适合调试已经运行的服务。比如一个Web服务器突然卡住了,你可以attach上去,看看它在干什么。

小技巧: attach到进程后,用thread apply all bt可以查看所有线程的堆栈。这个命令我几乎每次调试多线程程序都会用。

1.5 第一个GDB会话:从启动到退出

我们来走一遍完整的流程。假设有一个简单的C程序:

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

int main() {
    int a = 10;
    int b = 0;
    int c = a / b;  // 这里会崩溃
    printf("%d\n", c);
    return 0;
}

编译时记得加-g选项:

gcc -g -o test test.c

启动GDB:

gdb ./test

进入GDB后,你会看到(gdb)提示符。输入run运行程序:

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

Program received signal SIGFPE, Arithmetic exception.
0x000055555555460a in main () at test.c:6
6           int c = a / b;

GDB直接告诉我们:程序在第6行收到了SIGFPE信号(算术异常)。用list查看附近代码:

(gdb) list
1       #include <stdio.h>
2
3       int main() {
4           int a = 10;
5           int b = 0;
6           int c = a / b;
7           printf("%d\n", c);
8           return 0;
9       }

用print查看变量值:

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

问题一目了然:b是0,除以0导致崩溃。退出GDB用quit:

(gdb) quit

我的习惯: 每次调试完,我会用quit退出。如果GDB提示有未保存的断点,我会用save breakpoints保存下来,下次调试直接加载。

1.6 本章知识体系

下面这张图展示了本章的核心内容:

GDB入门 历史与哲学 1986年由Stallman发起 GNU计划的一部分 哲学:最大控制权 核心地位 系统编程必备 段错误/死锁/内存泄漏 非确定性bug调试 安装 Linux: apt/yum macOS: brew + 签名 验证: gdb --version 三种启动方式 gdb ./program gdb ./program core gdb -p PID 基本命令 run / list / print break / continue quit

这张图把本章的知识点串起来了。从GDB的历史哲学,到它在系统编程中的核心地位,再到安装和启动方式,最后是基本命令。你想想看,这些内容其实是一个完整的闭环——知道它是什么,为什么重要,怎么装,怎么用。

总结一下: GDB不是那种「学一次就忘」的工具。它值得你花时间深入掌握。我见过太多开发者,遇到bug就束手无策,其实很多时候,GDB的几条命令就能解决问题。