目前绝大部分的rtos都是没有基于mmu或者mpu来运行的,也就是说没有内存保护机制。在没有内存保护机制的前提下,最揪人心的莫过于任务栈空间的溢出了,任务栈空间的溢出随时就像一颗定时炸弹等着引爆,使用者浑身不自在。有没有100%的任务栈空间溢出检查机制呢?答案是没有的。但是可以做到99%以上的软件自动检测机制。
下面来用实际的例子来说明任务栈空间溢出的情况。
void task1()
{
RAW_U32 temp[1000]; (1)
………………………
/*use temp array*/
………………………
}
(1) 处代码的地方会消耗4K的任务栈空间,如果任务的栈空间小于4K, 那瞬间任务栈就溢出了。
还有一种典型的任务栈溢出的情况,任务的函数体内运行并不会造成任务栈的溢出,但是在切换任务的时候会导致溢出,因为切换任务的时候任务的栈会额外的承受一些空间,因为任务切换的时候,需要把当前任务的寄存器保存到当前的任务栈空间中,这样会有可能导致任务栈的溢出。
针对以上的情况,raw-os设计了4种任务栈的检测机制。一种是用户主动地去探测任务栈的使用情况,其余3种是raw-os系统会主动的实时监测任务栈溢出。
下面具体说明一下4种任务栈检测机制的原理。
第一种是为广大用户所熟知的检测方式,即用户可以主动调用函数raw_task_stack_check去检测特定的任务的堆栈使用情况,比如:
raw_task_stack_check(&task1, &free_stack);
这个函数的意义是检测task1的栈空间使用情况,获得的任务栈空间剩余空间会存入free_stack所指向的空间。
绝大部分rtos都实现了此种方式。这种方式简单明了,但是缺点也是很明显的,很难让用户实时去检测所有任务的栈空间使用情况。
raw-os提供了另外3种靠系统去检测任务栈空间溢出的方法,具体的原理如下:
假设任务的栈空间是往下增长的,假设任务函数体内已经栈溢出,在某个时候任务会主动切换到其它任务,或者通过中断触发到更高优先级的任务。这个时候在任务栈空间中已经保存了寄存器的时候,可以做两种判断机制:
第一种判断任务栈的底部是否是原先的0值,因为创建任务的时候,任务的栈会被全部清0,所以,如果这个时候任务栈的底部这个值不为0的话,说明栈空间已经溢出。
第二种判断是,在任务栈中保存了任务所有寄存器的前提下,判断当前任务的栈指针是否已经小于原先任务底部的栈值。在栈空间往下生长的情况下,如果是小于了,说明栈空间已经溢出了。
下面给出上面两种任务栈检测方式的代码,代码具体的位置在raw_state.c中,有兴趣的读者可以全面参考。
void raw_stack_check(void)
{
PORT_STACK *task_stack_start;
task_stack_start = raw_task_active->task_stack_base;
/*statck check method 1*/
if (*task_stack_start) {
RAW_ASSERT(0);
}
/*statck check method 2*/
if ((PORT_STACK *)(raw_task_active->task_stack) < task_stack_start) {
RAW_ASSERT(0);
}
}
有了上述的理论基础,理解这段代码也就不难了,不再详述。唯一需要注意的是raw_stack_check这个函数被调用的位置必须是任务切换的时候汇编里面,而且已经栈空间里面保存了当前任务的寄存器。
有了以上的2种方法做检测,可以90%以上帮助用户监测任务栈空间的使用情况。
raw-os的真正独到之处在于增加了第4种任务栈溢出的检测,大部分时候防患于未然,是最好的方式。只要找raw_config.h里面打开宏定义RAW_SYSTEM_CHECK,系统的空闲任务就会无时不刻的帮用户去实时检测任务栈空间的使用情况,只要一检测出任务的栈空间已经不足12%的整体空间的时候,会立即停掉整个系统,来通知用户任务的栈溢出了。目前只有raw-os一家提供了这种任务栈的检测机制。具体的代码可以参考raw_idle.c 不再细述。
具体任务栈空间移植的情况可以参考官网上的k60以及cortex-m3的工程移植。用户只要copy过去这些移植文件就可以,不用担心移植的麻烦性。
第一种方法:
task_stack_start = raw_task_active->task_stack_base;
/*statck check method 1*/
if (*task_stack_start) {
RAW_ASSERT(0);
}
栈空间从高地址向低地址升长,raw_task_active->task_stack_base就是任务的栈底(好像不能这么叫),就是任务栈空间最低的地址,所以,如果这个地址不为0,就说明入栈的数据已经把栈占满了,所以栈溢出了。
第二种方法:
当任务执行到raw_stack_check(void)这个函数时,是发生任务切的时候,确切的说是任务切换后,切换时已保存了应该保存的东西,再在这个函数里定义了一个局部变量task_stack_start也存在于该任务的栈区,如果raw_task_active->task_stack) < task_stack_start。。。。。“判断当前任务的栈指针是否已经小于原先任务底部的栈值” 这句话不是很理解啊。。。能再解释一下?
task_stack_start 是刚刚被切换掉的任务的栈的底部的栈值?
这两种方法都是处于任务未切换的时候,切换了检查就没意义了。假设任务1即将切换到任务2,这个时候检查的是任务1的栈,第二种方法检查的是任务1的sp指针是否已经压的小于已经分配的sp指针范围,sp指针是往下面增长的,所以超越了肯定是越界了。
其实任务栈的检查都是防患于未然的行为,要想做出谁溢出谁死亡,不影响其他任务的安全特性,只有浪费些内存,将栈做成页对齐的型式,采用MMU页保护的方法才能避免。栈检测应该是一种产品开发期间的统计行为,可以在创建任务时将栈初始化为全F或魔鬼数字,长时间多场景地运行系统,统计出各任务栈的使用情况,内存允许的话,可以将各任务栈大小设置为其最大栈使用值的1.5倍就基本安全了
第 一种检查的是栈空间的最底地址是否已经有数据被写过。。。。。写过,那肯定是溢出了。。
第二种检查的是当前任务的栈指针是否低于本任务栈空间的最低地址。。。。低于肯定溢出了。。这个理论是懂了。
但是代码:
if ((PORT_STACK *)(raw_task_active->task_stack) < task_stack_start) {
RAW_ASSERT(0);
}
raw_task_active->task_stack 这个变量是当前任务的栈指针,这个值是实时变化的?实时的指向系统的栈指针的吗。?
raw_task_active->task_stack 这个是当前任务的栈指针,在这个切换点上不会变化的。 task_stack_start也不会变化。
这个地方确实很重要的,找工作面试时被很多次问到,,,都是说栈溢出怎么规避。