1. 前言
函数的栈帧可以显示栈当前的使用情况,对于理解栈的使用以及定位bug比较有帮助。本文就使用一个简单的函数调用示例来学习栈帧的分布。主要使用32位和64位 gcc编译器分别进行编译,并查看相应的栈帧结构。
2. aach32栈帧分析
//ARCH: armv7
//GCC版本:arm-linux-gnueabi-gcc (Linaro GCC 5.4-2017.01) 5.4.1 20161213
int fun(int a,int b)
{
int c = 1;
int d = 2;
return 0;
}
int main(int argc,char **argv)
{
int a = 0;
int b = 1;
fun(a,b);
}
arm-linux-gnueabihf-gc -d a.out
反汇编后的结果:
......
00010398 <fun>:
//将fun函数的栈底指针r7入栈
10398: b480 push {r7}
//分配20个字节的栈空间
1039a: b085 sub sp, #20
//fun函数的帧指针指向栈底
1039c: af00 add r7, sp, #0
//形参0入栈
1039e: 6078 str r0, [r7, #4]
//形参1入栈
103a0: 6039 str r1, [r7, #0]
//局部变量c入栈
103a2: 2301 movs r3, #1
103a4: 60fb str r3, [r7, #12]
//局部变量d入栈
103a6: 2302 movs r3, #2
103a8: 60bb str r3, [r7, #8]
//保存返回值为0
103aa: 2300 movs r3, #0
103ac: 4618 mov r0, r3
//恢复sp为main函数的栈指针
103ae: 3714 adds r7, #20
103b0: 46bd mov sp, r7
//恢复fp(r7)为main函数的帧指针
103b2: bc80 pop {r7}
//返回到main函数
103b4: 4770 bx lr
103b6: bf00 nop
000103b8 <main>:
//将main函数的栈底和lr入栈
103b8: b580 push {r7, lr}
//分配16字节的栈空间
103ba: b084 sub sp, #16
103bc: af00 add r7, sp, #0
//形参argv入栈
103be: 6078 str r0, [r7, #4]
//形参argc入栈
103c0: 6039 str r1, [r7, #0]
//局部变量a入栈
103c2: 2300 movs r3, #0
103c4: 60fb str r3, [r7, #12]
//局部变量b入栈
103c6: 2301 movs r3, #1
103c8: 60bb str r3, [r7, #8]
//局部变量b存放到r1寄存器
103ca: 68b9 ldr r1, [r7, #8]
//局部变量a存放到r0寄存器
103cc: 68f8 ldr r0, [r7, #12]
//跳转到fun函数
103ce: f7ff ffe3 bl 10398 <fun>
103d2: 2300 movs r3, #0
103d4: 4618 mov r0, r3
103d6: 3710 adds r7, #16
103d8: 46bd mov sp, r7
103da: bd80 pop {r7, pc}
......
栈帧结构:
需要注意的是从上面的示例可以看到栈是如何被使用的,通过反汇编也可以看到是如何恢复到调用函数的,但是通过如上分析并不能看出栈帧是如何回溯的,因此栈帧的回溯需要依赖其他的部分。
3. aach64栈帧分析
//ARCH: armv8
//GCC版本:aarch64-linux-gnu-gcc (Linaro GCC 5.4-2017.01) 5.4.1 20161213
int fun2(int c,int d)
{
return 0;
}
int fun1(int a,int b)
{
int c = 1;
int d = 2;
fun2(c, d);
return 0;
}
int main(int argc,char **argv)
{
int a = 0;
int b = 1;
fun1(a,b);
}
aarch64-linux-gnu-objdump -d a.out
反汇编后的结果为:
0000000000400530 <fun2>:
//更新sp到fun2的栈底
400530: d10043ff sub sp, sp, #0x10
400534: b9000fe0 str w0, [sp,#12]
400538: b9000be1 str w1, [sp,#8]
40053c: 52800000 mov w0, #0x0 // #0
400540: 910043ff add sp, sp, #0x10
400544: d65f03c0 ret
0000000000400548 <fun1>:
//分配48字节栈空间,先更新sp=sp-48, 再入栈x29, x30, 此时sp指向栈顶
400548: a9bd7bfd stp x29, x30, [sp,#-48]!
40054c: 910003fd mov x29, sp
//入栈fun1参数0
400550: b9001fa0 str w0, [x29,#28]
//入栈fun1参数1
400554: b9001ba1 str w1, [x29,#24]
//入栈fun1局部变量c
400558: 52800020 mov w0, #0x1 // #1
40055c: b9002fa0 str w0, [x29,#44]
//入栈fun1局部变量d
400560: 52800040 mov w0, #0x2 // #2
400564: b9002ba0 str w0, [x29,#40]
400568: b9402ba1 ldr w1, [x29,#40]
40056c: b9402fa0 ldr w0, [x29,#44]
//跳转到fun2
400570: 97fffff0 bl 400530 <fun2>
400574: 52800000 mov w0, #0x0 // #0
400578: a8c37bfd ldp x29, x30, [sp],#48
40057c: d65f03c0 ret
0000000000400580 <main>:
//分配48字节栈空间,先更新sp=sp-48, 再入栈x29, x30, 此时sp指向栈顶
400580: a9bd7bfd stp x29, x30, [sp,#-48]!
//x29、sp指向栈顶
400584: 910003fd mov x29, sp
//入栈main参数0
400588: b9001fa0 str w0, [x29,#28]
//入栈main参数1
40058c: f9000ba1 str x1, [x29,#16]
//入栈变量a
400590: b9002fbf str wzr, [x29,#44]
400594: 52800020 mov w0, #0x1 // #1
//入栈变量b
400598: b9002ba0 str w0, [x29,#40]
40059c: b9402ba1 ldr w1, [x29,#40]
4005a0: b9402fa0 ldr w0, [x29,#44]
//跳转到fun1
4005a4: 97ffffe9 bl 400548 <fun1>
4005a8: 52800000 mov w0, #0x0 // #0
4005ac: a8c37bfd ldp x29, x30, [sp],#48
4005b0: d65f03c0 ret
4005b4: 00000000 .inst 0x00000000 ; undefined
对应栈帧结构为:
总结一下:
通过对aarch64代码反汇编的分析,可以得出:
- 每个函数在入口处首先会分配栈空间,且一次分配,确定栈顶,之后sp将不再变化;
- 每个函数的栈顶部存放的是caller的栈顶指针,即fun1的栈顶存放的是main栈顶指针;
- 对于最后一级callee函数,由于x29保存了上一级caller的栈顶sp指针,因此不在需要入栈保存,如示例中fun2执行时,此时x29指向fun1的栈顶sp
4. x86_64栈帧分析
X86-64有16个64位寄存器,分别是:
%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:
%rax 作为函数返回值使用。
%rsp 栈指针寄存器,指向栈顶
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…
%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
%rip:相当pc,指向下一条将要运行的指令的地址
注:rip相当于arm中的pc
ref: :https://blog.csdn.net/zhbt1234/article/details/54019620
//ARCH: x86_64
//GCC版本:gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
int fun2(int c,int d)
{
return 0;
}
int fun1(int a,int b)
{
int c = 1;
int d = 2;
func2(c, d);
return 0;
}
int main(int argc,char **argv)
{
int a = 0;
int b = 1;
fun1(a,b);
}
objdump -d a.out
反编译后的结果如下:
00000000000005fa <fun2>:
//rbp保存fun1函数的栈底,入栈
5fa: 55 push %rbp
//初始化fun2函数的帧指针为栈底
5fb: 48 89 e5 mov %rsp,%rbp
//参数c入栈
5fe: 89 7d fc mov %edi,-0x4(%rbp)
//参数d入栈
601: 89 75 f8 mov %esi,-0x8(%rbp)
//返回值0
604: b8 00 00 00 00 mov $0x0,%eax
//恢复fun1的栈底指针rbp
609: 5d pop %rbp
60a: c3 retq
000000000000060b <fun1>:
//rbp保存main函数的栈底,入栈
60b: 55 push %rbp
//初始化fun1函数的帧指针为栈底
60c: 48 89 e5 mov %rsp,%rbp
//分配栈空间
60f: 48 83 ec 18 sub $0x18,%rsp
//参数a入栈
613: 89 7d ec mov %edi,-0x14(%rbp)
//参数b入栈
616: 89 75 e8 mov %esi,-0x18(%rbp)
//局部变量c入栈
619: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
//局部变量d入栈
620: c7 45 fc 02 00 00 00 movl $0x2,-0x4(%rbp)
//取出参数c,d分别存放到edi, esi寄存器准备调用fun1函数
627: 8b 55 fc mov -0x4(%rbp),%edx
62a: 8b 45 f8 mov -0x8(%rbp),%eax
62d: 89 d6 mov %edx,%esi
62f: 89 c7 mov %eax,%edi
//调用fun2
631: e8 c4 ff ff ff callq 5fa <fun2>
636: b8 00 00 00 00 mov $0x0,%eax
63b: c9 leaveq
63c: c3 retq
000000000000063d <main>:
//rbp保存上一个函数的栈底,入栈
63d: 55 push %rbp
//初始化main函数的帧指针为栈底
63e: 48 89 e5 mov %rsp,%rbp
//分配0x20的栈空间
641: 48 83 ec 20 sub $0x20,%rsp
//入栈参数0
645: 89 7d ec mov %edi,-0x14(%rbp)
//入栈参数1
648: 48 89 75 e0 mov %rsi,-0x20(%rbp)
//入栈局部变量a
64c: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
//入栈局部变量b
653: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
//取出参数a,b分别存放到edi, esi寄存器准备调用fun1函数
65a: 8b 55 fc mov -0x4(%rbp),%edx
65d: 8b 45 f8 mov -0x8(%rbp),%eax
660: 89 d6 mov %edx,%esi
662: 89 c7 mov %eax,%edi
//调用fun1函数
664: e8 a2 ff ff ff callq 60b <fun1>
669: b8 00 00 00 00 mov $0x0,%eax
66e: c9 leaveq
66f: c3 retq
根据如上反汇编代码,通过gdb单步实时查看当前栈的情况:
main:
-----------------------
rsp:0x7fffffffe9c0 入栈main函数的父函数的rip
0x7fffffffe9b8 入栈main函数的父函数的rbp
rbp: 0x7fffffffe9b8
0x7fffffffe998: 0xffffeac8 0x00007fff 0x00000001 0x00000001
| main参数1 | |main参数0|
0x7fffffffe9a8: 0x555546cd 0x00005555 0x00000000 0x00000001
|局部变量a| |局部变量b|
0x7fffffffe9b8: 0xffffe9e0 0x00007fff 0xffffeac8 0x00007fff
| main函数父函数的rbp | | main函数父函数的rip |
fun1:
----------------------
rsp:0x7fffffffe990 入栈main函数的rip
0x7fffffffe988 入栈main函数的rbp
0x7fffffffe970 分配栈空间
rbp:0x7fffffffe988
0x7fffffffe968: 0x00000000 0x00000000 0x00000001 0x00000000
|fun1参数b| |fun1参数a|
0x7fffffffe978: 0x756e6547 0x00000001 0x00000002 0x00000000
|局部变量c| |局部变量d|
0x7fffffffe988: 0xffffe9b8 0x00007fff 0x55554672 0x00005555
| main函数的rbp | | main函数的rip |
func2:
-----------------------
rsp: 0x7fffffffe968 入栈fun1函数的rip
0x7fffffffe960 入栈fun1函数的rbp
rbp: 0x7fffffffe960
0x7fffffffe960: 0xffffe988 0x00007fff 0x55554636 0x00005555
| fun2函数的rbp | | fun2函数的rip |
fun2返回时:
--------------------------
rsp:0x7fffffffe968 出栈fun1函数的rbp
0x7fffffffe970 出栈fun1函数的rip
rbp:0x7fffffffe988
fun1返回时:
-------------------------
rsp:0x7fffffffe990 出栈main函数的rbp
0x7fffffffe998 出栈main函数的rip
rbp:0x7fffffffe9b8
栈帧结构为:
总结一下:
通过对x86 64代码反汇编的分析,与aarch64还是有区别的,可以得出:
- 每个函数在入口处首先会分配栈空间,且一次分配,确定栈顶,之后rsp将不再变化;
- 每个函数的栈底部存放的是caller的栈底指针,即fun1的栈底存放的是main栈底指针;
- 对于最后一级callee函数,rbp指向自身栈底,如示例中fun2执行时,此时rbp指向fun2的栈底
总结一句话:
aarch64当前栈顶保存上一级函数的栈顶;x86_64当前栈底保存上一级函数的栈底
参考文档
- https://gcc.gnu.org/gcc-5/changes.html#arm
- https://f5.pm/go-30007.html
- https://developer.arm.com/documentation/ihi0038/b/
- 内核中dump_stack的实现原理(1) —— 栈回溯