函数的调用,栈帧的创建和销毁

来源:互联网 发布:centos git 备份 编辑:程序博客网 时间:2024/06/06 18:42


一、什么是栈帧?

     在进行函数的调用(栈帧)的分析前,我们先了解下 main 函数的调用吧,通过以前的学习,我们知道 main 函数也是被调用的,先在  __tmainCRTStartup  函数中调用,而 __tmainCRTStartup 函数是在 mainCRTStartup  中被调用的。

     恰如这样的调用,我们发现每一次函数调用都是一个过程,这个过程我们称之为:函数的调用过程

    函数调用过程要为函数开辟栈空间,用于本次函数的调用中临时变量的保存、现场保护。这块栈空间我们称之为函数栈帧栈帧栈帧(stack frame)也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每


栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的


部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的


个栈帧,这个栈帧中维持着所需要的各种信息。


   栈帧的维护需要我们了解 ebp 和 esp 两个寄存器,在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针。ebp 存放了指向函数栈帧栈底的地址esp 存放了指向函数栈帧栈顶的地址

寄存器ebp称为“基址指针”,在未受改变之前始终指向栈底,用途是:在堆栈中寻址。 

寄存器esp称为“栈指针”,会随着数据的入栈出栈移动,也就是说始终指向栈 顶。

 

 


栈帧结构如下所示:


二、栈帧的主要作用:

是用来控制和保存一个过程的所有信息的。我们平时说的堆栈其实是指栈,而实际上堆和栈是两种不

同的内存分配。简单罗列如下各方面的异同点。

1).堆需要用户在程序中显式申请,栈不用,由系统自动完成。申请/释放堆内存的API,在C中是malloc/free,在

C++中是new/delete。申请与释放一定要配对使用,否则会造成内存泄漏(memory leak),久而久之系统就无内存

可用了,出现OOM(Out Of Memory)错误。一般在return/exit或break/continue等语句时容易忘记释放内存,

所以检查内存泄漏的代码时要关注这些语句,看它们前面是否有必要的释放语句free/delete。

2).堆的空间比较大,栈比较小。所以申请大的内存一般在堆中申请;栈上不要有较大的内存使用,比如大的静态数

组;而且除非算法必要,否则一般不要使用较深的迭代函数调用,那样栈消耗内存会随着迭代次数的增加飞涨。

3).关于生命周期。栈较短,随着函数退出或返回,本函数的栈就完成了使用;堆就要看什么时候释放,生命周期就什

么时候结束。

、栈帧结构和函数调用过程


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


栈帧(stack Frame):一次函数调用包括将数据和控制代码从的一个部分传递到另外一个部分,栈帧与某个过程调用一


一映射。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向


当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低址地)。

 

函数调用规则:


        _cdecl:按从右至左的顺序压参数入栈,由调用者把参数弹出栈。由于每次函数调用都要由编译器产生清楚堆


栈的代码,所以使用_cdecl的代码比使用_stdcall的代码要大很多,但是这种方式支持可变参数。对于C函数,名字修


饰约定为在函数名前加下划线。对于C++,除非特变使用extern C,C++使用不同的名字修饰方式。


        _stdcall:按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。调用约定在输出函数名前加上一个下划


线前缀,后面加上一个“@”符号和其参数的字节数。


        _fastcall:主要特点就是快,因为它是通过寄存器来传送参数的,和__stdcall很象,唯一差别就是头两个参数


通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ECX,第2个进EDX,其他参数是


从右向左的入stack。返回仍然通过EAX。


四、堆和栈


(1)首先要清楚的是程序对内存的使用分为以下几个区:


1) 栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中


的栈。


2)   堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中


的堆是两码事,分配方式类似于链表。


3) 全局区(static):全局变量和静态变量存放在此。


4) 文字常量区:常量字符串放在此,程序结束后由系统释放。


5) 程序代码区:存放函数体的二进制代码。


 

 

  

(2)其次是堆和栈的申请方式:


     栈由系统自动分配,速度较快,在windows下栈是向低地址扩展的数据结构,是一块连续的内存区域,大小是

2MB。


   堆需要程序员自己申请,并指明大小,速度比较慢。在C中用malloc,C++中用new。另外,堆是向高地址扩展的


数据结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。因此堆空间获取和使用比较灵活,可用空间较


大。

 




现在我们以Add()函数为例深入的研究一下函数的调用过程。

先看一段简单的代码



     

当我们要详细研究函数调用过程,必须得对照的汇编代码。 

从main函数的地方开始,要展开main函数的调用就得为main函数创建栈帧。 



1)我们知道ebp和esp是用来维护函数的栈底指针和栈顶指针,push ebp,将__mainCRTStarup函数的ebp压栈,在

它的栈顶开辟一块空间放入它的ebp; 

2)将esp给ebp,此时ebp与esp同时指向刚刚开辟空间的顶端;(创建栈帧的过程) 

3)esp减去0e4h大小的值,我们知道栈空间中元素存放顺序是由高地址到低地址,则该步骤在ebp的上面开辟了0e4h

大小的内存空间; 

4)ebx,esi,edi三块空间进行压栈,随着压栈的进行,esp指向edi的顶端; 

5)将10赋给a,20赋给b。 

6)b的值,将它mov给eax寄存器并且压栈;同理a的值,将它mov给ecx寄存器并且压栈。 

7)接着调用call指令。注意,在调用call指令的时候,在ecx的上方又开辟了一块空间用于存放call指令下一条指令的地

址(这个地址的作用是在call指令调用add函数结束的的时候jump指令能够找到call指令下一条指令的地址,从而回

到main函数中) 

8)由call指令进入add函数之后,第一步就是进行压栈,将main函数的ebp压栈保存在上面开辟的空间中。 创建一个空间,放进去内容为0;存放了指向函数栈帧栈底的地址。esp存放了指向函数栈帧栈顶的地址。


注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈

底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。




五、栈帧的一般总结:

1. 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间:

➢ 函数调用框架;

➢ 传递参数;

➢ 保存返回地址;

➢ 提供局部变量空间;

以x86体系结构为例

2. 堆栈寄存器和堆栈操作

 堆栈相关的寄存器

➢ esp,堆栈指针(stack pointer)

➢ ebp,基址指针(base pointer)

堆栈操作

➢ push 栈顶地址减少4个字节(32位)

➢ pop 栈顶地址增加4个字节

❖ ebp在C语言中用作记录当前函数调用基址

3. 利用堆栈实现函数调用和返回

❖其他关键寄存器

➢ cs : eip:总是指向下一条的指令地址

● 顺序执行:总是指向地址连续的下一条指令

● 跳转/分支:执行这样的指令的时候, cs : eip的值会根据程序需要被修改

● call:将当前cs : eip的值压入栈顶, cs : eip指向被调用函数的入口地址

● ret:从栈顶弹出原来保存在这里的cs : eip的值,放在cs : eip中


原创粉丝点击