第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 main或b main。程序一进 main 函数就停住。 - 按行号设断点:
break can_driver.c:128。在源文件的第 128 行停下。 - 按条件设断点:
break can_driver.c:128 if error_count > 5。只有满足条件才停,省得你一次次手动跳过。
我记得有一次,一个同事在调试 CAN 总线接收中断,每次进中断都停,结果根本没法复现问题。我建议他加个条件断点,只让它在特定 ID 帧到来时停下。问题一下子就定位了。条件断点,真的是调试利器。
4.3 单步执行:一步一步看代码怎么跑
单步执行,说白了就是让程序「走一步停一下」。GDB 提供了两个常用命令:
step(或s):步入。遇到函数调用会跟进去。next(或n):步过。遇到函数调用直接执行完,不进去。
你想想看,什么时候用 step?当你怀疑某个函数内部有问题时。什么时候用 next?当你确定这个函数没问题,只想看它返回后的结果时。
我个人的习惯是:先 next 快速扫一遍流程,发现可疑的地方再用 step 钻进去看。别一上来就 step,那样容易在层层调用里迷路。
实战经验:在调试 ECU 的启动流程时,我经常用 next 跳过初始化函数,直接看主循环。如果主循环跑飞了,再回头用 step 检查某个外设的初始化代码。
4.4 查看变量:看看程序到底在想什么
程序停下来了,接下来就是「问它」当前的状态。GDB 查看变量很简单:
print var_name或p 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 时,想改一个标志位来跳过校验,结果手滑写错了地址,直接把向量表给覆盖了。芯片当场死机,只能重新烧录。所以,改内存前,一定确认好地址和数据类型。
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 的高级用法,比如回溯调用栈、查看寄存器、以及如何调试多任务系统。准备好了吗?