RTOS任务都需要分配堆栈,堆栈大小不合理,会造成内存越界或者资源的浪费。
堆栈溢出
当任务所使用的堆栈空间超出分配给它的空间时,则会发生堆栈溢出。
堆栈溢出可能修改超过合法访问地址范围外的数据,严重时会导致 HardFault 系统崩溃。
其实,在大部分的RTOS中都提供了堆栈溢出检测的机制,下面我们结合常用的几个RTOS系统,详细解读下常见的堆栈溢出检测方法。
FreeRTOS堆栈溢出检测
可以通过 FreeRTOSConfig.h文件中定义configCHECK_FOR_STACK_OVERFLOW 宏来开启, 在任务切换时会自动执行检测栈溢出操作。
两种堆栈溢出检测方法:
方法一
- 开启方法:#define configCHECK_FOR_STACK_OVERFLOW 1
- 检测原理:通过在任务切换时,检测栈顶指针和栈起始指针,或者帧顶指针和栈结束指针是否越界,如果越界,在任务切换的时候触发堆栈溢出钩子Hook函数。
具体实现代码:
通过宏定义taskCHECK_FOR_STACK_OVERFLOW 来实现堆栈溢出检测。
(1)向下生长的栈
/* 如果打开堆栈溢出检测方法一,并且栈是向下生长 */
#if( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 ) )
/* 定义堆栈溢出检测的实现 */
#define taskCHECK_FOR_STACK_OVERFLOW() \
{ \
/* 判断栈顶指针 是否小于等于 栈起始指针 */ \
if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack ) \
{ \
/* 用户提供的栈溢出钩子Hook函数 */
vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB,
pxCurrentTCB->pcTaskName ); \
} \
}
#endif
(2)向上生长的栈
/* 如果打开堆栈溢出检测方法一,并且栈是向上生长 */
#if( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH > 0 ) )
/* 定义堆栈溢出检测的实现 */
#define taskCHECK_FOR_STACK_OVERFLOW() \
{ \
\
/* 判断栈顶指针 是否大于等于 栈底指针 */ \
if( pxCurrentTCB->pxTopOfStack >= pxCurrentTCB->pxEndOfStack ) \
{ \
/* 用户提供的栈溢出钩子Hook函数 */
vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB,
pxCurrentTCB->pcTaskName ); \
} \
}
#endif
方法二
- 开启方法:#define configCHECK_FOR_STACK_OVERFLOW 2
- 检测原理:在任务创建时,将任务栈所有的数据初始化为一个固定值0xa5, 通过任务切换的时候,来判断栈底16个或20个字节是否都为0xa5,如果被修改过,会触发堆栈溢出钩子Hook函数。
具体实现代码:
通过宏定义taskCHECK_FOR_STACK_OVERFLOW 来实现堆栈溢出检测。
(1)栈向下生长的情况:判断栈起始的16个字节是否被修改过
/* 如果打开堆栈溢出检测方法二,并且栈是向下生长 */
#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) && ( portSTACK_GROWTH < 0 ) )
/* 定义堆栈溢出检测的实现 */
#define taskCHECK_FOR_STACK_OVERFLOW() \
{ \
/* 获取当前任务栈的起始地址 */
const uint32_t * const pulStack = ( uint32_t * ) pxCurrentTCB->pxStack; \
const uint32_t ulCheckValue = ( uint32_t ) 0xa5a5a5a5; \
\
/* 判断栈起始的前4个数据是都等于0xa5 */
if( ( pulStack[ 0 ] != ulCheckValue ) || \
( pulStack[ 1 ] != ulCheckValue ) || \
( pulStack[ 2 ] != ulCheckValue ) || \
( pulStack[ 3 ] != ulCheckValue ) ) \
{ \
/* 如果不满足,则调用用户的栈溢出钩子Hook函数 */
vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB,
pxCurrentTCB->pcTaskName ); \
} \
}
#endif
(2)栈向上生长的情况:判断栈末尾的20个字节数据是否被修改过
#define tskSTACK_FILL_BYTE ( 0xa5U ) //定义数据0xa5
/* 如果打开堆栈溢出检测方法二,并且栈是向上生长 */
#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) && ( portSTACK_GROWTH > 0 ) )
/* 定义堆栈溢出检测的实现 */
#define taskCHECK_FOR_STACK_OVERFLOW() \
{ \
/* 获取当前任务的栈底指针的位置 */
int8_t *pcEndOfStack = ( int8_t * ) pxCurrentTCB->pxEndOfStack; \
static const uint8_t ucExpectedStackBytes[] = { tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, \
tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, \
tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, \
tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, \
tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE, tskSTACK_FILL_BYTE }; \
\
\
/* 栈底指针,先回退20个字节 */
pcEndOfStack -= sizeof( ucExpectedStackBytes ); \
\
/* 比较栈底的20个字节是否都为0xa5,从而判断栈底数据是否被修改过? */ \
if( memcmp( ( void * ) pcEndOfStack, ( void * ) ucExpectedStackBytes, sizeof( ucExpectedStackBytes ) ) != 0 ) \
{ \
/* 如果被修改过,说明可能发生栈溢出,调用栈溢出钩子Hook函数 */
vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \
} \
}
#endif
判断栈溢出检测的时机:
当然是在任务进行切换的时候,即 vTaskSwitchContext(void) 函数中,该函数中调用了
taskCHECK_FOR_STACK_OVERFLOW() 函数。
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
/* The scheduler is currently suspended - do not allow a context
switch. */
xYieldPending = pdTRUE;
}
...
...
/* Check for stack overflow, if configured. */
taskCHECK_FOR_STACK_OVERFLOW(); //检查栈溢出
...
...
}
存在的缺点:
方法一:
- 优点:速度快
- 缺点:不能检测所有的堆栈溢出,比如任务执行中确实出现了栈顶指针越界的情况,但是在任务切换之前 栈顶指针又指回到了合法位置,这个时候就检测不到栈溢出了。
方法二:
- 优点:速度比方法一慢一点(但对于用户来说还是很快的)
- 缺点:虽然几乎可以检测到所有的栈溢出情况,但是如果栈溢出时栈溢出值和栈标记值相同,即栈空间最后的几个字节修改完正好都是 0xa5 ,该情况就检测不到栈溢出了;或者压根没用去修改最后几个字节,但是还是溢出了,这个时候也检测不到栈溢出。
UCOSIII堆栈溢出检测
在UCOSIII中提供了任务创建函数OSTaskCreate,在进行任务创建的时候有两个参数:
stk_limit:设置堆栈深度的限制位置。这个值表示任务的堆栈满溢之前剩余的堆栈容量。
例如,指定 stk_size 值的 10%表示将达到堆栈限制,
当堆栈达到 90%满就表示任务的堆栈已满。
opt:用户可选的任务特定选项,可以取值如下:
OS_OPT_TASK_STK_CHK 启用任务的堆栈检测
OS_OPT_TASK_STK_CLR 任务创建时,清除堆栈
比如下面的示例:
//创建信号量任务
OSTaskCreate((OS_TCB *)&SEM_TCB, // 任务控制块指针
(CPU_CHAR *)"Sem", // 任务名称
(OS_TASK_PTR )Task_Sem, // 任务代码指针
(void *)0, // 传递给任务的参数parg
(OS_PRIO )TASK_SEM_PRIO, // 任务优先级
(CPU_STK *)&Sem_Stk[0], // 任务堆栈基地址
(CPU_STK_SIZE)TASK_SEM_STK_SIZE/10, // 堆栈剩余警戒线
(CPU_STK_SIZE)TASK_SEM_STK_SIZE, // 堆栈大小
(OS_MSG_QTY )0, // 可接收的最大消息队列数
(OS_TICK )0, // 时间片轮转时间
(void *)0, // 任务控制块扩展信息
(OS_OPT )(OS_OPT_TASK_STK_CHK |
OS_OPT_TASK_STK_CLR), // 任务选项
(OS_ERR *)&err); // 返回值
(1)在初始化任务的时候,会将所有的栈内容清空为0,使用后的栈不为0,在检测的时候只需从栈的低地址开始对为0的栈空间进行计数统计,然后通过计算就可以得出任务的栈使用了多少,还剩余多少。
(2)这些信息的统计都是由UCOSIII自带的统计任务函数进行的,每隔1/OSCfg_StatTaskRate_Hz 秒就进行更新。
其中StkLimitPtr的值就代表监测的任务堆栈溢出的水平线限制(称为“最高水位线”),如果需要使用栈溢出检测的功能, 需要在os_cfg_app.h文件中将OS_CFG_STAT_TASK_STK_CHK_EN宏定义配置为1。
然后用户自己在App_OS_TaskSwHook()钩子函数中实现在切换任务的时候,检测将要被载入CPU栈指针的值是否超出将要切换的任务的任务控制块中StkLimitPtr的限制。
- 使能堆栈溢出检测功能,需要打开宏定义OS_CFG_STAT_TASK_STK_CHK_EN
#define OS_CFG_STAT_TASK_STK_CHK_EN 1u /* Check task stacks from statistic task */
- 用户自己在任务切换的钩子函数中,实现剩余堆栈水平线限制的检测:
void App_OS_TaskSwHook (void)
{
/* 当执行任务切换的时候,会自动调用该钩子函数 */
}
(3) 在UCOSIII中提供了一个统计任务函数,在启用宏定义OS_CFG_STAT_TASK_EN后,系统会自动创建一个统计任务——OS_StatTask(), 它会在任务中计算CPU使用率,以及每个任务的栈使用情况。
具体实现代码如下:
void OS_StatTask (void *p_arg) //统计任务函数
{
#if OS_CFG_DBG_EN > 0u
#if OS_CFG_TASK_PROFILE_EN > 0u
OS_CPU_USAGE usage;
OS_CYCLES cycles_total;
OS_CYCLES cycles_div;
OS_CYCLES cycles_mult;
OS_CYCLES cycles_max;
#endif
OS_TCB *p_tcb;
#endif
OS_TICK ctr_max;
OS_TICK ctr_mult;
OS_TICK ctr_div;
OS_ERR err;
OS_TICK dly;
CPU_TS ts_start;
CPU_TS ts_end;
CPU_SR_ALLOC();
//使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和
//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
// SR(临界段关中断只需保存SR),开中断时将该值还原。
..........
..........
/* 栈检测 */
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u //如果启用了任务栈检测
OSTaskStkChk( p_tcb, //计算被激活任务的栈用量
&p_tcb->StkFree, //空闲栈大小
&p_tcb->StkUsed, //已用栈大小
&err);
#endif
..........
..........
}
(4)栈检测统计 OSTaskStkChk()
μC/OSIII提供OSTaskStkChk()函数用来进行栈检测统计,统计任务会以我们设定的运行频率不断更新栈使用的情况并且保存到任务控制块的StkFree和StkUsed成员变量中, 这两个变量分别表示任务栈的剩余空间与已使用空间大小,单位为任务栈大小的单位(在STM32中采用4字节)。
具体实现代码如下:
#if OS_CFG_STAT_TASK_STK_CHK_EN > 0u//如果启用了任务栈检测
void OSTaskStkChk (OS_TCB *p_tcb, (1)//目标任务控制块的指针
CPU_STK_SIZE *p_free, (2)//返回空闲栈大小
CPU_STK_SIZE *p_used, (3)//返回已用栈大小
OS_ERR *p_err) (4)//返回错误类型
{
CPU_STK_SIZE free_stk;
CPU_STK *p_stk;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必须用到该宏,该宏声明和
//定义一个局部变量,用于保存关中断前的 CPU 状态寄存器
// SR(临界段关中断只需保存SR),开中断时将该值还原。
#ifdef OS_SAFETY_CRITICAL//如果启用了安全检测
if (p_err == (OS_ERR *)0) //如果 p_err 为空
{
OS_SAFETY_CRITICAL_EXCEPTION(); //执行安全检测异常函数
return; //返回,停止执行
}
#endif
..........
..........
//如果CPU的栈是从高向低增长
#if CPU_CFG_STK_GROWTH == CPU_STK_GROWTH_HI_TO_LO
//从目标任务栈最低地址开始计算
p_stk = p_tcb->StkBasePtr;
while (*p_stk == (CPU_STK)0) //计算值为0的栈数目
{
p_stk++;
free_stk++;
}
#else //如果CPU的栈是从低向高增长
//从目标任务栈最高地址开始计算
p_stk = p_tcb->StkBasePtr + p_tcb->StkSize - 1u;
while (*p_stk == (CPU_STK)0) //计算值为0的栈数目
{
free_stk++;
p_stk--;
}
#endif
//返回任务栈的空闲大小
*p_free = free_stk;
//返回任务栈的已用大小
*p_used = (p_tcb->StkSize - free_stk);
*p_err = OS_ERR_NONE;
}
#endif
当然我们开发者也可以自己在某个任务中调用OSTaskStkChk()函数,进行统计某个任务的栈空间使用情况,而不必使用系统自带的统计任务。
RT-Thread堆栈溢出检测
在RT-Thread中采用了和FreeRTOS相同的检测方法,同时检测栈内的数据是否更改(即栈空间原来的数据'#',是否被改变)和 检测栈指针是否越界。
具体实现代码如下:
通过宏定义RT_USING_OVERFLOW_CHECK来实现堆栈溢出检测。
#ifdef RT_USING_OVERFLOW_CHECK //如果使能了堆栈溢出检测功能
static void _scheduler_stack_check(struct rt_thread *thread)
{
RT_ASSERT(thread != RT_NULL);
#ifdef ARCH_CPU_STACK_GROWS_UPWARD //向上增长的栈
if (*((rt_uint8_t *)((rt_ubase_t)thread->stack_addr + thread->stack_size - 1)) != '#' ||
#else //向下增长的栈,Cortex-M 系列属于这种
if (*((rt_uint8_t *)thread->stack_addr) != '#' ||
#endif
(rt_ubase_t)thread->sp <= (rt_ubase_t)thread->stack_addr ||
(rt_ubase_t)thread->sp >
(rt_ubase_t)thread->stack_addr + (rt_ubase_t)thread->stack_size)
{
rt_base_t level;
rt_kprintf("thread:%s stack overflow\n", thread->name);
level = rt_hw_interrupt_disable();
while (level); //栈溢出的话 就一直死在这里!!
}
#ifdef ARCH_CPU_STACK_GROWS_UPWARD //向上增长的栈
else if ((rt_ubase_t)thread->sp > ((rt_ubase_t)thread->stack_addr + thread->stack_size))
{
rt_kprintf("warning: %s stack is close to the top of stack address.\n",
thread->name);
}
#else //向下增长的栈,Cortex-M 系列属于这种
else if ((rt_ubase_t)thread->sp <= ((rt_ubase_t)thread->stack_addr + 32))
{
rt_kprintf("warning: %s stack is close to end of stack address.\n",
thread->name);
}
#endif
}
#endif
判断栈溢出检测的时机:
通常的做法就是在调度器切换线程的时候检查,调度器简化代码如下:
void rt_schedule(void)
{
rt_base_t level;
struct rt_thread *to_thread;
struct rt_thread *from_thread;
/* disable interrupt */
level = rt_hw_interrupt_disable();
to_thread = _get_highest_priority_thread(&highest_ready_priority);
..........
..........
from_thread = rt_current_thread;
rt_current_thread = to_thread;
#ifdef RT_USING_OVERFLOW_CHECK //如果使能了堆栈溢出检测功能
_rt_scheduler_stack_check(to_thread); //调用堆栈溢出检测
#endif
..........
..........
rt_hw_interrupt_enable(level);
}
同样存在的缺点,和FreeRTOS是一样的,有些场景下也是没办法完全覆盖到的。
综合对比来说,FreeRTOS和RT-Thread操作系统的堆栈溢出检测的方法,比较常用。