zoukankan      html  css  js  c++  java
  • [转] 函数调用的栈分配

    当调用(call)一个函数时,主调函数将声明中的参数表以逆序压栈,然后将当前的代码执行指针(eip)压栈,跳转到被调函数的入口点。
            进入被调函数时,函数将esp减去相应字节数获取局部变量存储空间。被调函数返回(ret)时,将esp加上相应字节数,归还栈空间,弹出主调函数压在栈中的代码执行指针(eip),跳回主调函数。再由主调函数恢复到调用前的栈。
          
    为了访问函数局部变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp
    的值保存在ebp中,并事先将原ebp的值压栈保存,以声明中的顺序(即压栈的相反顺序)来确定偏移量。
    访问函数的局部变量和访问函数参数的区别:
    局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。

          
    函数的返回值不同于函数参数,可以通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,将通过eax寄存
    器传递。如果返回值类型是64位变量,如_int64,则通过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如
    float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是struct或class类型,编译器将通过隐式修改函数的签名,以引
    用型参数的形式传回。由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。基于这个原因,C89规范中约定,不写明返回值类型的
    函数,返回值类型默认为int。这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。这种语法
    不兼容性是为了加强C++的类型安全,但同时也带来了一些代码兼容性问题。
    代码示例
    VarType Func (Arg1, Arg2, Arg3, ... ArgN)
    {
        VarType Var1, Var2, Var3, ...VarN;
        //...
        return VarN;
    }
    假设sizeof(VarType) = 4(DWORD), 则一次函数调用汇编代码示例为:
    调用方代码:

    push ArgN ; 依次逆序压入调用参数
    push ...
    push Arg1
    call Func_Address ; 压入当前EIP后跳转
    跳转至被调方代码:
    push ebp ; 备份调用方EBP指针
    mov ebp, esp ; 建立被调方栈底
    sub esp, N * 4; 为局部变量分配空间
    mov dword ptr[esp - 4 * 1 ], 0 ; 初始化各个局部变量 = 0 这里假定VarType不是类
    mov dword ptr[esp - 4 * ... ], 0
    mov dword ptr[esp - 4 * N ], 0
    . . . . . . ; 这里执行一些函数功能语句(比如将第N个参数[ebp + N * 4]存入局部变量), 功能完成后将函数返回值存至eax
    add esp, N * 4 ; 销毁局部变量
    mov esp, ebp ; 恢复主调方栈顶
    pop ebp ; 恢复主调方栈底
    ret ; 弹出EIP 返回主调方代码
    接上面调用方代码:
    add esp, N * 4 ; 释放参数空间, 恢复调用前的栈
    mov dword ptr[ebp - 4], eax ; 将返回值保存进调用方的某个VarType型局部变量
    进入函数时堆栈分配示意图
    内存低地址 | ESP - - - - - - - - - - - - - - - - EBP - - - - - - - - - - - - - - - - - - - - - >| 内存高地址
    Stack State:VarN . . . Var3 Var2 Var1 SFP EIP Arg1 Arg2 Arg3 . . . ArgN
    //资料
    区...............................................................................................................................
    SFP 解释:      
    除了堆栈指针(ESP指向堆栈顶部的的低地址)之外,
    为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP)。有些文章把它叫做局部基指针(LB-local base
    pointer)。从理论上来说, 局部变量可以用SP加偏移量来引用。 然而, 当有字被压栈和出栈后, 这些偏移量就变了。
    尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能。而且在所有情况下, 要引入可观的管理开销。
    而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现。
            因此,
    许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响。
    在Intel CPU中, BP(EBP)用于这个目的。 在Motorola CPU中,
    除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP。考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值,
    而局部变量的偏移量是负值。
          
    当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复这个被保存的FP称为SFP)。 然后它把SP复制到FP,
    创建新的FP, 把SP向前移动为局部变量保留空间。 这称为例程的序幕(prolog)工作。当例程退出时, 堆栈必须被清除干净,
    这称为例程的收尾(epilog)工作。 Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令,
    都可以用于有效地序幕和收尾工作。
             所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:
    void f()
    {
    int i,a[10];
    for(i=0;i}
            这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常谈的越界访问问
    题。但是这个错误造成的结果,是很多人没有想到的。这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老手估计的那样会
    默不作声,而是导致一个死循环。
            错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。C++标准对于越界访问只是说“未定义操作”。我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。是i!
          
    根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。但是,I386上栈是向下增长的,所以,a的地址低于i的地址。其结果是在循环的最后,
    a引用到了i自己!接下来的事情就不难预见了,a,也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到在你
    的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill了……
                   
                   
                   

    本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u3/92446/showart_1820202.html

  • 相关阅读:
    Fiddler工具抓包
    简单Ant打包
    android中ActionBar的几个属性
    yum安装php7.2
    java 获取计算机内存
    docker apache安装
    docker 安装 openresty
    docker 安装nginx
    docker安装gitlab
    java获取时间格式
  • 原文地址:https://www.cnblogs.com/hewei2012/p/3171865.html
Copyright © 2011-2022 走看看