異常處理

C++標準中只規定了異常處理的語法,各編譯器廠商也都予以實現。但由於C++標準中並沒有規定異常處理的實現過程,造成了不同廠商的編譯器,編譯後產生的異常處理代碼也不相同。本教程中一直使用微軟C++編譯器系列中的Microsoft Visual C++ 6.0,因此本章也不例外,依然使用VC6.0用於調試講解。

       C++中異常處理機制由trythrowcatch語句組成。

q        try語句塊中負責監視異常。

q        throw用於異常信息的發送,也稱之爲拋異常。

q        catch用於異常的捕獲,做出響應的處理。

 

VC下使用異常處理很方便,只要將以上三個步驟套用即可。但程序的運行過程中出現錯誤,編譯器是如何得知,並找到對應的處理方法?這些對於用於而言全都不得而知,通過本章的分析學習,瞭解異常處理的實現原理。先從最簡單的異常捕獲分析。見代碼清單1

 

代碼清單1     異常處理流程——Debug調試版

// C++源碼說明:製造最簡單的除0異常

int main(int argc, char* argv[]){

       try{

              throw 0;                                     // 拋出異常

       }

       catch(...){

              printf("異常觸發.../r/n");         // 異常除法後執行此處代碼

       }

       return 0;

}

// C++源碼與對應彙編代碼講解

int main(int argc, char* argv[]){

00401010     push        ebp

00401011     mov        ebp,esp

00401013     push      0FFh

       ; 異常處理函數,__ehhandler$_main函數分析見代碼清單2

00401015     push     offset __ehhandler$_main (00413440) 

0040101A   mov      eax,fs:[00000000]        

00401020     push     eax

00401021     mov      dword ptr fs:[0],esp              ; 註冊異常回調處理函數

; Debug環境初始化部分略

try{

00401041     mov      dword ptr [ebp-4],0

              throw 1;         // 拋出異常

00401048     mov    dword ptr [ebp-14h],1          ; 設置異常編號

0040104F     push      offset __TI1H (00426628)     ; 壓入異常結構

00401054     lea        eax,[ebp-14h]

00401057     push     eax                                     ; 壓入異常編號

       ; __CxxThrowException@8 函數講解見代碼清單3

00401058     call      __CxxThrowException@8 (00401790) ; 調用異常分配函數

       }

catch(...){

       printf("異常觸發.../r/n");     

0040105D   push      offset string "/xd2/xec/xb3/xa3/xb4/xa5/xb7/xa2.../r/n" (0042501c)

00401062     call       printf (00401200)

00401067     add      esp,4

       }

0040106A   mov      eax,offset __tryend$_main$1 (00401070)

0040106F     ret

return 0;

00401070     mov      dword ptr [ebp-4],0FFFFFFFFh

00401077     xor        eax,eax

}

00401079   mov         ecx,dword ptr [ebp-0Ch]

0040107C   mov         dword ptr fs:[0],ecx

       ; Debug還原環境部分略

00401091     ret

       代碼清單1中,首先壓入異常回調函數,用於產生異常是,接收並分配到對應的異常處理語句塊中。函數__ehhandler$_main便是異常處理的關鍵實現處,見代碼2分析。

 

代碼清單2     異常處理函數__ehhandler$_main分析——Debug調試版

=======================示例講解代碼截取自IDA==========================

00413140 __ehhandler$_main proc near             ; DATA XREF: _main+5o

00413140                 mov     eax, offset stru_426468     ; 利用eax傳參

00413145                 jmp     ___CxxFrameHandler

00413145 __ehhandler$_main endp

標號stru_426468處信息如下:

00426468      dword_426468     dd   19930520h           ; 編譯器生成標識數據

0042646C                      dd   2                                 ; 功能狀態標識2

00426470                      dd   offset stru_426640         ; 函數列表首地址

00426474                      dd   1                                 ; try語句塊1

00426478                      dd   offset stru_426670         ; 對應列表首地址

 

___CxxFrameHandler  ,實現如下:

00401210 ; int __cdecl __CxxFrameHandler(void *lp, struct EHRegistrationNode *, int, int)

00401210      var_8      = dword ptr   -8          

00401210      var_4     = dword ptr   -4          

00401210      lp        = dword ptr  8            ; 參數1

00401210      arg_4     = dword ptr  0Ch        ; 參數2

00401210      arg_8      = dword ptr  10h         ; 參數3

00401210       arg_C      = dword ptr  14h         ; 參數4

00401210

00401210       push      ebp

00401211       mov      ebp, esp                        ; 保存棧底,並重新設置棧底

00401213      sub     esp, 8                           ; 申請局部變量空間

00401216      push    ebx

00401217      push    esi

00401218      push    edi                               ; 保存環境

00401219      cld                                             ; DF位置0,每次操作後,sidi遞增

0040121A      mov       [ebp+var_8], eax           ; var_8局部變量保存stru_426468首地址

0040121D      push    0                      ; 壓入參數0作爲參數

0040121F     push    0                      ; 壓入參數0作爲參數

00401221      push    0                      ; 壓入參數0作爲參數

00401223      mov        eax, [ebp+var_8]

00401226     push    eax                     ; 壓入stru_426468結構首地址作爲參數

00401227       mov      ecx, [ebp+arg_C]

0040122A     push    ecx                     ; 壓入參數4作爲參數

0040122B      mov       edx, [ebp+arg_8]

0040122E       push    edx                           ; 壓入參數3作爲參數

0040122F     mov       eax, [ebp+arg_4]

00401232      push    eax                     ; 壓入參數2作爲參數

00401233      mov      ecx, [ebp+lp]

00401236     push    ecx                     ; 壓入參數1作爲參數

00401237     call      ___InternalCxxFrameHandler       ; 調用異常處理函數

0040123C     add            esp, 20h

0040123F      mov      [ebp+var_4], eax

00401242    pop       edi

00401243     pop        esi

00401244     pop       ebx

00401245     mov        eax, [ebp+var_4]

00401248      mov      esp, ebp

0040124A   pop       ebp

0040124B     retn

0040124B ___CxxFrameHandler endp

       代碼清單2異常回調函數實現部分,函數__CxxFrameHandler內實際是一箇中轉工作,並沒有真正意義上的完成異常派發。在其中調用了函數___InternalCxxFrameHandler,那麼是不是就是由它來完成的異常派發工作呢?先彆着急跟蹤分析,這個函數傳遞的參數較多,並且之前使用eax傳遞的結構體指針也沒有搞清楚含義。在不瞭解敵情的情況下盲目的跟進將會迷失在代碼的海洋中。

       那麼如何得知這些未知數據呢?依靠IDA強大的分析功能已經有了眉目。首先標號stru_426468被作爲函數__InternalCxxFrameHandler的一個參數傳遞,只需根據IDA查看其聲明即可得知結構類型,__InternalCxxFrameHandler函數聲明如下:

int __cdecl __InternalCxxFrameHandler(void *lp, struct EHRegistrationNode *, int, int, struct _s_FuncInfo *, int, struct EHRegistrationNode *, int)

       根據代碼清單2中的分析,標號地址stru_426468是作爲第5個參數被傳遞,對照以上代碼得出其結構類型爲_s_FuncInfo。結構說明如下:

struct _s_FuncInfo {

    DWORD MagicNumber;      // 編譯器生成標記固定數字0x19930520

    DWORD MaxState;         // 最大的功能狀態,棧展開數

    DWORD PUnwindMap;       // 函數列表

    DWORD NTryBlocks;       // try塊數量 

    DWORD PTryBlockMap;     // try塊列表

};

       PTryBlockMap是一個指向TRY_INFO結構的指針變量,在TRY_INFO結構中描述了try對應的catch塊信息,其結構定義如下:

TRY_INFO struc   

StartLevel             dd ?        ; try其實地址

EndLevel              dd ?        ; try終止地址

CatchLevel            dd ?

dwCatchCount       dd ?       ; catch塊的個數

pCatch                  dd ?      ; 指向CATCH_INFO類型的指針

field_14                dd ?

TRY_INFO ends

       CATCH_INFO結構中描述了catch塊中的具體信息,如捕捉類型,以及catch塊所對應的代碼地址等信息。其結構定義如下:

CATCH_INFO struc

Flag dd ?

pTypeInfo dd ?      ; catch塊要捕捉的類型

Offset dd ?            ; 用來複制異常對象的棧

CatchProc dd ?       ; catch塊的處理代碼

CATCH_INFO ends

這裏的pTypeInfo異常類型與

由此搞清楚了stru_426468結構中各參數的定義。代碼清單1中只是註冊了異常處理函數__ehhandler$_main,這個函數由是如何被調用呢?代碼清單1throw編譯後,被轉換成__CxxThrowException@8,在此函數中通過調用RaiseException來完成異常的派發,最終觸發異常處理函數__ehhandler$_main。調試過程中,在異常處理函數__ehhandler$_main設置斷點,等待異常觸發。當觸發斷點後,查看stru_426468結構中信息如圖1

1 標號stru_426468對應結構信息

 

       依照圖1中顯示,對應結構_s_FuncInfo信息如下:

q        MagicNumber由編譯生成的固定數字0x19930520

q        MaxState最大功能狀態爲2,表示棧展開數爲2

q        PUnwindMap函數列表首地址0x00426660

q        try塊數爲1,對照代碼清單1,其中只編寫了一個try塊。

q        try表地址爲0x00426670

 

有了stru_426468對應的數據,還缺少代碼清單2中函數__CxxFrameHandler4個參數的信息,調試分析看着棧中數據如圖2所示。

2 __CxxFrameHandler參數信息

 

根據圖2所示,此時棧頂ESP指向0x0012FAA8,對應內存窗口中查看數據信息如下:

q        地址0x7C9232A8爲函數返回地址,此地址爲系統使用地址。

q        參數void *lp保存信息爲0x0012FB90,對應數據見圖3

q        參數struct EHRegistrationNode * 保存信息爲0x0012FF74

q        參數3中保存數據0x0012FBB0

q        參數4中保存數據0x0012FF64

3 lp指向地址數據

 

到此,出現了許多未知數據,那麼這些數據它們都有着那些含義,又對應着怎樣的結構呢?根據分析它們的使用函數___InternalCxxFrameHandler得知,這個函數纔是真正的異常處理函數。見代碼清單3

 

代碼清單3     __InternalCxxFrameHandler分析——Debug調試版

; 反彙編代碼來源自IDA中截取

int __cdecl __InternalCxxFrameHandler(void *lp, struct EHRegistrationNode *, int, int, struct _s_FuncInfo *, int, struct EHRegistrationNode *, int)

       ; 函數內局部變量以及參數標記符定義

00403230      var_8   = dword ptr -8

00403230      var_4   = dword ptr -4

00403230      lp          = dword ptr  8             ; 參數標識符定義void *lp = 0x0012FB90

00403230      arg_4   = dword ptr  0Ch         ; struct EHRegistrationNode * = 0x0012FF74

00403230      arg_8   = dword ptr  10h         ; int = 0x0012FBB0 一個棧地址

00403230      arg_C      = dword ptr  14h         ; int = 0x0012FF64 一個棧地址

00403230      arg_10  = dword ptr  18h         ; struct _s_FuncInfo * 對應標號stru_426468

00403230      arg_14    = dword ptr  1Ch         ; int = 0

00403230      arg_18  = dword ptr  20h         ; struct EHRegistrationNode * = 0

00403230      arg_1C   = dword ptr  24h         ; int = 0

============================函數實現部分==============================

00403230     push      ebp

00403231     mov      ebp, esp

00403233      sub     esp, 8                    ; 申請局部變量棧空間

00403236      mov      eax, [ebp+arg_10]

=============================標誌判斷處===============================

00403239      cmp      dword ptr [eax], 19930520h  ; 對比標識符

0040323F     jnz     short loc_40324A   ; 對比代碼清單2中標識符數據,跳轉不成立

00403241      mov     [ebp+var_8], 0      ; 設置局部變量var_80

00403248     jmp     short loc_403252   ; 跳轉到標號loc_403252處,處理異常

loc_4035CA:

0040324A      call      ?_inconsistency@@YAXXZ ; _inconsistency(void)

0040324F     db         89h

00403250      db         45h

00403251      db           0F8h

loc_403252:                              ; 程序流程執行到此處

00403252      mov       ecx, [ebp+lp]         ; lp參數傳入ecx中,ecx = 0x0012FB90

=============================標誌判斷處===============================

00403255       mov       edx, [ecx+4]          ; 參考圖3edx中保存數據爲1

       ; 66h做位於運算,edx中結果爲0,像是在做標記檢查

00403258      and     edx, 66h               

0040325B     test      edx, edx                ; 檢測edx等於0跳轉到標號loc_40328E

0040325D     jz      short loc_40328E  ; 跳轉執行成功

0040325F     mov      eax, [ebp+arg_10]

00403262      cmp       dword ptr [eax+4], 0

00403266      jz      short loc_403284

00403268      cmp       [ebp+arg_14], 0

0040326C     jnz     short loc_403284

0040326E     push    0FFFFFFFFh

00403270      mov      ecx, [ebp+arg_10]

00403273      push    ecx

00403274      mov      edx, [ebp+arg_C]

00403277      push    edx

00403278      mov      eax, [ebp+arg_4]

0040327B     push    eax

0040327C     call      ___FrameUnwindToState

00403281     add        esp, 10h

00403284

loc_403284:                                     

00403284     mov      eax, 1

00403289      jmp      loc_40331B           ; 結束異常處理

loc_40328E:                                      ; 程序流程執行到此處                                

=============================標誌判斷處===============================

0040328E     mov     ecx, [ebp+arg_10] ; 獲取參數4ecxecx中保存地址0x0012FBB0

00403291     cmp     dword ptr [ecx+0Ch], 0 ; 比較參數3是否爲0,顯然不爲0

00403295      jz        short loc_403316    ; 此跳轉執行失敗

=============================標誌判斷處===============================

00403297      mov    edx, [ebp+lp]         ; 獲取lp4字節數據到edx中,0xE06D7363

0040329A     cmp     dword ptr [edx], 0E06D7363h ; 此處類似標識檢查

004032A0     jnz     short loc_4032EE   ; 跳轉失敗

=============================標誌判斷處===============================

004032A2     mov       eax, [ebp+lp]         ;傳入eax中數據爲lp指向地址0x 0012FB90

004032A5   cmp     dword ptr [eax+14h], 19930520h ; 查看圖3,獲取eax+14h數據

004032AC    jbe     short loc_4032EE  ; 跳轉成立,流程執行到標號loc_4032EE

004032AE   mov      ecx, [ebp+lp]         ; 繼續檢查工作

004032B1      mov      edx, [ecx+1Ch]

004032B4       mov       eax, [edx+8]

004032B7     mov      [ebp+var_4], eax

004032BA     cmp       [ebp+var_4], 0

004032BE     jz      short loc_4032EE

004032C0   mov      ecx, [ebp+arg_1C]

004032C3     and            ecx, 0FFh

004032C9     push    ecx

004032CA     mov      edx, [ebp+arg_18]

004032CD    push    edx

004032CE     mov      eax, [ebp+arg_14]

004032D1   push    eax

004032D2     mov      ecx, [ebp+arg_10]

004032D5     push    ecx

004032D6      mov      edx, [ebp+arg_C]

004032D9     push    edx

004032DA   mov      eax, [ebp+arg_8]

004032DD     push    eax

004032DE    mov      ecx, [ebp+arg_4]

004032E1     push    ecx

004032E2      mov      edx, [ebp+lp]

004032E5     push    edx

004032E6   call      [ebp+var_4]

004032E9     add            esp, 20h

004032EC   jmp      short loc_40331B

loc_4032EE:                       ; 程序流程執行到此處    

004032EE     mov      eax, [ebp+arg_18]  ; 將參數arg_18中數據傳入eax中,eax0

004032F1     push    eax              ; 壓入struct EHRegistrationNode *空指針

004032F2      mov      ecx, [ebp+arg_14]  ; 獲取參數arg_140值入棧

004032F5     push    ecx           

004032F6     mov      dl, byte ptr [ebp+arg_1C]      ; 獲取參數arg_1C0值入棧

004032F9     push    edx           

004032FA     mov    eax, [ebp+arg_10]  ; 標號stru_426468對應地址入棧

004032FD     push    eax          

004032FE     mov      ecx, [ebp+arg_C]   ; 將地址0x0012FF64入棧

00403301     push    ecx             

00403302      mov      edx, [ebp+arg_8]    ; 將地址0x0012FBB0入棧

00403305     push    edx            

00403306      mov      eax, [ebp+arg_4]    ; 將地址0x0012FF74入棧

00403309     push    eax              ; struct EHRegistrationNode *

0040330A      mov      ecx, [ebp+lp]         ; 獲取lp地址到ecx,入棧

0040330D   push    ecx         

       ; 通過此函數找到對應異常處理函數,FindHandler

0040330E     call      ___InternalCxxFrameHandler  

00403313       add            esp, 20h

00403316      mov      eax, 1

loc_40331B:                       ; 函數結尾處

0040331B     mov      esp, ebp

0040331D   pop     ebp

0040331E    retn

0040331E ___InternalCxxFrameHandler endp

       通過分析代碼清單3,結合已知數據,在__InternalCxxFrameHandler函數中並沒有真正的處理異常,而是進行某些標記的判斷,最終有函數FindHandler來完成異常處理。

FindHandler函數說明:(函數原型、實現過程查看frame_ce.cpp

FindHandler(EHExceptionRecord*,EHRegistrationNode*,_CONTEXT*,void *,_s_FuncInfo const *, uchar, int, EHRegistrationNode *)

       FindHandler函數的聲明中可以得知各傳遞參數的類型,由此揭開了之前分析過的各個未知參數類型。對應結果如下:

q        參數1對應類型:EHExceptionRecord*,保存數據0x0012FB90

q        參數2對應類型:EHRegistrationNode*,保存數據0x0012FF74

q        參數3對應類型:_CONTEXT*,保存數據0x0012FBB0

q        參數4對應類型:void *,保存數據0x0012FF64

q        參數5對應類型:_s_FuncInfo const *,保存數據0x00426040

q        其餘參數均傳遞0

 

EHExceptionRecord 結構定義如下:(結構體定義查看winnt.h

typedef struct _EXCEPTION_RECORD {

    DWORD       ExceptionCode;

    DWORD       ExceptionFlags;

    struct      _EXCEPTION_RECORD *ExceptionRecord;

    PVOID       ExceptionAddress;

    DWORD       NumberParameters;

    ULONG_PTR   ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD;

各成員功能說明:(詳細解釋可查看MSDN幫助文檔)

q        ExceptionCode:異常類型,產生異常的錯誤編號

q        ExceptionFlags:異常標記

q        ExceptionRecord:用於嵌套異常使用

q        ExceptionAddress:異常產生地址

q        NumberParameters:用於指定ExceptionInformation數組中的元素個數

q        ExceptionInformation:存儲異常處理的附加參數

   

在這個結構中,重點考察ExceptionCodeExceptionAddress,它們指定了最重要的異常信息。有了這兩項即可得到產生異常的原因,以及異常代碼所在地址。對應圖3解析異常原因如下:

ExceptionCode:異常類型爲0xE06D7363

ExceptionAddress:異常產生地址0x7C812AFB

   

在代碼清單3中“cmp      dword ptr [edx], 0E06D7363h”的檢測,是在判斷是否爲C++異常。EHRegistrationNode結構信息如圖4所示:

4 EHRegistrationNode結構信息

 

EHRegistrationNode結構有三個成員,各佔四個字節數據。第一項表示每個異常註冊節點信息。第二項表示異常註冊函數首地址爲0x00413440,對比代碼清單1中壓入棧中的異常註冊函數__ehhandler$_main。第三項表示當前異常狀態爲0表示未處理。

有了這些數據,在FindHandler函數中便會根據提供信息找到try塊對應的catch語句塊。那麼try信息是在合適傳入的呢?回顧代碼清單1 throw 1處,壓入了兩個參數,分別爲offset __TI1H (00426628),與異常類型1

       __TI1H (00426628)是用來說明異常類型的一個結構體。CXX_EXCEPTTION_TYPE,其說明如下:

CXX_EXCEPTTION_TYPE struc

Flag                  dd ?                    

pDestructor       dd ?              ; 用於記錄異常對象的析構函數首地址

field_8              dd ?

pTypeInfoTable dd ?              ; 類型列表

CXX_EXCEPTTION_TYPE ends

其中pTypeInfoTable是一個指向CXX_TYPE_INFO_TABLE結構的指針變量,該結構類型定義如下:

CXX_TYPE_INFO_TABLE struc

dwCount               dd ?                            ; CxxTypeInfo數組包含的個數

CxxTypeInfo CXX_TYPE_INFO 3 dup(?) ; 類型信息,可變長度

CXX_TYPE_INFO_TABLE ends

成員dwCount用於說明CxxTypeInfo數組元素個數,CxxTypeInfoCXX_TYPE_INFO結構類型的變長數組,CXX_TYPE_INFO結構說明如下:

CXX_TYPE_INFO struc

Flag dd ?                    ; 標誌

pTypeInfo dd ?             ;  C++的類型信息

dwThisPtrOffset dd ?    ; 基類的this指針偏移

dwVbaseDescr dd ?       ; 虛基類的描述

dwVbaseOffset dd ?      ; 虛基類的this指針偏移

dwSize dd ?                 ; 類的大小

pCopyCtor dd ?            ; 拷貝構造

CXX_TYPE_INFO ends

    根據以上代碼分析,總結C++異常處理流程如下:

q        在函數入口處,通過棧方式壓入異常處理的回調函數,如代碼清單1__ehhandler$_main。當壓入異常處理的回調函數後,棧中結構將發生改變。在函數返回地址與局部變量的中間部分將會插入異常回調函數註冊信息。此時trycatch信息已經登記。

q        throw轉換爲__CxxThrowException,調用異常回調函數。其中傳遞了兩個參數,第一個記錄了throw對象的首地址,或異常類型數值。第二個參數中則記錄了異常結構的相關信息。通過調用__CxxThrowException最終執行到異常回調函數中。

q        在異常回調函數中,進行相關檢查,根據之前傳入數據的記錄,一一檢查,根據異常類型,調用到對應的catch塊中。

q        如果當前異常發生try塊中,則取得try塊中的TRY_INFO結構,在TRY_INFO結構中的成員pCatch,該成員指向了CATCH_INFO結構,在此結構中記錄了指定 try 塊配套出現的所有 catch 塊相關信息,包括這個 catch 塊所能捕獲的異常類型以及起始地址等信息。通過遍歷,找到有效的catch塊。

q        在異常被拋出、捕獲後,將所有生命期已結束的對象正確地析構,將它們所佔用的空間正確地回收。

q        跳轉到以匹配的 catch 塊中,複製當前異常信息到 catch 塊,用於異常類型檢查。執行catch塊中代碼。

q        catch塊執行完畢後,析構throw拋出的臨時對象。

 

示例中分析的代碼並使用類作爲異常,而是使用了簡單的數字作爲異常拋出並捕獲,其流程與類大致相同。分析過程相似。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章