在程序开发阶段,少不了 Debug 调试,除去编程架构搭建不稳定所造成的错误外,大部分会出现或多或少的故障异常,而这些异常有可能是粗心或者对编程了解不到位所导致的 ‘ 堆栈溢出 ’ 、‘ 数组下标越界 ’ 、‘ 数学运算异常 ’ 等等;而本篇则主要以常见的进入 Hardfault 中断来进行分析错误来源。
# ARM Cortex-M 核心
ARM 架构,过去称作高级精简指令集机器(英语:Advanced RISC Machine,更早称作艾康精简指令集机器,Acorn RISC Machine),是一个精简指令集(RISC)处理器架构家族,其广泛地使用在许多嵌入式系统设计。
而对于 ARM Cortex-M 是 ARM 架构处理器核心中,低阶系列的统称,由安谋控股所授权。这组核心的特点为低成本以及高能源效率的微处理器而优化设计,已有上千万个消费性设备中有此系列的微处理器。此系列核心包括 Cortex-M0、Cortex-M0+、Cortex-M1、Cortex-M3、Cortex-M4、Cortex-M7、Cortex-M23、Cortex-M33、Cortex-M35P 和 Cortex-M55。其中 Cortex-M4 / M7 / M33 / M35P / M55 核心有浮点运算器的选项,若有浮点运算器的选项,会在型号后面说明,例如 Cortex-Mx with FPU 或 Cortex-MxF,其中 x 是核心编号。
如标题所述,其中 Cortex-M3/M4/M7 同属 ARMv7-M 架构,再细分一点,Cortex-M3 实现的是 ARMv7-M 架构,Cortex-M4 /Cortex-M7 实现的是 ARMv7E-M 架构。
# ARMv7-M 和 ARMv7E-M 架构区别
如果有玩过过 Cortex-M3 系列的芯片跟 Cortex-M4/M7 的芯片,或多或少都知道 Cortex-M4/M7 比 Cortex-M3 多了 DSP 跟硬件浮点运算,以下是 ARMv7E-M 架构的拓展功能介绍:
由于 ARMv7E-M 是 ARMv7-M 的一个拓展子集,所以以下统称 ARMv7-M 为 “ARMv7-M” 和 “ ARMv7E-M ” 的集合。
# ARM 寄存器
# 通用寄存器(R0 - R12)
R0-R7 被称为低组寄存器。所有指令都能访问它们。它们的字长全是 32 位,复位后的初始值是不可预料的。
R8-R12 被称为高组寄存器。这是因为只有很少的 16 位 Thumb 指令能访问它们,32 位的 thumb-2 指令则不受限制。它们也是 32 位字长,且复位后的初始值是不可预料的。
# 堆栈指针寄存器(SP)
在 ARMv7-M 架构中,共有两个堆栈指针 SP_main
和 SP_process
,有时候也会把这个堆栈指针寄存器称为 R13。
- 主堆栈指针(MSP),或写作
SP_main
。这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。 - 进程堆栈指针(PSP),或写作
SP_process
。用于常规的应用程序代码(不处于异常服用例程中时)。
# 链接寄存器(LR)
链接寄存器(LR) 主要用于在调用子程序时存储返回地址;有时候也会把这个链接寄存器称为 R14。
# 程序计数器(PC)
程序计数器作为 PC(或称为 R15)访问,它根据所执行指令的大小(在 ARM 状态下始终为 4 个字节)递增;并通过分支指令将目标地址加载到 PC 中。 您还可以使用数据处理指令直接加载 PC。
Note: 寄存器 R0-R12、SP、LR 和 PC 被称为 Arm 核心寄存器。这些寄存器可以描述为 R0-R15。更详细的说明可以看《ARMv7-M Architecture Reference Manual》B1.4 章 Registers 部分
# 异常定义
# 异常类型
# 异常更新
在入栈和取向量操作完成之后,执行服务例程之前,需要更新一系列的寄存器:
- SP:在入栈后会把堆栈指针(PSP 或 MSP)更新到新的位置。在执行服务例程时,将由 MSP 负责对堆栈的访问。
- PSR:更新 IPSR 位段(地处 PSR 的最低部分)的值为新响应的异常编号。
- PC:在取向量完成后,PC 将指向服务例程的入口地址,
- LR:在出入 ISR 的时候,LR 的值将得到重新的诠释,这种特殊的值称为 “EXC_RETURN”,在异常进入时由系统计算并赋给 LR,并在异常返回时使用它。EXC_RETURN 的二进制值除了最低 4 位外全为 1,而其最低 4 位则有另外的含义。
以上是在响应异常时核心寄存器的变化。另一方面,在 NVIC 中,也会更新若干个相关有寄存器。例如,新响应异常的悬起位将被清除,同时其活动位将被置位。
# 异常返回值
在进入异常服务程序后,将自动更新 LR 的值为特殊的 EXC_RETURN。这是一个高 28 位全为 1 的值,只有 [3:0] 的值有特殊含义,如下图所示。当异常服务例程把这个值送往 PC 时,就会启动处理器的中断返回序列。因为 LR 的值是由内核自动设置的,所以只要没有特殊需求,就不要改动它。
# 异常入口的堆栈对齐
Armv7-M 架构保证堆栈指针值至少是 4 字节对齐的。 但是,某些软件标准要求堆栈指针按 8 字节对齐,并且体系结构可以强制执行这种对齐。 在配置和控制寄存器的 CCR. STKALIGN 位指示中作为异常入口的一部分,决定着处理器是将 SP 对齐到 4 个字节还是 8 个字节。 该位是由编译器决定是否为:
・RW,在这种情况下,它的复位值是由编译器决定的。
・RO,在这种情况下它是 RAO,表示 8 字节 SP 对齐。
Arm 不赞成实现或使用 4 字节 SP 对齐。
Register --- CCR. STKALIGN bit:
下图显示了在异常发生时进入堆栈的信息框架,以及处理器如何在堆栈上保留一个额外的字(如果需要的话),以获得 8 字节堆栈对齐。(这里就是我们要分析地方)
# HardFault 异常分析处理
HardFault 是一种通用故障,它适用于所有不能被任何其他异常机制处理的故障类。通常,HardFault 用于不可恢复的系统故障,尽管这不是必需的,而且 HardFault 的某些用途可能是可恢复的。HardFault 永久启用,优先级固定为 -1。
# 方法一:先来个简单的,使用第三方组件 “cm_backtrace”
项目地址:https://github.com/armink/CmBacktrace
怎么用这里就不说了,毕竟官方已经写得很清楚了,而且还有相应的 Demo 例程。
使用该方法的特点是不需要过多的关注更底层的东西,只需移植好后配置相应的功能就好了,而且它可以离线(脱离仿真器)来寻找错误点;但是前提是你的串口正常,而且还需要预留部分内存供其执行。
# 方法二:仿真情况下的 bug 寻找
如果是平常的 while 循环执行,导致挂掉了无法切换任务,那么我们可以轻松通过上下文切换寻找问题;但是在 HardFault 异常中它并不给你显示执行错误的所在地方,而是直接跳到 HardFault 中断中,这往往让我们头大。
Keil 平台的可以看:
https://www.keil.com/appnotes/files/apnt209.pdf
IAR 平台的可以看:
https://www.iar.com/knowledge/support/technical-notes/debugger/debugging-a-hardfault-on-cortex-m/
GCC 平台的可以看:
方法三。。。
# 方法三:通过 ARM 寄存器逆向推导
使用该方法的前提是获取到第五个大点最后一张图展示的核心寄存器(R0-R3、R12、LR、PC、xPSR 以及原 SP)的数值。
1、常见的核心寄存器数据获取
一般地,我们会使用以下代码嵌入到 HardFault 中断中,使其在入口处打印服务异常时各核心寄存器的值:
/* Exception frame without floating-point storage | |
* hard fault handler in C, | |
* with stack frame location as input parameter | |
*/ | |
void | |
hard_fault_handler_c(unsigned int * hardfault_args) | |
{ | |
unsigned int stacked_r0; | |
unsigned int stacked_r1; | |
unsigned int stacked_r2; | |
unsigned int stacked_r3; | |
unsigned int stacked_r12; | |
unsigned int stacked_lr; | |
unsigned int stacked_pc; | |
unsigned int stacked_psr; | |
//Exception stack frame | |
stacked_r0 = ((unsigned long) hardfault_args[0]); | |
stacked_r1 = ((unsigned long) hardfault_args[1]); | |
stacked_r2 = ((unsigned long) hardfault_args[2]); | |
stacked_r3 = ((unsigned long) hardfault_args[3]); | |
stacked_r12 = ((unsigned long) hardfault_args[4]); | |
stacked_lr = ((unsigned long) hardfault_args[5]); | |
stacked_pc = ((unsigned long) hardfault_args[6]); | |
stacked_psr = ((unsigned long) hardfault_args[7]); | |
printf ("[Hard fault handler]\n"); | |
printf ("R0 = %x\n", stacked_r0); | |
printf ("R1 = %x\n", stacked_r1); | |
printf ("R2 = %x\n", stacked_r2); | |
printf ("R3 = %x\n", stacked_r3); | |
printf ("R12 = %x\n", stacked_r12); | |
printf ("LR = %x\n", stacked_lr); | |
printf ("PC = %x\n", stacked_pc); | |
printf ("PSR = %x\n", stacked_psr); | |
#ifndef CW | |
printf ("BFAR = %x\n", (*((volatile unsigned long *)(0xE000ED38)))); | |
printf ("CFSR = %x\n", (*((volatile unsigned long *)(0xE000ED28)))); | |
printf ("HFSR = %x\n", (*((volatile unsigned long *)(0xE000ED2C)))); | |
printf ("DFSR = %x\n", (*((volatile unsigned long *)(0xE000ED30)))); | |
printf ("AFSR = %x\n", (*((volatile unsigned long *)(0xE000ED3C)))); | |
#else | |
printf ("BFAR = %x\n", (*((volatile unsigned int *)(0xE000ED38)))); | |
printf ("CFSR = %x\n", (*((volatile unsigned int *)(0xE000ED28)))); | |
printf ("HFSR = %x\n", (*((volatile unsigned int *)(0xE000ED2C)))); | |
printf ("DFSR = %x\n", (*((volatile unsigned int *)(0xE000ED30)))); | |
printf ("AFSR = %x\n", (*((volatile unsigned int *)(0xE000ED3C)))); | |
#endif | |
for(;;) | |
{} | |
} | |
/* The prototype shows it is a naked function - in effect this is just an | |
assembly function. */ | |
void HardFault_Handler( void ) __attribute__( ( naked ) ); | |
/* The fault handler implementation calls a function called | |
prvGetRegistersFromStack(). */ | |
void HardFault_Handler(void) | |
{ | |
#ifdef CORTEX_M3_M4_M7 | |
asm volatile( | |
" tst lr, #4 \n" /* Check EXC_RETURN[2] */ | |
" ite eq \n" | |
" mrseq r0, msp \n" | |
" mrsne r0, psp \n" | |
"b hard_fault_handler_c \n" | |
: /* no output */ | |
: /* no input */ | |
: "r0" /* clobber */ | |
); | |
#else | |
asm volatile( | |
"movs r0, #4 \n" | |
"mov r1, lr \n" | |
"tst r0, r1 \n" /* Check EXC_RETURN[2] */ | |
"beq 1f \n" | |
"mrs r0, psp \n" | |
"ldr r1,=hard_fault_handler_c \n" | |
"bx r1 \n" | |
"1:mrs r0,msp \n" | |
"ldr r1,=hard_fault_handler_c \n" | |
: /* no output */ | |
: /* no input */ | |
: "r0" /* clobber */ | |
); | |
#endif | |
} |
Note:
值得注意的是 void HardFault_Handler (void); 函数是相应的 HardFault 中断函数,不同的厂家会定义不同的名称。
对于不同的编译器,如:armcc、iar、gcc for arm 等,需要把相应的关键字替换掉(eg:
asm
、naked
)。naked
拓展:https://www.keil.com/support/man/docs/armclang_ref/armclang_ref_jhg1476893564298.htmhttps://zhuanlan.zhihu.com/p/33933891
2、逆向定位入口
如果有看过方法二的两个链接,那么就很容易理解以下的分析了:
- 确保你能正常获取输出数据(包括但不限于仿真查看、串口打印、SWO 输出、SEGGER_RTT 输出等等)。
- 由于我们在第一点修改过代码,所以可以直接查看 LR 和 PC 两个的值;因为这两个的值是关键。
- 使用 addr2line 软件定位故障代码位置(使用方法:https://sourceware.org/binutils/docs-2.27/binutils/addr2line.html#addr2line) addr2line 属于 GNU Binutils 组件之一,获取可以从方法一里面的 tools 文件夹里获取,也可以从
安装路径\GNU Tools ARM Embedded\5.4 2016q3\bin\arm-none-eabi-addr2line.exe
提取出来。
3、使用演示
先制造一个 HardFault:
void fault_test_by_div0(void) { | |
volatile int * SCB_CCR = (volatile int *) 0xE000ED14; // SCB->CCR | |
int x, y, z; | |
*SCB_CCR |= (1 << 4); /* bit4: DIV_0_TRP. */ | |
x = 10; | |
y = 0; | |
z = x / y; | |
printf("z:%d\n", z); | |
} | |
/************************************************ | |
函数名称 : main | |
功 能 : 主函数入口 | |
参 数 : 无 | |
返 回 值 : 无 | |
*************************************************/ | |
int main( void ) | |
{ | |
#ifdef DEBUG | |
debug(); | |
#endif | |
BaseType_t xReturn = pdPASS; /* 定义一个创建信息返回值,默认为 pdPASS */ | |
prvSetupHardware(); | |
fault_test_by_div0(); | |
/* Start the tasks defined within this file/specific to this demo. */ | |
xReturn = xTaskCreate( (TaskFunction_t)prvUser_Task, /* 任务入口函数 */ | |
(const char *)"prvUser_Task", /* 任务名字 */ | |
(uint16_t)configMINIMAL_STACK_SIZE, /* 任务栈大小 */ | |
(void *)NULL, /* 任务入口函数参数 */ | |
(UBaseType_t)mainCREATOR_TASK_PRIORITY, /* 任务的优先级 */ | |
(TaskHandle_t *)UserTaskCreate_Handle ); /* 任务控制块指针 */ | |
if(pdPASS == xReturn) { | |
/* Start the scheduler. */ | |
vTaskStartScheduler(); | |
} | |
/* Will only get here if there was not enough heap space to create the | |
idle task. */ | |
return 0; | |
} | |
/*----------------------------- End -----------------------------*/ |
接着可以观察到程序已经跑到 HardFault_Handler 里并输出相应信息了:
然后利用 addr2line,执行命令 arm-none-eabi-addr2line.exe -e "可执行映像" -a -f "相应的值"
(注:实际命令并不需要双引号 “ ”
,应用可看下图):
-e
:指定可执行映像名称-a
:显示函数地址-f
:显示函数名称
然后,对于不同的编译器,可执行映像并不一样(像 Keil 是 .axf
、IAR 是 .out
);最后,如果细心的可以看到命令后面的两个值只有 PC 的值是对应上,第二个值并不是 LR 的值,而是其减去 4 的结果;若果你直接使用 LR 值,你会发现定位出来的下一条指令要执行的地方,至于为什么要减掉 4,是因为在 ARM 下执行指令的大小始终为 4 个字节递增,所以可以通过减掉 4,使其指向上一条内容。
# 方法四:透过最底层进行分析
这种方法不需要修改什么,在进入 HardFault_Handler 后直接分析核心寄存器。
同样的,还是先制造一个 HardFault,沿用方法三的 fault_test_by_div0 (); 函数代码,然后直到进入 HardFault_Handler 后,(注意,这里的 HardFault_Handler 中断函数并没有像方法三那样区嵌入代码,而是保持其原始的样子):
接着跟上面的一样,获取各核心寄存器的值(ps:这里用的仿真查看):
在这里就不是像方法三那样直接拿 LR 和 PC 这两个值来用了,必须进行层层分析:
查看 LR 值是对应下图的哪个:
通过转换,-7 为 0xFFFFFFF9,然后在第四大点的第三小点有讲过 LR=0xFFFF_FFF9 时是使用 MSP,而 LR=0xFFFF_FFFD 时则使用 PSP;所以我们可以确定该返回值是被压入到主堆栈指针(MSP)中。
找到 MSP 指示的值:
得到该值为 0x2002ffc8。
查看该值所对应的内存块:
得到紫色框里的两个数据(嘿嘿,是不是很神奇,居然跟方法三的 LR、PC 值一样),可能你会疑惑,为什么是提取该地址往后第六、七个数(4 字节递增),而不是其他呢?这就看第五个大点了,它的入栈顺序以及地址位置都是有规律的,可以归纳为下图:
按箭头方向,顺数第六、七是不是就是 LR、PC 值啊,而这里就是旧的内容信息;所以跟方法三比较一下,其实方法三的那段嵌入代码,就是让其改变各个核心寄存器的信息内容,使其不要存储跳转到 HardFault_Handler,而是存储跳转前的数据信息。
到了这步,就是使用 addr2line 来定位,这里就不说了,方法三里面有写。
# 参考:
ARM 架构
ARM Cortex-M
Arm® Cortex®-M4 in a nutshell
<<ARM Cortex-M3 权威指南>>
ARMv7-M Architecture Reference Manual
Overview of the ARM Architecture
error: Hard Fault Handler
How to debug a HardFault on an ARM Cortex-M MCU
Debugging a ARM Cortex-M Hard Fault
Analyzing HardFaults on Cortex-M CPU
ARM Cortex M3: Recovering from a Hard Fault
Debug a HardFault