24异常处理程序和软件异常.docx
- 文档编号:3497475
- 上传时间:2022-11-23
- 格式:DOCX
- 页数:30
- 大小:158.22KB
24异常处理程序和软件异常.docx
《24异常处理程序和软件异常.docx》由会员分享,可在线阅读,更多相关《24异常处理程序和软件异常.docx(30页珍藏版)》请在冰豆网上搜索。
24异常处理程序和软件异常
第24章异常处理程序和软件异常
异常是我们不希望有的事件。
在编写程序的时候,程序员不会想去存取一个无效的内存地址或用0来除一个数值。
不过,这样的错误还是常常会发生的。
CPU负责捕捉无效内存访问和用0除一个数值这种错误,并相应引发一个异常作为对这些错误的反应。
CPU引发的异常,就是所谓的硬件异常(hardwareexception)。
在本章的后面,我们还会看到操作系统和应用程序也可以引发相应的异常,称为软件异常(softwareexception)
当出现一个硬件或软件异常时,操作系统向应用程序提供机会来考察是什么类型的异常被引发,并能够让应用程序自己来处理异常。
下面就是异常处理程序的文法:
__try
{
//Guardedbody
...
}
__except(exceptionfilter)
{
//Exceptionhandler
...
}
注意__except关键字。
每当你建立一个try块,它必须跟随一个finally块或一个except块。
一个try块之后不能既有finally块又有except块。
但可以在try-except块中嵌套try-finally块,反过来也可以。
24.1通过例子理解异常过滤器和异常处理程序
与结束处理程序(前一章讨论过)不同,异常过滤器(exceptionfilter)和异常处理程序是通过操作系统直接执行的,编译程序在计算异常过滤器表达式和执行异常处理程序方面不做什么事。
下面几节的内容举例说明try-except块的正常执行,解释操作系统如何以及为什么计算异常过滤器,并给出操作系统执行异常处理程序中代码的环境。
24.1.1Funcmeister1
这里是一个try-exception块的更具体的例子。
DWORDFuncmeister1()
{
DWORDdwTemp;
//1.Doanyprocessinghere.
...
__try
{
//2.Performsomeoperation.
dwTemp=0;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//Handleanexception;thisneverexecutes.
...
}
//3.Continueprocessing.
return(dwTemp);
}
在Funcmeister1的try块中,只是把一个0赋给dwTemp变量。
这个操作决不会造成异常的引发,所以except块中的代码永远不会执行。
注意这与try-finally行为的不同。
在dwTemp被设置成0之后,下一个要执行的指令是return语句。
尽管在结束处理程序的try块中使用return、goto、continue和break语句遭到强烈地反对,但在异常处理程序的try块中使用这些语句不会产生速度和代码规模方面的不良影响。
这样的语句出现在与except块相结合的try块中不会引起局部展开的系统开销。
24.1.2Funcmeister2
让我们修改这个函数,看会发生什么事情:
DWORDFuncmeister2()
{
DWORDdwTemp=0;
//1.Doanyprocessinghere.
...
__try
{
//2.Performsomeoperation(s).
dwTemp=5/dwTemp;//Generatesanexception
dwTemp+=10;//Neverexecutes
}
__except(/*3.Evaluatefilter.*/EXCEPTION_EXECUTE_HANDLER)
{
//4.Handleanexception.
MessageBeep(0);
...
}
//5.Continueprocessing.
return(dwTemp);
}
Funcmeister2中,try块中有一个指令试图以0来除5。
CPU将捕捉这个事件,并引发一个硬件异常。
当引发了这个异常时,系统将定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在Windows的Excpt.h文件中(见表24-1)。
表24-1标识符及其定义
标识符
定义为
EXCEPTION_EXECUTE_HANDLER
1
EXCEPTION_CONTINUE_SEARCH
0
EXCEPTION_CONTINUE_EXECUTION
-1
下面几节将讨论这些标识符如何改变线程的执行。
在阅读这些内容时可参阅图24-1,该图概括了系统如何处理一个异常的情况。
图24-1系统如何处理一个异常
24.2EXCEPTION_EXECUTE_HANDLER
在Funcmeister2中,异常过滤器表达式的值是EXCEPTION_EXECUTE_HANDLER。
这个值的意思是要告诉系统:
“我认出了这个异常。
即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。
”在这个时候,系统执行一个全局展开(本章后面将讨论),然后执行向except块中代码(异常处理程序代码)的跳转。
在except块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。
这种机制使Windows应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。
但是,当except块执行后,代码将从何处恢复执行?
稍加思索,我们就可以想到几种可能性。
第一种可能性是从产生异常的CPU指令之后恢复执行。
在Funcmeister2中执行将从对dwTemp加10的指令开始恢复。
这看起来像是合理的做法,但实际上,很多程序的编写方式使得当前面的指令出错时,后续的指令不能够继续成功地执行。
在Funcmeister2中,代码可以继续正常执行,但是,Funcmeister2已不是正常的情况。
代码应该尽可能地结构化,这样,在产生异常的指令之后的CPU指令有望获得有效的返回值。
例如,可能有一个指令分配内存,后面一系列指令要执行对该内存的操作。
如果内存不能够被分配,则所有后续的指令都将失败,上面这个程序重复地产生异常。
这里是另外一个例子,说明为什么在一个失败的CPU指令之后,执行不能够继续。
我们用下面的程序行来替代Funcmeister2中产生异常的C语句:
malloc(5/dwTemp);
对上面的程序行,编译程序产生CPU指令来执行除法,将结果压入栈中,并调用malloc函数。
如果除法失败,代码就不能继续执行。
系统必须向栈中压东西,否则,栈就被破坏了。
所幸的是,微软没有让系统从产生异常的指令之后恢复指令的执行。
这种决策使我们免于面对上面的问题。
第二种可能性是从产生异常的指令恢复执行。
这是很有意思的可能性。
如果在except块中有这样的语句会怎么样呢:
dwTemp=2;
在except块中有了这个赋值语句,可以从产生异常的指令恢复执行。
这一次,将用2来除5,执行将继续,不会产生其他的异常。
可以做些修改,让系统重新执行产生异常的指令。
你会发现这种方法将导致某些微妙的行为。
我们将在“EXCEPTION_CONTINUE_EXECUTION”一节中讨论这种技术。
第三种可能性是从except块之后的第一条指令开始恢复执行。
这实际是当异常过滤器表达式的值为EXCEPTION_EXECUTE_HANDLER时所发生的事。
在except块中的代码结束执行后,控制从except块之后的第一条指令恢复。
24.2.1一些有用的例子
假如要实现一个完全强壮的应用程序,该程序需要每周7天,每天24小时运行。
在今天的世界里,软件变得这么复杂,有那么多的变量和因子来影响程序的性能,笔者认为如果不用SEH,要实现完全强壮的应用程序简直是不可能的。
我们先来看一个样板程序,即C的运行时函数strcpy:
char*strcpy(
char*strDestination,
constchar*strSource);
这是一个相当简单的函数,它怎么会引起一个进程结束呢?
如果调用者对这些参数中的某一个传递NULL(或任何无效的地址),strcpy就引起一个存取异常,并且导致整个进程结束。
使用SEH,就可以建立一个完全强壮的strcpy函数:
char*RobustStrCpy(char*strDestination,constchar*strSource)
{
__try
{
strcpy(strDestination,strSource);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//Nothingtodohere
}
return(strDestination);
}
这个函数所做的一切就是将对strcpy的调用置于一个结构化的异常处理框架中。
如果strcpy执行成功,函数就返回。
如果strcpy引起一个存取异常,异常过滤器返回EXCEPTION_EXECUTE_HANDLER,导致该线程执行异常处理程序代码。
在这个函数中,处理程序代码什么也不做,RobustStrCpy只是返回到它的调用者,根本不会造成进程结束。
我们再看另外一个例子。
这个函数返回一个字符串里的以空格分界的符号个数:
intRobustHowManyToken(constchar*str)
{
intnHowManyTokens=-1;//-1indicatesfailure
char*strTemp=NULL;//Assumefailure
__try
{
//Allocateatemporarybuffer
strTemp=(char*)malloc(strlen(str)+1);
//Copytheoriginalstringtothetemporarybuffer
strcpy(strTemp,str);
//Getthefirsttoken
char*pszToken=strtok(strTemp,"");
//Iteratethroughallthetokens
for(;pszToken!
=NULL;pszToken=strtok(NULL,""))
nHowManyTokens++;
nHowManyTokens++;//Add1sincewestartedat-1
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//Nothingtodohere
}
//Freethetemporarybuffer(guaranteed)
free(strTemp);
return(nHowManyTokens);
}
这个函数分配一个临时缓冲区并将一个字符串复制到里面。
然后该函数用C运行时函数strtok来获取字符串的符号。
临时缓冲区是必要的,因strtok要修改它所操作的串。
感谢有了SEH,这个非常简单的函数就处理了所有的可能性。
我们来看在几个不同的情况下函数是如何执行的。
首先,如果调用者向函数传递了NULL(或任何无效的内存地址),nHowManyTokens被初始化成-1。
在try块中对strlen的调用会引起存取异常。
异常过滤器获得控制并将控制转移给except块,except块什么也不做。
在except块之后,调用free来释放临时内存块。
但是,这个内存从未分配,所以结束调用free,向它传递NULL作为参数。
ANSIC明确说明用NULL作为参数调用free是合法的。
这时free什么也不做,这并不是错误。
最后,函数返回-1,指出失败。
注意进程并没有结束。
其次,调用者可能向函数传递了一个有效的地址,但对malloc的调用(在try块中)可能失败并返回NULL。
这将导致对strcpy的调用引起一个存取异常。
同样,异常过滤器被调用,except块执行(什么也不做),free被调用,传递给它NULL(什么也不做),返回-1,告诉调用程序该函数失败。
注意进程也没有结束。
最后,假定调用者向函数传递了一个有效的地址,并且对malloc的调用也成功了。
这种情况下,其余的代码也会成功地在nHowManyTokens变量中计算符号的数量。
在try块的结尾,异常过滤器不会被求值,except块中代码不会被执行,临时内存缓冲区将被释放,并向调用者返回nHowManyTokens。
使用SEH会感觉很好。
RobustHowManyToken函数说明了如何在不使用try-finally的情况下保证释放资源。
在异常处理程序之后的代码也都能保证被执行(假定函数没有从try块中返回—应避免的事情)。
我们再看一个特别有用的SEH例子。
这里的函数重复一个内存块:
PBYTERobustMemDup(PBYTEpbSrc,size_tcb)
{
PBYTEpbDup=NULL;//Assumefailure
__try
{
//Allocateabufferfortheduplicatememoryblock
pbDup=(PBYTE)malloc(cb);
memcpy(pbDup,pbSrc,cb);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
free(pbDup);
pbDup=NULL;
}
return(pbDup);
}
这个函数分配一个内存缓冲区,并从源块向目的块复制字节。
然后函数将复制的内存缓冲区的地址返回给调用程序(如果函数失败则返回NULL)。
希望调用程序在不需要缓冲区时释放它。
这是在except块中实际有代码的第一个例子。
我们看一看这个函数在不同条件下是如何执行的。
•如果调用程序对pbSrc参数传递了一个无效地址,或者如果对malloc的调用失败(返回NULL),memcpy将引起一个存取异常。
该存取异常执行过滤器,将控制转移到except块。
在except块内,内存缓冲区被释放,pbDup被设置成NULL以便调用程序能够知道函数失败。
这里,注意ANSIC允许对free传递NULL。
•如果调用程序给函数传递一个有效地址,并且如果对malloc的调用成功,则新分配内存块的地址返回给调用程序。
24.2.2全局展开
当一个异常过滤器的值为EXCEPTION_EXECUTE_HANDLER时,系统必须执行一个全局展开(globalunwind)。
这个全局展开使所有那些在处理异常的try_except块之后开始执行但未完成的try-finally块恢复执行。
图24-2是描述系统如何执行全局展开的流程图,在解释后面的例子时,请参阅这个图。
voidFuncOStimpy1()
{
//1.Doanyprocessinghere.
...
__try
{
//2.Callanotherfunction.
FuncORen1();
//Codehereneverexecutes.
}
__except(/*6.Evaluatefilter.*/EXCEPTION_EXECUTE_HANDLER)
{
//8.Aftertheunwind,theexceptionhandlerexecutes.
MessageBox(…);
}
//9.Exceptionhandled--continueexecution.
}
voidFuncORen1()
{
DWORDdwTemp=0;
//3.Doanyprocessinghere.
__try
{
//4.Requestpermissiontoaccessprotecteddata.
WaitForSingleObject(g_hSem,INFINITE);
//5.Modifythedata.
//Anexceptionisgeneratedhere.
g_dwProtectedData=5/dwTemp;
}
__finally
{
//7.Globalunwindoccursbecausefilterevaluated
//toEXCEPTION_EXECUTE_HANDLER.
//Allowotherstouseprotecteddata.
ReleaseSemaphore(g_hSem,1,NULL);
}
//Continueprocessing--neverexecutes.
...
}
函数FuncOStimpy1和FuncOren1结合起来可以解释SEH最令人疑惑的方面。
程序中注释的标号给出了执行的次序,我们现在开始做一些分析。
FuncOStimpy1开始执行,进入它的try块并调用FuncORen1。
FuncORen1开始执行,进入它的try块并等待获得信标。
当它得到信标,FuncORen1试图改变全局数据变量g_dwProtectedData。
但由于除以0而产生一个异常。
系统因此取得控制,开始搜索一个与except块相配的try块。
因为FuncORen1中的try与同一个finally块相配,所以系统再上溯寻找另外的try块。
这里,系统在FuncOStimpy1中找到一个try块,并且发现这个try块与一个except块相配。
系统现在计算与FuncOStimpy1中except块相联的异常过滤器的值,并等待返回值。
当系统看到返回值是EXCEPTION_EXECUTE_HANDLER的,系统就在FuncORen1的finally块中开始一个全局展开。
注意这个展开是在系统执行FuncOStimpy1的except块中的代码之前发生的。
对于一个全局展开,系统回到所有未完成的try块的结尾,查找与finally块相配的try块。
在这里,系统发现的finally块是FuncORen1中所包含的finally块。
当系统执行FuncORen1的finally块中的代码时,就可以清楚地看到SEH的作用了。
FuncORen1释放信标,使另一个线程恢复执行。
如果这个finally块中不包含ReleaseSemaphore的调用,则信标不会被释放。
在finally块中包含的代码执行完之后,系统继续上溯,查找需要执行的未完成finally块。
在这个例子中已经没有这样的finally块了。
系统到达要处理异常的try-except块就停止上溯。
这时,全局展开结束,系统可以执行except块中所包含的代码。
结构化异常处理就是这样工作的。
SEH比较难于理解,是因为在代码的执行当中与系统牵扯太多。
程序代码不再是从头到尾执行,系统使代码段按照它的规定次序执行。
这种执行次序虽然复杂,但可以预料。
按图24-1和图24-2的流程图去做,就可以有把握地使用SEH。
图24-2系统如何执行一个全局展开
为了更好地理解这个执行次序,我们再从不同的角度来看发生的事情。
当一个过滤器返回EXCEPTION_EXECUTE_HANDLER时,过滤器是在告诉系统,线程的指令指针应该指向except块中的代码。
但这个指令指针在FuncORen1的try块里。
回忆一下第23章,每当一个线程要从一个try-finally块离开时,必须保证执行finally块中的代码。
在发生异常时,全局展开就是保证这条规则的机制。
24.2.3暂停全局展开
通过在finally块里放入一个return语句,可以阻止系统去完成一个全局展开。
请看下面的代码:
voidFuncMonkey()
{
__try
{
FuncFish();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
MessageBeep(0);
}
MessageBox(…);
}
voidFuncFish()
{
FuncPheasant();
MessageBox(…);
}
voidFuncPheasant()
{
__try
{
strcpy(NULL,NULL);
}
__finally
{
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 24 异常 处理 程序 软件