本文重点在于如何分析栈空间,不在于如何获取栈空间,所以栈空间的打印本文不作描述。
关于如何打印栈空间,可参考我的另一篇博客《栈回溯之CmBacktrace》
调用关系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();
//......
}
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)
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)
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)
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
这里的日志是在HardFault
中打印的
=========== 线程堆栈信息 ===========
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
====================================
========================= 寄存器信息 =========================
R0 : feeeeeee R1 : f0000000 R2 : 00000000 R3 : 08008a0a
R12: 0000c000 LR : 0800a9a7 PC : 08008a76 PSR: 61000000
==============================================================
Cortex‐M 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32位数值。在下一次压栈时,SP 先自减 4,再存入新的数值。
这里的PC表示出问题的地方,对应到汇编文件中可知, fault_test()
中的如下内容出问题了
*SCB1 |= 0x10;
观察其上的堆栈操作,仅有如下操作,没有对LR进行操作,说明LR即为函数返回地址
0x08008A6C E92D0810 PUSH {r4,r11}
由于LR的LSB表示的是Thumb模式和ARM模式,所以返回地址为0800a9a6,对应为 test1()
中的
0x0800A9A6 E8BD4800 POP {r11,lr}
观察其上的堆栈操作,有如下操作
0x0800A994 E92D4800 PUSH {r11,lr}
所以 test1()
的LR想较于sp有3个偏移,即 addr: 2000104c,所以返回地址为0800a9e2,对应为 test2()
中的
0x0800A9E2 E8BD4800 POP {r11,lr}
观察其上的堆栈操作,有如下操作
0x0800A9D0 E92D4800 PUSH {r11,lr}
所以 test2()
的LR想较于sp有5个偏移,即 data: 08008e9f,所以返回地址为08008e9e,对应为 main()
中的
83: HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
0x08008E9E F8DFA0B4 LDR.W r10,[pc,#180] ; @0x08008F56
综上可以函数的调用关系为main() > test2() > test1() > fault_test() ,符合预期
上一节中还可以继续分析找到线程的入口,不过main()
数据略多,不利于手动分析。
本文也是旨在加深理解栈回溯的原理,实际中可能会使用BackTrace自动分析,可参考我的另一篇博客《栈回溯之CmBacktrace》