4. JTAG硬件接口与信号

GDB 这玩意儿,说实话,是嵌入式调试的“瑞士军刀”。你 JTAG 连上了,OpenOCD 跑起来了,最后跟目标芯片对话的,就是 GDB。很多新手觉得 GDB 就是设个断点、按个 continue,其实远不止这些。我个人习惯把 GDB 当成一个“动态的示波器”——它能让你在代码的任意时刻,把 CPU 的每一根神经都看透。

核心思想: GDB 不是用来“找 bug”的,它是用来“观察程序状态”的。你观察得越细,bug 就越藏不住。

4.1 断点管理:不只是 break

断点分好几种,别只会用 b main。我列个表,你一看就明白。

命令 作用 我常用的场景
break func_name 在函数入口停 调试某个模块的初始化流程
break file.c:line 在指定文件行号停 怀疑某行赋值有问题时
break *0x80001234 在绝对地址停 调试中断向量表或 Bootloader 时
tbreak 一次性断点,触发后自动删除 只想看某个条件是否第一次满足
watch var 数据断点,变量值变化时停 排查全局变量被意外修改
rwatch var 变量被读取时停 查谁在频繁读一个标志位

嗯,这里要注意:硬件断点数量有限。ARM Cortex-M 系列通常只有 4-8 个硬件断点。你设多了,GDB 会悄悄转成软件断点(往 Flash 里写 BKPT 指令)。这在调试只读存储器里的代码时,会直接失败。我曾经在调试一个量产固件时,一口气设了 10 个断点,结果程序跑飞了——就是因为 Flash 被改写了。

小技巧: 用 info breakpoints 查看所有断点状态。用 disable/enable 临时开关,别老删了重建。

4.2 单步执行:别只会 next

单步执行,说白了就是让代码“一帧一帧”地放。但很多人分不清 step 和 next 的区别。

  • step (s):步入。遇到函数调用,会跟进去。适合看库函数或底层驱动的实现细节。
  • next (n):步过。把整个函数调用当成一步。适合跳过你已经验证过的代码。
  • finish:一直执行到当前函数返回。适合快速跳出你不想看的函数体。
  • until:执行到指定行号或地址。比 next 更灵活,可以跳过循环体。

举个例子。你在一个 HAL_UART_Transmit() 函数里,想确认它到底有没有把数据写到寄存器里。用 step 进去,一行一行看。如果只是想跳过这个发送过程,用 next 就行。我建议你养成习惯:不确定的代码用 step,确定的代码用 next。

4.3 内存查看:x 命令的艺术

GDB 的 x 命令(examine)是查看内存的利器。格式是 x/[格式] [地址]。格式由三部分组成:数量、单位、显示格式。

(gdb) x/10xw 0x20000000   # 从 0x20000000 开始,显示 10 个 32 位字,十六进制
(gdb) x/20ub buf          # 显示 buf 数组的前 20 个字节,无符号十进制
(gdb) x/5i $pc            # 从当前 PC 开始,反汇编 5 条指令
(gdb) x/s 0x08001000      # 把地址当成字符串显示

单位选项:b 字节,h 半字(2 字节),w 字(4 字节),g 巨字(8 字节)。

显示格式:x 十六进制,d 十进制,u 无符号十进制,o 八进制,t 二进制,c 字符,s 字符串,i 指令。

避坑指南: 我曾经在调试一个 DMA 缓冲区时,用 x/100xw 查看,结果 GDB 访问了未映射的地址,触发了 HardFault。记住:不要越界访问。先用 info proc mappings 或者 mem 命令确认地址范围。

4.4 寄存器操作:直接操控 CPU

查看寄存器用 info registers 或简写 i r。只看特定寄存器:i r r0 r1 pc lr。

修改寄存器值:

(gdb) set $r0 = 0x12345678
(gdb) set $pc = 0x08002000   # 强制跳转,危险操作!
(gdb) set var my_var = 42    # 修改局部变量

你想想看,这有多强大?你可以直接修改 PC 指针,跳过一段有问题的代码,继续往下跑。我在调试一个电机驱动时,发现某个状态机卡死在了错误状态。我直接用 set $r0 = 0x02 把状态寄存器改成正常值,然后 continue,验证了后续逻辑是否正确。省去了重新编译烧录的 5 分钟。

但注意:修改 PC 和 SP 要极其小心。搞不好就直接 HardFault 了。我建议你先用 backtrace 看看调用栈,确认当前上下文,再动手。

4.5 反汇编:看编译器到底干了什么

有时候源码级调试不够用。比如你怀疑编译器优化出了问题,或者想确认某个内联函数是否真的被内联了。这时候就得看反汇编。

(gdb) disassemble /m my_function   # 混合显示源码和汇编
(gdb) disassemble 0x08001000,0x08001050  # 指定地址范围
(gdb) disassemble $pc, $pc+0x20    # 看当前 PC 附近的指令

我常用的套路是:在可疑的 C 代码行设断点,然后用 disassemble /m 看它对应的汇编。如果发现编译器生成了奇怪的指令(比如多余的加载/存储),那可能就是性能瓶颈或者 bug 的根源。

举个例子。你写了一个 volatile 变量,但发现它没有被正确读取。反汇编一看,编译器可能把它优化成了寄存器变量,根本没去内存读。这时候你就知道问题出在哪了。

4.6 Core Dump 分析:事后诸葛亮也是诸葛亮

程序跑飞了,或者 HardFault 了,但你没有连着调试器。怎么办?Core Dump 就是你的“黑匣子”。

在嵌入式领域,Core Dump 通常是指 CPU 在异常时自动保存的寄存器现场和栈内容。比如 ARM Cortex-M 的 HardFault 机制,会把 R0-R3, R12, LR, PC, xPSR 自动压栈。

分析步骤:

  1. 获取现场:从异常处理函数中,读出压栈的寄存器值。通常通过汇编代码提取。
  2. 加载到 GDB:用 target remote 或者直接加载 ELF 文件,然后用 set $pc = 0xXXXX 设置现场。
  3. 查看调用栈:backtrace 命令。如果栈被破坏了,可能看不到完整信息。
  4. 分析原因:看 PC 指向的指令,看 LR 指向的返回地址,看栈上的局部变量。
实战案例: 有一次客户反馈设备偶发死机。我让现场工程师把 HardFault 时的 R0-R3, PC, LR 打印出来。我用 GDB 加载固件,设置 PC 为死机时的地址,反汇编一看,是一条 LDR R0, [R1, #0x10]。再查 R1 的值,发现是一个野指针。最终定位到是一个全局结构体指针在中断中被修改了。

4.7 知识体系总览

下面这张图,把 GDB 核心调试命令的脉络梳理清楚了。你可以把它当成一个“调试地图”,遇到问题先看该走哪条路。

GDB 调试命令知识体系 GDB 调试会话 断点管理 • break / tbreak • watch / rwatch • info breakpoints • enable / disable 单步执行 • step (s) — 步入 • next (n) — 步过 • finish — 跳出函数 • until — 跳到指定行 内存查看 • x/10xw 0x2000 • x/20ub buf • x/5i $pc • x/s 0x08001000 寄存器操作 • info registers • set $r0 = value • set $pc = addr • set var = value 反汇编 • disassemble /m • disassemble addr1,addr2 • 混合源码/汇编 • 验证编译器优化 Core Dump 分析 • 获取异常现场 • 加载到 GDB • backtrace 看栈 • 反汇编定位原因 💡 调试思路:先设断点确认范围 → 单步缩小范围 → 内存/寄存器查现场 → 反汇编找根因 Core Dump 用于“事后分析”,其他命令用于“实时调试”

4.8 实战组合拳:一个完整的调试流程

光讲命令没意思。我给你一个我常用的调试流程,你照着来一遍,就能体会到 GDB 的威力。

  1. 发现问题:程序跑着跑着,某个 LED 不闪了。怀疑是定时器中断没触发。
  2. 设断点:break TIM2_IRQHandler。如果没停,说明中断根本没进来。
  3. 查寄存器:info registers 看 NVIC->ISER 和 NVIC->ICER,确认中断使能位。
  4. 看内存:x/1w &TIM2->SR 看状态寄存器,确认更新标志位是否置位。
  5. 反汇编:disassemble /m TIM2_IRQHandler,看是不是编译器把中断函数优化掉了(比如没加 __attribute__((interrupt)))。
  6. 修 bug:发现是中断向量表地址配错了。改完重新烧录,验证通过。

你看,整个过程不到 5 分钟。如果没有 GDB,你可能得加串口打印、重新编译、烧录、看日志……折腾半小时。

我的习惯: 每次调试前,先 set pagination off 关掉分页,再 set confirm off 关掉确认。省得老按回车。

好了,GDB 的核心命令就这些。别贪多,先把 break、step、next、x、info registers、disassemble 这六个用熟。剩下的命令,遇到具体场景再查手册,完全来得及。