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