C++標準中只規定了異常處理的語法,各編譯器廠商也都予以實現。但由於C++標準中並沒有規定異常處理的實現過程,造成了不同廠商的編譯器,編譯後產生的異常處理代碼也不相同。本教程中一直使用微軟C++編譯器系列中的Microsoft Visual C++ 6.0,因此本章也不例外,依然使用VC6.0用於調試講解。
C++中異常處理機制由try、throw、catch語句組成。
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,每次操作後,si、di遞增
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,這個函數由是如何被調用呢?代碼清單1中throw編譯後,被轉換成__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中函數__CxxFrameHandler中4個參數的信息,調試分析看着棧中數據如圖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_8爲0
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] ; 參考圖3,edx中保存數據爲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] ; 獲取參數4到ecx,ecx中保存地址0x0012FBB0
00403291 cmp dword ptr [ecx+0Ch], 0 ; 比較參數3是否爲0,顯然不爲0
00403295 jz short loc_403316 ; 此跳轉執行失敗
=============================標誌判斷處===============================
00403297 mov edx, [ebp+lp] ; 獲取lp首4字節數據到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中,eax爲0
004032F1 push eax ; 壓入struct EHRegistrationNode *空指針
004032F2 mov ecx, [ebp+arg_14] ; 獲取參數arg_14爲0值入棧
004032F5 push ecx
004032F6 mov dl, byte ptr [ebp+arg_1C] ; 獲取參數arg_1C爲0值入棧
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:存儲異常處理的附加參數
在這個結構中,重點考察ExceptionCode與ExceptionAddress,它們指定了最重要的異常信息。有了這兩項即可得到產生異常的原因,以及異常代碼所在地址。對應圖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數組元素個數,CxxTypeInfo是CXX_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。當壓入異常處理的回調函數後,棧中結構將發生改變。在函數返回地址與局部變量的中間部分將會插入異常回調函數註冊信息。此時try、catch信息已經登記。
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拋出的臨時對象。
示例中分析的代碼並使用類作爲異常,而是使用了簡單的數字作爲異常拋出並捕獲,其流程與類大致相同。分析過程相似。