单片机(STM32)Debug – 基于反汇编文件的栈回溯
目录
1. 栈回溯
1.1 原理概述
1.1.1 栈的工作原理
- 在进入fault处理程序(Handler)前,CM3内核会将几个必须的寄存器压栈保存;
- 栈中的PC寄存器代表着在进入Handler之前,程序所在的位置,也就是出错的那行代码;
- 找到出错代码后,我们还需要确定函数的调用关系;
- 在执行出错之前已经经过了多次函数调用(A > B > C),栈内部存放了函数调用过程中的寄存器信息;
- 我们需要关注的是LR寄存器的内容,其存储着函数返回后执行的下一条代码的地址;
- 在STM32微处理器上,代码存储在Flash中,对于F103C8T6(中容量产品),Flash地址空间是0x08000000 ~ 0x0801FC00,因此程序机器码的地址必然为0x0800xxxx,从栈中提取相应地址即可(为稳妥还可以加多一条判断:该机器码的前一条应该为BL指令,即跳转指令);
1.1.2 根据栈内容和反汇编文件找到函数调用关系
(1)获取反汇编文件
- 在Keil中添加构建后命令:
fromelf --text -a -c --output=xxx.dis xxx.axf
1. xxx.dis:xxx为生成的反汇编文件的名称,由用户自定义,工程构建结束后可以在工程同级目录下找到对应的反汇编文件;
2. xxx.axf:Keil生成的可执行文件,需要基于该文件生成.dis;该文件在Linker中指定;
(2)获取PC指针和各级LR指针
- 从栈顶开始打印各个数据,以32bit为基准,每32bit为一个寄存器的值;
- 首先关注进入Handler之前的PC值,其代表从哪行代码进入fault;
- 进入引发错误的相应函数后,根据后续打印的LR(0x0800xxxx)继续分析函数调用关系即可;
1.2 实例-基于HardFault_Handler
函数调用关系:main > DebugTest > A > B > C引入除零错误
int C(int val)
{
printf("Enter C()\r\n");
return (100 / val);
}
void B()
{
printf("Enter B()\r\n");
C(0);
printf("Exit B()\r\n");
}
void A()
{
printf("Enter A()\r\n");
B();
printf("Exit A()\r\n");
}
void Debug_Test()
{
// 使能除零错误:
// 将CM3_CCR(0xE000ED14)寄存器的DIV_0_TRP(bit 4)位清零
volatile int *CM3_CCR = (volatile int *)0xE000ED14;
*CM3_CCR |= (1<<4);
printf("Hello world\r\n");
A();
}
int main(void)
{
Serial_Init();
Debug_Test();
while (1) {
}
}
1.2.1 修改HardFault_Handler
-
发生硬fault中断时,CM3内核会进入HardFault_Handler进行处理;默认的HardFault_Handler就是一个死循环;
void HardFault_Handler(void) { /* Go to infinite loop when Hard Fault exception occurs */ while (1) { } }
-
应该修改HardFault_Handler,将堆栈指针传给一个C函数,并在C函数中打印出栈里的内容;
__asm void HardFault_Handler(void) { extern HardFault_Handler_C; TST LR, #4 // if(lr[2]==1) ITE EQ MRSEQ R0, MSP // lr[2] == 0, 代表使用的是msp,将其写入r0, 相当于给HardFault_Handler_C传参 MRSNE R0, PSP // lr[2] == 1, 将psp写入r0 B HardFault_Handler_C // 无条件跳转到HardFault_Handler_C }
-
在C函数HardFault_Handler_C中打印出栈的内容;
void HardFault_Handler_C(StackBuffer *debugBuffer) { // 打印进入Handler之前硬件压栈的寄存器 printf("HardFault_Handler is running\r\n"); printf("R0:0X%08x\r\n", debugBuffer->R0); printf("R1:0X%08x\r\n", debugBuffer->R1); printf("R2:0X%08x\r\n", debugBuffer->R2); printf("R3:0X%08x\r\n", debugBuffer->R3); printf("R12:0X%08x\r\n", debugBuffer->R12); printf("LR:0X%08x\r\n", debugBuffer->LR); printf("PC:0X%08x\r\n", debugBuffer->PC); printf("xPSR:0X%08x\r\n", debugBuffer->xPSR); // 打印函数调用过程中压栈的内容 uint32_t *funcStackPointer = (uint32_t*)((uint32_t)debugBuffer + sizeof(StackBuffer)); printf("funcStack:\r\n"); for (int i = 0; i < 1024; i++) { // 判断是否代码段地址 bool isCodeAddress = ((*funcStackPointer & 0xFFFF0000) == 0x08000000); uint32_t *previousAddress = (uint32_t *)(*funcStackPointer - 4 - 1); // 判断上一条指令是否BL指令 bool ThePreviousIsBL = ((*previousAddress & 0xf800f000) == 0xf800f000); // 打印出函数跳转地址 if (isCodeAddress && ThePreviousIsBL) { printf("reg val: 0x%08x\r\n", *funcStackPointer); } funcStackPointer++; } // 最后必须while循环,否则会回到触发fault的地方,再次触发fault while (1); }
1.2.2 分析反汇编文件
- 串口打印的寄存器数据如下:PC寄存器为0x080006e4;
- 在反汇编文件中找到0x080006e4所在位置,发现指向C函数中除法操作,确定是其引发的除零错误;
- 进一步找下一个LR值:0x080006b3,发现找不到,原因在于CM3内核仅支持Thumb指令集,其规定指令地址必须为奇数,且其通过编译系统自动将指令地址+1,因此反汇编码中指令地址全为偶数->去找0x080006b2
- 发现0x080006b2指向B函数的某条指令,且上一条是
BL C
指令,证明确实从此处进入C函数;
- 紧接着找下一个LR,0x08000685 – 1 = 0x08000684;发现在A函数中,且上一条为
BL B
- 下一个LR为:0x08000713 – 1 = 0x08000712;发现在Debug_Test函数中,且上一条为
BL A
;
- 下一个LR为:0x08001863-1=0x08001862,发现在main函数中,上一条为
BL Debug_Test
,至此找到完整的函数调用路径;
作者:Mryoungg