標籤:policy templates 記憶體 word pap 執行個體 obj 地方 pat
近期一直被一個問題所困擾,就是寫出來的程式老是出現無故崩潰,有的地方自己知道可能有問題,但是有的地方又根本沒辦法知道有什麼問題。更苦逼的事情是,我們的程式是需要7x24服務客戶,雖然不需要即時精準零差錯,但是總不能出現斷線遺失資料狀態。故剛好通過處理該問題,找到了一些解決方案,怎麼捕獲訪問非法記憶體位址或者0除以一個數。從而就遇到了這個結構化異常處理,今就簡單做個介紹認識下,方便大家遇到相關問題後,首Crowdsourced Security Testing道問題原因,再就是如何解決。廢話不多說,下面進入正題。
什麼是結構化異常處理
結構化異常處理(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;}
結果如下:
Func1nTemp = 22 //正常執行賦值finally nTemp = 22 //結束處理塊執行Func2finally 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;}
結果如下:
Func3nTemp = 22 //正常執行Func4except 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指令本身
面是兩種基本的使用方法:
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());
}
```
Windows結構化異常處理淺析