第24章 异常处理程序和软件异常
异常是我们不希望有的事件。在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。不过,这样的错误还是常常会发生的。C P U负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。C P U引发的异常,就是所谓的硬件异常( hardware exception)。在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常( software exception)当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。下面就是异常处理程序的文法:
__try { //Guarded body ... } __except(exception filter) { // Exception handler ... }
与结束处理程序(前一章讨论过)不同,异常过滤器( exception filter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。下面几节的内容举例说明t r y - e x c e p t块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。
24.1.1 Funcmeister1
这里是一个t r y - e x c e p t i o n块的更具体的例子。
DWORD Funcmeister1() { DWORD dwTemp; //1. Do any processing here. ... __try { //2. Perform some operation. dwTemp = 0; } __except(EXCEPTION_EXECUTE_HANDLER) { // Handle an exception; this never executes. ... } //3. Continue processing. return(dwTemp); }
尽管在结束处理程序的t r y块中使用r e t u r n、g o t o、c o n t i n u e和b r e a k语句遭到强烈地反对,但在异常处理程序的t r y块中使用这些语句不会产生速度和代码规模方面的不良影响。这样的语句出现在与e x c e p t块相结合的t r y块中不会引起局部展开的系统开销。
24.1.2 Funcmeister2
让我们修改这个函数,看会发生什么事情:
DWORD Funcmeister2() { DWORD dwTemp = 0; //1. Do any processing here. ... __try { //2. Perform some operation(s). dwTemp = 5 / dwTemp; // Generates an exception dwTemp += 10; // Never executes } __except( /* 3. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { //4. Handle an exception. MessageBeep(0); ... } //5. Continue processing. return(dwTemp); }
表24-1 标识符及其定义
标识符 | 定义为 |
E X C E P T I O N _ E X E C U T E _ H A N D L E R | 1 |
E X C E P T I O N _ C O N T I N U E _ S E A R C H | 0 |
E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N | -1 |
下面几节将讨论这些标识符如何改变线程的执行。在阅读这些内容时可参阅图2 4 - 1,该图概括了系统如何处理一个异常的情况。
图24-1 系统如何处理一个异常
24.2 EXCEPTION_EXECUTE_HANDLER
在F u n c m e i s t e r 2中,异常过滤器表达式的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开(本章后面将讨论),然后执行向e x c e p t块中代码(异常处理程序代码)的跳转。在e x c e p t块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使Wi n d o w s应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。
但是,当e x c e p t块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能性。
第一种可能性是从产生异常的C P U指令之后恢复执行。在F u n c m e i s t e r 2中执行将从对d w Te m p加1 0的指令开始恢复。这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的指令不能够继续成功地执行。
在F u n c m e i s t e r 2中,代码可以继续正常执行,但是, F u n c m e i s t e r 2已不是正常的情况。代码应该尽可能地结构化,这样,在产生异常的指令之后的C P U指令有望获得有效的返回值。例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操作。如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。
这里是另外一个例子,说明为什么在一个失败的C P U指令之后,执行不能够继续。我们用下面的程序行来替代F u n c m e i s t e r 2中产生异常的C语句:
malloc(5 / dwTemp);
所幸的是,微软没有让系统从产生异常的指令之后恢复指令的执行。这种决策使我们免于面对上面的问题。
第二种可能性是从产生异常的指令恢复执行。这是很有意思的可能性。如果在e x c e p t块中有这样的语句会怎么样呢:
dwTemp = 2;
第三种可能性是从e x c e p t块之后的第一条指令开始恢复执行。这实际是当异常过滤器表达式的值为E X C E P T I O N _ E X E C U T E _ H A N D L E R时所发生的事。在e x c e p t块中的代码结束执行后,控制从e x c e p t块之后的第一条指令恢复。
24.2.1 一些有用的例子
假如要实现一个完全强壮的应用程序,该程序需要每周7天,每天2 4小时运行。在今天的世界里,软件变得这么复杂,有那么多的变量和因子来影响程序的性能,笔者认为如果不用S E H,要实现完全强壮的应用程序简直是不可能的。我们先来看一个样板程序,即C的运行时函数s t r c p y:
char* strcpy( char* strDestination, const char* strSource);
使用S E H,就可以建立一个完全强壮的s t r c p y函数:
char* RobustStrCpy(char* strDestination, const char* strSource) { __try { strcpy(strDestination, strSource); } __except(EXCEPTION_EXECUTE_HANDLER) { // Nothing to do here } return(strDestination); }
我们再看另外一个例子。这个函数返回一个字符串里的以空格分界的符号个数:
int RobustHowManyToken(const char* str) { int nHowManyTokens = -1; // -1 indicates failure char* strTemp = NULL; // Assume failure __try { // Allocate a temporary buffer strTemp = (char*) malloc(strlen(str) + 1); // Copy the original string to the temporary buffer strcpy(strTemp, str); // Get the first token char* pszToken = strtok(strTemp, " "); // Iterate through all the tokens for(; pszToken != NULL; pszToken = strtok(NULL, " ")) nHowManyTokens++; nHowManyTokens++; // Add 1 since we started at -1 } __except(EXCEPTION_EXECUTE_HANDLER) { // Nothing to do here } //Free the temporary buffer (guaranteed) free(strTemp); return(nHowManyTokens); }
首先,如果调用者向函数传递了N U L L(或任何无效的内存地址),n H o w M a n y To k e n s被初始化成-1。在t r y块中对s t r l e n的调用会引起存取异常。异常过滤器获得控制并将控制转移给e x c e p t块,e x c e p t块什么也不做。在e x c e p t块之后,调用f r e e来释放临时内存块。但是,这个内存从未分配,所以结束调用f r e e,向它传递N U L L作为参数。ANSI C明确说明用N U L L作为参数调用f r e e是合法的。这时f r e e什么也不做,这并不是错误。最后,函数返回-1,指出失败。注意进程并没有结束。
其次,调用者可能向函数传递了一个有效的地址,但对m a l l o c的调用(在t r y块中)可能失败并返回N U L L。这将导致对s t r c p y的调用引起一个存取异常。同样,异常过滤器被调用,e x c e p t块执行(什么也不做),f r e e被调用,传递给它N U L L(什么也不做),返回-1,告诉调用程序该函数失败。注意进程也没有结束。
最后,假定调用者向函数传递了一个有效的地址,并且对m a l l o c的调用也成功了。这种情况下,其余的代码也会成功地在n H o w M a n y To k e n s变量中计算符号的数量。在t r y块的结尾,异常过滤器不会被求值, e x c e p t块中代码不会被执行,临时内存缓冲区将被释放,并向调用者返回n H o w M a n y To k e n s。
使用S E H会感觉很好。R o b u s t H o w M a n y To k e n函数说明了如何在不使用t r y - f i n a l l y的情况下保证释放资源。在异常处理程序之后的代码也都能保证被执行(假定函数没有从t r y块中返回—应避免的事情)。
我们再看一个特别有用的S E H例子。这里的函数重复一个内存块:
PBYTE RobustMemDup(PBYTE pbSrc, size_t cb) { PBYTE pbDup = NULL; // Assume failure __try { // Allocate a buffer for the duplicate memory block pbDup = (PBYTE) malloc(cb); memcpy(pbDup, pbSrc, cb); } __except(EXCEPTION_EXECUTE_HANDLER) { free(pbDup); pbDup = NULL; } return(pbDup); }
• 如果调用程序对p b S r c参数传递了一个无效地址,或者如果对m a l l o c的调用失败(返回N U L L),m e m c p y将引起一个存取异常。该存取异常执行过滤器,将控制转移到e x c e p t块。在e x c e p t块内,内存缓冲区被释放, p b D u p被设置成N U L L以便调用程序能够知道函数失败。这里,注意ANSI C允许对f r e e传递N U L L。
• 如果调用程序给函数传递一个有效地址,并且如果对m a l l o c的调用成功,则新分配内存块的地址返回给调用程序。
24.2.2 全局展开
当一个异常过滤器的值为E X C E P T I O N _ E X E C U T E _ H A N D L E R时,系统必须执行一个全局展开(global unwind)。这个全局展开使所有那些在处理异常的t r y _ e x c e p t块之后开始执行但未完成的t r y - f i n a l l y块恢复执行。图2 4 - 2是描述系统如何执行全局展开的流程图,在解释后面的例子时,请参阅这个图。
void FuncOStimpy1() { //1. Do any processing here. ... __try { //2. Call another function. FuncORen1(); // Code here never executes. } __except( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { //8. After the unwind, the exception handler executes. MessageBox(…); } //9. Exception handled--continue execution. } void FuncORen1() { DWORD dwTemp = 0; //3. Do any processing here. __try { //4. Request permission to access protected data. WaitForSingleObject(g_hSem, INFINITE); //5. Modify the data. // An exception is generated here. g_dwProtectedData = 5 / dwTemp; } __finally { //7. Global unwind occurs because filter evaluated // to EXCEPTION_EXECUTE_HANDLER. // Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // Continue processing--never executes. ... }
F u n c OSt i m p y 1开始执行,进入它的t r y块并调用F u n c O R e n 1。F u n c O R e n 1开始执行,进入它的t r y块并等待获得信标。当它得到信标,F u n c O R e n 1试图改变全局数据变量g _ d w P r o t e c t e d D a t a。但由于除以0而产生一个异常。系统因此取得控制,开始搜索一个与e x c e p t块相配的t r y块。因为F u n c O R e n 1中的t r y与同一个f i n a l l y块相配,所以系统再上溯寻找另外的t r y块。这里,系统在F u n c O S t i m p y 1中找到一个t r y块,并且发现这个t r y块与一个e x c e p t块相配。
系统现在计算与F u n c O S t i m p y 1中e x c e p t块相联的异常过滤器的值,并等待返回值。当系统看到返回值是E X C E P T I O N _ E X E C U T E _ H A N D L E R的,系统就在F u n c O R e n 1的f i n a l l y块中开始一个全局展开。注意这个展开是在系统执行F u n c O S t i m p y 1的e x c e p t块中的代码之前发生的。对于一个全局展开,系统回到所有未完成的t r y块的结尾,查找与f i n a l l y块相配的t r y块。在这里,系统发现的f i n a l l y块是F u n c O R e n 1中所包含的f i n a l l y块。
当系统执行F u n c O R e n 1 的f i n a l l y块中的代码时,就可以清楚地看到S E H的作用了。F u n c O R e n 1释放信标,使另一个线程恢复执行。如果这个f i n a l l y块中不包含R e l e a s e S e m a p h o r e的调用,则信标不会被释放。
在f i n a l l y块中包含的代码执行完之后,系统继续上溯,查找需要执行的未完成f i n a l l y块。在这个例子中已经没有这样的f i n a l l y块了。系统到达要处理异常的t r y - e x c e p t块就停止上溯。这时,全局展开结束,系统可以执行e x c e p t块中所包含的代码。
结构化异常处理就是这样工作的。S E H比较难于理解,是因为在代码的执行当中与系统牵扯太多。程序代码不再是从头到尾执行,系统使代码段按照它的规定次序执行。这种执行次序虽然复杂,但可以预料。按图2 4 - 1和图2 4 - 2的流程图去做,就可以有把握地使用S E H。
图24-2 系统如何执行一个全局展开
为了更好地理解这个执行次序,我们再从不同的角度来看发生的事情。当一个过滤器返回E X C E P T I O N _ E X E C U T E _ H A N D L E R时,过滤器是在告诉系统,线程的指令指针应该指向e x c e p t块中的代码。但这个指令指针在F u n c O R e n 1的t r y块里。回忆一下第2 3章,每当一个线程要从一个t r y - f i n a l l y块离开时,必须保证执行f i n a l l y块中的代码。在发生异常时,全局展开就是保证这条规则的机制。
24.2.3 暂停全局展开
通过在f i n a l l y块里放入一个r e t u r n语句,可以阻止系统去完成一个全局展开。请看下面的代码:
void FuncMonkey() { __try { FuncFish(); } __except(EXCEPTION_EXECUTE_HANDLER) { MessageBeep(0); } MessageBox(…); } void FuncFish() { FuncPheasant(); MessageBox(…); } void FuncPheasant() { __try { strcpy(NULL, NULL); } __finally { return; } }
全局展开启动,先执行F u n c P h e a s a n t的f i n a l l y块中的代码。这个代码块包含一个r e t u r n语句。这个r e t u r n语句使系统停止做展开, F u n c P h e a s a n t将实际返回到F u n c F i s h。然后F u n c F i s h又返回到函数F u n c M o n k e y。F u n c M o n k e y中的代码继续执行,调用M e s s a g e B o x。
注意F u n c M o n k e y的异常块中的代码从不会执行对M e s s a g e B e e p的调用。F u n c P h e a s a n t的f i n a l l y块中的r e t u r n语句使系统完全停止了展开,继续执行,就像什么也没有发生。
微软专门设计S E H按这种方式工作。程序员有可能希望使展开停止,让代码继续执行下去。这种方法为程序员提供了一种手段。原则上,应该小心避免在f i n a l l y块中安排r e t u r n语句。
24.3 EXCEPTION_CONTINUE_EXECUTION
我们再仔细考察一下异常过滤器,看它是如何计算出定义在E x c p t . h中的三个异常标识符之一的。在“F u n c m e i s t e r 2”一节中,为简单起见,在过滤器里直接硬编码了标识符E X C E P T I O N _E X E C U T E _ H A N D L E R,但实际上可以让过滤器调用一个函数确定应该返回哪一个标识符。这里是另外一个例子。
char g_szBuffer[100]; void FunclinRoosevelt1() { int x = 0; char *pchBuffer = NULL; __try { *pchBuffer = 'J'; x = 5 / x; } __except(OilFilter1(&pchBuffer)) { MessageBox(NULL, "An exception occurred", NULL, MB_OK); } MessageBox(NULL, "Function completed", NULL, MB_OK); } LONG OilFilter1(char **ppchBuffer) { if(*ppchBuffer == NULL) { *ppchBuffer = g_szBuffer; return(EXCEPTION_CONTINUE_EXECUTION); } return(EXCEPTION_EXECUTE_HANDLER); }
当O i l F i l t e r获得控制时,它要查看* p p c h B u ff e r是不是N U L L,如果是,把它设置成指向全局缓冲区g _ s z B u ff e r。然后这个过滤器返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。当系统看到过滤器的值是E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N时,系统跳回到产生异常的指令,试图再执行一次。这一次,指令将执行成功,字母‘ J’将放在g _ s z B u ff e r的第一个字节。
随着代码继续执行,我们又在t r y块中碰到除以0的问题。系统又要计算过滤器的值。这一次,O i l F i l t e r看到* p p c h B u ff e r不是N U L L,就返回E X C E P T I O N _ E X E C U T E _ H A N D L E R,这是告诉系统去执行e x c e p t块中的代码。这会显示一个消息框,用文本串报告发生了异常。
如你所见,在异常过滤器中可以做很多的事情。当然过滤器必须返回三个异常标识符之一,但可以执行任何其他你想执行的任务。
使用带警告的E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N
我们已经知道,修改F u n c l i n R o o s e v e l t 1函数中的问题可使系统继续执行,也可能不执行,这要取决于程序的目标C P U,取决于编译程序为C/C + +语句生成的指令,取决于编译程序的选项设置。
一个编译程序对下面的C / C + +语句可能生成两条机器指令:
*pchBuffer = 'J';
MOV EAX, [pchBuffer] // Move the address into a register MOV [EAX], 'J' // Move 'J' into the address
如果编译程序优化了代码,继续执行可能顺利;如果编译程序没有优化代码,继续执行就可能失败。这可能是个非常难修复的b u g,需要检查源代码生成的汇编语言程序,确定程序出了什么错。这个例子的寓意就是在异常过滤器返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N时,要特别地小心。
有一种情况可保证E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N每次总能成功:当离散地向一个保留区域提交存储区时。在第1 5章讨论过如何保存一个大的地址空间,并向这个地址空间离散地提交存储区。V M A l l o c示例程序说明了这个例子。编写V M A l l o c程序的一种更好的办法是必要时使用S E H提交存储区,而不是每次调用Virtual Alloc函数。
在第1 6章,我们讨论了线程栈。特别是,我们讲解了系统如何为线程的栈保留一个1 M B的地址空间范围,以及在线程需要内存区时,系统如何自动向栈提交新的内存区。为此,系统在内部建立了一个S E H框架。当一个线程试图去存取并不存在的栈存储区时,就产生一个异常。系统的异常过滤器可以确定这个异常是源于试图存取栈的保留地址空间。异常过滤器调用Vi r t u a l A l l o c向线程的栈提交更多的存储区,然后过滤器返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。这时,试图存取栈存储区的C P U指令可以成功执行,线程可以继续运行。
将虚拟内存技术同结构化异常处理结合起来,可以编写一些执行非常快,非常高效的程序。下一章的S p r e a d S h e e t示例程序将说明如何使用S E H有效地实现内存管理。这个代码执行得非常快。
24.4 EXCEPTION_CONTINUE_SEARCH
迄今为止我们看到的例子都很平常。通过增加一个函数调用,让我们来看看其他方面的问题:
char g_szBuffer[100]; void FunclinRoosevelt2() { char *pchBuffer = NULL; __try { FuncAtude2(pchBuffer); } __except(OilFilter2(&pchBuffer)) { MessageBox(…); } } void FuncAtude2(char *sz) { *sz = 0; } LONG OilFilter2(char **ppchBuffer) { if(*ppchBuffer == NULL) { *ppchBuffer = g_szBuffer; return(EXCEPTION_CONTINUE_EXECUTION); } return(EXCEPTION_EXECUTE_HANDLER); }
现在我们让问题变得更复杂一点,在程序中再增加一个t r y _ e x c e p t块。
char g_szBuffer[100]; void FunclinRoosevelt3() { char *pchBuffer = NULL; __try { FuncAtude3(pchBuffer); } __except(OilFilter3(&pchBuffer)) { MessageBox(…); } } void FuncAtude3(char *sz) { __try { *sz = 0; } __except(EXCEPTION_CONTINUE_SEARCH) { // This never executes. ... } } LONG OilFilter3(char **ppchBuffer) { if(*ppchBuffer == NULL) { *ppchBuffer = g_szBuffer; return(EXCEPTION_CONTINUE_EXECUTION); } return(EXCEPTION_EXECUTE_HANDLER); }
因为F u n c A t u d e 3的过滤器的值为E X C E P T I O N _ C O N T I N U E _ S E A R C H,系统将查找前面的t r y块(在F u n c l i n R o o s e v e l t 3里),并计算其异常过滤器的值,这里异常过滤器是O i l F i l t e r 3。O i l F i l t e r 3看到p c h B u ff e r是N U L L,将p c h B u ff e r设定为指向全局缓冲区,然后告诉系统恢复执行产生异常的指令。这将使F u n c A t u d e 3的t r y块中的代码执行,但不幸的是, F u n c A t u d e 3的局部变量s z没有变化,恢复执行失败的指令只是产生另一个异常。这样,又造成死循环。
前面说过,系统要查找最近执行的与e x c e p t块相匹配的t r y块,并计算它的过滤器值。这就是说,系统在查找过程当中,将略过那些与f i n a l l y块相匹配而不是与e x c e p t块相匹配的t r y块。这样做的理由很明显: f i n a l l y块没有异常过滤器,系统没有什么要计算的。如果前面例子中F u n c A t u d e 3包含一个f i n a l l y块而不是e x c e p t块,系统将在一开始就通过F u n c l i n R o o s e v e l t 3的O i l F i l t e r 3计算异常过滤器的值。
第2 5章提供有关E X C E P T I O N _ C O N T I N U E _ S E A R C H的更多信息。
一个异常过滤器在确定要返回什么值之前,必须分析具体情况。例如,异常处理程序可能知道发生了除以0引起的异常时该怎么做,但是不知道该如何处理一个内存存取异常。异常过滤器负责检查实际情况并返回适当的值。
下面的代码举例说明了一种方法,指出所发生异常的类别:
__try { x = 0; y = 4 / x; } __except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { // Handle divide by zero exception. }
DWORD GetExceptionCode();
1. 与内存有关的异常
• E X C E P T I O N _ A C C E S S _ V I O L AT I O N。线程试图对一个虚地址进行读或写,但没有做适当的存取。这是最常见的异常。
• E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T。线程试图读或写不支持对齐( a l i g n m e n t)的硬件上的未对齐的数据。例如, 1 6位数值必须对齐在2字节边界上,3 2位数值要对齐在4字节边界上。
• E X C E P T I O N _ A R R AY _ B O U N D S _ E X C E E D E D。线程试图存取一个越界的数组元素,相应的硬件支持边界检查。
• E X C E P T I O N _ I N _ PA G E _ E R R O R。由于文件系统或一个设备启动程序返回一个读错误,造成不能满足要求的页故障。
• E X C E P T I O N _ G U A R D _ PA G E。一个线程试图存取一个带有PA G E _ G U A R D保护属性的内存页。该页是可存取的,并引起一个E X C E P T I O N _ G U A R D _ PA G E异常。
• EXCEPTION_STA C K _ O V E R F L O W。线程用完了分配给它的所有栈空间。
• E X C E P T I O N _ I L L E G A L _ I N S T R U C T I O N。线程执行了一个无效的指令。这个异常由特定的C P U结构来定义;在不同的C P U上,执行一个无效指令可引起一个陷井错误。
• E X C E P T I O N _ P R I V _ I N S T R U C T I O N。线程执行一个指令,其操作在当前机器模式中不允许。
2. 与异常相关的异常
• E X C E P T I O N _ I N VA L I D _ D I S P O S I T I O N。一个异常过滤器返回一值,这个值不是E X C E P T I O N _ E X E C U T E _ H A N D L E R 、E X C E P T I O N _ C O N T I N U E _ S E A R C H、E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N三者之一。
• E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。一个异常过滤器对一个不能继续的异常返回E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N。
3. 与调试有关的异常
• EXCEPTION_BREAKPOINT。遇到一个断点。
• E X C E P T I O N _ S I N G L E _ S T E P。一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕。
• E X C E P T I O N _ I N VA L I D _ H A N D L E。向一个函数传递了一个无效句柄。
4. 与整数有关的异常
• EXCEPTION_INT_DIVIDE_BY_ZERO。线程试图用整数0来除一个整数
• EXCEPTION_INT_OVERFLOW。一个整数操作的结果超过了整数值规定的范围。
5. 与浮点数有关的异常
• E X C E P T I O N _ F LT _ D E N O R M A L _ O P E R A N D。浮点操作中的一个操作数不正常。不正常的值是一个太小的值,不能表示标准的浮点值。
• EXCEPTION_FLT _ D I V I D E _ B Y _ Z E R O。线程试图用浮点数0来除一个浮点。
• EXCEPTION_FLT _ I N E X A C T _ R E S U LT。浮点操作的结果不能精确表示成十进制小数。
• EXCEPTION_FLT _ I N VA L I D _ O P E R AT I O N。表示任何没有在此列出的其他浮点数异常。
• EXCEPTION_FLT _ O V E R F L O W。浮点操作的结果超过了允许的值。
• EXCEPTION_FLT _ S TA C K _ C H E C K。由于浮点操作造成栈溢出或下溢。
• EXCEPTION_FLT _ U N D E R F L O W。浮点操作的结果小于允许的值。
内部函数G e t E x c e p t i o n C o d e只能在一个过滤器中调用( - - e x c e p t之后的括号里),或在一个异常处理程序中被调用。下面的代码是合法的:
__try { y = 0; x = 4 / y; } __except( ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) || (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { switch(GetExceptionCode()) { case EXCEPTION_ACCESS_VIOLATION: //Handle the access violation. ... break; case EXCEPTION_INT_DIVIDE_BY_ZERO: //Handle the integer divide by?. ... break; } }
__try { y = 0; x = 4 / y; } __except(CoffeeFilter()) { // Handle the exception. ... } LONG CoffeeFilter(void) { //Compilation error: illegal call to GetExceptionCode. return((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH); }
__try { y = 0; x = 4 / y; } __except(CoffeeFilter(GetExceptionCode())) { //Handle the exception. ... } LONG CoffeeFilter(DWORD dwExceptionCode) { return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH); }
表24-2 一个错误代码的构成
位 | 3 1 - 3 0 | 2 9 | 2 8 | 2 7 - 1 6 | 1 5 - 0 |
内容 | 严重性系数 | 微软/客户 | 保留 | 设备代码 | 异常代码 |
意义 | 0=成功 1=信息 2=警告 3=错误 | 0=微软定义 的代码 1=客户定义 的代码 | 必须为0 | 微软定义 (见表2 4 - 3 ) | 微软/客户定义 |
目前,微软定义了下面一些设备代码(见表2 4 - 3 )。
表24-3 设备代码及其值
设备代码 | 值 | 设备代码 | 值 |
FA C I L I T Y _ N U L L | 0 | FA C I L I T Y _ C O N T R O L | 10 |
FA C I L I T Y _ R P C | 1 | FA C I L I T Y _ C E RT | 11 |
FA C I L I T Y _ D I S PAT C H | 2 | FA C I L I T Y _ I N T E R N E T | 12 |
FA C I L I T Y _ S TO R A G E | 3 | FA C I L I T Y _ M E D I A S E RV E R | 13 |
FA C I L I T Y _ I T F | 4 | FA C I L I T Y _ M S M Q | 14 |
FA C I L I T Y _ W I N 3 2 | 7 | FA C I L I T Y _ S E T U PA P I | 15 |
FA C I L I T Y _ W I N D O W S | 8 | FA C I L I T Y _ S C A R D | 16 |
FA C I L I T Y _ S E C U R I T Y | 9 | FA C I L I T Y _ C O M P L U S | 17 |
我们将E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常代码拆开来,看各位( b i t )都是什么。在Wi n B a s e . h中找到E X C E P T I O N _ A C C E S S _ V I O L AT I O N,它的值为0 x C 0 0 0 0 0 0 5:
C 0 0 0 0 0 0 5 (16进制) 1100 0000 0000 0000 0000 0000 0000 0101 (2进制)
当一个异常发生时,操作系统要向引起异常的线程的栈里压入三个结构,这三个结构是:E X C E P T I O N _ R E C O R D结构、C O N T E X T结构和E X C E P T I O N _ P O I N T E R S结构。
E X C E P T I O N _ R E C O R D结构包含有关已发生异常的独立于C P U的信息,C O N T E X T结构包含已发生异常的依赖于C P U的信息。E X C E P T I O N _ P O I N T E R S结构只有两个数据成员,二者都是指针,分别指向被压入栈的E X C E P T I O N _ R E C O R D和C O N T E X T结构:
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
PEXCEPTION_POINTERS GetExceptionInformation();
关于G e t E x c e p t i o n I n f o r m a t i o n函数,要记住的最重要事情是它只能在异常过滤器中调用,因为仅仅在处理异常过滤器时, C O N T E X T、E X C E P T I O N _ R E C O R D和E X C E P T I O N _P O I N T E R S才是有效的。一旦控制被转移到异常处理程序,栈中的数据就被删除。
如果需要在你的异常处理程序块里面存取这些异常信息(虽然很少有必要这样做),必须将E X C E P T I O N _ P O I N T E R S结构所指向的C O N T E X T数据结构和/或E X C E P T I O N _ R E C O R D数据结构保存在你所建立的一个或多个变量里。下面的代码说明了如何保存E X C E P T I O N _R E C O R D和C O N T E X T数据结构:
void FuncSkunk() { //Declare variables that we can use to save the exception //record and the context if an exception should occur. EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; ... __try { ... } __except( SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord, SavedContext = *(GetExceptionInformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) { //We can use the SavedExceptRec and SavedContext //variables inside the handler code block. switch(SavedExceptRec.ExceptionCode) { ... } } ... }
在F u n c S k u n k中,左边的表达式将执行,将栈中的E X C E P T I O N _ R E C O R D结构保存在Saved ExceptRec局部变量里。这个表达式的结果是S a v e d E x c e p t R e c的值。这个结果被丢弃,再计算右边下一个表达式。第二个表达式将栈中的C O N T E X T结构保存在S a v e d C o n t e x t局部变量里。第二个表达式的结果是S a v e d C o n t e x t,同样当计算第三个表达式时丢弃第二个表达式的结果。第三个表达式很简单,只是一个数值E X C E P T I O N _ E X E C U T E _ H A N D L E R。这个最右边的表达式的结果就是整个由逗号分隔的表达式组的结果。
由于异常过滤器的值是E X C E P T I O N _ E X E C U T E _ H A N D L E R,e x c e p t块中的代码要执行。这时,已被初始过的S a v e d E x c e p t R e c和S a v e d C o n t e x t变量可以在e x c e p t块中使用。要记住,S a v e d E x c e p t R e c和S a v e d C o n t e x t变量要在t r y块之外说明,这一点很重要。
我们都可以猜到, E X C E P T I O N _ P O I N T E R S 结构的E x c e p t i o n R e c o r d 成员指向E X C E P T I O N _ R E C O R D结构:
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
• ExceptionCode包含异常的代码。这同内部函数G e t E x c e p t i o n C o d e返回的信息是一样的。
• E x c e p t i o n F l a g s包含有关异常的标志。当前只有两个值,分别是0(指出一个可以继续的异常)和E X C E P T I O N _ N O N C O N T I N U A B L E(指出一个不可继续的异常)。在一个不可继续的异常之后,若要继续执行,会引发一个E X C E P T I O N _ N O N C O N T I N U A B L E _E X C E P T I O N异常。
• E x c e p t i o n R e c o r d指向另一个未处理异常的E X C E P T I O N _ R E C O R D结构。在处理一个异常的时候,有可能引发另外一个异常。例如,异常过滤器中的代码就可能用零来除一个数。当嵌套异常发生时,可将异常记录链接起来,以提供另外的信息。如果在处理一个异常过滤器的过程当中又产生一个异常,就发生了嵌套异常。如果没有未处理异常,这个成员就包含一个N U L L。
• ExceptionAddress指出产生异常的C P U指令的地址。
• N u m b e r P a r a m e t e r s 规定了与异常相联系的参数数量( 0 到1 5 )。这是在E x c e p t i o n I n f o r m a t i o n数组中定义的元素数量。对几乎所有的异常来说,这个值都是零。
• E x c e p t i o n I n f o r m a t i o n规定一个附加参数的数组,用来描述异常。对大多数异常来说,数组元素是未定义的。
E X C E P T I O N _ R E C O R D结构的最后两个成员,N u m b e r P a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n向异常过滤器提供一些有关异常的附加信息。目前只有一种类型的异常提供附加信息,就是E X C E P T I O N _ A C C E S S _ V I O L AT I O N。所有其他可能的异常都将N u m b e r P a r a m e t e r s设置成零。我们可以检验E x c e p t i o n I n f o r m a t i o n的数组成员来查看关于所产生异常的附加信息。
对于一个E X C E P T I O N _ A C C E S S _ V I O L AT I O N异常来说,E x c e p t i o n I n f o r m a t i o n [ 0 ]包含一个标志,指出引发这个存取异常的操作的类型。如果这个值是0,表示线程试图要读不可访问的数据。如果这个值是1,表示线程要写不可访问的数据。ExceptionInformation[1] 指出不可访问数据的地址。
通过使用这些成员,我们可以构造异常过滤器,提供大量有关程序的信息。例如,可以这样编写异常过滤器:
__try { ... } __except(ExpFltr(GetExceptionInformation()->ExceptionRecord)) { ... } LONG ExpFltr(PEXCEPTION_RECORD pER) { char szBuf[300], *p; DWORD dwExceptionCode = pER->ExceptionCode; sprintf(szBuf, "Code = %x, Address = %p", dwExceptionCode, pER->ExceptionAddress); //Find the end of the string. p = strchr(szBuf, 0); // I used a switch statement in case Microsoft adds // information for other exception codes in the future. switch(dwExceptionCode) { case EXCEPTION_ACCESS_VIOLATION: sprintf(p, "Attempt to %s data at address %p", pER->ExceptionInformation[0] ? "write" : "read", pER->ExceptionInformation[1]); break; default: break; } MessageBox(NULL, szBuf, "Exception", MB_OK | MB_ICONEXCLAMATION); return(EXCEPTION_CONTINUE_SEARCH); }
本质上,对C P U上每一个可用的寄存器,这个结构相应地包含一个成员。当一个异常被引发时,可以通过检验这个结构的成员找到更多的信息。遗憾的是,为了得到这种可能的好处,要求程序员编写依赖于平台的代码,以确认程序所运行的机器,使用适当的C O N T E X T结构。最好的办法是在代码中安排一个# i f d e f s指令。Wi n d o w s支持的不同C P U的C O N T E X T结构定义在Wi n N T. h文件中。
迄今为止,我们一直在讨论硬件异常,也就是C P U捕获一个事件并引发一个异常。在代码中也可以强制引发一个异常。这也是一个函数向它的调用者报告失败的一种方法。传统上,失败的函数要返回一些特殊的值来指出失败。函数的调用者应该检查这些特殊值并采取一种替代的动作。通常,这个调用者要清除所做的事情并将它自己的失败代码返回给它的调用者。这种错误代码的逐层传递会使源程序的代码变得非常难于编写和维护。
另外一种方法是让函数在失败时引发异常。用这种方法,代码更容易编写和维护,而且也执行得更好,因为通常不需要执行那些错误测试代码。实际上,仅当发生失败时也就是发生异常时才执行错误测试代码。
但令人遗憾的是,许多开发人员不习惯于在错误处理中使用异常。这有两方面的原因。第一个原因是多数开发人员不熟悉S E H。即使有一个程序员熟悉它,但其他程序员可能不熟悉它。如果一个程序员编写了一个引发异常的函数,但其他程序员并不编写S E H框架来捕获这个异常,那么进程就会被操作系统结束。
开发人员不使用S E H的第二个原因是它不能移植到其他操作系统。许多公司的产品要面向多种操作系统,因此希望有单一的源代码作为产品的基础,这是可以理解的。S E H是专门针对Wi n d o w s的技术。
本段讨论通过异常返回错误有关的内容。首先,让我们看一看Windows Heap函数,例如H e a p C r e a t e、h e a p A l l o c等。回顾第1 8章的内容,我们知道这些函数向开发人员提供一种选择。通常当某个堆( h e a p)函数失败,它会返回N U L L来指出失败。然而可以对这些堆函数传递H E A P _ G E N E R AT E _ E X C E P T I O N S标志。如果使用这个标志并且函数失败,函数不会返回N U L L,而是由函数引发一个S TAT U S _ N O _ M E M O RY软件异常,程序代码的其他部分可以用S E H框架来捕获这个异常。
如果想利用这个异常,可以编写你的t r y块,好像内存分配总能成功。如果内存分配失败,可以利用e x c e p t块来处理这个异常,或通过匹配t r y块与f i n a l l y块,清除函数所做的事。这非常方便。
程序捕获软件异常采取的方法与捕获硬件异常完全相同。也就是说,前一章介绍的内容可以同样适用于软件异常。
本节重讨论如何让你自己的函数引发软件异常,作为指出失败的方法。实际上,可以用类似于微软实现堆函数的方法来实现你的函数:让函数的调用者传递一个标志,告诉函数如何指出失败。
引发一个软件异常很容易,只需要调用R a i s e E x c e p t i o n函数:
VOID RaiseException( DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, CONST ULONG_PTR *pArguments);
如果要建立你自己的异常代码,要填充D W O R D的4个部分:
• 第3 1位和第3 0位包含严重性系数( s e v e r i t y )。
• 第2 9位是1(0表示微软建立的异常,如H e a p A l l o c的S TAT U S _ N O _ M E M O RY)。
• 第2 8位是0。
• 第2 7位到1 6位是某个微软定义的设备代码。
• 第1 5到0位是一个任意值,用来标识引起异常的程序段。
R a i s e E x c e p t i o n 的第二个参数d w E x c e p t i o n F l a g s ,必须是0 或E X C E P T I O N _N O N C O N T I N U A B L E。本质上,这个标志是用来规定异常过滤器返回E X C E P T I O N _CONTINUE _EXECUTION来响应所引发的异常是否合法。如果没有向R a i s e E x c e p t i o n传递EXCEPTION_ NONCONTINUABLE参数值,则过滤器可以返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N。正常情况下,这将导致线程重新执行引发软件异常的同一C P U指令。但微软已做了一些动作,所以在调用R a i s e E x c e p t i o n函数之后,执行会继续进行。
如果你向R a i s e E x c e p t i o n传递了E X C E P T I O N _ N O N C O N T I N U A B L E标志,你就是在告诉系统,你引发异常的类型是不能被继续执行的。这个标志在操作系统内部被用来传达致命(不可恢复)的错误信息。另外,当H e a p A l l o c引发S TAT U S _ N O _ M E M O RY软件异常时,它使用E X C E P T I O N _ N O N C O N T I N U A B L E标志来告诉系统,这个异常不能被继续。意思就是没有办法强制分配内存并继续运行。
如果一个过滤器忽略E X C E P T I O N _ N O N C O N T I N U A B L E并返回E X C E P T I O N _ C O N T I N U E _E X E C U T I O N,系统会引发新的异常:E X C E P T I O N _ N O N C O N T I N U A B L E _ E X C E P T I O N。
当程序在处理一个异常的时候,有可能又引发另一个异常。比如说,一个无效的内存存取有可能发生在一个f i n a l l y块、一个异常过滤器、或一个异常处理程序里。当发生这种情况时,系统压栈异常。回忆一下G e t E x c e p t i o n I n f o r m a t i o n函数。这个函数返回EXCEPTION_ POINTERS结构的地址。E X C E P T I O N _ P O I N T E R S的E x c e p t i o n R e c o r d成员指向一个EXCEPTION_ R E C O R D结构,这个结构包含另一个E x c e p t i o n R e c o r d成员。这个成员是一个指向另外的E X C E P T I O N _R E C O R D的指针,而这个结构包含有关以前引发异常的信息。
通常系统一次只处理一个异常,并且E x c e p t i o n R e c o r d成员为N U L L。然而如果处理一个异常的过程中又引发另一个异常,第一个E X C E P T I O N _ R E C O R D结构包含有关最近引发异常的信息,并且这个E X C E P T I O N _ R E C O R D结构的E x c e p t i o n R e c o r d成员指向以前发生的异常的E X C E P T I O N _R E C O R D结构。如果增加的异常没有完全处理,可以继续搜索这个E X C E P T I O N _ R E C O R D结构的链表,来确定如何处理异常。
R a i s e E x c e p t i o n的第三个参数n N u m b e r O f A rg u m e n t s和第四个参数p A rg u m e n t s,用来传递有关所引发异常的附加信息。通常,不需要附加的参数,只需对p A rg u m e n t s参数传递N U L L,这种情况下, R a i s e E x c e p t i o n函数忽略n N u m b e r O f A rg u m e n t s参数。如果需要传递附加参数,n N u m b e r O f A rg u m e n t s参数必须规定由p A rg u m e n t s参数所指向的U L O N G _ P T R数组中的元素数目。这个数目不能超过E X C E P T I O N _ M A X I M U M _ PA R A M E T E R S,EXCEPTION_ MAXIMUM_PARAMETERS 在Wi n N T. h中定义成1 5。
在处理这个异常期间,可使异常过滤器参照E X C E P T I O N _ R E C O R D结构中的N u m b e rP a r a m e t e r s和E x c e p t i o n I n f o r m a t i o n成员来检查n N u m b e r O f A rg u m e n t s和p A rg u m e n t s参数中的信息。
你可能由于某种原因想在自己的程序中产生自己的软件异常。例如,你可能想向系统的事件日志发送通知消息。每当程序中的一个函数发现某种问题,你可以调用R a i s e E x c e p t i o n并让某些异常处理程序上溯调用树查看特定的异常,或者将异常写到日志里或弹出一个消息框。你还可能想建立软件异常来传达程序内部致使错误的信息。