導讀:
從本篇文章開始,將全面闡述__try,__except,__finally,__leave異常模型機制,它也即是Windows系列操作系統平台上提供的SEH模型。主人公阿愚將在這里與大家分享SEH( 結構化異常處理)的學習過程和經驗總結。 深入理解請參閱<<windows 核心編程>>第23, 24章.
SEH實際包含兩個主要功能:結束處理(termination handling)和異常處理(exception handling)
每當你建立一個try塊,它必須跟隨一個finally塊或一個except塊。
一個try 塊之后不能既有finally塊又有except塊。但可以在try - except塊中嵌套try - finally塊,反過來
也可以。
__try __finally關鍵字用來標出結束處理程序兩段代碼的輪廓
不管保護體(try塊)
是如何退出的。不論你在保護體中使用return,還是goto,或者是longjump,結束處理程序
(finally塊)都將被調用。
在try使用__leave關鍵字會引起跳轉到try塊的結尾
SEH有兩項非常強大的功能。當然,首先是異常處理模型了,因此,這篇文章首先深入闡述SEH提供的異常處理模型。另外,SEH還有一個特別強大的功能,這將在下一篇文章中進行詳細介紹。
try-except入門
SEH的異常處理模型主要由try-except語句來完成,它與標准C++所定義的異常處理模型非常類似,也都是可以定義出受監控的代碼模塊,以及定義異常處理模塊等。還是老辦法,看一個例子先,代碼如下:
//seh-test.c
void main() { // 定義受監控的代碼模塊 __try { puts("in try"); } //定義異常處理模塊 __except(1) { puts("in except"); } }
呵呵!是不是很簡單,而且與C++異常處理模型很相似。當然,為了與C++異常處理模型相區別,VC編譯器對關鍵字做了少許變動。首先是在每個關鍵字加上兩個下划線作為前綴,這樣既保持了語義上的一致性,另外也盡最大可能來避免了關鍵字的有可能造成名字沖突而引起的麻煩等;其次,C++異常處理模型是使用catch關鍵字來定義異常處理模塊,而SEH是采用__except關鍵字來定義。並且,catch關鍵字后面往往好像接受一個函數參數一樣,可以是各種類型的異常數據對象;但是__except關鍵字則不同,它后面跟的卻是一個表達式(可以是各種類型的表達式,后面會進一步分析)。
try-except進階
與C++異常處理模型很相似,在一個函數中,可以有多個try-except語句。它們可以是一個平面的線性結構,也可以是分層的嵌套結構。例程代碼如下:
// 例程1
// 平面的線性結構
void main() { __try { puts("in try"); } __except(1) { puts("in except"); } // 又一個try-except語句 __try { puts("in try1"); } __except(1) { puts("in except1"); } }
// 例程2
// 分層的嵌套結構
void main() { __try { puts("in try"); // 又一個try-except語句 __try { puts("in try1"); } __except(1) { puts("in except1"); } } __except(1) { puts("in except"); } }
// 例程3
// 分層的嵌套在__except模塊中
void main() { __try { puts("in try"); } __except(1) { // 又一個try-except語句 __try { puts("in try1"); } __except(1) { puts("in except1"); } puts("in except"); } }
1. 受監控的代碼模塊被執行(也即__try定義的模塊代碼);
2. 如果上面的代碼執行過程中,沒有出現異常的話,那么控制流將轉入到__except子句之后的代碼模塊中;
3. 否則,如果出現異常的話,那么控制流將進入到__except后面的表達式中,也即首先計算這個表達式的值,之后再根據這個值,來決定做出相應的處理。這個值有三種情況,如下:
EXCEPTION_CONTINUE_EXECUTION (–1) 異常被忽略,控制流將在異常出現的點之后,繼續恢復運行。
EXCEPTION_CONTINUE_SEARCH (0) 異常不被識別,也即當前的這個__except模塊不是這個異常錯誤所對應的正確的異常處理模塊。系統將繼續到上一層的try-except域中繼續查找一個恰當的__except模塊。
EXCEPTION_EXECUTE_HANDLER (1) 異常已經被識別,也即當前的這個異常錯誤,系統已經找到了並能夠確認,這個__except模塊就是正確的異常處理模塊。控制流將進入到__except模塊中。
try-except深入
上面的內容中已經對try-except進行了全面的了解,但是有一點還沒有闡述到。那就是如何在__except模塊中獲得異常錯誤的相關信息,這非常關鍵,它實際上是進行異常錯誤處理的前提,也是對異常進行分層分級別處理的前提。可想而知,如果沒有這些起碼的信息,異常處理如何進行?因此獲取異常信息非常的關鍵。Windows提供了兩個API函數,如下:
LPEXCEPTION_POINTERS GetExceptionInformation(VOID);
DWORD GetExceptionCode(VOID);
其中GetExceptionCode()返回錯誤代碼,而GetExceptionInformation()返回更全面的信息,看它函數的聲明,返回了一個LPEXCEPTION_POINTERS類型的指針變量。那么EXCEPTION_POINTERS結構如何呢?如下,
typedef struct _EXCEPTION_POINTERS { // exp
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS;
呵呵!仔細瞅瞅,這是不是和上一篇文章中,用戶程序所注冊的異常處理的回調函數的兩個參數類型一樣。是的,的確沒錯!其中EXCEPTION_RECORD類型,它記錄了一些與異常相關的信息;而CONTEXT數據結構體中記錄了異常發生時,線程當時的上下文環境,主要包括寄存器的值。因此有了這些信息,__except模塊便可以對異常錯誤進行很好的分類和恢復處理。不過特別需要注意的是,這兩個函數只能是在__except后面的括號中的表達式作用域內有效,否則結果可能沒有保證(至於為什么,在后面深入分析異常模型的實現時候,再做詳細闡述)。看一個例程吧!代碼如下:
int exception_access_violation_filter(LPEXCEPTION_POINTERS p_exinfo) { if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { printf("存儲保護異常\n"); return 1; } else return 0; } int exception_int_divide_by_zero_filter(LPEXCEPTION_POINTERS p_exinfo) { if(p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) { printf("被0除異常\n"); return 1; } else return 0; } void main() { __try { __try { int* p; // 下面將導致一個異常 p = 0; *p = 45; } // 注意,__except模塊捕獲一個存儲保護異常 __except(exception_access_violation_filter(GetExceptionInformation())) { puts("內層的except塊中"); } //可以在此寫除0異常的語句 int b = 0; int a = 1 / b; } // 注意,__except模塊捕獲一個被0除異常 __except(exception_int_divide_by_zero_filter(GetExceptionInformation())) { puts("外層的except塊中"); } }
上面的程序運行結果如下:
存儲保護異常
內層的except塊中
Press any key to continue
呵呵!感覺不錯,大家可以在上面的程序基礎之上改動一下,讓它拋出一個被0除異常,看程序的運行結果是不是如預期那樣。
最后還有一點需要闡述,在C++的異常處理模型中,有一個throw關鍵字,也即在受監控的代碼中拋出一個異常,那么在SEH異常處理模型中,是不是也應該有這樣一個類似的關鍵字或函數呢?是的,沒錯!SEH異常處理模型中,對異常划分為兩大類,第一種就是上面一些例程中所見到的,這類異常是系統異常,也被稱為硬件異常;還有一類,就是程序中自己拋出異常,被稱為軟件異常。怎么拋出呢?還是Windows提供了的API函數,它的聲明如下:
VOID RaiseException(
DWORD dwExceptionCode, // exception code
DWORD dwExceptionFlags, // continuable exception flag
DWORD nNumberOfArguments, // number of arguments in array
CONST DWORD *lpArguments // address of array of arguments
);
很簡單吧!實際上,在C++的異常處理模型中的throw關鍵字,最終也是對RaiseException()函數的調用,也即是說,throw是RaiseException的上層封裝的更高級一類的函數,這以后再詳細分析它的代碼實現。這里還是看一個簡單例子吧!代碼如下:
int seh_filer(int code) { switch(code) { case EXCEPTION_ACCESS_VIOLATION : printf("存儲保護異常,錯誤代碼:%x\n", code); break; case EXCEPTION_DATATYPE_MISALIGNMENT : printf("數據類型未對齊異常,錯誤代碼:%x\n", code); break; case EXCEPTION_BREAKPOINT : printf("中斷異常,錯誤代碼:%x\n", code); break; case EXCEPTION_SINGLE_STEP : printf("單步中斷異常,錯誤代碼:%x\n", code); break; case EXCEPTION_ARRAY_BOUNDS_EXCEEDED : printf("數組越界異常,錯誤代碼:%x\n", code); break; case EXCEPTION_FLT_DENORMAL_OPERAND : case EXCEPTION_FLT_DIVIDE_BY_ZERO : case EXCEPTION_FLT_INEXACT_RESULT : case EXCEPTION_FLT_INVALID_OPERATION : case EXCEPTION_FLT_OVERFLOW : case EXCEPTION_FLT_STACK_CHECK : case EXCEPTION_FLT_UNDERFLOW : printf("浮點數計算異常,錯誤代碼:%x\n", code); break; case EXCEPTION_INT_DIVIDE_BY_ZERO : printf("被0除異常,錯誤代碼:%x\n", code); break; case EXCEPTION_INT_OVERFLOW : printf("數據溢出異常,錯誤代碼:%x\n", code); break; case EXCEPTION_IN_PAGE_ERROR : printf("頁錯誤異常,錯誤代碼:%x\n", code); break; case EXCEPTION_ILLEGAL_INSTRUCTION : printf("非法指令異常,錯誤代碼:%x\n", code); break; case EXCEPTION_STACK_OVERFLOW : printf("堆棧溢出異常,錯誤代碼:%x\n", code); break; case EXCEPTION_INVALID_HANDLE : printf("無效句病異常,錯誤代碼:%x\n", code); break; default : if(code & (1<<29)) printf("用戶自定義的軟件異常,錯誤代碼:%x\n", code); else printf("其它異常,錯誤代碼:%x\n", code); break; } return 1; } void main() { __try { puts("try塊中"); // 注意,主動拋出一個軟異常 RaiseException(0xE0000001, 0, 0, 0); } __except(seh_filer(GetExceptionCode())) { puts("except塊中"); } }
上面的程序運行結果如下:
hello
try塊中
用戶自定義的軟件異常,錯誤代碼:e0000001
except塊中
world
Press any key to continue
上面的程序很簡單,這里不做進一步的分析。我們需要重點討論的是,在__except模塊中如何識別不同的異常,以便對異常進行很好的分類處理。毫無疑問,它當然是通過GetExceptionCode()或GetExceptionInformation ()函數來獲取當前的異常錯誤代碼,實際也即是DwExceptionCode字段。異常錯誤代碼在winError.h文件中定義,它遵循Windows系統下統一的錯誤代碼的規則。每個DWORD被划分幾個字段,如下表所示:
例如我們可以在winbase.h文件中找到EXCEPTION_ACCESS_VIOLATION的值為0 xC0000005,將這個異常代碼值拆開,來分析看看它的各個bit位字段的涵義。
C 0 0 0 0 0 0 5 (十六進制)
1100 0000 0000 0000 0000 0000 0000 0101 (二進制)
第3 0位和第3 1位都是1,表示該異常是一個嚴重的錯誤,線程可能不能夠繼續往下運行,必須要及時處理恢復這個異常。第2 9位是0,表示系統中已經定義了異常代碼。第2 8位是0,留待后用。第1 6 位至2 7位是0,表示是FACILITY_NULL設備類型,它代表存取異常可發生在系統中任何地方,不是使用特定設備才發生的異常。第0位到第1 5位的值為5,表示異常錯誤的代碼。
如果程序員在程序代碼中,計划拋出一些自定義類型的異常,必須要規划設計好自己的異常類型的划分,按照上面的規則來填充異常代碼的各個字段值,如上面示例程序中拋出一個異常代碼為0xE0000001軟件異常。
總結
(1) C++異常模型用try-catch語法定義,而SEH異常模型則用try-except語法;
(2) 與C++異常模型相似,try-except也支持多層的try-except嵌套。
(3) 與C++異常模型不同的是,try-except模型中,一個try塊只能是有一個except塊;而C++異常模型中,一個try塊可以有多個catch塊。
(4) 與C++異常模型相似,try-except模型中,查找搜索異常模塊的規則也是逐級向上進行的。但是稍有區別的是,C++異常模型是按照異常對象的類型來進行匹配查找的;而try-except模型則不同,它通過一個表達式的值來進行判斷。如果表達式的值為1(EXCEPTION_EXECUTE_HANDLER),表示找到了異常處理模塊;如果值為0(EXCEPTION_CONTINUE_SEARCH),表示繼續向上一層的try-except域中繼續查找其它可能匹配的異常處理模塊;如果值為-1(EXCEPTION_CONTINUE_EXECUTION),表示忽略這個異常,注意這個值一般很少用,因為它很容易導致程序難以預測的結果,例如,死循環,甚至導致程序的崩潰等。
(5) __except關鍵字后面跟的表達式,它可以是各種類型的表達式,例如,它可以是一個函數調用,或是一個條件表達式,或是一個逗號表達式,或干脆就是一個整型常量等等。最常用的是一個函數表達式,並且通過利用GetExceptionCode()或GetExceptionInformation ()函數來獲取當前的異常錯誤信息,便於程序員有效控制異常錯誤的分類處理。
(6) SEH異常處理模型中,異常被划分為兩大類:系統異常和軟件異常。其中軟件異常通過RaiseException()函數拋出。RaiseException()函數的作用類似於C++異常模型中的throw語句。
第3種情況,也即由於出現異常而導致的“全局展開”,對於程序員而言,這也許是無法避免的,因為你在利用異常處理機制提高程序可靠健壯性的同時,不可避免的會引起性能上其它的一些開銷。呵呵!這世界其實也算瞞公平的,有得必有失。
但是,對於第2種情況,程序員完全可以有效地避免它,避免“局部展開”引起的不必要的額外開銷。實際這也是與結構化程序設計思想相一致的,也即一個程序模塊應該只有一個入口和一個出口,程序模塊內盡量避免使用goto語句等。但是,話雖如此,有時為了提高程序的可讀性,程序員在編寫代碼時,有時可能不得不采用一些與結構化程序設計思想相悖的做法,例如,在一個函數中,可能有多處的return語句。針對這種情況,SEH提供了一種非常有效的折衷方案,那就是__leave關鍵字所起的作用,它既具有像goto語句和return語句那樣類似的作用(由於檢測到某個程序運行中的錯誤,需要馬上離開當前的 __try塊作用域),但是又避免了“局部展開” 的額外開銷。還是看個例子吧!代碼如下:
#include <stdio.h> void test() { puts("hello"); __try { int* p; puts("__try塊中"); // 直接跳出當前的__try作用域 __leave; p = 0; *p = 25; } __finally { // 這里會被執行嗎?當然 puts("__finally塊中"); } puts("world"); } void main() { __try { test(); } __except(1) { puts("__except塊中"); } }
上面的程序運行結果如下:
hello
__try塊中
__finally塊中
world
Press any key to continue