C与C++中的异常处理4

类别:VC语言 点击:0 评论:0 推荐:
1.     实例剖析EH

    到现在为止,我仍然逗留在C和C++的范围内,但这次要稍微涉及一下汇编语言。目标:初步揭示Visual C++对EH的throw和catch的实现。本文不是巨细无遗的,毕竟我的原则是只关注(C/C++)语言本身。然而,简单的揭示EH的实现对理解和信任EH大有帮助。

1.1     我们所害怕的唯一一件事

    在throw过程中退栈时,EH追踪哪个局部对象需要析构,预先安排必须的析构函数的调用,并且将控制权交给正确的异常处理函数。为了完成EH所需的记录和管理工作,编译器暗中在生成的代码中注入了数据、指令和库引用。

    不幸的是,很多程序员(以及他们的经理)讨厌这种注入行为导致过分的代码膨胀。他们感到恐慌,认为EH会削弱程序的使用价值。所以,我认为EH触及了人们对未知的恐惧:因为源码中没有明确地表露出EH的工作,他们将作最坏的估算。

    为了战胜这种恐惧,让我们通过短小的Visual C++代码剖析EH。

1.2     例1:基线版本

    生成一个新的C++源文件EH.cpp如下:

class C

   {

public:

   C()

      {

      }

   ~C()

      {

      }

   };

 

void f1()

   {

   C x1;

   }

 

int main()

   {

   f1();

   return 0;

   }

 

    然后,创建一个新的Visual C++控制台项目,并包含EH.CPP为唯一的源文件。使用默认项目属性,但打开“生成源码/汇编混合的.asm文件”选项。编译出Debug版本。在我机器上,得到的EH.exe是23,040字节。

    打开EH.asm文件,你将发现f1()函数非常接近预料:设置栈框架,调用xl的构造和析构函数,然后重设栈框架。特别地,你将注意到没有任何EH产物或记录――并不奇怪,因为程序没有抛出或捕获任何异常。

1.3     例2:单异常处理函数

   现在将f1改为如下形式:

void f1()

   {

   C x1;

   try

      {

      }

   catch(char)

      {

      }

   }

 

    重新编译EH.exe,然后注意文件大小。在我机器上,大小从23,040字节增到29,696字节。有些心跳吧,EH导致了29%的文件大小的增加。但看一下绝对增加,才6,656字节,并且绝大部分是来自于固定大小的库开销。剩下的少量才是额外注入到EH.obj中的代码和数据。

    在EH.asm中,可以找到符号__$EHRec$定义了一个常量值,它表示对于栈框架的偏移量。每个函数都在其生成的代码中引用了__$EHRec$,编译器暗中定义了一个局部的“EH记录”记录对象。

    EH记录是暂时的:和需要在代码中有个永久的静态记录相比,它们存在于栈中,在函数被进入时产生,在函数退出是消失。在且仅在函数需要提早析构局部对象时,编译器增加了EH记录(并且由局部代码维护它)。

    隐含意思是,有些函数不需要EH记录。看这个,增加的第二个函数:

void f2()

   {

   }

没有涉及对象和异常。重新编译程序。EH.asm显示f1()的栈中和以前一样包括一个EH记录,但f2()的栈中没有。然而,如果将代码改成这样:

void f2()

   {

   C x2;

   f1();

   }

 

    f2()现在定义了一个局部的EH记录,即使f2()自己没有try块。为什么?因为f2()调用了f1(),而f1()可能抛出异常而终止f2(),因此需要提早析构x2。

    结论:如果一个包含局部对象的函数没有明确处理异常,但可能传递一个别人抛的异常,那么函数仍然需要一个EH记录和相应的维护代码。

    这使你苦恼了吗?只要短路异常链就可以了。在我们的例子中,将f1()的定义改成:

void f1() throw()

   {

   C x1;

   try

      {

      }

   catch(char)

      {

      }

   }

 

    现在f1()承诺不抛异常。结果,f2()不需要传递f1()的异常,也就不需要EH记录了。你可以重新编译程序来核实,查看EH.asm并发现f2()的代码不再提到__$EHRec$。

1.4     例3:多个异常处理函数

    EH记录及其支撑代码不是编译所引入的唯有的记录。对给定try块的每个处理函数,编译器也都创建了入口表。想看得清楚些,将现在的EH.asm改名另存,并将f1()扩展为:

void f1() throw()

   {

   C x1;

   try

      {

      }

   catch(char)

      {

      }

   catch(int)

      {

      }

   catch(long)

      {

      }

   catch(unsigned)

      {

      }

   }

 

    重新编译,然后比较两次的EH.asm。

    (提醒:下面列出的EH.asm,我没有忽略不相关的东西,也没有用省略号代替什么。精确的标号名在你的系统上可能不一样。并且不要以汇编语言分析器的眼光看这些代码。)

    在我的EH.asm中,相关的名字、描述符和注释如下:

PUBLIC ??_R0D@8 ; char `RTTI Type Descriptor'

PUBLIC ??_R0H@8 ; int `RTTI Type Descriptor'

PUBLIC ??_R0J@8 ; long `RTTI Type Descriptor'

PUBLIC ??_R0I@8 ; unsigned int `RTTI Type Descriptor'

 

_DATA SEGMENT

??_R0D@8 DD FLAT:??_7type_info@@6B@ ; char `RTTI Type Descriptor'

         DD ...

         DB '.D', ...

_DATA ENDS

 

_DATA SEGMENT

??_R0H@8 DD FLAT:??_7type_info@@6B@ ; int `RTTI Type Descriptor'

         DD ...

         DB '.H', ...

_DATA ENDS

 

_DATA SEGMENT

??_R0J@8 DD FLAT:??_7type_info@@6B@ ; long `RTTI Type Descriptor'

         DD ...

         DB '.J', ...

_DATA ENDS

 

_DATA SEGMENT

??_R0I@8 DD FLAT:??_7type_info@@6B@ ; unsigned int `RTTI Type Descriptor'

         DD ...

         DB '.I', ...

_DATA ENDS

 

    (对于“RTTI Type Descriptor”和“type_info”的注释提示我,Visual C++在EH和RTTI时使用了同样的类型名描述符。)

    编译器同样生成了对在xdata@x段中定义的类型描述符的引用。每个类型对应一个捕获这种类型的异常处理函数的地址。这种描述符/处理函数对构成了EH库代码分发异常时的分发表。这些也是从我的EH.asm下摘抄的,加上了注释和图表:

xdata$x SEGMENT

 

$T214 DD ...

      DD ...

      DD FLAT:$T217 ;---+

      DD ...        ;   |

      DD FLAT:$T218 ;---|---+

      DD 2 DUP(...) ;   |   |

      ORG $+4       ;   |   |

                    ;   |   |

$T217 DD ...        ;<--+   |

      DD ...        ;       |

      DD ...        ;       |

      DD ...        ;       |

                    ;       |

$T218 DD ...        ;<------+

      DD ...

      DD ...

      DD 04H        ; # of handlers

      DD FLAT:$T219 ;---+

      ORG $+4       ;   |

                    ;   |

$T219 DD ...        ;<--+

      DD FLAT:??_R0D@8 ; char RTTI Type Descriptor

      DD ...

      DD FLAT:$L206    ; catch(char) address

 

      DD ...

      DD FLAT:??_R0H@8 ; int RTTI Type Descriptor

      DD ...

      DD FLAT:$L207    ; catch(int) address

 

      DD ...

      DD FLAT:??_R0J@8 ; long RTTI Type Descriptor

      DD ...

      DD FLAT:$L208    ; catch(long) address

 

      DD ...

      DD FLAT:??_R0I@8 ; unsigned int RTTI Type Descriptor

      DD ...

      DD FLAT:$L209    ; catch(unsigned int) address

 

xdata$x ENDS

 

    分发表表头(标号$T214、 $T217和 $T218处的代码)是f1()专属的,并为f1()的所有异常处理函数共享。$T219出的分发表的每一个入口项都特属于f1()的一个特定的异常处理函数。

    更一般地,编译器为每一带try块的函数生成一个分发表表头,为每一个异常处理函数增加一个入口项。类型描述符为程序的所有分发表共享。(例如,程序中所有catch(long)的处理函数引用同样的??_R0J@8类型描述符。)

    提要:要减小EH的空间开销,应该将程序中捕获异常的函数数目减到最小,将函数中异常处理函数的数目减到最小,将异常处理函数所捕获的异常类型减到最小。

1.5     例四:抛异常

    用“抛一个异常”来将所有东西融会起来。将f1()的try语句改成这样:

try

   {

   throw 123; // type 'int' exception

   }

 

    重新编译程序,打开EH.asm,注意新出现的东西(我同样加了的注释和图表)。

; in these exported names, 'H' is the RTTI Type Descriptor

;   code for 'int' -- which matches the data type of

;   the thrown exception value 123

PUBLIC __TI1H

PUBLIC __CTA1H

PUBLIC __CT??_R0H@84

 

; EH library routine that actually throws exceptions

EXTRN __CxxThrowException@8:NEAR

 

; new static data blocks used by library

;   when throwing 'int' exception

xdata$x SEGMENT

 

__CT??_R0H@84 DD ...                ;<------+

              DD FLAT:??_R0H@8      ;       |   ??_R0H@8 is RTTI 'int'

                                    ;       |    Type Descriptor

              DD ...                ;       |

              DD ...                ;       |

              ORG $+4               ;       |

              DD ...                ;       |

              DD ...                ;       |

                                    ;       |

__CTA1H       DD ...                ;<--+   |

              DD FLAT:__CT??_R0H@84 ;---|---+

                                    ;   |

__TI1H        DD ...                ;   |  __TI1H is argument passed to

              DD ...                ;   |   __CxxThrowException@8

              DD ...                ;   |

              DD FLAT:__CTA1H       ;---+

 

xdata$x ENDS

 

    和类型描述符一样,这些新的数据块为全部程序共享,例如,所有抛int异常代码引用__TI1H. 。同样要注意:相同的类型描述符被异常处理函数和throw语句引用。

    翻到f1()处,相关部分如下:

;void f1() throw()

;   {

;   try

;      {

 

       ...

       push $L224 ; Address of code to adjust stack frame via handler

                  ;   dispatch table.  Invoked by __CxxThrowException@8.

       ...

 

;      throw 123;

 

       push OFFSET FLAT:__TI1H       ; Address of data area diagramed

                                     ;   above

       mov DWORD PTR $T213[ebp], 123 ; 123 is the exception's value

       lea eax, DWORD PTR $T213[ebp]

       push eax

       call __CxxThrowException@8    ; Call into EH library, which in

                                     ;   turn eventually calls $L224

                                     ;   and $L216 a.k.a. 'catch(int)'

;      }

;   // ...

;   catch(int)

 

    $L216:

 

;      {

 

       mov eax, $L182 ; Return to EH library, which jumps to $L182

       ret 0

 

;      }

;   // ...

 

    $L182:

 

;   // Call local-object destructors, clean up stack, return

;   }

 

$L224:                         ; This label referenced by 'try' code.

    mov eax, OFFSET FLAT:$T223 ; $T223 is handler dispatch table, what

                               ;   had previously been label $T214

                               ;   before we added 'throw 123'

    jmp ___CxxFrameHandler     ; internal library routine

 

    当程序运行时,__CxxThrowException@8(EH的库函数)调用了$L216,catch(int)处理函数的地址。当处理函数一结束,程序就继续顺EH库中的代码向下运行,跳到$L224,继续向下并最终跳到$L182。这个标号是f1()的终止和cleanup代码的地址,在其中调用了x1的析构函数。你可以在调试器下用单步进行验证。

1.6     小结

    所有的异常处理体系都导致开销。除非你愿意在没有任何异常安全体系的情况下执行代码,你必须同意付出速度和空间的代价。EH作为语言的特性有优点的:编译器明确知道EH的实现并可以据此优化它。

    除了编译器的优化,你自己还有很多方法来优化。在以后的文章中,我将揭示特定的方法来将EH的代价减到最小。有些方法是基于标准C++的,其它则依赖于Visual C++的具体实现。

本文地址:http://com.8s8s.com/it/it2698.htm