第23章 结束处理程序
23.0 前言
对于程序员来说,如果可以只关注于功能,不必关心这个过程过可能出现的if-else这些判断错误的代码,而被它们弄得头昏脑胀的话,那么编写程序就是一件非常愉快的事情了。
那么结构化异常处理(SEH)就是让这种场景实现的桥梁了。程序员暂时不必考虑代码中有任何问题,而把主要的精力集中到眼前的工作,再想可能发生的错误。微软也在使用这种技术,使得系统的代码更加健壮,我们的代码也可以这样做。
引入SEH所造成的负担有编译程序承担,而不是操作系统。当异常块出现的时候,编译程序会生成一些特殊的代码来完成这个任务。编译程序将会产生一些表来处理SEH的数据结构、提供一系列的回调函数。操作系统利用这些回调函数来处理异常。编译程序还得负责处理栈结构和其他内部的信息,以供操作系统处理异常的时候作参考。
由于编译程序在这里是比较重要的角色,那么提供编译器的厂家对编译器的实现过程就比较不同了。幸好程序员不必在意这一点,因为SEH的主要概念仍然是一样的。
SEH主要提供两个功能结束处理(Termination Handling)和异常处理(Exception Handling)。这一章讨论的时候结束处理,异常处理下一章继续。
23.1 通过例子理解结束处理程序
由于在使用SEH时,编译程序和操作系统直接参与了程序代码的执行,为了解释 SEH如何工作,最好的办法就是考察源代码例子,讨论例子中语句执行的次序。
因此,在下面几节给出不同的源代码片段,对每一个片段解释编译程序和操作系统如何改
变代码的执行次序。
23.2 Funcenstein1
下面的程序中加了标号的注释指出了代码执行的顺序。下面的代码中使用try-finally块并没有带来太大的好处,代码需要等待新标(semaphore),改变保护数据的内容,保存局部变量dwTemp的新值,释放新标,将新值返回给调用程序。
DWORD Funcenstein1() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
}
// 4. Continue processing.
return(dwTemp);
}
23.3Funcenstein2
现在给上面的代码中的try块的末尾增加了一个return语句,告诉编译程序在这里要退出这个函数并返回dwTemp变量的内容,现在这个变量的值是5。但是如果这个return语句被执行,该线程将不会释放新标,其他线程也将不会再获得对信标的控制。这显然会产生很大的问题:
DWORD Funcenstein1() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// return the new value.
return(dwTemp);
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
}
// 4. Continue processing-this code
// will never execute in this version.
dwTemp = 9;
return(dwTemp);
}
通过结束处理程序,我们就可以在return块被执行,试图退出try语句块的时候,finally中的代码确保被在这之前执行。在这个代码中,对释放新标的调用放在结束处理程序块中,保证信标总是被释放。这样就不会造成一个线程一直占有新标,否则将意味着所有等待新标的线程永远不会被分配CPU时间。
在finally语句块中的代码被执行后,函数才算实际的返回。任何出现在finally块之下的代码将不再执行,因为函数已在try块中返回。所以这个函数的返回值是5,而不是9。而这一点被编译器来确保执行。
可以想见,要完成这些事情,编译程序必须要生成额外的代码,系统要执行额外的工作。对于不同的CPU结束处理程序的实现方式也不尽相同。在编写这样的代码的时候,应该避免引起try块的代码过早地退出,这会影响到程序的性能。后面提到__leave关键的时候将会厘清如何实现这一点。
设计这样的异常处理的目的是用来捕捉异常的。如果一切正常,那么明确地检查这些情况,比起依赖操作系统和编译程序的SEH功能来捕捉常见的事情更有效。
注意当控制流自然地离开try块进入finally块时,系统的开销是最小的。在x86CPU上使用微软的编译程序执行这样的操作只有一个机器代码被执行。
23.4 Funcenstein3
DWORD Funcenstein1() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// Try to jump over the finally block.
goto ReturnValue;
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
}
dwTemp = 9;
// 4. Continue processing.
ReturnValue:
return(dwTemp);
}
这样修改的话就跳出try块的执行,也跳过了finally块的。此时不但返回值不会是9,而try块中的新标也不会别释放。造成的影响是非常大的。
23.5 Funcfutter1
现在来看看真正体现结束处理的价值:
DWORD Funcenstein1() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
dwTemp = Funcinator(g_dwProtectedData);
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
}
// 4. Continue processing.
return(dwTemp);
}
现在假设try块中的Funcinator函数调用时包含了一个错误,引起了一个无效内存访问。如果没有SEH,在这种情况下,家辉给用户线程一个很常见的Application Error对话框。当用户忽略这个错误对话框,该进程就结束了。此时,信标仍然被占用不被释放,那么任何等待新标的其他线程就不会被分配CPU时间。但如果此时对释放信标的函数放在finally块中,就可以保证信标获得释放,即使某些其他函数会引起内存访问错误。
如果结束处理程序设计的足够好,就能捕捉由于无效访问而结束的进程,甚至能捕捉setjump和longjump的组合,以及一些简单的语句例如break和continue。
23.6 突击测验FuncaDoodleDoo
经过上面的讨论,现在来测试一下讨论的成果:
DWORD FuncaDoodleDoo() {
DWORD dwTemp(0);
while (dwTemp < 10) {
__try {
if (dwTemp == 2)
continue;
if (dwTemp == 3)
break;
}
__finally {
dwTemp++;
}
dwTemp++;
}
dwTemp += 10;
return(dwTemp);
}
首先dwTemp被初始化为0。然后try块中的代码执行,但两个if语句的结果都不是TRUE,那么执行就移到finally中的代码了,在这里dwTemp的值递增1。然后离开finally块,dwTemp又递增了1,现在的值是2。
然后循环继续,dwTemp为2,try语句块中的第一个if为TRUE,continue将被执行。此时如果不存在try语句块,那么循环就会继续。而此时dwTemp的值是没有改变的,这就引起了一个无限循环。利用一个结束处理程序,系统知道continue语句要引起控制流过早地退出try语句块,而将执行转移到finally语句块。在这里,dwTemp被递增到3。因为控制流又返回到continue,回到循环的开头。
这一次循环开始后,第一个if语句的值是FALSE,但是第二个if语句的值是TRUE。系统又能捕捉到要跳出try块的意图,就先执行了finally的代码。现在dwTemp递增到4。由于break语句的执行,循环之后程序部分的控制恢复。这样,finally块之后的循环中的代码没有执行。循环下面的代码对dwTemp增加10,那么dwTemp的值就是14,这就是调用这个函数的结果。
尽管结束处理程序可以捕捉try块过早退出的大多数情况,但当线程或进程被结束时,它不能引起finally中的代码的执行。当调用ExitThread或者ExitProcess的时候,将立刻结束进程或者线程,而不会执行finally中的任何代码。另外如果由于某个程序调用TerminateThread或者TerminateProcess,也是一样。还有某些C运行时函数(例如abort)也会调用ExitProcess。你能做的就是阻止自己过早调用这些函数。
23.7 Funcenstein4
参考一个新的情况:
DWORD Funcenstein4() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// Return the new value.
return(dwTemp);
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
return(103);
}
// 4. Continue processing.
dwTemp = 9;
return(dwTemp);
}
在这里,与Funcenstein2不同的地方在于finally块中的代码多了一个return(103),所以函数Funcenstein4的返回值究竟是5还是103?finally块中的return语句起始值103存储的位置,正是存储临时变量5的内存,103覆盖了5。所以当函数返回的时候,103这个值就会返回给调用函数Funcenstein4的线程。
结束处理程序对于补救try块中的过早退出的代码是一把双刃剑。更好的方法是在结束处理程序的try语句块中避免出现类似return、continue、break、goto等语句从结束处理程序的try和finally块中移出,尽量减少编译程序生成的代码,提高程序的执行效率。这样也可以让代码更容易阅读和维护。
23.8 :Funcarama1
结束处理程序可以监护一个复杂的编程问题,先参阅下面的代码:
BOOL Funcarama1() {
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumBytesRead;
BOOL fOk;
hFile = CreateFile("SOMEDATA.DAT", GRNERIC_READ, FILE_SHARE_READ,
NULL, OPENT_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return(FALSE);
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
CloseHandle(hFile);
return(FASLE);
}
fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!fOk || dwNumBytesRead == 0) {
VirtualFree(pvBuf, MEM_RELEASE | MEM_COMMIT);
CloseHandle(hFile);
return(FASLE);
}
// Do some calc here.
// ...
// Clean up all the resourses.
VirtualFree(pvBuf, MEM_RELEASE | MEM_COMMIT);
CloseHandle(hFile);
return(TRUE);
}
这个函数的各种错误检查使这个函数非常难以阅读,也使这个函数难以理解、维护和修改。我们做点改变。
23.9 :Funcarama2
BOOL Funcarama1() {
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumBytesRead;
BOOL fOk, fSuccess(FALSE);
hFile = CreateFile("SOMEDATA.DAT", GRNERIC_READ, FILE_SHARE_READ,
NULL, OPENT_EXISTING, 0, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf != NULL) {
fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (fOk || dwNumBytesRead != 0) {
// Do some calc here.
fSuccess = TRUE;
}
}
VirtualFree(pvBuf, MEM_RELEASE | MEM_COMMIT);
}
CloseHandle(hFile);
return(FASLE);
}
这样修改应该容易理解些,不过还是不好修改和维护。而且当增加更多的条件语句时,这里的排版格式就会走向极端,很快就到最有边。
23.10 :Funcarama3
现在借助一个SEH结束处理程序重新编写它:
DWORD Funcarama3() {
// IMPORTANT: Initialize all you variables to assume failure。
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
__try{
DWORD dwNumBytesRead;
BOOL fOk;
hFile = CreateFile("SOMEDATA.DAT", GRNERIC_READ, FILE_SHARE_READ,
NULL, OPENT_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return(FALSE);
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
return(FASLE);
}
fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!fOk || dwNumBytesRead == 0) {
return(FASLE);
}
// Do something here.
}
__finally {
// Clean up all the resourses.
if (pvBuf != NULL) {
VirtualFree(pvBuf, MEM_RELEASE | MEM_COMMIT);
}
if (hFile != INVALI_HANDLE_VALUE) {
CloseHandle(hFile);
}
}
// Continue processing.
return(TRUE);
}
这个版本的真正好处是函数的素有清理代码都局部化在一个地方切只在一个地方:finally语句块。如果需要在这个函数中再增加些条件代码,只需要在finally中简单地增加一个清理行,不需要回到每一个可能失败的地方添加清理代码。
23.11 :Funcarama4:最终的边界
Funcarama3这个版本的问题是系统开销。就像在Funcenstein4中说到的,应该尽可能避免在try中使用return语句。
为此,微软在CC++编译器中增加了一个关键字:__leave。下面是一个使用了这个关键字的版本:
DWORD Funcarama4() {
// IMPORTANT: Initialize all you variables to assume failure。
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
// Assume that the function will not execute successfully.
BOOL fFunctionOk = FALSE;
__try{
DWORD dwNumBytesRead;
BOOL fOk;
hFile = CreateFile("SOMEDATA.DAT", GRNERIC_READ, FILE_SHARE_READ,
NULL, OPENT_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
__leave;
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
__leave;
}
fOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!fOk || dwNumBytesRead == 0) {
__leave;
}
// Do something here.
// ...
// Indicate that the entire function executed successfully.
fFunctionOk = TRUE;
}
__finally {
// Clean up all the resourses.
if (pvBuf != NULL) {
VirtualFree(pvBuf, MEM_RELEASE | MEM_COMMIT);
}
if (hFile != INVALI_HANDLE_VALUE) {
CloseHandle(hFile);
}
}
// Continue processing.
return(TRUE);
}
如之前提到一样,使用__leave关键字的开销并不足虑,而且也只是增加了一个BOOL变量,开销已经尽可能减小了。使用了这个关键字可以认为是跳转到了try语句块的末尾。
只是在进入try语句块的时候,需要将所有变量都初始化为无效值。这样才能发现那些值已经在try语句块中被修改,哪些资源需要释放。
23.12 关于finally块的说明
现在已经明确了强制进入finally语句块的情况:
- 从try语句块转过去的正常控制流;
- 局部展开:过早退出的强制转过去的控制流(goto, longjump, break, return等);
- 全局展开(global unwind),在发生的时候没有明显的标识,我们在之前的Funcfurter1中已经能看到。在Funcfurter1里面有一个对Funcinator函数的调用。如果Funcinator函数中引起了一个内存访问违规的问题,一个全局展开会使得Funcfurter1的finally语句块的执行。
以上3中情况都有可能导致finally中的代码开始执行。为了确定是哪一种情况,,可以调用内部函数AbnormalTermination函数:
BOOL AbnormalTermination();
这个内部函数只在finally语句块中调用,返回一个Boolean值。指出与finally块结合的try语句块是否过早退出。换句话说,如果控制流里靠try语句块并自然进入finally块,AbnormalTermination函数将返回FALSE。如果控制流非正常退出try块——通常由于goto、return、break或continue语句引起的局部展开,或由于内存访问违规或者其他异常引起的全局展开——对AbnormalTermination函数的调用将返回TRUE。没有办法区别finally块的之心那个是由于全局展开还是由于局部展开,这通常不会成为问题,只要避免编写执行局部展开的代码。
关于内部函数,可以看作是编译器写的内联函数。对于这种函数,编译器在看到它们的时候,不是生成对这个函数的调用,而是直接生成代码,是用空间换取时间的一种策略。
23.13 Funcfurter2
下面的函数说明了AbnormalTermination的使用方式:
DWORD Funcenstein2() {
DWORD dwTemp;
// 1.Do any processing here.
// ...
__try {
// 2. Request permission to access.
// protect data. and use it.
WaitForSingleObject(g_Sem, INFINITE);
dwTemp = Funcinator(g_dwProtectedData);
}
__finally {
// 3. Allow other to use protected data.
ReleaseSemaphore(g_Sem, 1, NULL);
if (!AbnormalTermination()) {
// No error occurred in the try block, and
// control flowed naturally from try into finally.
// ...
} else {
// Something caused an exception, and
// because there is no code in the block
// that would cause a premature exit. We must
// be executing in the finally block
// because of a global unwind.
// If there were a goto in the try block,
// we wouldn't know how we got here.
// ...
}
}
// 4. Continue processing.
return(dwTemp);
}
现在我们已经知道如何编写结束处理函数。下一章继续介绍异常过滤程序和异常处理程序更有用,更重要。
23.14 SEH结束处理示例程序
见代码清单23 SEHTerm.exe。