重新补了一定的C基础和C++基础后,来结合csapp把函数栈帧简单复习一下
1、什么是函数栈帧
如果接触过指针,知道指针的大小在32位下是四个字节,在64位下是八个字节。那么指针的大小,就定义为一帧的大小。这就是栈帧。
什么是函数栈帧呢?
首先在代码的数据和函数存储,可以大致分为几个区。
- 栈区:存放局部变量,形式参数
- 堆区:动态内存分配,如malloc,calloc,realloc,free
- 静态区:存放全局变量,静态变量
当然在链接那一块更加细分,不过这里大致分成这样就可以。
那函数栈帧实际上就是每一次的函数调用都要在内存中开辟空间,开辟的那一块就叫函数栈帧。
那函数栈帧里面存什么,存放一些变量数据。
2、代码的反汇编
我们可以在GCC反汇编或者VS反汇编下来观察。两者的汇编代码语法略有不同。不过可以互通。这里分别展示VS和GCC底下的反汇编。GCC反汇编想显示代码名字编译的时候±g
#include<stdio.h>
using namespace std;
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
return 0;
}
这一串代码背后到底发生了什么?如何看到背后的问题,VS有反汇编。
注:反汇编要在调试开始后,在调试–窗口部分–反汇编。
- VS
int main()
{
00641770 push ebp
00641771 mov ebp,esp
00641773 sub esp,0E4h
00641779 push ebx
0064177A push esi
0064177B push edi
0064177C lea edi,[ebp-0E4h]
00641782 mov ecx,39h
00641787 mov eax,0CCCCCCCCh
0064178C rep stos dword ptr es:[edi]
0064178E mov ecx,offset _DA109D2B_te@cpp (064C003h)
00641793 call @__CheckForDebuggerJustMyCode@4 (064120Dh)
int a = 10;
00641798 mov dword ptr [a],0Ah
int b = 20;
0064179F mov dword ptr [b],14h
int c = 0;
006417A6 mov dword ptr [c],0
c = Add(a, b);
006417AD mov eax,dword ptr [b]
006417B0 push eax
006417B1 mov ecx,dword ptr [a]
006417B4 push ecx
006417B5 call Add (0641186h)
006417BA add esp,8
006417BD mov dword ptr [c],eax
return 0;
006417C0 xor eax,eax
}
-
GCC
-
00000000000005fa <Add>: 5fa: 55 push %rbp 5fb: 48 89 e5 mov %rsp,%rbp 5fe: 89 7d ec mov %edi,-0x14(%rbp) 601: 89 75 e8 mov %esi,-0x18(%rbp) 604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 60b: 8b 55 ec mov -0x14(%rbp),%edx 60e: 8b 45 e8 mov -0x18(%rbp),%eax 611: 01 d0 add %edx,%eax 613: 89 45 fc mov %eax,-0x4(%rbp) 616: 8b 45 fc mov -0x4(%rbp),%eax 619: 5d pop %rbp 61a: c3 retq 000000000000061b <main>: 61b: 55 push %rbp 61c: 48 89 e5 mov %rsp,%rbp 61f: 48 83 ec 10 sub $0x10,%rsp 623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp) 62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp) 631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 638: 8b 55 f8 mov -0x8(%rbp),%edx 63b: 8b 45 f4 mov -0xc(%rbp),%eax 63e: 89 d6 mov %edx,%esi 640: 89 c7 mov %eax,%edi 642: e8 b3 ff ff ff callq 5fa <Add> 647: 89 45 fc mov %eax,-0x4(%rbp) 64a: b8 00 00 00 00 mov $0x0,%eax 64f: c9 leaveq 650: c3 retq 651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 658: 00 00 00 65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
相比之下我觉得GCC的反汇编更好看一点,但是两者本质是一样的。这里我选择用GCC的来分析。
-
在这里帮助理解的几个汇编指令就是push和call以及ret。以及需要提及的几个寄存器
-
寄存器
- eax
- ebx
- ecx
- edx
- ebp
- esp
- 其中ebp和esp这2个寄存器中存放的是地址。这2个地址是用来维护函数栈帧的。每一个函数调用都要创建一个空间。 ebp是基指针,esp是栈指针。另外%rip是程序下一条指令的地址。
-
push xxx指令
- Push指令的作用就是把当前的值放到栈里面去,然后将栈顶拓展.对应的就是将esp-1. 【这里描述的地址是下低上高】
-
call指令
-
call指令调用的过程
-
其中%rip 是存放程序计数器的值。 %rsp存放栈指针。
-
当调用了call之后,将%rsp往下扩展,并在新的栈帧中放入call后的指令的地址,用以返回的时候回到这个地方。同时更改rip的值为call的值。
-
-
-
ret指令
- 用于返回,其对应于return。
- 作用是弹出栈顶的值赋值给%rip并将%rsp回收空间
3、反汇编代码的分析与栈帧
#include<stdio.h>
using namespace std;
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
c = Add(a, b);
return 0;
}
00000000000005fa <Add>:
5fa: 55 push %rbp
5fb: 48 89 e5 mov %rsp,%rbp
5fe: 89 7d ec mov %edi,-0x14(%rbp)
601: 89 75 e8 mov %esi,-0x18(%rbp)
604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
60b: 8b 55 ec mov -0x14(%rbp),%edx
60e: 8b 45 e8 mov -0x18(%rbp),%eax
611: 01 d0 add %edx,%eax
613: 89 45 fc mov %eax,-0x4(%rbp)
616: 8b 45 fc mov -0x4(%rbp),%eax
619: 5d pop %rbp
61a: c3 retq
000000000000061b <main>:
61b: 55 push %rbp
61c: 48 89 e5 mov %rsp,%rbp
61f: 48 83 ec 10 sub $0x10,%rsp
623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp)
62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp)
631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
638: 8b 55 f8 mov -0x8(%rbp),%edx
63b: 8b 45 f4 mov -0xc(%rbp),%eax
63e: 89 d6 mov %edx,%esi
640: 89 c7 mov %eax,%edi
642: e8 b3 ff ff ff callq 5fa <Add>
647: 89 45 fc mov %eax,-0x4(%rbp)
64a: b8 00 00 00 00 mov $0x0,%eax
64f: c9 leaveq
650: c3 retq
651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
658: 00 00 00
65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
-
Main函数过程调用Add前分析
-
61b: 55 push %rbp 61c: 48 89 e5 mov %rsp,%rbp 61f: 48 83 ec 10 sub $0x10,%rsp 623: c7 45 f4 0a 00 00 00 movl $0xa,-0xc(%rbp) 62a: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp) 631: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 638: 8b 55 f8 mov -0x8(%rbp),%edx 63b: 8b 45 f4 mov -0xc(%rbp),%eax 63e: 89 d6 mov %edx,%esi 640: 89 c7 mov %eax,%edi
-
对于这部分的汇编代码和图的简要解释
- 在(61b)之前的是调用main函数之前的栈帧,main函数也是函数,它是被其他函数调用的。(再下去就要涉及os的一些知识了)
- push %rbp
- 保存此时的%rbp值,目的是为了之后能返回上一个调用main函数的%rbp值
- 两个打⭐的%rbp是同一个值。
- mov %rsp,%rbp
- 是将%rbp的值赋给%rsp,此时%rsp和%rbp指向一块位置。目的是为了新函数的管理
- sub $0x10,%rsp
- %rsp往下,实际上就是开辟main函数的栈帧空间
- %rsp和%rsp一起此时对应一个区域,能管理这块栈帧
- movl $0xa,-0xc(%rbp)
- 给a赋值10
- movl $0x14,-0x8(%rbp)
- 给b赋值20
- movl $0x0,-0x4(%rbp)
- 给c赋值0
- mov -0x8(%rbp),%edx
- %edx保存b的值,准备传参
- mov -0xc(%rbp),%eax
- %eax保存a的值,准备传参
- mov %edx,%esi
- 实际上这里就是Add函数参数中的对形参的一个赋值(c++中就是拷贝)
- mov %eax,%edi
- 实际上这里就是Add函数参数中的对形参的一个赋值(c++中就是拷贝)
- 可以发现,C语言中给被调函数参数赋值的过程实际上是调用函数准备的。
-
-
Add过程分析
-
00000000000005fa <Add>: 5fa: 55 push %rbp 5fb: 48 89 e5 mov %rsp,%rbp 5fe: 89 7d ec mov %edi,-0x14(%rbp) 601: 89 75 e8 mov %esi,-0x18(%rbp) 604: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 60b: 8b 55 ec mov -0x14(%rbp),%edx 60e: 8b 45 e8 mov -0x18(%rbp),%eax 611: 01 d0 add %edx,%eax 613: 89 45 fc mov %eax,-0x4(%rbp) 616: 8b 45 fc mov -0x4(%rbp),%eax 619: 5d pop %rbp 61a: c3 retq
-
12行的过程和main函数开始的12行的过程是一样的。
- 同样是为了基指针和栈指针的管理
-
3~6行则是函数内部的一个初始化
- 可以看到,此时的Add函数内部的参数存放在栈空间中。所以能理解为什么说函数内的局部变量存放在栈空间中。
-
7~8行将参数的值复制到寄存器中进行运算,结果保存在%eax中。【同时注意一般函数的调用参数和返回的寄存器都是有约定俗称的协议,如11行一般以%eax作为返回值的寄存器】
-
9行完成运算,此时运算后的值存在%eax中
-
10行,将函数内的局部变量c的值赋值为计算得到的答案
-
11行,此时将局部变量c的值(栈空间的值赋值给%eax),准备返回
- 可能有些奇怪为什么看似多此一举?这是对应C/C++中,值返回的时候,是将值拷贝给一个临时变量,再返回的。
-
12行的pop %rbp。
- 将%rbp获得此时栈顶指针指向的空间的内容。也就是回到main函数的时候%rbp的值
- %rsp回收空间。
-
13行的retrun。
- 程序计数器器(%rip)跳转回Call Add指令的下一条指令的地址。
- %rsp回收空间
- 此时回到Main函数
10.
-
-
回到Main函数后的过程
-
647: 89 45 fc mov %eax,-0x4(%rbp) 64a: b8 00 00 00 00 mov $0x0,%eax 64f: c9 leaveq 650: c3 retq 651: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 658: 00 00 00 65b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
-
1行–将获得返回值赋值给Main里的c
-
2行–清空%eax
-
3行–leava
-
ATT汇编的leava等价于
-
movl %ebp, %esp
popl %ebp 【32位下】
-
若为64bits 将使用rbp和rsp 寄存器
-
-
执行的是和Add函数pop的时候一样的操作
-
-
4行retq
- 和Add函数的ret一样,Main也是函数,也是要返回到上一层
-
至此就完成了该程序的基本运行。
然而每个编译器的具体处理情况略有不同。
再来看眼VS下的Add()
VS下的Add就在004516F3的位置又进行了开辟Add函数空间的操作。
而且存在对空间进行初始化rep stos。其中这也是为什么VS用未初始化的空间一般调试出来的是烫烫烫烫烫烫。因为vs一般用该字节码进行空间的初始化。
4、函数栈帧的创建和销毁与C以及C++的联系
比如
- 函数传参的形参拷贝
- 函数值返回时的临时变量
- 对被调用函数临时变量传址返回或者传引用返回的error
- 该空间地址完全可以被其他指令覆盖,导致错误输出。