硬件环境: AC7811通用开发板 ATC-LINK
软件环境:keil 5.23
前段时间有客户提到AC7811怎么实现栈溢出检测,便想到使用MPU模块在栈顶设置一段禁用区,以在栈使用溢出时访问到禁用区,产生memory manage fault,从而进行下一步的处理。再便想到可以通过该功能来实现带操作系统的用户栈溢出检测功能。
MPU的配置:
MPU可以依据实际的使用情况灵活配置,我这里为了简便,便创建了8个任务,每个任务都使用512字节的栈空间,这样便于我配置MPU。
MPU的配置如下:
- if ((MPU->TYPE & 0x800) == 0) ///<do not support MPU
- {
- return;
- }
-
- MPU->CTRL = 0x04; ///<enable backround region, disable MPU
-
- MPU->RNR = 0; ///<select region 0
- MPU->RBAR = 0x2000E000;
- MPU->RASR = 0x07000017;
-
- MPU->RNR = 1; ///<select region 1
- MPU->RBAR = 0x2000E000;
- MPU->RASR = 0x03000017;
-
- MPU->CTRL |= 1;
-
-
- SCB->SHCSR |= 1 << 16; ///<enable memory manage interrupt
通过sct文件将8个任务的栈空间都存放再0x2000E000开始的位置,这里配置了两个region,启用了背景region。region0 用于配置该区域为只读。region1配置该区域可读可写,两个region重叠,因此该区域最终是可读可写的。
任务切换:
- void App_OS_TaskSwHook (void)
- {
- if (OSTCBHighRdyPtr->ExtPtr != 0)
- {
- MPU->RNR = 1; ///<select region 0
- MPU->RBAR = 0x2000E000;
- MPU->RASR = *((uint32_t *)OSTCBHighRdyPtr->ExtPtr);
-
- }
- }
进行任务切换时,在任务切换hook中,重新设置region1,主要为了修改子region。按内核手册的说法,一个region分为8个子region,可以通过SRD寄存器除能响应的子region。因此我们每要切换一个任务时,应该除能除此任务外的其他任务栈的写入权限。这里的配置参数我们通过OS提供的OSTCBHighRdyPtr->ExtPtr参数传入。
- static const uint32_t AppTask1Ext = 0x0300FD17;
- static const uint32_t AppTask2Ext = 0x0300FB17;
- static const uint32_t AppTask3Ext = 0x0300F717;
- static const uint32_t AppTask4Ext = 0x0300EF17;
- static const uint32_t AppTask5Ext = 0x0300DF17;
- static const uint32_t AppTask6Ext = 0x0300BF17;
- static const uint32_t AppTask7Ext = 0x03007F17;
该参数在创建任务的时候传入,每个任务只使能自己的子region,除能其他任务的子region。这样其他子region无效,对应的其他栈便适用region0的规则,变成了只读(这里设置只读是因为OS的STAT任务有对各任务栈使用情况进行统计,如果直接禁止访问会导致该任务无法运行)。
对于操作系统自带的任务,我们不纳入管理范围,通过OSTCBHighRdyPtr->ExtPtr的非零判断,可以排除操作系统自己的任务。
最后,在OS进行任务切换时,会同时使用到当前任务栈和就绪的最高优先级任务栈,为了解决此时同时使用两个栈的问题,我在这里暂时关闭了MPU:
- OS_CPU_PendSVHandler
- CPSID I ; Prevent interruption during context switch
- ;add by jason for mpu
- LDR R1, =0xE000ED94
- LDR R0, =0x00000004
- STR R0, [R1]
- ;end
- MRS R0, PSP ; PSP is process stack pointer
- CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time
- SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
- STM R0, {R4-R11}
- LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;
- LDR R1, [R1]
- STR R0, [R1] ; R0 is SP of process being switched out
- ; At this point, entire context of process has been saved
- OS_CPU_PendSVHandler_nosave
- PUSH {R14} ; Save LR exc_return value
- LDR R0, =OSTaskSwHook ; OSTaskSwHook();
- BLX R0
- POP {R14}
- LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy;
- LDR R1, =OSPrioHighRdy
- LDRB R2, [R1]
- STRB R2, [R0]
- LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;
- LDR R1, =OSTCBHighRdyPtr
- LDR R2, [R1]
- STR R2, [R0]
- LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
- LDM R0, {R4-R11} ; Restore r4-11 from new process stack
- ADDS R0, R0, #0x20
- MSR PSP, R0 ; Load PSP with new process SP
- ORR LR, LR, #0x04 ; Ensure exception return uses process stack
- CPSIE I
- ;add by jason for mpu
- LDR R1, =0xE000ED94
- LDR R0, =0x00000005
- STR R0, [R1]
- ;end
- BX LR ; Exception return will restore remaining context
- END
最后就是核心的异常处理了:
- void MemManage_Handler(void)
- {
- OS_ERR err;
- printf("%s is overflow\r\n", OSTCBCurPtr->NamePtr);
- OS_RdyListRemove(OSTCBCurPtr);
- OSTaskDel(OSTCBCurPtr, &err);
- OSSched();
- //while(1);
- }
这里我在发生栈溢出后,移除并删除了当前任务,同时进行新的任务调度(这里对UCOS III的运行还不熟悉,所以这里使用上比较粗暴直接了,不确定是否会有其他问题)。
测试:
测试的方法也很简单,我在task2中申请了一个大于512字节的局部数组,然后对超过512字节后面的位置进行写入操作。此时的访问应当会落在其他任务的栈空间上,因为其他任务栈空间已经配置只读,所以会产生memmanage fault。
- static void AppTask2 (void *p_arg)
- {
- OS_ERR err;
- uint32_t testdata[130];
- p_arg = p_arg;
- while(1)
- {
- printf("Task2\r\n");
- testdata[130] = testdata[130];
- OSTimeDlyHMSM(0, 0, 1, 0,
- OS_OPT_TIME_HMSM_STRICT,
- &err);
- }
- }
测试log输出如下:
似乎不需要额外的执行testdata[130] = testdata[130];因为printf打印函数运行的时候已经栈溢出了,所以printf函数一旦访问栈空间,就会产生memmanage fault。接着我们删除了task2.因此其他任务还可以照常执行。
- Creating Application Tasks...
- Creating Application Events...
- Task1
- App Task2 is overflow
- Task3
- Task4
- Task5
- Task6
- Task7
- Task1
- Task3
- Task4
- Task5
- Task6
- Task7
总结:这功能想了想感觉实际用途不是很大,但在设计之初,我们总会发生一些意外的栈溢出问题,在OS下会很难定位到具体是哪个任务哪个函数导致了栈溢出。这个方法或许会比较方便大家定位栈溢出问题?当然,这里面还有很多不成熟的地方,比如第一个任务发生溢出的话,溢出的位置小于0x2000E000,不在只读范围内,因此可能还需要在这个区域额外设置一个只读的临界区。
最后附上测试用的代码:
UCOSIII_sample.rar
(2.37 MB, 下载次数: 3)
|