本文转自公众号,欢迎关注
https://mp.weixin.qq.com/s/uzaGLFTDBAn8wyR84yaiIw
RTOS的环境开发中,栈的溢出检测是一个重要的工作。栈溢出检测我们可以借助硬件的MPU等实现,也可以使用软件检测。这里分享Freertos中的实现。这里基于Cortex-M4硬件平台,一些具体的代码就未贴出了,顺便介绍了一下Cortex-M4栈相关的基础知识。
复位后汇编代码
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
会进入__main将栈内容写为0。该部分由编译器产生代码实现。
栈的位置是链接脚本中指定。
xTaskCreate -> prvInitialiseNewTask将任务栈填充为tskSTACK_FILL_BYTE = ( 0xa5U )
然后调用pxPortInitialiseStack初始化任务栈上下文
任务初始化时 |
---|
高地址 |
->任务切出时栈指针 |
低地址 |
任务运行一段时间后 |
---|
高地址 |
已使用部分 |
->任务切出时栈指针 |
未使用部分 |
低地址 |
对应实际中断后的栈如下:
vPortSVCHandler函数模拟中断返回
__asm void vPortSVCHandler( void )
{
PRESERVE8
/* Get the location of the current TCB. */
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
/* Pop the core registers. */
ldmia r0!, {r4-r11, r14}
msr psp, r0
isb
mov r0, #0
msr basepri, r0
bx r14
}
其中
ldr r3, =pxCurrentTCB
ldr r1, [r3]
ldr r0, [r1]
是获取栈指针r0即指向任务栈表中R4位置
ldmia r0!, {r4-r11, r14}是恢复R4-R11和portINITIAL_EXC_RETURN
msr psp, r0,更新栈指针,指向指向任务栈表中R0位置
bx r14模拟中断返回 恢复R0-R3 R12 PC xPSR(硬件实现)。
由于R14=portINITIAL_EXC_RETURN=0xfffffffd
根据手册描述
返回时使用PSP栈,返回后使用PSP栈。与初始化对应。
栈初始化时LR = prvTaskExitError 进入子函数时LR会入栈,退出子函数时LR出栈。
所以如果任务不是while(1)形式而是在最后return则最终会进入
prvTaskExitError执行。一般rtos的任务都是while(1)结构 不return。
复位后使用MSP,任务根据返回时的LR值portINITIAL_EXC_RETURN使用PSP见“2.任务切换”。
中断中固定使用MSP。
中断函数和mian使用中断向量第一个字指向的栈区域。
任务使用任务栈。
在os启动前默认时使用msp,根据中断向量的第一个字加载msp
硬件实现,或者bootloader跳转到应用时配置。
启动os时prvStartFirstTask,又重新将中断向量第一个字加载到msp。
今后中断就使用msp对应的栈,即os启动前main使用的栈。
因为main一去不复返,所以这里覆盖使用main时的栈,这样可以节约内存。
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
msr msp, r0
栈初始化时全部初始化为0xA5,运行一段时间后栈顶部分使用变为其他值。
检查栈底有多少连续的0xA5即可知道栈剩余多少。
Freertos提供接口函数uxTaskGetSystemState获取栈信息。
Shell中输入ps查看(具体代码未贴出)。
根据4.和5.的分析,中断和main函数栈使用中断向量第一个字对应的栈区域。
由于__main.c会将栈内容清除为0.所以在启动第一个任务前将栈重新填充为0xa5。
有__main.c之前将栈填充为0xa5又会被清除为0,将填充代码放在了任务启动前prvStartFirstTask函数中。这样main函数到prvStartFirstTask之前的栈使用大小不可监控。
只能监控后续中断使用的栈大小。如果要检测main函数栈使用则要将填充代码放在main函数执行的第一条代码后,需要嵌入汇编影响代码阅读和可移植性,所以不按这种方式。
实际上main函数栈溢出也没关系 ,但是编程必须要求提供手动初始化变量的代码,而不是依赖于编译器的初始化。
比如有一个变量static int i =0;
编译器提供代码在__main中会对该变量初始化,如果main函数栈溢出覆盖了这个变量的值。
那么在任务函数执行时提供 void mode_init(void)函数
手动再次初始化该变量i=0.
就可以避免问题。
建议在模块任务启动时对属于模块的全局变量再次提供”构造函数”手动初始化。
修改freertos底层移植代码
__asm void prvStartFirstTask( void )
{
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
ldr r0, =0xE000ED08
ldr r0, [r0]
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
msr msp, r0
//;初始化栈为0xA5A5A5A5
MOV R2,#0xA5A5A5A5
LDR R0, =0x4000
MRS R1, MSP
SUBS R1,R1,#4
LOOP STR R2,[R1,#0x00]
SUBS R0,R0,#4
SUBS R1,R1,#4
CMP R0,#0x00
BNE LOOP
增加检测代码
其中0x4000需要根据实际设置的栈大小修改。0xE000ED08为中断向量表地址。
/*****************************************************************************
* fn uint32_t bsp_sys_getstack(void)
* brief 获取栈大小.
* note .
* return 剩余栈字节数
*****************************************************************************
*/
uint32_t bsp_sys_getstack(void)
{
uint32_t size = 0;
uint32_t* p = (uint32_t*)(*(uint32_t*)(*(uint32_t*)0xE000ED08) - 0x4000);
while(*p == (uint32_t)0xA5A5A5A5)
{
size += 4;
p++;
}
return size;
}
Shell中输入stack命令查看(具体代码未贴出)
简单来说软件实现栈检测,就是将栈初始化为固定值。如果栈有使用则初始化值会变化,软件从栈底开始查找看剩余多少内容没有被改写就是剩余多少栈未使用。软件检测不是可靠的,因为溢出可能是跳跃的,即栈底一部分实际未用指针直接跳到了更后面的溢出位置,软件检测还存在延迟,所以软件检测一般可用于评估栈使用大小。使用硬件MPU更可靠,设置只有本任务只能访问本任务栈对应的空间,一旦访问其他空间就可以触发MPU中断这样更及时可靠检测。
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !