1. GDB入门

说实话,我刚开始做嵌入式开发那会儿,调试全靠printf大法。代码里到处塞打印语句,编译、下载、看串口输出,改一行代码就得重复整个流程。直到有一天,我的导师扔给我一句话:「去学学GDB,别再用这种原始人方式了。」

嗯,从那以后,我的调试效率至少翻了三倍。今天咱们就来聊聊GDB——这个陪伴了我十几年的老朋友。

1.1 GDB的历史:一个传奇的诞生

GDB的全称是GNU Debugger。它诞生于1988年,由Richard Stallman发起,是GNU项目的重要组成部分。你想想看,一个工具能活30多年还在持续更新,这本身就说明了很多问题。

我记得最早接触GDB是在大学做Linux内核实验的时候。那时候的GDB还是纯命令行界面,连个彩色输出都没有。但就是这样一个「简陋」的工具,帮我找到了一个隐藏了三个星期的内存越界bug。从那天起,我就再也没怀疑过它的价值。

GDB的发展历程中有几个关键节点:

  • 1988年:Richard Stallman开始编写GDB,最初只支持C语言
  • 1990年代:逐步支持C++、Fortran、Java等语言
  • 2000年代:加入远程调试功能,开始支持嵌入式开发
  • 2010年代至今:支持Python脚本扩展、多线程调试、硬件调试器等高级特性

核心认知:GDB不是某个公司的商业产品,而是开源社区集体智慧的结晶。它的设计哲学是「小而美」——核心功能稳定可靠,扩展能力通过脚本和插件实现。

1.2 GDB能做什么:不只是「看变量」那么简单

很多人以为GDB就是个「断点+看变量」的工具。说实话,这种理解太肤浅了。我在项目中用GDB解决过的问题,远不止这些。

GDB的核心能力可以归纳为以下几类:

能力分类 具体功能 我的实际应用场景
断点管理 普通断点、条件断点、硬件断点、观察点 调试RTOS任务切换时,用条件断点只在特定任务ID触发
程序控制 单步执行、跳过函数、继续执行、反向调试 分析死锁问题时,用反向调试回溯到锁获取的瞬间
数据查看 变量值、内存内容、寄存器、堆栈回溯 排查栈溢出时,用堆栈回溯找到调用链的异常点
代码修改 运行时修改变量值、调用函数、跳转执行 临时绕过某个函数的返回值检查,验证后续逻辑
远程调试 通过串口、网络、JTAG连接目标板 调试ARM Cortex-M芯片时,用OpenOCD+GDB组合
脚本扩展 Python脚本自动化调试流程 批量检查所有链表节点是否损坏,写个脚本一键执行

我的经验:GDB最强大的地方不是单个功能,而是这些功能的组合。比如「条件断点+观察点+堆栈回溯」这套组合拳,几乎能解决90%的运行时bug。

1.3 GDB的安装:跨平台其实没那么复杂

安装GDB这件事,不同平台有不同的玩法。我这些年折腾过各种环境,踩过不少坑,下面直接给你最靠谱的方案。

Linux平台

这是GDB的「主场」,安装最简单:

# Ubuntu/Debian
sudo apt-get install gdb

# CentOS/RHEL/Fedora
sudo yum install gdb

# 验证安装
gdb --version

我个人习惯用源码编译安装最新版,因为有些老版本对C++17的支持不太好:

# 下载源码
wget https://ftp.gnu.org/gnu/gdb/gdb-14.1.tar.gz
tar -xzf gdb-14.1.tar.gz
cd gdb-14.1

# 配置并编译
./configure --prefix=/usr/local/gdb-14.1
make -j$(nproc)
sudo make install

注意:如果你要调试嵌入式目标板,编译时一定要加上--enable-targets=all参数。我曾经因为没加这个参数,折腾了一整天才发现GDB不认识ARM的二进制文件。

macOS平台

macOS上推荐用Homebrew安装:

brew install gdb

# 注意:macOS需要额外签名
# 创建证书并授权
codesign -fs gdb-cert /usr/local/bin/gdb

这里有个坑——macOS的SIP(系统完整性保护)会阻止GDB附加到其他进程。我当初搞这个签名搞了俩小时,后来发现直接关闭SIP反而更省事。当然,生产环境不建议这么干。

Windows平台

Windows上我推荐两种方式:

  • MinGW-w64:轻量级,适合命令行爱好者
  • MSYS2:包管理更完善,我目前的主力方案
# MSYS2安装
pacman -S mingw-w64-x86_64-gdb

# 或者下载MinGW-w64的预编译包
# 解压后把bin目录加入PATH即可

1.4 基本启动:从Hello World开始

理论说完了,咱们直接上手。写一个最简单的C程序:

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

int main() {
    int a = 10;
    int b = 20;
    int c = a + b;
    printf("c = %d\n", c);
    return 0;
}

编译时一定要加-g参数,否则GDB看不到源码和变量名:

gcc -g -o hello hello.c

启动GDB有三种方式,我分别说说:

方式一:直接启动并加载程序

gdb hello

进入GDB后,你会看到类似这样的提示符:

(gdb) 

这时候程序还没运行。输入run或者简写r就能启动:

(gdb) run
Starting program: /home/user/hello 
c = 30
[Inferior 1 (process 12345) exited normally]
(gdb) 

方式二:先启动GDB,再加载程序

gdb
(gdb) file hello
(gdb) run

这种方式的好处是,你可以先设置一些全局选项,再加载程序。我经常这么干:

gdb
(gdb) set pagination off
(gdb) set print pretty on
(gdb) file hello

方式三:调试正在运行的进程

这个在嵌入式开发中特别有用。比如你的程序已经跑起来了,突然卡住了:

# 先找到进程ID
ps aux | grep hello

# 附加到进程
gdb -p 12345

重要提醒:附加到正在运行的进程时,程序会暂停。调试完记得用detach命令分离,否则程序会一直卡着。我曾经在服务器上调试完忘了detach,导致线上服务挂了半小时...嗯,那天的复盘会开得特别长。

1.5 第一个调试会话:走一遍流程

咱们用刚才的hello程序,走一遍完整的调试流程:

gdb hello

(gdb) break main          # 在main函数入口设断点
(gdb) run                  # 运行,停在main入口
(gdb) next                 # 单步执行,跳过a=10
(gdb) print a              # 查看a的值,此时a还没初始化
(gdb) next                 # 执行a=10
(gdb) print a              # 查看a的值,应该是10
(gdb) next                 # 执行b=20
(gdb) print b              # 查看b的值
(gdb) next                 # 执行c=a+b
(gdb) print c              # 查看c的值,应该是30
(gdb) continue             # 继续执行到结束
(gdb) quit                 # 退出GDB

看到没?每一步都能看到变量的变化。这就是GDB比printf强的地方——你不用提前想好要打印什么,随时可以查看任意变量的值。

小技巧:print命令可以简写为p,next可以简写为n,continue可以简写为c。调试多了你会发现,少打几个字母能省不少时间。

1.6 本章知识体系

下面这张图是我自己整理的GDB入门知识结构,你可以把它当作学习路线图:

GDB入门知识体系 GDB调试器 发展历史 1988年GNU项目 · 30+年演进 核心能力 断点 · 单步 · 数据查看 · 远程调试 跨平台安装 Linux · macOS · Windows Stallman发起 多语言支持 Python扩展 断点管理 程序控制 数据查看 apt/yum安装 源码编译 Homebrew/MSYS2 三种启动方式 gdb 程序名 gdb → file加载 gdb -p 进程ID

这张图把GDB入门的核心脉络梳理清楚了。从历史背景到核心能力,再到安装方式和启动方法,每一步都是你后续深入学习的基础。

说实话,GDB入门并不难。难的是养成用GDB调试的习惯。我见过太多开发者,明明装了GDB,遇到bug第一反应还是加printf。我的建议是:从今天开始,任何bug都先用GDB试试。哪怕只是看个变量值,也比printf快得多。