|
jimmy 戰志杰 編譯
本文編譯自Jeffrey Richter先生的“Advanced Windows”部分章節。 1、引言 在“C++中例外的處理”一文中(見計算機世界網2001年12月20日),我們討論了C++中的例外(或異常)處理。本文將進一步探討Visual C++中的結構異常處理。 想象一下,如果在編程過程中你不需要考慮任何錯誤,你的程序永遠不會出錯,有足夠的內存,你需要的文件永遠存在,這將是一件多么愉快的事。這時你的程序不需要太多的if語句轉來轉去,非常容易寫,容易讀,也容易理解。如果你認為這樣的編程環境是一種夢想,那么你就會喜歡結構異常處理(structu reed exception handling)。 結構異常處理的本質就是讓你專心于如何去完成你的任務。如果在程序運行過程中出現任何錯誤,系統會接收(catch)并通知(notify)你。雖然利用結構異常處理你不可能完全忽略你的程序出錯的可能性,但是結構異常處理確確實實允許你將你的主要任務與錯誤處理分離開來。這種分離使得你可以集中精力于你的工作,而在以后在考慮可能的錯誤。 結構異常處理的主要工作是由編譯器來完成的,而不是由操作系統。編譯器在遇到例外程序段時需要產生額外的特殊代碼來支持結構異常處理。所以,每一個編譯器產品供應商可能使用自己的語法和規定。這里我們采用微軟的Visual C++編譯器來進行討論。 注意不要將這里討論的結構異常處理與C++中的異常處理混為一談。C++中的異常處理是另一種形式的異常處理,它使用了C++的關鍵詞catch和throw。 微軟最早在Visual C++版本2.0引進結構異常處理。結構異常處理主要由兩部分組成:中斷處理(termination handling)和例外處理(exception handling)。 2、中斷處理句柄(termination handler) 2.1、中斷處理句柄定義 中斷處理句柄保證了,不論進程如何離開另一程序段--這里稱之為守衛體(guarded body),該句柄內的程序段永遠會被調用和執行。微軟的Visual C++編譯器的中斷處理句柄語法為 __try { // Guarded body . . . } __finally { // Termination handler . . . } 這里的__try和__finally勾畫出了中斷處理句柄的兩個部分。在上面的例子中,操作系統和編譯器一起保證了不論包含在__try內的程序段出現何種情況,包含在__finally內的程序段永遠會被運行。不論你在__try內的程序段中調用return、goto或longjump,__finally內的中斷處理句柄永遠會被調用。其流程為 // 1、執行try程序段前的代碼 __try { // 2、執行try程序段內的代碼 } __finally { // 3、執行finally程序段內的代碼 } // 4、執行finally程序段后的代碼 2.2、幾個例子 下面我們通過幾個具體例子來討論中斷處理句柄是如何工作的。 2.2.1、例1--Funcenstein1 清單一給出了我們的第一個例子。 DWORD Funcenstein1(void) { 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 others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return (dwTemp); } 例1 Funcenstein1函數代碼 在函數Funcenstein1中,我們使用了try-finally程序塊。但是它們并沒有為我們做多少工作:等待一個指示燈信號,改變保護數據的內容,將新的數據指定給一個局域變量dwTemp,釋放指示燈信號,返回新的數據給調用函數。 2.2.2、例2--Funcenstein2 現在讓我們對Funcenstein1稍稍做一些改動,看看會出現什么情況(見清單二)。 DWORD Funcenstein2(void) { 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 others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing--this code will never execute in this version. dwTemp = 9; return (dwTemp); } 例2 Funcenstein2函數代碼 在函數Funcenstein2中,我們在try程序段里加入了一個return返回語句。該返回語句告訴編譯器,你想離開函數Funcenstein2并返回dwTemp內的內容5給調用函數。然而,如果此返回語句被執行,本線程永遠不會釋放指示燈信號,其它線程也就永遠不會得到該指示燈信號。你可以想象,在多線程程序中這是一個多么嚴重的問題。 但是,使用了中斷處理句柄避免了這種情況發生。當返回語句試圖離開try程序段時,編譯器保證了在finally程序段內的代碼得到執行。所以,finally程序段內的代碼保證會在try程序段中的返回語句前執行。在函數Funcenstein2中,將調用ReleaseSemaphore放在finally程序段內保證了指示燈信號會得到釋放。 在finally程序段內的代碼被執行后,函數Funcenstein2立即返回。這樣,因為try程序段內的return返回語句,任何finally程序段后的代碼都不會被執行。因而Funcenstein2返回值是5,而不是9。 必須指出的是,當遇到例2中這種過早返回語句時,編譯器需要產生額外的代碼以保證finally程序段內的代碼的執行。此過程稱作為局域展開。當然,這必然會降低整個程序的效率。所以,你應該盡量避免使用這類代碼。在后面我們會討論關鍵詞__leave,它可以幫助我們避免編寫出現局域展開一類的代碼。 2.2.3、例3--Funcenstein3 現在讓我們對Funcenstein2做進一步改動,看看會出現什么情況(見例3)。 DWORD Funcenstein3(void) { 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; // Try to jump over the finally block. goto ReturnValue; } __finally { // 3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } dwTemp = 9; // 4. Continue processing. ReturnValue: return (dwTemp); } 例3 Funcenstein3函數代碼 在函數Funcenstein3中,當遇到goto語句時編譯器會產生額外的代碼以保證finally程序段內的代碼得到執行。但是,這一次finally程序段后ReturnValue標簽后面的代碼會被執行,因為try或finally程序段內沒有返回語句。函數的返回值是5。同樣,由于goto語句打斷了從try程序段到finally程序段的自然流程,程序的效率會降低。 2.2.4、例4--Funcfurter1 現在讓我們來看中斷處理真正展現其功能的一個例子。(見例4)。 DWORD Funcfurter1(void) { 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 others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // 4. Continue processing. return (dwTemp); } 例4 Funcfurter1函數代碼 設想try程序段內調用的Funcinator函數具有某種缺陷而造成無效內存讀寫。在16位視窗應用程序中,這會導致一個已定義好的錯誤信息對話框出現。在用戶關閉對話框的同時該應用程序也終止運行。在不具有try-finally的Win32應用程序中,這會導致程序終止運行,指示燈信號永遠不會得到釋放。這就造成了等待該指示燈信號的其它線程會永遠等待下去。而將ReleaseSemaphore放在finally程序段內則從根本上保證了不論何種情況出現指示燈信號都會得到釋放。 如果中斷處理句柄能夠處理由于無效內存讀寫而造成的程序中斷,我們就完全有理由相信它能夠處理諸如setjump/longjump、break和continue這類的中斷轉移。事實也正是這樣。 2.3、小測試 下面一個例子(見清單五)請讀者猜測一下函數FuncaDoodleDoo的返回值。(答案為14) DWORD FuncaDoodleDoo(void) { DWORD dwTemp = 0; while (dwTemp 〈 10) { __try { if (dwTemp == 2) continue; if (dwTemp == 3) break; } __finally { dwTemp++; } dwTemp++; } dwTemp += 10; return (dwTemp); } FuncaDoodleDoo函數代碼 雖然中斷處理句柄能夠接收出現在try程序段內的絕大部分異常情況,但是如果線程或進程中斷執行的話,則finally程序段內的代碼不會被執行。調用ExitThread或ExitProcess就會立即造成線程或進程的中斷,而不會執行finally程序段。另外,如果其它的應用程序調用ExitThread或ExitProcess而造成你的線程或進程中斷,你程序中的finally程序段也不會被執行。一些C函數如abort會調用ExitProcess,也會導致你的finally程序段不被執行。對此你無能為力。但你可以防止你自己提早調用ExitThread或ExitProcess。 2.4、應用例子 我們已經討論了中斷處理句柄的句法及語法。現在我們進一步討論如何利用中斷處理句柄來簡化一個比較復雜的編程問題。 首先讓我們來看一個沒有使用中斷處理句柄的例子,程序源代碼見例6。 BOOL Funcarama1 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return (FALSE); } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { CloseHandle(hFile); return (FALSE); } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return (FALSE); } // Do some calculation on the data. . . . // Clean up all the resources. VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); CloseHandle(hFile); return (TRUE); } 例6 沒有使用中斷處理句柄的Funcarama1函數代碼 在上例Funcarama1函數中,所有的錯誤診斷使得該函數難以理解、維護和修改。當然,我們可以對Funcarama1函數進行一些改動,使其易于理解(見例7)。 BOOL Funcarama2 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; DWORD dwNumBytesRead; BOOL fOk, fSuccess = FALSE; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile != INVALID_HANDLE_VALUE) { lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf != NULL) { fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (fOk || (dwNumBytesRead != 0)) { // Do some calculation on the data. . . . fSuccess = TRUE; } } VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); } CloseHandle(hFile); return (fSuccess); } 例7 沒有使用中斷處理句柄的Funcarama2函數代碼 雖然函數Funcarama2容易理解,但是仍然難于維護和修改。 現在讓我們來利用中斷處理句柄重寫Funcaram1函數,其代碼如清單八。 BOOL Funcarama3 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; __try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return (FALSE); } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { return (FALSE); } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); return (FALSE); } // Do some calculation on the data. . . . } __finally { // Clean up all the resources. if (lpBuf != NULL) VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return (TRUE); } 例8 使用了中斷處理句柄的Funcarama3函數代碼 Funcarama3函數版的好處是所有的清除工作都集中在一個地方:finally程序段內。這樣在我們需要對該函數增加新的條件語句時,我們只需要在finally程序段內簡單增添一行清除語句就可以了,而不必回過頭來在每一出可能出錯的地方添加清除語句。 Funcarama3函數的真正問題在于其效率。我們以前說過應盡可能的避免在try程序段內使用return語句。為了避免這種情況,微軟在它的編譯器里引進了另一個關鍵詞__leave。利用關鍵詞__leave重寫的Funcarama3函數見例9。 BOOL Funcarama4 (void) { HANDLE hFile = INVALID_HANDLE_VALUE; LPVOID lpBuf = NULL; // Assume that the function will not execute successfully. BOOL fFunctionOk = FALSE; __try { DWORD dwNumBytesRead; BOOL fOk; hFile = CreateFile("SOMEDATA.DAT", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { __leave; } lpBuf = VitualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE); if (lpBuf == NULL) { __leave; } fOk = ReadFile(hFile, lpBuf, 1024, &dwNumBytesRead, NULL); if (!fOk || (dwNumBytesRead == 0)) { VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); __leave; } // Do some calculation on the data. . . . // Indicate that the entire function executed successfully. fFunctionOk = TRUE; } __finally { // Clean up all the resources. if (lpBuf != NULL) VirtualFree(lpBuf, MEM_RELEASE | MEM_DECOMMIT); if (hFile != INVALID_HANDLE_VALUE) CloseHandle(hFile); } // Continue processing. return (fFunctionOk); } 例9 使用了中斷處理句柄和關鍵詞__leave的Funcarama4函數代碼 try程序段內的關鍵詞__leave導致程序運行指針直接跳到try程序段的結尾(你可以將此看成為跳到try程序段的結束花括弧)。這樣,因為控制流程將“自然”的離開try程序段,進入finally程序段,所以不需付出額外代價而導致效率降低。但是你需要引進一個新的變量來指示整個函數的運行是否成功。 從try程序段到finally程序段,控制流程既可以是自然進入,也可以是由于異常的出現而導致控制流程過早離開try程序段而進入finally程序段。為確定何種情況下造成finally程序段的運行,我們可以調用AbnormalTermination函數來診斷。 BOOL AbnormalTermination(VOID); 該函數只能在finally程序段內調用以診斷與此finally相對應的try程序段是否是過早離開。如果AbnormalTermination的返回值是FALSE,表明程序流程是自然離開try程序段。否則,則是過早離開。 3、異常處理句柄(Exception handler) 3.1、異常(或例外)處理句柄的定義 異常(或例外)是你不希望出現的事件。在一個完好的應用程序中,你不希望讀寫無效內存地址或除數為零的情況出現。但是這類錯誤的確會發生。在出現這類錯誤時,CPU會負責提出針對該類錯誤的例外。當CPU提出一個例外時,我們稱之為硬件異常(或例外)(hardware exception)。操作系統和應用程序自身也可以提出自己的異常。這類異常我們稱之為軟件異常(或例外)(software exception)。 當一個硬件異常或軟件異常被提出時,操作系統向你的程序提供一種機會使得你的程序可以診斷那類異常被提出并允許你的程序對此進行處理。異常處理句柄的語法為 __try { // Guarded body . . . } __except (exception filter) { // Exception handler . . . } 請注意關鍵詞__except。當你建立一個try程序段時,它必須跟隨一個finally程序段或一個except程序段。一個try程序段不能同時既跟隨一個finally程序段又跟隨一個except程序段。一個try程序段也不能同時跟隨多個finally程序段或多個except程序段。但是,try-finally程序段卻可以嵌套在try-except程序段內,或try-except程序段嵌套在try-finally程序段內。 3.2、幾個例子 不同于中斷處理句柄,異常處理句柄直接由操作系統執行,編譯器不需要做太多工作。下面我們通過幾個具體例子來討論異常處理句柄是如何工作的。 3.2.1、例5--Funcmeister1函數 下面是一個使用了try-except異常處理句柄的函數Funcmeister1,其代碼見清單十。 DWORD Funcmeister1 (void) { DWORD dwTemp; // 1. Do any processing here. . . . __try { // 2. Perform some operation. dwTemp = 0; } __except (EXCEPTION_EXECUTE_HANDLER) { // 3. Handle an exception; this never executes. . . . } // 3. Continue processing. return (dwTemp); } 例10 例5Funcmeister1函數代碼 在Funcmeister1函數中的try程序段內,我們簡單地將dwTemp賦值為零。該操作不會導致任何異常的提出。所以,except程序段內的程序永遠不會被執行。請注意,這有別于中斷處理句柄try-finally。在執行了dwTemp賦值語句后的下一個執行語句是return返回語句。 雖然我們不鼓勵在try程序段內使用return, goto, continue和break語句,但是在異常處理句柄的try程序段內使用這些語句不會象中斷處理句柄那樣造成運行代碼的增加和效率下降。 3.2.2、例6--Funcmeister2函數 讓我們對Funcmeister1函數進行一些改動,看看會出現什么情況。改動后的函數見例11。 DWORD Funcmeister2 (void) { DWORD dwTemp = 0; // 1. Do any processing here. . . . __try { // 2. Perform some operation(s). dwTemp = 5 / dwTemp; // Generate an exception dwTemp += 10; // Never excutes } __except ( /* 3. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { // 4. Handle an exception; this never executes. MessageBeep(0); . . . } // 5. Continue processing. return (dwTemp); } 例11 例6Funcmeister2函數代碼 函數Funcmeister2中的try程序段dwTemp = 5 / dwTemp語句導致CPU提出一個硬件異常。當該異常被提出時,操作系統會尋找相對應的except程序段的起始位置并評估其異常篩選表達式(exception filter expression)。異常篩選表達式可以取下列標識符值之一。這些標識符定義在Win32 EXCPT.H頭文件中。 標識符 定義為 EXCEPTION_EXECUTE_HANDLER 1 EXCEPTION_CONTINUE_SEARCH 0 EXCEPTION_CONTINUE_EXECUTION -1 3.3、異常篩選(exception filter) EXCEPTION_EXECUTE_HANDLER表明當一個異常出現時,運行程序跳到except程序段轉而執行except程序段內的代碼。except程序段內的代碼執行完后,系統認為該異常已處理完,接著繼續執行except程序段后的代碼。 EXCEPTION_CONTINUE_EXECUTION表明當一個異常出現時,運行程序不立即執行except程序段內的代碼而返回try程序段內產生異常的語句繼續執行該語句。 EXCEPTION_CONTINUE_SEARCH表明當一個異常出現時,運行程序不執行該except程序段內的代碼而尋求由高一級的異常處理句柄來處理此異常。 Win32 WINBASE.H頭文件中定義了可能出現的各種異常代碼。我們可以通過調用GetExceptionCode函數來診斷何種異常被提出,從而決定異常處理句柄該采取何種行動。GetExceptionCode函數定義為 DWORD GetExceptionCode(VOID); 它的返回值表明何種異常出現。下面的程序說明如何調用GetExceptionCode函數。 __try { x = 0; y = 4 / x; } __except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { // Handle divide by zero exception. } 當一個異常發生時,操作系統會將有關該異常的信息儲存在三個結構中,并將它們存放在提出此異常線程的堆棧里。這三個結構是EXCEPTION_RECORD,CONTEXT,和EXCEPTION_POINTERS。EXCEPTION_RECORD儲存著與CPU無關的異常信息,CONTEXT則儲存著與CPU有關的異常信息。EXCEPTION_POINTERS結構包含了兩個分別指向EXCEPTION_RECORD和CONTEXT的指針。 typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS 假如你的程序需要這些異常信息,你可以通過調用GetExceptionInformation函數來獲取。 LPEXCEPTION GetExceptionInformation(void); GetExceptionInformation函數返回一個指向EXCEPTION_POINTERS結構的指針。下面的函數說明了如何調用GetExceptionInformation函數。 void FuncSkunk (void) { // Declare variables that we can use to save the exception // record and the context if an exception should occur. EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; . . . __try { . . . } __except ( SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord, SavedContext = *(GetExceptionInformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) { // We can use the SavedExceptRec and SavedContext // variables inside the handler code block. switch (SavedExceptRec.ExceptionCode) { . . . } } . . . } 注意,在上面的異常篩選表達式程序中我們使用了C語言的“,”操作符。許多程序員對此并不是很熟悉。該操作符告訴編譯器從左到右運行由“,”分離的各表達式。在所有的表達式都運行完后,返回最后一個(或最右面的)表達式的值。 4、軟件異常(software exception) 至此為止我們所討論的是如何處理由CPU提出的硬件異常(hardware exception)。通常,操作系統或應用程序自身提出的軟件異常也非常有用。例如,HeapAlloc函數就提供了一個非常好的利用軟件異常的例子。在調用HeapAlloc時,你可以設置HEAP_GENERATE-EXCEPTIONS指示旗(flag)。這樣如果HeapAlloc不能滿足你的內存分配要求,它會產生一個STATUS_NO_MEMORY軟件異常。 假如你想利用這個異常,你可以在你的try程序段內繼續編寫你的代碼,如同內存分配總是會成功一樣。如果內存分配失敗,你可以利用except程序段來處理這個異常或利用finally程序段來做清除工作。 你的程序不需要知道你要處理的異常是軟件異常還是硬件異常。你利用try-finally和try-except來處理軟件異常和硬件異常的方式是一樣的。但是你可以讓你的程序象HeapAlloc函數一樣提出自己的異常。為了在你的程序中提出軟件異常,你需要調用RaiseException函數。 VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD cArguments, LPDWORD lpArguments); 關于該函數的使用,請參考微軟的有關文獻。 5、結論 結構異常處理由中斷處理和例外處理兩部分組成。采用結構異常處理使得你可以將精力集中在你的程序應用代碼設計上,從而使得應用方案的設計更方便、具體。采用結構異常處理編寫的程序更易于理解、修改和維護,從而增加了程序的可讀性和維護性。
|