一、概述
在日常开发中,我们常常需要查看指定线程的栈信息,常见的场景包括:
- 死锁(deadlock)
- 高 CPU 使用率(high CPU usage)
- 忙循环(busy loop)
- 系统崩溃(crash)
- 内存调试(memory debug)
通常情况下,借助调试工具(如 JLink),可以通过 gdb 和断点(breakpoint,简称 bp)的方式实现这些功能。但在设备封包发布和外围调试功能关闭之后,这些功能在真实设备上往往无法使用。
为了解决这一问题,openvela 支持在运行环境中查看特定线程的栈信息。
二、配置说明
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)的回溯实现
CONFIG_UNWINDER_FRAME_POINTER
# 基于栈指针(stack pointer)的回溯实现
CONFIG_UNWINDER_STACK_POINTER
# 基于 ARM 架构的 .exidx 段的回溯实现
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 编译器上存在相关限制(详情参考 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
更多信息参考:RISC-V Backtrace 实现
4、Xtensa
在 Xtensa 架构中,backtrace 默认支持栈回溯,不需要额外启用 -fno-omit-frame-pointer 选项。
三、操作使用
在代码中使用 backtrace 系列函数需要包含头文件 #include <execinfo.h>。本章介绍如何使用 backtrace 获取函数调用堆栈信息、打印日志及定位错误行。
1、在代码中使用 backtrace
使用 backtrace 系列函数可以捕获当前程序状态,打印堆栈信息。以下是常见函数说明,
更多细节请参考 Linux Man Page: backtrace。
#include<execinfo.h>
/* 存储当前程序状态至__array数组,array最大数量为__size
返回值为实际存储的元素个数
*/
extern int backtrace (void **__array, int __size) __nonnull ((1));
/*
功能:返回一个字符串数组,存放从 backtrace 获取的信息。
参数:
- __array: backtrace 返回的指针数组
- __size: backtrace 的返回值
注意:该方法会调用 `backtrace_malloc(FAR void *const *buffer, int size)` 申请空间,定义在
nuttx/libs/libc/misc/lib_execinfo.c 文件中。
*/
extern char **backtrace_symbols (void *const *__array, int __size)
__THROW __nonnull ((1));
/*
功能:和 backtrace_symbols 类似,但无需申请额外空间,直接写入文件描述符 __fd。
*/
extern void backtrace_symbols_fd (void *const *__array, int __size, int __fd)
__THROW __nonnull ((1));
2、dump_stack()
-
在 openvela 系统中,可以直接调用 dump_stack() 函数打印堆栈信息。例如:
// 在 examples/hello/hello_main.c 文件中添加测试代码 37 int main(int argc, FAR char *argv[]) 38 { 39 printf("Hello, World!!\n"); +40 dump_stack(); // 打印堆栈信息 41 return 0; 42 }
打印输出示例:
ap> hello Hello, World!! [07:06:21] [28] [ INFO] [BackTrace|28|0]: 0xc070a96 0xc063d7c 0xc0809bc 0xc063d38 0xc0587de
-
使用 addr2line 工具解析打印出的回溯地址到具体的代码行:
addr2line -fe nuttx 0xc070a96 0xc063d7c 0xc0809bc 0xc063d38 0xc0587de
解析结果示例:
nuttx/arch/arm/src/armv8-m/arm_backtrace.c:446 nuttx/libs/libc/sched/sched_dumpstack.c:60 apps/examples/hello/hello_main.c:40 nuttx/libs/libc/sched/task_startup.c:151 nuttx/sched/task/task_start.c:130
-
利用 dump_stack() 的地址定位错误位置,或者通过打开符号表功能直接打印函数名称,方便分析。
3、dumpstack 命令
在命令行中,可以使用 dumpstack 命令获取指定任务的 backtrace。以下是使用方法:
使用 dumpstack [pid] 获取任务堆栈
-
获取进程的堆栈。
# 查看进程状态 ap> ps ... 26 100 RR Task --- Waiting Semaphore 00000010 004056 000648 15.9% 0.0% Telnet daemon 0x3c4293e0 # 查看 PID 为 26 的 backtrace ap> dumpstack 26 [15:18:53] [29] [ INFO] [BackTrace|26|0]: 0xc06b1c2 0x2154aa 0x20f312 0xc2ca118 0xc2c9bc4 0xc2c9c34 0xc08d894 0xc063d38 [15:18:53] [29] [ INFO] [BackTrace|26|1]: 0xc0587de
-
使用 addr2line 解析回溯地址。
addr2line -e nuttx 0xc06b1c2 0x2154aa 0x20f312 0xc2ca118 0xc2c9bc4 0xc2c9c34 0xc08d894 0xc063d38
-
查看解析结果。
解析结果示例如下:
nuttx/arch/arm/src/armv8-m/arm_switchcontext.S:79 nuttx/net/utils/net_lock.c:128 nuttx/net/tcp/tcp_accept.c:281 nuttx/net/inet/inet_sockif.c:889 nuttx/net/socket/accept.c:141 (discriminator 4) nuttx/net/socket/accept.c:270 apps/netutils/telnetd/telnetd_daemon.c:240 (discriminator 3) nuttx/libs/libc/sched/task_startup.c:151
使用 dumpstack [pid_start] [pid_end] 查看多个进程堆栈
查看 PID 从 0 到 26 的 backtrace 信息:
ap> dumpstack 0 26
[15:21:07] [30] [ INFO] [BackTrace|0|0]: 0xc054ba0 0xc3129f0 0xc000056
[15:21:07] [30] [ INFO] [BackTrace|1|0]: 0xc06b1c2 0xc0572e2 0xc0595e6 0xc0587d4
[15:21:07] [30] [ INFO] [BackTrace|26|0]: 0xc06b1c2 0xc2154aa 0xc0587d4
...
4、crash 日志中的调用栈解析
[15:21:21] [25] [ INFO] [BackTrace|25|0]: 0xc070aa6 0xc063d7c 0xc070f58 0xc060948 0xc06b2a2 0xc054d1e 0xc070dc8 0xc06b21e
[15:21:21] [25] [ INFO] [BackTrace|25|1]: 0xc087556 0xc0847fa 0xc087864 0xc0889b4 0xc0837a8 0xc063d38 0xc0587de
crash thread 25:
$ addr2line -e nuttx 0xc070aa6 0xc063d7c 0xc070efc 0xc0569c8 0xc070fba 0xc060948 0xc06b2a2 0xc054d1e 0xc070dc8 0xc06b21e 0xc087556 0xc0847fa 0xc087864 0xc0889b4 0xc0837a8 0xc063d38
nuttx/arch/arm/src/armv8-m/arm_backtrace.c:446
nuttx/libs/libc/sched/sched_dumpstack.c:60
nuttx/arch/arm/src/armv8-m/arm_assert.c:167