堆栈虽然是常用的内容,但是一般都浅尝辄止,很多人都只理解到:
- 栈上存放的是局部变量等
- 堆上存放的是申请的变量
实际上,堆栈除了上述特点以外,还有一些很重要的特性。
- 栈上还保存了函数的返回地址
- 栈上还(有可能,不使用寄存器传参的话)保存了传递的参数
- 局部的const 变变量
- 发生中断时,会生成并使用中断栈的概念。
- linux中还区分了用户空间栈和内核栈
栈的结构 #

int foo() {
return 1;
}
int sub(int a,int b) {
foo();
return a-b;
}
int add(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j,int k, int l,
int m, int n) {
int aa = m+n;
sub(a,b);
return 2;
}
int main() {
int a=1;
int b=2;
return add(1, 2,3,4,5,6,7,8,9,10,11,12,13,14);
}
foo:
str fp, [sp, #-4]!
add fp, sp, #0
mov r3, #1
mov r0, r3
add sp, fp, #0
ldr fp, [sp], #4
bx lr
sub:
push {fp, lr}
add fp, sp, #4
sub sp, sp, #8
str r0, [fp, #-8]
str r1, [fp, #-12]
bl foo
ldr r2, [fp, #-8]
ldr r3, [fp, #-12]
sub r3, r2, r3
mov r0, r3
sub sp, fp, #4
pop {fp, lr}
bx lr
add:
push {fp, lr}
add fp, sp, #4
sub sp, sp, #24
str r0, [fp, #-16]
str r1, [fp, #-20]
str r2, [fp, #-24]
str r3, [fp, #-28]
ldr r2, [fp, #36]
ldr r3, [fp, #40]
add r3, r2, r3
str r3, [fp, #-8]
ldr r1, [fp, #-20]
ldr r0, [fp, #-16]
bl sub
mov r3, #2
mov r0, r3
sub sp, fp, #4
pop {fp, lr}
bx lr
main:
push {fp, lr}
add fp, sp, #4
sub sp, sp, #48
mov r3, #1
str r3, [fp, #-8]
mov r3, #2
str r3, [fp, #-12]
mov r3, #14
str r3, [sp, #36]
mov r3, #13
str r3, [sp, #32]
mov r3, #12
str r3, [sp, #28]
mov r3, #11
str r3, [sp, #24]
mov r3, #10
str r3, [sp, #20]
mov r3, #9
str r3, [sp, #16]
mov r3, #8
str r3, [sp, #12]
mov r3, #7
str r3, [sp, #8]
mov r3, #6
str r3, [sp, #4]
mov r3, #5
str r3, [sp]
mov r3, #4
mov r2, #3
mov r1, #2
mov r0, #1
bl add
mov r3, r0
mov r0, r3
sub sp, fp, #4
pop {fp, lr}
bx lr
栈帧 Frame Pointer / 堆栈跟踪 #
参考文档:
- MCU学习笔记/11gcc 单片机开发.md — # 发生函数调用时,栈如何变化和工作
- 参考:https://alexkalmuk.medium.com/how-stack-trace-on-arm-works-5634b35ddca1
- 参考:https://www.brendangregg.com/blog/2024-03-17/the-return-of-the-frame-pointers.html
正文:
栈回溯的不外乎其原理就是两种:第一种是基于硬件的栈回溯,这种要求占用一个寄存器frame pointer指针,实时的保存当前栈顶的位置,且规定当前栈顶一定存放了上一个栈的fp和lr。这种原理对硬件有要求,且对栈的结构也有要求。例如arm64的aaps规范就规定了栈的结构。
第二种是基于软件的栈回溯,编译程序的同时生成对应的栈调试信息,比如unwind table、DWARF或者ORC(x86架构)等,当发生异常时,需要软件去这些信息中读取调试信息,确定栈的大小和返回地址,并进行栈回溯。 值得一提的是,这种方式需要每个栈都必须读取一次ram,所以并不适用于离线分析。
下面以linux中arm32的栈回溯代码举例:
dump_stack //kernel
->show_stack //arch/arm/kernel/traps.c
-->dump_backtrace //这里根据有没有生成unwind table,走不同的编译分支
--->unwind_backtrace
---->unwind_frame //到unwind table中寻找
再以arm64的栈回溯代码举例:
dump_stack //kernel
->show_stack //arch/arm64/kernel/stacktrace.c
-->dump_backtrace
--->arch_stack_walk
---->kunwind_stack_walk
----->do_kunwind
------>kunwind_next
------->unwind_next_frame_record
state->fp = READ_ONCE(*(unsigned long *)(fp));
state->pc = READ_ONCE(*(unsigned long *)(fp + 8));
示例:在MCU中利用FP指针进行栈回溯 #
为了使得发生异常时,可以追溯调用栈,GNU提出了栈帧 Frame Pointer的概念。 当函数调用和返回时,按以下流程工作:
调用:
- 保存当前的old_LR、old_FP。 使得新的new_FP指向此位置。
- 向上移动SP,留出空间
- 赋值等,计算
返回
- 检查当前的 new_FP, 其位置应该指向 old_LR、old_FP
- 将old_LR加载到PC;将old_FP加载到FP;将new_FP+8加载到SP。
- 返回上一个函数执行
如果按照上述流程工作,当发生异常时,我们可以按照返回的流程去反向查找出调用栈。
也可以不按上述流程工作,那就需要编译器在汇编代码中,手动计算SP、LR的位置,是可以的,只是一旦出错后,将无法查找调用栈。 -fomit-frame-pointer 编译选项可以关闭栈帧。
示例 #
我们随便找一个单片机工程,在编译选项中增加 -fno-omit-frame-pointer
-mapcs-frame
注:这里的apcs指 ARM Procedure Call Standard
然后查看反汇编文件:
arm-none-eabi-objdump -S -g -x xxx.elf > objdump.output
可以看到每个函数的开头,都增加了
push {.... fp ip lr pc}
这样的代码。说明Frame pointer开启成功了
然后就是栈的追溯:
common_exception_handle:
/*出现错误时,需要知晓的信息:1.错误代码行LR 2.对应错误参数:R0~R13. 对应的调用栈SP*/
/*编译选项中加入-mapcs-frame可以强制使用frame pointer进行栈回溯
/*保存R0-R12,LR到栈上.*/
STMFD
SP!, {R0-R12}
/* 读取CPSR */
MRS R0, CPSR
/*判断Ibit*/
AND R1, R0, #0x20
CMP R1, #0
BEQ cpsr_is_arm
ADD LR, LR ,4/* thumb指令集PC位置需要+4*/
cpsr_is_arm:
PUSH {LR}
/*切换CPSR,读取SP*/
ORR R1, R0, #0x1F
MSR CPSR, R1
MOV R1, SP
MSR CPSR, RØ
PUSH {R1}
PUSH {fp}
MRS R0, SPSR
PUSH {R0}
/*调用函数的时候传递一个参数*/
MOV R0, SP
b dabort_main
struct abort_info {
uint32 SPSR;
uint32 FP;
uint32 SP;
uint32 LR;
uint32 R0;
uint32 R1;
uint32 R2;
uint32 R3;
uint32 R4;
uint32 R5;
uint32 R6;
uint32 R7;
uint32 R8;
uint32 R9;
uint32 R10;
uint32 R11;
uint32 R12;
};
struct frame_pointer {
uint32 pre_fp;
uint32 ip;
uint32 LR;
uint32 PC;
}
void.dabort_main (void.* sp){
/* 栈上保存的内容为(由高地址到低地址)
RO~R12
LR
SP
FP
SPSR*/
hprintf(TSENV, "DATA ABORT ERROR! sp at 0x%x\n", sp)
hprintf(TSENV, "SPSR = 0x%x\n", ((struct abort_info*)sp)->SPSR);
hprintf(TSENV, "FP.=0x%x\n", ((struct abort_info.*)sp)->FP);
hprintf(TSENV, "SP.= 0x%x\n", ((struct abort_info *)sp)->SP)
hprintf(TSENV, "R0.= 0x%x\n", ((struct abort_info *)sp)->R0)
...
...
hprintf(TSENV, "Start back trace\n");
/*@TODO
@WARNING!!!
gcc.version-9.2.1.20191025 (release).[ARM/arm-9-branch.r[evision 277599] (GNU Tools for Arm Embe
不同gcc版本,fp的位置不同,此版本fp的位置位于sp-1个byte
所以这里需要再-3个byte
4803464: ela0c00d
4803468: e92dd8f0
480346c: e24cb004
mov ip, sp
push
{r4, r5, r6, r7, fp, ip, ip, lr,.pc}
sub fp, ip, .#4 */
#define.FP_OFFSET_GCC_QUIRKS 3
uint32 * fp_ptr=(uint32* )((struct abort_info.*)sp)->FP;
fp_ptr = fp_ptr - FP_OFFSET_GCC_QUIRKS;
struct frame_pointer* fp = fp_ptr;
#define.VALID_FP_ADDRESS_LOW.0x48000000
#define.VALID FP_ADDRESS_HI 0X4880000
for(;;).{
if((fp > VALID_FP_ADDRESS_HI) || (fp < VALID_FP_ADDRESS_LOW))
break;
hprintf(TSENV, "fp:0x%x, prep->fp:0x%x ip: 0x%x, LR: 0xx, PC: 0x%x\n", fp, fp->pre_fp, fp->ip
fp = fp->pre_fp;
while(1);
附录A:GDB中的backtrace如何工作 #
下载arm gcc源码:https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
总结:GDB针对不同的架构有不同的栈回溯方式,通常是Framepointer的方式。 但是对于32bit arm,采用的是逐行进行指令分析的方式。
add_com ("backtrace", backtrace_command...) //binutils-gdb/gdb/stack.c
->backtrace_command_1()
for (fi = trailing; fi && count--; fi = get_prev_frame (fi))
{
QUIT;
/* Don't use print_stack_frame; if an error() occurs it probably
means further attempts to backtrace would fail (on the other
hand, perhaps the code does or could be fixed to make sure
the frame->prev field gets set to NULL in that case). */
print_frame_info (fp_opts, fi, 1, LOCATION, 1, 0);
if ((flags & PRINT_LOCALS) != 0)
print_frame_local_vars (fi, false, NULL, NULL, 1, gdb_stdout);
/* Save the last frame to check for error conditions. */
trailing = fi;
}
-->get_prev_frame()
--->get_prev_frame_always_1()
---->frame_register_unwind_location()
----->frame_register_unwind()
------>frame_unwind_register_value()
------->arm_prologue_prev_register() //跟架构强相关binutils-gdb/gdb/arm-tdep.c
-------->arm_make_prologue_cache()
--------->arm_scan_prologue()
---------->arm_analyze_prologue() //解析指令,反推SP FP LR
for (current_pc = prologue_start;
current_pc < prologue_end;
current_pc += 4)
{
uint32_t insn = insn_reader.read (current_pc, byte_order_for_code);
if (insn == 0xe1a0c00d) /* mov ip, sp */
{
regs[ARM_IP_REGNUM] = regs[ARM_SP_REGNUM];
continue;
}
else if ((insn & 0xffff0fff) == 0xe52d0004) /* str Rd,
[sp, #-4]! */
{
if (stack.store_would_trash (regs[ARM_SP_REGNUM]))
break;
regs[ARM_SP_REGNUM] = pv_add_constant (regs[ARM_SP_REGNUM], -4);
stack.store (regs[ARM_SP_REGNUM], 4,
regs[bits (insn, 12, 15)]);
continue;
}
else if ((insn & 0xffff0000) == 0xe92d0000)
/* stmfd sp!, {..., fp, ip, lr, pc}
or
stmfd sp!, {a1, a2, a3, a4} */
{
附录B CPU栈初始化 #
在CPU启动时,必须要尽可能早的指定栈在内存中的位置,之后CPU会自动的使用此位置来作为栈。
对于cortex-m0(armv6) 和 cortex-M7(armv7)系列,通过vector向量表中的offset+0B~4B来指定栈的位置。如下图所示:

