目录

前言

问题描述

一.HardFault_Handler修改

1.1 原HardFault_Handler

1.2 重写HardFault_Handle

1.3 修改完成后

二. 复现致命错误

2.1 复现错误打印

2.2 错误信息细分解读

2.3 大致问题分析

三. 原因调试

3.1 可能的原因排序:

四. 排查与修复步骤

4.1 立即增加地址有效性检查(防御性编程)

4.2 检查 simulate_phone() 中所有指针类型转换

4.3 检查数组边界

4.4 添加 当前主栈指针(MSP)的值

4.4.1 这里截取部分指令代码

4.4.2 第一次执行没有问题:栈的空间已经返回

4.4.3 第二次出现问题

五. 真正崩溃原因:数据总线错误 – 非法地址访问

5.1 原始打印hex2Str()函数

5.2 解决方法:

1. 添加边界检查即可

2. 更安全的方案:使用调用者提供的缓冲区


        最后,定位到和解决了问题:问题为hex2Str()函数二维数组(char hexStr[4][128])缓冲区溢出问题;这个函数通过hexStr[0-3]的下标,循环存储共四次回复数据;这个偶发性问题特别有意思,每次循环到下标hex[3]进行存储时特别容易出问题;所有4的倍数次(即每次循环到hexStr[3]的下标进行存储时),存储溢出少量数据就会直接触发:HardFault_Handler。

        后续的HardFault_Handler的重写函数寻找问题和定位过程如下;

前言

        在 STM32 等 Cortex-M 工程中,Error_Handler 和 HardFault_Handler 是系统启动文件中定义的异常处理函数。默认实现通常是无限循环,一旦进入,程序将永久停在那里,不再执行任何应用代码,表现为死机、无响应。

        但 HardFault 时可能串口还能用(取决于错误是否破坏了关键外设),不过 Cortex-M 在进入 HardFault 时通常不会禁用串口,可以通过寄存器直接读 PC、LR

更好的是借助 Keil/IAR IDE 的 Fault 分析窗口 或 在线调试(Debug) 直接查看异常栈。

Handler 触发条件 后果
HardFault_Handler 所有无法被其他异常处理的硬件错误,例如:非法内存访问、总线错误、未对齐访问、非法指令等。 CPU 进入 HardFault 异常,执行该函数。默认实现是 while(1) 死循环,程序卡死,所有外设保持进入异常前的状态,中断可能不再响应。
Error_Handler                 这是 HAL 库用户自定义的错误回调,通常在 HAL 初始化或操作出错时被调用(例如 HAL_Init() 失败、时钟配置错误)。 与 HardFault 一样,默认是死循环,程序停住。

我这里使用uart调试输出HardFault_Handler,这样写文章更直观些;

问题描述

在写(STM32<->ST25R3911B)非接触功能业务分支函数中,自己测试新增的sim_phone()函数(作用:与nfca卡片进行数据交互)重复执行时前3次没问题,第四次必定出现问题,不接收nfc回复场景就不会有任何问题,这里while(1)循环多次测试,

可以看到,这里报错就只有HardFault_Handler提示,具体什么什么原因导致的完全不清楚

一.HardFault_Handler修改

1.1 原HardFault_Handler

HardFault_Handler当前代码定义为:printf输出一段打印,非常不直观;需要输出有效详细,知道大概是什么原因导致的HardFault_Handler,所以重写这个函数

1.2 重写HardFault_Handle

 只需要检测SCB->CFSR;这个结构体成员的不同标志位,即可看到是大致是哪里的问题

如:问题(异常返回时出栈错误;异常进入时入栈错误;指令总线错误等)

读 ARM 内核的 System Control Block (SCB) 寄存器

寄存器 寄存器
CFSR (Configurable Fault Status) 最常用! 32 位分三段:MMFSR(内存管理)、BFSR(总线)、UFSR(用法)。每一位对应一种具体错误
HFSR (Hard Fault Status) 看是不是由于其他故障升级上来的(如 DEBUGEVT、FORCED)
BFAR (Bus Fault Address) 如果报总线错误,这里存的是触发故障的具体内存地址
MMFAR (MemManage Fault Address) 如果报内存管理错误(MPU 越界),这里存违规地址

传入的 pulFaultStackAddress 就是硬件压栈完成后的栈顶位置

Cortex-M3 的硬件压栈顺序是固定死的(从高地址到低地址):

寄存器 死机排查作用
PC 最关键! 触发 HardFault 的那条指令的地址。对着反汇编 .map 文件找,就知道死在哪一行代码
LR 谁调用的? 根据这个地址可以回溯调用栈,找到上层函数
R0-R3 如果死机前正在传参数或计算,这些值能告诉你当时处理的数据
xPSR 看 IPSR 字段知道当前中断号,看 APSR 知道标志位状态

/**
  * @brief This function handles Hard fault interrupt.
  */

 __asm void HardFault_Handler(void)
{
    IMPORT prvGetRegistersFromStack
    
    TST     LR, #4                  ; 检查异常返回使用MSP还是PSP
    ITE     EQ
    MRSEQ   R0, MSP                 ; 线程模式使用主栈指针
    MRSNE   R0, PSP                 ; 处理模式使用进程栈指针
    B       prvGetRegistersFromStack ; 跳转到C函数处理
}

/**
 * @brief  解析异常栈帧并打印详细错误信息
 * @param  pulFaultStackAddress: 栈帧起始地址
 */
void prvGetRegistersFromStack(uint32_t *pulFaultStackAddress)
{
    // Cortex-M3 异常栈帧固定顺序(硬件自动压栈)
    uint32_t r0  = pulFaultStackAddress[0];
    uint32_t r1  = pulFaultStackAddress[1];
    uint32_t r2  = pulFaultStackAddress[2];
    uint32_t r3  = pulFaultStackAddress[3];
    uint32_t r12 = pulFaultStackAddress[4];
    uint32_t lr  = pulFaultStackAddress[5];  // 崩溃前最后一次函数调用的返回地址
    uint32_t pc  = pulFaultStackAddress[6];  // 崩溃时正在执行的指令地址
    uint32_t psr = pulFaultStackAddress[7];  // 程序状态寄存器

    // 打印错误头部
    printf("\r\n\r\n");
    printf("=============================================\r\n");
    printf("          HARD FAULT OCCURRED!               \r\n");
    printf("=============================================\r\n");
    printf("System halted.\r\n\r\n");

    // 核心寄存器信息
    printf("=== Core Registers ===\r\n");
    printf("PC (Fault Address): 0x%08X\r\n", pc);
    printf("LR (Return Address): 0x%08X\r\n", lr);
    printf("PSR: 0x%08X\r\n", psr);
    printf("R0: 0x%08X, R1: 0x%08X, R2: 0x%08X, R3: 0x%08X\r\n", r0, r1, r2, r3);
    printf("R12: 0x%08X\r\n", r12);

    // 故障状态寄存器(最关键的诊断信息)
    printf("\r\n=== Fault Status Registers ===\r\n");
    printf("CFSR: 0x%08X\r\n", SCB->CFSR);
    printf("HFSR: 0x%08X\r\n", SCB->HFSR);
    printf("BFAR: 0x%08X\r\n", SCB->BFAR);
    printf("MMFAR: 0x%08X\r\n", SCB->MMFAR);

    // 错误类型自动解析(仅保留STM32F103支持的位)
    printf("\r\n=== Error Details ===\r\n");
    uint32_t cfsr = SCB->CFSR;

    // 内存管理故障位
    if(cfsr & (1 << 0)) printf("✗ 指令访问违规(执行了不存在的内存地址)\r\n");
    if(cfsr & (1 << 1)) printf("✗ 数据访问违规(读写了不存在的内存地址)\r\n");
    if(cfsr & (1 << 3)) printf("✗ 异常返回时出栈错误\r\n");
    if(cfsr & (1 << 4)) printf("✗ 异常进入时入栈错误\r\n");

    // 总线故障位
    if(cfsr & (1 << 8))  printf("✗ 指令总线错误\r\n");
    if(cfsr & (1 << 9))  printf("✗ 精确数据总线错误(错误地址在BFAR中)\r\n");
    if(cfsr & (1 << 10)) printf("✗ 不精确数据总线错误\r\n");
    if(cfsr & (1 << 11)) printf("✗ 异常返回时出栈总线错误\r\n");
    if(cfsr & (1 << 12)) printf("✗ 异常进入时入栈总线错误\r\n");

    // 用法故障位
    if(cfsr & (1 << 16)) printf("✗ 执行了未定义的指令\r\n");
    if(cfsr & (1 << 17)) printf("✗ 无效处理器状态(通常是跳转到非Thumb地址)\r\n");
    if(cfsr & (1 << 18)) printf("✗ 无效的PC值加载\r\n");
    if(cfsr & (1 << 19)) printf("✗ 尝试访问不存在的协处理器\r\n");
    if(cfsr & (1 << 24)) printf("✗ 未对齐的内存访问\r\n");
    if(cfsr & (1 << 25)) printf("✗ 除以零错误\r\n");

    // 清除故障标志
    SCB->CFSR = 0xFFFFFFFF;
    SCB->HFSR = 0xFFFFFFFF;

    // 死循环,防止程序继续运行
    while(1);
}

1.3 修改完成后

解析异常栈帧并打印详细错误信息;根据标志位输出,可大致判断是什么问题,然后后再细分子问题

二. 复现致命错误

2.1 复现错误打印

更改HardFault_Handle后,编译运行,复现错误,uart打印的log调试(如下):

根据修改HardFault提供的信息和运行日志,问题已经比较清晰了:程序在执行完NFC通信(simulate_phone()函数)后,发生了“精确数据总线错误”(PRECISERR),错误地址为0x44344630,这是一个明显无效的地址(正常STM32F103的SRAM地址范围为0x20000000~0x20004FFF,Flash为0x08000000~0x0801FFFF)。下面我会逐步分析原因并给出解决方案。

2.2 错误信息细分解读

正常STM32F103的SRAM地址范围:0x20000000~0x20004FFFFlash为0x08000000~0x0801FFFF;

寄存器/寄存器 含义
PC 0x08001638 触发异常时正在执行的指令地址
LR 0x080015D7 异常发生前的返回地址(可能指示调用关系)
BFAR/MMFAR 0x44344630(明显错误) 引发总线错误的精确访问地址
CFSR 0x00008200 指示:PRECISERR (bit9) + BFARVALID (bit7)
HFSR 0x40000000 FORCED —— Hard Fault 由其它异常(总线错误)引起

2.3 大致问题分析

核心原因:CPU试图从地址 0x44344630 读取或写入数据,但这个地址不在任何有效存储器区域(甚至不在外设地址空间),导致总线错误,并被提升为HardFault

三. 原因调试

3.1 可能的原因排序:

  1. simulate_phone() 或它内部的NFC解析函数,错误地将NFC回复中的某个字段(长度、状态字、UID等)当作了指针地址来使用,且没有做范围检查。

  2. 缓冲区溢出:某个数组写越界,污染了函数指针或返回地址,导致后续跳转到了非法地址。

  3. 栈溢出:局部变量过多或递归调用导致栈顶指针指向非法区域,但BFAR地址 0x38373437 更像数据直接拼凑而成,栈溢出通常表现为PC=0xFFFFFFFD等。

  4. 野指针:之前某处释放了内存但仍继续使用。

具体函数如下:

void sim_phone(void)
{
  #if 1 


  rfalNfcbPollerInitialize();   /* Initialize for NFC-B */
  rfalFieldOnAndStartGT();      /* Turns the Field On if not already and start GT timer */

    // if (read_appkey_hash() != 0)
    // {
    //   //read_cos_version_part();
    //   platformDelay(200);
    //   return -1;
    // }   

    rfalFieldOff();
    platformDelay(100);
    //打开字段,执行初始避碰
    rfalFieldOnAndStartGT();
    platformDelay(100);
    // #define RFAL_TXRX_FLAGS_DEFAULT_RAWDATA    RFAL_TXRX_FLAGS_CRC_TX_MANUAL|RFAL_TXRX_FLAGS_DEFAULT|RFAL_TXRX_FLAGS_CRC_RX_KEEP
    // ReturnCode err;
    // uint8_t rxarr[256];
    // uint16_t rxarr_len =256;
    // uint16_t actLen = 0;

    #define RFAL_TXRX_FLAGS_DEFAULT_RAWDATA    RFAL_TXRX_FLAGS_CRC_TX_MANUAL|RFAL_TXRX_FLAGS_DEFAULT|RFAL_TXRX_FLAGS_CRC_RX_KEEP
    ReturnCode err;
    uint8_t rxarr[256];
    uint16_t rxarr_len =256;
    uint16_t actLen = 0;


    #if 1//业务代码
    #endif


rfalFieldOff();
platformDelay(100);
//打开字段,执行初始避碰
rfalFieldOnAndStartGT();
platformDelay(100);
static uint8_t s22_cmd1[] = {0x00,0x5a,0x00,0x00,0x5a,};
static uint8_t s22_cmd2[] = {0x00,0x00,0x16,0x00,0x00,0x52,0x00,0x68,};
//0x00,0x00,0x76,0x00,0x00,0x10,0x67,0x17,0x2d,0x5e,0xdf,0x1c,0xb7,0x74,0xe2,0xd5,0xdf,0xc7,0x1b,0xb5,0xb6,0x36,0x08,0xce,//s22
static uint8_t s22_cmd3[] = {0x00,0x00,0x76,0x00,0x00,0x10,0x87,0x1d,0x25,0x38,0x4d,0x63,0x74,0x3c,0x8c,0x55,0x92,0x8e,0xaa,0x66,0x6e,0x31,0x06,0x97,};
// 0x00,0x00,0x76,0x00,0x00,0x10,0x86,0xb3,0x78,0x37,0x83,0x3c,0x91,0x46,0x0e,0xc7,0x9c,0x17,0x8f,0xd6,0xb8,0x3f,0x07,0xe8,
//static uint8_t s22_cmd3[] = {0x00,0x00,0x76,0x00,0x00,0x10,0x86,0xb3,0x78,0x37,0x83,0x3c,0x91,0x46,0x0e,0xc7,0x9c,0x17,0x8f,0xd6,0xb8,0x3f,0x07,0xe8,};
static uint8_t s22_cmd4[] = {0x00,0x54,0x22,0x1f,0x00,0x12,0x00,0xa7,};
static uint8_t s22_cmd5[] = {0x00,0x54,0x22,0x10,0x00,0x00,0x00,0x86,};

printf("=============Simulated mobile phone========\r\n\r\n\r\n");
err = rfalTransceiveBlockingTxRx(s22_cmd1, sizeof(s22_cmd1), rxarr, rxarr_len, &actLen, RFAL_TXRX_FLAGS_DEFAULT_RAWDATA, RFAL_NFCB_FWTSENSB*800*4 );// RFAL_NFCB_FWTSENSB*100
printf("test nfcb_request err----------------%d\r\n",err);
printf("actLen = %d\r\n",actLen);
printf("cmd1_rxarr: %s\r\n", hex2Str((uint8_t*)rxarr, actLen));
platformDelay(150);

err = rfalTransceiveBlockingTxRx(s22_cmd2, sizeof(s22_cmd2), rxarr, rxarr_len, &actLen, RFAL_TXRX_FLAGS_DEFAULT_RAWDATA, RFAL_NFCB_FWTSENSB*800*4 );// RFAL_NFCB_FWTSENSB*100
printf("test nfcb_request err----------------%d\r\n",err);
printf("actLen = %d\r\n",actLen);
printf("cmd2_rxarr: %s\r\n", hex2Str((uint8_t*)rxarr, actLen));
platformDelay(150);

err = rfalTransceiveBlockingTxRx(s22_cmd3, sizeof(s22_cmd3), rxarr, rxarr_len, &actLen, RFAL_TXRX_FLAGS_DEFAULT_RAWDATA, RFAL_NFCB_FWTSENSB*800*4 );// RFAL_NFCB_FWTSENSB*100
printf("test nfcb_request err----------------%d\r\n",err);
printf("actLen = %d\r\n",actLen);
printf("cmd3_rxarr: %s\r\n", hex2Str((uint8_t*)rxarr, actLen));
platformDelay(150);

err = rfalTransceiveBlockingTxRx(s22_cmd4, sizeof(s22_cmd4), rxarr, rxarr_len, &actLen, RFAL_TXRX_FLAGS_DEFAULT_RAWDATA, RFAL_NFCB_FWTSENSB*800*4 );// RFAL_NFCB_FWTSENSB*100
printf("test nfcb_request err----------------%d\r\n",err);
printf("actLen = %d\r\n",actLen);
printf("cmd4_rxarr: %s\r\n", hex2Str((uint8_t*)rxarr, actLen));
platformDelay(150);

err = rfalTransceiveBlockingTxRx(s22_cmd5, sizeof(s22_cmd5), rxarr, rxarr_len, &actLen, RFAL_TXRX_FLAGS_DEFAULT_RAWDATA, RFAL_NFCB_FWTSENSB*800*4 );// RFAL_NFCB_FWTSENSB*100
printf("test nfcb_request err----------------%d\r\n",err);
printf("actLen = %d\r\n",actLen);
printf("cmd5_rxarr: %s\r\n", hex2Str((uint8_t*)rxarr, actLen));
platformDelay(150);

printf("=============end========\r\n\r\n\r\n");

#endif
}

四. 排查与修复步骤

4.1 立即增加地址有效性检查(防御性编程)

这里添加了,对nfc的发送和接收数据进行测试,发现是没任何问题的

uint32_t addr = get_addr_from_nfc_packet();
if (addr >= 0x20000000 && addr <= 0x20004FFF)   // SRAM范围
{
    // 可以安全访问
}
else if (addr >= 0x08000000 && addr <= 0x0801FFFF) // Flash范围
{
    // 可以安全访问(只读)
}
else
{
    printf("Invalid address detected: 0x%08X, skip!\r\n", addr);
    return;  // 或进行错误处理
}

4.2 检查 simulate_phone() 中所有指针类型转换

1.(uint32_t*)&buffer[x]

2.(void (*)())buffer

3. 任何将 uint8_t* 强制转换为 uint32_t* 后解引用的操作

4.注意处理NFC返回的 cmd2_rxarr、cmd3_rxarr 等长数据包这是没有问题的

4.3 检查数组边界

发送和接收数组是有明确边界的,也是没问题

4.4 添加 当前主栈指针(MSP)的值

while(1),执行函数每次进入打印值:MSP;函数里面也要添加;

4.4.1 这里截取部分指令代码

4.4.2 第一次执行没有问题:栈的空间已经返回

4.4.3 第二次出现问题

可以看到栈完全没有变化、完全平衡、完全正常!

这里找到问题了,每次都是回复第二条指令时出错,这里就是非法访问的情况;

程序试图去访问一个非法地址:0x38373437 这个地址根本不是 RAM、不是 Flash、不是寄存器 → CPU 直接拒绝执行 → HardFault

五. 真正崩溃原因:数据总线错误 – 非法地址访问

   但是现在打印的是栈区域(uint8_t rxarr[256];),因为是随机存储,所以就容易发现非法访问了

hex2Str函数原型为:可以看到,这里最多是63字节,我接收时数据达到87字节,返回非法地址,后续打印就会直接报HardFault错误,地址不在任何有效存储器区域(甚至不在外设地址空间)

5.1 原始打印hex2Str()函数

        原来的打印hex2Str()可以看到这里使用二维数组,可以保存四行printf打印,保存的每行(共四行):最多处理 128/2 - '\0' = 63字节(每个字符转为2个十六进制字符,加上一个字符串结束符'\0';最多只能安全转换 63 字节的原始数据)

#define MAX_HEX_STR         4
#define MAX_HEX_STR_LENGTH  128
char hexStr[MAX_HEX_STR][MAX_HEX_STR_LENGTH];
uint8_t hexStrIdx = 0;

char* hex2Str(unsigned char * data, size_t dataLen)
{
  unsigned char * pin = data;
    const char * hex = "0123456789ABCDEF";
    char * pout = hexStr[hexStrIdx];
    uint8_t i = 0;
    uint8_t idx = hexStrIdx;
    if(dataLen == 0)
    {
      pout[0] = 0;     
    } 
    else     
    {
      for(; i < dataLen - 1; ++i)
      {
          *pout++ = hex[(*pin>>4)&0xF];
          *pout++ = hex[(*pin++)&0xF];
      }
      *pout++ = hex[(*pin>>4)&0xF];
      *pout++ = hex[(*pin)&0xF];
      *pout = 0;
    }    
    
    hexStrIdx++;
    hexStrIdx %= MAX_HEX_STR;
    
    return hexStr[idx];
}
5.2 解决方法:

修改MAX_HEX_STR_LENGTH  128大小 或者

重写char* hex2Str(unsigned char * data, size_t dataLen)函数

1. 添加边界检查即可

修改缓冲区大小,足够容纳256字节的十六进制转换结果(256×2+1=513)

// 修改缓冲区大小,足够容纳256字节的十六进制转换结果(256×2+1=513)
#define MAX_HEX_STR_LENGTH  513
#define MAX_HEX_STR         4

char hexStr[MAX_HEX_STR][MAX_HEX_STR_LENGTH];
uint8_t hexStrIdx = 0;

char* hex2Str(unsigned char * data, size_t dataLen)
{
    unsigned char * pin = data;
    const char * hex = "0123456789ABCDEF";
    char * pout = hexStr[hexStrIdx];
    uint8_t idx = hexStrIdx;
    size_t maxChars = MAX_HEX_STR_LENGTH - 1;  // 预留结束符位置
    
    if(dataLen == 0)
    {
        pout[0] = '\0';
    }
    else
    {
        // 计算实际能转换的最大字节数,防止溢出
        size_t maxConvert = maxChars / 2;
        size_t convertLen = (dataLen > maxConvert) ? maxConvert : dataLen;
        
        for(size_t i = 0; i < convertLen; i++)
        {
            *pout++ = hex[(pin[i] >> 4) & 0x0F];
            *pout++ = hex[pin[i] & 0x0F];
        }
        *pout = '\0';
        
        // 如果数据被截断,可以添加提示
        if(dataLen > maxConvert)
        {
            printf("Warning: hex2Str truncated %zu bytes\n", dataLen - maxConvert);
        }
    }
    
    hexStrIdx = (hexStrIdx + 1) % MAX_HEX_STR;
    return hexStr[idx];
}
2. 更安全的方案:使用调用者提供的缓冲区

避免使用全局缓冲区,让调用者负责提供足够大的缓冲区,从根本上消除全局变量溢出的风险:

// 重新设计hex2Str函数,不使用全局变量
void hex2Str(const unsigned char * data, size_t dataLen, char * outBuf, size_t outBufSize)
{
    if(outBuf == NULL || outBufSize == 0) return;
    
    const char * hex = "0123456789ABCDEF";
    char * pout = outBuf;
    size_t maxChars = outBufSize - 1;
    size_t convertLen = (dataLen * 2 <= maxChars) ? dataLen : (maxChars / 2);
    
    for(size_t i = 0; i < convertLen; i++)
    {
        *pout++ = hex[(data[i] >> 4) & 0x0F];
        *pout++ = hex[data[i] & 0x0F];
    }
    *pout = '\0';
}

// 调用方式
char hexBuf[513];
hex2Str(rxarr, actLen, hexBuf, sizeof(hexBuf));
printf("cmd2_rxarr: %s\r\n", hexBuf);

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐