函数调用栈

作者: 可bing | 来源:发表于2018-09-10 11:32 被阅读2次

对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈,而函数调用是发生在栈上的。
代码段:保存程序文本,指令指针eip就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,可读可写可执行

相关寄存器

ebp:基址指针寄存器,存放当前函数栈帧的基地址
esp:堆栈(Stack)指针寄存器,存放当前函数栈帧的栈顶地址
eip: 指令寄存器,指向下一条指令的地址
exp:存放函数返回值

函数调用栈结构图


1025005-20160924105903152-717146733.gif

入栈过程

1、将调用者函数的ebp入栈
2、将调用者函数的栈顶指针esp赋值给被调用函数的ebp
3、按从右到左的顺序将被调用函数的参数入栈
4、按声明的顺序将被调用函数的局部变量入栈
5、将调用函数的下一个指令地址作为返回地址入栈
6、将被调用函数的第一条指令地址赋值给eip寄存器
7、开始执行被调用函数指令

ebp寄存器处于一个非常重要的位置,该寄存器中存放的地址可以作为基准,向栈底方向可以获取返回地址,传入参数值,向栈顶方向可以获取函数的局部变量。而esp所指向的内存中又存放着上一层函数调用的ebp值。

出栈过程

1、将函数返回值存入eax寄存器中
2、执行leave指令
3、执行ret指令

带异常回退的函数调用栈

栈展开

栈展开(unwinding)是指当前的try...catch...块匹配成功或者匹配不成功异常对象后,从try块内异常对象的抛出位置,到try块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。如果当前函数内对抛出的异常对象匹配不成功,则从最外层的try语句到当前函数体的起始位置处的局部变量也依次被逆序析构,实现栈展开,然后再回退到调用栈的上一层函数内从函数调用点开始继续处理该异常。

catch语句如果匹配异常对象成功,在完成了对catch语句的参数的初始化(对传值参数完成了参数对象的copy构造)之后,对同层级的try块执行栈展开。

相关数据结构


struct UNWINDTBL {

    int nNextIdx;

    void (*pfnDestroyer)(void *this);

    void    *pObj; 

};

struct CATCHBLOCK {

    //...



    type_info  *piType;

    void        *pCatchBlockEntry;

}

struct TRYBLOCK {

    //...

    int        nBeginStep;

    int    nEndStep;

    CATCHBLOCK  tblCatchBlocks[]; 

};

struct EHDL {

    //...



    UNWINDTBL  tblUnwind[];

    TRYBLOCK    tblTryBlocks[];



    //... 

};

struct EXP {

    EXP    *piPrev; //成员指向链表的上一个节点,它主要用于在函数调用栈中逐级向上寻找匹配的 catch 块,并完成栈回退工作。

    EHDL    *piHandler; //成员指向完成异常捕获和栈回退所必须的数据结构(主要是两张记载着关键数据的表:“try”块表:tblTryBlocks 及“栈回退表”:tblUnwind)。

    int    nStep; //成员用来定位 try 块,以及在栈回退表中寻找正确的入口。

};

调用栈示意图


1025005-20160924105930137-526226122.png

栈展开过程

nStep 变量用于跟踪函数内局部对象的构造、析构阶段。再配合编译器为每个函数生成的 tblUnwind 表,就可以完成退栈机制。表中的 pfnDestroyer 字段记录了对应阶段应当执行的析构操作(析构函数指针);pObj 字段则记录了与之相对应的对象 this 指针偏移。将 pObj 所指的偏移值加上当前栈框架基址(EBP),就是要代入 pfnDestroyer 所指析构函数的 this 指针,这样即可完成对该对象的析构工作。而 nNextIdx 字段则指向下一个需要析构对象所在的行(下标)。

在发生异常时,异常处理器首先检查当前函数栈框架内的 nStep 值,并通过 piHandler 取得 tblUnwind[] 表。然后将 nStep 作为下标带入表中,执行该行定义的析构操作,然后转向由 nNextIdx 指向的下一行,直到 nNextIdx 为 -1 为止。在当前函数的栈回退工作结束后,异常处理器可沿当前函数栈框架内 piPrev 的值回溯到异常处理链中的上一节点重复上述操作,直到所有回退工作完成为止。

相关文章

  • 宏、普通函数、内联函数之间的区别

    普通函数 调用时向栈中push函数帧,调用结束后pop函数帧。编译器会在函数调用语句的前后,插入入栈和出栈的辅助代...

  • 函数调用栈平衡

    栈平衡 栈平衡:函数调用前后的栈顶指针指向的位置不变 内平栈 外平栈 内平栈: 指的是在函数调用返回之前使栈保持...

  • Tips:inline 与force_inline

    前期准备 函数入栈和出栈 函数每次入栈都会调用call指令,调用后还需要出栈返回到原来调用的地方。这个时间开销实际...

  • [转载]C语言函数调用栈

    原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...

  • 第九章 基于共享变量的并发(二)Goroutine和线程

    一、动态栈(Growable Stacks) 栈(stack):当前正在被调用或被挂起(旨在调用其他函数)的函数的...

  • 11-27函数

    函数 函数的重点是识别调用约定、参数类型和返回值类型 调用约定: cdecl调用方平栈 stdcall被调用方平栈...

  • Javascript调用栈与尾递归实现

    JavaScript函数调用栈 理解JavaScript的函数调用栈是非常重要的。它可以让我们知道Javscrip...

  • 汇编(八)

    现场保护 平栈内:外平栈.png *现场保护 函数调用 xcode函数汇编

  • 第三章:递归

    递归 盒子里面找钥匙 基线条件和递归条件 栈 调用栈 调用另一个函数时,当前函数暂停并处于未完成的状态 递归调用栈...

  • Stack Queue

    在函数调用的过程中,操作系统使用函数调用栈来维护函数调用的状态。每个函数运行过程中有一段属于自己的栈帧结构,主要用...

网友评论

    本文标题:函数调用栈

    本文链接:https://www.haomeiwen.com/subject/kzthgftx.html