《Windows核心编程》读书笔记二十三章 终止处理程序
第二十三章 终止处理程序
本章内容
通过实例理解终止处理程序
VC++支持SEH(Structured exception handling)来使得开发代码过程能够把代码和错误处理的逻辑分离开来。
SEH是编译器在编译代码过程中加入了特殊的处理代码,生成SEH的数据结构和操作系统相关的代码。参考深入解析Win32结构化异常
SEH实际上包含两方面的功能:终止处理(termination handling)和异常处理(exception handling)
终止处理确保不管一个代码块(被保护的代码块)是如何退出的,另一个代码块(终止处理程序)总能被调用和执行。终止处理的语法如下:
__try{
// Guard body
// ...
}
__finally{
// Termination handler
// ...
}
编译器和操作系统协同工作保障了无论被保护的代码是如何退出的(return, goto, longjump, 除非调用了ExitProcess , ExitThread, TerminateProcess, TerminateThread来终止进程或线程) ,终止处理程序都会表调用(__finally语句块中的代码)
通过实例理解终止处理程序
1. Funcenstein1函数
DWORD Funcenstein1() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
}
// 4. continue processing
return dwTemp;
}
2. Funcenstein2函数
DWORD Funcenstein2() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// return the new value.
return dwTemp;
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
}
// 4. continue processing
// will never execute in this version
dwTemp = 9;
return dwTemp;
}
通过__finally保证了信号量被即使释放。
在__try语句块中返回return的时候,由于存在__finally语句块,会强制执行__finally块中的代码然后再return
编译器在编译__try语句块中发现存在一个return语句(类似ret指令),编译器会先将返回值保存在一个临时变量中(通常是push xxx)。然后再调转到finally代码块(这个过程称为局部展开)一旦finally代码块执行完毕,编译器所创建的临时变量的值就会返回给函数调用者。
可以看到ret语句被编译成了一系列语句,最后通过一系列跳转到了_finally语句块内并执行其中的代码。
同时__finally语句块执行以后最后有一条ret指令最终会返回所有调用然后让函数返回。
注意在try语句块中使用return语句会生成大量了代码,多次跳转才能回到finally语句块,这会损失性能。应该改用__leave
另外异常处理通常用来捕获哪些本来不应该发生的异常。如果是常见问题,应该显示地检查这些问题以提供运行效率(例如空指针访问,除0错误等)
通常让__try语句正常执行以后进入__finally这样的开销是最低的
3. Funcenstein3
DWORD Funcenstein3() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// return the new value.
goto ReturnValue;
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
}
// 4. continue processing
// will never execute in this version
ReturnValue:
dwTemp = 9;
return dwTemp;
}
goto 语句试图直接跳出到return语句。编译器会检查出并进行局部展开保证执行了__finally块以后才会跳转到ReturnValue处。
4. Funcfurter1
DWORD Funcfurter1() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
}
// 4. continue processing
// will never execute in this version
return dwTemp;
}
假设try代码块会有语句导致访问非法内存,如果没有SEH,会被Windows错误报告程序截获,弹出一个对话框然后终止应用程序。
但finally语句也不是总是能保证被执行。例如栈被破坏了,finally语句块所在的内存可能已经被覆盖了。这种情况下应用程序得不到任何提示被强制终止。
或者有异常导致SEH链的中断,__finally处理块也不一定能得到执行。
如果异常发生在异常过滤程序里,终止处理也不会被执行。
尽量限制在catch或者finally块中代码所做的工作,否则进程可能会在finally块执行前突然终止。
5. 突击测验: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返回4.
每个循环代码都会执行一次__finally块。无论continue, break; (甚至longjump 和setjump也能被SEH展开)
只有ExitProcess,ExitThread, TerminateProcess, TerminateThread可以强制跳过finally块。(应该尽量避免调用这些函数)
6. Funcenstein4 函数
DWORD Funcenstein4() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
g_dwProtectedData = 5;
dwTemp = g_dwProtectedData;
// Return the new value.
return dwTemp;
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
return 103;
}
// 4. continue processing
// will never execute in this version
return dwTemp;
}
以上代码最后将返回103,因为Finally语句块中的103会替换默认的返回值。
一条好的法则,不要在try块中存放return语句。
7. Funcarama1 ~10. Funcarama4
包含错误处理程序的代码如何使用try finally改进使代码易读
DWORD Funcarama1() {
// IMPORTANT: Initialize all variables to assume failure.
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumBytesRead;
BOOL bOk;
hFile = CreateFile(TEXT("SOMEDATA.DAT"), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return FALSE;
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
CloseHandle(hFile);
return FALSE;
}
bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!bOk || (dwNumBytesRead != 1024)) {
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
CloseHandle(hFile);
return FALSE;
}
// Do some calculation on the data.
// ...
// Clean up all the resources.
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
CloseHandle(hFile);
return TRUE;
}
这种代码非常不好,如果代码需要操作的句柄数量多,后面需要增加大量的释放语句,非常容易出错。而且可读性也很差。
DWORD Funcarama2() {
// IMPORTANT: Initialize all variables to assume failure.
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
DWORD dwNumBytesRead;
BOOL bOk, bSuccess = FALSE;
hFile = CreateFile(TEXT("SOMEDATA.DAT"), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile != INVALID_HANDLE_VALUE) {
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf != NULL) {
bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (bOk && (dwNumBytesRead != 1024)) {
// Do some calculation on the data.
// ...
bSuccess = TRUE;
}
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
}
CloseHandle(hFile);
}
return bSuccess;
}
这种代码比第一种略微简洁一些,但是会产生大量的代码缩进。也难以维护。
DWORD Funcarama3() {
// IMPORTANT: Initialize all variables to assume failure.
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
__try{
DWORD dwNumBytesRead;
BOOL bOk;
hFile = CreateFile(TEXT("SOMEDATA.DAT"), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return FALSE;
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
return FALSE;
}
bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!bOk || (dwNumBytesRead != 1024)) {
return FALSE;
}
// Do some calculation on the data.
// ...
}
__finally{
// Clean up all the resources.
if (pvBuf != NULL)
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
}
return TRUE;
}
Funcarama3的精髓在于所有清理工作都放在finally快。以后添加新的代码,可以只在finally块添加相应的清理代码。
由于在try语句块使用了return 局部展开,因此性能可能会有所损失。
最终改进版
DWORD Funcarama4() {
// IMPORTANT: Initialize all variables to assume failure.
HANDLE hFile = INVALID_HANDLE_VALUE;
PVOID pvBuf = NULL;
// Assume that the function will not execute successfully.
BOOL bFunctionOk = FALSE;
__try{
DWORD dwNumBytesRead;
BOOL bOk;
hFile = CreateFile(TEXT("SOMEDATA.DAT"), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
__leave;
}
pvBuf = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
if (pvBuf == NULL) {
__leave;
}
bOk = ReadFile(hFile, pvBuf, 1024, &dwNumBytesRead, NULL);
if (!bOk || (dwNumBytesRead != 1024)) {
__leave;
}
// Do some calculation on the data.
// ...
// Indicate that the entire function executed successfully.
bFunctionOk = TRUE;
}
__finally{
// Clean up all the resources.
if (pvBuf != NULL)
VirtualFree(pvBuf, MEM_RELEASE | MEM_DECOMMIT);
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
}
return bFunctionOk;
}
将return改成__leave 减少性能开销。
不过这种方式在进入try语句块之前需要将所有句柄都初始化为无效值。
11. Finally快注意事项:
1. try块到finally的正常代码控制流
2. 局部展开:从try块中提前退出(goto, longjump, continue, break, return)将程序控制流强制转入finally块
第三种--全局展开。如果触发了异常
可以在finally快中使用内在函数AbnormalTermination来确定是哪种情况进入finally块
int __cdecl _abnormal_termination(void);
如果代码从正常流入finally块,该函数返回FALSE
否则返回TRUE
12. Funcfurter2函数
演示如何使用AbnormalTermination
DWORD Funcfurther2() {
DWORD dwTemp;
// 1. Do any processing here.
__try{
// 2. Request permission to access
// protected data, and then use it.
WaitForSingleObject(g_hSem, INFINITE);
dwTemp = Funcinator(g_dwProtectedData);
}
__finally{
// 3. Allow other to use protected data.
ReleaseSemaphore(g_hSem, 1, NULL);
if (!AbnormalTermination()) {
// No errors 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 try 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 there.
}
}
// 4. continue processing
return dwTemp;
}
总结使用终止处理程序的理由:
1)因为清理工作集中在一个地方执行,并保证能得到执行,从而简化了错误处理。
2)提高代码可读性
3)让代码更容易维护
4)如果正确使用,它们对程序性能和体积影响是微小的。
13. SEH终止处理示例程序
演示了如何处理异常的流程。
在vista以前的操作系统中。若try块异常退出,finally块还是有机会被执行的。finally块因为全局展开而被执行(触发异常)一个线程入口点(entry point)被try/except块所保护。为了触发全局展开__except块的异常过滤程序应该返回EXCEPTION_EXECUTE_HANDLER
在Vista以后,为了提高错误记录和报告的可靠性,从架构上对未处理异常的处理过程做了一个重大的改动(参考25章)。一个明显的缺点是用来作为保护的异常过滤程序返回的是EXCEPTION_CONTINUE_SEARCH , 进程将马上终止,从而导致finally块没有机会执行。
SEHTerm.exe会检查系统是不是Vista。将弹出一个消息框让我们选择是否使用try/except来保护错误函数。
try/finally将会被一个异常过滤程序所保护起来,该过滤返回EXCEPTION_EXECUTE_HANDLER.其作用是保证当异常抛出时,全局展开会被触发,让finally块代码可以执行。
笔者将代码稍作修改支持Vista以上的版本(Win7)
/******************************************************************************
Module: SEHTerm.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */
#include <windows.h>
#include <tchar.h>
///////////////////////////////////////////////////////////////////////////////
BOOL IsWindowsVista() {
// Code from Chapter 4
// Prepare the OSVERSIONINFOEX structure to indicate Windows Vista.
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMajorVersion = 6;
osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;
// Prepare the condition mask.
DWORDLONG dwConditionMask = 0; // Your MUST initialize this to 0.
VER_SET_CONDITION(dwConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwConditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
VER_SET_CONDITION(dwConditionMask, VER_PLATFORMID, VER_EQUAL);
// Perform the version test.
if (VerifyVersionInfo(&osver, VER_MAJORVERSION | VER_MINORVERSION |
VER_PLATFORMID, dwConditionMask)) {
// The host system is Windows Vista or newer.
return TRUE;
}
else {
// The host system is NOT Windows Vista.
return FALSE;
}
}
void TriggerException() {
__try{
int n = MessageBox(NULL, TEXT("Perform invalid memory access?"),
TEXT("SEHTerm: In try block"), MB_YESNO);
if (n == IDYES) {
*(PBYTE)NULL = 5; // This causes an access violation
}
}
__finally {
PCTSTR psz = AbnormalTermination()
? TEXT("Abnormal termination") : TEXT("Normal termination");
MessageBox(NULL, psz, TEXT("SEHTerm: In finally block"), MB_OK);
}
MessageBox(NULL, TEXT("Normal function termination"),
TEXT("SEHTerm: After finally block"), MB_OK);
}
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, PTSTR, int) {
// In Windows Vista, a global unwind occurs if an except filter
// returns EXCEPTION_EXECUTE_HANDLER. If an unhandled exception
// occurs, the process is simply terminated and the finally blocks
// are not executed.
if (IsWindowsVista()) {
DWORD n = MessageBox(NULL, TEXT("Protect with try/except?"),
TEXT("SEHTerm: workflow"), MB_YESNO);
if (n == IDYES) {
__try{
TriggerException();
}
__except (EXCEPTION_EXECUTE_HANDLER) {
// But the system dialog will not appear.
// So, popup a message box.
MessageBox(NULL, TEXT("Abnormal process termination"),
TEXT("Process entry point try/except handler"), MB_OK);
// Exit with a dedicated error code
return -1;
}
}
else {
TriggerException();
}
}
else {
TriggerException();
}
MessageBox(NULL, TEXT("Normal process termination"),
TEXT("SEHTerm: before leaving the main thread"), MB_OK);
return 0;
}