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

    http://kingj.iteye.com/blog/1555017

    http://www.cnblogs.com/rain-lei/p/3622057.html

     

    函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果,还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢?
    对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈
    代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
    数据段:保存初始化的全局变量和静态变量,可读可写不可执行
    BSS:未初始化的全局变量和静态变量
    堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
    栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
    如图所示
    寄存器
    EAX:累加(Accumulator)寄存器,常用于函数返回值
    EBX:基址(Base)寄存器,以它为基址访问内存
    ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
    EDX:数据(Data)寄存器,常用于乘除法和I/O指针
    ESI:源变址寄存器
    EDI:目的变址寄存器
    ESP:堆栈(Stack)指针寄存器,指向堆栈顶部
    EBP:基址指针寄存器,指向当前堆栈底部
    EIP:指令寄存器,指向下一条指令的地址

    当调用(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 * ; 销毁局部变量
    mov esp, ebp ; 恢复主调方栈顶
    pop ebp ; 恢复主调方栈底
    ret ; 弹出EIP 返回主调方代码

    接上面调用方代码: 
    add esp, N * ; 释放参数空间, 恢复调用前的栈 
    mov dword ptr[ebp - 4], eax ; 将返回值保存进调用方的某个VarType型局部变量

     

    下面用一系列图说明
    1) 将实参,返回地址入栈
    2)把原ebp的地址压栈保存,让新的ebp等于esp,栈顶开始变为栈底
    2) 实参通过ebp+来访问,局部变量通过ebp-来访问

     

     
    接下来是返回过程 (correction: "pop ebp" in the 2nd picture)
        

     所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:

    void f()

    {

    int i,a[10];

    for(i=0;i<=10;++i)a[i]=0;/An error occurs here!

    }

            这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常 谈的越界访问问题。但是这个错误造成的结果,是很多人没有想到的。这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老 手估计的那样会默不作声,而是导致一个死循环。
            错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。C++标准对于越界访问只是说“未定义操作”。我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。是i! 
            根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。但是,I386上栈是向下增长的,所以,a的地址低于i的地址。其结果是在循环的最 后,a[i]引用到了i自己!接下来的事情就不难预见了,a[i],也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到 在你的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill了……

     

     

  • 相关阅读:
    解决使用git出现 The file will have its original line endings in your working directory
    SpringBoot集成flowable碰见DMN不能初始化
    CF268D Wall Bars
    CF1327F AND Segments
    P2900 [USACO08MAR]Land Acquisition G
    CF279B Books
    CF859E Desk Disorder
    CF1147B Chladni Figure
    CF1147E Rainbow Coins
    P3565 [POI2014]HOT-Hotels
  • 原文地址:https://www.cnblogs.com/qiangxia/p/4263295.html
Copyright © 2011-2022 走看看