相比较其他传统的语言,C++的一个变革的特征是支持异常处理。相对于传统语言的不清楚容易错误的错误处理机制,C++的异常处理是一个非常好的替代。在正常的代码和错误处理代码之间清楚的分割使得程序非常整洁和宜于维护。本文讨论编译器怎么实现异常处理。假设读者熟悉异常处理的语法。本文包含一个异常处理的VC++的库来替代VC++的异常处理,使用这个函数:
install_my_handler();
在这以后,程序中发生的任何异常(包含抛出异常到stackunwinding,调用catch块和继续执行)都使用我自己的异常处理库。
译者注:当异常出现时,正常的执行流被中断,异常处理机制开始在当前范围寻找匹配的处理函数。如果找不到,把当前函数从栈中弹出,在调用者中继续寻找。这个过程称为stackunwinding
像其他C++特征一样,C++的标准并没有指定异常处理的实现机制。这使得C++实现者可以使用任何实现机制。我将讲述VC++怎么实现的。VC++把异常处理置于SHE(Structuredexceptionhangling)的上面。SHE是windows操作系统提供的结构化的异常处理。
SHE导论
在本讨论中,我将考虑那些显式的异常。例如被0除,空指针访问等。当异常出现,中断会产生,控制被转到OS。OS调用异常处理,检查从异常发出的函数开始的函数调用顺序,执行stackunwinding和控制转移。我们可以开发自己的异常处理函数,在OS中注册。OS就会在异常事件时调用它们。
Windows定义了一个特别的结构用来注册:
EXCEPTION_REGISTRATION:
struct EXCEPTION_REGISTRATION
EXCEPTION_REGISTRATION *prev;
DWORD handler;
;
要注册自己的异常处理函数,创建这个结构并将它的地址保存在段(由FS寄存器指向)的0偏移处。如下面的伪汇编指令:
mov FS:[0], exc_regp
结构中的prev字段表示EXCEPTION_REGISTRATION链表。当我们注册了这个EXCEPTION_REGISTRATION结构,我们使用这个prev字段保存以前注册的结构的地址。
关于异常回调函数,windows要求异常处理的信号,定义在excp.h文件中:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext);
现在你可以忽略所有的参数和返回类型。下面的程序在OS中注册了一个异常处理句柄并将产生一个被0除的异常。这个异常被抓到并将打印一个消息:
#include
#include
using std::cout;
using std::endl;
struct EXCEPTION_REGISTRATION
EXCEPTION_REGISTRATION *prev;
DWORD handler;
;
EXCEPTION_DISPOSITION myHandler(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext)
cout << "In the exception handler" << endl;
cout << "Just a demo. exiting..." << endl;
exit(0);
return ExceptionContinueExecution; //will not reach here
int g_div = 0;
void bar()
//initialize EXCEPTION_REGISTRATION structure
EXCEPTION_REGISTRATION reg, *preg = ?
reg.handler = (DWORD)myHandler;
//get the current head of the exception handling chain
DWORD prev;
_asm
mov EAX, FS:[0]
mov prev, EAX
reg.prev = (EXCEPTION_REGISTRATION*) prev;
//register it!
_asm
mov EAX, preg
mov FS:[0], EAX
//generate the exception
int j = 10 / g_div; //Exception. Divide by 0.
int main()
bar();
return 0;
/*output
In the exception handler
Just a demo. exiting...
-*/
注意:windows严格地定义了一个规则:EXCEPTION_REGISTRATION结构应该在栈内,并且要在以前的代码的低的内存地址。规则不满足,windows将中止程序。
函数和堆栈
堆栈是一块连续的内存,用来保存函数的局部对象。更明确的说,每一个函数都有关联的栈帧(译注:stackframe,在调用函数时,进入函数以后第一句应该是pushebp,然后movebp,esp,所以ebp一般都指向当前函数进入时的栈顶,而且指向的内容是上一层调用函数进入时栈顶,如此向外,最后找到0,就是系统的入口,这样一个函数使用的那些栈应该就是一帧)来保存所有的函数局部对象和函数表达式产生的临时对象(译注:1.C++里对象的意义很广泛,不只是class,结构,简单类型也是对象2.临时对象,学过编译原理的话应该很清楚,举个例子,比如有一个函数是intmyfun();你在函数中这样写intret=myfun();这样myfun()返回的结果就放到了ret里,如果你写成myfun();而不理它的返回值,你虽然不理它,但是仍然会有它返回值存放的地方,这就是一个临时的对象,在vc的调试环境了,看auto变量的页面就可以看到临时的变量)。请注意上面描述只是很典型的情况。而实际上,编译器可能会储存所有或部分的变量到寄存器里,以便获得更快的执行速度(译注:编译器优化)。堆栈是一个处理器(CPU)级就支持的概念(译注:之所以这么说,因为汇编代码里就有push和pop).处理器提供内部的寄存器和特殊的指令来实现堆栈处理
图 2显示了一个典型的栈,这是当函数foo调用函数bar,然后bar调用函数widget以后的栈的内容。请注意栈是向下增长的(译注:平时我在纸上画栈都是低地址在上,所以作者的这个图看起来感觉有点怪,但是看懂应该没有问题),这意味着下一个将要入栈的元素所在的地址将比前一个元素的地址更小(低)。编译器用ESP寄存器来鉴别当前的栈帧,在上图所示的情况下,widget时正在执行鼓票的函数,EBP寄存器指向了widget的栈帧(就是函数进入时,push了ebp以后的栈顶位置)。函数访问局部变量都是用局部变量所在的位置相对于帧顶的偏移量。编译器在编译的时候就把所有的局部变量从名字变成固定的相对于帧顶的偏移,例如,widget函数访问它的一个局部变量就用ebp-24来指明它的位置
上图也显示了ESP寄存器,在图示的情况下,它指向了堆栈的最后一项,也就是处于widget帧的尾部,下一帧将从这个位置开始
处理器支持两种栈操作:push 和 pop:
pop EAX
意味着从esp所在的位置读4个字节到eax中,然后把esp增加4(32位的处理器下)。同样的,
push EBP
意味着把esp减4,然后把ebp的内容写到esp指向的地方。
当编译器编译一个函数, 它在函数头部添加一些创建和初始化函数栈帧的代码,同样,在函数结尾加上从堆栈里弹出栈帧的代码。
典型的,编译器在函数头部生成
Push EBP ; save current frame pointer on stack
Mov EBP, ESP ; Activate the new frame
Sub ESP, 10 ; Subtract. Set ESP at the end of the frame
第一句保存当前的帧指针ebp到堆栈里,第二句通过设置ebp到当前的esp来激活当前的函数帧,.第三句设置esp寄存器到当前帧的尾部,就是把esp减去本函数内的局部对象的总长度。编译器在编译的时候就知道有多少的局部对象和每个对象的长度,所以能够清楚地知道一个函数的帧的确切长度
在函数结束时把当前帧从堆栈中弹出
Mov ESP, EBP
Pop EBP ; activate caller"s frame
Ret ; return to the caller
恢复ESP和EBP,然后执行RET
当处理器执行RET指令时,实际上类似执行了一条popeip,把栈里保存的EIP弹出,然后跳到EIP处开始执行。相反,call指令执行的时候先把当前的EIP推入堆栈,然后jmp到相应的地址,
图 3 显示了运行时堆栈的更多详细信息。如图所示,函数参数也是函数帧的一部分,调用函数者把参数推入堆栈,然后函数返回否执行
Add ESP, args_size
或者,采用另一种RET指令,如下
Ret 24
相当于返回后,执行了 ADD ESP , 24
注意没有进程里的每隔线程都有它自己的茶
www.stockdatas.cn www.stockbests.cn www.stocknewss.cn