程序员应了解的那些事(28) 函数调用过程中的栈帧结构及其变化

前言:本文旨在从汇编代码的角度出发,分析函数调用过程中栈帧的变化。
※栈帧的简单介绍:
       当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧。下图描述了栈帧的通用结构。栈帧是一段有界限的内存区间,由最顶端的两个指针界定,寄存器%ebp为基址指针寄存器(extended base pointer),而寄存器%esp为栈指针寄存器(extended stack pointer)(也就是说寄存器%ebp保存了所分配内存的最高地址,寄存器%esp保存了所分配内存的最低地址)。当程序执行时,栈指针(栈顶)可以移动,因此大多数信息的访问都是相对于桢指针的。

※函数调用前:
       在函数被调用之前,调用者会为调用函数做准备,具体来说就是传参。准备被调用函数需要的参数。就上图而言,当前帧是为调用函数而开辟的栈帧,而参数1~参数n就是调用者传给被调用的参数,在当前帧中体现为设置参数构造区域。我们可以看到在当前帧上面仅挨着的是一个返回地址,这个返回地址是什么?
      调用者在调用完函数以后,肯定需要从下一条指令接着执行。而这个地址就是下一条指令的地址。调用函数的指令为:
call <函数名>。这里的call就实现了下一地址的储存。具体来说,call指令执行时,先把下一条指令的地址入栈,再跳转到对应函数执行的起始处。

       我们看到<+37>这一行再调用sum()函数。我们先看之前的几行:先设置变量值,再设置参数构造区,最后再调用函数。虽然这只是一个简单的例子,但是对于绝大数的调用来说,都符合这个规则,就是在调用函数前的几行,做的事情就是构造函数的参数。设置完成后再执行call。执行call的时候,先把下一条指令的地址入栈,再跳转。 

※被调用函数运行时:
     
 这里直接对代码进行说明。我们先查看sum函数的汇编代码:

        我们看到函数的前两行:
        push    %ebp
        mov     %esp,%ebp

        对于绝大部分函数来说,前面的两行都跟这两行一样,我们来具体分析这两句干了什么: 

        首先是 push    %ebp。当前%ebp保存的是调用者栈帧的栈底地址,那么push    %ebp就是将调用者栈帧的栈底地址压入栈,即保存旧的%ebp。
        接着是mov   %esp,%ebp。我们刚刚把旧的%ebp的值保存了下来,但是%ebp值并没有发生改变,而我们现在在执行一个新的函数,那么%ebp保存的应该是新的栈帧的栈底。所以才把当前%esp储存的地址赋值给%ebp。(这里说明一下,对于每一次push操作,%esp储存的地址会-1)。那么这样一来的话,相当于调用者栈帧的栈顶现在作为了新的栈帧的栈底(并且该栈底保存的是调用者栈帧的栈底地址,请记住这一点)。而此时新的栈帧的栈底和栈底位于同一个位置。
        而我们在函数里面是要执行各种操作的,所以我们需要给新栈帧分配一定的内存。这也就是后面接着的:sub $0x10,%esp。将%esp低地址移动16个字节。有了这么多的储存空间,才能支持函数里面的各种操作(也就是图中所述)。其实在这之前,还可能有一些push 语句,比如push %ebx之类的。这些push操作的目的其实同push %ebp差不多,都是保存调用者的值,以便在函数运行完以后再恢复数据。
       再接着就是利用储存空间执行具体的操作了。最后说明一点的是,函数的返回值一般储存在%eax寄存器中。这个看上述代码也可以看出来:在执行sum()函数时,最后操作完的结构储存在%eax寄存器中。在执行完sum()函数以后,后面<+45>那一句就是将%eax寄存器的值赋值给主函数中的一个参数。

※被调用函数运行结束时:
        这里主要对:leave和ret指令进行分析。
        首先是leave指令:在许多的地方都可以找到它的解释:用leave指令可以使栈做好返回的准备,它等价于下面的代码序列:

        movl    %ebp    %esp
        popl    %ebp

       首先是movl    %ebp    %esp。当前%ebp保存的是什么?没错,当前栈帧的栈底地址,所以这一句话的作用就是把%esp给放回到调用者栈帧的栈顶。联系到进入函数时的语句movl    %esp    %ebp,其实这就是个逆过程,旨在恢复原来栈顶的状态。
       然后是popl    %ebp。popl是对栈顶元素进行出栈,而现在的栈顶(也是栈底)储存的是什么呢(上面请大家记住的东西就派上用场了),储存就是调用者栈帧的栈底地址。popl    %ebp就是把这一地址赋值给%ebp(其实这个也可以看作push    %ebp的逆过程),所以这一句话就是恢复调用者栈帧的栈底。这样一来的话调用者栈帧就基本上是恢复到原来的状态了。
        然后呢,显然上面也说了leave只是做好返回的准备。准备什么呢,我们调用完函数以后,调用者还需要接着向下执行指令,那么调用完函数以后就应该跳转到该函数的下一条指令的地址。这么跳转?还记得我们的call指令吗--先将下一条指令的地址入栈,然后跳转。这里ret的作用就是把哪一个地址给弹出栈,并且跳转到地址对应的语句,再接着执行,这样以来一个函数就完整地运行结束了。
        对照最开始地那个结构图来说,这两条语句地作用就是:(leave指令)先将栈指针%esp移动到桢指针%ebp,然后把被保存地%ebp赋值给寄存器%ebp(此时%esp+1,指向返回地址)。(ret指令)然后把返回地址出栈,并跳转到返回地址对应地指令。

附录:栈帧的其他介绍----------------------------------------------

一. 栈帧的概念
        栈帧也叫作过程活动记录,是一个函数的执行环境,在栈帧中记录着函数的变量和返回地址。
        每个栈帧对应一个未完成的函数,每个函数都有其相对应的栈帧。在调用被调函数时,栈帧被创立;被调函数结束后,进行出栈,释放临时变量,返回至被调函数的下一步指令的地址处。
        在栈帧中,地址由高向低延伸。栈帧的最高地址为栈底,最低地址为栈顶。
二.使用栈帧的情况

        在函数调用中,有很多情况都可以用栈帧的原理进行解释。
        1. 虚实结合(函数的实参传给形参)
        2. 局部变量的释放
        3. 被调函数结束后的返回地址
        4. return C 的值的带回
三. 分析栈帧的创建过程
        首先,了解关于栈帧的几个名词
       1. ebp—栈底  指针寄存器。其内存放一个指针,该指针指向栈帧的底部。
       2. esp—栈顶  指针寄存器。其内存放一个指针,该指针指向栈帧的顶部。
       3. PC指针 : 永远指向当前运行指令的下一条指令。(rip寄存器)
        用下面这个简单程序分析栈帧的使用情况

#include<stdio.h>
int fun(int x,int y)
{
   int c = 20;  
   return c;
}
int main()
{
   int a = 1;
   int b = 2;
   int ret = fun (a , b);
   return 0;
}

  栈帧的创建和使用分为三个阶段
第一阶段主函数为调用者函数,进入调用者函数,在系统栈中为调用者函数分配栈帧。


    <1>在调用者函数的栈帧中为变量 a,b 分配相应的存储空间
    <2> 程序执行至 int fun(int x,int y) 时,进行实参与形参的虚实结合,形成临时拷贝并将其压入栈中。值的注意的是,
实参与形参的虚实结合的顺序是从右向左,先进行参数 b 的临时拷贝,再进行 a 的临时拷贝。
    <3>形参实例化之后,执行 call 指令。
     call 指令有两个重要的作用:
    ① 执行 jmp 指令,跳转至被调函数的入口地址,执行被调函数的相关操作
    ② 保存被调函数的下一条指令的地址,便于被调函数结束后的返回
        以上步骤运行结束之后,调用者函数的栈帧创建结束,下面开始进入被调函数的栈帧的创建。 

【第二阶段】在主函数中运行到被调函数时,为被调函数分配栈帧。

<1>push ebp        
       保存调用者函数的栈底,放在被调函数的栈底,便于返回
       被调函数结束后的返回仅靠 call 指令中的记录下一指令的地址是无法实现的,需要将 call 指令和 push 指令结合起来,才能使被调函数结束后能返回到正确的位置。
<2> move ebp ,esp;  将调用者函数的栈顶赋值给被调函数的栈底。此时,被调函数的栈底就是调用者函数的栈顶。
<3> sub esp ,occh;   给被调函数的栈顶减去一个随机值,指针下移,为被调函数的栈帧创造空间。

【 第三阶段】被调函数结束后,出栈,释放局部变量,带回返回值,返回到主函数运行指令的下一步指令。
<1>move  eax ,dword ptr [c] ;  将返回值 c 的值保存在寄存器 eax 中
<2> move  esp ,ebp;                将栈底的地址赋给栈顶的地址。栈顶移动到栈底的位置,释放局部变量。
<3> pop ebp;    将被调函数栈底保存的调用者函数的栈底地址弹出
<4> ret ;            返回地址出栈,返回至主函数的下一指令处。
<5> sub  esp , occh;    被调函数结束后,局部变量被释放,使栈顶指针上移,恢复到未形参实例化之前的位置。
<6> move  dword ptr [ret] ,eax ;  将寄存器中存储的返回值 c 返回至主函数中的变量 ret 中。
 以上就是整个函数调用的栈帧使用原理。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值