在分享中学习,才能让自己跑得更快。
SEH(structured exception handing),中文叫做结构化异常处理。
我们使用SEH,不是意味着可以完全无视代码之中可能出现的那一些异常,但是可以将程序的功能实现和程序的异常处理区分开来。也就是说我们先把程序的功能都实现起来,到后面再去处理程序在运行的时候可能遇到的一系列情况。
Microsoft把SEH加入Windows的原因之一就是SEH简化了操作系统的本身开发,微软使用SEH可以让操作系统更加地健壮,而我们也能使用SEH让我们的程序更加健壮。注意一点让SEH运行起来编译器的工作远远大于操作系统,进入和离开异常处理区域的时候编译器都必须要生成一系列的特殊代码与一系列支持SEH的数据结构表,还要提供回调函数给操作系统去调用,一边操作系统遍历异常区域。编译器的工作还不仅仅如此,还要准备进程的stack frame和一些内部信息,这些都是操作系统需要使用或者引用的。
让编译器支持SEH并不是一个简单任务,而不同的编译器厂商都以不同的方式去实现,但还好,大部分的编译器厂商都遵循了Microsoft建议的语法。
注意:结构化异常处理SEH和C++的异常处理是不一样的!C++异常处理就是使用关键字catch和throw。
SEH包括了两个方面的功能:终止处理(termination handing)和异常处理(exception handing)。
1.终止处理
终止处理确保不管the guarded body是怎么样子退出的,另外一个终止处理程序都能够执行。
如下
_try
{
//the guarded body
//也就是被保护的代码
}
_finally
{
//Termination handing
//也就是终止处理程序
}
首先介绍_try和_finalll是关键字,他们的作用是标记了终止处理程序的两个部分:the guarded body 和Termination handing。在这段代码中,操作系统和编译器确保了不管the guarded body代码是如何退出的,不管是使用了return还是使用了goto等等语句,Termination handing代码都会被调用。(但是如果使用的是exirprocess,exitthread,terminateprocess,terminatethread函数来终止线程或者进程的话,termination handing代码就得不到执行。)
接下来我们看代码来理解SEH终止处理程序。
1.1
DWORD FUNCTION1()
{
//code ①
_try
{
//code ②
}
_finally
{
//code ③
}
return ④
}
代码中的序号就是代码的执行顺序。这个函数顺序没啥说的。
1.2
DWORD FUNCTION2()
{
//code
DWORD I=0;
_try
{
//code
I=5;
return I;
}
_finally
{
//code
}
I=6;
return I;
}
哈哈哈,这段代码有点意思呀,try区域的结尾有一个return呀,应该是返回5还是返回6呢?
通过终止处理程序可以防止过早执行return语句,当return语句试图退出try区域的时候,编译器会让finally代码在return之前执行。finally代码执行完成之后,函数就可以执行return语句返回了,所以因为try区域内包含了一个return语句,所以finally区域后的代码都不会被执行了,所以这个函数返回的是5,而不是6。
编译器如何保证finally区域可以在try区域退出前被执行?OK,当编译器检查代码时候,会发现try区域里面居然有一个return语句,哎呀所以编译器就得做点事情啦,于是编译器就会生成一些代码先把返回值(例子中的5)保存在一个由它创建的临时变量里面,然后再执行finally区域的代码。这个过程就叫做局部展开(local unwind)。就是说系统因为try区域的代码提前退出而执行finally区域代码时就会发生局部展开。而一旦finally区域代码执行完毕,编译器就会把临时变量中的值取出来再当做函数返回值。
所以开头的时候说为了SEH的运行编译器做的工作远远大于操作系统。当然在不同CPU体系结构上终止处理工作的执行步骤也不同。但是应该注意要避免在try区域里面使用return语句,因为这对程序的性能是有影响的!不用return用什么呢?OK,用_leave关键字(下面介绍)。
应该注意,SEH是用来捕获那一些程序异常的!如果是常见的问题,我们应该改变代码来解决问题而不是依赖于操作系统和编译器的SEH来捕捉这些问题。
如果代码控制流正常地离开try区域(就像FUNCTION1函数那样子)而进入finally区域,那么对程序的性能影响是最低的,因为编译器只是多加了一条机器指令。
好的我们继续。
1.3
DWORD FUNCTION3()
{
DWORD I=0;
_try
{
I=5;
goto AreaA;
}
_finally
{
}
I=6;
AreaA:
return I;
}
在这里,编译器看到try区域中的goto语句的时候就会发生局部展开而执行finally区域代码块,第一次因为try区域和finally区域都没有函数返回语句,所以就跳到AreaA标签去执行下去,而跳过了I=6这一条代码,所以函数最终返回的是5;因为goto破坏了try区域到finally区域的正常流程,所以也可能有较大的性能影响,影响程度取决于CPU的体系架构。
1.4
DWORD GetSystemIndex()
{ DWORD I=0;
.......
//运算出错而导致程序访问非法内存
return I;
}
DWORD FUNCTION4()
{
DWORD I=0;
_try
{
I= GetSystemIndex();
}
_finally
{
}
return I;
}
这个例子才能看到终止处理程序的价值。
GetSystemIndex函数错误而导致程序访问非法内存,如果没有SEH的存在,那么一般情况下最终会导致Windows错误报告弹出一个对话框说:Application has stopped working。我们一旦取消这个对话框程序也就ko了。正是因为有了SEH的存在,发生访问非法内存的时候,finally区域的代码得以执行而我们也可以做一些最后该做的事情,比如释放信号量等内核对象。
1.5
DWORD FUNCTION5()
{
DWORD I=0;
while(I<10)
{
_try
{
if(I==2) continue;
if(I==3) break;
}
_finally
{
I++;
}
I++;
}
I+=10;
return I;
}
这个函数将返回多少呢?答案是I=14;
当if(I==2)为true的时候,continue会导致finally区域执行所以I++后I变成了3,而后再次循环。这一次if(I==3)为true了,所以执行break语句,再次进入finally区域,执行finally区域完成后没有return语句,所以继续执行finally区域后面的代码。
1.6
DWIRD FUNCTION6()
{
HANDLE hFile=INVALID_HANDLE_VALUD;
PVOID pBuf=NULL;
DWORD dwByte=0;
BOOK bOk=FALSE;
hFile=CreateFile(TEXT("A.txt"),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,NULL,NULL);
if(hFile==INVALID_HANDLE_VALUD)
return bOk;
pBuf=VirtualAlloc(NULL,1024,MEM_COMMIT,PAGE_READWRITE);
if(pBuf==NULL)
{
CloseHandle(hFile);
return bOk;
}
bOk=ReadFile(hFile,pBuf,1024,&dwByte,NULL)
if(!bOk||(dwByte==0))
{
CloseHandle(hFile); VirtuaFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
return bOk;
}
bOk=true;
VirtuaFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
CloseHandle(hFile);
return bOk;
}
这个函数的错误代码检查是不是让你失望?这个函数功能还没有全部完成,如果在多加那么几个函数,一眼看过去真的乱如麻呀!
但是接下来我们用SEH来实现一下。
DWORD FUNCTION7()
{
HANDLE hFile=INVALID_HANDLE_VALUE;
PVOID pBuf=NULL;
BOOL bFunctionOk=FALSE;
_try
{
DWORD dwByte=0;
BOOL bOk=FALSE;
hFile=CreateFile(TEXT("A.BAT"),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,NULL,NULL);
if(hFile==INVALID_HANDLE_VALUE)
_leave;
pBuf=VirtualAlloc(NULL,1024,MEM_COMMIT,PAGE_READWRITE);
if(pBuf==NULL)
_leave;
bOk=ReadFile(hFile,pBuf,1024,&dwByte,NULL);
if(!bOk||(dwByte==0))
_leave;
//其它代码......
bFunctionOk=true;
}
_finally
{
if(pBuf!=NULL) VirtualFree(pBuf,MEM_RELEASE|MEM_DECOMMIT);
if(hFile!INVALID_HANDLE_VALUE)
CloseHandle(hFile);
}
return bFunctionOk;
}
怎么样?是不是比上面那个函数好多了?关键字_leave会导致代码执行到try区域的结尾而正常地进入finally区域,所以不会产生大开销。但是我们需要定义一个bool变量来说明函数运行结果成功还是失败。最后在finally区域里面检查那一些变量需要释放。
但是finally区域需要注意一些事项:
两种情况下执行finally区域,第一种情况下就是从try区域正常进入finally区域,而第二种就是产生局部展开,就是从try区域提前退出(由于goto,continue,break,return的语句引起)将程序控制流强制进入finally区域。
但还有一种情形下会发生,就是全局展开(以后介绍SEH的异常处理程序会讲)。
finally区域执行是由于上面三种情况之一引起的,但是要确定是哪一种情况下引起的,我们需要调用内在函数AbnormalTermination();
函数原型 BOOL AbnormalTermination();
什么叫内在函数呢?内在函数就是由编译器识别并处理的特殊函数,编译器会为内联函数生成内联代码,而不是生成代码来调用内在函数,就如memcpy就是内在函数。
我们只能在finally区域里面调用这个内在函数,内在函数会返回一个bool变量来表明一个与当前finally区域相关的try区域是否提前退出。就是说从try区域正常进入finally,内在函数就会返回false。如果控制流从try区域异常退出(如goto,break等引起局部展开,或者代码抛出异常而引起全局展开),那么内在函数的返回值就是true。但是如何进一步区分是全局展开还是局部展开呢?简单呀,不要在try区域里面用break,goto,continue,return而是使用leave,那么内在函数返回值为true,那就是全局展开了呀!局部展开是我们可以控制的呀。
最后我们总结一下SEH的终止处理程序的使用理由:
因为清理工作都在finally区域执行,而且保证得到执行,从而简化了错误处理(比如访问到了非法内存而释放内核对象)和清理内存。
提高了代码可读性。
让代码更容易维护。
正常使用下,对程序的性能和体积影响很小。