一、问题
在c++的语法中,可以在函数声明中添加throw(),throw(type1, type2)之类的说明,前者声明该函数不被抛出任何异常,后者则是声明该函数只会抛出type1,type2类型的异常。当然这里并不是像孔乙己一样来说明回字的四种写法;更不是为这个语法摇旗呐喊,相反,各种论调都是不建议使用这中语法的。这里讨论这个问题只是觉得这个语义对于编译器来说将如何实现呢?因为这个语法是一个看起来比较别扭的功能,可以预感到编译器实现这个功能的时候应该也会比较别扭。
下面是一个最为简单直观的例子,从这个例子上看,函数声明为只抛出double类型异常,但是事实上抛出了一个int类型的异常,这个问题在编译的时候没有任何编译错误,但是运行的时候会出现进程直接被SIGABRT退出,这个也是尽量不要使用这种语法的一个有力说明。
tsecer@harry: cat nothrowcallthrow.cpp
#include <stdio.h>
struct harry
{
public:
void tsecer() throw(double)
{
throw 0;
}
};
int main()
{
try
{
harry().tsecer();
}
catch (int &i)
{
printf("catch a int %d expection
", i);
}
catch(...)
{
printf("catch default expection
");
}
return 0;
}
tsecer@harry: g++ nothrowcallthrow.cpp -o nothrowcallthrow
tsecer@harry: ./nothrowcallthrow
terminate called after throwing an instance of 'int'
Aborted (core dumped)
tsecer@harry:
二、编译器如何实现这个功能
这里的例子里是只列出了double类型,但是理论上可以允许任意多类型。尽管如此,一个函数可以抛出的异常是可以枚举的,这样就需要编译器为这个函数进行一些定制化生成,从而允许且只允许这些声明的类型被抛出。这里我们可以想象为一个黑洞,只是允许一些特定的类型从函数中逃逸;或者说看作一个国家有签证的人才可以出国。从编译器的实现来看,大致是这样一个思想,比方说对于前面的函数
void tsecer() throw(type1, type2)
{
mainbody
}
编译器可以转换为这样的形式
void tsecer() throw(type1, type2)
{
try
{
mainbody
}
catch (type1 &)
{
throw
}
catch (type2 &)
{
throw
}
catch(...)
{
expection handler
}
}
其中的try catch就是编译器生成的对于应用不可见的代码。
三、看下前面代码生成的汇编
(gdb) disas harry::tsecer
Dump of assembler code for function _ZN5harry6tsecerEv:
0x000000000040094e <_ZN5harry6tsecerEv+0>: push %rbp
0x000000000040094f <_ZN5harry6tsecerEv+1>: mov %rsp,%rbp
0x0000000000400952 <_ZN5harry6tsecerEv+4>: sub $0x10,%rsp
0x0000000000400956 <_ZN5harry6tsecerEv+8>: mov %rdi,0xfffffffffffffff8(%rbp)
0x000000000040095a <_ZN5harry6tsecerEv+12>: mov $0x4,%edi
0x000000000040095f <_ZN5harry6tsecerEv+17>: callq 0x400778 <__cxa_allocate_exception@plt>
0x0000000000400964 <_ZN5harry6tsecerEv+22>: mov %rax,%rdi
0x0000000000400967 <_ZN5harry6tsecerEv+25>: mov %rdi,%rax
0x000000000040096a <_ZN5harry6tsecerEv+28>: movl $0x0,(%rax)
0x0000000000400970 <_ZN5harry6tsecerEv+34>: mov $0x0,%edx
0x0000000000400975 <_ZN5harry6tsecerEv+39>: mov $0x500eb0,%esi
0x000000000040097a <_ZN5harry6tsecerEv+44>: callq 0x4007c8 <__cxa_throw@plt>
0x000000000040097f <_ZN5harry6tsecerEv+49>: mov %rax,0xfffffffffffffff0(%rbp)
0x0000000000400983 <_ZN5harry6tsecerEv+53>: cmp $0xffffffffffffffff,%rdx
0x0000000000400987 <_ZN5harry6tsecerEv+57>: je 0x400992 <_ZN5harry6tsecerEv+68>
0x0000000000400989 <_ZN5harry6tsecerEv+59>: mov 0xfffffffffffffff0(%rbp),%rdi
0x000000000040098d <_ZN5harry6tsecerEv+63>: callq 0x4007b8 <_Unwind_Resume@plt>
0x0000000000400992 <_ZN5harry6tsecerEv+68>: mov 0xfffffffffffffff0(%rbp),%rdi
0x0000000000400996 <_ZN5harry6tsecerEv+72>: callq 0x400768 <__cxa_call_unexpected@plt>
End of assembler dump.
(gdb)
反汇编最后的__cxa_call_unexpected就是未匹配类型的默认处理函数,不在声明中声明的类型将在这里被集中处理,从名字上看,进而导致进程退出。
四、允许类型的识别
在__cxa_throw之后,有一个判断,如果类型返回值不等于-1,则会继续展开,这个地方可以推测,对于允许逸出的类型,这个地方返回值应该是-1;对应地,不允许逃逸的类型将会返回-1,从而跳转到__cxa_call_unexpected来终止进程。
0x0000000000400983 <_ZN5harry6tsecerEv+53>: cmp $0xffffffffffffffff,%rdx
0x0000000000400987 <_ZN5harry6tsecerEv+57>: je 0x400992 <_ZN5harry6tsecerEv+68>
__gxx_personality_v0函数中
while (1)
{
p = action_record;
p = read_sleb128 (p, &ar_filter);
read_sleb128 (p, &ar_disp);
if (ar_filter == 0)
{
……
}
else if (ar_filter > 0)
{
……
}
else
{
// Negative filter values are exception specifications.
// ??? How do foreign exceptions fit in? As far as I can
// see we can't match because there's no __cxa_exception
// object to stuff bits in for __cxa_call_unexpected to use.
// Allow them iff the exception spec is non-empty. I.e.
// a throw() specification results in __unexpected.
if (throw_type
? ! check_exception_spec (&info, throw_type, thrown_ptr,
ar_filter)
: empty_exception_spec (&info, ar_filter))
{
saw_handler = true;
break;
}
}
if (ar_disp == 0)
break;
action_record = p + ar_disp;
}
if (saw_handler)
{
handler_switch_value = ar_filter;
found_type = found_handler;
}
……
/* For targets with pointers smaller than the word size, we must extend the
pointer, and this extension is target dependent. */
_Unwind_SetGR (context, __builtin_eh_return_data_regno (0),
__builtin_extend_pointer (ue_header));
_Unwind_SetGR (context, __builtin_eh_return_data_regno (1),
handler_switch_value);
_Unwind_SetIP (context, landing_pad);
#ifdef __ARM_EABI_UNWINDER__
if (found_type == found_cleanup)
__cxa_begin_cleanup(ue_header);
#endif
return _URC_INSTALL_CONTEXT;
}
可见其中的handler_switch_value最终还是从生成的代码中的ar_filter中读取,该值的读取通过 read_sleb128 (p, &ar_filter);函数完成。前面的代码对于负数的判断也是如果匹配则继续搜索,不在类型列表则返回负数(-1),这也是throw 1走到的流程。