結構化異常處理(**structured exception handling**,下文簡稱:**SEH**),是作為一種系統機制引入到操作系統中的,本身與語言無關。在我們自己的程式中使用**SEH**可以讓我們集中精力開發關鍵功能,而把程式中所可能出現的異常進行統一的處理,使程式顯得更加簡潔且增加... ...
近期一直被一個問題所困擾,就是寫出來的程式老是出現無故崩潰,有的地方自己知道可能有問題,但是有的地方又根本沒辦法知道有什麼問題。更苦逼的事情是,我們的程式是需要7x24服務客戶,雖然不需要實時精準零差錯,但是總不能出現斷線丟失數據狀態。故剛好通過處理該問題,找到了一些解決方案,怎麼捕獲訪問非法記憶體地址或者0除以一個數。從而就遇到了這個結構化異常處理,今就簡單做個介紹認識下,方便大家遇到相關問題後,首先知道問題原因,再就是如何解決。廢話不多說,下麵進入正題。
什麼是結構化異常處理
結構化異常處理(structured exception handling,下文簡稱:SEH),是作為一種系統機制引入到操作系統中的,本身與語言無關。在我們自己的程式中使用SEH可以讓我們集中精力開發關鍵功能,而把程式中所可能出現的異常進行統一的處理,使程式顯得更加簡潔且增加可讀性。
使用SHE,並不意味著可以完全忽略代碼中可能出現的錯誤,但是我們可以將軟體工作流程和軟體異常情況處理進行分開,先集中精力乾重要且緊急的活,再來處理這個可能會遇到各種的錯誤的重要不緊急的問題(不緊急,但絕對重要)
當在程式中使用SEH時,就變成編譯器相關的。其所造成的負擔主要由編譯程式來承擔,例如編譯程式會產生一些表(table)來支持SEH的數據結構,還會提供回調函數。
註:
不要混淆SHE和C++ 異常處理。C++ 異常處理再形式上表現為使用關鍵字catch和throw,這個SHE的形式不一樣,再windows Visual C++中,是通過編譯器和操作系統的SHE進行實現的。
在所有 Win32 操作系統提供的機制中,使用最廣泛的未公開的機制恐怕就要數SHE了。一提到SHE,可能就會令人想起 *__try、__finally* 和 *__except* 之類的詞兒。SHE實際上包含兩方面的功能:終止處理(termination handing) 和 異常處理(exception handing)
終止處理
終止處理程式確保不管一個代碼塊(被保護代碼)是如何退出的,另外一個代碼塊(終止處理程式)總是能被調用和執行,其語法如下:
__try
{
//Guarded body
//...
}
__finally
{
//Terimnation handler
//...
}
**__try和 __finally** 關鍵字標記了終止處理程式的兩個部分。操作系統和編譯器的協同工作保障了不管保護代碼部分是如何退出的(無論是正常退出、還是異常退出)終止程式都會被調用,即**__finally**代碼塊都能執行。
try塊的正常退出與非正常退出
try塊可能會因為return,goto,異常等非自然退出,也可能會因為成功執行而自然退出。但不論try塊是如何退出的,finally塊的內容都會被執行。
int Func1()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
//正常執行
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__finally{
//結束處理
cout << "finally nTemp = " << nTemp << endl;
}
return nTemp;
}
int Func2()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
//非正常執行
return 0;
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__finally{
//結束處理
cout << "finally nTemp = " << nTemp << endl;
}
return nTemp;
}
結果如下:
Func1
nTemp = 22 //正常執行賦值
finally nTemp = 22 //結束處理塊執行
Func2
finally nTemp = 0 //結束處理塊執行
以上實例可以看出,通過使用終止處理程式可以防止過早執行return語句,當return語句視圖退出try塊的時候,編譯器會讓finally代碼塊再它之前執行。對於在多線程編程中通過信號量訪問變數時,出現異常情況,能順利是否信號量,這樣線程就不會一直占用一個信號量。當finally代碼塊執行完後,函數就返回了。
為了讓整個機制運行起來,編譯器必鬚生成一些額外代碼,而系統也必須執行一些額外工作,所以應該在寫代碼的時候避免再try代碼塊中使用return語句,因為對應用程式性能有影響,對於簡單demo問題不大,對於要長時間不間斷運行的程式還是悠著點好,下文會提到一個關鍵字**__leave**關鍵字,它可以幫助我們發現有局部展開開銷的代碼。
一條好的經驗法則:不要再終止處理程式中包含讓try塊提前退出的語句,這意味著從try塊和finally塊中移除return,continue,break,goto等語句,把這些語句放在終止處理程式以外。這樣做的好處就是不用去捕獲哪些try塊中的提前退出,從而時編譯器生成的代碼量最小,提高程式的運行效率和代碼可讀性。
####finally塊的清理功能及對程式結構的影響
在編碼的過程中需要加入需要檢測,檢測功能是否成功執行,若成功的話執行這個,不成功的話需要作一些額外的清理工作,例如釋放記憶體,關閉句柄等。如果檢測不是很多的話,倒沒什麼影響;但若又許多檢測,且軟體中的邏輯關係比較複雜時,往往需要化很大精力來實現繁瑣的檢測判斷。結果就會使程式看起來結構比較複雜,大大降低程式的可讀性,而且程式的體積也不斷增大。
對應這個問題我是深有體會,過去在寫通過COM調用Word的VBA的時候,需要層層獲取對象、判斷對象是否獲取成功、執行相關操作、再釋放對象,一個流程下來,本來一兩行的VBA代碼,C++ 寫出來就要好幾十行(這還得看操作的是幾個什麼對象)。
下麵就來一個方法讓大家看看,為什麼有些人喜歡腳本語言而不喜歡C++的原因吧。
為了更有邏輯,更有層次地操作 Office,Microsoft 把應用(Application)按邏輯功能劃分為如下的樹形結構
Application(WORD 為例,只列出一部分)
Documents(所有的文檔)
Document(一個文檔)
......
Templates(所有模板)
Template(一個模板)
......
Windows(所有視窗)
Window
Selection
View
.....
Selection(編輯對象)
Font
Style
Range
......
......
只有瞭解了邏輯層次,我們才能正確的操縱 Office。舉例來講,如果給出一個VBA語句是:
Application.ActiveDocument.SaveAs "c:\abc.doc"
那麼,我們就知道了,這個操作的過程是:
- 第一步,取得Application
- 第二步,從Application中取得ActiveDocument
- 第三步,調用 Document 的函數 SaveAs,參數是一個字元串型的文件名。
這隻是一個最簡單的的VBA代碼了。來個稍微複雜點的如下,在選中處,插入一個書簽:
ActiveDocument.Bookmarks.Add Range:=Selection.Range, Name:="iceman"
此處流程如下:
- 獲取Application
- 獲取ActiveDocument
- 獲取Selection
- 獲取Range
- 獲取Bookmarks
- 調用方法Add
獲取每個對象的時候都需要判斷,還需要給出錯誤處理,對象釋放等。在此就給出偽碼吧,全寫出來篇幅有點長
#define RELEASE_OBJ(obj) if(obj != NULL) \
obj->Realse();
BOOL InsertBookmarInWord(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE;
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
return FALSE;
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
RELEASE_OBJ(pDispApplication);
return FALSE;
}
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
RELEASE_OBJ(pDispApplication);
return FALSE;
}
hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
return FALSE;
}
hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
return FALSE;
}
hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
return FALSE;
}
hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
return FALSE;
}
ret = TRUE;
return ret;
這隻是偽碼,雖然也可以通過goto減少代碼行,但是goto用得不好就出錯了,下麵程式中稍不留神就goto到不該取得地方了。
BOOL InsertBookmarInWord2(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE;
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
goto exit6;
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
goto exit5;
}
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
goto exit4;
}
hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
goto exit4;
}
hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
goto exit3;
}
hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
got exit2;
}
hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
goto exit1;
}
ret = TRUE;
exit1:
RELEASE_OBJ(pDispApplication);
exit2:
RELEASE_OBJ(pDispDocument);
exit3:
RELEASE_OBJ(pDispSelection);
exit4:
RELEASE_OBJ(pDispRange);
exit5:
RELEASE_OBJ(pDispBookmarks);
exit6:
return ret;
此處還是通過SEH的終止處理程式來重新該方法,這樣是不是更清晰明瞭。
BOOL InsertBookmarInWord3(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE;
__try{
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
return FALSE;
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
return FALSE;
}
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL)){
return FALSE;
}
hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL)){
return FALSE;
}
hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL)){
return FALSE;
}
hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL)){
return FALSE;
}
hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr)){
return FALSE;
}
ret = TRUE;
}
__finally{
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
}
return ret;
這幾個函數的功能是一樣的。可以看到在InsertBookmarInWord中的清理函數(RELEASE_OBJ)到處都是,而InsertBookmarInWord3中的清理函數則全部集中在finally塊,如果在閱讀代碼時只需看try塊的內容即可瞭解程式流程。這兩個函數本身都很小,可以細細體會下這兩個函數的區別。
關鍵字 __leave
在try塊中使用**__leave關鍵字會使程式跳轉到try塊的結尾,從而自然的進入finally塊。
對於上例中的InsertBookmarInWord3,try塊中的return完全可以用__leave** 來替換。兩者的區別是用return會引起try過早退出系統會進行局部展開而增加系統開銷,若使用**__leave**就會自然退出try塊,開銷就小的多。
BOOL InsertBookmarInWord4(const string& bookname)
{
BOOL ret = FALSE;
IDispatch* pDispApplication = NULL;
IDispatch* pDispDocument = NULL;
IDispatch* pDispSelection = NULL;
IDispatch* pDispRange = NULL;
IDispatch* pDispBookmarks = NULL;
HRESULT hr = S_FALSE;
__try{
hr = GetApplcaiton(..., &pDispApplication);
if (!(SUCCEEDED(hr) || pDispApplication == NULL))
__leave;
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL))
__leave;
hr = GetActiveDocument(..., &pDispDocument);
if (!(SUCCEEDED(hr) || pDispDocument == NULL))
__leave;
hr = GetSelection(..., &pDispSelection);
if (!(SUCCEEDED(hr) || pDispSelection == NULL))
__leave;
hr = GetRange(..., &pDispRange);
if (!(SUCCEEDED(hr) || pDispRange == NULL))
__leave;
hr = GetBookmarks(..., &pDispBookmarks);
if (!(SUCCEEDED(hr) || pDispBookmarks == NULL))
__leave;
hr = AddBookmark(...., bookname);
if (!SUCCEEDED(hr))
__leave;
ret = TRUE;
}
__finally{
RELEASE_OBJ(pDispApplication);
RELEASE_OBJ(pDispDocument);
RELEASE_OBJ(pDispSelection);
RELEASE_OBJ(pDispRange);
RELEASE_OBJ(pDispBookmarks);
}
return ret;
}
異常處理程式
軟體異常是我們都不願意看到的,但是錯誤還是時常有,比如CPU捕獲類似非法記憶體訪問和除0這樣的問題,一旦偵查到這種錯誤,就拋出相關異常,操作系統會給我們應用程式一個查看異常類型的機會,並且運行程式自己處理這個異常。異常處理程式結構代碼如下
__try {
// Guarded body
}
__except ( exception filter ) {
// exception handler
}
註意關鍵字**__except**,任何try塊,後面必須更一個finally代碼塊或者except代碼塊,但是try後又不能同時有finally和except塊,也不能同時有多個finnaly或except塊,但是可以相互嵌套使用
異常處理基本流程
int Func3()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
nTemp = 22;
cout << "nTemp = " << nTemp << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER){
cout << "except nTemp = " << nTemp << endl;
}
return nTemp;
}
int Func4()
{
cout << __FUNCTION__ << endl;
int nTemp = 0;
__try{
nTemp = 22/nTemp;
cout << "nTemp = " << nTemp << endl;
}
__except (EXCEPTION_EXECUTE_HANDLER){
cout << "except nTemp = " << nTemp << endl;
}
return nTemp;
}
結果如下:
Func3
nTemp = 22 //正常執行
Func4
except nTemp = 0 //捕獲異常,
Func3中try塊只是一個簡單操作,故不會導致異常,所以except塊中代碼不會被執行,Func4中try塊視圖用22除0,導致CPU捕獲這個事件,並拋出,系統定位到except塊,對該異常進行處理,該處有個異常過濾表達式,系統中有三該定義(定義在Windows的Excpt.h中):
1. EXCEPTION_EXECUTE_HANDLER:
我知道這個異常了,我已經寫了代碼來處理它,讓這些代碼執行吧,程式跳轉到except塊中執行並退出
2. EXCEPTION_CONTINUE_SERCH
繼續上層搜索處理except代碼塊,並調用對應的異常過濾程式
3. EXCEPTION_CONTINUE_EXECUTION
返回到出現異常的地方重新執行那條CPU指令本身
面是兩種基本的使用方法:
方式一:直接使用過濾器的三個返回值之一
__try { …… } __except ( EXCEPTION_EXECUTE_HANDLER ) { …… }
方式二:自定義過濾器
```
__try {
……
}
__except ( MyFilter( GetExceptionCode() ) )
{
……
}
LONG MyFilter ( DWORD dwExceptionCode )
{
if ( dwExceptionCode == EXCEPTION_ACCESS_VIOLATION )
return EXCEPTION_EXECUTE_HANDLER ;
else
return EXCEPTION_CONTINUE_SEARCH ;
}
<br>
##.NET4.0中捕獲SEH異常
在.NET 4.0之後,CLR將會區別出一些異常(都是SEH異常),將這些異常標識為破壞性異常(Corrupted State Exception)。針對這些異常,CLR的catch塊不會捕捉這些異常,一下代碼也沒有辦法捕捉到這些異常。
try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
因為並不是所有人都需要捕獲這個異常,如果你的程式是在4.0下麵編譯並運行,而你又想在.NET程式里捕捉到SEH異常的話,有兩個方案可以嘗試:
- 在托管程式的.config文件里,啟用legacyCorruptedStateExceptionsPolicy這個屬性,即簡化的.config文件類似下麵的文件:
App.Config
這個設置告訴CLR 4.0,整個.NET程式都要使用老的異常捕捉機制。
- 在需要捕捉破壞性異常的函數外面加一個HandleProcessCorruptedStateExceptions屬性,這個屬性只控制一個函數,對托管程式的其他函數沒有影響,例如:
[HandleProcessCorruptedStateExceptions]
try{
//....
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
```