堆栈及函数调用栈帧

来源:互联网 发布:网上的淘宝刷手可靠吗 编辑:程序博客网 时间:2024/05/18 22:51

函数调用是程序设计中最为重要的一个环节,函数调用的操作包括从一块代码到另一块代码之间的双向数据传递和执行的过程。数据传递包括:函数参数和返回值。
大多数的cpu上的程序使用栈来实现函数的调用操作,栈用来传递函数的参数,保存返回的信息,存储寄存器中的数据。
单个函数的调用操作所使用的栈部分称为栈帧结构,栈帧结构的两端由两个指针来指定。寄存器ebp通常用作帧的指针,esp用作栈的指针,esp随着数据的入栈和出栈。因此对于函数中的大部分数据都是基于栈帧指针ebp来实现。
一、堆和栈
栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值。其操作方式类似与数据结构中的栈。
堆区(heap):一般由程序员进行分配释放,程序员不释放时由操作系统进行回收。和数据结构中的堆是两码事,分配方式类似与链表。
全局区(static):全局变量和静态变量存在这里。
文字常量区:常量字符存放在这里,程序结束后由系统进行释放。
程序代码区:存放函数体的二进制代码。
这里写图片描述
栈由系统自动分配,速度较快,windows下栈是向下的,栈是向低地址扩展的数据结构,一块连续的内存区域大小为2Mb。
堆由程序员自己申请释放,指明大小创建的速度较慢。在C、C++中用malloc和new分别进行创建,堆是向高地址扩展的数据结构。是不连续的内存块,堆的大小受限与计算机的虚拟内存的大小。
二、栈帧的结构和函数调用过程

栈在函数调用中的作用:参数传递、局部变量分配、保存调用的返回地址、保存寄存器以便恢复。
栈帧(stack Frame):本质是一个栈,不过这种栈专门用来保存函数调用过程中的相关信息(参数、返回地址、本地变量)。栈帧其实是两个指针寄存器,寄存器ebp为帧指针,寄存器esp为栈指针。程序运行栈指针可以移动,栈帧的主要作用是控制和保存一个过程的所以信息。
栈帧结构图如下:
这里写图片描述
函数调用规则:
_cdecl:从右至左的顺序压参数入栈,由调用者把参数弹出栈。由于每次函数调用都要由编译器产生清楚的堆栈代码。所以使用_cdecl比使用_stdcall的代码大很多,这种方式支持可变参数。对于C函数,名字修饰约定在函数名前面加下划线。对于C++,除非特变使用extern C,C++使用不同的名字修饰方式。

_stdcall:按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数。

_fastcall:主要特点就是快,因为它是通过寄存器来传送参数的,和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ECX,第2个进EDX,其他参数是从右向左的入stack。返回仍然通过EAX。
三、函数调用的实例

int MyFunction(int x, int y, int z){    int a, b, c;    a = 10;    b = 5;    c = 2;    ...}int TestFunction(){    int x = 1, y = 2, z = 3;    MyFunction1(1, 2, 3);    ...}

对于这个函数调用MyFunction()时的汇编代码如下:

_MyFunction:    push %ebp            ; //保存%ebp的值    movl %esp, $ebp      ; //%esp的值赋给%ebp,使新的%ebp指向栈顶    movl -12(%esp), %esp ; //分配额外空间给本地变量    movl $10, -4(%ebp)   ;     movl $5,  -8(%ebp)   ;     movl $2,  -12(%ebp)  ; 

调用者做了两件事 :
1、将被调用函数的参数按照从右到左的顺序压入栈中。
2、将返回地址压入栈中。

被调用者也做了两件事:
1、将老的(调用者的)ebp压入栈,此时,esp指向它。
2、将esp的值赋给ebp,这样ebp就有了新的值,它也指向存放老的ebp的栈空间。这样它就成了函数MyFunction()栈帧的栈底,我们就保存了调用者的ebp,并且创建了新的栈帧。

这样在ebp更新之后,先分配一块0x12字节的空间用于存放本地的变量,这一步由sub和mov指令实现。这样就可以给a,b,c赋值了。

函数的返回:
上面讲的是函数的调用过程,现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将 esp 移到 ebp 处,然后再弹出旧的 ebp 的值到 ebp。这样,ebp 就恢复到了函数调用前的状态了。

int MyFunction( int x, int y, int z ){    int a, int b, int c;    ...    return;}

汇编如下:

_MyFunction:    push %ebp    movl %esp, %ebp    movl -12(%esp), %esp    ...    mov %ebp, %esp    pop %ebp    ret

最后有一个 ret 指令,这个指令相当于 pop + jum。它首先将数据(返回地址)弹出栈并保存到 %eip 中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。

总结:
上面也就是关于对这一块知识的总结,对于函数调用这块抓住ebp和esp,就能明白函数如何通过栈帧进行调用和返回的。

0 0
原创粉丝点击