函数调用栈

对于程序,编译器会为其分配一段内存,在逻辑上可以分为代码段、数据段、堆、栈,而函数调用则是发生在栈上。
代码段:保存程序文本,指令指针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 的值回溯到异常处理链中的上一节点重复上述操作,直到所有回退工作完成为止。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 10,211评论 1 19
  • 栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函...
    zjfclimin阅读 9,601评论 0 5
  • 还有82天就是研究生入学考试了。 关于英语语言文学要考的是四科,没有一科拿得出手。都知道考研的每个人都不容...
    学语言的汪阅读 806评论 0 1
  • 去年底,与战友聊天,无意中谈起“阿冰”。 不久,朋友圈里传来“阿冰与原勤务队XXX恋爱了”的消息。 又过了一段时间...
    春哗秋拾阅读 3,459评论 0 4
  • 之前看过《不安的时候,坐下来写》,当时还写了10条清单,之后却没有再次全文阅读过了。 而在书中,到目前为止还记得的...
    雪晴_雪晴阅读 2,435评论 0 2