函数栈帧的调用过程

来源:互联网 发布:在职研究生 知乎 编辑:程序博客网 时间:2024/06/05 09:51

初学者,个人总结,不介意做参考



说到函数栈帧的调用过程,我们应当首先了解一下以下内容:


一、堆和栈
首先要清楚的是程序对内存的使用分为以下几个区:
l        栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
l         堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。
l         全局区(static):全局变量和静态变量存放在此。
l         文字常量区:常量字符串放在此,程序结束后由系统释放。
l         程序代码区:存放函数体的二进制代码。
典型的内存区域分配如图所示:


 
 
  
其次是堆和栈的申请方式:
栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是2MB。
堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc,C++中用new。另外,堆是向高地址扩展的数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较大。
 
二、栈帧结构和函数调用过程


栈在函数调用中的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以供恢复。


栈帧(stack Frame):一次函数调用包括将数据和控制从代码的一个部分传递到另外一个部分,栈帧与某个过程调用一一映射。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低址地)。
 
函数调用规则:
l         _cdecl:按从右至左的顺序压参数入栈,由调用者把参数弹出栈。由于每次函数调用都要由编译器产生清楚堆栈的代码,所以使用_cdecl的代码比使用_stdcall的代码要大很多,但是这种方式支持可变参数。对于C函数,名字修饰约定为在函数名前加下划线。对于C++,除非特变使用extern C,C++使用不同的名字修饰方式。
l         _stdcall:按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数。
l         _fastcall:主要特点就是快,因为它是通过寄存器来传送参数的,和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ECX,第2个进EDX,其他参数是从右向左的入stack。返回仍然通过EAX。

三:我们通过一个函数来详细阐述函数栈帧的调用过程

#include<stdio.h>


int  _add(int a,inrt b)

{

     int temp =0;

     temp=a+b;

    return   temp;
}

int main()

{
    int a=10;

   int b=20;

    int ret=_add(a,b);

    return 0;

}

栈帧:机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实是两个指针寄存器,寄存器ebp为帧指针,而寄存器esp为栈指针,当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的)。总之简单一句话,栈帧的主要作用是用来控制和保存一个过程的所有信息的。

栈上的地址分为高地址和低地址,而我们的ebp就是指向我们栈底的指针,esp是指向我们栈顶的指针。由于栈是由高地址向低地址开辟,所以我们的ebp就相当于位于高地址,esp位于我们的低地址。

    我们首先是进入main函数,我们要做的第一件事就是在内存的栈中为我们的函数开辟一个栈帧,姑且把我们的下方作为高地址,我们的上方作为低地址,也就是如图所示,我们的最下方为我们的ebp栈底指针,地址我们暂时随便给一个0x100,上面说过在栈上我们是由高地址向低地址开辟空间,所以我们的最上方就给是我们的栈顶指针寄存器esp。接下来就是为a开辟四字节空间,为b开辟四字节空间。然后为ret开辟四字节空间。接着就是调用_add函数。所以我们就在我们的main函数,也就是调用者这里把函数中的参数压入栈中,就是图中最上面的地方。这时候我们会想:我调用完_add函数,我怎么回来呢?我的地址怎么找?所以我们这里就要用到汇编中call这个命令,这个命令会记录下你调用函数下一行的地址,也就是你调用完函数回来的地方。我们想一下,函数的各种运行就是ebp和esp两个指针各种偏移实现的。更准确的说是栈底指针ebp的偏移实现的。所以我的栈底指针还在main函数那里,这怎么行,所以马上让栈底指针等于栈顶指针,这样就把栈底指针寄存器ebp移到了我们esp的位置。

    接下来就是我们来调用我们的_add函数了。首先和main函数一样,进来的时候我们要先给函数开辟栈帧,然后我们的栈顶指针就跑了过去。接下来就是为temp申请一段空间,将a放入一个叫做eax的寄存器中,然后将b+a放入eax寄存器中。然后将寄存器eax放到temp的地址中。调用return语句,将temp的值通过寄存器eax带出来,返回到我们主函数中,这时,主函数中有一个叫pc寄存器的指针来接收eax带出来的值,并将其放入我们ret的地址中。然后我们的ebp回到我们main函数的栈底,esp回到压栈之前的位置,相当于释放掉_add的地址空间。