栈回溯之使用帧指针分析

说明

  • 使用 --use_frame_pointer 选项会保留一个寄存器来存储帧指针
  • 对于支持 Thumb-2 技术的较新处理器(ARMv6T2 及更高版本),保留寄存器始终为 R11
  • 对于不支持 Thumb-2 技术的旧处理器,保留寄存器是 ARM 代码中的 R11 和 Thumb 代码中的 R7

不建议对 Cortex-M 控制器使用选项 --use_frame_pointer

此选项不会为执行时间或代码密度带来好处,μVision 调试器也不需要此选项来正确显示应用程序的调用关系。


其基本原理就是在每个函数开始入栈R11(FP)和LR

PUSH          {r11,lr}

这里的FP会指向上一个函数的LR,那么我们就能知道每一个函数的LR,就能很快推出程序的调用关系

入栈之后会更新FP的值为当前函数的LR的入栈地址,方便下一级别函数直接入栈

栈回溯之使用帧指针分析_第1张图片



在STM32H7上启用帧指针

在Keil5中Options(魔术棒)> C/C++ > Misc Controls 加上启用帧指针FP(Frame Pointer)(默认不启用)的编译选项:

--use_frame_pointer

附:在GCC中启用

 -fno-omit-frame-pointer  -fno-optimize-sibling-calls



测试源码

调用关系main() > test2() > test1() > fault_test()

void fault_test() 
{
	volatile int *SCB1 = (volatile int *) 0xFEEEEEEE;
    // 非对齐访问 会挂掉
	*SCB1 |= 0x10;

    printf("SCB1: %d\r\n", *SCB1);
}

void test1()
{
	printf("test 1 start\r\n");
	fault_test();
	printf("test 1 end\r\n");
}

void test2()
{
	printf("test 2 start\r\n");
	test1();
	printf("test 2 end\r\n");
}

int main(void)
{
	//......
	test2();
	//......
}



汇编

void fault_test(void)

0x08008A6C E92D0810  PUSH          {r4,r11}
    20:         volatile int *SCB1 = (volatile int *) 0xFEEEEEEE; 
0x08008A70 4806      LDR           r0,[pc,#24]  ; @0x08008A8C
0x08008A72 F10D0B04  ADD           r11,SP,#0x04
    21:         *SCB1 |= 0x10; 
    22:  
0x08008A76 6801      LDR           r1,[r0,#0x00]
0x08008A78 F0410110  ORR           r1,r1,#0x10
0x08008A7C 6001      STR           r1,[r0,#0x00]
    23:     printf("SCB1: %d\r\n", *SCB1); 
0x08008A7E 6801      LDR           r1,[r0,#0x00]
0x08008A80 A003      ADR           r0,{pc}+0x10  ; @0x08008A90
0x08008A82 E8BD0810  POP           {r4,r11}
0x08008A86 F7FEBE03  B.W           __0printf (0x08007690)

void test1()

0x0800A994 E92D4800  PUSH          {r11,lr}
0x0800A998 F10D0B04  ADD           r11,SP,#0x04
    28:         printf("test 1 start\r\n"); 
0x0800A99C A004      ADR           r0,{pc}+0x14  ; @0x0800A9B0
0x0800A99E F7FCFE77  BL.W          __0printf (0x08007690)
    29:         fault_test(); 
0x0800A9A2 F7FEF863  BL.W          fault_test (0x08008A6C)
0x0800A9A6 E8BD4800  POP           {r11,lr}
    30:         printf("test 1 end\r\n"); 
0x0800A9AA A005      ADR           r0,{pc}+0x18  ; @0x0800A9C0
0x0800A9AC F7FCBE70  B.W           __0printf (0x08007690)

void test2()

0x0800A9D0 E92D4800  PUSH          {r11,lr}
0x0800A9D4 F10D0B04  ADD           r11,SP,#0x04
    35:         printf("test 2 start\r\n"); 
0x0800A9D8 A004      ADR           r0,{pc}+0x14  ; @0x0800A9EC
0x0800A9DA F7FCFE59  BL.W          __0printf (0x08007690)
    36:         test1(); 
0x0800A9DE F7FFFFD9  BL.W          test1 (0x0800A994)
0x0800A9E2 E8BD4800  POP           {r11,lr}
    37:         printf("test 2 end\r\n"); 
0x0800A9E6 A005      ADR           r0,{pc}+0x18  ; @0x0800A9FC
0x0800A9E8 F7FCBE52  B.W           __0printf (0x08007690)

int main(void)

    66:         test2(); 
0x08008E9A F001FD99  BL.W          test2 (0x0800A9D0)
    83:                 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); 
0x08008E9E F8DFA0B4  LDR.W         r10,[pc,#180]  ; @0x08008F56



增加打印FP

如何打印栈空间和FP不是本文的重点,可参考我的另一篇博客《栈回溯之CmBacktrace》

基于CmBacktrace修改,其原打印栈空间的函数原型为

void cm_backtrace_fault(uint32_t fault_handler_lr, uint32_t fault_handler_sp) 

修改为

void cm_backtrace_fault(uint32_t fault_handler_lr, uint32_t fault_handler_sp, uint32_t fault_handler_fp) 

HardFault

HardFault_Handler    PROC
MemManage_Handler
    MOV     r0, lr                  ; get lr
    MOV     r1, sp                  ; get stack pointer (current is MSP)
    BL      cm_backtrace_fault

Fault_Loop
    BL      Fault_Loop              ;while(1)
    ENDP

    END

增加第三个参数后为

HardFault_Handler    PROC
MemManage_Handler
    MOV     r0, lr                  ; get lr
    MOV     r1, sp                  ; get stack pointer (current is MSP)
    MOV     r2, r11                 ; get frame pointer (if enabled)
    BL      cm_backtrace_fault

Fault_Loop
    BL      Fault_Loop              ;while(1)
    ENDP

    END

日志

=========== 线程堆栈信息 ===========
  addr: 20001040    data: deadbeef
  addr: 20001044    data: 2000104c
  addr: 20001048    data: 20001054
  addr: 2000104c    data: 0800a9e3
  addr: 20001050    data: 200010cc
  addr: 20001054    data: 08008e9f
  addr: 20001058    data: 23232323
  addr: 2000105c    data: 23232323
  addr: 20001060    data: 23232323
  addr: 20001064    data: 23232323
  addr: 20001068    data: 23232323
  addr: 2000106c    data: 23232323
  addr: 20001070    data: 23232323
  addr: 20001074    data: 23232323
  addr: 20001078    data: 23232323
  addr: 2000107c    data: 23232323
  addr: 20001080    data: 23232323
  addr: 20001084    data: 23232323
  addr: 20001088    data: 23232323
  addr: 2000108c    data: 00000000
  addr: 20001090    data: deadbeef
  addr: 20001094    data: deadbeef
  addr: 20001098    data: deadbeef
  addr: 2000109c    data: deadbeef
  addr: 200010a0    data: deadbeef
  addr: 200010a4    data: deadbeef
  addr: 200010a8    data: deadbeef
  addr: 200010ac    data: deadbeef
  addr: 200010b0    data: 200010c4
  addr: 200010b4    data: 08009247
  addr: 200010b8    data: deadbeef
  addr: 200010bc    data: deadbeef
  addr: 200010c0    data: 200010cc
  addr: 200010c4    data: 08008f81
  addr: 200010c8    data: deadbeef
  addr: 200010cc    data: 08008155
====================================
========================= 寄存器信息 =========================
  PSP: 20001040  FP : 20001044
  R0 : feeeeeee  R1 : f0000000  R2 : 00000000  R3 : 08008a0a
  R12: 0000c000  LR : 0800a9a7  PC : 08008a76  PSR: 61000000
==============================================================



分析

这里选择了在HardFault的第一现场获取了FP并打印,分析不同的优化等级和代码结构

发现FP的值有三种指向可能:(如不认同,非常欢迎留言)

  1. 使用-O0优化等级,编译器会严格按顺序入栈R11和LR并更新R11的值,那么此时R11的值就为LR入栈地址,即R11指向的值和LR一致
  2. 在末级函数中没有调用任何函数,编译器可能不会入栈LR,那么此时R11更新后的值为R11入栈地址,即R11指向的值指向上一个函数的LR
  3. 还有可能在入栈R11后还没来得及更新就出错了,那么此时R11指向的值为上一个函数的LR

观察fault_test()的汇编可知,符合这里的第二种情况,开发分析:

这里的PC表示出问题的地方,对应到汇编文件中可知, fault_test() 中的如下内容出问题了

*SCB1 |= 0x10; 

LR表示函数返回地址,由于LR的LSB表示的是Thumb模式和ARM模式,所以返回地址为0800a9a6,对应为 test1()中的

0x0800A9A6 E8BD4800  POP           {r11,lr}

FP : 20001044 指向的值为 addr: 20001044 data: 2000104c

那么2000104c 指向的值addr: 2000104c data: 0800a9e3为上一个函数 test1()的LR,对应为 test2()中的

0x0800A9E2 E8BD4800  POP           {r11,lr}

addr: 2000104c data: 0800a9e3的上一个数据 addr: 20001048 data: 20001054为 test1()中的FP

那么它指向的值 addr: 20001054 data: 08008e9f为test2()的LR,对应为 main()中的

    83:                 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); 
0x08008E9E F8DFA0B4  LDR.W         r10,[pc,#180]  ; @0x08008F56

addr: 20001054 data: 08008e9f的上一个数据 addr: 20001050 data: 200010cc为栈底,表示回溯完毕

综上可以函数的调用关系为main() > test2() > test1() > fault_test() ,符合预期

好像这里并不能像CmBackTrace一样回溯到线程的入口函数



可能会产生的错误

现象

使用带有选项 –use_frame_pointer 的 ARMCC 5,在某些情况下可能会为 Cortex-M 微控制器生成错误代码。此错误代码可能会导致偶发的运行时崩溃或硬故障,下面是一个示例:

int foo(void)
{
    int w = 4;
    int x = 5;
    int y = 6;
    int z = 7;
    return w + x + y + z;
}

这将生成以下代码:

foo
        0x00000000:    e92d4810    -..H    PUSH     {r4,r11,lr}
        0x00000004:    f10d0b08    ....    ADD      r11,sp,#8
        0x00000008:    2404        .$      MOVS     r4,#4
        0x0000000a:    2105        .!      MOVS     r1,#5
        0x0000000c:    2206        ."      MOVS     r2,#6
        0x0000000e:    2307        .#      MOVS     r3,#7
        0x00000010:    1860        '.      ADDS     r0,r4,r1
        0x00000012:    4410        .D      ADD      r0,r0,r2
        0x00000014:    4418        .D      ADD      r0,r0,r3
        0x00000016:    46dd        .F      MOV      sp,r11
        0x00000018:    b082        ..      SUB      sp,sp,#8
        0x0000001a:    e8bd8810    ....    POP      {r4,r11,pc}

如果地址 0x00000016 处的 MOV 指令和地址 0x00000018 处的 SUB 指令之间发生中断,则中断堆栈可能会损坏地址 0x0000001a处的 POP 指令将加载到 r4 和 r11 的值。这是因为当中断发生时,它们位于 SP 寄存器中的地址下方。

此编译器选项不能在μVision中通过复选框进行选择。必须在Options(魔术棒)> C/C++ > Misc Controls对话框中手动指定它。

原因

这是 Arm 编译器版本 5.06u6 及更低版本 (armcc.exe) 的代码生成中的错误。

建议

不建议对 Cortex-M 控制器使用选项 --use_frame_pointer。此选项不会为执行时间或代码密度带来好处。μVision 调试器也不需要此选项来正确显示应用程序的调用堆栈。

解决

这是 5.06u6 之前的已知问题。Arm 编译器 5.06u7 修复了此问题,MDK 版本 5.32 及更高版本也将包含此编译器版本。

你可能感兴趣的:(C语言,栈回溯,STM32,单片机,RTOS)