C++编译器如何实现异常处理惩罚
副标题#e#
译者注:本文在网上已经有几个译本,但都不完整,所以我抉择本身把它翻译过来。固然力争信、雅、达,但鉴于这是我的第一次翻译经验,不敷之处敬请体谅并指出。
与传统语言对比,C++的一项革命性创新就是它支持异常处理惩罚。传统的错误处理惩罚方法常常满意不了要求,而异常处理惩罚则是一个极好的替代办理方案。它将正常代码和错误处理惩罚代码清晰的分别隔来,措施变得很是清洁而且容易维护。本文接头了编译器如何实现异常处理惩罚。我将假定你已经熟悉异常处理惩罚的语法和机制。本文还提供了一个用于VC++的异常处理惩罚库,要用库中的处理惩罚措施替换掉VC++提供的谁人,你只需要挪用下面这个函数:
install_my_handler();
之后,措施中的所有异常,从它们被抛出到仓库展开(stack unwinding),再到挪用catch块,最后到措施规复正常运行,都将由我的异常处理惩罚库来打点。
与其它C++特性一样,C++尺度并没有划定编译器应该如何来实现异常处理惩罚。这意味着每一个编译器的提供商都可以用它们认为得当的方法来实现它。下面我会描写一下VC++是怎么做的,但纵然你利用其它的编译器或操纵系统①,本文也应该会是一篇很好的进修质料。VC++的实现方法是以windows系统的布局化异常处理惩罚(SEH)②为基本的。
布局化异常处理惩罚—概述
在本文的接头中,我认为异常可能是被明晰的抛出的,可能是由于除零溢出、空指针会见等引起的。当它产生时会发生一其间断,接下来节制权就会通报到操纵系统的手中。操纵系统将挪用异常处理惩罚措施,查抄从异常产生位置开始的函数挪用序列,举办仓库展开和节制权转移。Windows界说了布局“EXCEPTION_REGISTRATION”,使我们可以或许向操纵系统注册本身的异常处理惩罚措施。
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
};
注册时,只需要建设这样一个布局,然后把它的地点放到FS段偏移0的位置上去就行了。下面这句汇编代码演示了这一操纵:
mov FS:[0], exc_regp
prev字段用于成立一个EXCEPTION_REGISTRATION布局的链表,每次注册新的EXCEPTION_REGISTRATION时,我们都要把本来注册的谁人的地点存到prev中。
那么,谁人异常回调函数长什么样呢?在excpt.h中,windows界说了它的原形:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD *ExcRecord,
void* EstablisherFrame,
_CONTEXT *ContextRecord,
void* DispatcherContext);
不要管它的参数和返回值,我们先来看一个简朴的例子。下面的措施注册了一个异常处理惩罚措施,然后通过除以零发生了一个异常。异常处理惩罚措施捕捉了它,打印了一条动静就完事大吉并退出了。
#include <iostream>
#include <windows.h>
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; //不会运行到这
}
int g_div = 0;
void bar()
{
//初始化一个EXCEPTION_REGISTRATION布局
EXCEPTION_REGISTRATION reg, *preg = ®
reg.handler = (DWORD)myHandler;
//取恰当前异常处理惩罚链的“头”
DWORD prev;
_asm
{
mov EAX, FS:[0]
mov prev, EAX
}
reg.prev = (EXCEPTION_REGISTRATION*) prev;
//注册!
_asm
{
mov EAX, preg
mov FS:[0], EAX
}
//发生一个异常
int j = 10 / g_div; //异常,除零溢出
}
int main()
{
bar();
return 0;
}
/*——-输出——————-
In the exception handler
Just a demo. exiting…
———————————*/
留意EXCEPTION_REGISTRATION必需界说在栈上,而且必需位于比上一个结点更低的内存地点上,Windows对此有严格要求,达不到的话,它就会立即终止历程。
函数和仓库
仓库是用来生存局部工具的持续内存区。更明晰的说,每个函数都有一个相关的栈桢(stack frame)来生存它所有的局部工具和表达式计较进程顶用到的姑且工具,至少理论上是这样的。但现实中,编译器常常会把一些工具放到寄存器中以便能以更快的速度会见。仓库是一个处理惩罚器(CPU)条理的观念,为了哄骗它,处理惩罚器提供了一些专用的寄存器和指令。
#p#分页标题#e#
图1是一个典范的仓库,它示出了函数foo挪用bar,bar又挪用widget时的情景。请留意仓库是向下增长的,这意味着新压入的项的地点低于原有项的地点。
#p#副标题#e#
凡是编译器利用EBP寄存器来指示当前勾当的栈桢。本例中,CPU正在运行widget,所以图中的EBP指向了widget的栈桢。编译器在编译时将所有局部工具理会成相对付栈桢指针(EBP)的牢靠偏移,函数则通过栈桢指针来间接会见局部工具。举个例子,典范的,widget会见它的局部变量时就是通过会见栈桢指针以下的、有着确定位置的几个字节来实现的,好比说EBP-24。
上图中也画出了ESP寄存器,它叫栈指针,指向栈的最后一项。在本例中,ESP指着widget的栈桢的末端,这也是下一个栈桢(假如它被建设的话)的开始位置。
处理惩罚器支持两种范例的栈操纵:压栈(push)和弹栈(pop)。好比,
pop EAX
的浸染是从ESP所指的位置读出4字节放到EAX寄存器中,并把ESP加上(记着,栈是向下增长的)4(在32位处理惩罚器上);雷同的,
push EBP
的浸染是把ESP减去4,然后将EBP的值放到ESP指向的位置中去。
编译器编译一个函数时,会在它的开头添加一些代码来为其建设并初始化栈桢,这些代码被称为序言(prologue);同样,它也会在函数的末了处放上代码来排除栈桢,这些代码叫做尾声(epilogue)。
一般环境下,序言是这样的:
Push EBP ; 把本来的栈桢指针生存到栈上
Mov EBP, ESP ; 激活新的栈桢
Sub ESP, 10 ; 减去一个数字,让ESP指向栈桢的末端
第一条指令把本来的栈桢指针EBP生存到栈上;第二条指令通过让EBP指向主调函数的EBP的生存位置来激活被调函数的栈桢;第三条指令把ESP减去了一个数字,这样ESP就指向了当前栈桢的末端,而这个数字是函数要用到的所有局部工具和姑且工具的巨细。编译时,编译器知道函数的所有局部工具的范例和“体积”,所以,它能很容易的计较出栈桢的巨细。
尾声所做的正好和序言相反,它必需把当前栈桢从栈上排除去:
Mov ESP, EBP
Pop EBP ; 激活主调函数的栈桢
Ret ; 返回主调函数
它让ESP指向主调函数的栈桢指针的生存位置(也就是被调函数的栈桢指针指向的位置),弹出EBP从而激活主调函数的栈桢,然后返回主调函数。
一旦CPU碰着返回指令,它就要做以下两件事:把返回地点从栈中弹出,然后跳转到谁人地点去。返回地点是主调函数执行call指令挪用被调函数时自动压栈的。Call指令执行时,会先把紧随在它后头的那条指令的地点(被调函数的返回地点)压入栈中,然后跳转到被调函数的开始位置。图2更具体的描画了运行时的仓库。如图所示,主调函数把被调函数的参数也压进了仓库,所以参数也是栈桢的一部门。函数返回后,主调函数需要移除这些参数,它通过把所有参数的总体积加到ESP上来到达目标,而这个别积可以在编译时知道:
Add ESP, args_size
虽然,也可以把参数的总体积写在被调函数的返回指令的后头,让被调函数去移除参数,下面的指令就在返回主调函数前从栈中移去了24个字节:
Ret 24
取决于被调函数的挪用约定(call convention),这两种方法每次只能用一个。你还要留意的是每个线程都有本身独立的仓库。
C++和异常
回想一下我在第一节中先容的EXCEPTION_REGISTRATION布局,我们曾用它向操纵系统注册了产生异常时要被挪用的回调函数。VC++也是这么做的,不外它扩展了这个布局的语义,在它的后头添加了两个新字段:
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
int id;
DWORD ebp;
};
VC++会为绝大部门函数③添加一个EXCEPTION_REGISTRATION范例的局部变量,它的最后一个字段(ebp)与栈桢指针指向的位置重叠。函数的序言建设这个布局并把它注册给操纵系统,尾声则规复主调函数的EXCEPTION_REGISTRATION。id字段的意义我将在下一节先容。
VC++编译函数时会为它生成两部门数据:
a)异常回调函数
b)一个包括函数重要信息的数据布局,这些信息包罗catch块、这些块的地点和这些块所体贴的异常的范例等等。我把这个布局称为funcinfo,有关它的具体接头也在下一节。
#p#分页标题#e#
图3是思量了异常处理惩罚之后的运行时仓库。widget的异常回调函数位于由FS:[0]指向的异常处理惩罚链的开始位置(这是由widget的序言配置的)。异常处理惩罚措施把widget的funcinfo布局的地点交给函数__CxxFrameHandler,__CxxFrameHandler会查抄这个布局看函数中有没有catch块对当前的异常感乐趣。假如没有的话,它就返回ExceptionContinueSearch给操纵系统,于是操纵系统会从异常处理惩罚链表中取得下一个结点,并挪用它的异常处理惩罚措施(也就是挪用当前函数的谁人函数的异常处理惩罚措施)。
这一进程将一直举办下去——直处处理惩罚措施找到一个能处理惩罚当前异常的catch块为止,这时它就不再返回操纵系统了。可是在挪用catch块之前(由于有funcinfo布局,所以知道catch块的进口,拜见图3),必需举办仓库展开,也就是清理掉当前函数的栈桢下面的所有其他的栈桢。这个操纵稍微有点巨大,因为:异常处理惩罚措施必需找到异常产生时保留在这些栈桢上的所有局部工具,并依次挪用它们的析构函数。后头我将对此举办具体先容。
异常处理惩罚措施把这项事情委托给了各个栈桢本身的异常处理惩罚措施。从FS:[0]指向的异常处理惩罚链的第一个结点开始,它依次挪用每个结点的处理惩罚措施,汇报它仓库正在展开。与之相呼应,这些处理惩罚措施会挪用每个局部工具的析构函数,然后返回。此进程一直举办到与异常处理惩罚措施自身相对应的谁人结点为止。
由于catch块是函数的一部门,所以它利用的也是函数的栈桢。因此,在挪用catch块之前,异常处理惩罚措施必需激活它所附属的函数的栈桢。
其次,每个catch块都只接管一个参数,其范例是它但愿捕捉的异常的范例。异常处理惩罚措施必需把异常工具自己可能是异常工具的引用拷贝到catch块的栈桢上,编译器在funcinfo中记录了相关信息,处理惩罚措施按照这些信息就能知道到哪去拷贝异常工具了。
拷贝完异常并激活栈桢后,处理惩罚措施将挪用catch块。而catch块将把节制权下一步要转移到的地点返返来。请留意:固然这时仓库已经展开,栈桢也都被排除了,但它们占据的内存空间并没有被包围,所有的数据都还好好的待在栈上。这是因为异常处理惩罚措施仍在执行,象其他函数一样,它也需要栈来存放本身的局部工具,而其栈桢就位于产生异常的谁人函数的栈桢的下面。catch块返回今后,异常处理惩罚措施需要“杀掉”异常工具。从此,它让ESP指向方针函数(节制官僚转移到的谁人函数)的栈桢的末端——这样就把(包罗它本身的在内的)所有栈桢都删除了,然后再跳转到catch块返回的谁人地点去,就胜利的完成整个异常处理惩罚任务了。但它怎么知道方针函数的栈桢末端在哪呢?事实上它没法知道,所以编译器把这个地点生存到了栈桢上(由媒介来完成),如图3所示,栈桢指针EBP下面第16个字节就是。
虽然,catch块也大概抛出新异常,可能是将本来的异常从头抛出。处理惩罚措施必需对此有所筹备。假如是抛出新异常,它必需杀掉本来的谁人;而假如是从头抛出本来的异常,它必需能继承流传(propagate)这个异常。
这里我要出格强调一点:由于每个线程有本身独立的仓库,所以每个线程也都有本身独立的、由FS:[0]指向的EXCEPTION_REGISTRATION链。
C++和异常—2
图4是funcinfo的机关,留意这里的字段名大概与VC++编译器实际利用的不完全一致,并且我也只给出了和我们的接头相关的字段。仓库展开表(unwind table)的布局留到下节再接头。
异常处理惩罚措施在函数中查找catch块时,它首先要判定异常产生的位置是否在当前函数(产生异常的谁人函数)的一个try块中。是则查找与此try块相关的catch块表,不然直接返回。
先来看看它奈何找try块。编译时,编译器给每个try块都分派了start id和end id。通过funcinfo布局,异常处理惩罚措施可以会见这两个id,见图4。编译器为函数中的每个try块都生成了相关的数据布局。
上一节中,我说过VC++给EXCEPTION_REGISTRATION布局加上了一个id字段。回想一下图3,这个布局位于函数的栈桢上。异常产生时,处理惩罚措施读出这个值,看它是否在try块的两个id确定的区间[start id,end id]中。是的话,异常就产生在这个try块中;不然继承查察try块表中的下一个try块。
#p#分页标题#e#
谁认真更新id的值,它的值又应该是什么呢?本来,编译器会在函数的多个位置安插代码来更新id的值,以回响措施的及时运行状态。好比说,编译器会在进入try块的处所加上一条语句,把try块的start id写到栈桢上。
找到try块后,处理惩罚措施就遍历与其关联的catch块表,看是否有对当前异常感乐趣的catch块。在try块产生嵌套时,异常将既源于内层try块,也源于外层try块。这种环境下,处理惩罚措施应该按先内后外的顺序查找catch块。但它其实没须要体贴这些,因为,在try块表中,VC++老是把内层try块放在外层try块的前面。
异常处理惩罚措施尚有一个困难就是“如何按照catch块的相关数据布局判定这个catch块是否愿意处理惩罚当前异常”。这是通过较量异常的范例和catch块的参数的范例来完成的。譬喻下面这个措施:
void foo()
{
try
{
throw E();
}
catch(H)
{
//.
}
}
假如H和E的范例完全沟通的话,catch块就要捕捉这个异常。这意味着处理惩罚措施必需在运行时举办范例较量,对C等语言来说,这是不行能的,因为它们无法在运行时获得工具的范例。C++则差异,它有了运行时范例识别(runtime type identification,RTTI),并提供了运行时范例较量的尺度要领。C++在尺度头文件中界说了type_info类,它能在运行时代表一个范例。catch块数据布局的第二个字段(ptype_info,见图4)是一个指向type_info布局的指针,它在运行时就代表catch块的参数范例。type_info也重载了==运算符,可以或许指出两种范例是否完全沟通。这样,异常处理惩罚措施只要较量(挪用==运算符)catch块参数的type_info(可以通过catch块的相关数据布局来会见)和异常的type_info是否沟通,就能知道catch块是不是愿意捕捉当前异常了。
catch块的参数范例可以通过funcinfo布局获得,但异常的type_info从哪来呢?当编译器遇到
throw E();
这条语句时,它会为异常生成一个excpt_info布局,如图5所示。照旧要提醒你留意这里用的名字大概与VC++利用的纷歧致,并且仍然只有与我们的接头相关的字段。从图中可以看出,异常的type_info可以通过excpt_info布局获得。由于异常处理惩罚措施需要拷贝异常工具(在挪用catch块之前),也需要消除去它(在挪用catch块之后),所以编译器在这个布局中同时提供了异常的拷贝结构函数、巨细和析构函数的信息。
在catch块的参数是基类,而异常是派生类时,异常处理惩罚措施也应该挪用catch块。然而,这种环境下,较量它们的type_info绝对是不相等,因为它们原来就不是沟通的范例。并且,type_info类也没有提供任何其他函数或运算符来指出一个类是另一个类的基类。但异常处理惩罚措施还必需得去挪用catch块!为了办理这个问题,编译器只能为处理惩罚措施提供更多的信息:假如异常是派生类,那么etypeinfo_table(通过excpt_info会见)将包括多个指向etype_info(扩展了type_info,这个名字是我启的)的指针,它们别离指向了各个基类的etype_info。这样,处理惩罚措施就可以把catch块的参数和所有这些type_info较量,只要有一个沟通,就挪用catch块。
在竣事这一部门之前,尚有最后一个问题:异常处理惩罚措施是怎么知道异常和excpt_info布局的?下面我就要答复这个问题。
VC++会把throw语句翻译成下面的样子:
//throw E(); //编译器会为E生成excpt_info布局
E e = E(); //在栈上建设异常
_CxxThrowException(&e, E_EXCPT_INFO_ADDR);
__CxxThrowException会把节制权连带它的两个参数都交给操纵系统(节制权转移是通过软件间断实现的,请拜见RaiseException)。而操纵系统,在为挪用异常回调函数做筹备时,会把这两个参数打包到一个_EXCEPTION_RECORD布局中。接着,它从EXCEPTION_REGISTRATION链表的头结点(由FS:[0]指向)开始,依次挪用各节点的异常处理惩罚措施。并且,指向当前EXCEPTION_REGISTRATION布局的指针也会作为异常处理惩罚措施的第二个参数呈现。前面已经说过,VC++中的每个函数都在栈上建设并注册了EXCEPTION_REGISTRATION布局。所以通报这个参数可以让处理惩罚措施知道许多重要信息,好比说:EXCEPTION_REGISTRATION的id字段(用于查找catch块)、函数的栈桢(用于清理栈桢)和EXCEPTION_REGISTRATION结点在异常链表中的位置(用于仓库展开)等。第一个参数是指向_EXCEPTION_RECORD布局的指针,通过它可以找到异常和它的excpt_info布局。下面是excpt.h中界说的异常回调函数的原型:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD* ExcRecord,
void* EstablisherFrame,
_CONTEXT *ContextRecord,
void* DispatcherContext);
后两个参数和我们的接头干系不大。函数的返回值是一个列举范例(也在excpt.h中界说),我前面已经说过,假如处理惩罚措施找不到catch块,它就会向系统返回ExceptionContinueSearch,对本文而言,我们只要知道这一个返回值就行了。_EXCEPTION_RECORD布局是在winnt.h中界说的:
struct _EXCEPTION_RECORD
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
_EXCEPTION_RECORD* ExcRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[15];
}EXCEPTION_RECORD;
ExceptionInformation数组中元素的个数和范例取决于ExceptionCode字段。假如是C++异常(异常代码是0xe06d7363,源于throw语句),那么数组中将包括指向异常和excpt_info布局的指针;假如是其他异常,那数组中根基上就不会有什么内容,这些异常包罗除零溢出、会见违例等,你可以在winnt.h中找到它们的异常代码。
ExceptionFlags字段用于汇报异常处理惩罚措施应该采纳什么操纵。假如它是EH_UNWINDING(见Except.inc),那是说仓库正在展开,这时,处理惩罚措施要清理栈桢,然后返回。不然处理惩罚措施应该在函数中查找catch块并挪用它。清理栈桢意味着必需找到异常产生时保留在栈桢上的所有局部工具,并挪用其析构函数,下一节我们迁就此举办具体接头。
清理栈桢
#p#分页标题#e#
C++尺度明晰指出:仓库展开事情必需挪用异常产生时所有保留的局部工具的析构函数。如下面的代码:
int g_i = 0;
void foo()
{
T o1, o2;
{
T o3;
}
10/g_i; //这里会产生异常
T o4;
//…
}
foo有o1、o2、o3、o4四个局部工具,但异常产生时,o3已经“灭亡”,o4还未“出生”,所以异常处理惩罚措施应该只挪用o1和o2的析构函数。
前面已经说过,编译器会在函数的许多处所安插代码来记录当前的运行状态。实际上,编译器在函数中配置了一些要害区域,并为它们分派了id,进入要害区域时要记录它的id,退出时规复前一个id。try块就是一个例子,其id就是start id。所以,在try块的进口,编译器会把它的start id记到栈桢上去。局部工具从建设到销毁也确定了一个要害区域,可能,换句话说,编译器给每个局部工具分派了独一的id,譬喻下面的措施:
void foo()
{
T t1;
//.
}
编译器会在t1的界说后头(也就是t1建设今后),把它的id写到栈桢上:
void foo()
{
T t1;
_id = t1_id; //编译器插入的语句
//.
}
上面的_id是编译器偷偷建设的局部变量,它的位置与EXCEPTION_REGISTRATION的id字段重叠。雷同的,在挪用工具的析构函数前,编译器会规复前一个要害区域的id。
清理栈桢时,异常处理惩罚措施读出id的值(通过EXCEPTION_REGISTRATION布局的id字段或栈桢指针EBP下面的4个字节来会见)。这个id可以表白,函数在运行到与它相关联的谁人点之前没有产生异常。所有在这一点之前界说的工具都已初始化,应该挪用这些工具中的一部门或全部工具的析构函数。请留意某些工具是属于子块(如前面代码中的o3)的,产生异常时大概已经销毁了,不该该挪用它们的析构函数。
编译器还为函数生成了另一个数据布局——仓库展开表(unwindtable,我启的名字),它是一个unwind布局的数组,可通过funcinfo来会见,如图4所示。函数的每个要害区域都有一个unwind布局,这些布局在展开表中呈现的序次和它们所对应的区域在函数中的呈现序次完全沟通。一般unwind布局也会关联一个工具(别忘了,每个工具的界说都开发了要害区域,并有id与其对应),它内里有如何销毁这个工具的信息。每当编译器遇到工具界说,它就生成一小段代码,这段代码知道工具在栈桢上的地点(就是它相对付栈桢指针的偏移),并能销毁它。unwind布局中有一个字段用于生存这段代码的进口地点:
typedef void (*CLEANUP_FUNC)();
struct unwind
{
int prev;
CLEANUP_FUNC cf;
};
try块对应的unwind布局的cf字段是空值NULL,因为没有与它对应的工具,所以也没有对象需要它去销毁。通过prev字段,这些unwind布局也形成了一个链表。异常处理惩罚措施清理栈桢时,会读取当前的id值,以它为索引取得展开表中对应的项,并挪用其第二个字段指向的清理代码,这样,谁人与之关联的工具就被销毁了。然后,处理惩罚措施将以当前unwind布局的prev字段为索引,继承在展开表中找下一个unwind布局,挪用其清理代码。这一进程将一直反复,直到链表的末了(prev的值是-1)。图6画出了本节开始时提到的那段代码的仓库展开表。
此刻把new运算符也加进来,对付下面的代码:
T* p = new T();
#p#分页标题#e#
系统会首先为T分派内存,然后挪用它的结构函数。所以,假如结构函数抛出了异常,系统就必需释放这些内存。因此,动态建设那些拥有“有为的结构函数”的范例时,VC++也为new运算符分派了id,而且仓库展开表中也有与其对应的项,其清理代码将释放分派的内存空间。挪用结构函数前,编译器把new运算符的id存到EXCEPTION_REGISTRATION布局中,结构函数顺利返回后,它再把id规复本钱来的值。
更进一步说,结构函数抛出异常时,工具大概方才结构了一部门,假如它有子成员工具或子基类工具,而且产生异常时它们中的一部门已经结构完成的话,就必需挪用这些工具的析构函数。和普通函数一样,编译器也给结构函数生成了相关的数据来辅佐完成这个任务。
展开仓库时,异常处理惩罚措施挪用的是用户界说的析构函数,这一点你必需留意,因为它也有大概抛出异常!C++尺度划定仓库展开进程中,析构函数不能抛出异常,不然系统将挪用std::terminate。
实现
本节我们接头其他三个有待具体表明的问题:
a)如何安装异常处理惩罚措施
b)catch块从头抛出异常或抛出新异常时应该如那里理惩罚
c)如何对所有线程提供异常处理惩罚支持
伴同本文,有一个演示项目,查察个中的readme.txt文件可以获得一些编译方面的辅佐①。
第一项任务是安装异常处理惩罚措施,也就是把VC++的处理惩罚措施替换掉。从前面的接头中,我们已经清楚地知道__CxxFrameHandler函数是VC++所有异常处理惩罚事情的进口。编译器为每个函数都生成一段代码,它们在产生异常时被挪用,把相应的funcinfo布局的指针交给__CxxFrameHandler。
install_my_handler()函数会改写__CxxFrameHandler的进口处的代码,让措施跳转到my_exc_handler()函数。不外,__CxxFrameHandler位于只读的内存页,对它的任何写操纵城市导致会见违例,所以必需首先用VirtualProtectEx把该内存页的掩护方法改成可读写,等改写完毕后,再改回只读。写入的数据是一个jmp_instr布局。
//install_my_handler.cpp
#include <windows.h>
#include "install_my_handler.h"
//C++默认的异常处理惩罚措施
extern "C"
EXCEPTION_DISPOSITION __CxxFrameHandler(
struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext
);
namespace
{
char cpp_handler_instructions[5];
bool saved_handler_instructions = false;
}
namespace my_handler
{
//我的异常处理惩罚措施 EXCEPTION_DISPOSITION
my_exc_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
) throw();
#pragma pack(push, 1)
struct jmp_instr
{
unsigned char jmp;
DWORD offset;
};
#pragma pack(pop)
bool WriteMemory(void* loc, void* buffer, int size)
{
HANDLE hProcess = GetCurrentProcess();
//把包括内存范畴[loc,loc+size]的页面的掩护方法改成可读写
DWORD old_protection;
BOOL ret = VirtualProtectEx(hProcess, loc, size, PAGE_READWRITE, &old_protection);
if(ret == FALSE)
return false;
ret = WriteProcessMemory(hProcess, loc, buffer, size, NULL);
//规复本来的掩护方法
DWORD o2;
VirtualProtectEx(hProcess, loc, size, old_protection, &o2);
return (ret == TRUE);
}
bool ReadMemory(void* loc, void* buffer, DWORD size)
{
HANDLE hProcess = GetCurrentProcess();
DWORD bytes_read = 0;
BOOL ret = ReadProcessMemory(hProcess, loc, buffer, size, &bytes_read);
return (ret == TRUE && bytes_read == size);
}
bool install_my_handler()
{
void* my_hdlr = my_exc_handler; void* cpp_hdlr = __CxxFrameHandler;
jmp_instr jmp_my_hdlr;
jmp_my_hdlr.jmp = 0xE9;
//从__CxxFrameHandler+5开始计较偏移,因为jmp指令长5字节
jmp_my_hdlr.offset = reinterpret_cast(my_hdlr) – (reinterpret_cast(cpp_hdlr) + 5);
if(!saved_handler_instructions)
{
if(!ReadMemory(cpp_hdlr, cpp_handler_instructions, sizeof(cpp_handler_instructions)))
return false;
saved_handler_instructions = true;
}
return WriteMemory(cpp_hdlr, &jmp_my_hdlr, sizeof(jmp_my_hdlr));
}
bool restore_cpp_handler()
{
if(!saved_handler_instructions)
return false;
else
{
void* loc = __CxxFrameHandler;
return WriteMemory(loc, cpp_handler_instructions, sizeof(cpp_handler_instructions));
}
}
}
编译指令#pragma pack(push, 1)汇报编译器不要在jmp_instr布局中填充任何用于对齐的空间。没有这条指令,jmp_instr的巨细将是8字节,而我们需要它是5字节。
此刻从头回到异常处理惩罚这个主题上来。挪用catch块时,它大概从头抛出异常或抛出新异常。前一种环境下,异常处理惩罚措施必需继承流传(propagate)当前异常;后一种环境下,它需要在继承之前销毁本来的异常。此时,处理惩罚措施要面临两个困难:“如何知道异常是源于catch块照旧措施的其他部门”和“如何跟踪本来的异常”。我的办理要领是:在挪用catch块之前,把当前异常生存在exception_storage工具中,并注册一个专用于catch块的异常处理惩罚措施——catch_block_protector。挪用get_exception_storage()函数,就能获得exception_storage工具:
exception_storage* p = get_exception_storage();
p->set(pexc, pexc_info);
注册 catch_block_protector;
挪用catch块; //….
#p#分页标题#e#
这样,当catch块(从头)抛出异常时,措施将会执行catch_block_protector。假如是抛出了新异常,这个函数可以从exception_storage工具中疏散出前一个异常并销毁它;假如是从头抛出本来的异常(可以通过ExceptionInformation数组的前两个元素知道是新异常照旧旧异常,后一种环境下着两个元素都是0,拜见下面的代码),就通过拷贝ExceptionInformation数组来继承流传它。下面的代码就是catch_block_protector()函数的实现。
//——————————————————————-
// 假如这个处理惩罚措施被挪用了,可以断定是catch块(从头)抛出了异常。
// 异常处理惩罚措施(my_handler)在挪用catch块之前注册了它。其任务是判定
// catch块抛出了新异常照旧从头抛出了本来的异常,并采纳相应的操纵。
// 在前一种环境下,它需要销毁通报给catch块的前一个异常工具;在后一种
// 环境下,它必需找到本来的异常并将其生存到ExceptionRecord中供异常
// 处理惩罚措施利用。
//——————————————————————-
EXCEPTION_DISPOSITION catch_block_protector(
_EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT *ContextRecord,
void* DispatcherContext
) throw ()
{
EXCEPTION_REGISTRATION *pFrame;
pFrame= reinterpret_cast<EXCEPTION_REGISTRATION*>(EstablisherFrame);
if(!(ExceptionRecord->ExceptionFlags & (_EXCEPTION_UNWINDING | _EXCEPTION_EXIT_UNWIND)))
{
void *pcur_exc = 0, *pprev_exc = 0;
const excpt_info *pexc_info = 0, *pprev_excinfo = 0;
exception_storage* p = get_exception_storage();
pprev_exc = p->get_exception();
pprev_excinfo = p->get_exception_info();
p->set(0, 0);
bool cpp_exc = ExceptionRecord->ExceptionCode == MS_CPP_EXC;
get_exception(ExceptionRecord, &pcur_exc);
get_excpt_info(ExceptionRecord, &pexc_info);
if(cpp_exc && 0 == pcur_exc && 0 == pexc_info) //从头抛出
{
ExceptionRecord->ExceptionInformation[1] = reinterpret_cast<DWORD>(pprev_exc);
ExceptionRecord->ExceptionInformation[2] = reinterpret_cast<DWORD>(pprev_excinfo);
}
else
{
exception_helper::destroy(pprev_exc, pprev_excinfo);
}
}
return ExceptionContinueSearch;
}
下面是get_exception_storage()函数的一个实现:
exception_storage* get_exception_storage()
{
static exception_storage es;
return &es;
}
在单线程措施中,这是一个完美的实现。但在多线程中,这就是个劫难了,想象一下多个线程会见它,并把异常工具生存在内里的情景吧。由于每个线程都有本身的仓库和异常处理惩罚链,我们需要一个线程安详的get_exception_storage实现:每个线程都有本身单独的exception_storage,它在线程启动时被建设,并在竣事时被销毁。Windows提供的线程局部存储(thread local storage,TLS)可以满意这个要求,它能让每个线程通过一个全局键值来会见为这个线程所私有的工具副本,这是通过TlsGetValue()和TlsSetValue这两个API来完成的。
Excptstorage.cpp中给出了get_exception_storage()函数的实现。它会被编译成动态链接库,因为我们可以籍此知道线程的建设和退出——系统在这两种环境下城市挪用所有(当前历程加载的)dll的DllMain()函数,这让我们有时机建设特定于线程的数据,也就是exception_storage工具。
//excptstorage.cpp
#include "excptstorage.h"
#include <windows.h>
namespace
{
DWORD dwstorage;
}
namespace my_handler
{
__declspec(dllexport) exception_storage* get_exception_storage() throw ()
{
void * p = TlsGetValue(dwstorage);
return reinterpret_cast <exception_storage*>(p);
}
}
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
using my_handler::exception_storage;
exception_storage *p;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//主线程(第一个线程)不会收到DLL_THREAD_ATTACH通知,所以,
//与其相关的操纵也放在这了
dwstorage = TlsAlloc();
if (-1 == dwstorage)
return FALSE;
p = new exception_storage();
TlsSetValue(dwstorage, p);
break ;
case DLL_THREAD_ATTACH:
p = new exception_storage();
TlsSetValue(dwstorage, p);
break;
case DLL_THREAD_DETACH:
p = my_handler::get_exception_storage();
delete p;
break ;
case DLL_PROCESS_DETACH:
p = my_handler::get_exception_storage();
delete p;
break ;
}
return TRUE;
}
结论
综上所述,异常处理惩罚是在操纵系统的协助下,由C++编译器和运行时异常处理惩罚库配合完成的。
注释和参考资料
#p#分页标题#e#
① 本文写作期间,微软宣布了Visual Studio 7.0。本文的异常处理惩罚库主要是在运行于奔驰处理惩罚器的windows2000上利用VC++6.0编译和测试的。但我也在VC++5.0和VC++7.0 beta版上测试过。6.0和7.0之间有一些不同,6.0先把异常(或其引用)拷贝到catch块的栈桢上,然后在挪用catch块之前举办仓库展开;7.0则先举办仓库展开。在这方面,我的库代码的行为较量靠近6.0版。
② 拜见Matt Pietrek颁发在MSDN上的文章《structured exception handling》。
③ 假如一个函数既不含try块,也没有界说任何具有“有为的析构函数”的工具,那么编译器将不为它生成用于异常处理惩罚的数据。