6.4 栈回溯
6.4 栈回溯
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言函数调用栈原理、栈帧指针(FP)、ARM Cortex-M 异常处理、第4章任务管理
🎯 学习目标
- 理解栈回溯(Stack Backtrace)在嵌入式系统调试中的核心价值
- 掌握
call_back_trace_t数据结构与深度限制机制 - 了解架构相关全局变量
g_code_start_addr等的作用 - 掌握
dump_stack栈内容转储函数的实现原理 - 掌握
task_stack_show任务栈回溯命令的使用方法 - 了解
assert_hook弱函数的应用层重写机制
一、概述
栈回溯(Stack Backtrace)是诊断程序崩溃和异常的重要工具。当系统发生异常(如 HardFault、断言失败)时,通过栈回溯技术可以还原函数调用链,定位崩溃前的代码路径,大大加速问题排查效率。
OneOS 通过 STACK_TRACE_EN 宏开启栈回溯功能,核心能力包括:
- 栈内容转储:通过
EXC_DUMP_STACK宏控制,在异常时将任务栈或中断栈的原始内容全部 dump 输出 - 调用链回溯:通过栈帧指针(FP)逐帧回溯,还原函数调用路径
- 代码段判断:利用
g_code_start_addr/g_code_end_addr判断回溯地址是否在合法代码段内 - 深度限制:通过
CALL_BACK_TRACE_MAX_DEPTH限制最大回溯深度,防止无限回溯 - 任务保护:内置任务(如 idle、recycle)受保护,不允许栈回溯操作
- 断言钩子:
assert_hook为OS_WEAK弱函数,可在应用层重写,实现自定义断言处理逻辑
二、核心数据结构
2.1 call_back_trace_t 结构体
定义在 kernel/include/debug/stack_trace/stack_trace.h 第36-40行,存储栈回溯结果:
struct call_back_trace
{
uint32_t depth;
os_size_t back_trace[CALL_BACK_TRACE_MAX_DEPTH];
};
typedef struct call_back_trace call_back_trace_t;字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
depth | uint32_t | 实际回溯到的调用层级数,从0开始 |
back_trace[] | os_size_t[] | 回溯地址数组,back_trace[0] 为最近调用的函数地址,back_trace[depth-1] 为最远调用的函数地址 |
2.2 CALL_BACK_TRACE_MAX_DEPTH
最大回溯深度宏,定义在 oneos_config.h 中,通常取值为 8~16。该值越大,回溯信息越完整,但占用的栈空间和计算时间也越多。
三、架构相关全局变量
以下全局变量声明在 stack_trace.h 第45-48行,定义并初始化为 0 在 stack_trace.c 第38-41行,由各架构(arch)在启动时根据链接脚本符号赋值,用于地址合法性判断:
extern os_size_t g_code_start_addr; /* Start address of code segment. */
extern os_size_t g_code_end_addr; /* End address of code segment. */
extern os_size_t g_main_stack_start_addr; /* Start address of main stack. */
extern os_size_t g_main_stack_end_addr; /* End address of main stack. */对应的定义(stack_trace.c 第38-41行,初始化为 0):
/* The following variables need to be initialized in each arch. */
os_size_t g_code_start_addr = 0; /* Start address of code segment. */
os_size_t g_code_end_addr = 0; /* End address of code segment. */
os_size_t g_main_stack_start_addr = 0; /* Start address of main stack. */
os_size_t g_main_stack_end_addr = 0; /* End address of main stack. */变量用途:
| 变量 | 用途 |
|---|---|
g_code_start_addr | 判断回溯地址是否在代码段内,过滤无效地址 |
g_code_end_addr | 同上,形成代码段地址范围 [start, end) |
g_main_stack_start_addr | 用于 dump 主栈内容时的边界判断 |
g_main_stack_end_addr | 同上,形成主栈地址范围 [start, end) |
这些变量在初始化时设为0,需要每个架构在启动代码中根据链接脚本的符号地址进行赋值。例如,在 ARM Cortex-M 架构中,通常从链接脚本导出 __text_start 和 __text_end 等符号。
四、核心API详解
4.1 dump_stack —— Dump 栈内容
extern void dump_stack(uint32_t stack_start_addr, uint32_t stack_size, os_size_t *stack_pointer);仅在 EXC_DUMP_STACK 宏开启时编译。从当前栈指针 stack_pointer 开始,遍历输出栈地址和对应的数据内容,直到栈顶(stack_start_addr + stack_size)。
实现逻辑(stack_trace.c 第56-67行):
void dump_stack(uint32_t stack_start_addr, uint32_t stack_size, os_size_t *stack_pointer)
{
os_size_t *sp;
os_kprintf("======================= Stack info =====================\r\n");
for (sp = stack_pointer; (uint32_t)sp < stack_start_addr + stack_size; sp++)
{
os_kprintf(" addr: 0x%08x data: %08x\r\n", sp, *sp);
}
os_kprintf("-------------------------------------------------------------\r\n");
}输出格式:每行打印一个栈地址和对应的32位数据值,便于分析栈中保存的返回地址、局部变量等信息。
4.2 task_stack_show —— 显示任务栈回溯
extern os_err_t task_stack_show(os_task_id task, uint32_t context);根据任务ID或任务名,获取该任务的栈信息,然后调用架构相关的回溯函数进行栈回溯,并将调用链信息输出到控制台。
该函数通过 Shell 命令 show_task_stack 暴露给用户,支持通过任务名或任务ID指定目标任务。
4.3 assert_hook —— 断言钩子函数
OS_WEAK void assert_hook(void);当系统断言失败时,OS_ASSERT 宏会调用此钩子函数。OneOS 提供了默认实现(stack_trace.c 第80-111行):
OS_WEAK void assert_hook(void)
{
os_irq_lock();
if (os_is_irq_active() == OS_TRUE)
{
os_kprintf("=================Interrupt stack trace======================\r\n");
interrupt_stack_show();
}
else
{
task_stack_show(os_get_current_task(), 0);
}
/*The following function calls OS_ASSERT. To avoid recursive calls to OS_ASSERT, delete the following code. */
#ifdef OS_USING_SHELL
/*
os_kprintf("======================= Heap Info =======================\r\n");
extern os_err_t sh_memshow(int32_t argc, char **argv);
sh_memshow(0, OS_NULL);
extern os_err_t sh_show_task_info(int32_t argc, char **argv);
sh_show_task_info(0, OS_NULL);
*/
#endif /* OS_USING_SHELL */
while (1)
;
}执行逻辑:
- 关中断保护,防止回溯过程被打断
- 判断当前是否在中断上下文中:若在中断中,显示中断栈回溯;否则显示当前任务栈回溯
#ifdef OS_USING_SHELL块中的sh_memshow/sh_show_task_info代码被整体注释掉(源码原样保留注释),避免其内部调用OS_ASSERT造成断言递归- 进入死循环,防止系统继续运行导致二次崩溃
由于 assert_hook 声明为 OS_WEAK,开发者可以在应用层重新定义同名函数,覆盖默认实现,加入自定义处理逻辑(如保存日志到 Flash、发送诊断报文等)。
4.4 task_is_protected —— 判断任务是否受保护
os_bool_t task_is_protected(const char *name);判断指定任务是否为系统保护任务(stack_trace.c 第126-138行)。内置的 idle 任务和 recycle 任务属于受保护任务,不允许进行栈回溯操作,防止破坏系统稳定性。
os_bool_t task_is_protected(const char *name)
{
OS_ASSERT(name != OS_NULL);
if ((strncmp(name, OS_RECYCLE_TASK_NAME, OS_NAME_MAX) == 0) || (strncmp(name, OS_IDLE_TASK_NAME, OS_NAME_MAX) == 0))
{
return OS_TRUE;
}
else
{
return OS_FALSE;
}
}4.5 call_back_trace_init —— 初始化
os_err_t call_back_trace_init(void);栈回溯模块的初始化函数,通过 OS_INIT_CALL 在组件初始化阶段自动调用(stack_trace.c 第149-168行):
os_err_t call_back_trace_init(void)
{
if (arch_call_back_trace_init() == OS_FAILURE)
{
return OS_FAILURE;
}
#ifdef OS_USING_ASSERT
#ifdef OS_USING_SAFETY_MECHANISM
#include <os_safety.h>
os_safety_assert_set_hook(assert_hook);
#endif
#endif
return OS_SUCCESS;
}
OS_INIT_CALL(call_back_trace_init, OS_INIT_LEVEL_COMPONENT, OS_INIT_SUBLEVEL_LOW);初始化流程:
- 调用架构相关的回溯初始化函数
arch_call_back_trace_init() - 若启用安全机制,将
assert_hook注册为安全断言钩子
五、栈回溯原理
5.1 基本原理
栈回溯的核心依赖栈帧指针(Frame Pointer,FP / R11)。在 ARM Cortex-M 架构中:
- 每次函数调用时,编译器会保存返回地址(LR)和上一帧的 FP 到栈中
- 通过当前 FP 可以找到上一帧的 FP 和返回地址
- 不断沿着 FP 链回溯,直到到达初始帧或超出合法范围
栈帧结构(ARM Cortex-M,向下增长栈):
高地址 ┌──────────────────┐
│ 返回地址 (LR) │ ← 当前 SP + 偏移
├──────────────────┤
│ 上一帧 FP │ ← 当前 FP 指向此处
├──────────────────┤
│ 局部变量 │
├──────────────────┤
│ 下一帧返回地址 │
├──────────────────┤
│ 下一帧 FP │ ← 上一帧 FP 指向此处
低地址 └──────────────────┘5.2 地址合法性判断
在回溯过程中,每取到一个返回地址,需判断其是否在代码段内。以下为原理示意(并非源码原样,实际实现见各架构的 arch_stack_trace):
if ((addr >= g_code_start_addr) && (addr < g_code_end_addr))
{
/* 合法地址,加入回溯结果 */
}
else
{
/* 非法地址,终止回溯 */
break;
}这种判断利用 g_code_start_addr / g_code_end_addr 过滤掉栈中的随机数据,防止回溯到无效地址导致误导。判断采用半开区间 [g_code_start_addr, g_code_end_addr)。
5.3 深度限制
通过 CALL_BACK_TRACE_MAX_DEPTH 限制最大回溯深度,防止:
- 递归调用导致的无限回溯
- 栈损坏导致的无尽循环
- 回溯时间过长影响系统实时性
数值演算(深度与空间):设 CALL_BACK_TRACE_MAX_DEPTH = 8(默认 8~16),os_size_t 在 32 位平台占 4 字节:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 回溯地址数组 | 8 * 4 | 32 字节 |
depth 字段 | sizeof(uint32_t) | 4 字节 |
struct call_back_trace 总大小 | 4 + 32(对齐后) | 36 字节 |
即每次回溯结果最多占用 36 字节(struct call_back_trace 成员本身),不随任务数增加。深度越大,能还原的调用链越长,但写入 back_trace[] 的时间也越长。
5.4 Shell 命令
栈回溯功能通过 Shell 命令 show_task_stack 暴露给用户(stack_trace.c 第185-223行):
SH_CMD_EXPORT(show_task_stack,
sh_task_stack_show,
"show stack call back trace of a task, para is the name or tid of task");使用方式:
OneOS> show_task_stack task_name
OneOS> show_task_stack 0x20001234命令逻辑:
- 检查参数是否包含任务名或任务ID
- 检查任务是否受保护(idle/recycle 不允许回溯)
- 通过
os_task_get_id(name)查找任务,若未找到则将参数作为任务ID解析 - 调用
task_stack_show()执行回溯
六、使用示例
6.1 异常处理中的栈回溯
当系统发生 HardFault 等异常时,异常处理函数会调用栈回溯来定位问题:
/* 硬件错误异常处理 */
void HardFault_Handler(void)
{
/* 获取异常发生时的栈帧 */
uint32_t *stack_frame;
__asm volatile(
"TST LR, #4 \n"
"ITE EQ \n"
"MRSEQ R0, MSP \n"
"MRSNE R0, PSP \n"
"MOV %0, R0 \n"
: "=r"(stack_frame)
);
/* Dump 栈内容 */
dump_stack(g_main_stack_start_addr,
g_main_stack_end_addr - g_main_stack_start_addr,
stack_frame);
/* 显示调用链回溯 */
os_kprintf("=================Call back trace======================\r\n");
task_stack_show(os_get_current_task(), (uint32_t)stack_frame);
}6.2 手动触发栈 dump
在调试过程中,可以在关键位置手动调用栈 dump 来观察栈使用情况:
void debug_check_stack(void)
{
os_task_id current_task = os_get_current_task();
/* 获取当前任务栈信息 */
os_kprintf("Current task: %s\r\n", current_task->name);
os_kprintf("Stack start: 0x%08x\r\n", current_task->stack_addr);
os_kprintf("Stack size: %d bytes\r\n", current_task->stack_size);
/* 获取当前栈指针 */
uint32_t sp;
__asm volatile("MOV %0, SP" : "=r"(sp));
/* Dump 当前栈内容 */
dump_stack((uint32_t)current_task->stack_addr,
current_task->stack_size,
(os_size_t *)sp);
}6.3 自定义 assert_hook 示例
开发者可以在应用层重写 assert_hook,实现自定义的断言处理逻辑:
#include <stack_trace.h>
#include <os_assert.h>
/* 重写弱函数 assert_hook,增加 Flash 日志存储 */
void assert_hook(void)
{
os_irq_lock();
/* 保存当前系统时间戳 */
uint32_t crash_tick = os_tick_get_value();
/* 保存当前任务名 */
os_task_id current_task = os_get_current_task();
/* 写入 Flash 日志区域(假设有此函数) */
crash_log_save(crash_tick, current_task->name);
/* 执行栈回溯 */
if (os_is_irq_active() == OS_TRUE)
{
os_kprintf("=================Interrupt stack trace======================\r\n");
interrupt_stack_show();
}
else
{
os_kprintf("=================Task [%s] stack trace======================\r\n",
current_task->name);
task_stack_show(current_task, 0);
}
/* 可选:触发系统复位,而非死循环 */
/* NVIC_SystemReset(); */
while (1)
;
}6.4 Shell 命令使用示例
OneOS> show_task_stack worker
=================Task [worker] stack trace======================
Call back trace depth: 5
[0] 0x08001234 in task_entry at tasks.c:45
[1] 0x08001567 in os_task_entry at os_task.c:234
[2] 0x08001ABC in os_task_switch at os_task_switch.c:89
[3] 0x08002000 in os_schedule at os_schedule.c:123
[4] 0x08003000 in main at main.c:67
-------------------------------------------------------------📝 本节小结
核心要点回顾
- 编译开关:栈回溯整体由
STACK_TRACE_EN宏开启(stack_trace.h第34行#ifdef STACK_TRACE_EN包裹全部接口),栈内容转储由EXC_DUMP_STACK宏单独控制。 - 核心结构:
call_back_trace_t含depth(回溯深度)和back_trace[](地址数组,长度由CALL_BACK_TRACE_MAX_DEPTH决定)。 - 地址合法性:四个架构全局变量
g_code_start_addr/g_code_end_addr/g_main_stack_start_addr/g_main_stack_end_addr,在stack_trace.c中初始化为 0,由各架构启动代码赋值;回溯用半开区间[start, end)过滤无效地址。 - 回溯实现:
dump_stack(依赖EXC_DUMP_STACK)转储原始栈内容,task_stack_show逐帧还原调用链,均以栈指针为起点遍历到栈顶。 - 断言钩子:
assert_hook为OS_WEAK弱函数,默认实现"关中断 → 中断栈/任务栈二选一回溯 → 死循环",可在应用层重写实现自定义处理。 - 任务保护:
task_is_protected识别 idle/recycle 内置任务,禁止对其回溯,防止破坏系统稳定性。 - 初始化注册:
call_back_trace_init通过OS_INIT_CALL(组件级、低子级)自动调用,并在启用安全机制时把assert_hook注册为安全断言钩子。 - Shell 暴露:
show_task_stack命令支持按任务名或任务 ID 回溯(命令函数sh_task_stack_show源码stack_trace.c第185-218行)。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 源码结构体/全局变量加中文注释 | 违反"逐字一致"规范 → 保留源码英文注释,中文解释写正文 |
| 误以为 4 个全局变量在 stack_trace 内赋值 | 它们初始化为 0,实际由各架构(arch)启动代码按链接脚本符号赋值 |
| 回溯到无效地址不停 | 必须用 g_code_start_addr/g_code_end_addr 半开区间过滤 |
| 对 idle/recycle 任务执行回溯 | 会破坏系统 → 先 task_is_protected 判断 |
自定义 assert_hook 没关中断 | 回溯过程会被其它中断打断 → 先 os_irq_lock() |
忘记 STACK_TRACE_EN | 所有接口(含结构体)都被 #ifdef 裁剪 → 不开宏则无栈回溯能力 |
以为 dump_stack 能还原调用链 | 它只转储原始栈数据,还原调用链靠 task_stack_show |
下一步
- 6.5 IPC-Trace:了解 OneOS 的 IPC(进程间通信)追踪工具,掌握对信号量、互斥量等内核对象的获取/释放进行追踪的方法,结合本节栈回溯共同定位并发问题。