第4章:GDB基础操作——启动、断点、单步、变量与内存

说实话,GDB 这东西,我刚入行时觉得它又丑又难用。

那时候我习惯用 IDE,点一下鼠标就能调试。直到有一次,我在一个嵌入式项目里,芯片的 JTAG 接口死活连不上 IDE,现场又没有图形环境。我盯着黑乎乎的终端,硬着头皮敲下了第一行 gdb 命令。嗯,从那以后,我再也没觉得 GDB 难用了。

这一章,我就带你过一遍 GDB 最核心的五个操作。你想想看,调试器说白了就干这几件事:启动、停住、走一步、看看值、改改数据。掌握了这些,你就能在 ECU 的代码里自由穿行了。

4.1 启动 GDB:三种常见姿势

启动 GDB 其实不复杂,但我见过不少新手在这栽跟头。我习惯分三种场景来记:

场景 命令 说明
直接调试可执行文件 gdb ./my_ecu.elf 最常用,加载符号表
附加到正在运行的进程 gdb -p 1234 调试已经跑起来的任务
连接远程调试服务器 gdb 内输入 target remote :2331 ECU 调试的标配方式

我个人最常用的是第三种。为什么?因为 ECU 通常没有显示器,代码在开发板上跑,GDB 在 PC 上跑,中间靠网络或 JTAG 连起来。我曾经在一个项目中,远程调试一个跑飞了的任务,愣是靠 target remote 把问题揪了出来。

小技巧:启动时加个 -q 参数(gdb -q ./xxx.elf),可以省掉那一大段版本声明,直接进入调试界面。我每次都用,清爽。

4.2 设置断点:让程序停在你想停的地方

断点就是给程序设个「停车点」。GDB 里设置断点的方式很多,我挑三个最实用的:

  • 按函数名设断点:break mainb main。程序一进 main 函数就停住。
  • 按行号设断点:break can_driver.c:128。在源文件的第 128 行停下。
  • 按条件设断点:break can_driver.c:128 if error_count > 5。只有满足条件才停,省得你一次次手动跳过。

我记得有一次,一个同事在调试 CAN 总线接收中断,每次进中断都停,结果根本没法复现问题。我建议他加个条件断点,只让它在特定 ID 帧到来时停下。问题一下子就定位了。条件断点,真的是调试利器。

注意:条件断点如果条件表达式写得太复杂,或者每次求值都很慢,会影响实时性。在 ECU 的实时任务里,我建议尽量用简单的条件,比如比较一个全局变量。

4.3 单步执行:一步一步看代码怎么跑

单步执行,说白了就是让程序「走一步停一下」。GDB 提供了两个常用命令:

  • step(或 s):步入。遇到函数调用会跟进去。
  • next(或 n):步过。遇到函数调用直接执行完,不进去。

你想想看,什么时候用 step?当你怀疑某个函数内部有问题时。什么时候用 next?当你确定这个函数没问题,只想看它返回后的结果时。

我个人的习惯是:先 next 快速扫一遍流程,发现可疑的地方再用 step 钻进去看。别一上来就 step,那样容易在层层调用里迷路。

实战经验:在调试 ECU 的启动流程时,我经常用 next 跳过初始化函数,直接看主循环。如果主循环跑飞了,再回头用 step 检查某个外设的初始化代码。

4.4 查看变量:看看程序到底在想什么

程序停下来了,接下来就是「问它」当前的状态。GDB 查看变量很简单:

  • print var_namep var_name:打印变量的值。
  • print/x var_name:以十六进制显示。
  • print/d var_name:以十进制显示。
  • display var_name:每次停顿时自动打印该变量。

我曾经调试过一个电机控制算法,发现电流值总是不对。用 p current 一看,数值在正常范围。但用 p/x current 一看,发现低两位被莫名置位了。原来是寄存器配置时掩码写错了。你看,换个显示格式,问题就暴露了。

小技巧:info locals 可以一次性打印当前函数的所有局部变量。省得一个个敲 p

4.5 修改内存:直接动手改数据

调试时,有时候光看还不够,你得动手改。GDB 允许你直接修改内存或变量的值:

  • set var_name = 42:修改变量的值。
  • set {int}0x20001000 = 0x55:直接修改地址 0x20001000 处的 4 字节数据。
  • set {char[4]}0x20001000 = "ABC":修改一段连续内存。

嗯,这里要注意。修改内存是个危险操作,尤其是在嵌入式系统里。我曾经在调试一个 Bootloader 时,想改一个标志位来跳过校验,结果手滑写错了地址,直接把向量表给覆盖了。芯片当场死机,只能重新烧录。所以,改内存前,一定确认好地址和数据类型。

警告:在 ECU 的实时运行中,修改内存可能会触发意想不到的副作用。比如你改了一个定时器的计数值,可能导致中断提前或延迟。建议只在程序停住时修改,并且改完后单步执行几步观察效果。

4.6 一个完整的调试小例子

说了这么多,不如看个完整的流程。假设我在调试一个 CAN 报文发送函数:

(gdb) target remote :2331
(gdb) file my_ecu.elf
(gdb) break can_send_message if msg_id == 0x123
(gdb) continue
// 程序运行,遇到条件断点停下
(gdb) print msg_data
$1 = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
(gdb) set msg_data[3] = 0xFF
(gdb) next
// 执行发送函数
(gdb) print tx_status
$2 = 0  // 发送成功
(gdb) quit

你看,整个过程就是:连上目标板、设断点、跑起来、看数据、改数据、单步走、确认结果。是不是很清晰?

我个人觉得,GDB 就像一把瑞士军刀。刚开始你可能觉得它不如 IDE 顺手,但用久了你会发现,它几乎能处理所有调试场景。尤其是在嵌入式领域,很多时候你根本没有图形界面可用。这时候,GDB 就是你唯一的依靠。

下一章,我会带你深入 GDB 的高级用法,比如回溯调用栈、查看寄存器、以及如何调试多任务系统。准备好了吗?