第24章 SEH結構化異常處理_異常處理及軟件異常


24.1  程序的結構

(1)try/except框架

__try{

   //被保護的代碼塊
       ……
}

__except(except fileter/*異常過濾程序*/){
    //異常處理程序
}

(2)說明

  ①當__try塊中的代碼發生異常時,__except()中的過濾程序就被調用。

  ②過濾程序可以是一個簡單的表達式或一個函數(返回值應為EXCEPTION_CONTINUE_SEARCH、EXCEPT_CONTINUE_EXECUTE或EXCEPT_EXECUTE_HANDLER之一)

  ③過濾表達式中可以調用GetExceptionCode和GetExceptionInformation函數取得正在處理的異常信息。但這兩個函數不能在異常處理程序中使用。

  ④與try/finally不同,try/except中可以使用return、goto、continue和break,它們並不會導致局部展開。

24.2 異常過濾程序

(1)返回值

標識

說明

EXCEPTION_EXECUTE_HANDLER

1

執行except花括號內代碼,同時執行全局展開。最后程序從except花括號后面的第一句代碼繼續運行。

EXCEPTION_CONTINUE_SEARCH

0

向外層查找帶except的try塊,並調用對應的異常過濾程序。

EXCEPTION_CONTINUE_EXECUTE

-1

重新執行導致異常的那條CPU指令本身。

(2)全局展開——異常過濾程序返回EXCEPTION_EXECUTE_HANDLER是會執行全局展開

  ①當某個__try塊中的代碼觸發了異常時(也可能是__try塊中調用的函數中引發異常),操作系統會從最靠近引發異常代碼的地方開始從下層往上層查找__except塊(這里的層是指__try塊的嵌套層),對於找到的每一個__except塊,會先計算它的異常過濾器,如果過濾器返回EXCEPTION_CONTINUE_SEARCH,則說明此__except塊不處理此類異常,需要繼續往上層查找,如果某過濾器返回EXCEPTION_EXECUTE_HANDLER則說明此__except塊可以處理此類異常,即找到了異常的處理代碼,此時停止查找,但是在執行該__except塊中的異常處理代碼之前,要先進行全局展開。

  ②全局展開的過程與查找__except塊的過程類似,只不過這次是查找從底層向上查找__finally塊,查找過程中遇到的每一個__finally塊中的代碼都被執行,直到查找到前面說的處理異常的__except塊那一層停止,這時全局展開完成,然后執行__except塊中的異常處理代碼。

  ③執行完異常處理代碼之后,指令流從__except塊后的第一條指令開始。從這里也可以看出全局展開也是為了保證__finally語義的正確性,因為指令流從引發異常代碼轉到到__except異常處理代碼時也導致了指令流從__try塊嵌套層中所有與__finally對應的__try塊中流出,由前面的__finally語義說明可知,此時必須要執行全局展開過程以包成__finally語義的正確性。

(3)停止全局展開——將return置於finally塊中可阻止全局展開。【未定義行為,VC2013直接報錯了!】

(4)慎用EXCEPTION_CONTINUE_EXECUTION

  ①嘗試修復錯誤,出現失敗的實例分析

  *pchBuffer = TEXT("J");//C/C++語句

  //編譯后的產生的機器指令
  MOV EAX,DWORD PTR[pchBuffer]
  MOV WORD PTR[EAX], 'J'  //導致異常的指令。當異常過濾程序捕獲該異常后,修正
                          //pchBuffer,讓其指向一個正確的地址。並讓系統重新
                          //執行第二要CPU指令。問題在於寄存器不可能自動更新
                          //以反映變量pchBuffer的更新,於是該異常又致另一個導
                          //異常,程序陷入了死循環

  ②虛擬內存結合SEH可實現按需調撥存儲器,有時能寫出運行速度快和高效的應用程序(見第15章的《如何預訂大塊地址空間和為地址空間稀疏調撥存儲器》

  ③系統為線程棧建了一個SEH框。當線程試圖訪問棧中尚未調撥存儲器的區域時,會引發一個異常。系統內部的異常過濾程序會捕獲到該異常並在內部調用VirtualAlloc為線程棧調撥更多的存儲,並且返回EXCEPTION_CONTINUE_EXECUTION讓原先拋出異常的指令重新執行下去。

【SEHAndMemory】演示虛擬內存的按需調撥

#include <windows.h>
#include <tchar.h>
#include <locale.h>

#define PAGELIMIT 80
LPBYTE lpNxtPage;
DWORD dwPages = 0;
DWORD dwPageSize;//頁面大小,一般為4KB

INT PageFualtExceptionFilter(DWORD dwCode){
    LPVOID lpvResult;

    //不是非法訪問內存
    if (dwCode !=EXCEPTION_ACCESS_VIOLATION){
        return EXCEPTION_EXECUTE_HANDLER;//執行except塊的異常處理程序代碼
    }

    //當超過指定的頁面數時
    if (dwPages >=PAGELIMIT){
        return EXCEPTION_EXECUTE_HANDLER;//執行except塊的異常處理程序代碼
    }

    //非法訪問內存,則為預訂的空間提交下一頁物理存儲器
    lpvResult = VirtualAlloc((LPVOID)lpNxtPage, dwPageSize, MEM_COMMIT, PAGE_READWRITE);
    if (lpvResult == NULL){
        return EXCEPTION_EXECUTE_HANDLER;//執行except塊的異常處理程序代碼
    }

    //提交成功
    dwPages++;
    lpNxtPage += dwPageSize;

    _tprintf(_T("第%d頁提交成功!\n"), dwPages);
    return EXCEPTION_CONTINUE_EXECUTION; //重新執行觸發異常的那條CPU指令
}

int main(){
    _tsetlocale(LC_ALL, _T("chs"));

    LPVOID lpvBase;LPTSTR lpPtr;BOOL bSuccess;
    SYSTEM_INFO sSysInfo;
    GetSystemInfo(&sSysInfo);
    dwPageSize = sSysInfo.dwPageSize;

    _tprintf(_T("CPU頁面大小為%dKB.\n"), sSysInfo.dwPageSize / 1024);

    //預訂存儲器
    lpvBase = VirtualAlloc(NULL, PAGELIMIT*dwPageSize, MEM_RESERVE, PAGE_NOACCESS);

    lpPtr = (LPTSTR)(lpNxtPage = (LPBYTE)lpvBase);
    for (DWORD i = 0; i < PAGELIMIT*dwPageSize/sizeof(TCHAR);i++){
        __try{
            lpPtr[i] = _T('a');//寫入一個字節的數據
        }
        __except (PageFualtExceptionFilter(GetExceptionCode())){
            _tprintf(_T("異常被處理\n"));
            //ExitProcess(GetLastError());
        }
    }

    bSuccess = VirtualFree(lpvBase, 0, MEM_RELEASE);
    _tprintf(_T("釋放操作%s.\n"), bSuccess ? _T("成功") : _T("失敗"));
    _tsystem(_T("PAUSE"));
    return 0;
}

24.3 GetExceptionCode

(1)GetExceptionCode是個內聯函數,其代碼直接嵌入到被調用的地方(注意與函數調用的區別),它的返回值表明剛剛發生的異常的類型(定義在WinBase.h中,如EXCEPTION_ACCESS_VIOLATION)

(2)該函數只能在異常過濾程序里(即__except之后的小括號內)或者異常處理程序的代碼里調用(__except塊后面的花括號內),但不能在異常過濾函數中使用。

//合法代碼

__try{

     y = 0;

     x = / y;

}

__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?

       EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH){ //在__except塊的小括內使用,合法

     switch (GetExceptionCode()){ //__except塊的花括中使用,合法!

      ......

     }

}

//非法代碼

LONG MyFilter(void){}

{

     //在異常過濾函數中使用GetExceptionCode,不合法!

     return ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?

         EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

 

__try{

     y = 0;

     x = 4 / y;

}

__except(MyFilter()){ //可改成將GetExceptionCode作為參數傳給MyFilter的形式。

    //處理異常

}

(3)異常錯誤代碼的規則

31-30

29

28

27-16

15-0

內容

嚴重性

Microsoft/

Customer

保留位

設備代碼

異常代碼

含義

0=Success

1=Informational

2=Warning

3=error

0=Mircosoft所定義的代碼

1=Customer所定義的代碼

一直為0

前256個值為Micorsoft所保留。(如FACILITY_NULL(0)表示該異常可以在系統任何設備出現,並不只發生在一些特定的設備上)

由Microsoft/

Customer所定義的代碼

24.4 GetExceptionInformation

(1)GetExceptionInformation可獲取異常發生時,系統向發生異常的線程棧中壓入的EXCEPTION_RECORD、CONTEXT和EXCEPTION_POINTERS結構中的異常信息或CPU有關的信息

(2)這個函數只能在異常過濾程序中調用(即__except塊的小括號),因為EXCEPT_RECORD、CONTEXT和EXCEPTION_POINTER數據結構只有在系統計算異常過濾程序時才有效。一旦控制流被轉移到其他地方,這些棧上的數據結構會被銷毀。但我們可以自己保存他們,以備后用。

//保存棧中異常信息的方法
void FuncSkunk(){
    //聲明一些可以保存異常信息的結構體,須在try塊外面聲明
    EXCEPTION_RECORD SavedExceptRec;
    CONTEXT          SavedContext;
 
    __try{
 
    }
    __except ( //注意逗號表達式,取最后一個表達式為整個表達式的值。
        SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord,
        SavedContext = *(GetExceptionInformation())->ContextRecord,
        EXCEPTION_EXECUTE_HANDLER){
        //異常處理
    }
}

(3)EXCEPTION_RECORD結構體——剛發生的異常的詳細信息

字段

說明

DWORD ExceptionCode

異常代碼,就是GetExceptionCode函數的返回值

DWORD ExceptionFlags

異常標志

0—表示繼續的異常;EXCEPTION_NONCONTINUABLE—不可繼續的異常,如果程序試圖在一個不可繼續的異常之后繼續執行,會引發EXCEPTION_NONCONTINUABLE_EXCEPTION異常。

PEXCEPTION_RECORD pExceptionRecord

指向另一個未處理異常的EXCEPTION_RECORD結構。(即嵌套異常發生時,異常會形成異常鏈)

PVOID ExceptionAddress

導致異常的CPU指令的地址

DWORD NumberParameters

ExceptionInformation數組里元素的個數。對絕大部分的異常來說,這個值為0。

ULONG_PTR ExceptionInformation

[EXCEPTION_MAXIMUM_PARAMETERS]

描述異常的附加參數數組,對絕大部分的異常來說,這個數組元素都未定義。

24.5 軟件異常——RaiseException函數

參數

說明

DWORD dwExceptionCode

要拋出異常的標識符,可參考《異常錯誤代碼規則》來編寫

DWORD dwExceptionFlags

必須下列兩者之一

0:

EXCEPTION_NONCONTINUABLE:異常不可繼續,即不能再異常過濾程序中返回EXCEPTION_CONTINUE_EXECUTE,否則重新執行那條導致錯誤的CPU指令會繼續拋出一個新的EXCEPTION_NONCONTINUABLE_EXCEPTION異常。

DWORD nNumberOfArguments

用來傳遞有關拋出異常的附加信息。一般不需要。可將nNumberOfArgument設為0。pArguments設為NULL。

Const ULONG_PTR* pArguments

返回值

void

 【RaiseException程序】——演示自己拋出的軟件異常

#include <tchar.h>
#include <windows.h>

DWORD FilterFunction(){
    _tprintf(_T("1")); //第1句被輸出的語句
    return EXCEPTION_EXECUTE_HANDLER;
}

int main(){
    __try{
        __try{
            RaiseException(1, 0, 0, NULL);
        }
        __finally{
            _tprintf(_T("2")); //第2句被輸出的語句
        }
    }
    __except (FilterFunction()){
        _tprintf(_T("3\n")); //第3句被輸出的語句
    }
    _tsystem(_T("PAUSE"));
return 0; }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM