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行,由各架构(arch)在启动时初始化,用于地址合法性判断(源码 stack_trace.c 第38-41行):
extern os_size_t g_code_start_addr; /* 代码段起始地址 */
extern os_size_t g_code_end_addr; /* 代码段结束地址 */
extern os_size_t g_main_stack_start_addr; /* 主栈(MSP)起始地址 */
extern os_size_t g_main_stack_end_addr; /* 主栈(MSP)结束地址 */变量用途:
| 变量 | 用途 |
|---|---|
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 栈内容
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 —— 显示任务栈回溯
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);
}
while (1)
;
}执行逻辑:
- 关中断保护,防止回溯过程被打断
- 判断当前是否在中断上下文中:若在中断中,显示中断栈回溯;否则显示当前任务栈回溯
- 进入死循环,防止系统继续运行导致二次崩溃
由于 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
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 地址合法性判断
在回溯过程中,每取到一个返回地址,需判断其是否在代码段内:
if ((addr >= g_code_start_addr) && (addr < g_code_end_addr))
{
/* 合法地址,加入回溯结果 */
}
else
{
/* 非法地址,终止回溯 */
break;
}这种判断可以过滤掉栈中的随机数据,防止回溯到无效地址导致误导。
5.3 深度限制
通过 CALL_BACK_TRACE_MAX_DEPTH 限制最大回溯深度,防止:
- 递归调用导致的无限回溯
- 栈损坏导致的无尽循环
- 回溯时间过长影响系统实时性
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
-------------------------------------------------------------📝 本节小结
本节详细介绍了 OneOS 的栈回溯功能。栈回溯通过 STACK_TRACE_EN 宏开启,核心数据结构 call_back_trace_t 记录回溯深度和地址数组。dump_stack 函数用于转储栈原始内容,task_stack_show 用于还原函数调用链。assert_hook 为弱函数,可在应用层重写实现自定义断言处理。架构相关全局变量(g_code_start_addr 等)用于地址合法性判断。通过 Shell 命令 show_task_stack 可方便地在运行时查看任意任务的调用栈信息。
🔗 相关链接
- 头文件:
kernel/include/debug/stack_trace/stack_trace.h - 源文件:`kernel/source/debug/stack_trace