Cortex-M HardFault问题定位以及CmBacktrace分析

 

使用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

 

 

posted @ 2020-04-06 13:49  蓝色雨only  阅读(1616)  评论(0编辑  收藏  举报