4.6.2 堆栈溢出检测
4.6.2 堆栈溢出检测
📚 本节导读
学习时长: 约 30 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务栈概念、内存布局、RTOS 任务管理、位运算基础
🎯 学习目标
- 理解堆栈溢出的原理与危害
- 掌握 OneOS 栈水印(
'$'填充)与边界检查两种检测方法 - 理解栈溢出检测在任务切换时的触发机制
- 掌握栈查询 API(
os_task_get_stack_begin/end/top) - 掌握溢出处理钩子的注册与行为
- 掌握栈大小估算与配置的最佳实践
一、堆栈溢出概念
1.1 什么是堆栈溢出
每个任务在创建时分配了固定大小的栈空间。当任务运行过程中使用的栈空间超过分配的大小时,就会发生堆栈溢出(Stack Overflow)。
堆栈溢出是 RTOS 开发中最常见也最危险的 bug 之一,因为:
- 它可能破坏相邻任务的数据结构或栈空间
- 它可能导致不可预测的系统行为(数据损坏、HardFault)
- 它通常很难通过静态分析发现
为什么栈溢出很危险(讲"为什么"):
嵌入式系统任务栈是向下增长的内存区域。当函数调用过深、局部变量过大时,栈指针 SP 会不断向低地址方向移动,一旦越过栈的起始边界 stack_begin,就会改写栈下方内存——那里很可能是其他任务的数据结构、全局变量甚至代码区。这种破坏不会立刻报错,而是会在之后某个随机时刻显现出诡异的行为,且极难定位。因此必须在溢出发生时尽早发现并处理。
1.2 堆栈溢出的典型原因
| 原因 | 说明 |
|---|---|
| 栈大小估算不足 | 创建任务时分配的栈空间太小 |
| 深层递归调用 | 函数递归调用导致栈深度过大 |
| 大局部变量 | 函数中定义了大型数组或结构体 |
| 中断嵌套 | 多层中断嵌套消耗大量栈空间 |
| 函数调用链过长 | 深度嵌套的函数调用 |
1.3 栈相关字段与检测开关
任务栈的三大地址字段
任务控制块(TCB)中维护了三个与栈相关的字段,它们是所有栈检测与查询的基础。
来源:kernel/source/os_prototypes.h
struct os_task
{
/* begin: The order, position and content cannot be changed */
void *stack_top; /* Point to SP */
void *stack_begin; /* The begin address of task stack */
void *stack_end; /* The end address of task stack */
uint16_t state; /* Task state, refer to OS_TASK_STATE_INIT, OS_TASK_STATE_READY ... */
/* end: The order, position and content cannot be changed */
};
typedef struct os_task os_task_t;| 字段 | 类型 | 含义 |
|---|---|---|
stack_top | void * | 任务栈顶部指针,指向当前栈顶(SP),随任务运行不断变化 |
stack_begin | void * | 任务栈起始地址(低地址,栈的边界起点),任务创建时固定 |
stack_end | void * | 任务栈结束地址(高地址,stack_begin + stack_size),任务创建时固定 |
这三个字段在 _k_task_init() 中被赋值(kernel/source/os_task.c):
task->stack_begin = stack_begin;
task->stack_end = (void *)((uint8_t *)stack_begin + stack_size);
task->stack_top = os_hw_stack_init(entry, arg, stack_begin, stack_size, k_task_exit);注意:
stack_begin/stack_end是固定边界,stack_top才是动态变化的。判断是否溢出,本质是判断stack_top是否越出[stack_begin, stack_end)区间。
栈水印('$' 填充)
栈水印检测依赖一个固定填充值 '$'(即字符 $,ASCII 码 0x24)。任务创建时,内核会把整个栈空间填满 '$':
来源:arch/arm/armv7m/arch_task.c
void *os_hw_stack_init(void (*task_entry)(void *arg),
void *arg,
void *stack_begin,
uint32_t stack_size,
void (*task_exit)(void))
{
struct stack_frame_nofpu *stack_frame;
uint8_t *stack_top;
uint32_t index;
memset(stack_begin, '$', stack_size);
stack_top = (uint8_t *)stack_begin + stack_size;
/*Compatible with old interfaces*/
stack_top = (uint8_t *)OS_ALIGN_DOWN((os_ubase_t)stack_top, OS_ARCH_STACK_ALIGN_SIZE);
stack_top -= sizeof(struct stack_frame_nofpu);
/* TODO: Need to check stack size */
stack_frame = (struct stack_frame_nofpu *)stack_top;
/* Initialize all registers */
for (index = 0; index < sizeof(struct stack_frame_nofpu) / sizeof(uint32_t); index++)
{
((uint32_t *)stack_frame)[index] = 0xDEADBEEF;
}
stack_frame->r0 = (uint32_t)arg; /* r0 : argument */
stack_frame->r1 = 0; /* r1 */
stack_frame->r2 = 0; /* r2 */
stack_frame->r3 = 0; /* r3 */
stack_frame->r12 = 0; /* r12 */
stack_frame->lr = (uint32_t)task_exit; /* lr */
stack_frame->pc = (uint32_t)task_entry; /* entry point, pc */
stack_frame->psr = 0x01000000UL; /* PSR */
/*
*Do not save floating point to stack. Thread mode + PSP stack.
*Once hardware floating is used:CONTROL.FPCA = 1 EXC_RETURN(LR):4bit=0.
*This behavior is automatically executed by the hardware.
*/
stack_frame->exc_return = 0xFFFFFFFDUL;
return (void *)stack_top;
}关键理解:
memset(stack_begin, '$', stack_size)把整个栈填成0x24。任务运行后,被使用过的区域会覆盖掉'$',而未使用区域仍保留'$'。- 因此,检查
stack_begin处的字节是否仍是'$',就能判断栈是否被"用穿"(溢出到边界)。 - 同时,从
stack_begin起第一个不是'$'的字节位置,就代表了任务历史最大栈使用量(水印水位)。
检测开关配置
来源:kernel/Kconfig
config OS_USING_OVERFLOW_CHECK
bool "Using stack overflow checking"
default y
help
Enable task stack overflow checking. The stack overflow is checking when
each task switch.
config OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK
bool "Using interrupt stack overflow checking"
default n
help
Enable interrupt stack overflow checking. The interrupt stack overflow is checking when
each interrupt exit.
config OS_USING_SAFETY_MECHANISM
bool "Enable function safety mechanism"
select OS_USING_ASSERT
select OS_USING_OVERFLOW_CHECK
default n| 配置项 | 默认值 | 说明 |
|---|---|---|
OS_USING_OVERFLOW_CHECK | y | 任务栈溢出检测。在每个任务切换时检查出/入两个任务是否溢出 |
OS_USING_INTERRUPT_STACK_OVERFLOW_CHECK | n | 中断栈溢出检测,每次中断退出时检查 |
OS_USING_SAFETY_MECHANISM | n | 功能安全机制,select OS_USING_OVERFLOW_CHECK(开启安全机制会强制打开溢出检测) |
说明:
OS_USING_OVERFLOW_CHECK默认开启,即默认在每次任务切换时都做栈溢出检查。这也是os_task_stack_is_overflow函数存在的条件(见 2.3 节)。
二、检测方法与触发机制
2.1 检测方法总览
OneOS 对任务栈溢出采用两种互补的检查手段,都在 os_task_stack_is_overflow() 中实现:
| 手段 | 检查内容 | 目的 |
|---|---|---|
| 水印检查 | *(uint8_t *)stack_begin 是否仍是 '$' | 判断栈是否"用穿"到起始边界 |
| 边界检查 | stack_top 是否落在 [stack_begin, stack_end) 区间内 | 判断 SP 是否越出栈边界 |
两种手段只要触发一个就判定为溢出(逻辑或)。
2.2 触发时机:任务切换时检查
栈溢出检查不是每时每刻都做,而是在每个任务切换点触发。切换时调用 os_task_switch_notify(),其内部在 OS_USING_OVERFLOW_CHECK 使能时调用 _k_task_stack_check()。
完整源码(os_task_switch_notify(),kernel/source/os_task.c):
void os_task_switch_notify(void)
{
os_task_t *current_task;
os_task_t *next_task;
#ifdef OS_USING_SMP
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
current_task = g_os_current_task[current_cpu_index];
next_task = g_os_next_task[current_cpu_index];
#else
current_task = g_os_current_task;
next_task = g_os_next_task;
#endif
#ifdef OS_USING_OVERFLOW_CHECK
_k_task_stack_check(current_task, next_task);
#endif
#ifdef OS_USING_SMP
OS_TASK_HOOK_CALL(
gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task), current_cpu_index));
#else
OS_TASK_HOOK_CALL(gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task)));
#endif
}分步详解:
第一步:获取当前任务与下一个任务(无条件执行)
os_task_t *current_task;
os_task_t *next_task;
#ifdef OS_USING_SMP
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
current_task = g_os_current_task[current_cpu_index];
next_task = g_os_next_task[current_cpu_index];
#else
current_task = g_os_current_task;
next_task = g_os_next_task;
#endif- 取出切换前的当前任务(即将被切换出去)和下一个任务(即将被切换进来)。
- 单核(
OS_USING_SMP未定义)时直接取全局变量g_os_current_task、g_os_next_task。 - 多核(SMP)时按当前 CPU 索引取各自的当前/下一任务。
第二步:调用栈溢出检查(条件 OS_USING_OVERFLOW_CHECK)
#ifdef OS_USING_OVERFLOW_CHECK
_k_task_stack_check(current_task, next_task);
#endif- 只有开启了
OS_USING_OVERFLOW_CHECK才执行检查。 - 检查出任务和入任务两个的栈(见 2.3 节
_k_task_stack_check)。 - 若任一任务溢出,
_k_task_stack_check内部会打印日志并调用os_safety_task_stack_overflow_process()。
第三步:调用任务切换钩子(无条件执行)
#ifdef OS_USING_SMP
OS_TASK_HOOK_CALL(
gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task), current_cpu_index));
#else
OS_TASK_HOOK_CALL(gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task)));
#endif- 与栈溢出无关,用于任务切换钩子(
gs_os_task_switch_hook)的通知,SMP 时额外带上 CPU 索引。
os_task_switch_notify() 条件走向总结
os_task_switch_notify() 条件走向总结:
├── 获取 current_task / next_task
│ ├── OS_USING_SMP 已定义 → 按 os_cpu_id_get() 索引取 g_os_current_task[]/g_os_next_task[]
│ └── OS_USING_SMP 未定义 → 直接取 g_os_current_task / g_os_next_task
├── 若 OS_USING_OVERFLOW_CHECK 已定义
│ └── 调用 _k_task_stack_check(current_task, next_task)
│ ├── 出任务(current_task)溢出 → 打印日志 + os_safety_task_stack_overflow_process()
│ └── 入任务(next_task)溢出 → 打印日志 + os_safety_task_stack_overflow_process()
└── 调用 OS_TASK_HOOK_CALL 通知任务切换钩子(SMP 带 CPU 索引)2.3 核心辅助函数 _k_task_stack_check
一句话定位:_k_task_stack_check 是栈溢出检查的核心封装,在每次任务切换时被 os_task_switch_notify() 调用,对"出任务"和"入任务"分别做一次溢出判定;若溢出则打印详细错误日志并调用安全处理函数。它不持有锁,运行于调度器的上下文中。
完整源码(kernel/source/os_task.c):
static void _k_task_stack_check(const os_task_t *from_task, const os_task_t *to_task)
{
os_bool_t is_overflow;
OS_ASSERT((OS_NULL != from_task));
OS_ASSERT((OS_NULL != to_task));
is_overflow = os_task_stack_is_overflow(from_task->stack_top, from_task->stack_begin, from_task->stack_end);
if (OS_TRUE == is_overflow)
{
OS_KERN_LOG(KERN_ERROR,
TASK_TAG,
"Stack overflow, switch from task(%s), sp is 0x%p stack_begin is %p stack_end is %p.\r\n",
from_task->name,
from_task->stack_top,
from_task->stack_begin,
from_task->stack_end);
os_safety_task_stack_overflow_process();
}
is_overflow = os_task_stack_is_overflow(to_task->stack_top, to_task->stack_begin, to_task->stack_end);
if (OS_TRUE == is_overflow)
{
OS_KERN_LOG(KERN_ERROR,
TASK_TAG,
"Stack overflow, switch to task(%s), sp is 0x%p stack_begin is %p stack_end is %p.\r\n",
to_task->name,
to_task->stack_top,
to_task->stack_begin,
to_task->stack_end);
os_safety_task_stack_overflow_process();
}
return;
}分步详解:
第一步:断言入参非空(条件 from_task/to_task 非空)
OS_ASSERT((OS_NULL != from_task));
OS_ASSERT((OS_NULL != to_task));- 用断言保证两个任务指针非空。若为空说明任务系统内部状态异常(这是编程错误,用断言暴露)。
第二步:检查"出任务"(from_task)是否溢出(条件 OS_TRUE == is_overflow 时进入)
is_overflow = os_task_stack_is_overflow(from_task->stack_top, from_task->stack_begin, from_task->stack_end);
if (OS_TRUE == is_overflow)
{
OS_KERN_LOG(KERN_ERROR,
TASK_TAG,
"Stack overflow, switch from task(%s), sp is 0x%p stack_begin is %p stack_end is %p.\r\n",
from_task->name,
from_task->stack_top,
from_task->stack_begin,
from_task->stack_end);
os_safety_task_stack_overflow_process();
}- 调用
os_task_stack_is_overflow()判定"即将被切走的任务"是否溢出(判定逻辑见 2.4 节)。 - 若溢出,用
OS_KERN_LOG打印错误:任务名、栈顶 SP、栈起始地址、栈结束地址。 - 然后调用
os_safety_task_stack_overflow_process()进入安全处理(见 3.2 节)。
第三步:检查"入任务"(to_task)是否溢出(条件 OS_TRUE == is_overflow 时进入)
is_overflow = os_task_stack_is_overflow(to_task->stack_top, to_task->stack_begin, to_task->stack_end);
if (OS_TRUE == is_overflow)
{
OS_KERN_LOG(KERN_ERROR,
TASK_TAG,
"Stack overflow, switch to task(%s), sp is 0x%p stack_begin is %p stack_end is %p.\r\n",
to_task->name,
to_task->stack_top,
to_task->stack_begin,
to_task->stack_end);
os_safety_task_stack_overflow_process();
}- 与第二步对称,检查"即将被切进来运行的任务"。这很关键:因为正要运行的任务如果栈已溢出,一旦运行会立即破坏内存,所以进入前必须检查。
第四步:返回(无条件执行)
return;- 正常情况下返回;若某一步检测到溢出且未注册钩子,
os_safety_task_stack_overflow_process()会死循环停机,不会走到这里。
_k_task_stack_check() 条件走向总结
_k_task_stack_check(from_task, to_task) 条件走向总结:
├── 断言 from_task、to_task 非空(否则触发断言)
├── 检查 from_task(出任务)
│ ├── os_task_stack_is_overflow(from_task) 为真
│ │ ├── OS_KERN_LOG 打印 "switch from task(...)" 错误
│ │ └── os_safety_task_stack_overflow_process() 进入安全处理
│ └── 为假 → 不处理
├── 检查 to_task(入任务)
│ ├── os_task_stack_is_overflow(to_task) 为真
│ │ ├── OS_KERN_LOG 打印 "switch to task(...)" 错误
│ │ └── os_safety_task_stack_overflow_process() 进入安全处理
│ └── 为假 → 不处理
└── return2.4 核心辅助函数 os_task_stack_is_overflow
一句话定位:os_task_stack_is_overflow 是真正的溢出判定函数,由 _k_task_stack_check 调用。它把"水印检查 + 边界检查"合并在一个 if 条件里,返回该任务是否溢出。它不持有锁,是架构相关函数(每个架构各有一个实现,逻辑一致)。
完整源码(arch/arm/armv7m/arch_task.c):
os_bool_t os_task_stack_is_overflow(void *stack_top, void *stack_begin, void *stack_end)
{
os_bool_t is_overflow;
OS_ASSERT(OS_NULL != stack_top);
OS_ASSERT(OS_NULL != stack_begin);
OS_ASSERT(OS_NULL != stack_end);
if ((*((uint8_t *)stack_begin) != '$') || ((os_ubase_t)stack_top < (os_ubase_t)stack_begin) ||
((os_ubase_t)stack_top >= (os_ubase_t)stack_end))
{
is_overflow = OS_TRUE;
}
else
{
is_overflow = OS_FALSE;
}
return is_overflow;
}分步详解:
第一步:断言三个指针非空(条件三者非空)
OS_ASSERT(OS_NULL != stack_top);
OS_ASSERT(OS_NULL != stack_begin);
OS_ASSERT(OS_NULL != stack_end);- 保证三个栈指针非空,否则说明 TCB 未正确初始化。
第二步:判断是否溢出(条件:水印被破坏 或 SP 越界)
if ((*((uint8_t *)stack_begin) != '$') || ((os_ubase_t)stack_top < (os_ubase_t)stack_begin) ||
((os_ubase_t)stack_top >= (os_ubase_t)stack_end))
{
is_overflow = OS_TRUE;
}这是一个"三条件取或"的判断,任一为真即溢出。逐个分析:
| 子条件 | 含义 | 判定 |
|---|---|---|
*((uint8_t *)stack_begin) != '$' | stack_begin 处的水印字节被改写 | 水印溢出:栈已向下用穿到起始边界 |
(os_ubase_t)stack_top < (os_ubase_t)stack_begin | SP 低于栈起始地址 | 下溢出:栈指针越过栈底 |
(os_ubase_t)stack_top >= (os_ubase_t)stack_end | SP 高于或等于栈结束地址 | 上溢出:栈指针越过栈顶 |
为什么用 '$' 作为水印(讲"为什么"):'$'(0x24)是一个"不太可能被正常数据天然撞上"的填充值。任务创建时整个栈填满 '$',一旦某处栈被写穿,stack_begin 处的 '$' 必然被覆盖,从而被检测到。同时 os_hw_stack_max_used() 利用 '$' 来计算最大栈用量(见 2.5 节)。
数值演算(水印与边界判定):
假设某任务栈:stack_begin = 0x20000000,stack_size = 0x400(1024 字节),则 stack_end = 0x20000400。
| 场景 | stack_begin 字节 | stack_top | 判定结果 |
|---|---|---|---|
| 正常运行,栈没用穿 | 0x24(仍是 '$') | 0x200003F8(区间内) | FALSE,未溢出 |
| 栈向下写穿到起始边界 | 0x00(被覆盖) | 0x20000010 | TRUE(水印条件触发) |
| SP 低于栈底 | 0x24 | 0x1FFFFFF8(< begin) | TRUE(下溢出触发) |
| SP 越出栈顶 | 0x24 | 0x20000400(>= end) | TRUE(上溢出触发) |
第三步:返回判定结果(无条件执行)
else
{
is_overflow = OS_FALSE;
}
return is_overflow;- 未溢出时置
OS_FALSE,最终返回is_overflow。
os_task_stack_is_overflow() 条件走向总结
os_task_stack_is_overflow(stack_top, stack_begin, stack_end) 条件走向总结:
├── 断言 stack_top / stack_begin / stack_end 非空
├── 若 (stack_begin 处字节 != '$') 或 (stack_top < stack_begin) 或 (stack_top >= stack_end)
│ └── 返回 OS_TRUE(溢出)
└── 否则(水印完好且 SP 在区间内)
└── 返回 OS_FALSE(未溢出)2.5 栈最大用量测量 os_hw_stack_max_used
一句话定位:os_hw_stack_max_used 用于计算任务历史最大栈用量(字节数)。它从栈起始地址开始,跳过所有仍是 '$' 的水印字节,第一个非 '$' 的位置即代表栈被用到的最深处。常用于查看任务栈的实际峰值占用,辅助栈大小调优。
完整源码(arch/arm/armv7m/arch_task.c):
uint32_t os_hw_stack_max_used(void *stack_begin, uint32_t stack_size)
{
uint8_t *addr;
uint32_t max_used;
addr = (uint8_t *)stack_begin;
while (*addr == '$')
{
addr++;
}
max_used = (uint32_t)((os_ubase_t)stack_begin + stack_size - (os_ubase_t)addr);
return max_used;
}分步详解:
第一步:从栈起始地址开始扫描水印(无条件执行)
addr = (uint8_t *)stack_begin;
while (*addr == '$')
{
addr++;
}addr从stack_begin出发,逐个字节检查是否为'$'。只要还是'$'就继续向上(高地址)移动。- 循环结束时,
addr指向第一个被实际使用过的字节(水印被破坏的位置),即栈使用的"最深水位"。
第二步:计算最大用量(无条件执行)
max_used = (uint32_t)((os_ubase_t)stack_begin + stack_size - (os_ubase_t)addr);stack_begin + stack_size就是stack_end。- 用
stack_end - addr得到"从栈顶到最深使用处的距离",即最大栈用量。
数值演算表:假设 stack_begin = 0x20000000,stack_size = 0x400(1024 字节),栈运行后最深使用到 addr = 0x20000300:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
stack_begin + stack_size | 0x20000000 + 0x400 | 0x20000400(stack_end) |
stack_end - addr | 0x20000400 - 0x20000300 | 0x100 |
max_used | — | 256 字节(占栈的 25%) |
注意:
os_hw_stack_max_used需要栈仍在栈空间范围内扫描。若栈已严重溢出(stack_begin处'$'已被破坏),第一步循环可能立即退出,addr仍指向stack_begin,此时计算出的用量接近满栈,且无法反映越界破坏。它更适合在未溢出前提下评估栈余量。
三、栈查询 API 与溢出处理钩子
3.1 栈查询 API
3.1.1 os_task_get_stack_begin —— 获取任务栈起始地址
函数签名:
void *os_task_get_stack_begin(os_task_id tid)参数/返回值:
| 项目 | 说明 |
|---|---|
tid | 任务 ID |
| 返回值 | 该任务的栈起始地址 stack_begin |
完整源码(kernel/source/os_task.c):
void *os_task_get_stack_begin(os_task_id tid)
{
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
return task->stack_begin;
}分步详解:
第一步:将任务 ID 转为 TCB 指针(无条件执行)
task = OS_TYPE_CONVERT(os_task_t *, tid);os_task_id本质就是os_task_t *的别名,通过OS_TYPE_CONVERT做显式类型转换,得到 TCB 指针task。
第二步:断言任务指针非空(条件 task != OS_NULL)
OS_ASSERT(task != OS_NULL);- 若传入非法
tid(如OS_NULL),触发断言,暴露调用方 bug。
第三步:返回栈起始地址(无条件执行)
return task->stack_begin;- 直接返回 TCB 中的
stack_begin字段。
3.1.2 os_task_get_stack_end —— 获取任务栈结束地址
函数签名:
void *os_task_get_stack_end(os_task_id tid)参数/返回值:
| 项目 | 说明 |
|---|---|
tid | 任务 ID |
| 返回值 | 该任务的栈结束地址 stack_end |
完整源码(kernel/source/os_task.c):
void *os_task_get_stack_end(os_task_id tid)
{
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
return task->stack_end;
}分步详解:
第一步:将任务 ID 转为 TCB 指针(无条件执行)
task = OS_TYPE_CONVERT(os_task_t *, tid);- 与 3.1.1 相同,得到 TCB 指针。
第二步:断言任务指针非空(条件 task != OS_NULL)
OS_ASSERT(task != OS_NULL);- 防空指针。
第三步:返回栈结束地址(无条件执行)
return task->stack_end;- 返回
stack_end。
3.1.3 os_task_get_stack_top —— 获取任务栈顶(当前 SP)
函数签名:
void *os_task_get_stack_top(os_task_id tid)参数/返回值:
| 项目 | 说明 |
|---|---|
tid | 任务 ID |
| 返回值 | 该任务的栈顶指针(当前 SP)stack_top |
完整源码(kernel/source/os_task.c):
void *os_task_get_stack_top(os_task_id tid)
{
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(task != OS_NULL);
return task->stack_top;
}分步详解:
第一步:将任务 ID 转为 TCB 指针(无条件执行)
task = OS_TYPE_CONVERT(os_task_t *, tid);- 与 3.1.1 相同。
第二步:断言任务指针非空(条件 task != OS_NULL)
OS_ASSERT(task != OS_NULL);- 防空指针。
第三步:返回栈顶指针(无条件执行)
return task->stack_top;- 返回
stack_top。注意stack_top是动态变化的当前 SP。
易混淆点:
stack_top(动态 SP)与stack_end(固定栈顶边界)概念不同。stack_end永远不变,stack_top随任务运行上下浮动。文档 4.1 示例中用stack_end - stack_top计算"已使用量"就依赖这一关系。
3.1.4 三个查询 API 对比
| API | 返回字段 | 含义 | 是否动态变化 |
|---|---|---|---|
os_task_get_stack_begin | stack_begin | 栈起始(低地址边界) | 固定 |
os_task_get_stack_end | stack_end | 栈结束(高地址边界) | 固定 |
os_task_get_stack_top | stack_top | 当前栈顶(SP) | 随运行变化 |
3.2 溢出处理钩子
os_safety_task_stack_overflow_set_hook —— 注册溢出处理钩子
函数签名:
void os_safety_task_stack_overflow_set_hook(void (*hook)(void))参数:
| 参数 | 类型 | 说明 |
|---|---|---|
hook | void (*)(void) | 溢出时将被调用的回调函数指针 |
完整源码(kernel/source/os_safety.c):
void os_safety_task_stack_overflow_set_hook(void (*hook)(void))
{
gs_os_safety_task_stack_overflow_hook = hook;
}分步详解:
第一步:参数为无参无返回值的回调指针(无条件执行)
hook 是 void (*hook)(void) 类型,指向用户自定义的溢出处理函数。
第二步:写入全局钩子指针(无条件执行)
gs_os_safety_task_stack_overflow_hook = hook;- 将钩子保存到全局静态指针
gs_os_safety_task_stack_overflow_hook(定义于os_safety.c顶部,初始为OS_NULL)。 - 此后每次栈溢出处理都会调用它。
前提:
os_safety_*系列函数只有在OS_USING_SAFETY_MECHANISM使能时才编译为真实函数。未使能时,os_safety_task_stack_overflow_process()是os_safety.h中的宏("关中断 + 死循环"),且os_safety_task_stack_overflow_set_hook函数不存在(编译报错)。
os_safety_task_stack_overflow_process —— 处理栈溢出
形态一(OS_USING_SAFETY_MECHANISM 未使能,os_safety.h 宏):
#define os_safety_task_stack_overflow_process() \
do \
{ \
os_irq_disable(); \
while (1) \
; \
} while (0)形态二(OS_USING_SAFETY_MECHANISM 使能,os_safety.c 真实函数):
void os_safety_task_stack_overflow_process(void)
{
if (OS_NULL != gs_os_safety_task_stack_overflow_hook)
{
gs_os_safety_task_stack_overflow_hook();
}
else
{
os_irq_disable();
while (1)
;
}
return;
}分步详解(以"形态二"为例):
第一步:检查是否注册了溢出钩子(条件 gs_os_safety_task_stack_overflow_hook != OS_NULL)
if (OS_NULL != gs_os_safety_task_stack_overflow_hook)
{
gs_os_safety_task_stack_overflow_hook();
}- 若注册了钩子,调用它,把溢出处理权交给用户(如记录现场、复位)。
第二步:未注册则停机(条件 gs_os_safety_task_stack_overflow_hook == OS_NULL)
else
{
os_irq_disable();
while (1)
;
}- 关中断 + 死循环停机,阻止已溢出系统继续运行造成破坏。
第三步:返回(无条件执行)
return;- 语法兜底,正常不会执行到(要么钩子自行处理,要么死循环)。
os_safety_task_stack_overflow_process() 条件走向总结
os_safety_task_stack_overflow_process() 条件走向总结:
├── OS_USING_SAFETY_MECHANISM 未使能(宏形态)
│ └── os_irq_disable() + while(1) 死循环停机
└── OS_USING_SAFETY_MECHANISM 使能(函数形态)
├── gs_os_safety_task_stack_overflow_hook != OS_NULL
│ └── 调用钩子
└── gs_os_safety_task_stack_overflow_hook == OS_NULL
└── os_irq_disable() + while(1) 死循环停机3.3 溢出检测与处理的完整流程
任务切换点
│
▼
os_task_switch_notify()
│
└─ OS_USING_OVERFLOW_CHECK 使能?
└─ 是 → _k_task_stack_check(current, next)
├─ 检查出任务 stack
│ └─ os_task_stack_is_overflow()?
│ ├─ 是 → OS_KERN_LOG 打印错误
│ │ └─ os_safety_task_stack_overflow_process()
│ └─ 否 → 继续
├─ 检查入任务 stack
│ └─ os_task_stack_is_overflow()?
│ ├─ 是 → OS_KERN_LOG 打印错误
│ │ └─ os_safety_task_stack_overflow_process()
│ └─ 否 → 继续
└─ 完成四、使用场景
4.1 一句话核心
栈溢出检测用于在开发/调试阶段尽早发现任务栈分配不足——一旦某个任务因调用过深或局部变量过大而"用穿"栈,系统在任务切换时即可定位到是哪个任务溢出,从而及时调整栈大小或修复代码。
4.2 检测与调试对比
| 手段 | 做什么 | 适用阶段 |
|---|---|---|
自动溢出检测(OS_USING_OVERFLOW_CHECK) | 任务切换时自动检查,溢出即报错/停机 | 开发调试,默认开启 |
栈使用量查询(os_task_get_stack_*) | 手动查看各任务栈峰值用量 | 栈大小调优 |
溢出钩子(os_safety_task_stack_overflow_set_hook) | 溢出时执行自定义兜底(复位/记录) | 生产兜底 |
4.3 结合源码的使用注意点
结合 os_task.c / arch_task.c / os_safety.c 实际实现:
- 检测是"切换时"而非"实时":溢出检查只在任务切换时触发,若某任务长时间占用 CPU 且栈已溢出,要等它被切走时才发现。因此栈分配应留足余量。
- 钩子需开启
OS_USING_SAFETY_MECHANISM:否则os_safety_task_stack_overflow_set_hook函数不存在,无法编译(见 3.2 节前提)。 - 钩子全局唯一:
gs_os_safety_task_stack_overflow_hook只有一个,多次注册会覆盖前一个。 '$'是水印而非真实数据:栈中的'$'是填充值,不能当作有效数据使用;os_hw_stack_max_used依赖它统计最大用量。- 栈溢出会破坏相邻内存:检测到溢出后应立即处理(复位),不要继续运行。
stack_top是动态 SP:用它计算已用栈量时要用stack_end - stack_top(见 4.1 示例)。
五、代码示例
5.1 检查当前任务的栈使用情况
以下示例获取当前任务(用 os_get_current_task())的栈边界,打印已用与总量。注意:os_get_current_task() 是 OneOS 获取当前任务的标准 API。
#include <os_task.h>
#include <os_util.h>
void check_stack_usage(void)
{
os_task_id self;
void *stack_top;
void *stack_begin;
void *stack_end;
os_uint32_t stack_total;
os_uint32_t stack_used;
/* 步骤 1:获取当前任务 ID */
self = os_get_current_task();
/* 步骤 2:获取当前任务的三个栈地址 */
stack_top = os_task_get_stack_top(self);
stack_begin = os_task_get_stack_begin(self);
stack_end = os_task_get_stack_end(self);
/* 步骤 3:计算总大小与已用量 */
stack_total = (os_uint32_t)stack_end - (os_uint32_t)stack_begin;
stack_used = (os_uint32_t)stack_end - (os_uint32_t)stack_top;
/* 步骤 4:打印栈信息 */
os_kprintf("Task: %s\r\n", os_task_get_name(self));
os_kprintf("Stack total: %d bytes\r\n", stack_total);
os_kprintf("Stack used: %d bytes\r\n", stack_used);
os_kprintf("Stack usage: %d%%\r\n", (stack_used * 100) / stack_total);
/* 步骤 5:使用率超过 80% 时告警 */
if ((stack_used * 100) / stack_total > 80)
{
os_kprintf("WARNING: Stack usage is high!\r\n");
}
}实际效果:在当前任务内调用,会打印类似:
Task: app_task
Stack total: 2048 bytes
Stack used: 512 bytes
Stack usage: 25%若当前任务栈使用率超 80%,还会追加一行 WARNING: Stack usage is high!。
说明:
stack_used = stack_end - stack_top计算的是当前瞬时已用栈量(从栈顶到 SP 的距离),而非历史峰值。要看历史最大用量应使用水印测量(见 2.5 节)。
5.2 周期性检查当前任务栈水位
OneOS 未提供公开的"按索引遍历所有任务"的 API,但提供 os_task_get_total_count() 统计任务总数。最常见的做法是在某个调试任务中周期性检查当前任务的栈余量,一旦余量过低即告警。
#include <os_task.h>
#include <os_util.h>
/* 栈余量低于该比例(20%)即告警 */
#define STACK_WARN_PERCENT 20
void check_current_task_stack(void)
{
os_task_id self;
void *stack_top;
void *stack_begin;
void *stack_end;
os_uint32_t stack_total;
os_uint32_t stack_used;
os_uint32_t used_percent;
/* 步骤 1:获取当前任务 */
self = os_get_current_task();
/* 步骤 2:获取栈边界 */
stack_top = os_task_get_stack_top(self);
stack_begin = os_task_get_stack_begin(self);
stack_end = os_task_get_stack_end(self);
/* 步骤 3:计算已用量与占比 */
stack_total = (os_uint32_t)stack_end - (os_uint32_t)stack_begin;
stack_used = (os_uint32_t)stack_end - (os_uint32_t)stack_top;
used_percent = (stack_used * 100) / stack_total;
/* 步骤 4:打印栈使用情况 */
os_kprintf("Task %s: used %d / %d bytes (%d%%)\r\n",
os_task_get_name(self), stack_used, stack_total, used_percent);
/* 步骤 5:余量不足时告警,提醒调大栈 */
if (used_percent > (100 - STACK_WARN_PERCENT))
{
os_kprintf("WARNING: Task %s stack is nearly full!\r\n", os_task_get_name(self));
}
}实际效果:在当前任务内调用,会打印当前任务栈的已用量、总量和占比;若栈使用超过 80%,则追加一条 WARNING 提示需要调大栈。可在空闲任务或调试任务中周期性调用,以持续监控各任务(由调用者自行决定在哪个任务里调用)。
说明:若需要监控其他任务的栈,可在目标任务的 TCB 仍存活时,用
os_task_get_id(const char *name)按任务名取得其tid,再调用os_task_get_stack_begin/end/top。
5.3 自定义堆栈溢出处理钩子
#include <os_safety.h>
#include <os_task.h>
static void my_stack_overflow_hook(void)
{
os_task_id self;
uint8_t *top;
uint8_t *begin;
uint8_t *end;
/* 步骤 1:获取发生溢出的当前任务 */
self = os_get_current_task();
/* 步骤 2:记录故障任务名 */
os_kprintf("!!! STACK OVERFLOW !!!\r\n");
os_kprintf("Fault task: %s\r\n", os_task_get_name(self));
/* 步骤 3:打印栈现场,便于人工分析 */
top = (uint8_t *)os_task_get_stack_top(self);
begin = (uint8_t *)os_task_get_stack_begin(self);
end = (uint8_t *)os_task_get_stack_end(self);
os_kprintf("Stack top: 0x%p, begin: 0x%p, end: 0x%p\r\n", top, begin, end);
/* 步骤 4:进入安全状态,避免已损坏的栈继续运行 */
/* 生产环境可将故障信息写入非易失存储后再复位或停在这里 */
os_irq_disable();
while (1)
{
}
}
void init_stack_overflow_handler(void)
{
/* 步骤 5:注册溢出处理钩子 */
os_safety_task_stack_overflow_set_hook(my_stack_overflow_hook);
}实际效果:当任意任务栈溢出时,os_safety_task_stack_overflow_process() 会调用 my_stack_overflow_hook,打印故障任务名与栈现场,随后关中断停住,而不是默认的"裸死循环无信息"。相比默认处理,钩子提供了现场信息输出,便于定位是哪个任务溢出。
编译前提:必须开启
OS_USING_SAFETY_MECHANISM,否则os_safety_task_stack_overflow_set_hook不存在。
六、栈大小估算
6.1 估算方法
栈大小取决于多个因素:
| 因素 | 估算方法 |
|---|---|
| 函数调用深度 | 每层调用约 64-256 字节(取决于架构和局部变量) |
| 局部变量 | 所有局部变量的总和 |
| 中断上下文 | 中断服务函数使用的栈空间(ISR 通常使用任务栈) |
| 嵌套中断 | 最大中断嵌套深度 × 每个 ISR 的栈需求 |
| 编译器优化 | 优化后可能减少栈使用 |
| 安全余量 | 建议保留 20%-30% 的余量 |
6.2 经验值
| 任务类型 | 建议栈大小 |
|---|---|
| 简单任务(LED 闪烁、按键扫描) | 256 - 512 字节 |
| 中等任务(串口通信、传感器读取) | 512 - 1024 字节 |
| 复杂任务(网络协议、文件系统) | 2048 - 4096 字节 |
| 初始化任务(启动后删除) | 1024 - 2048 字节 |
6.3 确定实际栈大小
推荐的步骤:
- 初始分配:给任务分配一个较大的栈(如 2048 字节)
- 运行测试:在典型负载下运行系统一段时间
- 检查栈使用:用栈查询 API(见 5.1)或
os_hw_stack_max_used(见 2.5)查看实际栈峰值使用量 - 调整大小:将栈大小调整为实际使用量的 1.3-1.5 倍
- 验证:在边界条件下再次验证
七、最佳实践
- 开启栈溢出检测:保持
OS_USING_OVERFLOW_CHECK默认开启,开发期能尽早暴露栈不足。 - 定期检查栈使用:在系统空闲或调试任务中定期检查各任务的栈使用情况。
- 预留安全余量:栈大小至少预留 20% 的安全余量。
- 避免大局部变量:大型数组或结构体应使用动态分配或静态分配。
- 限制递归深度:避免深度递归,或使用迭代替代递归。
- 注册溢出钩子:生产环境中注册钩子,确保溢出时能及时响应(需开
OS_USING_SAFETY_MECHANISM)。 - 代码审查:关注函数调用链的深度和局部变量的大小。
📝 本节小结
核心要点回顾
- 任务栈三大地址:
stack_begin(起始边界)、stack_end(结束边界)、stack_top(动态 SP)。 - 栈水印:任务创建时用
memset(stack_begin, '$', stack_size)填满'$'(0x24),运行后未使用区仍是'$'。 os_task_stack_is_overflow():三条件取或判定溢出——①stack_begin处不是'$'(水印破坏)、②stack_top < stack_begin(下溢出)、③stack_top >= stack_end(上溢出)。- 检测时机:每次任务切换时
os_task_switch_notify()→_k_task_stack_check()检查出/入两个任务。 - 查询 API:
os_task_get_stack_begin/end/top分别返回三个栈地址。 - 溢出处理:
os_safety_task_stack_overflow_process()有钩子则调钩子,否则关中断死循环;钩子需开OS_USING_SAFETY_MECHANISM。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
用 os_task_self() 获取当前任务 | OneOS 标准 API 是 os_get_current_task() |
混淆 stack_top 与 stack_end | stack_top 是动态 SP,stack_end 是固定栈顶边界 |
用 stack_top - stack_begin 算已用量 | 栈向下增长,已用量应为 stack_end - stack_top |
| 以为溢出检测是实时的 | 检测只在任务切换时触发 |
未开 OS_USING_SAFETY_MECHANISM 就注册钩子 | 该函数不存在,需先开启配置 |
| 多处注册溢出钩子 | 钩子全局唯一,后注册覆盖先注册 |
把 '$' 水印当真实数据 | '$' 只是填充值,用于检测/统计 |
| 栈溢出后继续运行 | 检测到溢出应立即复位或进入安全兜底 |
下一步
至此第 4 章"OneOS 内核组件详解"全部完成。接下来进入设备层,学习 OneOS 的设备驱动框架:
- 5.1 设备驱动框架:了解 OneOS 设备模型的统一抽象、设备注册与操作接口,掌握如何在应用层通过标准 API 操作各类外设。