使用Cortex-M系列MCU开发程序时不可避免的会遇到HardFault问题,常用的方法由HardFault_S.s和HardFault_C.c两个文件组成,代码分别如下:
/* Assembly file for gcc */ .text .syntax unified .thumb .type HardFault_Handler, %function .global HardFault_Handler .global HardFault_Handler_c /* 此处需要补充对MSP有效性的检查,以防止进入Lockup */ HardFault_Handler: tst lr, #4 ite eq mrseq r0, msp /* stacking was using MSP */ mrseq r0, psp /* stacking was using PSP */ mov r1, lr /* second parameter */ ldr r2,=HardFault_Handler_c bx r2 .end
// Second part of the HardFault handler in C void HardFault_Handler_C(unsigned long * hardfault_args, unsigned int lr_value) { unsigned long stacked_r0; unsigned long stacked_r1; unsigned long stacked_r2; unsigned long stacked_r3; unsigned long stacked_r12; unsigned long stacked_lr; unsigned long stacked_pc; unsigned long stacked_psr; unsigned long cfsr; unsigned long bus_fault_address; unsigned long memmanage_fault_address; bus_fault_address = SCB->BFAR; memmanage_fault_address = SCB->MMFAR; cfsr = SCB->CFSR; 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 ("[HardFault]\n"); printf ("- Stack frame:\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); printf ("- FSR/FAR:\n"); printf (" CFSR = %x\n", cfsr); printf (" HFSR = %x\n", SCB->HFSR); printf (" DFSR = %x\n", SCB->DFSR); printf (" AFSR = %x\n", SCB->AFSR); if (cfsr & 0x0080) printf (" MMFAR = %x\n", memmanage_fault_address); if (cfsr & 0x8000) printf (" BFAR = %x\n", bus_fault_address); printf ("- Misc\n"); printf (" LR/EXC_RETURN= %x\n", lr_value); while(1); // endless loop }
汇编文件中的HardFault_Handler判断出错前使用的是MSP还是PSP,之后调用C语言编写的HardFault_Handler_C处理函数,在其中打印输出内核寄存器以及相关Fault Status寄存器,
之后根据打印出来的PC和LR在反汇编程序中定位在具体位置和具体原因。
当项目复杂度增加时,需要知道出错时整个函数调用顺序以加速定位问题,并且每次都手动去反汇编文件中查找效率也比较低。github上的CmBacktrace开源软件可有效解决此问题,
CmBacktrace由RT-Thread的大佬开源,具体思路与上述描述的思路差不多,但是增加堆栈调用的追溯,类似与GDB中bt的功能。
其实现追溯堆栈的功能的核心代码如下:
/* copy called function address */ for (; sp < stack_start_addr + stack_size; sp += sizeof(size_t)) { /* the *sp value may be LR, so need decrease a word to PC */ pc = *((uint32_t *) sp) - sizeof(size_t); /* the Cortex-M using thumb instruction, so the pc must be an odd number */ if (pc % 2 == 0) { continue; } /* 此处应先判断是BL还是BLX */ /* fix the PC address in thumb mode */ pc = *((uint32_t *) sp) - 1; if ((pc >= code_start_addr) && (pc <= code_start_addr + code_size) && (depth < CMB_CALL_STACK_MAX_DEPTH) /* check the the instruction before PC address is 'BL' or 'BLX' */ && disassembly_ins_is_bl_blx(pc - sizeof(size_t)) && (depth < size)) { /* the second depth function may be already saved, so need ignore repeat */ if ((depth == 2) && regs_saved_lr_is_valid && (pc == buffer[1])) { continue; } buffer[depth++] = pc; } }
/* check the disassembly instruction is 'BL' or 'BLX' */ static bool disassembly_ins_is_bl_blx(uint32_t addr) { uint16_t ins1 = *((uint16_t *)addr); uint16_t ins2 = *((uint16_t *)(addr + 2)); #define BL_INS_MASK 0xF800 #define BL_INS_HIGH 0xF800 #define BL_INS_LOW 0xF000 #define BLX_INX_MASK 0xFF00 #define BLX_INX 0x4700 if ((ins2 & BL_INS_MASK) == BL_INS_HIGH && (ins1 & BL_INS_MASK) == BL_INS_LOW) { return true; } else if ((ins2 & BLX_INX_MASK) == BLX_INX) { return true; } else { return false; } }
for循环解析整个栈空间的数据(从当前栈指针的位置一直到栈底),之后要完全理解循环体的代码,需要了解Cortex-M内部的一些机制具体如下:
a.Thumb指令是16bit的,之后又扩展了32bit的指令
b.Cortex-M系列只支持Thumb状态,不支持ARM状态,这个特点决定LR寄存器在保存函数调用的返回地址时,地址的bit0必须是1
c.函数调用由BL或BLX指令实现,其指令编码如下:
有了以上这些知识可以去分析循环体部分的代码了。
对堆栈中的数据首先判断奇偶,是偶数直接跳过 ,返回地址必须是奇数
判断是否在代码段的地址空间内 ,链接时确定text段的放置位置
根据堆栈中记录的返回地址是发生跳转指令的下一条指令,因为BL是32bit指令,BLX是16bit指令,所以需要分别判断。
根据PC值取出此处的指令,根据指令编码判断是否为BL或BLX指令。
将所有条件均满足的数据存储,以供后续输出。
之后根据add2line命令可以轻易的知道函数的调用顺序。
基本原理就是这样。
以上是自己对于HardFault处理方法和CmBacktrace的理解,可能有错误,如发现请及时指出,互相学习,共同进步。
0412:
之前理解PC-4时以为是三级流水线的原因,其实不是,发生函数调用时会flush指令流水线,所以正确的理解应该是返回地址的前一条指令。
2020/11/19
今天突然想到cmbacktrace的有时候打印函数调用关系不准确的原因:
1.调用栈空间没有被破坏
2.函数返回退栈的过程中并不会清除已使用的内存,导致无差别扫描栈空间有概率误解析
相关资源:
1.book:The Definitive Guide to Arm® Cortex®-M3 and Cortex®-M4 Processors-Newnes (2014)-Joseph Yiu
2.book:DDI0403E_c_armv7m_arm
3.https://github.com/armink/CmBacktrace