• 函数调用过程栈帧变化详解



    函数调用另一个词语表示叫作过程。一个过程调用包括将数据(以过程参数和返回值的形式)控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。
     
    大多数机器,包括IA32,只提供转移控制到过程和从过程中转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操纵程序栈来实现。
     
    在了解本文章之前,您需要先对程序的进程空间有所了解,即对进程如何使用内存?如果你知道这些,下面的内 容将是很easy的事情了。为了您的回顾还是将简单的分布图贴出来,便于您的回顾。

    我们先来了解一个概念,栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储为单个过程(函数调用)分配的那部分栈称为栈帧。栈帧其实 是两个指针寄存器,寄存器%ebp为帧指针(指向该栈帧的最底部),而寄存器%esp为栈指针(指向该栈帧的最顶部),当程序运行时,栈指针可以移动(大多数的信息的访问都是通过帧指针的,换句话说,就是如果该栈存在,%ebp帧指针是不移动的,访问栈里面的元素可以用-4(%ebp)或者8(%ebp)访问%ebp指针下面或者上面的元素)。总之简单 一句话,栈帧的主要作用是用来控制和保存一个过程的所有信息的。栈帧结构如下所示:

    此处注意:这里面有一个错误,即:“保存的寄存器、局部变量和临时值”处应该是ebp-4。
    栈是从高地址向低地址存储。所以越是低的地址,越是靠后入栈。

    如果你已经对这个图已经非常了解了,那么就没有必要再看下去了。因为下面的内容都是对这幅图的讲解。

      假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。另外,当P调用Q时,P中的返回地址被压入栈中,形成P的栈帧的末尾 (返回地址就是当程序从Q返回时应该继续执行的地方)。Q的栈帧从保存的帧指针的值开始,后面到新的栈指针之间就是该过程的部分了。

      过程实例讲解:

    下面以这个程序为例进行简要说明函数调用的基本过程。

    int swap_add(int* xp,int* yp) {
        int x = *xp;
        int y = *yp;
        *xp = y;
        *yp = x;
        return x+y;
    }
    int caller(){
        int arg1 = 534;
        int arg2 = 1057;
        int sum = swap_add(&arg1,&arg2);
        int diff = arg1 - arg2;
        
        return sum * diff;
    } 
     
    经过汇编之后caller部分的代码如下:
     
    caller:
        pushl %ebp   //保存%ebp 
        movl %esp,%ebp    //设置新的帧指针为旧的栈指针
        subl $24,%esp  //分配24子节的栈空间
        movl $534,-4(%ebp) //设置arg1=534
        movl $1057,-8(%ebp) //设置arg2=1057
        leal -8(%ebp),%eax //计算&arg2
        movl %eax,4(%esp) //将&arg2存入栈中
        leal -4(%ebp),%eax //计算&arg1
        movl %eax,(%esp) //将&arg1存入栈中
        call swap_add //调用swap_add-------------------》过程调用

    这段代码先保存了%ebp的一个副本,将新的过程(该函数的ebp)的ebp设置为栈帧的开始位置。然后将栈指针减去24,从而在栈上分配了24字 节的空间(你应该思考一下为什么是24字节),然后是初始化两个局部变量,计算两个局部变量的地址并存入栈中,形成了函数swap_add的参数。将这些 参数存储到相对于栈指针偏移量为0和+4的地方,留待稍后的swap_add调用访问。然后调用swap_add.

    接下的代码是swap_add的函数部分:

    swap_add:
        pushl %ebp //save old %ebp
        movl %esp,%ebp  //set %ebp as frame pointer
        pushl %ebx     //save %ebx
         
        movl 8(%ebp),%edx   //Get xp
        movl 12(%ebp),%ecx   //Get yp
        movl (%edx),%ebx   //Get x
        movl (%ecx),%eax    //Get u
        movl %eax,(%edx)    //Store y as xp
        movl %ebx,(%ecx)      //Sotre x as yp
        addl %ebx,%eax         //return value = x + y
         
        popl %ebx        //restore  %ebx
        popl %ebp        //restore %ebp
        ret        //从过程调用中返回, 将控制转移回caller
    代码分为3部分 建立部分:初始化栈帧;主体部分:执行过程的实体计算;结束部分:回复栈帧的状态,以及过程返回。这一部分的代码比较简单,就不在一一介绍,根据以上的3 部分,划分的已经很清晰了。(说明一点程序在执行到swap_add的代码之前,也就是在执行call语句已经把返回地址压入栈中)值得注意的是最后一部 分的popl %ebx   popl %ebp。它的作用是恢复了之前存储的栈帧指针的值,也就是调用程序的原始栈帧指针。从而程序就可以得到返回(有些细心的人会问那返回值咋么办?呵呵,返 回值是存入了%eax中,在接下来的调用程序caller中直接访问该寄存器就可以了)。 
     
    正如前面所讲的那样,栈向低地址方向增长,而栈指针%esp指向栈顶元素,可以利用pushl将数据存入栈中并利用popl指令从栈中取出。将栈指针的值减小适当的值可以分配没有指定初始值的数据的空间,例如:subl $24,%esp。类似的,通过增加栈指针来释放空间。

    下面就是返回之后继续执行的部分代码了:

    movl -4(%ebp),%edx
    subl -8(%ebp),%edx
    imull %edx,%eax   //为了计算diff, 
    leave          //为返回准备栈,GCC 产生的代码有时候会使用leave指令来释放栈帧,
             //而有时会使用一个或者两个popl指令。两个方法都可行。
    ret //从过程调用中返回

     为了计算diff,从栈中取出arg1,和arg2的值,并将寄存器%eax当做swap_add的返回值。

    整个过程的栈变化如下所示:

  • 相关阅读:
    【Javascript】call
    【Android Studio】 资源下载
    【Javascript】数组之二
    【Java】Springboot集成Druid
    【Android Studio】Gradle
    【Postgres】根据字段数据创建空间字段
    【时空大数据】Access 到 Postgres 数据迁移遇到的ODBC坑----驱动程序和应用程序之间的体系结构不匹配
    【大数据】Hadoop单机安装配置
    【大数据】虚拟机免密登录
    【大数据】虚拟机网络配置-CentOS
  • 原文地址:https://www.cnblogs.com/zlcxbb/p/5759776.html
Copyright © 2020-2023  润新知