c++ 异常处理(1)

异常(exception)是c++中新增的一个特性,它提供了一种新的方式来结构化地处理错误,使得程序可以很方便地把异常处理与出错的程序分离,而且在使用上,它语法相当地简洁,以至于会让人错觉觉得它底层的实现也应该很简单,但事实上并不是这样。恰恰因为它语法上的简单没有规定过多细节,从而留给了编译器足够的空间来自己发挥,因此在不同操作系统,不同编译器下,它的实现是有很大不同的。这篇文章介绍了windows和visual c++是怎样基于SEH来实现c++上的异常处理的,讲得很详细,虽然已经写了很久,但原理性的东西到现在也没过时,有兴趣可以去细读一下。

至于linux下gcc是怎样做的,网上充斥着各种文档,很多但也比较杂,我这儿就简单把我这几天看到的,想到的,理解了的,不理解的,做个简单的总结,欢迎指正。

异常抛出后,发生了什么事情?

根据c++的标准,异常抛出后如果在当前函数内没有被捕捉(catch), 它就要沿着函数的调用链继续往上抛,直到走完整个调用链,或者直到在某个函数中找到相应的catch。如果走完调用链都没有找到相应的catch, 那么std::terminate()就会被调用,这个函数默认是把程序abort, 而如果最后找到了相应的catch,就会进入该catch块,执行相应的操作。

这个catch代码有一个专门的名字叫作:landing pad

程序在执行landing pad里的代码前,要进行一个叫作stack unwind的过程,该过程要做的事情就是从抛出异常的函数开始,清理调用栈上的已经创建了的局部变量并退出该函数,这个清理过程以函数调用桢为单位,直到相应的catch所在的函数为止。

c++ 异常处理(1)
void func1()
{
  cs a; // stack unwind时被析构。
  throw 3;
}

void func2()
{
  cs b;
  func1();
}

void func3()
{
  cs c;
  try 
  {
    func2();
  }
  catch (int)
  {
    //进入这里之前, func1, func2已经被unwind.
  }
}
c++ 异常处理(1)

可以看出,unwind的过程可以简单看成是函数调用的逆过程,这个过程由一个专门的stack unwind库来进行,在intel平台上,这个库属于Itanium ABI接口中的一部分,它与具体的语言无关,由系统提供实现,任何上层语言都可以在这个接口的基础上实现各自的异常处理,GCC就基于这个接口来实现它来实现c++的异常处理。

Itanium C++ ABI

Itanium ABI定义了一系列函数及相应的结构来建立整个异常处理的流程及框架,主要的函数包括以下:

c++ 异常处理(1)
_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind
c++ 异常处理(1)

其中_Unwind_RaiseException函数就是用于进行stack unwind的, 它在用户执行throw时被调用,然后从当前函数开始,对调用栈上每个函数桢都调用一个叫作personality routine的函数(__gxx_personality_v0),该函数由上层的语言定义及实现,_Unwind_RaiseException会在内部把当前函数栈的调用现场重建,然后传给personality routine, personality routine则主要负责做两件事情:

1)检查当前函数是否含有相应catch可以处理上面抛出的异常。

2)清掉调用栈上的局部变量。

需要注意的是,这两件事情是分开来做的,首先从抛异常的地方开始,一帧一帧向上检查,直到找到含有相应catch的函数(或发现到了尽头则terminate),如果找到,则再次回到异常抛出所在的函数,一帧一帧地清理调用栈的变量,换而言之,_Unwind_RaiseException遍历了两次函数调用栈,这两个阶段可以大概用如下伪代码表示:

c++ 异常处理(1)
_Unwind_RaiseException(exception)
{
    bool found = false;
    while (1)
     {
// 建立上个函数的上下文 context
= build_context(); found = personality_routine(exception, context, SEARCH); if (found or reach the end) break; } while (found) { context = build_context(); personality_routine(exception, context, UNWIND); if (reach_catch_function) break; } }
c++ 异常处理(1)

 

ABI中的函数使用到了两个自定义的结构体,用于传递一些内部的信息。

c++ 异常处理(1)
struct _Unwind_Context;

struct _Unwind_Exception {
  uint64     exception_class;
  _Unwind_Exception_Cleanup_Fn exception_cleanup;
  uint64     private_1;
  uint64     private_2;
};
c++ 异常处理(1)

根据接口的介绍,_Unwind_Context是一个对调用者透明的结构,用于表示程序运行的上下文,主要就是一些寄存器的值,函数返回地址等,它由接口实现者来定义及创建,但我没在接口中找到它的定义,只在gcc的源码里找到了一份它的定义

c++ 异常处理(1)
struct _Unwind_Context
{
  void *reg[DWARF_FRAME_REGISTERS+1];
  void *cfa;
  void *ra;
  void *lsda;
  struct dwarf_eh_bases bases;
  _Unwind_Word args_size;
};
c++ 异常处理(1)

至于_Unwind_Exception,顾名思义,它在unwind库内用于表示一个异常。

C++ ABI.

基于前面介绍的Itanium ABI,编译器层面也定义一系列ABI来与之交互。
当我们在代码中写下"throw xxx"时,编译器会分配一个结构来表示该异常,该异常有一个头部,定义如下:

c++ 异常处理(1)
struct __cxa_exception 
{ std::type_info
* exceptionType; void (*exceptionDestructor) (void *); unexpected_handler unexpectedHandler; terminate_handler terminateHandler; __cxa_exception * nextException; int handlerCount; int handlerSwitchValue; const char * actionRecord; const char * languageSpecificData; void * catchTemp; void * adjustedPtr; _Unwind_Exception unwindHeader; };
c++ 异常处理(1)

注意其中最后一个变量:_Unwind_Exception unwindHeader,就是前面Itanium接口里提到的接口内部用的结构体。当用户throw一个异常时,编译器会帮我们调用相应的函数分配出如下一个结构:

c++ 异常处理(1)

 

其中_cxa_exception就是头部,exception_obj则是"throw xxx"中的xxx,这两部分在内存中是连续的。

 

异常对象由函数__cxa_allocate_exception()进行创建,最后由__cxa_free_exception()进行销毁。

当我们在程序里执行了抛出异常后,编译器为我们做了如下的事情:

1)调用__cxa_allocate_exception函数,分配一个异常对象。

2)调用__cxa_throw函数,这个函数会将上面分配的异常对象做一些初始化。

3)__cxa_throw() 会被用Itanium ABI里的_Unwind_RaiseException()从而开始unwind.

4) 找到对应的catch代码,调用__cxa_begin_catch()

5) 执行catch中的代码。

6)调用__cxa_end_catch().

7) 调用_Unwind_Resume().

从c++的角度看,一个完整的异常处理流程就完成了,当然,其中省略了很多的细节。

Unwind的过程

 unwind的过程是从__cxa_throw()里开始的,请看如下源码:

c++ 异常处理(1)
extern "C" void
__cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo,
void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
{
   PROBE2 (throw, obj, tinfo);

   // Definitely a primary.
   __cxa_refcounted_exception *header = __get_refcounted_exception_header_from_obj (obj);
   header->referenceCount = 1;
   header->exc.exceptionType = tinfo;
   header->exc.exceptionDestructor = dest;
   header->exc.unexpectedHandler = std::get_unexpected ();
   header->exc.terminateHandler = std::get_terminate ();
   __GXX_INIT_PRIMARY_EXCEPTION_CLASS(header->exc.unwindHeader.exception_class);
   header->exc.unwindHeader.exception_cleanup = __gxx_exception_cleanup;

   #ifdef _GLIBCXX_SJLJ_EXCEPTIONS
   _Unwind_SjLj_RaiseException (&header->exc.unwindHeader);
   #else
   _Unwind_RaiseException (&header->exc.unwindHeader);
   #endif

   // Some sort of unwinding error. Note that terminate is a handler.
   __cxa_begin_catch (&header->exc.unwindHeader);
   std::terminate ();
}
c++ 异常处理(1)

 

如前面所说,unwind分为两个阶段,一个用来搜索catch,一个用来清理调用栈:

c++ 异常处理(1)
/* Raise an exception, passing along the given exception object.  */

_Unwind_Reason_Code
_Unwind_RaiseException(struct _Unwind_Exception *exc)
{
  struct _Unwind_Context this_context, cur_context;
  _Unwind_Reason_Code code;

  uw_init_context (&this_context);
  cur_context = this_context;

  /* Phase 1: Search.  Unwind the stack, calling the personality routine
     with the _UA_SEARCH_PHASE flag set.  Do not modify the stack yet.  */
  while (1)
    {
      _Unwind_FrameState fs;

      code = uw_frame_state_for (&cur_context, &fs);

      if (code == _URC_END_OF_STACK)
    /* Hit end of stack with no handler found.  */
    return _URC_END_OF_STACK;

      if (code != _URC_NO_REASON)
    /* Some error encountered.  Ususally the unwinder doesn‘t
       diagnose these and merely crashes.  */
    return _URC_FATAL_PHASE1_ERROR;

      /* Unwind successful.  Run the personality routine, if any.  */
      if (fs.personality)
    {
      code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class,
                    exc, &cur_context);
      if (code == _URC_HANDLER_FOUND)
        break;
      else if (code != _URC_CONTINUE_UNWIND)
        return _URC_FATAL_PHASE1_ERROR;
    }

      uw_update_context (&cur_context, &fs);
    }

  /* Indicate to _Unwind_Resume and associated subroutines that this
     is not a forced unwind.  Further, note where we found a handler.  */
  exc->private_1 = 0;
  exc->private_2 = uw_identify_context (&cur_context);

  cur_context = this_context;
  code = _Unwind_RaiseException_Phase2 (exc, &cur_context);
  if (code != _URC_INSTALL_CONTEXT)
    return code;

  uw_install_context (&this_context, &cur_context);
}


static _Unwind_Reason_Code
_Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc,
                  struct _Unwind_Context *context)
{
  _Unwind_Reason_Code code;

  while (1)
    {
      _Unwind_FrameState fs;
      int match_handler;

      code = uw_frame_state_for (context, &fs);

      /* Identify when we‘ve reached the designated handler context.  */
      match_handler = (uw_identify_context (context) == exc->private_2
               ? _UA_HANDLER_FRAME : 0);

      if (code != _URC_NO_REASON)
    /* Some error encountered.  Usually the unwinder doesn‘t
       diagnose these and merely crashes.  */
      return _URC_FATAL_PHASE2_ERROR;

      /* Unwind successful.  Run the personality routine, if any.  */
      if (fs.personality)
      {
        code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler,
                    exc->exception_class, exc, context);
        if (code == _URC_INSTALL_CONTEXT)
          break;
        if (code != _URC_CONTINUE_UNWIND) 
          return _URC_FATAL_PHASE2_ERROR;
      }

      /* Don‘t let us unwind past the handler context.  */
      if (match_handler)
         abort ();

      uw_update_context (context, &fs);
    }

  return code;
}
c++ 异常处理(1)

上面两个函数分别对应了unwind过程中的这两个阶段,注意其中的:

uw_init_context()
uw_frame_state_for()
uw_update_context()

这几个函数主要是用来重建函数调用现场的,它们的实现涉及到一大堆的细节,这儿先不细说,大概原理就是,对于调用链上的函数来说,它们的很大一部分上下文是可以从堆栈上恢复回来的,如ebp,esp,返回值等。编译器为了从栈上获取这些信息,它在编译代码的时候,建立了很多表项用于记录每个可以抛异常的函数的相关信息,这些信息就在重建上下文时能够指导程序怎么去搜索栈上的东西。

 

做点有意思的事情

说了一大堆,下面写个测试的程序简单回顾一下前面所说的关于异常处理的大概流程:

c++ 异常处理(1)
#include <iostream>
using namespace std;

void test_func3()
{
    throw 3;

    cout << "test func3" << endl;
}

void test_func2()
{
    cout << "test func2" << endl;
    try
    {
        test_func3();
    }
    catch (int)
    {
        cout << "catch 2" << endl;
    }
}

void test_func1()
{
    cout << "test func1" << endl;
    try
    {
        test_func2();
    }
    catch (...)
    {
        cout << "catch 1" << endl;
    }
}

int main()
{
    test_func1();
    return 0;
}
c++ 异常处理(1)

上面的程序运行起来后,我们可以在__gxx_personality_v0 里下一个断点。

c++ 异常处理(1)
Breakpoint 2, 0x00dd0a46 in __gxx_personality_v0 () from /usr/lib/libstdc++.so.6
(gdb) bt
#0  0x00dd0a46 in __gxx_personality_v0 () from /usr/lib/libstdc++.so.6
#1  0x00d2af2c in _Unwind_RaiseException () from /lib/libgcc_s.so.1
#2  0x00dd10e2 in __cxa_throw () from /usr/lib/libstdc++.so.6
#3  0x08048979 in test_func3 () at exc.cc:6
#4  0x080489ac in test_func2 () at exc.cc:16
#5  0x08048a52 in test_func1 () at exc.cc:29
#6  0x08048ad1 in main () at exc.cc:39
(gdb)
c++ 异常处理(1)

 

从这个调用栈可以看出,异常抛出后,我们的程序都做了些什么。如果你觉得好玩,你甚至可以尝试去hook掉其中某些函数,从而改变异常处理的行为,这种hack的技巧在某些时候是很有用的,比如说我现在用到的一个场景:

我们使用了一个第三库,这个库里有一个消息循环,它是放在一个try/catch里面的。

c++ 异常处理(1)
void wxEntry()
{
    try
    {
       call_user_func();
    }
    catch(...)
    {
       unhandled_exception();
    }
}
c++ 异常处理(1)

call_user_func()会调用一系列的函数,其中涉及我们自己写的代码,在某些时候我们的代码抛异常了,而且我们没有捕捉住,因此wxEntry里最终会catch住,调用unhandled_exception(), 这个函数默认调用一些清理函数,然后把程序abort,而在调用清理函数的时候,由于我们的代码已经行为不正常了,在种情况下去清理通常又会引出很多其它的奇奇怪怪的错误,最后就算得到了coredump也很难判断出我们的程序哪里出了问题。所以我们希望当我们的代码抛出异常且没有被我们自己处理而在wxEntry()中被捕捉了的话,我们可以把抛异常的地方的调用栈给打出来。

一开始我们尝试把__cxa_throw给hook了,也就是每当有人一抛异常,我们就把当时的调用栈给打出来,这个方案可以解决问题,但是问题很明显,它影响了所有抛异常的代码的执行效率,毕竟收集调用栈相对来说是比较费时的。

其实我们并没必要对每个throw都去处理,问题的关键就在于我们能不能识别出我们所想要处理的异常。

在这个案例中,我们恰恰可以,因为所有没被处理的异常,最终都会统一上抛到wxEntry中,那么我们只要hook一下personality routine,看看当前unwind的是不是wxEntry不就可以了吗!

c++ 异常处理(1)
#include <execinfo.h>
#include <dlfcn.h>
#include <cxxabi.h>
#include <unwind.h>

#include <iostream>
using namespace std; void test_func1(); static personality_func gs_gcc_pf = NULL; static void hook_personality_func() { gs_gcc_pf = (personality_func)dlsym(RTLD_NEXT, "__gxx_personality_v0"); } static int print_call_stack() { //to do. } extern "C" _Unwind_Reason_Code __gxx_personality_v0 (int version, _Unwind_Action actions, _Unwind_Exception_Class exception_class, struct _Unwind_Exception *ue_header, struct _Unwind_Context *context) { _Unwind_Reason_Code code = gs_gcc_pf(version, actions, exception_class, ue_header, context); if (_URC_HANDLER_FOUND == code) { //找到了catch所有的函数 //当前函数内的指令的地址 void* cur_ip = (void*)(_Unwind_GetIP(context)); Dl_info info; if (dladdr(cur_ip, &info)) { if (info.dli_saddr == &test_func1) { // 当前函数是目标函数 print_call_stack(); } } } return code; } void test_func3() { char* p = new char[2222222222222]; cout << "test func3" << endl; } void test_func2() { cout << "test func2" << endl; try { test_func3(); } catch (int) { cout << "catch 2" << endl; } } void test_func1() { cout << "test func1" << endl; try { test_func2(); } catch (...) { cout << "catch 1" << endl; } } int main() { hook_personality_func(); test_func1(); return 0; }
c++ 异常处理(1)

c++ 异常处理(1),布布扣,bubuko.com

c++ 异常处理(1)

上一篇:蓝桥杯 第三届C/C++预赛真题(8) 密码发生器(水题)


下一篇:javascript中跨源资源共享