1. 简介
本文简要介绍了PowerPC体系结构下vxWorks操作系统的异常处理机制,指出当前机制存在的缺陷,即:中断级的异常处理在任务上下文中执行,异常处理堆栈基址依赖于当前任务的栈帧指针,如果传入堆帧异常会导致访问非法地址甚至系统死机。笔者结合当前的工作对异常处理的堆栈空间进行了优化,类似中断处理,将异常处理放在独立的堆栈空间,增强了系统的健壮性。
主要章节如下:
2 PowerPC体系结构的异常机制
3 vxWorks的异常处理流程
4 异常切换的设计与实现
如果读者对异常机制比较熟悉,可以直接跳过2、3章节。如果有问题可与笔者联系,新浪微博账号:@白银之手骑士团
2. PowerPC体系结构的异常机制
PowerPC体系结构中所有的异常可以分为由指令导致的异常以及由系统导致的异常。由系统导致的异常包括系统复位异常,机器检查异常,外部中断,DEC计数器异常。PowerPC异常分类如下:

在PowerPC的异常机制中,作为对外部信号,错误,或者指令执行过程中遇到的非法状态的反应,处理器将进入supervisor模式。当发生异常的时候,此时处理器状态的信息被保存到特定的寄存器中,然后CPU会跳转到与所发生的异常相对应的内存地址处(异常向量)去执行。
当处理器判定异常可以被获取,并对其进行处理的时候,处理器进行如下的操作:
(1)将当前的PC指针保存到SRR0寄存器中;
(2)将当前的MSR寄存器的值保存到SRR1中;
(3)设置MSR的值,新的MSR值起作用的时间开始于从异常向量地址处取第一条指令;
(4)利用新的MSR值,从异常对应的异常向量处取指令和执行指令。注意异常向量在内存中的位置依赖于MSR的IP位。
下表列出了当发生了对应的异常后,MSR的值将被处理器如何设置,注意其中的几个位:EE,IR和DR位。这几位在发生异常后都将被处理器设置为0,也就是说处理器在获取异常之后禁止外部中断和DEC中断,并关闭MMU功能。

3. vxWorks的异常处理流程
我们知道,总的来说PowerPC的异常处理机制就是在发生异常时将当时关键的处理器状态(PC和MSR)保存到暂存寄存器SRR0和SRR1中,然后跳转到与所发生异常对应的异常向量地址处执行。
而vxWorks要做的工作,就是要在系统初始化的时候正确地安装好异常处理入口程序,异常处理程序以及异常退出程序,以便在发生异常时能够进一步将当前处理器的状态(LR寄存器,XER寄存器,通用寄存器等)保存到内存中,并调用恰当的函数来处理异常,在异常处理之后恢复被异常打断的任务,使之继续执行。
vxWorks的异常处理代码主要集中在文件excALib.s,excArchLib.c,intALib.s和intArchLib.c中,这几个文件位于vxWorks源代码的\target\src\arch\ppc目录下。
3.1 异常处理的数据结构
异常向量表是vxWorks异常处理机制中的一个重要的数据结构,异常向量表的每个表项是一个类型为EXC_TBL的对象。该类型定义如下:
UINT32 vecOff; /* vector offset*/ STATUS (*excCnctRtn) (); /* routine to connect the exception handler*/ void (*excHandler) (); /* exceptionhandler routine */ UINT32 vecOffReloc; /* vector offset relocation address */
异常向量表的表项数量和内容随CPU不同而不同,对于PPC603和PPCEC603处理器而言,其异常向量表中的表项如下:
{_EXC_OFF_RESET, excConnect, excExcHandle, 0}, /* system reset */ {_EXC_OFF_MACH, excConnect, excExcHandle, 0}, /* machine chk */ {_EXC_OFF_DATA, excConnect, excExcHandle, 0}, /* data access */ {_EXC_OFF_INST, excConnect, excExcHandle, 0}, /* instr access */ {_EXC_OFF_INTR, excIntConnect, excIntHandle, 0}, /* ext int */ {_EXC_OFF_ALIGN, excConnect, excExcHandle, 0}, /* alignment */ {_EXC_OFF_PROG, excConnect, excExcHandle, 0}, /* program */ {_EXC_OFF_FPU, excConnect, excExcHandle, 0}, /* fp unavail */ {_EXC_OFF_DECR, excIntConnect, excIntHandle, 0}, /* decrementer */ {_EXC_OFF_SYSCALL, excConnect, excExcHandle, 0}, /* system call */ {_EXC_OFF_TRACE, excConnect, excExcHandle, 0}, /* trace excepti */ {_EXC_OFF_INST_MISS,excConnect, excExcHandle, 0}, /* i-trsl miss */ {_EXC_OFF_LOAD_MISS,excConnect, excExcHandle, 0}, /*d-trsl miss */ {_EXC_OFF_STORE_MISS,excConnect, excExcHandle, 0}, /* d-trslmiss */ {_EXC_OFF_INST_BRK, excConnect, excExcHandle, 0}, /* instr BP */ {_EXC_OFF_SYS_MNG, excConnect, excExcHandle, 0}, /* sys mgt*/ {0, (STATUS (*)()) NULL, (void (*) ()) NULL, 0}, /* end of table */
3.2 异常处理流程
往对应的异常向量地址处安装对应的异常处理程序由函数excVecInit实现,excVecInit在函数usrInit中被调用。该函数的主要操作如下:
for (ix = 0;excBlTbl[ix].excHandler != (void (*)()) NULL; ix++) excBlTbl[ix].excCnctRtn((VOIDFUNCPTR *) excBlTbl[ix].vecOff, (VOIDFUNCPTR)excBlTbl[ix].excHandler,(VOIDFUNCPTR*)excBlTbl[ix].vecOffReloc);
也就是说遍历整个异常向量表,对每种类型的异常,用对应的连接程序连接对应的异常处理函数。连接函数有两个,一个是excIntConnect,对应于外部中断异常和DEC异常。另一个是excConnect,对应于其它类型异常。在函数excVecInit的最后,调用函数excVecBaseSet设置异常向量的基地址,异常向量的基地址有两个可能的值,一个是0x0000_0000,另一个是0xFFF0_0000,设置完后设置MSR寄存器的IP位,使之保持一致。设置完后使能MACHINE CHECK异常。
下面简要的分析一下异常的连接函数,首先考察excConnect,其调用关系如下:
excConnect(VOIDFUNCPTR * vector, VOIDFUNCPTR routine) --> excRelocConnect(vector, routine, (VOIDFUNCPTR *) 0) --> ExcConnectVector(newVector, FALSE, excEnt, routine, excExit) 再考察excIntConnect,其调用关系如下:
excIntConnect(VOIDFUNCPTR * vector, VOIDFUNCPTR routine) --> excRelocIntConnect (vector, routine, (VOIDFUNCPTR *) 0) --> excConnectVector(newVector, FALSE, intEnt, routine, intExit) 注意两个函数最后调用excConnectVector函数时参数的差别。
接下来分析函数excConnectVector,该函数的原型为:
INSTR *newVector, /* calculated exc vector */ BOOL isCrt, /* is critical exc? */ VOIDFUNCPTR entry, /*handler entry */ VOIDFUNCPTR routine, /*routine to be called */ VOIDFUNCPTR exit /*handler exit */
注意excConnectVector函数的如下3条语句
entBranch = blCompute (entry, &newVector[entOff]); routineBranch = blCompute (routine,&newVector[isrOff]); exitBranch = blCompute (exit, &newVector[exitOff]);
blCompute函数生成一条bl指令,该bl指令本身的地址为blCompute函数的第2个参数,分支的目标地址为第1个参数。此外注意excConnectVector函数的如下语句序列:
bcopy ((char *) excConnectCode, (char *)newVector, vecSize); newVector[entOff] = entBranch; newVector[isrOff] = routineBranch; newVector[exitOff] = exitBranch;
这几条语句实际的操作就是在异常向量对应的内存地址处写上如下的指令:
0x7c7343a6, /* 0 0x00 mtspr SPRG3, p0 */ 0x7c6802a6, /* 1 0x04 mflr p0 */ 0x48000001, /* 2(6) 0x08/18 bl xxxEnt */ 0x38610000, /* 3 0x0c addi r3, sp, 0 */ 0x9421fff0, /* 4 0x10 stwu sp, -FRAMEBASESZ(sp) */ 0x48000001, /* 5(9) 0x14/24 bl xxxHandler */ 0x38210010, /* 6 0x18 addi sp, sp, FRAMEBASESZ */ 0x48000001 /* 7(11) 0x1c/2c bl xxxExit */
对于每种类型的异常,其对应的异常向量处的指令模式都如上所示,所不同的是每种异常对应的异常处理入口函数、实际的异常处理函数以及异常处理的退出函数。例如对于外部中断以及DEC异常,对应的入口函数为intEnt,epic_int_handler(以COREF2板MPC8241的BSP为例),intExit,对于其它类型的异常,对应的函数分别为excEnt,excExcHandle和excExit。
在异常向量处安装好上面所示的指令之后,当处理器发生异常并跳转到异常向量后就依次执行相应的异常入口函数,异常处理函数以及异常退出函数了。下面将重点考察一下异常入口函数以及异常退出函数excEnt,excExit以及intEnt以及intExit。
3.3 excEnt函数解析
mfspr p0,HID0 /* get HID0 value */ mtspr SPRG0,p0 /* save temporily HID0 */ ori p0,p0, _PPC_HID0_DLOCK /* set the DLOCKbit */ mtspr HID0,p0 /*LOCK the Data Cache */
简单的对上述指令进行说明,跳转到该函数执行之前,p0中存放的是处理器发生异常时LR寄存器的值。然后改写HID0寄存器的值,锁Data Cache。这里对锁Data Cache进行一下说明,之所以要锁Data Cache,是因为当异常发生后,处理器会将MMU功能关闭。这时Cache就不再由MMU的WIMG位来控制了。如果此时Data Cache是打开的,那么下面语句要保存到内存栈中的SP,LR等寄存器的值将被写到Cache中,而不是写到内存中。而异常处理程序是在MMU打开的情况下执行的,此时Cache由MMU的WIMG位来控制,如果保存异常时寄存器信息的栈是非Cacheable的,在excExit中有恢复LR,SP和MSR寄存器的操作,这时会从内存中读出值写到这些寄存器中,但是真实的值此时还保存在Cache中。为了解决这个问题,所以锁Cache。
stwu sp, -_PPC_ESF_STK_SIZE(sp) /* update SP */ stw p0, _PPC_ESF_LR(sp) /* save LR */ mfspr p0,SPRG3 /* load saved P0 */ stw p0,_PPC_ESF_P0(sp) /* save generalregister P0 */ mfspr p0,SRR0 /* load saved PC to P0 */ stw p0,_PPC_ESF_PC(sp) /* save PC */ mfspr p0,SRR1 /* load saved MSR to P0 */ stw p0,_PPC_ESF_MSR(sp) /* save MSR */ stw p1,_PPC_ESF_P1(sp) /* save generalregister P1 */ mfcr p1 /* load CR to P1 */ stw p1,_PPC_ESF_CR(sp) /* save CR */
简单的对上述指令进行说明,直接在当前的栈上开辟出一块空间,将处理器发生异常时LR寄存器的值,PC指针,MSR寄存器的值,P1寄存器的值,CR寄存器的值保存到栈中对应的位置。
mfspr p1,SPRG0 /* load temporily saved HID0*/ mtspr HID0,p1 /* UNLOCK the Data Cache */ andi. p0,p0, _MSR_RI | _MSR_FP | _MSR_IR | _MSR_DR | _MSR_IS | _MSR_DS | _PPC_MSR_EE mtmsr p1 /* ENABLE INTERRUPT */
简单的对上述指令进行说明,上述指令先恢复HID0寄存器的值,然后将当前的MSR寄存器的值读出来,值得注意的是CPU在发生异常后,会改变MSR的值,并且新的MSR的值在CPU跳转到对应的异常向量后起作用,而发生异常时MSR的值被保留到寄存器SRR1中。以上这几条指令的作用就是将MSR的值恢复。例如,如果在发生异常时,CPU是允许外部中断的,那么在此处之后,CPU就允许外部中断发生了。
mfspr p0, LR /* save exceptionnumber to P0*/ lis p1, HIADJ(excExtendedVectors) lwz p1, LO(excExtendedVectors)(p1) /*get excExtendedVectors */ cmpwi p1,0 /* if 0, short vectors */ li p1,20 /* 4 * (EXT_ISR_OFF - (ENT_OFF +1)) */ addi p1,p1, 12 /* 4 * (ENT_OFF + 1) */ stw p0, _PPC_ESF_VEC_OFF(sp) /* store to ESF */ mfspr p0,CTR /*load CTR to P0 */ stw p0, _PPC_ESF_CTR(sp) /* save CTR */ mfspr p1,XER /* load XER to P0 */ stw p1,_PPC_ESF_XER(sp) /* save XER */
以上几条命令是分别将本次异常对应的异常向量(eg.0x300),CTR寄存器,XER寄存器的值保存到栈中对应的位置。
stw p1,_PPC_ESF_DAR(sp) /* save DAR */ stw p1, _PPC_ESF_DSISR(sp) /* save DSISR */ stw r0, _PPC_ESF_R0(sp) /* save general register 0 */ addi r0,r1, _PPC_ESF_STK_SIZE stw r0, _PPC_ESF_R1(sp) /* save exception sp */ stw r2,_PPC_ESF_R2(sp) /* save generalregister 2 */
以上几条指令将DAR,DSISR寄存器的值,R0寄存器的值,发生异常时栈指针寄存器SP的值,通用寄存器R2的值保存到栈中对应的位置。
stw p2,_PPC_ESF_P2(sp) /* save generalregister 5 */ stw p3,_PPC_ESF_P3(sp) /* save generalregister 6 */ stw t16,_PPC_ESF_T16(sp) /* save generalregister 30 */ stw t17,_PPC_ESF_T17(sp) /* save generalregister 31 */
以上的指令保存通用寄存器的值。
3.4 excExit函数解析
excExit函数主要的操作就是将栈中保存的异常时的信息恢复到寄存器中。
lwz r0,_PPC_ESF_R0(sp) /* restore generalregister 0 */ lwz r2,_PPC_ESF_R2(sp) /* restore generalregister 2 */ lwz t16,_PPC_ESF_T16(sp) /* restore generalreg 30 */ lwz t17,_PPC_ESF_T17(sp) /* restore generalreg 31 */
上述指令将栈中保存的通用寄存器值恢复到栈中。
lwz p0,_PPC_ESF_CTR(sp) /* load savedCTR to P0*/ mtspr CTR,p0 /* restore CTR */ lwz p0,_PPC_ESF_XER(sp) /* load saved XER toP0 */ mtspr XER,p0 /* restore XER */ lwz p0,_PPC_ESF_LR(sp) /* load saved LR toP0 */ mtspr LR,p0 /* restore LR */ lwz p0,_PPC_ESF_CR(sp) /* load the saved CRto P0 */ mtcrf 255,p0 /*restore CR */
上述指令将栈中保存的一些特殊寄存器的值恢复到特殊寄存器中。
RI_MASK(p0, p0 ) /* mask RI bit */ INT_MASK(p0,p0) /*clear EE bit in msr */ mtmsr p0 /* DISABLE INTERRUPT */ lwz p0,_PPC_ESF_PC(sp) /* load the saved PCto P0 */ mtspr SRR0,p0 /* and restore SRR0 (PC) */ lwz p0,_PPC_ESF_MSR(sp) /* load the saved MSRto P0 */ mtspr SRR1,p0 /* and restore SRR1 (MSR) */ lwz p0,_PPC_ESF_P0(sp) /* restore p0 */ lwz sp,_PPC_ESF_SP(sp) /* restore the stackpointer */ #endif /*_WRS_TLB_MISS_CLASS_SW */ rfi /*return to context of the */
上述的这段指令先将当前的MSR寄存器的值读出来,修改EE位,禁止外部中断,写回MSR中。然后将栈中保存的发生异常时的PC值和MSR值写到SRR1和SRR0中,最后利用rfi指令将SRR1和SRR0中的值恢复到MSR寄存器和PC中。
4. 异常栈的切换
4.1 切换异常栈概念及原因
细心的读者可以发现,在excConnectVector函数生成的语句序列里,要顺序调用三个函数完成异常的最终处理:excEnt() --> excHandler() --> excExit(),其中excEnt和excExit两个函数我们解析过了,在excALib.s里对异常现场的寄存器的保存和恢复。excHandler在中断级别对异常的处理,包括将异常信息拷贝到当前任务的异常空间,执行异常处理钩子等,他的参数就是excEnt保存在堆栈的寄存器信息,代码如下:
ESFPPC * pEsf /* pointer to exception stack frame */ taskIdCurrent->pExcRegSet = pRegs; /* for taskRegs[GS]et */ taskIdDefault ((int)taskIdCurrent); /* update default tid */ bcopy ((char *) &excInfo, (char *)&(taskIdCurrent->excInfo), sizeof (EXC_INFO)); /*copy in exc info */ if (_func_sigExcKill != NULL) _func_sigExcKill((int) vecNum, vecNum, pRegs); if (_func_excInfoShow != NULL) /* default show rtn? */ (*_func_excInfoShow) (&excInfo, TRUE); if (excExcepHook != NULL) (*excExcepHook) (taskIdCurrent,vecNum, pEsf); /* 异常处理钩子 */ taskSuspend (0); /* whoa partner... */ taskIdCurrent->pExcRegSet = (REG_SET *)NULL;
可以看出,异常的处理完全是在当前的任务的堆栈空间执行,没有像中断处理那样独立的堆栈,这样做有下面几个缺陷:
(1)任务的二次异常。异常处理钩子在出现异常的任务空间执行,钩子函数是用户自定义实现的处理函数,不可避免的也会出现数据访问或程序等异常,这样就产生了二次异常,新的异常产生后,会再次出发异常处理机制,这样循环往复,不仅导致任务空间越界(vxWorks系统是个扁平的单一空间内存结构),最初异常的信息也很难查找到。
(2) 内存空间的破坏甚至系统当机。如果异常发生时,sp的值被破坏成无效的值,比如0xffffff,PowerPC系统向低地址空间压栈,异常处理程序会直接使用这个无效的sp值,此时的堆栈空间已经不在是任务堆栈空间,继续执行异常钩子只会破坏其他内存,如果这块内存是中断向量表,覆盖了中断向量空间,系统就会死机。
(3) 任务无法恢复。有些异常是可以恢复的,比如人为覆盖了TCB数据结构里保存的寄存器内容,如果把任务控制块里各个寄存器的值恢复成覆盖前的值,任务应该可以恢复。当然像数据访问异常,对齐异常这种不可逆的无法恢复。在现有的处理机制上,如果sp的值覆盖成非法值,可能会造成整个系统的崩溃,切换到独立的堆栈空间,完全 可以恢复这个任务的执行。
4.2 切换异常栈的实现
了解了原理,实现起来就非常轻松。为了可靠,我们将异常任务栈切换到数据段的数组空间。这样的好处是,数组空间的地址在编译时就已经确定,不会像动态申请的内存空间一样,不可靠且地址的值容易被改写。首先我们在代码某处定义一个全局数组:
#define VX_EXC_STACK_SIZE (16*1024) charvxExcStack[VX_EXC_STACK_SIZE];
修改excALib.s文件内的excEnt函数的两条汇编语句。
修改点1修改前:
stwu sp,- _PPC_ESF_STK_SIZE(sp) /* update SP */ 修改点1修改后:
mtspr SPRG2,p0 /* SPRG2 = LR */ lis p0, HIADJ(vxExcStack) /* load exception. stack addr to p0. */ ori p0, p0, LO(vxExcStack) +VX_EXC_STACK_SIZE stwu sp,- _PPC_ESF_STK_SIZE(p0) /* allocate ESF inexcepion.stack */ mr sp, p0 /* sp is needed save */
上述改动是将sp从任务堆栈空间,切换到我们开辟的代码段数组空间。首先将原有的p0(r3)寄存器的值先保存寄存器堆栈空间内,然后把vxExcStack地址的高16位放入p0并向左移16位,再将vxExcStack地址的低16位加上数组空间长度的值放入p0(由于PowerPC堆栈是从高地址向低地址增长的),这样就得到了新的独立堆栈空间sp基地址。然后把这个新的sp值赋予sp寄存器,并恢复p0原有的值。 修改点2修改前:
addi r0, r1,_PPC_ESF_STK_SIZE 修改点2修改后:
lwz r0, 0(r1) 这个修改就是保存原有LR的值。
4.3 切换异常栈的验证
我们手工制造异常,先将任务挂起,然后将TCB(任务控制块)里的的寄存器上下文保存到一个局部的数组空间内(这样做是便于将来恢复)然后覆盖TCB里的寄存器上下文,特别是堆栈指针修改为0xffffff00,恢复任务的执行,这样任务从被覆盖的寄存器上下文执行,会产生异常,触发异常处理机制。我们还提供了一个恢复的测试函数,将原有保存的寄存器上下文拷贝回任务寄存器空间。
异常堆栈切换前的处理会直接在原来0xffffff00地址基础上建立堆栈空间,会访问到非法地址而导致系统挂起甚至当机;异常堆栈切换后,原来的堆栈指针保留,在新的全局数组上展开异常处理,不会导致越界等问题,如果原来的寄存器上下文拷贝过来,任务还可以恢复。
我们的异常堆栈切换验证代码如下。
char stackBuffer[STACKBUFSIZE]; STATUS myExceptLaunch() { /* 启动任务用于异常测试 */ sprintf(excName, "tExcept%02d", excCnt++); silverTaskId = taskSpawn(excName, 160, 0, 8192, (FUNCPTR)silverEntry, 0,0,0,0,0,0,0,0,0,0); if (ERROR == silverTaskId) { printf("Except create task failed!\n"); void silverHandy(int param) { /* 任务处理的主体函数 */ printf("i step back %d!\n", i); static void silverEntry() { /* 任务处理入口函数 */ taskDelay(sysClkRateGet()*5); printf("silverLooper %d!\n", silverLooper++); silverHandy(silverLooper); STATUS myExceptMake() { /* 保存后覆盖任务异常寄存器上下文,制造异常 */ taskSuspend(silverTaskId); pSiverRegs = (void *)(silverTaskId + 0x130); /* sp reg offset in windTcb */ pSilverSp = (int *)(pSiverRegs + 4); memset(stackBuffer, 0, STACKBUFSIZE); bcopy(pSiverRegs, (void *)stackBuffer, STACKBUFSIZE); memset(pSiverRegs+8, 0, STACKBUFSIZE/2-8); taskResume(silverTaskId); /* then except will occur */ STATUS myExceptRecover() { /* 拷贝回原来保存的寄存器上下文,恢复异常 */ bcopy((void *)stackBuffer, pSiverRegs, STACKBUFSIZE/2); taskResume(silverTaskId);
在串口调试环境下,先执行函数:myExceptLaunch 启动任务,然后执行:myExceptMake制造异常,再调myExceptRecover恢复。
可以看到,异常堆栈切换前,执行myExeptMake函数制造异常后,系统挂死,串口不在有响应,因为越界覆盖了中断向量表的内容;而异常切换后,异常产生后,调用myExceptRecover成功恢复了任务上下文,任务继续执行。
5. 参考文献
1. 《The Programming Environments for 32-Bit Microprocessors》
2. vxWorks源代码
3. 《PPC的堆栈回溯》(文档)
|