写点什么

鸿蒙轻内核 M 核的故障管家:Fault 异常处理

发布于: 刚刚

​​摘要:本文先简单介绍下 Fault 异常类型,向量表及其代码,异常处理 C 语言程序,然后详细分析下异常处理汇编函数实现代码。

 

本文分享自华为云社区《鸿蒙轻内核M核源码分析系列十八 Fault异常处理》,作者:zhushy。

 

Fault 异常处理模块与 OpenHarmonyLiteOS-M 内核芯片架构相关,提供对 HardFault、MemManage、BusFault、UsageFault 等各种故障异常处理。有关 Cortex-M 芯片相关的知识不在本文讨论,请自行参考《Cortex™-M7 Devices Generic User Guide》等官方资料。本文先简单介绍下 Fault 异常类型,向量表及其代码,异常处理 C 语言程序,然后详细分析下异常处理汇编函数实现代码。文中所涉及的源码,以 OpenHarmonyLiteOS-M 内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_m 获取。

1、Fault Type 异常类型


如下图中的 Fault 类型表格所示,Fault 表示各种故障,Handler 表示故障处理机制,Bit Name 标记故障的寄存器的 Bit 位,Fault status register 故障状态寄存器。该图摘自《Cortex™-M7 Devices Generic User Guide》。



2、Vector table 向量表


向量表包含栈指针的复位值和开始地址,也叫异常向量。异常可以看作特殊的中断,异常编号 Exception number, 中断请求号 IRQ number,偏移值 offset,向量 Vector 的对应关系如下图所示,本文主要关注 NMI、HardFault、Memorymanagement fault、Bus fault、Usagefault、SVCall 等异常。



​在中断初始化时,会初始化该异常向量表,代码位置 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c。⑴处的 HalExcNMI,⑵处的 HalExcHardFault,⑶处的 HalExcMemFault,⑷处的 HalExcBusFault,⑸处的 HalExcUsageFault,⑹处的 HalExcSvcCall 这些中断异常处理函数定义在 kernel\arch\arm\cortex-m7\gcc\los_exc.S。本文我们主要分析这些汇编函数的代码。


⑺处开始的这两行代码也比较重要,通过更改系统处理控制与状态寄存器(System Handler Control and State Register)的 bit 位来使能相应的异常,通过更改配置与控制寄存器(Configuration and Control Register)的 bit 位来使能除零异常。


LITE_OS_SEC_TEXT_INIT VOID HalHwiInit(VOID){#if (LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT == 1)    UINT32 index;    g_hwiForm[0] = 0;             /* [0] Top of Stack */    g_hwiForm[1] = Reset_Handler; /* [1] reset */    for (index = 2; index < OS_VECTOR_CNT; index++) { /* 2: The starting position of the interrupt */        g_hwiForm[index] = (HWI_PROC_FUNC)HalHwiDefaultHandler;    }    /* Exception handler register */⑴  g_hwiForm[NonMaskableInt_IRQn + OS_SYS_VECTOR_CNT]   = HalExcNMI;⑵  g_hwiForm[HARDFAULT_IRQN + OS_SYS_VECTOR_CNT]        = HalExcHardFault;⑶  g_hwiForm[MemoryManagement_IRQn + OS_SYS_VECTOR_CNT] = HalExcMemFault;⑷  g_hwiForm[BusFault_IRQn + OS_SYS_VECTOR_CNT]         = HalExcBusFault;⑸  g_hwiForm[UsageFault_IRQn + OS_SYS_VECTOR_CNT]       = HalExcUsageFault;⑹  g_hwiForm[SVCall_IRQn + OS_SYS_VECTOR_CNT]           = HalExcSvcCall;    g_hwiForm[PendSV_IRQn + OS_SYS_VECTOR_CNT]           = HalPendSV;    g_hwiForm[SysTick_IRQn + OS_SYS_VECTOR_CNT]          = SysTick_Handler;
/* Interrupt vector table location */ SCB->VTOR = (UINT32)(UINTPTR)g_hwiForm;#endif#if (__CORTEX_M >= 0x03U) /* only for Cortex-M3 and above */ NVIC_SetPriorityGrouping(OS_NVIC_AIRCR_PRIGROUP);#endif
/* Enable USGFAULT, BUSFAULT, MEMFAULT */⑺ *(volatile UINT32 *)OS_NVIC_SHCSR |= (USGFAULT | BUSFAULT | MEMFAULT); /* Enable DIV 0 and unaligned exception */ *(volatile UINT32 *)OS_NVIC_CCR |= DIV0FAULT;
return;}
复制代码

3、HalExcHandleEntry 异常处理 C 程序入口


HalExcHandleEntry 异常处理函数是汇编异常函数跳转到 C 语言程序的入口,定义在文件 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,被 kernel\arch\arm\cortex-m7\gcc\los_exc.S 文件中的汇编函数调用。函数参数由汇编程序中的 R0-R3 寄存器传值进来,汇编程序中的寄存器和 HalExcHandleEntry 函数参数对应关系如下表所示:



​下面我们分析下函数的源代码,⑴处的标签表示异常类型参数的高 16 位用于特色的标记,主要用于标记故障地址是否有效、是否故障发生在中断中,是否支持浮点等。⑵处增加中断计数和嵌套异常数目。⑶记录异常类型,⑷处如果记录了有效的故障地址,则获取故障地址。⑸处如果当前运行任务存在时,若标记了异常发生在中断,则记录中断号,并记录异常发生在中断内,否则记录任务编号,并记录异常发生在任务内。如果当前运行任务为空,则异常发生在初始化阶段。⑹处如果异常类型里包含支持浮点数的标记,则相应处理下。⑺处输出异常信息到控制台。


LITE_OS_SEC_TEXT_INIT VOID HalExcHandleEntry(UINT32 excType, UINT32 faultAddr, UINT32 pid, EXC_CONTEXT_S *excBufAddr){⑴  UINT16 tmpFlag = (excType >> 16) & OS_NULL_SHORT; /* 16: Get Exception Type */⑵  g_intCount++;    g_excInfo.nestCnt++;
⑶ g_excInfo.type = excType & OS_NULL_SHORT;
⑷ if (tmpFlag & OS_EXC_FLAG_FAULTADDR_VALID) { g_excInfo.faultAddr = faultAddr; } else { g_excInfo.faultAddr = OS_EXC_IMPRECISE_ACCESS_ADDR; }⑸ if (g_losTask.runTask != NULL) { if (tmpFlag & OS_EXC_FLAG_IN_HWI) { g_excInfo.phase = OS_EXC_IN_HWI; g_excInfo.thrdPid = pid; } else { g_excInfo.phase = OS_EXC_IN_TASK; g_excInfo.thrdPid = g_losTask.runTask->taskID; } } else { g_excInfo.phase = OS_EXC_IN_INIT; g_excInfo.thrdPid = OS_NULL_INT; }⑹ if (excType & OS_EXC_FLAG_NO_FLOAT) { g_excInfo.context = (EXC_CONTEXT_S *)((CHAR *)excBufAddr - LOS_OFF_SET_OF(EXC_CONTEXT_S, uwR4)); } else { g_excInfo.context = excBufAddr; }
⑺ OsDoExcHook(EXC_INTERRUPT); OsExcInfoDisplay(&g_excInfo); HalSysExit();}
复制代码


4、Los_Exc 异常处理汇编函数


上文介绍 Vectortable 向量表时,已经提到了在文件 kernel\arch\arm\cortex-m7\gcc\los_exc.S 中定义的的异常处理函数,如下。当发生 Fault 故障异常时,会调度执行这些异常处理函数,本节会详细分析函数的源代码来掌握内核如何处理这些发生的异常。这 6 个函数处理过程类似,我们选择 2 个典型的函数进行分析。


.global  HalExcNMI.global  HalExcHardFault.global  HalExcMemFault.global  HalExcBusFault.global  HalExcUsageFault.global  HalExcSvcCall
复制代码

4.1 HalExcNMI


当发生 NMI(Non Maskable Interrupt,不可屏蔽中断)时,会触发运行 HalExcNMI 汇编函数,该函数的执行流程如下图。下文会结合该流程图来阅读函数代码。



HalExcNMI 函数代码如下,⑴处给 R0 寄存器赋值 OS_EXC_CAUSE_NMI,该值等于 16,对应文件 kernel\arch\arm\cortex-m7\gcc\los_arch_interrupt.h 中的异常类型宏定义 OS_EXC_CAUSE_NMI,均为 16。该值对应 HalExcHandleEntry 函数的第一个参数。⑵处设置故障地址,该值对应 HalExcHandleEntry 函数的第二个参数。⑶处跳转到函数 osExcDispatch 继续执行。


    .type HalExcNMI, %function    .global HalExcNMIHalExcNMI:    .fnstart    .cantunwind⑴  MOV  R0, #OS_EXC_CAUSE_NMI⑵  MOV  R1, #0⑶  B  osExcDispatch    .fnend
复制代码


​下面分析的一些函数比较通用,其他异常处理函数也都会调用。

4.1.1 osExcDispatch 函数


osExcDispatch 函数代码如下,⑴处加载 InterruptActive Bit Registers 中断活跃位寄存器基地址。中断活跃位寄存器共有 8 个,NVIC_IABR0-NVIC_IABR7,每个寄存器包含 32 位,可以对应 32 个中断号,共支持 256 个中断。其中,IABR[0]的 bit 位 0~31 分别对应中断号 0~31;IABR[1]的 bit 位 0~31 对应中断 32~63;其他以此类推。⑵处设置循环计数,对应 8 个寄存器,后文会循环遍历 8 个寄存器查询是否存在活跃的中断。


    .type osExcDispatch, %function    .global osExcDispatchosExcDispatch:    .fnstart    .cantunwind⑴  LDR   R2, =OS_NVIC_ACT_BASE⑵  MOV   R12, #8                       // R12 is hwi check loop counter    .fnend
复制代码


4.1.2 _hwiActiveCheck 函数


执行完上述 osExcDispatch 函数代码后,会继续执行随后的函数_hwiActiveCheck 的代码。⑴处读取活跃位寄存器的数值,然后执行⑵比较寄存器数值与 0 的大小,如果相等,说明该活跃位寄存器对应的中断均不活跃,然后跳转到_hwiActiveCheckNext。如果不等于 0,则执行⑶,参数类型的高 16 位标记为中断。⑷处代码根据中断活跃位计算中断号,并赋值给寄存器 R2,该值对应 HalExcHandleEntry 函数的第三个参数。具体计算方式为,首先反转活跃中断位寄存器数值 R3,并保存到 R2,然后计算高位 0 的数量。把计数值 R12 加 1,然后左移 5 位(等于乘以 32),然后加上 R2,就是中断号。


    .type _hwiActiveCheck, %function    .global _hwiActiveCheck_hwiActiveCheck:    .fnstart    .cantunwind⑴  LDR   R3, [R2]                      // R3 store active hwi register when exc⑵  CMP   R3, #0    BEQ   _hwiActiveCheckNext
// exc occurred in IRQ⑶ ORR R0, R0, #FLAG_HWI_ACTIVE⑷ RBIT R2, R3 CLZ R2, R2 AND R12, R12, #1 ADD R2, R2, R12, LSL #5 // calculate R2 (hwi number) as pid .fnend
复制代码


4.1.3 _ExcInMSP 函数和_NoFloatInMsp 函数


如果有活跃的中断,则继续执行后续的代码。处理中断时,使用的主栈处理函数_ExcInMSP。⑴处比较异常返回值和 #0XFFFFFFED 的大小,如果相等说明支持浮点计算则继续执行后续代码,如果不相等则不支持浮点计算,会跳转到函数_NoFloatInMsp 函数。有关异常返回值的更多信息请参考《Cortex™-M7 Devices Generic User Guide》表格 Table 2-15 Exception return behavior。


如果支持浮点计算时,执行⑵把栈指针加上 104 赋值给 R3 寄存器,然后压栈,该值对应 HalExcHandleEntry 函数的第四个参数。104 的大小应该来源于结构体 EXC_CONTEXT_S。⑶处把寄存器 PRIMASK 数值复制到 R12 寄存器,然后把 R4-R12 寄存器压栈。⑷处把浮点寄存器压栈,⑸处跳转到函数_handleEntry。


当不支持浮点计算时,执行函数_NoFloatInMsp。⑹处把栈指针加上 32 赋值给 R3 寄存器,然后压栈,该值对应 HalExcHandleEntry 函数的第四个参数。然后把 R3 压栈,把寄存器 PRIMASK 数值复制到 R12,然后压栈 R4-R12。和支持浮点时的差别就是,不需要压栈 D8-D15 寄存器。⑺处把参数类型高位上加上不支持浮点的标记,然后跳转到函数_handleEntry。


    .type _ExcInMSP, %function    .global _ExcInMSP_ExcInMSP:    .fnstart    .cantunwind⑴  CMP   LR, #0XFFFFFFED    BNE   _NoFloatInMsp⑵  ADD   R3, R13, #104    PUSH  {R3}⑶  MRS   R12, PRIMASK                  // store message-->exc: disable int?    PUSH {R4-R12}                       // store message-->exc: {R4-R12}#if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \     (defined(__FPU_USED) && (__FPU_USED == 1U)))⑷  VPUSH {D8-D15}#endif⑸  B     _handleEntry  .fnend
.type _NoFloatInMsp, %function .global _NoFloatInMsp_NoFloatInMsp: .fnstart .cantunwind⑹ ADD R3, R13, #32 PUSH {R3} // save IRQ SP // store message-->exc: MSP(R13)
MRS R12, PRIMASK // store message-->exc: disable int? PUSH {R4-R12} // store message-->exc: {R4-R12}⑺ ORR R0, R0, #FLAG_NO_FLOAT B _handleEntry .fnend
复制代码


4.1.4 _hwiActiveCheckNext 函数


遍历中断活跃位寄存器时,如果前一个寄存器没有活跃的中断则执行函数_hwiActiveCheckNext 判断下一个寄存器是否有活跃的中断。⑴处把活跃位寄存器地址偏移 4 字节,计数减 1,如果还有其他活跃位寄存器,则跳转到函数_hwiActiveCheck 继续判断。否则执行后续的代码,⑵处加载 SystemHandler Control and State Register(缩写 SHCSRS)系统处理控制与状态寄存器的地址,然后加载半字节数值。⑶处加载掩码 0xC00,该数值二进制的第 10、第 11 位为 1。SHCSRS 寄存器的第 11 位对应 SysTick 异常活跃位,第 10 位对应 PendSV 异常活跃位。⑷处 R2、R3 进行逻辑与计算,然后把结果与 0 进行比较,如果结果为 0,说明没有发生 ysTick 异常或 PendSV 异常。如果结果为 1,说明发生了异常,需要执行⑸跳转到函数_ExcInMSP 继续执行,上文已分析该函数。⑹处获取全局变量 g_taskScheduled 的地址,然后获取其数值,与 1 进行比较。如果等于 1,说明系统已经开始任务调度,会继续执行后续的代码。如果不为 1,系统未调度,处于初始化阶段,需要跳转到函数_ExcInMSP 继续执行。


如果系统开始了任务调度,此时使用进程栈 PSP,执行⑺,判断系统是否支持浮点计算。如果支持则继续执行,否则跳转到函数_NoFloatInPsp。⑻处开始的代码和函数_NoFloatInPsp 可以对比着阅读,前者需要压栈浮点寄存器,后者不需要。⑻处把栈指针复制到 R2 寄存器,然后把栈指针减去 96。⑼处把 PSP 线程栈指针值赋值给 R3 寄存器,然后把 R3 加 104 赋值给寄存器 R12,计算出来的值是任务栈指针,然后进行压栈。


⑽处复制 PRIMASK 寄存器数值到 R12,然后把寄存器 R4-R12 压栈,接着压栈浮点寄存器 D8-D15。⑾处从 PSP 栈指针开始把 R4-R11、D8-D15 出栈,然后从 R13 栈指针开始把 D8-D15、R4-R11 进行压栈。⑿处跳转到函数_handleEntry 继续指向。


    .type _hwiActiveCheckNext, %function    .global _hwiActiveCheckNext_hwiActiveCheckNext:    .fnstart    .cantunwind⑴  ADD   R2, R2, #4                        // next NVIC ACT ADDR    SUBS  R12, R12, #1    BNE   _hwiActiveCheck
/*NMI interrupt exception*/⑵ LDR R2, =OS_NVIC_SHCSRS LDRH R2,[R2]⑶ LDR R3,=OS_NVIC_SHCSR_MASK⑷ AND R2, R2,R3 CMP R2,#0⑸ BNE _ExcInMSP // exc occured in Task or Init or exc // reserved for register info from task stack
⑹ LDR R2, =g_taskScheduled LDR R2, [R2] TST R2, #1 // OS_FLG_BGD_ACTIVE BEQ _ExcInMSP // if exc occurred in Init then branch⑺ CMP LR, #0xFFFFFFED //auto push floating registers BNE _NoFloatInPsp
// exc occurred in Task⑻ MOV R2, R13 SUB R13, #96 // add 8 Bytes reg(for STMFD)
⑼ MRS R3, PSP ADD R12, R3, #104 PUSH {R12} // save task SP
⑽ MRS R12, PRIMASK PUSH {R4-R12} VPUSH {D8-D15}
// copy auto saved task register
⑾ LDMFD R3!, {R4-R11} // R4-R11 store PSP reg(auto push when exc in task) VLDMIA R3!, {D8-D15} VSTMDB R2!, {D8-D15} STMFD R2!, {R4-R11}⑿ B _handleEntry .fnend
.type _NoFloatInPsp, %function .global _NoFloatInPsp_NoFloatInPsp: .fnstart .cantunwind MOV R2, R13 // no auto push floating registers SUB R13, #32 // add 8 Bytes reg(for STMFD)
MRS R3, PSP ADD R12, R3, #32 PUSH {R12} // save task SP
MRS R12, PRIMASK PUSH {R4-R12}
LDMFD R3, {R4-R11} // R4-R11 store PSP reg(auto push when exc in task) STMFD R2!, {R4-R11} ORR R0, R0, #FLAG_NO_FLOAT .fnend
复制代码


4.1.5 _handleEntry 函数


继续分析函数_handleEntry。代码很简单,⑴把栈指针复制给 R3,该值对应 HalExcHandleEntry 函数的第四个参数。⑵处关闭中断,关闭 Fault 异常,然后执行⑵跳转到 C 语言的函数 HalExcHandleEntry。


_handleEntry:    .fnstart    .cantunwind⑴  MOV R3, R13                         // R13:the 4th param⑵  CPSID I    CPSID F    B  HalExcHandleEntry
NOP .fnend
复制代码


4.2 HalExcUsageFault


当发生使用异常 UsageFault 时,会触发运行 HalExcUsageFault 汇编函数,该函数的执行流程如下图。下文会结合该流程图来阅读函数代码。



HalExcUsageFault 函数代码如下,⑴处把可配置故障状态寄存器 Configurable Fault Status Register(CFSR)的地址复制到 R0 寄存器,然后读取寄存器值到 R0 寄存器。⑵处把 0x030F 赋值给 R1 寄存器,然后左移 16 位。UsageFaultStatus Register 使用故障状态寄存器的有效性如下,即 0-3,8-9 为有效位,0x030F 的二进制对应这些有效位。⑶处进行逻辑与,这样就计算出实际的使用故障对应的 bit 位。⑷处把 R12 赋值为 0,然后会继续执行后续的汇编代码 osExcCommonBMU。



    .type HalExcUsageFault, %function    .global HalExcUsageFaultHalExcUsageFault:    .fnstart    .cantunwind⑴  LDR  R0, =OS_NVIC_FSR    LDR  R0, [R0]
⑵ MOVW R1, #0x030F LSL R1, R1, #16⑶ AND R0, R0, R1⑷ MOV R12, #0
.fnend
复制代码


​​4.2.1 g_uwExcTbl 数组


在看 osExcCommonBMU 函数的代码之前需要了解下 g_uwExcTbl 数组,g_uwExcTbl 数组定义在文件 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,代码如下。


该数组包含 32 个元素,每个元素对应 CFSR 寄存器的一个 bit 位,元素数值在 LiteOS-M 中定义为异常类型。比如 OS_EXC_UF_DIVBYZERO 等于异常类型 10,为除零异常。


UINT8 g_uwExcTbl[FAULT_STATUS_REG_BIT] = {    0, 0, 0, 0, 0, 0, OS_EXC_UF_DIVBYZERO, OS_EXC_UF_UNALIGNED,    0, 0, 0, 0, OS_EXC_UF_NOCP, OS_EXC_UF_INVPC, OS_EXC_UF_INVSTATE, OS_EXC_UF_UNDEFINSTR,    0, 0, 0, OS_EXC_BF_STKERR, OS_EXC_BF_UNSTKERR, OS_EXC_BF_IMPRECISERR, OS_EXC_BF_PRECISERR, OS_EXC_BF_IBUSERR,    0, 0, 0, OS_EXC_MF_MSTKERR, OS_EXC_MF_MUNSTKERR, 0, OS_EXC_MF_DACCVIOL, OS_EXC_MF_IACCVIOL};
复制代码


4.2.2 osExcCommonBMU 函数


现在来分析下汇编代码 osExcCommonBMU。⑴处计算出 R0 数值的高位 0 的个数,加载数组全局变量 g_uwExcTbl 地址到 R3 寄存器,然后执行⑵计算是第几个数组元素,加载元素值到 R0 寄存器。⑶处 R0 与 R12 进行逻辑或运算,没有什么影响。R0 对应 HalExcHandleEntry 函数的第一个参数。后续会继续执行 osExcDispatch 函数,前文已经分析过。


    .type osExcCommonBMU, %function    .global osExcCommonBMUosExcCommonBMU:    .fnstart    .cantunwind⑴  CLZ  R0, R0    LDR  R3, =g_uwExcTbl⑵  ADD  R3, R3, R0    LDRB R0, [R3]⑶  ORR  R0, R0, R12    .fnend
复制代码


小结


本文介绍了 Fault 异常类型,向量表及其代码,异常处理 C 语言程序,异常处理汇编函数实现代码。感谢阅读,如有任何问题、建议,都可以博客下留言给我,谢谢。

参考资料

Cortex™-M7 Devices Generic User Guide Download


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
鸿蒙轻内核M核的故障管家:Fault异常处理