1. GDB入门

大家好,我是你们的老朋友。今天咱们聊聊GDB——这个陪伴了我十几年的调试器。

说实话,刚入行那会儿,我对GDB是又爱又恨。爱的是它功能强大,恨的是命令行操作太不友好。但后来我明白了,越是强大的工具,越值得花时间去掌握。

1.1 GDB的历史与定位

GDB全称是GNU Debugger,诞生于1988年。那会儿我还没入行呢(笑)。它的创造者是Richard Stallman,GNU项目的发起人。

为什么要聊历史?因为理解GDB的定位,能帮你更好地用它。

GDB的核心定位就三个:

  • 源码级调试:让你在源代码层面观察程序行为
  • 跨语言支持:C、C++、Go、Rust... 主流语言基本都支持
  • 远程调试:这是嵌入式开发的命脉

重要认知:GDB不是IDE里的那个调试按钮。它是调试器的"底层引擎"。VS Code、Eclipse、CLion这些IDE的调试功能,底层调用的都是GDB。

1.2 GDB在嵌入式与服务器开发中的核心价值

我经常跟团队里的新人说:调试器是程序员的第三只眼。尤其在下面两个场景,GDB的价值无可替代。

嵌入式开发:没有GDB,寸步难行

嵌入式开发有个特点:目标板资源极其有限。没有显示器、没有键盘、甚至没有操作系统。你想想看,printf打印调试?不存在的。

我在做STM32项目时遇到过这种情况:程序跑飞了,但不知道在哪一行。用LED闪烁定位?太原始了。用逻辑分析仪?太麻烦。

GDB远程调试一上,直接断点打在可疑位置,单步执行,寄存器值一目了然。嗯,这才是调试该有的样子。

调试方式 适用场景 效率
printf打印 简单逻辑验证 低
LED闪烁 硬件调试 极低
GDB远程调试 复杂逻辑、性能问题 高

服务器开发:线上问题排查的利器

服务器端开发,最怕什么?线上程序崩溃,core dump一坨,但不知道原因。

我曾经接手过一个项目,服务每隔3小时就挂一次。日志看不出问题,代码review也找不到原因。最后用GDB分析core dump,发现是一个野指针在特定条件下触发的。

说白了,GDB就是你的"事后诸葛亮"。程序挂了,它还能告诉你挂在哪里、为什么挂。

1.3 GDB的安装与基本启动

安装GDB其实很简单。但不同平台有些小差异,我踩过坑,所以给你说说。

Linux平台

# Ubuntu/Debian
sudo apt-get install gdb

# CentOS/RHEL
sudo yum install gdb

# 验证安装
gdb --version

我的建议:尽量用系统包管理器安装。自己编译GDB虽然能获得最新版本,但依赖问题容易让人头疼。

macOS平台

# 使用Homebrew
brew install gdb

# 注意:macOS需要额外签名
# 否则GDB无法附加到进程

这里有个坑:macOS从Mojave开始,对调试器的权限限制很严格。我曾经折腾了一下午,才发现是签名问题。

嵌入式交叉编译环境

嵌入式开发需要交叉编译的GDB。比如你开发ARM Cortex-M,需要在x86的电脑上调试ARM的程序。

# 以ARM为例
sudo apt-get install gdb-multiarch

# 或者编译特定架构的GDB
./configure --target=arm-none-eabi
make
sudo make install

基本启动方式

启动GDB有三种常见方式:

  1. 直接调试可执行文件
gdb ./my_program
  1. 附加到正在运行的进程
gdb -p 1234   # 1234是进程PID
  1. 分析core dump文件
gdb ./my_program core.1234

注意:分析core dump时,可执行文件必须包含调试符号(编译时加-g选项)。否则你看到的只有一堆地址,没有源码信息。

1.4 GDB调试的核心流程

我画了一张图,帮你理解GDB调试的整体流程:

GDB调试核心流程 编译(-g选项) 启动GDB 设置断点 运行 断点命中? 单步调试/查看变量 继续运行/等待下次命中

这张图展示了GDB调试的完整闭环。你想想看,从编译开始,到设置断点,再到运行和调试,每一步都有讲究。

1.5 我的第一个GDB调试会话

光说不练假把式。咱们来个最简单的例子:

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

int add(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int x = 10;
    int y = 20;
    int sum = add(x, y);
    printf("sum = %d\n", sum);
    return 0;
}

编译并调试:

# 编译(一定要加-g)
gcc -g -o test test.c

# 启动GDB
gdb ./test

# 在GDB内部
(gdb) break main
(gdb) run
(gdb) next
(gdb) print x
$1 = 10
(gdb) print y
$2 = 20
(gdb) step
(gdb) print result
$3 = 30
(gdb) continue

小技巧:GDB支持Tab补全。输入bre然后按Tab,会自动补全为break。这个习惯我从入行就养成了,效率提升不少。

1.6 避坑指南

最后,分享几个我踩过的坑:

  • 忘记加-g选项:编译出来的程序没有调试符号,GDB只能看到地址,看不到源码。我曾经因为这个排查了半小时。
  • 优化级别太高:-O2或-O3优化后,代码执行顺序可能改变,单步调试会跳来跳去。调试阶段建议用-O0。
  • strip操作:发布版本通常会strip掉符号表。但调试时一定要保留未strip的版本。

嗯,第一章就到这里。记住一句话:GDB不是万能的,但没有GDB是万万不能的。