gcc如何处理函数的throw属性

一、问题
在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\n", i);
    }
    catch(...)
    {
        printf("catch default expection\n");
    }
    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走到的流程。

posted on 2019-03-07 09:58  tsecer  阅读(387)  评论(0编辑  收藏  举报

导航