函数堆栈调用的步骤:
1.开辟形参内存并进行初始化;
2.压入下一行指令的地址;
3.压入调用方栈底指针的值;
4.开辟全局变量所需的栈空间并初始化。
堆栈的定义:
编译器一般使用堆栈实现函数调用。堆栈是存储器的一个区域,嵌入式环境有时需要程序员自己定义一个数组作为堆栈。Windows为每个线程自动维护一个堆栈,堆栈的大小可以设置。编译器使用堆栈来堆放每个函数的参数、局部变量等信息。
栈上的两个指针:
在栈上开辟内存后,在栈上访问变量和数据都是通过访问ebp指针的偏移量来实现。一个栈用两个指针来表示,其中ebp为栈底指针,esp为栈顶指针。从下往上看,栈底是高地址,栈顶是低地址。
堆栈相关的寄存器:
ebp:栈底指针寄存器---------->指向系统栈最上面一个栈帧的底部;
esp:栈顶指针寄存器---------->指向系统栈最上面一个栈帧的栈顶;
下面通过实例来进行函数堆栈的调用过程:
int sum(int a,int b)
{
int tmp = 0;
tmp = a+b;
return tmp;
}
int main()
{
int a = 10;
int b = 20;
int prt=0;
prt = sum(a,b);
printf("prt = %d\n",prt);
}
下面描述具体的调用过程:
一、压栈:
调用方:(每次压栈esp都向上移动)
1.将形参变量的地址和值压栈:由下向上将 a =10 ,b = 20 ,ret = 0 依次压入函数栈帧。
2.将实参压栈:从反汇编中可以看出先将 b的值 20 放到eax寄存器中,然后eax入栈,再将a的值 10 放到ecx寄存器中,然后ecx入栈。所以形参是在调用方开辟内存。在调用函数时,实参是从右向左入栈,这样做的目的是可以确定参数的个数和大小。
3.调用 call 指令,跳转到下一行指令的函数,将下一行指令的地址入栈。
被调用方:
1.ebp压栈:将调用方函数的栈底地址记录到被调用方函数栈底中(上图中我们假设地址为0X100),为了方便清栈时esp回退到调用方栈底。将esp 赋给 ebp 。从上图中我们可以看到,紫色的框是调用方函数的栈帧。ebp压栈的内存就是被调用方函数的栈底。
2.返回prt:将a = 10 和 b = 20 相加赋给prt。通过eax寄存器将prt的值带回.
二、清栈:
1.在调用方调用完sum函数后,系统会对被调用方的地址空间进行回收,进行清栈操作,最后对调用方函数进行清栈,整个程序运行结束。ebp栈底指针又重新指向栈的底部。
从程序开始运行申请开辟地址空间,到程序运行结束进行清栈操作,这是次完整的函数堆栈调用过程。
小结:
1.形参在哪里开辟内存?
形参所需的内存由调用方进行开辟;
2.形参的入栈顺序?
形参入栈顺序为由右向左进行入栈,这样能够确定参数的个数以及大小;
3.sum 函数如何回退到main 函数?
一般函数的返回值由寄存器带出,当返回值为4个字节时,由eax寄存器带出;为8个字节时,由eax和edx寄存器带出;大于8个字节时,返回值由临时变量带出,临时变量空间在调用方栈上开辟,一个eax寄存器中保存临时变量的地址。