基本原理是每次调用函数时,ARM cpu 都会将函数返回地址放入 LR 寄存器中,多层调用时,C 编译器会自动将 LR寄存器内容push到栈空间。这样一来,随着函数调用的不断深入,栈中保存着整个调用链的返回地址。
我们通过获取出错时的栈地址,遍历栈空间,由于这些数据除了返回地址,还可能是局部变量,也可能是寄存器值。因此需要进一步筛选。
我们假设数据是真的返回地址,那么其前面一条代码必然是 BL 或 BLX 指令,通过检查该地址出的前一条代码,我们就可筛选出真的返回地址,从而得到整个函数的调用链。找出错误位置。
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* 检查对应地址处是否是 BL, BLX */
static int check_ins(uint32_t addr)
{
#define BL_INS_MASK 0xF800
#define BL_INS_HIGH 0xF800
#define BL_INS_LOW 0xF000
#define BLX_INX_MASK 0xFF00
#define BLX_INX 0x4700
uint16_t ins1 = *((uint16_t *)addr);
uint16_t ins2 = *((uint16_t *)(addr + 2));
if ((ins2 & BL_INS_MASK) == BL_INS_HIGH && (ins1 & BL_INS_MASK) == BL_INS_LOW)
return 1;
else if ((ins2 & BLX_INX_MASK) == BLX_INX)
return 1;
else
return 0;
}
typedef struct
{
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t psr;
} fault_reg_t;
/* 根据栈回溯 */
static void backtrace(uint32_t pc, uint32_t sp)
{
extern uint32_t _estack;
extern uint32_t _code_start;
extern uint32_t _code_end;
uint32_t code_start = (uint32_t)(uint32_t *)&_code_start;
uint32_t code_end = (uint32_t)(uint32_t *)&_code_end;
uint32_t stack_top = (uint32_t)(uint32_t *)(&_estack);
printf("backtrace: %08X ", pc);
for (; sp < stack_top; sp += sizeof(size_t))
{
pc = *((uint32_t *)sp) - sizeof(size_t);
if (pc % 2 == 0)
continue;
pc = *((uint32_t *)sp) - 1;
if (pc < code_start || pc >= code_end)
continue;
int r = check_ins(pc - sizeof(size_t));
if (!r)
continue;
// 获取 pc 值
printf("%08X ", pc);
}
printf("\n");
}
void hardfault_backtrace(uint32_t lr, uint32_t sp)
{
fault_reg_t *reg = (fault_reg_t *)sp;
backtrace(reg->pc, sp);
while (1)
;
}
void softfault_backtrace(void)
{
uint32_t pc, sp;
asm("mov %0, lr" : "=r"(pc));
asm("mov %0, sp" : "=r"(sp));
backtrace(pc, sp);
while (1)
;
}
使用举例
// 将 hardfault_hadnler 设到中断向量表中的 hard fault
void hardfault_hadnler(void)
{
asm("mov r0, lr");
asm("mov r1, sp");
asm("bl hardfault_backtrace");
while (1)
;
}
// 也可在断言失败时打印调用栈,从而快速定位断言失败原因
#define assert(expr) if(!(expr)) softfault_backtrace();
打印出的只是pc地址,需要利用 addr2line 将其转化为文件所在行, 例如
已知:
backtrace: 0000007D 0000007C 00000022
则:
addr2line.exe -e build/demo.elf -a 0000007D 0000007C 00000022