HardFault 错误信息的解释和可采用的处理方法
1.发生中断时处理器的行为
不考虑其他细节,M3内核在发生中断时首先自动将如下8个寄存器压栈。因此在中断处理函数中,发生中断时正常执行时的寄存器数值已经被压入了堆栈中。在中断处理函数开始执行时,除了PC,LR,SP等控制寄存器,从r0-r12等这些通用寄存器的数据是没有变化的。下图描述了M3内核将寄存器压栈的顺序:
地址 | 寄存器 | 被保存的顺序 |
---|---|---|
旧SP(N-0) | 原先已压入的内容 | - |
(N-4) | xPSR | 2 |
(N-8) | PC | 1 |
(N-12) | LR | 8 |
(N-16) | R12 | 7 |
(N-20) | R3 | 6 |
(N-24) | R2 | 5 |
(N-28) | R1 | 4 |
新SP(N-32) | R0 | 3 |
2.编译器通过栈来实现函数调用
C编译器通过栈来实现函数的调用,即在栈中记录程序执行的轨迹并辅助寄存器进行参数传递。具体如何实现C函数的调用,历史上有很多的规范,这些规范叫做调用惯例。
对于ARM处理器来说,有一个官方的规范AAPCS(Procedure Call Standard for the ARM® Architecture)详细描述了进行函数调用时如何进行参数的传递和调用路径的记录等。
如下仅对使用栈记录调用路径的行为进行简单描述:查看编译器生成的汇编代码可以得知,大多数的函数调用通过BL语句实现,BL语句将当前程序下一条指令的地址存入LR寄存器,并跳转到指定的地方(子函数开始的地方)开始执行。子函数中如果还需要调用孙子函数,就会在函数的入口处将LR的值压栈,以便函数执行结束后能够返回父函数。因此依次找到栈中LR的数值,就能找到调用路径中各个函数的地址。最后根据map文件翻译出各函数的名称,就可以得到函数的调用路径了。
如下是一个简单函数汇编代码的例子,函数OnPowerOff调用了函数FS_Deinit,函数FS_Deinit调用了SPIFFS_unmount。可以看出OnPowerOff函数的入口如将LR压入栈中(此时LR中保存的是函数OnPowerOff的返回地址,也就是调用OnPowerOff的父函数中的某条指令的地址),然后调用了FS_Deinit。同样FS_Deinit也在入口处将LR压入栈中(此时LR中保存的是OnPowerOff函数中POP指令的地址),然后再调用SPIFFS_unmount。返回的过程,依次将栈中保存的返回地址直接出栈到PC寄存器,完成函数的返回。这样,如果某个函数将栈中的返回地址写坏,则函数在返回时就会跳转到某个随机的地方,这就是常说的“程序跑飞了”。
OnPowerOff PROC
;;;202 void OnPowerOff(void)
0001b2 b510 PUSH {r4,lr}
;;;203 {
;;;204 FS_Deinit();
0001b4 f7fffffe BL FS_Deinit
;;;205 }
0001b8 bd10 POP {r4,pc}
;;;206
ENDP
FS_Deinit PROC
;;;166 void FS_Deinit(void)
000158 b510 PUSH {r4,lr}
;;;167 {
;;;168 SPIFFS_unmount(&g_fs);
00015a 4810 LDR r0,|L1.412|
00015c f7fffffe BL SPIFFS_unmount
;;;169 return;
;;;170 }
000160 bd10 POP {r4,pc}
;;;171
ENDP
3.对信息的继续挖掘
- 通用寄存器
通用寄存器中可供挖掘的信息并不多,通常情况下r0-r3寄存器保存着函数的前四个参数(其余的参数在栈中保存),需要注意的是:这四个寄存器的数值仅在函数开始执行的时候是可靠的,在函数执行的过程中可能被改变。在函数返回时,寄存器r0和r1用于保存返回值(根据返回数据的大小,决定仅使用r0还是同时使用r0和r1)。同样这两个寄存器仅在子函数刚返回时数值才是可靠的。 - 特殊功能寄存器
特殊功能寄存器就是PC、LR和SP了。
SP指向当前的栈顶,在知晓栈的结构时,可以根据SP访问栈中的数据。
在中断处理函数中LR有特殊用法,其中保存了返回被中断地点的方法,而不是通常情况下的返回地址。因此在Hardfault处理函数中寄存器LR和PC的值没有太多参考意义,被处理器自动压栈的LR和PC最有用,PC记录了被中断打断前正在执行的指令地址(也是正在执行的函数地址),LR记录了被中断打断前,正在执行的函数的父函数的地址。根据这两个地址,可以找到引发Hardfault异常的函数和语句,以及其父函数(如果辅以汇编代码继续对栈的内容进行分析,则可以回溯整个调用路径)。
而具体引发Hardfault异常的原因,可以根据下面章节介绍的SCB寄存器来查看。 - SCB寄存器
在M3/M4处理器标准外设中,有一个叫做SCB(System Control Block)的部分,其中有6个寄存器记录了发生Hardfault异常的原因。
CMSIS规范中对SCB寄存器的定义:
typedef struct
{
__I uint32_t CPUID;
__IO uint32_t ICSR;
__IO uint32_t VTOR;
__IO uint32_t AIRCR;
__IO uint32_t SCR;
__IO uint32_t CCR;
__IO uint8_t SHP[12];
__IO uint32_t SHCSR;
__IO uint32_t CFSR; //主要关注
__IO uint32_t HFSR; //主要关注
__IO uit32_t DFSR;
__IO uint32_t MMFAR; //主要关注
__IO uint32_t BFAR; //主要关注
__IO uint32_t AFSR;
__I uint32_t PFR[2];
__I uint32_t DFR;
__I uint32_t ADR;
__I uint32_t MMFR[4];
__I uint32_t ISAR[5];
uint32_t RESERVED0[5];
__IO uint32_t CPACR;
} SCB_Type;
CFSR、HFSR、MMFAR、BFAR几个寄存器是我们需要关注的,AFSR是平台相关的暂时忽略。上述寄存器中CFSR又可以分为三个寄存器分别是:UFSR,BFSR,MFSR。上述寄存器的内存分布如下表所示:
地址 | 寄存器 | 全名 | 尺寸 |
---|---|---|---|
0xE000 ED28 | MFSR | MemManage fault 状态寄存器 | 字节 |
0XE000 ED29 | BFSR | 总线 fault 状态寄存器 | 字节 |
0XE000 ED2A | UFSR | 用法 fault 状态寄存器 | 半字 |
0XE000 ED2C | HFSR | 硬 fault 状态寄存器 | 字 |
0XE000 ED30 | DFSR | 调试 fault 状态寄存器 | 字 |
0XE000 ED3C | AFSR | 辅助 fault 状态寄存器 | 字 |
各寄存器数据的描述如下:
MFSR 中可能出现的错误及原因:
位 | 可能的原因 |
---|---|
MSTKERR | 入栈时发生错误(异常响应序列开始时) 1.堆栈指针的值被破坏 2.堆栈容易过大,已经超出MPU允许的region范围 |
MUNSTKERR | 出栈时发生错误(异常响应序列终止时),入栈时没有发生错误,出栈时却出错,总令人有些匪夷所思,可能的原因是 1.异常服务例程破坏的堆栈指针 2. MPU配置被异常服务例程更改 |
DACCVIOL | 内存访问保护违例。这是MPU发挥作用的体现。常常是用户应用程序企图访问特权级region所致 |
IACCVIOL | 1.内存访问保护违例。常常是用户应用程序企图访问特权级region。入栈的PC给出的地址,就是产生问题代码之所在 2.跳转到不可执行指令的regions 3.异常返回时,使用了无效的EXC_RETURN值 4.向量表中有无效的向量。例如,异常在向量建立之前就发生了,或者加载的是用于传统ARM内核的可执行映像 |
BFSR 中可能出现的错误及原因:
位 | 可能的原因 |
---|---|
STKERR | (自动)入栈期间出错 1.堆栈指针的值被破坏 2.堆栈容易太大,到达了未定义存储器的区域 3.PSP未经初始化就使用 |
UNSTKERR | (自动)出栈器件出错。如果没有发生过STKERR,则最可能的就是异常处理器件把SP的值破坏了 |
IMPRECISERR | 与设备传送数据的过程中发生总线错误。可能因为设备未经初始化而引起:在用户级访问了特权级的设备,或者传送的数据单位尺寸不能为设备所接受。此时,有可能是LDM/STM指令造成了非精确总线fault。 |
PRECISERR | 在数据访问期间的总线错误。通过BFAR可以获取具体的地址。发生fault的原因同上。 |
IBUSERR | 同MFSR中的IACCVIOL |
UFSR 中可能出现的错误及原因:
位 | 可能的原因 |
---|---|
DIVBYZERO | 当DIV_0_TRP置位时发生除数为零。导致此fault的指令可以从入栈的PC读取 |
UNALIGNED | 当UNALIGN_TRP置位时发生未对齐访问。导致此fault的指令可以从入栈的PC读取 |
NOCP | 企图执行一个协处理器指令。导致此fault的指令可以从入栈的PC读取 |
INVPC | 1.异常返回时使用了无效的 EXC_RETURN,例如: 1)当 EXC_RETURN = 0xFFFF FFF1 时却要返回线程模式 2)当 EXC_RETURN = 0xFFFF FFF9 时却要返回 handler 模式 2.无效的异常活动状态,例如: 1)当前异常的活动状态已经清除了,却在此时执行异常返回。往往是因为滥用 VECTCLRACTIVE 或清除了 SHCSR 中活动状态所致 2)在还有其他异常的活动位置位时,却要返回线程模式 3.由于堆栈指针错误导致了 IPSR 的值不正确。对于 INVPC fault ,入栈的 PC 指出了该 fault 服务例程在何处抢占了其他的代码。这个问题往往是比较隐晦的程序错误造成的,欲详细调查该问题的原因,最好使用ITM的跟踪功能。 4.ICI/IT 位对当前指令无效。当LDM/STM 指令被异常打断后,在异常服务例程中又更改了入栈的 PC。结果在中断返回时,非零的 ICI 位段作用到了不是用 ICI 位段的指令上。如果是其他原因破坏了 PSR 的值,也可能导致此 fault。 |
INVSTATE | 1.加载到 PC 中的跳转地址值是偶数(LSB=0)。通过检查入栈 PC 的值,一下子就可以查出该问题。 2.向量地址的 LSB=0,诊断方法同上。 3.入栈的 PSR 在异常处理过程之中被破坏,使得在返回时内核尝试进入 ARM 状态 |
UNDEFINSTR | 1.使用了 CM3 不支持的指令 2.代码段中的数据被破坏 3.连接时加载了 ARM 目标码。请检查编译阶段的位置 4.指令对其的问题。例如,在使用 GNU 工具链时,忘记了 .ascii后使用 .align,就有可能导致下一条指令没有对齐 |
解读SCB寄存器时应首先根据HFSR寄存器判断产生Hardfault的原因,如果确认是fault上访的情况,则依次检查BFSR、UFSR和HFSR确定具体的错误原因和地址。