硬件环境: 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)
|