2. GDB 基础实战:从安装到断点精讲
调试器这东西,说白了就是嵌入式开发者的「第三只眼」。你代码写得再漂亮,跑起来不对,就得靠它来揪出问题。GDB 是我用得最顺手的调试工具,没有之一。今天咱们就把 GDB 的安装、配置和核心调试命令过一遍,尤其是断点——这玩意儿用好了,调试效率能翻倍。
2.1 安装与配置:别让环境卡住你
GDB 的安装其实很简单,但不同平台有点小区别。我习惯在 Linux 主机上开发,然后交叉编译到目标板。
快速安装命令
# Ubuntu/Debian
sudo apt-get install gdb gdb-multiarch
# 如果需要调试 ARM 目标
sudo apt-get install gdb-arm-none-eabi
# 验证安装
gdb --version
嗯,这里要注意:如果你用的是 macOS,建议用 Homebrew 安装,别用自带的那个老版本。
我的习惯:装完 GDB 后,第一件事是配置 .gdbinit 文件。这个文件会在 GDB 启动时自动加载,省得每次敲重复命令。
# ~/.gdbinit
set pagination off
set confirm off
set print pretty on
set history save on
你想想看,每次调试都要敲 set pagination off 多烦人?写到配置文件里,一劳永逸。
2.2 基本调试命令:三板斧先练熟
GDB 的命令很多,但常用的就那么几个。我把它分成三组,你先练熟这组「生存命令」。
| 命令 | 缩写 | 作用 | 我的用法 |
|---|---|---|---|
break |
b |
设置断点 | b main 或 b file.c:42 |
run |
r |
启动程序 | r arg1 arg2 |
next |
n |
单步跳过(不进入函数) | 直接敲 n |
step |
s |
单步进入(进入函数内部) | 直接敲 s |
continue |
c |
继续执行到下一个断点 | 直接敲 c |
print |
p |
打印变量值 | p var 或 p *ptr |
举个例子,你写了个嵌入式程序,跑起来发现某个变量值不对。我会这么做:
# 启动调试
gdb ./my_embedded_program
# 在 main 函数设断点
(gdb) b main
# 运行
(gdb) r
# 单步到怀疑的代码行
(gdb) n
(gdb) n
# 打印变量看看
(gdb) p counter
$1 = 42
# 继续跑
(gdb) c
你看,就这么几行命令,就能把程序跑起来并观察变量。我在项目中遇到过很多次,新手一上来就想着用复杂功能,其实把 n、s、p 这三个命令用熟,80% 的 bug 都能定位到。
避坑指南:我曾经在调试一个 RTOS 任务时,用 next 跳过了任务切换函数,结果发现变量值莫名其妙变了。后来才意识到,next 不会进入函数内部,但函数内部的副作用(比如修改全局变量)依然会发生。所以,当你怀疑某个函数改了全局状态时,记得用 step 进去看。
2.3 断点类型:三种武器各有所长
断点不是只有一种。GDB 支持三种断点类型,每种都有它的适用场景。我画了张图帮你理清关系。
2.3.1 软件断点:最常用,但有限制
软件断点是 GDB 的默认断点。它的原理是把目标指令替换成一条 trap 指令(比如 ARM 的 BKPT),程序执行到这里就会停下来。
# 设置软件断点
(gdb) b main
(gdb) b 42 # 在当前文件第 42 行
(gdb) b file.c:100 # 指定文件
# 查看所有断点
(gdb) info b
# 删除断点
(gdb) d 1 # 删除编号为 1 的断点
软件断点数量不限,这是它的优势。但有个致命限制:不能用在只读内存上。比如你的代码烧在 Flash 里,软件断点就写不进去。
我的经验:调试 Bootloader 时,代码在 Flash 里跑,软件断点根本不管用。我当时折腾了半天,还以为 GDB 坏了。后来换成硬件断点,问题秒解。
2.3.2 硬件断点:Flash 调试的救星
硬件断点利用 CPU 的调试寄存器来实现。它不修改内存,所以可以在 ROM 或 Flash 上使用。代价是数量有限——ARM Cortex-M 系列通常只有 4-8 个。
# 设置硬件断点
(gdb) hbreak main
(gdb) hbreak 0x08000050 # 在指定地址设置
# 查看硬件断点
(gdb) info b
Num Type Disp Enb Address What
1 hbreak keep y 0x08000050 main
你想想看,如果你的程序有 10 个关键路径要调试,但硬件断点只有 4 个,怎么办?我的做法是:先用软件断点调试 RAM 中的代码,最后用硬件断点验证 Flash 中的关键路径。
2.3.3 数据断点:抓野指针的神器
数据断点也叫「监视点」(watchpoint)。它不监控指令执行,而是监控内存地址的读写。这在找野指针、内存越界时特别好用。
# 监控变量读写
(gdb) watch my_var
# 监控变量写操作(只监控写入)
(gdb) awatch my_var
# 监控变量读操作
(gdb) rwatch my_var
# 条件监视点
(gdb) watch my_var if my_var > 100
我曾经遇到一个 bug:某个全局结构体变量莫名其妙被改写了。代码几千行,根本不知道是谁干的。我用了 watch 命令,程序一跑,GDB 立刻停在修改它的那行代码上——原来是一个中断服务程序里不小心用了指针偏移算错了地址。
注意:数据断点也是硬件资源,数量有限。而且它会让程序跑得特别慢,因为 CPU 每执行一条指令都要检查内存访问。所以,别在循环里设 watchpoint,否则你会等到怀疑人生。
2.4 实战组合拳:一个完整的调试流程
光说不练假把式。咱们用一个实际场景串一遍:假设你写了个 LED 闪烁程序,但灯不闪。
// led.c
#include "stm32f4xx.h"
void delay(volatile int count) {
while(count--);
}
int main() {
RCC->AHB1ENR |= (1 << 3); // 使能 GPIOD 时钟
GPIOD->MODER |= (1 << 24); // PD12 设为输出
while(1) {
GPIOD->ODR ^= (1 << 12); // 翻转 PD12
delay(1000000);
}
}
调试步骤:
- 编译带调试信息:
arm-none-eabi-gcc -g -O0 led.c -o led.elf - 启动 GDB:
arm-none-eabi-gdb led.elf - 连接目标板:
(gdb) target remote localhost:3333(假设用 OpenOCD) - 在 main 设断点:
(gdb) b main - 运行:
(gdb) c - 单步执行:
(gdb) n,观察寄存器配置是否正确 - 打印寄存器值:
(gdb) p/x GPIOD->MODER - 如果怀疑 delay 函数:
(gdb) s进入 delay 内部
你看,这一套组合拳下来,问题出在哪儿基本就清楚了。我个人习惯是:先设断点确认程序能跑到怀疑的位置,再用 print 看变量,最后用 watch 抓异常修改。
2.5 小结
GDB 的基础操作其实不难,难的是养成调试思维。别一上来就想着用 IDE 的图形界面,命令行 GDB 在嵌入式调试中更灵活、更可控。尤其是远程调试场景,你不可能把 IDE 装到目标板上。
三种断点各有用途:软件断点日常够用,硬件断点救急 Flash 调试,数据断点专治内存问题。把它们用熟了,调试效率至少提升一倍。