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 知识体系总览

下面这张图把这八个命令的关系梳理清楚了。我建议你把它存下来,调试时对照着用。

GDB 八大命令知识体系 GDB 调试会话 run / continue next / step break watch print backtrace 启动/复位调试 run + break 单步追踪逻辑 next + step + print 崩溃定位 backtrace + frame 调试流程建议 设断点 → run → 观察行为 → next/step 追踪 → print 检查数据 → bt 回溯异常

3.8 实战组合拳

光记命令没用,得知道什么时候用什么。我总结了几种常见场景的「命令组合」:

  1. 定位 HardFault:bt → frame N → info locals → p/x $LR
  2. 检查外设配置:b HAL_XXX_Init → run → p/x *(reg_addr) → c
  3. 追踪变量变化:watch my_var → c → 每次写入自动暂停 → bt 看谁改的
  4. 调试中断响应:b ISR_Entry → run → s 单步 → p/x $IPSR

核心要点:

  • run/continue 控制执行流,next/step 控制粒度
  • break 设陷阱,watch 盯数据
  • print 看眼前,backtrace 看全局
  • 调试优化代码时,优先用硬件断点和 watchpoint