目录前言问题描述一.HardFault_Handler修改1.1 原HardFault_Handler1.2 重写HardFault_Handle1.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_HandlerHardFault_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、FORCEDBFAR(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~0x20004FFFFlash为0x08000000~0x0801FFFF。下面我会逐步分析原因并给出解决方案。2.2 错误信息细分解读正常STM32F103的SRAM地址范围0x20000000~0x20004FFFFlash为0x08000000~0x0801FFFF寄存器/寄存器值含义PC0x08001638触发异常时正在执行的指令地址LR0x080015D7异常发生前的返回地址可能指示调用关系BFAR/MMFAR0x44344630(明显错误)引发总线错误的精确访问地址CFSR0x00008200指示PRECISERR(bit9) BFARVALID(bit7)HFSR0x40000000FORCED—— Hard Fault 由其它异常总线错误引起2.3 大致问题分析核心原因CPU试图从地址0x44344630读取或写入数据但这个地址不在任何有效存储器区域甚至不在外设地址空间导致总线错误并被提升为HardFault。三. 原因调试3.1 可能的原因排序simulate_phone()或它内部的NFC解析函数错误地将NFC回复中的某个字段长度、状态字、UID等当作了指针地址来使用且没有做范围检查。缓冲区溢出某个数组写越界污染了函数指针或返回地址导致后续跳转到了非法地址。栈溢出局部变量过多或递归调用导致栈顶指针指向非法区域但BFAR地址0x38373437更像数据直接拼凑而成栈溢出通常表现为PC0xFFFFFFFD等。野指针之前某处释放了内存但仍继续使用。具体函数如下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 (*)())buffer3. 任何将 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[(*pin4)0xF]; *pout hex[(*pin)0xF]; } *pout hex[(*pin4)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×21513)// 修改缓冲区大小足够容纳256字节的十六进制转换结果(256×21513) #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);