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