异常处理

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抛出的临时对象。

 

示例中分析的代码并使用类作为异常,而是使用了简单的数字作为异常抛出并捕获,其流程与类大致相同。分析过程相似。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值