本帖最后由 luobeihai 于 2024-11-23 20:12 编辑
#申请原创# @21小跑堂
最近在使用Geehy的芯片进行开发过程中,遇到了无法使用在线调试仿真的情况下,去分析开发过程中遇到的HardFault异常。于是,在解决问题的过程中,解锁了不用在线仿真的情况下,也可以对HardFault进行错误定位分析。
1. 内核异常与xPSR
1.1 内核异常
Cortex-M 内核的MCU,异常架构支持多种内核异常和外部中断,编号 1-15 的为系统异常,16 及以上的则为外部中断。
下表是Cortext-M的内核异常,可以看到我们将要分析的Hardfault异常,其编号是3。
HardFault 异常是无需使能,它有固定的异常优先级-1。当用户没有去使能 MemManage/Bus/Usage这三种Fault时,那么如果发生了这三种错误的话,都会触发Hardfault异常中断。
如果用户要使能MemManage/Bus/Usage这三种Fault,可以在芯片上电启动时进行配置。
SCB->SHCSR |= 0x00007000;
不过在大多数情况下,用户不会对使能这些异常,也就是说发生这些错误基本都是响应Hardfault中断服务函数。
1.2 xPSR
我们如何知道当前执行的中断是Hardfault中断?这就需要了解xPSR,即程序状态寄存器了。
程序状态寄存器(Program Status Register)一共涉及到三个寄存器,分别为:Application PSR(APSR)、Execution PSR(EPSR)和Interrupt PSR(IPSR)。如下所示, 这三个寄存器可以组合为一个寄存器一起访问,标记为xPSR。
程序状态寄存器的位定义如下: N: 负数标识位(Negative flag) Z: 零标识位(Zero flag) C: 进位或者无借位标识位(Carry or NOT borrow flag) V: 溢出标识位(Overflow flag) Q: Sticky saturation flag GE: 大于等于标识位(Greater-Than or Equal flags for each byte lane) ICT/IT: Interrupt-Continuable Instruction(ICT)bits, IF-THEN instruction status bit for conditional execution. T: Thumb状态标识位,始终为1;尝试清除该位将触发一个错误异常。 Exception Number: 异常代号,标志着正在处理的异常。
所以,我们通过通过程序状态寄存器的bit[0:8]位,可以得知当前执行的中断服务函数是否位Hardfault(如何才能获取xPSR寄存器的值,下面会进行介绍)。
2. 栈帧与内核寄存器
在异常入口处被压人栈空间的数据块为栈帧。
对于Cortex-M内核的处理器,如果不具有浮点单元的话,那么栈帧都是8个字(1个字32bit)大小的;而如果具有浮点单元的话,那么栈帧可能是8或者26个字。 处理器在运行程序过程中,当被中断打断了当前执行的程序,跳转到执行中断服务程序之前,会将PC、LR寄存器,还有一些通用寄存器压入栈中(PUSH),这就是保存现场。当中断服务函数处理完之后,又会将原来压栈的数据,从栈中恢复到内核寄存器(POP),这就是恢复现场。
大部分情况下,都是保存8个字的栈帧,那么一般都是保存如下图内核寄存器的值:
也就是说,跳到中断服务函数之前,压栈的内核寄存器,从低地址到高地址依次有:R0/R1/R2/R3/R12/LR/PC/xPSR,共8个内核寄存器。 压栈的内核寄存器一共有8个,这里Cortex-M内核手册说会有返回地址压栈,这个返回地址保存的其实就是执行了这条指令后触发了中断,然后保存的这条指令的地址。
3. 如何使用调试器定位错误代码
下面介绍不使用在线仿真的情况下,只使用调试器如何进行Hardfault分析。
这里以Jlink调试器为例,如果大家对Jlink调试器的一些命令不熟悉的话,可以参考下网上的资源。或者这篇博文:https://blog.csdn.net/qq_30095921/article/details/128311887
对于Jlink命令行的使用这篇文章介绍的已经非常的详细了。
3.1 获取内核寄存器
1、打开Jlink Commander,输入下面命令连接到芯片。
2、输入h命令获取内核寄存器
3.2 如何判断当前是否触发Hardfault
我们前面就已经介绍过,xPSR寄存器的低9bit的值,存放这当前中断的编号。所以上面通过Jlink获取到内核寄存器的xPSR的值之后,即可判断到其是否触发了Hardfault异常(中断编号3)。
3.3 分析定位错误代码的原理
前面我们已经知道,压栈的内核寄存器有8个。而且上面获取到了内核寄存器的值之后,有一个SP的值。我们通过读取SP所执行的栈空间,就可以获取到产生Hardfault错误之前,8个内核寄存器的状态。
根据上图通过mem32命令,读取SP指针指向的地址0x20000420,8个字的栈数据,其内核寄存器值和对应的栈地址如下表:
其中最重要的就是LR和PC的值,我们通过LR可以知道跳转到Hardfault中断服务函数之前,下一条指令的地址;而通过PC值,可以知道执行了那一条指令地址导致的Hardfault异常。然后我们再结合编译生成的.map文件,或者反汇编文件知道各个函数的地址,我们就可以明确的确定具体是哪条指令导致的Hardfault异常了。
4. 案例分析
下面通过几个案例进行分析,可以理解的比较深刻。
4.1 非法地址访问
1、在main函数中故意调用下面这个函数访问0x12345678的非法地址。
void illegal_address_access(void)
{
uint32_t temp = 0;
temp = *(uint32_t *)0x12345678;
}
2、执行代码触发Hardfault错误之后,我们通过Jlink获取内核寄存器值。
其中最重要的LR值是0x08000D4B,PC值是0x08000D00 。
3、通过生成的反汇编代码(或者.map文件),我们去搜索这两个地址值,或者搜索到与之相近的地址值,然后就可以确定具体出错的是那个函数,哪条语句。
所以可以确定导致Hardfault错误的函数是:illegal_address_access 。
4.2 非法函数调用
1、故意调用下面的非法函数。
void illegal_function_call(void)
{
void (*pFunc)(void);
pFunc = (void (*)(void))0x08FFAAAA;
pFunc(); // 通过函数指针调用非法函数
}
2、通过Jlink获取内核寄存器值。
3、搜索反汇编代码的0x08000D03(LR)和0x08FFAAAA(PC)地址值,确定代码出错的函数和位置。
通过分析可以确定是 illegal_function_call 这个函数导致的 Hardfault 错误。是因为 BLX r4 这条指令,跳转到了一个非法地址 0x08FFAAAA 。
这里这条指令的地址是0x08000D00,但是为什么栈帧里面的压栈数据,PC值不是这条指令的地址呢?这是因为BLX跳转指令,会把跳转的地址0x08FFAAAA赋值给PC,所以栈帧里面记录的不是跳转指令的地址值,而是要跳转的地址值,即0x08FFAAAA(这一点对于我最近做的项目很有用,终于搞清楚了是什么原因导致的Hardfault问题)。
为了方便大家分析,下面附件是这两个案例的工程代码。
|
不进行在线仿真,通过调试器的命令进行Hardfault问题的定位和分析。