简介 #
hardfault是单片机开发中经常遇到的“错误(exception)”,这类错误非常的难以定位和解决。 本文的目的是描述hardfault产生的原因、调试解决办法。
产生原因 #
什么是错误 #
任何编程语言都可能发生“错误(exception)”。最简单的例子,任何编程语言都不能执行除零操作,即 int a=1/0
一定会报错。
不同的编程语言在处理错误时有不同的处理方法。在C语言中,任何错误会直接导致程序崩溃;在Java、JavaScript、Nodejs等高级语言中,可以使用try-catch
对错误进行捕获,只有未捕获的错误才会导致程序崩溃。
例如以下代码可以正常运行:
public class Main
{
public static void main(String[] args) {
try {
int a = 1/0;
} catch(Exception e) {
System.out.println("Catch error!");
}
System.out.println("Hello World");
}
}
//Output:
//Catch error!
//Hello World
C++崩溃
c语言程序崩溃
java中崩溃
什么是Hardfault #
在ARM的Cortex架构中,设计了一种中断叫做hardfault interrupt。 当发生了任何“错误(exception)”的时候,会触发此中断,并调用 Hardfault_Handle
。
例如:非法地址访问、除0、等。
调试及解决办法 #
从原理上分析 #
从原理上看hardfault只是进入了一个特殊的中断,并且在进入中断时,将一些SR寄存器(Status Register)标志位置位,以提供非常少的必要的信息:例如错误类型。
https://www.keil.com/appnotes/files/apnt209.pdf
解决办法 #
理论 #
通常要解决hardfault,要知道哪一行代码导致了hardfault。
因此解决办法是去“栈(Stack)”中寻找“帧(Frame)”:
帧(Frame): 当在函数A中执行函数B的时候,会将A的局部变量、地址等保存在栈中;然后跳转到B执行,执行完毕后,再从栈中取出A的地址并恢复A的局部变量,然后继续执行A函数。 上述过程中保存A的局部变量、地址等就叫做保存一帧。
http://kirste.userpage.fu-berlin.de/chemnet/use/info/gdb/gdb_7.html
当发生了hardfault的时候,去栈中寻找上一帧,从而寻找到最近执行的函数位置。
SP、LR、PC、PSR、MSP、PSP寄存器: #
MSP和PSP寄存器区别: https://www.sciencedirect.com/topics/engineering/main-stack-pointer
- SP Stack Pointer: 栈指针,用于表示当前执行函数所用的栈
- LR Link Register: 链接指针,用于表示当前执行函数返回地址
- PC Program Counter: 编程计数器,用于表示当前执行的指令地址
- PSR program status register: TODO
- MSP Main Stack Pointer: TODO
- PSP Process Stack Pointer: TODO
MSP和PSP都是栈指针。 当跑裸机的时候,只使用MSP。 当跑操作系统(RTOS)的时候,会使用MSP+PSP。
SP指针一定等于MSP或者PSP指针两者之一。
裸机:
当跑裸机的时候,指使用一个栈,SP == MSP 等于程序所用的指针。
操作系统:
当跑操作系统的时候,由于涉及到多线程切换,会用到多个栈,所以除了SP指针,还需要PSP指针来协助。
PSP指针指向当前线程所用的栈,MSP指针指向主栈。
STM32上的实际操作 #
如下图所示,发生了一次hardfault,并且在hardfault handle中打了一个断点:
因为这里使用了操作系统,所以应该看PSP指针所指向的栈:0x2401CC40
然后寻找帧Frame:
对栈上的数据进行观察,0x081xxxx的数据可能是地址。 比如上图中的 08161058
。 反查这个地址:
无关,再观察到0817A559
,再反查:
有关了,可以看到是这里有问题。
最佳实践: #
帧中的地址 #
上述有提到,帧是由局部变量+函数返回地址等组成的,而我们要寻找其中的函数返回地址。 帧是没有明显规律的,只能通过经验观察。
值得一提的是,函数返回地址附近一定有跳转指令,即B BL BLX
等汇编指令。
比如上图所示0x0817A559
附近有BL.W指令。而0x08161058
附近没有相关指令。所以前者是一个帧地址,后者不是。
TODO如果在hardfault中又产生了hardfault?
TODO: 从栈中寻找信息:
https://github.com/armink/CmBacktrace/blob/master/README_ZH.md
参考资料: #
cortex-M3 M4 M7 hardfault:
https://www.keil.com/appnotes/files/apnt209.pdf
gdb指令,通过帧寻找调用栈:
http://kirste.userpage.fu-berlin.de/chemnet/use/info/gdb/gdb_7.html
MSP、SP、PSP寄存器的区别:
https://www.sciencedirect.com/topics/engineering/main-stack-pointer
栈溢出导致的错误:
https://blog.csdn.net/lanximu/article/details/18259829
cortex 寄存器汇总:
https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-processor/programmers-model/core-registers?lang=en
PSR寄存器:
https://www.sciencedirect.com/topics/computer-science/current-program-status-register
附录:离线调试hardfault #
上述的操作都是建立在使用jlink调试器的场景下。 在实际产品中,由于各种各样的奇怪因素,往往我们不能使用jlink调试器。
例如:硬件设计时没有拉出jlink引脚、结构设计时没有预留jlink口、设备在遥远的国外。等等因素。
所以这里介绍一下如何离线调试hardfault。 整体思路和 linux的 coredump 类似: 发生hardfault时,将内存、寄存器等信息存到文件里面,然后离线分析。
建议参考已有现成的方案:
zephyr的coredump: https://docs.zephyrproject.org/latest/services/debugging/coredump.html
具体代码:https://github.com/zephyrproject-rtos/zephyr/blob/main/kernel/fatal.c#L116