2. GDB基础调试:GDB安装与配置、断点设置、单步执行、变量查看
调试嵌入式程序,说白了就是跟代码的「灵魂」对话。而GDB,就是我最顺手的那个翻译官。
我记得刚入行那会儿,调试Zephyr程序全靠printf大法,满屏刷日志,眼睛都快看瞎了。后来被一位老工程师按着头学会了GDB,才发现之前走了多少弯路。这一章,我就把GDB最核心的基础操作掰开揉碎讲给你听。
2.1 GDB的安装与配置
Zephyr SDK里其实已经自带了arm-none-eabi-gdb,但很多人装完SDK就直接开干,结果发现连不上目标板。嗯,这里要注意:你得确认工具链路径对不对。
快速检查GDB是否可用:
# 查看Zephyr SDK中的GDB版本
$ $ZEPHYR_SDK_INSTALL_DIR/arm-zephyr-eabi/bin/arm-zephyr-eabi-gdb --version
# 如果输出类似 "GNU gdb (Zephyr SDK) 12.1" 就说明OK
我个人习惯把GDB路径加到环境变量里,省得每次敲一长串。在 ~/.zephyrrc 里加一行:
export PATH=$ZEPHYR_SDK_INSTALL_DIR/arm-zephyr-eabi/bin:$PATH
然后 source ~/.zephyrrc 刷新一下,之后直接敲 arm-zephyr-eabi-gdb 就能用了。
小技巧: 如果你用VS Code + Cortex-Debug插件,可以在 .vscode/launch.json 里指定gdb路径,这样点一下就能启动调试会话,不用每次都敲命令行。
2.2 启动GDB并连接目标
调试Zephyr程序,通常有两种方式:
- QEMU模拟器 —— 适合纯逻辑调试,不需要硬件
- JTAG/SWD硬件调试器 —— 真机调试,能看外设寄存器
先看QEMU的方式。编译一个hello_world示例:
# 在Zephyr根目录
$ west build -b qemu_cortex_m3 samples/hello_world
$ west build -t run
QEMU启动后,它会等待GDB连接。另开一个终端:
$ arm-zephyr-eabi-gdb build/zephyr/zephyr.elf
(gdb) target remote localhost:1234
连上了。就这么简单。
硬件调试器稍微麻烦一点。我用的是J-Link,连接命令大概是:
(gdb) target remote localhost:2331
(gdb) monitor reset
(gdb) continue
你想想看,如果连不上,八成是端口被占用了,或者调试器驱动没装好。我曾经在一个项目里折腾了整整一下午,最后发现是USB线接触不良……
2.3 断点设置——让程序在你想停的地方停下来
断点是调试的灵魂。没有断点,你只能看着程序跑飞干瞪眼。
| 命令 | 作用 | 示例 |
|---|---|---|
break / b |
设置断点 | b main 或 b file.c:42 |
tbreak |
临时断点(只触发一次) | tbreak k_sleep |
rbreak |
正则匹配断点 | rbreak z_* |
delete / d |
删除断点 | d 1 删除编号1的断点 |
info breakpoints |
列出所有断点 | info b |
举个例子,我想在 main.c 第42行停住:
(gdb) b main.c:42
Breakpoint 1 at 0x8000a1c: file src/main.c, line 42.
然后输入 c(continue)让程序跑起来,它就会停在那一行。
注意: Zephyr是多线程RTOS,断点可能被多个线程同时命中。如果你只想让某个特定线程停下来,可以用 thread apply 2 break 这种写法。不过这是进阶内容,后面章节会细讲。
2.4 单步执行——一步一步看代码怎么跑
断点停住之后,怎么往前走?GDB提供了几个「步进」命令:
step/s—— 步入函数内部next/n—— 步过,不进入函数finish—— 执行到当前函数返回until—— 执行到指定行
我个人的习惯是:
- 想确认函数逻辑时用
step - 想跳过库函数调用时用
next - 想快速跳出函数时用
finish
看个实际场景。假设我在 k_sem_take 调用前停住了:
(gdb) n
k_sem_take(&my_sem, K_FOREVER); // 执行这行,但不进入k_sem_take内部
(gdb) s
// 如果用了s,就会进入k_sem_take函数体
为什么会这样?因为 step 会钻进任何有源码的函数里。如果函数没有调试符号(比如Zephyr内核的某些内联函数),GDB会直接跳过。嗯,这时候别慌,不是你的问题。
2.5 变量查看——看看内存里到底存了什么
断点停住了,步进也走了,但变量值对不对?这才是关键。
| 命令 | 作用 | 示例 |
|---|---|---|
print / p |
打印变量值 | p counter |
display |
每次停顿时自动打印 | display counter |
info locals |
显示所有局部变量 | info locals |
ptype |
查看变量类型 | ptype my_struct |
x |
查看内存原始内容 | x/4wx 0x20000000 |
举个例子,我想看一个结构体指针的内容:
(gdb) p my_dev
$1 = (const struct device *) 0x20000100
(gdb) p *my_dev
$2 = {name = 0x8000a00 "uart0", config = 0x8000b00, api = 0x8000c00}
(gdb) p my_dev->name
$3 = 0x8000a00 "uart0"
你看,结构体里的字段一目了然。如果某个指针是 0x0 或者 0xdeadbeef,那基本就是野指针了。我曾经在一个Zephyr驱动里排查了三天,最后发现是设备结构体没初始化,指针指向了随机地址。
实用技巧: 用 display 命令可以省去每次手动 print 的麻烦。比如 display/x counter 会以十六进制格式自动显示counter的值,每次单步完都能看到变化。
2.6 知识结构总览
下面这张图把GDB基础调试的核心流程串起来了。你可以把它当成一张「调试地图」,遇到问题按图索骥就行。
2.7 实战小练习
光看不练假把式。我建议你打开一个Zephyr示例项目,按下面的步骤走一遍:
- 编译
samples/basic/blinky并启动QEMU调试 - 在
main函数入口设断点 - 用
next单步执行,观察LED翻转逻辑 - 用
print查看延时变量的值 - 用
display自动追踪某个计数器
做完这些,GDB基础操作你就基本掌握了。后面章节我们会深入Zephyr特有的调试技巧,比如多线程调试、内核对象监控、性能分析等等。但地基先打牢,别急着飞。