4.6.4 堆栈溢出检测
2026/7/16大约 6 分钟内核组件内核错误处理堆栈溢出栈检测
4.6.4 堆栈溢出检测
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务栈概念、内存布局、RTOS 任务管理
🎯 学习目标
- 理解堆栈溢出在 RTOS 中的危害
- 掌握堆栈溢出的检测方法和原理
- 学会使用栈相关 API 进行栈检查
- 掌握栈大小估算和配置的最佳实践
一、堆栈溢出概念
1.1 什么是堆栈溢出
每个任务在创建时分配了固定大小的栈空间。当任务运行过程中使用的栈空间超过分配的大小时,就会发生堆栈溢出(Stack Overflow)。
堆栈溢出是 RTOS 开发中最常见也最危险的 bug 之一,因为:
- 它可能破坏相邻任务的数据结构或栈空间
- 它可能导致不可预测的系统行为(数据损坏、HardFault)
- 它通常很难通过静态分析发现
1.2 堆栈溢出的典型原因
| 原因 | 说明 |
|---|---|
| 栈大小估算不足 | 创建任务时分配的栈空间太小 |
| 深层递归调用 | 函数递归调用导致栈深度过大 |
| 大局部变量 | 函数中定义了大型数组或结构体 |
| 中断嵌套 | 多层中断嵌套消耗大量栈空间 |
| 函数调用链过长 | 深度嵌套的函数调用 |
二、检测方法
2.1 栈水印(Watermark)检测
栈水印检测是最常用的堆栈溢出检测方法:
任务栈空间布局:
┌──────────────────────┐ stack_end (高地址)
│ │
│ 已使用的栈空间 │ ← SP (栈指针)
│ │
├──────────────────────┤
│ 未使用的栈空间 │
│ │
│ 水印填充区域 │ ← 初始化为特定值(如 0xCC)
└──────────────────────┘ stack_begin (低地址)原理:
- 任务创建时,将栈空间的未使用部分填充为特定的水印值(如
0xCCCCCCCC) - 运行时,通过检查栈底部的填充值是否被修改来判断栈的使用情况
- 如果填充值被覆盖,说明栈使用量已经接近或超过了分配的大小
2.2 栈边界检查
通过检查栈指针是否超出栈边界来检测溢出:
void *stack_top = os_task_get_stack_top(task_id);
void *stack_begin = os_task_get_stack_begin(task_id);
void *stack_end = os_task_get_stack_end(task_id);
/* 检查栈指针是否在有效范围内 */
if (stack_top < stack_begin || stack_top >= stack_end)
{
/* 栈溢出! */
}三、栈相关 API
3.1 获取栈信息
定义在 kernel/include/os_task.h 中:
/* 获取任务栈顶部(当前 SP) */
void *os_task_get_stack_top(os_task_id tid);
/* 获取任务栈起始地址 */
void *os_task_get_stack_begin(os_task_id tid);
/* 获取任务栈结束地址 */
void *os_task_get_stack_end(os_task_id tid);3.2 堆栈溢出处理
定义在 kernel/include/os_safety.h 中:
/* 注册堆栈溢出处理钩子 */
void os_safety_task_stack_overflow_set_hook(void (*hook)(void));
/* 处理堆栈溢出 */
void os_safety_task_stack_overflow_process(void);四、代码示例
4.1 检查当前任务的栈使用情况
#include <os_task.h>
#include <os_util.h>
void check_stack_usage(void)
{
os_task_id self = os_task_self();
void *stack_top = os_task_get_stack_top(self);
void *stack_begin = os_task_get_stack_begin(self);
void *stack_end = os_task_get_stack_end(self);
os_uint32_t stack_total = (os_uint32_t)stack_end - (os_uint32_t)stack_begin;
os_uint32_t stack_used = (os_uint32_t)stack_end - (os_uint32_t)stack_top;
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);
/* 警告:使用超过 80% */
if (stack_used * 100 / stack_total > 80)
{
os_kprintf("WARNING: Stack usage is high!\r\n");
}
}4.2 检查所有任务的栈使用情况
void check_all_tasks_stack(void)
{
os_task_id tid = OS_NULL;
os_uint32_t total_tasks = os_task_get_total_count();
os_uint32_t i;
os_kprintf("=== Stack Usage Report ===\r\n");
for (i = 0; i < total_tasks; i++)
{
tid = os_task_get_by_index(i);
if (tid == OS_NULL)
continue;
void *stack_top = os_task_get_stack_top(tid);
void *stack_begin = os_task_get_stack_begin(tid);
void *stack_end = os_task_get_stack_end(tid);
os_uint32_t total = (os_uint32_t)stack_end - (os_uint32_t)stack_begin;
os_uint32_t used = (os_uint32_t)stack_end - (os_uint32_t)stack_top;
os_kprintf("%-16s: %4d / %4d bytes (%3d%%)\r\n",
os_task_get_name(tid),
used, total,
(used * 100) / total);
}
os_kprintf("==========================\r\n");
}4.3 自定义堆栈溢出处理钩子
#include <os_safety.h>
static void my_stack_overflow_hook(void)
{
os_task_id self = os_task_self();
/* 记录故障信息 */
os_kprintf("!!! STACK OVERFLOW !!!\r\n");
os_kprintf("Fault task: %s\r\n", os_task_get_name(self));
void *stack_top = os_task_get_stack_top(self);
void *stack_begin = os_task_get_stack_begin(self);
void *stack_end = os_task_get_stack_end(self);
os_kprintf("Stack top: 0x%08X\r\n", stack_top);
os_kprintf("Stack begin: 0x%08X\r\n", stack_begin);
os_kprintf("Stack end: 0x%08X\r\n", stack_end);
/* 保存关键信息到非易失存储 */
save_fault_info_to_flash("STACK_OVERFLOW");
/* 执行系统复位 */
os_hw_reboot();
}
void init_stack_overflow_handler(void)
{
os_safety_task_stack_overflow_set_hook(my_stack_overflow_hook);
}五、栈大小估算
5.1 估算方法
栈大小取决于多个因素:
| 因素 | 估算方法 |
|---|---|
| 函数调用深度 | 每层调用约 64-256 字节(取决于架构和局部变量) |
| 局部变量 | 所有局部变量的总和 |
| 中断上下文 | 中断服务函数使用的栈空间(ISR 通常使用任务栈) |
| 嵌套中断 | 最大中断嵌套深度 × 每个 ISR 的栈需求 |
| 编译器优化 | 优化后可能减少栈使用 |
| 安全余量 | 建议保留 20%-30% 的余量 |
5.2 经验值
| 任务类型 | 建议栈大小 |
|---|---|
| 简单任务(LED 闪烁、按键扫描) | 256 - 512 字节 |
| 中等任务(串口通信、传感器读取) | 512 - 1024 字节 |
| 复杂任务(网络协议、文件系统) | 2048 - 4096 字节 |
| 初始化任务(启动后删除) | 1024 - 2048 字节 |
5.3 确定实际栈大小
推荐的步骤:
- 初始分配:给任务分配一个较大的栈(如 2048 字节)
- 运行测试:在典型负载下运行系统一段时间
- 检查栈使用:使用
check_stack_usage()或调试器查看实际栈使用量 - 调整大小:将栈大小调整为实际使用量的 1.3-1.5 倍
- 验证:在边界条件下再次验证
六、最佳实践
- 启用栈水印填充:在任务创建时填充栈空间为已知值,便于检测栈使用量
- 定期检查栈使用:在系统空闲或调试任务中定期检查各任务的栈使用情况
- 预留安全余量:栈大小至少预留 20% 的安全余量
- 避免大局部变量:大型数组或结构体应使用动态分配或静态分配
- 限制递归深度:避免深度递归,或使用迭代替代递归
- 注册堆栈溢出钩子:在生产环境中注册钩子,确保溢出时能及时响应
- 代码审查:关注函数调用链的深度和局部变量的大小
📝 本节小结
堆栈溢出是 RTOS 开发中最常见也最危险的 bug。OneOS 通过栈水印检测和栈边界检查两种方法检测堆栈溢出,并提供 os_task_get_stack_top/begin/end API 用于获取栈信息,以及 os_safety_task_stack_overflow_set_hook 注册自定义处理钩子。栈大小的合理估算需要综合考虑函数调用深度、局部变量、中断嵌套等因素,建议通过实际测量确定,并预留 20%-30% 的安全余量。