如何从HardFault异常中定位到错误代码

调试Cortex-M处理器的程序,最常见的异常就是HardFault错误,这种错误一般是因为非法访问内存或者非法指令引起的,比如写空指针,程序跑飞等情况。 但常常出现HardFault异常后,难以直接定位到出错代码,只能通过读取栈顶数据来定位错误代码。 那么发生HardFault异常的栈顶数据该怎么解读?


在Cortex-M相关文档中异常模型章节可以找到一张图表,下图是Cortex-M4当异常发生时,硬件会自动往栈顶写入的数据结构。 从图中可以看出Cortex-M4有两种模式,一种是带FPU指针的结构,另一种是不带FPU的结构,其中不带FPU的结构和Cortex-M3是完全一样的。 即便两种模式的栈结构稍有差异,但其实栈顶的前8个数据都是一样的,分别是R0,R1,R2,R3,R12,LR,PC。 在发生HardFault错误时,我们最关心的就是LR和PC的值,PC代表发生错误的那条指令,LR代表当前函数返回后的下一条指令地址。


下面用实例来说明如何从HardFault错误时的栈顶数据找到出错代码。 用Keil写一段测试代码如下,bad函数指针指向无效地址,会引起非法指令异常,进入HardFault。

/*
* HardFault错误演示
* 蒋晓岗<kerndev@foxmail.com>
*/
#include <string.h>

typedef void (*bad_func)(void);

//跳转到无效地址,程序跑飞
void fault(void)
{
	bad_func bad;
	bad = (bad_func)0xDEADDEAD;
	bad();
	memset(bad, 0, sizeof(bad));
}

int main(void)
{
	fault();
	while(1);
}

看看运行时的效果,程序一运行,马上就进入HardFault_Handler,此时查看实时寄存器的值SP=0x200003F0,PC=0x08080236这是一条死循环指令; 然后在右边的内存查看窗口中输入SP,按回车可以看到栈顶的内存数据,第一个数据20000060就是发生异常时R0寄存器的值,下面依次是R1,R2,R3,R12,LR,PC。 要定位到出错的代码,重点关注PC和LR的值。 栈顶数据中 PC=0xDEADDEAC,LR=0x080802E7.

在得到HardFault异常发生时的PC和LR值之后,我们就可以去反汇编窗口,右键选择“Show Disassembly at Address”,手动输入地址直接定位到该错误指令。 首先看PC=0xDEADDEAC,这是一个无效地址。 即使我们用反汇编去查看这个地址也是看不到任何有效汇编指令的,由此断定我们的程序一定是跑飞了。 但是为什么跑飞呢,我们只能继续分析LR的值,来寻找答案。

然后看LR=0x080802E7,幸好这是一个位于FLASH代码段的地址,在反汇编地址中输入0x080802E6。 注解:为什么不是输入0x080802E7呢?因为LR的最低位表示的是指令类型ARM/Thumb,通常最低位都是1,因此要减去1。

从汇编窗口可以看到,LR指向的指令是MOVS R0,#0x00,通常情况下引发异常的元凶是它的上一条指令,BLX R4,这是一个函数调用, 引发HardFault异常的元凶就是这个函数调用,跳转到R4寄存器所指示的地址。 由于R4寄存器的值不在栈里保存,只能查看实时寄存器的值,左侧显示R4=0xDEADDEAD。 那么这就真相大白了,BLX跳转到0xDEADDEAD地址,该地址是无效地址,从该地址读取不到合法的指令,因此引发HardFault异常。

注:明明跳转的地址是0xDEADDEAD,而HardFault之后栈顶PC的值是0xDEADDEAC呢? 因为Cortex-M是Thumb2指令集,是2字节对齐的,跳转指令会自动进行字节对齐。实际传入PC的值就是0xDEADDEAC。


总结:

当程序进入HardFault异常时,我们可以通过在线调试器,读取SP内存,找到引发异常的PC指针,定位到错误的指令。

如果此时PC指针是无效地址,那么还可以通过LR返回地址,推测出现异常的函数调用。