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

原创 2002年03月01日 08:53:00

1.     实例剖析EH

    到现在为止,我仍然逗留在CC++的范围内,但这次要稍微涉及一下汇编语言。目标:初步揭示Visual C++EHthrowcatch的实现。本文不是巨细无遗的,毕竟我的原则是只关注(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.exe23,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++EHRTTI时使用了同样的类型名描述符。)

    编译器同样生成了对在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@8EH的库函数)调用了$L216catch(int)处理函数的地址。当处理函数一结束,程序就继续顺EH库中的代码向下运行,跳到$L224,继续向下并最终跳到$L182。这个标号是f1()的终止和cleanup代码的地址,在其中调用了x1的析构函数。你可以在调试器下用单步进行验证。

1.6     小结

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

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

C++异常处理示例

这两天我写了一个测试c++异常处理机制的例子,感觉有很好的示范作用,在此贴出来,给c++异常处理的初学者入门。本文后附有c++异常的知识普及,有兴趣者也可以看看。      下面的代码直接贴到你...
  • loveRooney
  • loveRooney
  • 2014年08月08日 14:49
  • 831

C++中异常处理中的异常重新抛出的一种用法

本文讨论了C++异常处理中重复抛出异常的一种用法
  • u010857719
  • u010857719
  • 2016年09月08日 21:56
  • 763

Android NDK之JNI异常处理

处理本机代码中的异常      为了处理以Java代码实现的方法执行中抛出的异常,或者是以本机代码编写的方法抛出的Java异常,JNI提供了Java异常机制的钩子程序。该机制与C/C++中常规函...
  • u013378580
  • u013378580
  • 2016年08月25日 16:24
  • 637

C++异常处理类与自定义异常处理类

转自:http://blog.csdn.net/makenothing/article/details/43273137 例1:自定义一个继承自excepton的异常类myExcep...
  • u014805066
  • u014805066
  • 2017年03月29日 17:32
  • 544

c++异常处理机制示例及讲解

原文链接:http://ticktick.blog.51cto.com/823160/191881 下面的代码直接贴到你的console工程中,可以运行调试看看效果,并分析c++的异常机制。 ...
  • u013727453
  • u013727453
  • 2015年04月24日 15:30
  • 1752

深入理解C++中的异常处理机制

异常处理 增强错误恢复能力是提高代码健壮性的最有力的途径之一,C语言中采用的错误处理方法被认为是紧耦合的,函数的使用者必须在非常靠近函数调用的地方编写错误处理代码,这样会使得其变得笨拙和难以使用。C...
  • u013982161
  • u013982161
  • 2016年11月06日 12:35
  • 829

【VS开发】C++异常处理操作

异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制。 也许我们已经使用过异常,但是你会是一种习惯吗,不要老是想着当我打开一个文件的时候才用异常判断一下,我知道对你来说你喜欢...
  • LG1259156776
  • LG1259156776
  • 2016年05月21日 21:34
  • 1671

基于TCP传输的网络编程异常处理

 基于TCP传输的网络编程异常处理 一:进程一端退出(exit,CTRL+C,挂掉)(跟主动CLOSE、主动关机一样)  内核会关闭所有句柄触发FIN分节发送(但如果设置了SO_LINGER...
  • doitsjz
  • doitsjz
  • 2017年03月11日 14:15
  • 625

linux C 异常处理的方式

目前遇到这样的问题,大概在2000多台服务器里面有100多多台一个c进程挂掉了,由于公司各种的流程调试起来非常困难。 这几天google了下找到了一些资料,捕获异常堆栈的,如http://spin....
  • wangyin159
  • wangyin159
  • 2015年07月23日 21:02
  • 606

C++/MFC全局未知异常捕获并进行调试

C++/MFC全局未知异常捕获Dump出来并进行调试全局捕获未知异常函数名: WINBASEAPI LPTOP_LEVEL_EXCEPTION_FILTER WINAPI Set...
  • KellyGod
  • KellyGod
  • 2017年04月01日 00:25
  • 1392
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C与C++中的异常处理4
举报原因:
原因补充:

(最多只允许输入30个字)