单片机(STM32)Debug – 基于反汇编文件的栈回溯

目录

  • 1. 栈回溯
  • 1.1 原理概述
  • 1.1.1 栈的工作原理
  • 1.1.2 根据栈内容和反汇编文件找到函数调用关系
  • (1)获取反汇编文件
  • (2)获取PC指针和各级LR指针
  • 1.2 实例-基于HardFault_Handler
  • 1.2.1 修改HardFault_Handler
  • 1.2.2 分析反汇编文件
  • 1. 栈回溯

    1.1 原理概述

    1.1.1 栈的工作原理
    1. 在进入fault处理程序(Handler)前,CM3内核会将几个必须的寄存器压栈保存;
      image-20241010173515365
    2. 栈中的PC寄存器代表着在进入Handler之前,程序所在的位置,也就是出错的那行代码;
    3. 找到出错代码后,我们还需要确定函数的调用关系
      1. 在执行出错之前已经经过了多次函数调用(A > B > C),栈内部存放了函数调用过程中的寄存器信息;
      2. 我们需要关注的是LR寄存器的内容,其存储着函数返回后执行的下一条代码的地址;
    4. 在STM32微处理器上,代码存储在Flash中,对于F103C8T6(中容量产品),Flash地址空间是0x08000000 ~ 0x0801FC00,因此程序机器码的地址必然为0x0800xxxx,从栈中提取相应地址即可(为稳妥还可以加多一条判断:该机器码的前一条应该为BL指令,即跳转指令);
      image-20241010174657648
    1.1.2 根据栈内容和反汇编文件找到函数调用关系
    (1)获取反汇编文件
    1. 在Keil中添加构建后命令:fromelf --text -a -c --output=xxx.dis xxx.axf
      image-20241010175024464 1. xxx.dis:xxx为生成的反汇编文件的名称,由用户自定义,工程构建结束后可以在工程同级目录下找到对应的反汇编文件;
      2. xxx.axf:Keil生成的可执行文件,需要基于该文件生成.dis;该文件在Linker中指定;
      image-20241010175443964
    (2)获取PC指针和各级LR指针
    1. 从栈顶开始打印各个数据,以32bit为基准,每32bit为一个寄存器的值;
    2. 首先关注进入Handler之前的PC值,其代表从哪行代码进入fault;
      image-20241010221815027
    3. 进入引发错误的相应函数后,根据后续打印的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
    1. 发生硬fault中断时,CM3内核会进入HardFault_Handler进行处理;默认的HardFault_Handler就是一个死循环;

      void HardFault_Handler(void)
      {
        /* Go to infinite loop when Hard Fault exception occurs */
        while (1)
        {
        }
      }
      
    2. 应该修改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
      }
      
    3. 在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 分析反汇编文件
    1. 串口打印的寄存器数据如下:PC寄存器为0x080006e4;
      image-202410111033075
    2. 在反汇编文件中找到0x080006e4所在位置,发现指向C函数中除法操作,确定是其引发的除零错误;
      image-20241010235023789
    3. 进一步找下一个LR值:0x080006b3,发现找不到,原因在于CM3内核仅支持Thumb指令集,其规定指令地址必须为奇数,且其通过编译系统自动将指令地址+1,因此反汇编码中指令地址全为偶数->去找0x080006b2
    4. 发现0x080006b2指向B函数的某条指令,且上一条是BL C指令,证明确实从此处进入C函数;
      image-20241010234949205
    5. 紧接着找下一个LR,0x08000685 – 1 = 0x08000684;发现在A函数中,且上一条为BL B
      image-20241010235607845
    6. 下一个LR为:0x08000713 – 1 = 0x08000712;发现在Debug_Test函数中,且上一条为BL A
      image-20241010235403748
    7. 下一个LR为:0x08001863-1=0x08001862,发现在main函数中,上一条为BL Debug_Test,至此找到完整的函数调用路径;
      image-20241010235533999

    作者:Mryoungg

    物联沃分享整理
    物联沃-IOTWORD物联网 » 单片机(STM32)Debug – 基于反汇编文件的栈回溯

    发表回复