鸿蒙轻内核M核源码分析系列十八 Fault异常处理 原创 精华
鸿蒙轻内核M核源码分析系列十八 Fault异常处理
【本文正在参与优质创作者激励】
Fault异常处理模块与OpenHarmony LiteOS-M
内核芯片架构相关,提供对HardFault
、MemManage
、BusFault
、UsageFault
等各种故障异常处理。有关Cortex-M芯片相关的知识不在本文讨论,请自行参考《Cortex™-M7 Devices Generic User Guide》等官方资料。本文先简单介绍下Fault异常类型,向量表及其代码,异常处理C语言程序,然后详细分析下异常处理汇编函数实现代码。文中所涉及的源码,以OpenHarmony LiteOS-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、Memory management fault、Bus fault、Usage fault、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位来使能除零异常。
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
函数参数对应关系如下表所示:
寄存器 | 参数 | 含义 |
---|---|---|
R0 | UINT32 excType | 异常类型,高16位也会记录故障地址是否有效、是否中断、是否浮点等标记信息 |
R1 | UINT32 faultAddr | 故障地址 |
R2 | UINT32 pid | 任务编号或中断编号 |
R3 | EXC_CONTEXT_S *excBufAddr | 异常上下文信息地址 |
下面我们分析下函数的源代码,⑴处的标签表示异常类型参数的高16位用于特色的标记,主要用于标记故障地址是否有效、是否故障发生在中断中,是否支持浮点等。⑵处增加中断计数和嵌套异常数目。⑶记录异常类型,⑷处如果记录了有效的故障地址,则获取故障地址。⑸处如果当前运行任务存在时,若标记了异常发生在中断,则记录中断号,并记录异常发生在中断内,否则记录任务编号,并记录异常发生在任务内。如果当前运行任务为空,则异常发生在初始化阶段。⑹处如果异常类型里包含支持浮点数的标记,则相应处理下。⑺处输出异常信息到控制台。
4、Los_Exc异常处理汇编函数
上文介绍Vector table向量表时,已经提到了在文件kernel\arch\arm\cortex-m7\gcc\los_exc.S
中定义的的异常处理函数,如下。当发生Fault故障异常时,会调度执行这些异常处理函数,本节会详细分析函数的源代码来掌握内核如何处理这些发生的异常。这6个函数处理过程类似,我们选择2个典型的函数进行分析。
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
继续执行。
下面分析的一些函数比较通用,其他异常处理函数也都会调用。
4.1.1 osExcDispatch
函数
osExcDispatch
函数代码如下,⑴处加载Interrupt Active Bit Registers
中断活跃位寄存器基地址。中断活跃位寄存器共有8个,NVIC_IABR0-NVIC_IABR7
,每个寄存器包含32位,可以对应32个中断号,共支持256个中断。其中,IABR[0]
的 bit位0~31 分别对应中断号031;`IABR[1]`的bit位031对应中断32~63;其他以此类推。⑵处设置循环计数,对应8个寄存器,后文会循环遍历8个寄存器查询是否存在活跃的中断。
4.1.2 _hwiActiveCheck
函数
执行完上述osExcDispatch
函数代码后,会继续执行随后的函数_hwiActiveCheck
的代码。⑴处读取活跃位寄存器的数值,然后执行⑵比较寄存器数值与0的大小,如果相等,说明该活跃位寄存器对应的中断均不活跃,然后跳转到_hwiActiveCheckNext
。如果不等于0,则执行⑶,参数类型的高16位标记为中断。⑷处代码根据中断活跃位计算中断号,并赋值给寄存器R2,该值对应HalExcHandleEntry
函数的第三个参数。具体计算方式为,首先反转活跃中断位寄存器数值R3,并保存到R2,然后计算高位0的数量。把计数值R12加1,然后左移5位(等于乘以32),然后加上R2,就是中断号。
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
。
4.1.4 _hwiActiveCheckNext
函数
遍历中断活跃位寄存器时,如果前一个寄存器没有活跃的中断则执行函数_hwiActiveCheckNext
判断下一个寄存器是否有活跃的中断。⑴处把活跃位寄存器地址偏移4字节,计数减1,如果还有其他活跃位寄存器,则跳转到函数_hwiActiveCheck
继续判断。否则执行后续的代码,⑵处加载System Handler 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
继续指向。
4.1.5 _handleEntry
函数
继续分析函数_handleEntry
。代码很简单,⑴把栈指针复制给R3,该值对应HalExcHandleEntry
函数的第四个参数。⑵处关闭中断,关闭Fault异常,然后执行⑵跳转到C语言的函数HalExcHandleEntry
。
4.2 HalExcUsageFault
当发生使用异常UsageFault时,会触发运行HalExcUsageFault
汇编函数,该函数的执行流程如下图。下文会结合该流程图来阅读函数代码。
HalExcUsageFault
函数代码如下,⑴处把可配置故障状态寄存器Configurable Fault Status Register(CFSR)的地址复制到R0寄存器,然后读取寄存器值到R0寄存器。⑵处把0x030F赋值给R1寄存器,然后左移16位。UsageFault Status Register
使用故障状态寄存器的有效性如下,即0-3,8-9为有效位,0x030F的二进制对应这些有效位。⑶处进行逻辑与,这样就计算出实际的使用故障对应的bit位。⑷处把R12赋值为0,然后会继续执行后续的汇编代码osExcCommonBMU
。
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,为除零异常。
4.2.2 osExcCommonBMU
函数
现在来分析下汇编代码osExcCommonBMU
。⑴处计算出R0数值的高位0的个数,加载数组全局变量g_uwExcTbl
地址到R3寄存器,然后执行⑵计算是第几个数组元素,加载元素值到R0寄存器。⑶处R0与R12进行逻辑或运算,没有什么影响。R0对应HalExcHandleEntry
函数的第一个参数。后续会继续执行osExcDispatch
函数,前文已经分析过。
小结
本文介绍了Fault异常类型,向量表及其代码,异常处理C语言程序,异常处理汇编函数实现代码。感谢阅读,如有任何问题、建议,都可以博客下留言给我,谢谢。
参考资料
【本文正在参与优质创作者激励】
详细的讲解,围观一波。
谢谢 多多指正