3. GDB 基本命令:run、continue、next、step、break、watch、print、backtrace
调试器这东西,说白了就是让你能「看」进芯片内部。GDB 的命令看着多,但真正高频使用的,其实就这八条。我刚开始用 GDB 时也记不住那么多,后来发现把这几个玩熟了,90% 的调试场景都能应付。
咱们一个一个来。我会结合 Cortex-M 的实际场景讲,不是光念手册。
3.1 启动与暂停:run 和 continue
run(r)——启动程序。在 Cortex-M 开发中,我们通常用 load 命令把固件烧进去,然后 run 让芯片从复位向量开始跑。不过要注意,如果芯片已经在跑,run 会先发 kill 信号终止当前进程,再重新加载。
实际用法:
(gdb) load # 烧录固件到 Flash
(gdb) run # 从复位开始执行
(gdb) run <args> # 带参数启动(裸机下很少用)
continue(c)——让暂停的程序继续跑。我遇到过不少新手,打完断点后不知道让程序继续,盯着屏幕发呆。其实 c 就是「接着跑」的意思。
我的习惯:调试外设驱动时,我经常在初始化函数末尾设断点,然后 run 一次,检查寄存器配置是否正确。确认无误后 c 继续,观察外设行为。
3.2 单步执行:next 和 step
这两个命令是调试的「显微镜」。区别很关键:
| 命令 | 缩写 | 行为 | Cortex-M 场景 |
|---|---|---|---|
| next | n | 单步执行,不进入函数内部 | 跳过 HAL 库函数,只看自己写的逻辑 |
| step | s | 单步执行,进入函数内部 | 追踪中断服务函数或底层驱动 |
next(n)——把函数调用当成一条指令跳过。比如你调了 HAL_UART_Transmit(),next 不会钻进那个几百行的库函数,而是直接执行完,停在下一行。
step(s)——会钻进函数内部。我调试中断嵌套问题时,经常用 step 进入异常处理函数,看栈帧是怎么保存的。
我曾经踩过的坑:在优化模式下(-O2 或 -O3),next 和 step 的行为会变得「诡异」。编译器可能重排指令,导致单步时跳来跳去。我的建议是:调试阶段用 -O0 -g 编译,别省这点优化时间。
3.3 断点管理:break
break(b)——设置断点。Cortex-M 支持硬件断点和软件断点。GDB 会自动选择,但了解区别有助于排查问题。
(gdb) b main # 在 main 函数入口设断点
(gdb) b 42 # 在当前文件第 42 行设断点
(gdb) b file.c:100 # 在指定文件的行号设断点
(gdb) b *0x08000100 # 在绝对地址设断点(常用于外设寄存器)
(gdb) info b # 列出所有断点
(gdb) delete 1 # 删除编号为 1 的断点
硬件断点数量有限(Cortex-M 通常只有 4-6 个),软件断点则不限数量。但软件断点会修改 Flash 内容(插入 BKPT 指令),在只读区域或 OTP 区域会失败。
避坑指南:调试 Bootloader 时,千万别在 Flash 擦写期间设软件断点。我曾经这么干过,结果断点指令被当成数据擦掉了,程序直接跑飞。后来我改用硬件断点,或者等擦写完成再设。
3.4 数据监视:watch
watch——监视变量或内存地址的变化。当被监视的值被写入时,程序自动暂停。
(gdb) watch my_var # 监视变量 my_var 的写入
(gdb) watch *(int*)0x20001000 # 监视地址 0x20001000 的 4 字节
(gdb) awatch my_var # 读写都触发(access watch)
(gdb) rwatch my_var # 只读触发(read watch)
硬件 watchpoint 数量更少(通常 2-4 个),而且 Cortex-M0/M0+ 不支持 awatch 和 rwatch,只有 watch(写入监视)。
实战场景:调试 DMA 传输时,我经常用 watch 监视目标缓冲区的第一个字。DMA 写完数据后,程序自动暂停,我就能确认传输是否完成、数据是否正确。
3.5 数据查看:print
print(p)——查看变量或表达式的值。这是调试时用得最频繁的命令之一。
(gdb) p my_var # 打印变量值
(gdb) p/x my_var # 十六进制显示
(gdb) p/t my_var # 二进制显示
(gdb) p &my_var # 打印变量地址
(gdb) p *(struct_type*)addr # 按结构体类型解析内存
(gdb) p $r0 # 打印寄存器 r0 的值
在 Cortex-M 调试中,print 还能直接读取外设寄存器:
(gdb) p/x *(uint32_t*)0x40023800 # 读取 RCC 时钟配置寄存器
(gdb) p/x $PC # 查看当前程序计数器
(gdb) p/x $LR # 查看返回地址
我的习惯:调试中断优先级问题时,我经常用 p/x $BASEPRI 查看当前屏蔽优先级,用 p/x $IPSR 查看当前中断号。这些特殊寄存器在 GDB 里可以直接访问。
3.6 调用栈回溯:backtrace
backtrace(bt)——打印函数调用栈。这是定位崩溃问题的利器。
(gdb) bt # 打印完整调用栈
(gdb) bt full # 打印调用栈 + 局部变量
(gdb) bt 5 # 只打印最近 5 层
(gdb) frame 2 # 切换到第 2 层栈帧
(gdb) info locals # 查看当前栈帧的局部变量
当程序跑进 HardFault 时,bt 能告诉你:是从哪个函数、哪条指令跳进来的。配合 frame 切换栈帧,可以逐层检查参数和局部变量。
我曾经遇到的坑:栈溢出时,bt 打印的信息可能是错的。因为栈指针已经被破坏,GDB 无法正确回溯。这时候我会先检查 $sp 是否在合法范围内,或者用 monitor stack(J-Link 命令)查看栈使用情况。
3.7 知识体系总览
下面这张图把这八个命令的关系梳理清楚了。我建议你把它存下来,调试时对照着用。
3.8 实战组合拳
光记命令没用,得知道什么时候用什么。我总结了几种常见场景的「命令组合」:
- 定位 HardFault:
bt→frame N→info locals→p/x $LR - 检查外设配置:
b HAL_XXX_Init→run→p/x *(reg_addr)→c - 追踪变量变化:
watch my_var→c→ 每次写入自动暂停 →bt看谁改的 - 调试中断响应:
b ISR_Entry→run→s单步 →p/x $IPSR
核心要点:
- run/continue 控制执行流,next/step 控制粒度
- break 设陷阱,watch 盯数据
- print 看眼前,backtrace 看全局
- 调试优化代码时,优先用硬件断点和 watchpoint