栈中函数调用原理_详解

来源:互联网 发布:php 文本封装超链接 编辑:程序博客网 时间:2024/05/17 22:31


 

           
函数调用是程序设计中的重要环节,本文就函数调用的过程进行分析。

一、eip、ebp、esp介绍

 EIP,EBP,ESP都是系统的寄存器,里面存储的是些地址,我们系统中栈的实现上离不开他们三个。 我知道栈的数据结构主要特点是 后进先处。它还有两个作用: 1.栈是用来存储临时变量,函数传递的中间结果。 2.操作系统维护的,对于程序员是透明的。

下面我们就通过一个小例子说说栈的原理。

先写个小程序:

 1 2 3 4 5 6 7 8 9
void fun(void)
{
printf("helloworld");
}
void main(void)
{
fun()
printf("函数调用结束");
}
来自CODE的代码片
snippet_file_0.txt

当程序进行函数调用的时候,我们经常说的是先将函数压栈,当函数调用结束后,再出栈。这一切的工作都是系统帮我们自动完成的。但在完成的过程中,系统会用到下面三种寄存器:EIP、ESP、EBP。

当调用fun函数开始时,三者的作用。

  1. EIP寄存器里存储的是CPU下次要执行的指令的地址。 也就是调用完fun函数后,让CPU知道应该执行main函数中的printf("函数调用结束")语句了。
  2. EBP寄存器里存储的是是栈的栈底指针,通常叫栈基址,这个是一开始进行fun()函数调用之前,由ESP传递给EBP的。(在函数调用前你可以这么理解:ESP存储的是栈顶地址,也是栈底地址。)
  3. ESP寄存器里存储的是在调用函数fun()之后,栈的栈顶。并且始终指向栈顶。
当调用fun函数结束后,三者的作用:

  1. 系统根据EIP寄存器里存储的地址,CPU就能够知道函数调用完,下一步应该做什么,也就是应该执行main函数中的printf(“函数调用结束”)。
  2. EBP寄存器存储的是栈底地址,而这个地址是由ESP在函数调用前传递给EBP的。等到调用结束,EBP会把其地址再次传回给ESP。所以ESP又一次指向了函数调用结束后,栈顶的地址。

二、堆和栈

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

  1. 栈区(stack):由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似于数据结构中的栈。
  2. 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表。
  3. 全局区(static):全局变量和静态变量存放在此。
  4. 文字常量区:常量字符串放在此,程序结束后由系统释放。
  5. 程序代码区:存放函数体的二进制代码。

典型的内存区域分配如图所示:


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

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

首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。下图为典型的存取器安排,观察栈在其中的位置


入栈操作:push eax; 等价于 esp=esp-4,eax->[esp];如下图


出栈操作:pop eax; 等价于 [esp]->eax,esp=esp+4;如下图


我们来看下面这个C程序在执行过程中,栈的变化情况

  1  2  3  4  5  6  7  8  9 10 11
void func(int m, int n) {
int a, b;
a = m;
b = n;
}
main() {
...
func(m, n);
L: 下一条语句
...
}
来自CODE的代码片
snippet_file_0.txt

在main调用func函数前,栈的情况,也就是说main的栈帧:


从低地址esp到高地址ebp的这块区域,就是当前main函数的栈帧。当main中调用func时,写成汇编大致是:

push m

push n; 两个参数压入栈

call func; 调用func,将返回地址填入栈,并跳转到func


当跳转到了func,来看看func的汇编大致的样子:

__func:

push ebp; 这个很重要,因为现在到了一个新的函数,也就是说要有自己的栈帧了,那么,必须把上面的函数main的栈帧底部保存起来,栈顶是不用保存的,因为上一个栈帧的顶部讲会是func的栈帧底部。(两栈帧相邻的)

       mov ebp, esp; 上一栈帧的顶部,就是这个栈帧的底部;暂时先看现在的栈的情况


到这里,新的栈帧开始了

sub esp, 8;  int a, b 这里声明了两个int,所以esp减小8个字节来为a,b分配空间

mov dword ptr [esp+4],[ebp+12];   a=m

mov dword ptr [esp], [ebp+8];b=n         

这样,栈的情况变为:


ret 8 ;  返回,然后8是什么意思呢,就是参数占用的字节数,当返回后,esp-8,释放参数m,n的空间。由此可见,通过ebp,能够很容易定位到上面的参数。当从func函数返回时,首先esp移动到栈帧底部(即释放局部变量),然后把上一个函数的栈帧底部指针弹出到ebp,再弹出返回地址到cs:ip上,esp继续移动划过参数,这样,ebp,esp就回到了调用函数前的状态,即现在恢复了原来的main的栈帧。

0 0
原创粉丝点击