1. 程式人生 > >Windows結構化異常處理淺析

Windows結構化異常處理淺析

null 崩潰 plc 處理程序 了解 got AC doc pdo

近期一直被一個問題所困擾,就是寫出來的程序老是出現無故崩潰,有的地方自己知道可能有問題,但是有的地方又根本沒辦法知道有什麽問題。更苦逼的事情是,我們的程序是需要7x24服務客戶,雖然不需要實時精準零差錯,但是總不能出現斷線丟失數據狀態。故剛好通過處理該問題,找到了一些解決方案,怎麽捕獲訪問非法內存地址或者0除以一個數。從而就遇到了這個結構化異常處理,今就簡單做個介紹認識下,方便大家遇到相關問題後,首先知道問題原因,再就是如何解決。廢話不多說,下面進入正題。


什麽是結構化異常處理

結構化異常處理(structured exception handling,下文簡稱:SEH),是作為一種系統機制引入到操作系統中的,本身與語言無關。在我們自己的程序中使用SEH

可以讓我們集中精力開發關鍵功能,而把程序中所可能出現的異常進行統一的處理,使程序顯得更加簡潔且增加可讀性。

使用SHE,並不意味著可以完全忽略代碼中可能出現的錯誤,但是我們可以將軟件工作流程和軟件異常情況處理進行分開,先集中精力幹重要且緊急的活,再來處理這個可能會遇到各種的錯誤的重要不緊急的問題(不緊急,但絕對重要)

當在程序中使用SEH時,就變成編譯器相關的。其所造成的負擔主要由編譯程序來承擔,例如編譯程序會產生一些表(table)來支持SEH的數據結構,還會提供回調函數。

註:
不要混淆SHE和C++ 異常處理。C++ 異常處理再形式上表現為使用關鍵字catchthrow,這個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塊可能會因為returngoto,異常等非自然退出,也可能會因為成功執行而自然退出。但不論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調用WordVBA的時候,需要層層獲取對象、判斷對象是否獲取成功、執行相關操作、再釋放對象,一個流程下來,本來一兩行的VBA代碼,C++ 寫出來就要好幾十行(這還得看操作的是幾個什麽對象)。

下面就來一個方法讓大家看看,為什麽有些人喜歡腳本語言而不喜歡C++的原因吧。

為了更有邏輯,更有層次地操作 OfficeMicrosoft 把應用(Application)按邏輯功能劃分為如下的樹形結構

Application(WORD 為例,只列出一部分)
  Documents(所有的文檔)
        Document(一個文檔)
            ......
  Templates(所有模板)
        Template(一個模板)
            ......
  Windows(所有窗口)
        Window
        Selection
        View
        .....
  Selection(編輯對象)
        Font
        Style
        Range
        ......
  ......

只有了解了邏輯層次,我們才能正確的操縱 Office。舉例來講,如果給出一個VBA語句是:

Application.ActiveDocument.SaveAs "c:\abc.doc"

那麽,我們就知道了,這個操作的過程是:

  1. 第一步,取得Application
  2. 第二步,從Application中取得ActiveDocument
  3. 第三步,調用 Document 的函數 SaveAs,參數是一個字符串型的文件名。

這只是一個最簡單的的VBA代碼了。來個稍微復雜點的如下,在選中處,插入一個書簽:

 ActiveDocument.Bookmarks.Add Range:=Selection.Range, Name:="iceman"

此處流程如下:

  1. 獲取Application
  2. 獲取ActiveDocument
  3. 獲取Selection
  4. 獲取Range
  5. 獲取Bookmarks
  6. 調用方法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塊。
對於上例中的InsertBookmarInWord3try塊中的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後又不能同時有finallyexcept塊,也不能同時有多個finnalyexcept塊,但是可以相互嵌套使用


異常處理基本流程

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 //捕獲異常,

Func3try塊只是一個簡單操作,故不會導致異常,所以except塊中代碼不會被執行,Func4try塊視圖用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());
}
```

Windows結構化異常處理淺析