24异常处理程序和软件异常Word文件下载.docx
- 文档编号:16402871
- 上传时间:2022-11-23
- 格式:DOCX
- 页数:30
- 大小:158.22KB
24异常处理程序和软件异常Word文件下载.docx
《24异常处理程序和软件异常Word文件下载.docx》由会员分享,可在线阅读,更多相关《24异常处理程序和软件异常Word文件下载.docx(30页珍藏版)》请在冰豆网上搜索。
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;
//2.Performsomeoperation(s).
dwTemp=5/dwTemp;
//Generatesanexception
dwTemp+=10;
//Neverexecutes
__except(/*3.Evaluatefilter.*/EXCEPTION_EXECUTE_HANDLER)
//4.Handleanexception.
MessageBeep(0);
//5.Continueprocessing.
Funcmeister2中,try块中有一个指令试图以0来除5。
CPU将捕捉这个事件,并引发一个硬件异常。
当引发了这个异常时,系统将定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在Windows的Excpt.h文件中(见表24-1)。
表24-1标识符及其定义
标识符
定义为
EXCEPTION_EXECUTE_HANDLER
1
EXCEPTION_CONTINUE_SEARCH
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)
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
//Allocateatemporarybuffer
strTemp=(char*)malloc(strlen(str)+1);
//Copytheoriginalstringtothetemporarybuffer
strcpy(strTemp,str);
//Getthefirsttoken
char*pszToken=strtok(strTemp,"
"
);
//Iteratethroughallthetokens
for(;
pszToken!
=NULL;
pszToken=strtok(NULL,"
))
nHowManyTokens++;
//Add1sincewestartedat-1
//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;
//Allocateabufferfortheduplicatememoryblock
pbDup=(PBYTE)malloc(cb);
memcpy(pbDup,pbSrc,cb);
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()
__try
//2.Callanotherfunction.
FuncORen1();
//Codehereneverexecutes.
__except(/*6.Evaluatefilter.*/EXCEPTION_EXECUTE_HANDLER)
//8.Aftertheunwind,theexceptionhandlerexecutes.
MessageBox(…);
//9.Exceptionhandled--continueexecution.
voidFuncORen1()
//3.Doanyprocessinghere.
//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()
FuncFish();
voidFuncFish()
FuncPheasant();
voidFuncPheasant()
strcpy(NULL,NULL);
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 24 异常 处理 程序 软件
![提示](https://static.bdocx.com/images/bang_tan.gif)