GDB调试基础:从安装到远程调试实战
说实话,GDB 这东西,我刚开始接触嵌入式 Rust 时觉得它又老又土。命令行界面,黑乎乎的,哪有 IDE 点按钮来得爽?但后来被现实狠狠教育了一回——有一次 RTIC 任务莫名其妙跑飞,IDE 调试器直接卡死,最后还是靠 GDB 一条命令一条命令把问题揪出来的。从那以后,我再也不敢小看这个"老古董"了。
GDB 的安装与配置
先说说安装。不同平台略有差异,但核心思路一样:
| 平台 | 安装命令 | 备注 |
|---|---|---|
| Ubuntu/Debian | sudo apt install gdb-multiarch |
推荐,支持多架构 |
| macOS | brew install arm-none-eabi-gdb |
针对 ARM 目标 |
| Windows | 下载 ARM 官方 GDB 包 | 或使用 WSL + gdb-multiarch |
我个人习惯用 gdb-multiarch。为什么?因为嵌入式项目经常换芯片——今天 STM32,明天 nRF52840,后天可能又切到 RISC-V。一个 GDB 通吃所有架构,省心。
gdb-multiarch --version,确认支持的目标架构里有 arm 或 riscv。我见过有人装了半天,结果发现装的是 x86 版 GDB,连目标板都连不上。
常用命令:调试三板斧
GDB 命令很多,但日常调试真正高频使用的,其实就那么几个。我按使用频率排个序:
1. 断点管理:break
break 是调试的起点。没有断点,程序一溜烟就跑完了,你啥也看不到。
(gdb) break main # 在 main 函数入口设断点
(gdb) break 42 # 在当前文件第 42 行设断点
(gdb) break src/main.rs:56 # 指定文件的行号
(gdb) break my_function # 在函数名设断点
(gdb) info breakpoints # 列出所有断点
(gdb) delete 1 # 删除编号为 1 的断点
嗯,这里要注意:Rust 的函数名经过名字修饰(name mangling),直接写 Rust 源码里的函数名可能找不到。怎么办?用 break 加 Tab 键补全,GDB 会列出所有匹配的符号。
2. 单步执行:next 与 step
这两个命令的区别,说白了就是"跨不跨函数调用":
| 命令 | 行为 | 适用场景 |
|---|---|---|
next (n) |
执行当前行,遇到函数调用直接跳过 | 确认整体流程时用 |
step (s) |
进入函数内部,一行一行跟进去 | 怀疑某个函数有 bug 时用 |
我曾经犯过一个低级错误:在 RTIC 的中断处理函数里用 step 单步跟踪,结果 GDB 直接卡死。为什么?因为中断处理函数里调用了临界区保护,单步执行时锁没释放,死锁了。后来我学乖了——中断处理函数里尽量用 next,别轻易 step。
3. 继续执行:continue
continue (c) 就是"放行",让程序一直跑到下一个断点或结束。配合条件断点特别好用:
(gdb) break 42 if counter > 100 # 条件断点
(gdb) continue # 放行,直到满足条件
4. 查看数据:print
print 是调试的"眼睛"。没有它,你就像在黑屋子里修电路:
(gdb) print my_variable # 打印变量值
(gdb) print/x 0x1234 # 十六进制显示
(gdb) print &my_variable # 打印变量地址
(gdb) print *ptr # 解引用指针
(gdb) display my_variable # 每次停顿时自动打印
print 一个已经 move 掉的变量,GDB 可能显示为 <optimized out> 或乱值。这不是 GDB 的问题,是变量确实已经不存在了。遇到这种情况,检查一下变量的生命周期。
5. 回溯调用栈:backtrace
backtrace (bt) 是故障定位的"核武器"。程序崩溃时,第一件事就是打 bt:
(gdb) backtrace # 完整调用栈
(gdb) backtrace 5 # 只看最近 5 层
(gdb) frame 2 # 切换到第 2 层栈帧
(gdb) info locals # 查看当前帧的局部变量
我记得有一次调试 RTIC 的优先级反转问题,程序跑到某个任务里就 hard fault。打 bt 一看,调用栈里居然出现了三个不同的中断处理函数嵌套——明显是优先级配置出了问题。没有 backtrace,这种问题靠肉眼根本看不出来。
远程调试原理
嵌入式调试和桌面程序最大的区别是什么?目标程序不在你的电脑上跑,而是在开发板上跑。这就引出了远程调试的概念。
说白了,远程调试就是"两头分家":
- GDB 客户端:跑在你的 PC 上,负责接收你的命令、解析符号、显示源码
- GDB 服务器:跑在调试器上(如 J-Link、ST-Link、OpenOCD),负责和芯片通信、控制执行、读写内存
两者之间通过 TCP/IP 或 USB 通信,协议是 GDB 远程串行协议(Remote Serial Protocol, RSP)。
- 启动 GDB 服务器(如 OpenOCD),监听某个端口(默认 3333)
- 启动 GDB 客户端,连接服务器:
target remote localhost:3333 - 加载符号:
file target/thumbv7em-none-eabihf/debug/my_app - 开始调试:
load下载固件,monitor reset halt复位并暂停
下面这张图把整个流程串起来了:
实战:连接开发板
理论说完了,来点实际的。假设你有一块 STM32 开发板,用 OpenOCD 做 GDB 服务器:
# 终端 1:启动 OpenOCD
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# 终端 2:启动 GDB 并连接
gdb-multiarch target/thumbv7em-none-eabihf/debug/my_app
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) load
(gdb) break main
(gdb) continue
target remote 连不上,折腾了半天发现是 OpenOCD 的端口号不是默认的 3333。用 netstat -an | grep 3333 检查一下端口是否在监听。另外,有些调试器(如 J-Link)默认端口是 2331,记得确认。
连接成功后,你就可以像调试本地程序一样操作了。设断点、看变量、单步跟踪、回溯调用栈——所有命令都一样。唯一的区别是,每次 continue 后,程序是在开发板上真实运行的,不是模拟器。
嗯,最后说一句:GDB 的 .gdbinit 文件值得花点时间配置。我一般会把常用的宏和别名写进去,比如 alias bt = backtrace、alias r = run,省得每次敲全名。调试效率就是这么一点点抠出来的。