一、现象
- 在一个RTOS任务中,分配低栈空间,任务运行途中卡死(运行3-10S),任务状态依旧处于running。
- uxTaskGetStackHighWaterMark循环检查任务历史最小剩余一直有余量。
- configCHECK_FOR_STACK_OVERFLOW设定为2,但是vApplicationStackOverflowHook一直未触发。
- 当增加栈空间后问题解决。
二、为什么uxTaskGetStackHighWaterMark检查有余量,但实际堆栈溢出
- FreeRTOS给每个任务分配栈时,会在栈首和栈尾填充特定Magic Number(通常是0xA5A5A5A5),
HighWaterMark是靠这些填充数据检测剩余空间的。 - 但是,如果你的代码一瞬间局部变量太大(比如创建局部数组/调用复杂函数栈帧爆了),导致越界,
- HighWaterMark统计的是过去最小的情况,而不是你爆栈的那一瞬间。
如果刚好轻微破坏了TCB附近的数据(而不是马上崩溃),任务状态还能显示正常,但实际上已经逻辑死循环或异常了。
void myTask(void *arg) {
float hugeArray[1000]; // 大局部数组!直接吃掉4K栈空间!
...
}
或者调用了深层递归函数、复杂printf等都会瞬间爆栈,HighWaterMark完全来不及更新。
三、为什么vApplicationStackOverflowHook未能触发
(1)机制
FreeRTOS 栈溢出检测机制原理是:
-
创建任务时,在栈顶(低地址)打一段固定模式(如 0xA5A5A5A5)。
-
任务切换时检测这个栈顶位置有没有被破坏。
(2)原因
实际上是典型的“逻辑栈溢出”,但不是物理栈破坏,所以:
-
FreeRTOS 没检测到“栈顶被污染”,就不会触发 vApplicationStackOverflowHook()。
-
但你的任务栈空间确实不足,局部变量、深层递归调用、库函数内部临时变量,偷偷用了大量栈,导致:
任务运行到一半,自己踩到了自己的堆栈空间; -
但是没有越界到栈保护区(FreeRTOS只能检查到栈顶附近是否被破坏,无法实时感知栈快满了)。
所以表面上没检测到栈溢出,但实际上栈已经不够用了。 -
如果只是栈用了太多,比如接近爆了但还没破坏到栈顶的标记区,FreeRTOS是检测不到的。
(3)深层原因
通常 MCU(比如 STM32)上的栈是向下增长的。 假设初始堆栈分配是这样的(地址从上往下):
[ 高地址 ]
| | <- 起始地址(栈顶)
| |
| 局部变量 | <- 函数内分配的变量/数组
| 返回地址 | <- 调用链的返回地址
| 更多局部变量 |
| |
| |
| |
| |
[ 低地址 ]
如果你开启了 configCHECK_FOR_STACK_OVERFLOW,FreeRTOS会在栈的最低端(最低地址附近)放一个特殊的哨兵区(如固定值0xA5A5A5A5),周期性去检查它有没有被破坏。 比如:
[ 高地址 ]
| |
| 局部变量 |
| ... |
| |
| 栈还没到底 |
| |
| ---栈保护哨兵区---
| 0xA5A5A5A5 |
[ 低地址 ]
所以,只有当整个栈一路冲到最低地址附近,踩坏了哨兵区,才触发 vApplicationStackOverflowHook()。
但实际上,绝大多数栈溢出,都是在没到底就出事了 —— 因为:
-
你的局部数组/局部变量/递归调用/深层函数调用
-
直接在还在“安全区”内的栈空间分配太多内存
-
把局部返回地址、栈帧链表给破坏
[ 高地址 ]
| 函数A的局部变量 |
| 函数A的返回地址 |
| 函数B的局部变量 | <--- 这里超了
| 函数B的返回地址 |
| 栈空间没用完! |
| 栈保护区还很远! |
| 0xA5A5A5A5 |
[ 低地址 ]
为什么局部变量踩坏了比踩保护区更容易?
因为:
-
任务本来就需要动态调用函数、分配局部变量
-
如果有稍大一点的局部数组(比如 float temp[100]),马上就能把调用链踩坏
-
FreeRTOS不会实时知道你用栈用到哪一层,只能靠定时检查低地址哨兵区
-
只有最严重、最大量的堆栈爆发,才会真的压到最低保护区
所以——
- 栈空间超了,“先自己挂掉”,而不是“先让FreeRTOS察觉”。
四、为什么任务状态还能是 Running?
因为 FreeRTOS 只根据任务控制块(TCB)里的调度信息判断一个任务是否 “Running”。
-
只要你没手动挂起任务
-
任务还没被删除
-
没崩出到 HardFault_Handler
那么 FreeRTOS 认为你这个任务仍然“理论上在跑”。
但是实际上:
-
程序计数器(PC寄存器)已经跳飞了
-
或者跑在了非法地址死循环
-
或者 CPU异常硬件错误,但 FreeRTOS不知情
所以你看到的是 “Running”,但实际上任务早已 “精神失联” 了。🧟♂️
可以这么理解:
[ 栈起始 ] <-- FreeRTOS保护区
| |
| 任务调用时局部变量 |
| 函数调用返回地址 |
| .... |
| 超出栈容量这里踩坏自己|
| [死机、卡死、跳飞] |
[ 栈末尾 ]