书接上文,做菜好吃靠的是火候。调试代码也一样:不只是“能熟”,而是“恰到好处”。
很多 Zephyr 开发者在刚接触 QEMU 时,往往停留在“程序跑起来就行”的阶段。但当项目开始复杂起来,HardFault、BusFault、栈溢出、异常重入等问题便会接踵而至。此时,掌握一套高效的调试方法,远比盲目猜测问题原因更加重要。
本文将为大家带来两种方式演示如何在 QEMU 上调试 Zephyr 程序:
- 方式 A:手动启动 QEMU,自行开启 GDB Server;
- 方式 B:使用
west -t debugserver 一键启用调试环境。
两种方式都能完成调试,只不过区别在于:
- 方式 A 更像自己掌勺控火,灵活自由,适合深入研究底层机制;
- 方式 B 则像把点火工作交给副厨,省心省力,更适合日常开发和团队协作。
1. 上灶前的预处理:把信息与安全阀门先打开
正式调试前,建议在 prj.conf 中启用调试配置:
CONFIG_PRINTK=y
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_EXCEPTION_DEBUG=y
CONFIG_DEBUG_COREDUMP=y
CONFIG_INIT_STACKS=y
CONFIG_STACK_SENTINEL=y
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_ISR_STACK_SIZE=4096
这些配置看似普通,实际价值很高:
- CONFIG_EXCEPTION_DEBUG:异常发生时自动打印寄存器、PC、LR 等关键信息。
- CONFIG_DEBUG_COREDUMP:支持生成 Core Dump,便于后续分析。
- CONFIG_INIT_STACKS:初始化栈空间,帮助发现栈使用情况。
- CONFIG_STACK_SENTINEL:增加栈哨兵检测,能够第一时间发现栈溢出。
- MAIN/ISR Stack Size:在问题排查阶段,建议直接提升至 4096 字节,避免被栈不足误导。
很多所谓“神秘 HardFault”,最终根因其实只是栈不够大。
2. 方式 A:手动启动 QEMU,自行连接 GDB
对于想深入理解调试原理的同学,推荐先掌握这种方式。
第一步,编译目标程序:
west build -b qemu_cortex_m3 D:\path\to\my_app -p always
第二步,手动起 QEMU,先停在入口等你:
PowerShell
qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -kernel
关键参数说明:
-S:开机暂停,等你下断点;
-s:在本地 1234 端口开 GDB Server;
-nographic:把串口输出绑到当前终端,方便看日志。
第三步,连上 GDB:
arm-zephyr-eabi-gdb build\zephyr\zephyr.elf
(gdb) target remote :1234
(gdb) break main
(gdb) continue
在 WSL 或类 UNIX 终端里,还可以开启 TUI:
(gdb) layout split
看当前 PC 附近的指令:
(gdb) x/10i $pc
指令级单步:
(gdb) si
(gdb) ni
回溯与寄存器:
(gdb) bt
(gdb) info registers
只反汇编当前函数:
(gdb) disassemble
如果怀疑是异常致死,预埋断点在异常入口:
(gdb) break z_arm_hard_fault
(gdb) break z_arm_fault
(gdb) continue
命中后记下 pc 与 lr,用 addr2line 回到源码:
arm-zephyr-eabi-addr2line -e build\zephyr\zephyr.elf 0x08001234
3. 方式 B:让 west 帮你把火点好
当你不想记 QEMU 那一长串参数时,把点火交给 west。
第一步,编译依旧:
west build -b qemu_cortex_m3 D:\path\to\my_app -p always
第二步,开一个终端运行:
west -t debugserver
此时 west 会代你启动 QEMU,并以暂停方式等待连接,默认端口仍是 1234。
第三步,另开一个终端上 GDB:
arm-zephyr-eabi-gdb build\zephyr\zephyr.elf
(gdb) target remote :1234
(gdb) break main
(gdb) continue
后续使用的 GDB 命令与方式 A 完全一致。
4. 真实案例:QEMU 报 HardFault/Lockup 怎么办?
典型症状:刚起步就看到类似 “Lockup: can't escalate … to HardFault”。
先不要慌。按照下面顺序排查,通常都能快速定位问题。
快速处置顺序如下:
MAIN_STACK_SIZE、ISR_STACK_SIZE,先 4096 起步;
- 打开
CONFIG_EXCEPTION_DEBUG 和 CONFIG_STACK_SENTINEL,重建运行;
- 在
z_arm_hard_fault、z_arm_fault 下断点,命中后 info registers;
x/10i $pc 看故障点附近指令,是非法地址、未对齐访问、还是异常重入;
addr2line 把 pc、lr 定位到源码行,沿调用链找可疑入口;
- 如初始化了 QEMU 不支持的外设,先在
prj.conf 里关闭相关驱动,验证是否是某外设引发的 BusFault。
5. 两种方式如何选择?
- 观察更底层现象、需要特殊 QEMU 参数、或做原理实验:用“手动 gdbserver”。
- 想快速稳定、流程一致、适合团队协作:
west -t debugserver。
- 两者可以随时切换:日常用
west,疑难杂症切到手动。
6. 收尾与加餐
当你能在 Windows + MSYS2 + QEMU 的组合里熟练起锅、控火、下菜,就已经具备了“无板推进”的生产力。建议把以下流程固化进团队工程手册或 CI,或者按照现在大模型的开发流程生成一个 Skills:
west build-b qemu_cortex_m3 构建;
west build-t run;
west-t debugserver 或手动 -s -S 开启调试;
- 打包一份
.gdbinit:开启 set disassemble-next-line on、display/i $pc、常用断点与打印别名;
- 将关键模块配套
ztest 用 QEMU 走回归。
当你学会使用 GDB 查看寄存器、分析调用栈、定位 HardFault,并把这些能力融入日常开发流程之后,调试将不再依赖运气,而是一套可复用、可复制的方法论。
其实 HardFault 不可怕,可怕的是找不到它。
有了 QEMU、GDB 和 Zephyr 提供的调试能力,我们完全可以做到:问题出现时第一时间发现它,定位它,最终消灭它。这套方法论也值得在云栈社区与更多嵌入式同行一起打磨。