一、 概述
在日常开发中,查看指定线程的栈信息是常见需求,尤其在处理死锁、高CPU使用率、忙循环、系统崩溃或内存调试等场景时。通常,开发者可借助JLink等调试工具,通过gdb和断点(bp)实现这些功能,但设备封包发布后,外围调试功能关闭,这些方法在真实设备上往往失效。为解决这一限制,openvela支持在运行环境中直接查看特定线程的栈信息。当程序崩溃或线程阻塞时,backtrace(回溯/堆栈跟踪)能清晰展示从当前执行点到初始调用者的完整调用链,是诊断死锁、崩溃等问题的核心工具。
二、 配置说明
1. 通用配置说明
openvela 支持 ARM、RISC-V 和 Xtensa 三种架构的通用配置,具体如下:
# 开启 backtrace 功能,默认不显示函数名称
CONFIG_SCHED_BACKTRACE=y
CONFIG_SYSTEM_DUMPSTACK=y
# 如果需要符号名支持,启用以下选项;
# 若 Flash 空间不足,可以通过 addr2line 手动解析地址到符号
CONFIG_ALLSYMS=y
# 启用架构支持
CONFIG_ARCH_HAVE_BACKTRACE=y
其中,对于ARM 平台,由于代码资源限制及 Thumb-2 特性的要求,提供以下三种 backtrace 实现方式供选择:
# 基于帧指针(frame pointer)的回溯实现
# 原理:依赖编译器生成的固定栈帧结构(如 x86 的 EBP 链)
# 优势:实现简单,无额外内存开销
# 限制:需编译器强制 -fno-omit-frame-pointer,性能损失约 3-5%
CONFIG_UNWINDER_FRAME_POINTER
# 基于栈指针(stack pointer)的回溯实现
# 原理:暴力扫描栈内存识别返回地址模式
# 优势:不依赖编译器选项,支持无帧指针优化(-O2)
# 风险:可能误判数据为代码地址(错误率约 0.7%)
CONFIG_UNWINDER_STACK_POINTER
# 基于 ARM 架构的 .exidx 段的回溯实现
# 原理:解析 ELF 中 .exidx 段的展开指令(ARM EABI 标准)
# 优势:100% 精确回溯,支持异常处理栈展开
# 依赖:需链接器生成 .exidx 段(-funwind-tables 编译选项)
CONFIG_UNWINDER_ARM
目前,openvela 支持 ARM、RISC-V 和 Xtensa 三种体系结构的 backtrace 功能,各架构的配置和实现如下。
2. ARM
- ARM Cortex-A/R
在 ARM Cortex-A 和 Cortex-R 系列中,backtrace 实现支持以下两种方式,选择其中一种即可:
# 方式 1:基于 fp 寄存器的回溯实现
CONFIG_UNWINDER_FRAME_POINTER
# 方式 2:基于 .exidx 段的回溯实现
CONFIG_UNWINDER_ARM
- ARM Cortex-M
在 ARM Cortex-M 系列中,backtrace 有以下三种实现方式,根据项目需求选择其一:
# 方式 1:基于 fp 的回溯实现(需注意编译器限制)
CONFIG_UNWINDER_FRAME_POINTER=y
# 方式 2:基于 sp 的回溯实现,适用于小资源设备
CONFIG_UNWINDER_STACK_POINTER=y
# 方式 3:基于 .exidx 段的回溯实现
CONFIG_UNWINDER_ARM=y
- CONFIG_UNWINDER_FRAME_POINTER:GCC 编译器上存在相关限制
- CONFIG_UNWINDER_STACK_POINTER:通过解析 bl 和 blx 指令来获取程序计数器(PC)地址,特别适用于资源有限的嵌入式设备,但由于指令解析的启发式方法,存在一定的误判概率。如果项目代码被分散到多个区域,需要通过以下 API 配置多个代码区域:
void up_backtrace_init_code_regions(FAR void **regions)
- CONFIG_UNWINDER_ARM:该栈回溯机制在编译链接阶段生成异常处理索引表(.exidx段),实现精确的指令级栈展开,但会导致最终代码体积增加约5%-8%。
3. RISC-V
在 RISC-V 架构中,backtrace 基于帧指针(frame pointer)实现,只需启用以下配置:
CONFIG_FRAME_POINTER=y
CONFIG_SCHED_BACKTRACE=y
4. Xtensa
在 Xtensa 架构中,backtrace 默认支持栈回溯,不需要额外启用 -fno-omit-frame-pointer 选项。
三. 具体案例及心得
系统因为某些原因 crash, 除开类似 core dump 这类核心转储工具外,log 本身也能提供很多直观的信息给用户。我们必须优先分析dump日志以捕获第一现场的关键信息,例如内存状态、寄存器值、调用堆栈和异常类型等核心数据,这些信息能精确还原崩溃瞬间的系统环境,帮助快速定位根本原因,避免后续调试中的信息丢失或误判。下面以一份研发手表crash日志为例(资源已提供下载),讲述如何对 crash log 进行解析。
要先找到是哪个 core 引起的crash,即看第一现场:
[2025-07-01 13:51:33.176] [12/31 00:00:24] [ 0] [cp] dump_assert_info: Assertion failed panic: at file: /home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/boards/best1600_ep/common/src/bes_global_shmem.h:100 task: Idle_Task process: Kernel 0x1404c619
[2025-07-01 13:51:33.179] [12/31 00:00:24] [ 0] [cp] up_dump_register: R0: 20533150 R1: 00000064 R2: 00000037 R3: 00000007
[2025-07-01 13:51:33.180] [12/31 00:00:24] [ 0] [cp] up_dump_register: R4: 20532d88 R5: 34210e0c R6: 34210820 R7: 20540ab8
[2025-07-01 13:51:33.180] [12/31 00:00:24] [ 0] [cp] up_dump_register: R8: 00000000 R9: 00000224 R10: 20532e50 R11: 00000064
[2025-07-01 13:51:33.182] [12/31 00:00:24] [ 0] [cp] up_dump_register: R12: 00000000 SP: 20540aa0 LR: 1404cf2f PC: 1404cf2f
[2025-07-01 13:51:33.183] [12/31 00:00:24] [ 0] [cp] up_dump_register: xPSR: 68030037 BASEPRI: 00000000 CONTROL: 00000004
[2025-07-01 13:51:33.184] [12/31 00:00:24] [ 0] [cp] up_dump_register: EXC_RETURN: 00000000
[2025-07-01 13:51:33.184] [12/31 00:00:24] [ 0] [cp] dump_stackinfo: IRQ Stack:
[2025-07-01 13:51:33.186] [12/31 00:00:24] [ 0] [cp] dump_stackinfo: base: 0x20540400
[2025-07-01 13:51:33.186] [12/31 00:00:24] [ 0] [cp] dump_stackinfo: size: 00002048
[2025-07-01 13:51:33.187] [12/31 00:00:24] [ 0] [cp] dump_stackinfo: sp: 0x20540aa0
[2025-07-01 13:51:33.187] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540a80: 20540aa0 20540400 20540ab8 205d03d0 000007f0 20532e50 205d0bc0 1404cebd
[2025-07-01 13:51:33.189] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540aa0: 34210e0c 00000064 20532e2c 34214f84 1404c619 00000000 20540b08 00000000
[2025-07-01 13:51:33.190] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540ac0: 20540c00 00000006 20532d88 20533150 34210820 34210e0c 00000064 7474754e
[2025-07-01 13:51:33.191] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540ae0: 00000058 2050c864 2050c880 14007a67 61695848 20696d6f 63746157 34532068
[2025-07-01 13:51:33.193] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540b00: 49536520 3044204d 14004436 140076cb 302e3000 0000302e 14017aad 0132b3a0
[2025-07-01 13:51:33.194] [12/31 00:00:24] [ 0] [cp] stack_dump: 0x20540b20: 20540b28 6534779f 64653264 39323365 6c754a20 20312020 35323032 3a313120
可以从此处看到cp 被动 crash ,物理串口强制输出,关键字样: bes_global_shmem.h line:
100。不是第一crash现场,继续分析在sensor输出了
[2025-07-01 13:51:35.379] [12/31 00:00:24] [ 0] [sen] Panic remote cpu ap:
[2025-07-01 13:51:35.379] [12/31 00:00:24] [ 0] [sen] Panic remote cpu cp:
可知案发现场是sensor核中某些错误使得ap、cpb被动crash。根据sensor打印的back trace信息
[sen] sched_dumpstack: backtrace| 0: 0x00613680 0x00618af2 0x006136b2 0x00617fd6 0x00618262 0x0063c98c 0x006014de 0x00631390
[sen] sched_dumpstack: backtrace| 0: 0x00633ea0 0x00603982 0x006381ce 0x00634454 0x006174cc 0x006036fe 0x00633e0c 0x0063bade
利用./prebuilts/gcc/linux/arm/bin/arm-none-eabi-addr2line -ife
~/vela_ap.elf [backtrace]解析出错误的堆栈信息
syslog
??:?
dump_task
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/misc/assert.c:397 (discriminator 12)
sched_backtrace
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/sched/sched_backtrace.c:106
sched_dumpstack
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/libs/libc/sched/sched_dumpstack.c:71
nxsched_foreach
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/sched/sched_foreach.c:75
dump_tasks
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/misc/assert.c:546
dump_fatal_info
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/misc/assert.c:752
_assert
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/misc/assert.c:907
__aeabi_uldivmod
??:?
bes_oneshot_start
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/chips/bes/bes_oneshot.c:239
bes_timer_start
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/chips/bes/bes_tickless.c:339
bes_start
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/chips/bes/bes_tickless_alarm.c:155
oneshot_tick_start
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/include/nuttx/timers/oneshot.h:364
up_alarm_tick_start.isra.0
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/drivers/timers/arch_alarm.c:354
__assert
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/libs/libc/assert/lib_assert.c:38
bes_rptun_check_cpu
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/boards/best1600_ep/common/src/bes_global_shmem.h:100
bes_mbtx_rx_handler
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/chips/bes/up_rptun_m552sens.c:112
hal_rmt_ipc_rx_irq_handler
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/framework/services/platform/hal/hal_rmt_ipc.c:108
up_irq_handler
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/framework/services/rtos/nuttx/nuttx_os.c:75
irq_dispatch
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/sched/irq/irq_dispatch.c:144
arm_doirq
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/arch/arm/src/armv8-m/arm_doirq.c:103
exception_common
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/nuttx/arch/arm/src/armv8-m/arm_exception.S:225
_start
/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/framework/services/tests/sensor_hub/core/sensor_hub_main.c:368
定位到是/home/work/ssd1/workspace/Vela-o62-rel-5.0-Build/vendor/bes/chips/bes/up_rptun_m552sens.c:112中,回到相关代码中看,分析出原来是up_rptun_m552sens.c文件调用bes_rptun_check_cpu函数时,该函数进入到PANIC()中。
通过Back trace回溯堆栈的方式我们可以列出当前函数调用关系。但如何熟练地使用调试工具非常重要。不管是GDB、WinDbg还是Visual Studio,这些工具都能生成清晰的调用栈。调用栈能够让我们快速看到程序崩溃时正在执行的函数,明白程序运行到哪一步出了问题。在复杂的程序中,调用栈清楚地展示模块之间的关系,帮助我们缩小问题范围。
但实际上调用栈只显示崩溃的函数名和地址。对程序员来说会查看源代码、日志和变量状态能更准确地找到问题。比如,检查函数的参数、变量或指针是否异常,像空指针或越界访问,就能很快发现常见错误。完整的调试信息也很重要。如果没有符号表,只能看到一堆内存地址,分析就无从下手。
理解程序的运行逻辑也是关键所在。回溯不仅告诉我程序在哪崩了,还能提示问题出现的原因。通过调用栈,可以找到问题可能的起点,检查代码逻辑、边界条件或多线程冲突。比如,栈溢出可能因为递归太深,空指针可能是对象没初始化。
对于程序员而言,每次定位崩溃都是一次学习,记录问题的原因和解决方法,积累经验。在遇到复杂问题时,结合日志、断点和回溯反复验证,就可以一步步找到答案。