2. Zephyr基础回顾

调试嵌入式程序,说白了就是跟代码的「灵魂」对话。而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基础调试的核心流程串起来了。你可以把它当成一张「调试地图」,遇到问题按图索骥就行。

GDB基础调试流程 GDB基础调试核心流程 1. 安装与配置 2. 启动并连接目标 3. 设置断点 4. 单步执行 5. 变量查看 6. 分析定位问题 发现问题 → 修改代码 → 重新调试(循环迭代) 关键命令速查 • 断点:break / b、tbreak、rbreak、delete、info breakpoints • 单步:step / s、next / n、finish、until • 变量:print / p、display、info locals、ptype、x • 控制:continue / c、run / r、quit / q

2.7 实战小练习

光看不练假把式。我建议你打开一个Zephyr示例项目,按下面的步骤走一遍:

  1. 编译 samples/basic/blinky 并启动QEMU调试
  2. 在 main 函数入口设断点
  3. 用 next 单步执行,观察LED翻转逻辑
  4. 用 print 查看延时变量的值
  5. 用 display 自动追踪某个计数器

做完这些,GDB基础操作你就基本掌握了。后面章节我们会深入Zephyr特有的调试技巧,比如多线程调试、内核对象监控、性能分析等等。但地基先打牢,别急着飞。