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抛出的临时对象。
示例中分析的代码并使用类作为异常,而是使用了简单的数字作为异常抛出并捕获,其流程与类大致相同。分析过程相似。