分享
 
 
 

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

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

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++的具体实现。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有