函数调用过程原理及函数栈帧分析
2017-08-10 14:36
281 查看
转载地址:https://segmentfault.com/a/1190000007977460
在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不同的数据、地址压入或者弹出栈。因此,为了更好地理解函数的调用,我们需要先来看看栈是怎么工作的。
简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。
这里是一个push操作的例子。假设我们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还未写入数据的区域。现在我们将0x50压入栈中:
我们再来看看pop操作的例子:
这里有两点需要注意的,第一,上面例子中栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的,因此这里也用这种形式的栈;第二,pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。有了这些栈的基本知识,我们现在可以来看看在x86-32bit系统下,C语言函数是如何调用的了。
栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用
一般来说,我们将
我们直接通过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,我们假设它被其他函数调用。
对于这个函数,当调用时,
光看代码可能还是不太明白,我们先来看看此时的栈是什么样的:
此时调用者做了两件事情:第一,将被调用函数的参数按照从右到左的顺序压入栈中。第二,将返回地址压入栈中。这两件事都是调用者负责的,因此压入的栈应该属于调用者的栈帧。我们再来看看被调用者,它也做了两件事情:第一,将老的(调用者的)
只要这步弄明白了,下面的操作就好理解了。在
上面讲的都是函数的调用过程,我们现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将
其汇编大致如下:
我们注意到最后有一个
到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原
在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不同的数据、地址压入或者弹出栈。因此,为了更好地理解函数的调用,我们需要先来看看栈是怎么工作的。
栈是什么?
简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。这里是一个push操作的例子。假设我们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还未写入数据的区域。现在我们将0x50压入栈中:
// 将0x50的压入栈 push $0x50
我们再来看看pop操作的例子:
// 将0x50弹出栈 pop
这里有两点需要注意的,第一,上面例子中栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的,因此这里也用这种形式的栈;第二,pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。有了这些栈的基本知识,我们现在可以来看看在x86-32bit系统下,C语言函数是如何调用的了。
栈帧是什么?
栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,我们用 %ebp指向栈底,也就是基址指针;用
%esp指向栈顶,也就是栈指针。下面是一个栈帧的示意图:
一般来说,我们将
%ebp到
%esp之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,
%ebp,
%esp等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。
函数调用实例
函数的调用
我们直接通过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,我们假设它被其他函数调用。int [b]MyFunction[/b](int x, int y, int z) { int a, b, c; a = 10; b = 5; c = 2; ... } int [b]TestFunction[/b]() { 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) ;
光看代码可能还是不太明白,我们先来看看此时的栈是什么样的:
此时调用者做了两件事情:第一,将被调用函数的参数按照从右到左的顺序压入栈中。第二,将返回地址压入栈中。这两件事都是调用者负责的,因此压入的栈应该属于调用者的栈帧。我们再来看看被调用者,它也做了两件事情:第一,将老的(调用者的)
%ebp压入栈,此时
%esp指向它。第二,将
%esp的值赋给
%ebp,
%ebp就有了新的值,它也指向存放老
%ebp的栈空间。这时,它成了是函数
MyFunction()栈帧的栈底。这样,我们就保存了“调用者”函数的
%ebp,并且建立了一个新的栈帧。
只要这步弄明白了,下面的操作就好理解了。在
%ebp更新后,我们先分配一块0x12字节的空间用于存放本地变量,这步一般都是用
sub或者
mov指令实现。在这里使用的是
movl。通过使用
mov配合
-4(%ebp),
-8(%ebp)和
-12(%ebp)我们便可以给
a,
b和
c赋值了。
函数的返回
上面讲的都是函数的调用过程,我们现在来看看函数是如何返回的。从下面这个例子我们可以看出,和调用函数时正好相反。当函数完成自己的任务后,它会将 %esp移到
%ebp处,然后再弹出旧的
%ebp的值到
%ebp。这样,
%ebp就恢复到了函数调用前的状态了。
int [b]MyFunction[/b]( 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中,然后处理器根据这个地址无条件地跳到相应位置获取新的指令。
总结
到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原 %ebp和
%esp,就能明白函数是如何通过栈帧进行调用和返回的了。希望这篇文章对你有帮助!
相关文章推荐
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- C++基础知识:c 函数调用过程原理及函数栈帧分析
- C函数调用过程原理及函数栈帧分析
- c语言函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- 函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- C函数调用过程原理及函数栈帧分析
- c函数调用过程原理及函数栈帧分析
- C函数调用原理及函数栈帧分析
- 堆栈、栈帧与函数调用过程分析
- 堆栈、栈帧与函数调用过程分析