C函数调用过程原理及函数栈帧分析

在x86的计算机系统中,内存空间中的栈主要用于保存函数的参数,返回值,返回地址,本地变量等。一切的函数调用都要将不一样的数据、地址压入或者弹出栈。所以,为了更好地理解函数的调用,咱们须要先来看看栈是怎么工做的。html

栈是什么?

简单来讲,栈是一种LIFO形式的数据结构,全部的数据都是后进先出。这种形式的数据结构正好知足咱们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操做,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。sass

这里是一个push操做的例子。假设咱们有一个栈,其中黄色部分是已经写入数据的区域,绿色部分是还未写入数据的区域。如今咱们将0x50压入栈中:数据结构

// 将0x50的压入栈
push $0x50

图一:压栈操做

咱们再来看看pop操做的例子:ide

// 将0x50弹出栈
pop

图二:出栈操做

这里有两点须要注意的,第一,上面例子中栈的生长方向是从高地址到低地址的,这是由于在下文讲的栈帧中,栈就是向下生长的,所以这里也用这种形式的栈;第二,pop操做后,栈中的数据并无被清空,只是该数据咱们没法直接访问。有了这些栈的基本知识,咱们如今能够来看看在x86-32bit系统下,C语言函数是如何调用的了。函数

栈帧是什么?

栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程当中的各类信息(参数,返回地址,本地变量等)。栈帧有栈顶和栈底之分,其中栈顶的地址最低,栈底的地址最高,SP(栈指针)就是一直指向栈顶的。在x86-32bit中,咱们用 %ebp 指向栈底,也就是基址指针;用 %esp 指向栈顶,也就是栈指针。下面是一个栈帧的示意图:学习

图三:栈帧示意图

通常来讲,咱们将 %ebp%esp 之间区域当作栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并非整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程当中,咱们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程当中,1)“调用者”须要知道在哪里获取“被调用者”返回的值;2)“被调用者”须要知道传入的参数在哪里,3)返回的地址在哪里。同时,咱们须要保证在“被调用者”返回后,%ebp, %esp 等寄存器的值应该和调用前一致。所以,咱们须要使用栈来保存这些数据。ui

函数调用实例

函数的调用

咱们直接经过实例来看函数是如何调用的。这是一个有参数但没有调用任何函数的简单函数,咱们假设它被其余函数调用。spa

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)  ;

光看代码可能仍是不太明白,咱们先来看看此时的栈是什么样的:code

图四:被调用者栈帧的生成

此时调用者作了两件事情:第一,将被调用函数的参数按照从右到左的顺序压入栈中。第二,将返回地址压入栈中。这两件事都是调用者负责的,所以压入的栈应该属于调用者的栈帧。咱们再来看看被调用者,它也作了两件事情:第一,将老的(调用者的) %ebp 压入栈,此时 %esp 指向它。第二,将 %esp 的值赋给 %ebp, %ebp 就有了新的值,它也指向存放老 %ebp 的栈空间。这时,它成了是函数 MyFunction() 栈帧的栈底。这样,咱们就保存了“调用者”函数的 %ebp,而且创建了一个新的栈帧。

只要这步弄明白了,下面的操做就好理解了。在 %ebp 更新后,咱们先分配一块0x12字节的空间用于存放本地变量,这步通常都是用 sub 或者 mov 指令实现。在这里使用的是 movl。经过使用 mov 配合 -4(%ebp), -8(%ebp)-12(%ebp) 咱们即可以给 a, bc 赋值了。

图五:本地变量赋值后的栈帧

函数的返回

上面讲的都是函数的调用过程,咱们如今来看看函数是如何返回的。从下面这个例子咱们能够看出,和调用函数时正好相反。当函数完成本身的任务后,它会将 %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 中,而后处理器根据这个地址无条件地跳到相应位置获取新的指令。

图六:被调用者返回后的栈帧

总结

到这里,C函数的调用过程就基本讲完了。函数的调用其实不难,只要搞懂了如何保存以及还原 %ebp%esp,就能明白函数是如何经过栈帧进行调用和返回的了。但愿这篇文章对你有帮助!

引用

在我学习栈帧以及写这篇文章的过程当中,参考了下面这些文章,在这我感谢他们对我提供的大力的帮助。若是你对这些文章感兴趣,请访问如下连接:
1. x86 Instruction Set Reference
2. x86 Disassembly/Functions and Stack Frames
3. x86 Assembly Guide

相关文章
相关标签/搜索