发新帖本帖赏金 100.00元(功能说明)我要提问
返回列表
打印
[APM32F4]

栈回溯方法自动分析定位APM32 Hardfault错误

[复制链接]
10722|5
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 luobeihai 于 2023-9-30 23:34 编辑

#申请原创# @21小跑堂

1. 前言
在调试代码的时候,我们时不时就会遇到 Hard Fault 错误,导致程序崩溃。

对于 Cortex-M3/M4 内核来说,通常我们都知道可以通过内核的 Hard Fault 故障状态寄存器的值来进行分析具体的原因,以及定位故障代码的地址,和分析代码调用关系。

通常的方法就是使用仿真器,然后观察进入 Hard Fault 中断时,CPU内部的寄存器,然后手动分析(单步调试、观察内存地址值、函数调用栈等),一步步定位代码的错误位置,还原函数调用的逻辑关系。
手动分析过程繁琐,我们可以使用栈回溯原理,把进入 Hard Fault 中断前一刻的CPU内部寄存器保存下来,然后自动回溯栈内容,把进入 Hard Fault 之前的函数调用关系分析出来,并且根据故障状态寄存器进行故障结果诊断。

下面是使用栈回溯方法自动分析定位 Hard Fault 错误。主要实现的功能有:
  • 1、保存发生错误瞬间的CPU寄存器
  • 2、自动诊断 Hard Fault 故障原因
  • 3、自动回溯进入 Hard Fault 中断之前的函数调用关系(当然要精确定位到哪个文件、哪一行需要借助gcc工具链)

对于所有 Cortex-M3/M4 内核的 MCU ,该代码都是适用的。下面以 APM32F411 为例进行分析。

2. 栈回溯分析的原理

2.1 函数调用时压栈
函数调用,本质就是一条跳转指令,对应的汇编就是 BL / BLX 指令。
对于BL指令,该指令会自动保存下一条指令的地址到LR寄存器,然后再对LR寄存器进行压栈,当函数返回时,就把LR寄存器的值,赋值给PC,这样就可以返回执行下一条指令了。
比如说,我在main函数调用了test_debug函数:
main()
{
    test_debug();
}

下面是test_debug函数的反汇编代码:
可以看到,进入该函数后,首先会把lr、还有一些其他如果会在该函数用到的CPU内部寄存器进行压栈。
那么,当有多个函数调用关系时,比如 func3 -> func2 -> func1 的调用关系,栈的具体排布如下:

2.2 中断/异常处理过程压栈
函数在调用过程中,为了保存跳转到另一个函数前那一瞬间,CPU内部的寄存器值不被另一个函数破坏,会先进行压栈,当函数返回时,再把保存在栈中的值恢复回CPU内部。

对于 Cortex-M 内核的 MCU ,对异常/中断的处理过程,也是类似的。CPU在跳转到异常/中断程序之前,也需要把这些不**被中断程序所改变的CPU内部寄存器的值保存到栈中。对于 ARM 架构来说,调用过程遵循  ATPCS (ARM-Thumb Produce Call Standard),ARM-Thumb过程调用标准。

该标准规定了硬件会自动保存CPU内部哪些寄存器的值,以及这些寄存器的压栈顺序。根据这份标准,R0-R3, R12, R14(SP), PC, PSR 这8个寄存器的值,是Cortex-M架构硬件决定的,跳到异常处理函数之前会自动进行压栈处理的,而且压栈顺序固定。

但是对于 R4-R11 这几个寄存器,称为 “被调用者保存的寄存器”(对于中断而言,那就是中断服务函数是被调用者),也就是说这几个寄存器是被调用的子程序需要确保在执行该函数时确保数值不会发生变化。所以这几个寄存器是不会硬件自动压栈的。如果我们想要保存R0-R11,那么就需要我们自己写代码手动压栈。
下图就是 ATPCS 标准规定的,函数调用过程中寄存器的使用情况:
那么根据这一份标准,我们要保存进入异常瞬间时,CPU内部所有寄存器的值,就只需要软件对 R4-R11 寄存器进行压栈就行,其余的寄存器是硬件会自动进行压栈的。
最终,中断/异常处理过程的压栈情况是:
我们根据异常处理的压栈情况,就可以获取到进入 Hard Fault 异常的瞬间,CPU内部的所有寄存器值。
然后再根据其他函数的压栈情况,根据函数执行完后的返回地址,就可以自动分析出函数的调用关系了。

3. 保存发生 Hard Fault 瞬间的寄存器
根据前面的中断/异常处理过程压栈分析,我们要保存发生 Hard Fault 瞬间的CPU内部所有寄存器信息,只需要获取到 MSP 的地址值,然后去读取该地址的内容即可。
3.1 修改 HardFault_Handler 中断处理函数
HardFault_Handler 中断处理函数,在 .s 文件会有一个默认的弱定义汇编函数,我们需要更改该汇编函数。主要要实现的功能是:
  • 1、软件压栈保存R4-R11、EXC_RETURN的值
  • 2、获取到 MSP/PSP 的值(可根据异常返回值 EXC_RETURN bit2位进行判断,使用的是MSP还是PSP栈)
  • 3、把MSP/PSP的值作为函数参数,跳转执行最终的C处理代码

具体代码如下:
    IMPORT hard_fault_handler_c
    EXPORT HardFault_Handler
HardFault_Handler    PROC

    ; get sp to r0
    TST     lr, #0x04               ; if(!EXC_RETURN[2])
    ITE     EQ
    MRSEQ   r0, msp                 ; [2]=0 ==> Z=1, get fault context from handler.
    MRSNE   r0, psp                 ; [2]=1 ==> Z=0, get fault context from thread.

    ; softwore save r4-r11, lr
    STMFD   r0!, {r4 - r11}         ; push r4 - r11 register
    STMFD   r0!, {lr}               ; push exec_return register

    ; update sp register
    TST     lr, #0x04               ; if(!EXC_RETURN[2])
    ITE     EQ
    MSREQ   msp, r0                 ; [2]=0 ==> Z=1, update stack pointer to MSP.
    MSRNE   psp, r0                 ; [2]=1 ==> Z=0, update stack pointer to PSP.

    ; execute hard fault handler
    BL      hard_fault_handler_c

JumpToMyself
    B      JumpToMyself             ; while(1)
    ENDP

代码分析:
1、该汇编代码第一部分,进来就先判断使用的是MSP(主栈)还是PSP(线程栈),一般来说如果是裸机程序,使用的都是MSP。如果使用了RTOS,而且异常是发生在RTOS启动之后,那么使用的就是PSP。然后把栈指针的值赋值给R0寄存器。
2、第二部分,就是软件压栈,软件把R4-R11、EXC_RETURN的值保存到栈中。
3、第三部分。因为软件压栈过程中,栈会向下自动生长的,对于上面的代码 R0 的值就是软件压栈之后,SP指针应该要要位于的位置。所以我们需要把 R0 的值更新到MSP/PSP中。
4、做完这些之后,就执行BL指令,跳转到具体的 C 处理函数 hard_fault_handler_c 。其中R0的值,其实就是目前的SP的值。
hard_fault_handler_c 函数原型是:void hard_fault_handler_c(struct exception_info *exception_info);
最后跳转到这个函数运行,传递给这个函数的参数就是R0的值,也就是MSP的指针,然后我们根据这个地址值以及前面分析的栈的排布顺序,我们定义的结构体类型如下:

struct exception_info
{
    uint32_t exc_return;
    uint32_t r4;
    uint32_t r5;
    uint32_t r6;
    uint32_t r7;
    uint32_t r8;
    uint32_t r9;
    uint32_t r10;
    uint32_t r11;
    uint32_t r0;
    uint32_t r1;
    uint32_t r2;
    uint32_t r3;
    uint32_t r12;
    uint32_t lr;
    uint32_t pc;
    uint32_t psr;
};

这个结构体的成员排布顺序,就是根据中断/异常处理过程压栈的顺序定义的,然后我们又知道了这个结构体的变量又是指向MSP的,这样就可以打印出所有的CPU寄存器值了。

4. 自动诊断 Hard Fault 故障原因

4.1 故障状态寄存器
对于 Cortex-M3/M4 内核来说,内核定义了 Hard Fault 故障状态寄存器,根据这些故障状态寄存器的值,我们就可以分析出产生 Hard Fault 的故障原因。下面是参考Cortex-M3/M4权威指南这本书,关于Hard Fault 故障状态寄存器的定义。
主要有可配置故障状态寄存器、硬件错误状态寄存器、调试错误状态寄存器。其中配置故障状态寄存器又划分为3类,分别占用不同的字节空间。如下图:
对于最低字节,是内存管理错误状态;第二字节是总线错误状态;最高两字节是用法错误状态。
总之,我们根据这些故障状态寄存器,就可以分析出产生 Hard Fault 的具体原因了。

4.2 寄存器的封装
根据手册,把这些寄存器的位域进行封装起来,以供我们代码调用分析。
/**
* Cortex-M3/M4 Registers for Fault Status and Address Information
*/
typedef struct hard_fault_regs {
    union {
        volatile uint8_t value;
        struct {
            volatile uint8_t IACCVIOL    : 1;     // Instruction access violation
            volatile uint8_t DACCVIOL    : 1;     // Data access violation
            volatile uint8_t Reserved1   : 1;
            volatile uint8_t MUNSTKERR   : 1;     // Unstacking error
            volatile uint8_t MSTKERR     : 1;     // Stacking error
            volatile uint8_t MLSPERR     : 1;     // Floating point lazy stacking error
            volatile uint8_t Reserved2   : 1;
            volatile uint8_t MMARVALID   : 1;     // Indicates the MMAR is valid
        } bits;
    } CFSR_MFSR;  // Memory Management Fault Status Register (0xE000ED28)

    union {
        volatile uint8_t value;
        struct {
            volatile uint8_t IBUSERR    : 1;      // Instruction access error
            volatile uint8_t PRECISERR  : 1;      // Precise data access error
            volatile uint8_t IMPREISERR : 1;      // Imprecise data access error
            volatile uint8_t UNSTKERR   : 1;      // Unstacking error
            volatile uint8_t STKERR     : 1;      // Stacking error
            volatile uint8_t LSPERR     : 1;      // Floating point lazy stacking error
            volatile uint8_t Reserved   : 1;
            volatile uint8_t BFARVALID  : 1;      // Indicates BFAR is valid
        } bits;
    } CFSR_BFSR;   // Bus Fault Status Register (0xE000ED29)

    union {
        volatile uint16_t value;
        struct {
            volatile uint16_t UNDEFINSTR : 1;     // Attempts to execute an undefined instruction
            volatile uint16_t INVSTATE   : 1;     // Attempts to switch to an invalid state (e.g., ARM)
            volatile uint16_t INVPC      : 1;     // Attempts to do an exception with a bad value in the EXC_RETURN number
            volatile uint16_t NOCP       : 1;     // Attempts to execute a coprocessor instruction
            volatile uint16_t Reserved   : 4;
            volatile uint16_t UNALIGNED  : 1;     // Indicates that an unaligned access fault has taken place
            volatile uint16_t DIVBYZERO0 : 1;     // Indicates a divide by zero has taken place (can be set only if DIV_0_TRP is set)
        } bits;
    } CFSR_UFSR;  // Usage Fault Status Register (0xE000ED2A)

    union {
        volatile uint32_t value;
        struct {
            volatile uint32_t Reserved1  : 1;
            volatile uint32_t VECTBL     : 1;      // Indicates hard fault is caused by failed vector fetch
            volatile uint32_t Reserved2  : 28;
            volatile uint32_t FORCED     : 1;      // Indicates hard fault is taken because of bus fault/memory management fault/usage fault
            volatile uint32_t DEBUGEVT   : 1;      // Indicates hard fault is triggered by debug event
        } bits;
    } HFSR;  // Hard Fault Status Register (0xE000ED2C)

    union {
        volatile uint32_t value;
        struct {
            volatile uint32_t HALTED   : 1;         // The processor is halted is by debugger request (including single step)
            volatile uint32_t BKPT     : 1;         // The debug event is caused by a breakpoint
            volatile uint32_t DWTTRAP  : 1;         // The debug event is caused by a watchpoint
            volatile uint32_t VCATCH   : 1;         // The debug event is caused by a vector catch
            volatile uint32_t EXTERNAL : 1;         // The debug event is caused by an external signal
            volatile uint32_t Reserved : 27;
        } bits;
    } DFSR;  // Debug Fault Status Register (0xE000ED30)

    volatile uint32_t MMAR;    // Memory Management Fault Address Register (0xE000ED34)
    volatile uint32_t BFAR;    // Bus Fault Manage Address Register (0xE000ED38)
    volatile uint32_t AFSR;    // Auxiliary Fault Status Register (0xE000ED3C), Vendor controlled
} hard_fault_regs_t;

/* hard fault registers base address */
#define HARD_FAULT_REGS                    ((hard_fault_regs_t *) 0xE000ED28)

这些故障状态寄存器的基地址是 0xE000ED28 ,所以我们最后定义 HARD_FAULT_REGS 这个宏,指向该地址,就可以读取到全部的寄存器值了。然后调用下面的函数,自动诊断 Hard Fault 故障原因。
static void hard_fault_diagnosis(void)
{
    if (HARD_FAULT_REGS->HFSR.bits.VECTBL)
    {
        printf("Hard fault is caused by failed vector fetch.\r\n");
    }
   
    /* bus fault/memory management fault/usage fault */
    if (HARD_FAULT_REGS->HFSR.bits.FORCED)
    {
        /* Memory Management Fault */
        if (HARD_FAULT_REGS->CFSR_MFSR.value)
        {
            memory_management_fault_diagnosis();
        }
        
        /* Bus Fault */
        if (HARD_FAULT_REGS->CFSR_BFSR.value)
        {
            bus_fault_diagnosis();
        }
        
        /* Usage Fault */
        if (HARD_FAULT_REGS->CFSR_UFSR.value)
        {
            uasge_fault_diagnosis();
        }
    }
   
    /* debug fault */
    if (HARD_FAULT_REGS->HFSR.bits.DEBUGEVT)
    {
        debug_fault_diagnosis();
    }
}

5. 自动回溯错误现场函数调用关系
当发生 Hard Fault 异常时,我们还想知道进入 Hard Fault 之前的函数调用关系。

根据前面的栈回溯原理分析,只要我们知道了 SP 指针之后,我们就可以获取到整个栈的内容了,而且每个函数的栈内容,第一条都是这个函数执行完之后的返回地址。只要我们知道了这个地址值,那么我们就可以找到进入异常之前的函数调用关系了。

下面以一部分汇编代码的调用过程来说明会更加清晰明了。下图是 main 函数的反汇编代码:

上面的main函数,当想要在main函数调用 test_debg 函数时,首先会把下一条指令的地址保存到栈里面,也就是 0x0800142e 这个地址值(当然,Cortex-M架构使用的是 ARM Thumb 指令集,所以实际保存的值会 + 1)。而这个地址值的前面一条指令,就是 BL 指令,即跳转到 test_debg 函数。这样我们根据BL指令,就可以找到所有的函数调用关系了。

也就是说,栈里面保存的函数执行完之后的返回地址值,是最关键所在。但是主要的问题就是我们如何在整片栈里面把这个返回地址值给挑出来?

我们从上面的汇编代码观察,这个值必定满足下面的几个条件:
  • 1、一定是位于可执行域的地址区间。对于APM32F411就是0x08000000开头的地址
  • 2、因为 Cortex-M架构使用的是 ARM Thumb 指令集,那么这个返回地址值,它的 bit0 位必定是1
  • 3、这个返回地址值的前面,所保存的一条指令,它必定是一条跳转指令(BL/BLX)。也就是我们读取返回地址的前面一个地址值(返回地址 - 1 - 4)的Flash内容,它的指令码肯定是满足 BL/BLX 指令格式的。

根据上面的这几个条件,我们就可以在整片栈的内容中,挑出跳转指令的地址值了。然后根据跳转指令,我们就可以回溯函数的调用关系了。
具体代码如下:
/* analysis call stack information */
static void backtrace_call_stack_info(struct exception_info *exception_info)
{
    /* Stack base and end address and length */
    extern const int * STACK$Base;
    extern const int * STACK$Limit;
    extern const int * STACK$Length;
   
    /* .tetx section base address and length */
    extern int * Image$ER_IROM1$Base;
    extern int * Image$ER_IROM1$Length;

    uint32_t lr;
    uint32_t pc;
    uint32_t sp = (uint32_t)(exception_info + 1);
   
    uint32_t stack_len = (uint32_t)&STACK$Length;
   
    uint32_t code_start = (uint32_t)&Image$ER_IROM1$Base;
    uint32_t code_len = (uint32_t)&Image$ER_IROM1$Length;

    printf("You can use the command to get more callback information: \r\n");
    printf("arm-none-eabi-addr2line -e yourself.axf -a -f ");
    printf("%08x ", exception_info->pc);
    printf("%08x ", exception_info->lr - 1 - 4);
   
    for ( ; sp < (uint32_t)(exception_info + 1) + stack_len; sp += 4 )
    {
        /* the first value is lr register */
        lr = *((uint32_t *) sp);
        
        /*  the Cortex-M using thumb instruction, so lr register bit0 must is 1 */
        if ((lr & 0x01) != 0x01)
        {
            continue;
        }
        
        /* get lr previous a value */
        pc = lr - 1 - 4;

        /* use this value as the address to read flash, the value read must be a BL/BLX instruction */
        if ((pc >= code_start) && (pc < code_start + code_len) && \
            disassembly_ins_is_bl_blx(pc))
        {
            printf("%08x ", pc);
        }
    }

    printf("\r\n");
}

上述代码,会使用到 MDK-Keil 链接器定义的一些符号,通过这些符号去获取到可执行域的代码地址和大小,还有栈的起始地址和大小。
其中最关键的部分就是for循环,它会遍历整个栈空间,然后把BL/BLX指令的地址值给找出来。
找出地址值之后,根据这个地址值然后通过反汇编文件,我们可以了解到函数的调用关系了。但是如果不想看反汇编文件,就想知道在哪个文件、哪一行调用的,就需要借助 gcc 的编译工具链其中的一个工具,arm-none-eabi-addr2line 。使用这个工具执行下面的命令,就可以打印出那个文件,哪一行调用了哪个函数了。
arm-none-eabi-addr2line -e yourself.axf -a -f xxx xxx xxx

其中 yourself.axf 文件的名字,是你自己工程编译出来的文件名(IAR平台是xxx.out格式文件)。xxx 后面指的就是我们代码找出来的跳转指令的地址值。

6. 使用示例
1、 制造 Hard Fault 错误
我们故意插入一段制造 Hard Fault 错误的代码,然后让程序进入 Hard Fault 异常,去执行我们的 Hard Fault 分析代码。
/* test function_1 divide by 0 error */
void test_func1(void)
{
    int a, b, c;

    a = 10;
    b = 0;
   
    /* Divide by 0 error */
    c =  a / b;
   
    /* dummy */
    (void) c;
}

void test_debug(void)
{
    /* SCB->CCR address 0xE000ED14 */
    volatile int *SCB_CCR = (volatile int *)0xE000ED14;
    /* SCB->CCR bit4 : DIV_0_TRP */
    *SCB_CCR |= (1 << 4);   // open divide by 0 error

    test_func1();
}

在 test_func1 函数中,执行一条除0运算,这样会触发CPU的除0错误。

2、观察打印CPU寄存器的信息,错误诊断信息等。
执行了上述代码之后,在串口终端打印的信息如下:

3、使用 arm-none-eabi-addr2line 工具,去获取详细的函数调用信息。
arm-none-eabi-addr2line 根据属于gcc工具链的其中一个工具,可以去官网下载这个工具就行。gun的官网我打不开,然后我是从下面的网站下载的。

https://launchpad.net/gcc-arm-embedded/5.0/5-2016-q3-update

然后,我们打开windows命令行窗口,然后输入下面命令:
arm-none-eabi-addr2line -e stack_backtrace_hardfault.axf -a -f 080015e8 080015d8 0800142a 08000278

可以看到打印出了详细的函数调用过程,而且最后调用的 test_func1 函数,是运行了该函数的第66行,就导致了 Hard Fault 异常。
下面是工程源码。

APM32F411_Backstack_Hardfault.zip (945.8 KB)






使用特权

评论回复

打赏榜单

21小跑堂 打赏了 100.00 元 2023-10-11
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2023-10-11 16:57 回复TA
提供一种详细的Hardfault错误定位方法,从原理到实例逐步讲解,实用性较好,结构完整紧凑,优质原创! 
沙发
76290391| | 2023-10-10 14:31 | 只看该作者
6666666666666a

使用特权

评论回复
板凳
publicpeople| | 2023-10-17 09:01 | 只看该作者
高手确实厉害。

使用特权

评论回复
地板
HORSE7812| | 2023-10-20 11:12 | 只看该作者
确实牛X,虽然没能看懂。

使用特权

评论回复
5
19960206| | 2024-1-18 09:20 | 只看该作者
大佬厉害呀

使用特权

评论回复
发新帖 本帖赏金 100.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

11

主题

50

帖子

2

粉丝