3. GDB 基础命令与 JTAG 操作:load、continue、break、step、info registers、monitor halt/reset
好,咱们进入实战环节。这一章我带你过一遍 GDB 配合 JTAG 最常用的几个命令。说实话,很多驱动工程师调试死锁时,翻来覆去用的就是这些。你把这几个命令练熟了,80% 的现场都能应付。
3.1 建立连接:load 与目标板握手
先说说 load。这个命令的作用是把你的 ELF 或者二进制镜像烧写到目标板的 RAM 或 Flash 里。我刚开始用的时候,总以为 load 只是把文件传过去,其实它背后做了三件事:擦除、写入、校验。
(gdb) target remote localhost:2331
(gdb) load /home/project/my_driver.elf
Loading section .text, size 0x2a4c lma 0x80000000
Loading section .data, size 0x1a0 lma 0x80002a4c
Start address 0x80000000, load size 0x2bec
Transfer rate: 112 KB/sec, 12 bytes/write.
看到这个输出,说明烧写成功了。注意那个 Start address,它就是复位向量或者入口地址。我个人习惯在 load 之后立刻执行一次 monitor reset,确保 CPU 从干净状态开始跑。
load 之后可能需要断电重启。RAM 运行的话,直接 continue 就行。
3.2 运行与暂停:continue 与 monitor halt/reset
continue(简写 c)就是让 CPU 跑起来。但死锁排查的关键在于——你得知道什么时候让它停。
monitor halt 是 JTAG 的硬暂停。它不依赖软件断点,直接通过调试接口把 CPU 冻住。我在项目中遇到过一种情况:系统已经死锁,软件断点根本触发不了,这时候 monitor halt 是唯一的救命稻草。
(gdb) monitor halt
target halted due to debug-request, current mode: Supervisor
xPSR: 0x01000000 pc: 0x80001a3c
(gdb) info registers
r0 0x00000001 1
r1 0x2000ff00 536936704
pc 0x80001a3c 0x80001a3c <spin_lock+8>
看到 pc 停在 spin_lock+8,我心里就有数了——八成是自旋锁没释放。你再看看 monitor reset,这个命令会复位 CPU,但不会复位调试逻辑。我一般用它来重现问题:复位后立刻 halt,然后单步跟踪初始化流程。
| 命令 | 作用 | 适用场景 |
|---|---|---|
monitor halt |
硬件暂停 CPU | 死锁、死循环、无法响应断点 |
monitor reset |
硬件复位 CPU | 重现问题、重启调试会话 |
continue |
恢复执行 | 正常调试流程 |
monitor reset 不会复位外设寄存器。如果你需要完全干净的硬件状态,得用 monitor reset halt 组合。
3.3 设置断点:break 的三种用法
break(简写 b)是软件断点。它会在指定地址插入一条 BKPT 指令,CPU 执行到那里就会陷入调试异常。我常用的有三种形式:
- 按函数名设断点:
b spin_lock—— 最简单,适合入口函数。 - 按行号设断点:
b driver.c:128—— 精确到某一行。 - 按地址设断点:
b *0x80001a3c—— 适合没有符号表的情况。
(gdb) b spin_lock
Breakpoint 1 at 0x80001a30: file kernel/locking/spinlock.c, line 178.
(gdb) b driver.c:128
Breakpoint 2 at 0x80002b4c: file driver.c, line 128.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x80001a30 spin_lock
2 breakpoint keep y 0x80002b4c driver.c:128
我曾经踩过一个坑:在中断处理函数里设断点,结果断点触发后系统直接跑飞。后来才发现,有些 RTOS 的中断上下文不允许长时间暂停。解决办法是用 monitor halt 代替软件断点,或者把断点设在中断入口处,单步跳过关键部分。
3.4 单步跟踪:step 与 next 的区别
step(简写 s)会进入函数内部,next(简写 n)会跳过函数。死锁排查时,我更喜欢用 step,因为死锁往往藏在某个函数调用的深处。
(gdb) step
spin_lock (lock=0x2000ff00) at kernel/locking/spinlock.c:178
178 {
(gdb) step
179 arch_spin_lock(&lock->rlock);
(gdb) step
arch_spin_lock (lock=0x2000ff00) at arch/arm/include/asm/spinlock.h:55
55 {
(gdb) info registers pc
pc 0x80001a4c 0x80001a4c <arch_spin_lock+16>
你看,三步就追到了架构相关的自旋锁实现。如果这时候 pc 卡在某个循环里不动,基本可以断定是锁竞争导致的死锁。
pc 在同一个地址来回跳,先别慌。可能是编译器优化后的循环展开。这时候用 info registers 看看循环变量,比瞎猜强。
3.5 查看现场:info registers 与关键寄存器解读
info registers(简写 i r)会 dump 出所有通用寄存器和状态寄存器。死锁现场,我最关注三个东西:
- PC(程序计数器):当前执行到哪了?是不是在锁函数里?
- LR(链接寄存器):谁调了当前函数?回溯调用栈。
- SP(栈指针):栈有没有溢出?有没有被踩?
(gdb) info registers
pc 0x80001a3c 0x80001a3c <spin_lock+8>
lr 0x80002b80 0x80002b80 <my_driver_write+24>
sp 0x2000fe80 0x2000fe80
r0 0x00000001 1
r1 0x2000ff00 536936704
xPSR 0x01000000 16777216
看到 lr 是 my_driver_write+24,说明是驱动写函数调用了 spin_lock。再结合 r0 是 1,r1 是锁的地址,现场就很清楚了。
info registers 显示的值不对劲,先执行一次 monitor halt 强制刷新。
3.6 知识体系:命令之间的协作关系
下面这张图,是我自己总结的 GDB+JTAG 调试流程。你照着这个顺序走,基本不会漏掉关键信息。
嗯,这张图把整个调试流程串起来了。你从连接目标开始,到暂停、设断点、运行、查看现场,每一步都有对应的命令。死锁排查时,我通常会在第 2 步和第 5 步之间反复循环——halt 一次,看看寄存器,再 continue,再 halt,直到抓到锁竞争的瞬间。
3.7 实战避坑:我曾经犯过的错
最后分享几个我踩过的坑,你遇到了能少走弯路:
- 忘记 halt 直接设断点:CPU 还在跑,你设的断点可能被覆盖。先
monitor halt再设断点,稳一点。 - 单步时忽略 LR:有一次我单步追了半天,发现 PC 在一个循环里打转,其实 LR 早就指向了错误返回地址。后来养成习惯,每步都看一眼 LR。
- 复位后不 halt 直接跑:复位后 CPU 会从入口开始执行,如果你不 halt,可能几毫秒就跑到死锁点之外了。复位后立刻 halt,再设断点,再 continue。