在前面学习中我们可能有很多困惑比如
1.局部变量是怎样创建的
2.为什么局部变量的值是随机值
3.函数是怎样传参的,传参的顺序是怎样的
4.形参和实参的关系是什么
5.函数调用是怎么做的
6.函数调用是结束后怎么返回的
当你学会了函数栈帧的创建和销毁以上的内容就迎刃而解了
注意:不同的编译器函数调用过程中函数栈帧的创建和销毁的表示效果是不同的
前置概念
寄存器
集成在CPU中的就是寄存器高速临时存储单元,用于临时存放指令,数据,地址和状态信息,是CPU与内存,外设交互的中转站。寄存器的核心作用之一就是记录要访问的地址,在本文中核心涉及到两种寄存器:
1.ebp:基址指针,函数调用时指向当前栈帧的底部。
2.esp:栈指针,始终指向栈顶。
这两个寄存器就是用来维护正在调用的函数栈帧。
C/C++的内存划分
每次函数调用都需要从栈区申请空间,这块空间就是函数栈帧(由高地址向低地址使用)
比如说在栈区上为main函数开辟了一块空间,这块空间就称为main函数的函数栈帧。
此时esp和ebp就用来维护这块函数栈帧。
常见汇编指令的解释
push 压栈:在栈顶放一个元素,利用调整栈顶指针并写入内容
pop 出栈:从栈顶拿走一个元素,利用调整栈顶指针并删除数据
call:调用函数时,先将当前指令的下一条指令push到栈,再跳转到函数入口。
mov:将源操作数的值复制到目标操作数
在VS013中main函数也是被其他函数(_tmainCRTStartup)调用的,所以在调用的过程中就有在栈区对main函数空间的开辟。下面我们来了解一下main函数栈帧是如何开辟的
示例代码:
#include
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);
printf("%d\n", c);
return 0;
}
main函数栈帧的开辟
下面就是调用main函数的汇编代码
调用main函数之前,esp,ebp在维护_tmainCRTStartup的函数栈帧
1.压栈了一个ebp存储的地址(4字节)这个地址是指向_tmainCRTStartup函数的栈底
2.把esp的值给ebp
3.esp减去0E4h(228)
目前为止,成功为main函数开辟了一块空间,大小是228字节,注意这个空间的大小是由编译器和系统决定,并不是一个固定值。下面就是对这块空间进行初始化
4.通过进行了三次压栈,压栈的内容不用管,只需知道有这个过程就能够了
5.将ebp-0E4h(228)之后的地址放到edi中,将39h(57)放到ecx,0XCCCCCCCCh放到eax。
6.从edi的位置开始,往下39h(57)个doubleword(4字节)的内容改成eax的内容
不难发现57*4=228正好等于为main函数开辟空间的大小,也就是说这个过程就是将main函数栈帧初始化为0XCCCCCCCC。
目前为止就结束了对main函数栈帧的开辟和初始化。
局部变量的创建(问题1、2)
1.把0Ah(10)放到ebp-8的位置,此时变量a的地址就是ebp-8
想象一下,若是没有给变量a赋值,那么变量a里面储存的就是0XCCCCCCCC,因为不同编译器对main函数初始化不同,所以一个随机值。就是未初始化的局部变量
ADD函数的传参以及ADD函数栈帧的开辟(挑战3、4、5)
1.将ebp(main)-14h(b)的值放到eax中,也就是将局部变量b的值放进eax,并将eax压栈。此时形参b的地址就是ebp(main)-14h
2.将ebp(main)-8(a)的值放进ecx中,也就是将局部变量a的值放进ecx,并将ecx压栈。此时形参a的地址就是ebp(main)-8。
3.开始调用ADD函数,并将call指令下一条指令压栈。
进入ADD函数,依旧先是为ADD函数开辟空间,并对空间进行初始化,与main函数方法相同
进行计算任务,并返回(困难6)
1.将ebp(ADD)+8(形参a)的值放到eax中,然后与ebp(ADD)+0Ch(形参b)的值相加。最后将eax中的加和放到ebp(ADD)-8(局部变量z)中
在函数还没调用的时候,先进行压栈为形参开辟空间,然后将实参的值拷贝到这块空间。真正进入函数内部进行x+y的时候,是通过指针偏移来找到形参。注意:在这个过程中,只在ADD函数栈帧中进行了z变量的创建和参数求和,并没有在ADD函数中创建形参,允许认为形参是在main函数的函数栈帧内部。
所以说形参是实参的临时拷贝,在函数中对形参的修改并不会影响实参。
2.最后将ebp(ADD)-8(变量z)的值放到eax中,由于出函数之后函数栈帧销毁,所以要暂时存到寄存器中
3.弹出edi,esi,ebx。
现在开始回收ADD函数的空间
4.把ebp的值赋给esp
5.弹出栈底的数据,并储存在ebp中,而这个资料就是main函数的栈底地址。现在ebp就指向main函数的栈底,esp指向call指令的下一条指令,接着维护main函数。
6.ret:回到call指令下一条指令的地址,同时esp+4
esp+8,将两个形参的内存还给操作系统。
7.
将eax的值(返回值)放到ebp-20h(变量c)中
总结
1.局部变量通过在栈区开辟空间来创建
2.随机值是在main函数开辟的时候放进去的
3.在函数没调用之前,利用压栈将参数拷贝到栈顶,然后借助指针的偏移对参数进行引用
4.形参是通过压栈开辟出的一块独立的空间,并将实参的值复制到这块空间中。形参和实参是两块独立的空间,所以说形参是实参的一个临时拷贝。传参顺序是从右往左。
5.先将call指令的下一条指令的地址压栈,便于函数结束时定位到指令,然后开始开辟空间并对其进行初始化。
6.通过寄存器将返回值带出