c++ 異常處理(1)

異常 (exception) 是 c++ 中新增的一個特性,它提供了一種新的方式來結構化地處理錯誤,使得程序能夠很方便地把異常處理與出錯的程序分離,並且在使用上,它語法至關地簡潔,以致於會讓人錯覺以爲它底層的實現也應該很簡單,但事實上並非這樣。偏偏由於它語法上的簡單沒有規定過多細節,從而留給了編譯器足夠的空間來本身發揮,所以在不一樣操做系統,不一樣編譯器下,它的實現是有很大不一樣的。這篇文章介紹了 windows 和 visual c++ 是怎樣基於 SEH 來實現 c++ 上的異常處理的,講得很詳細,雖然已經寫了好久,但原理性的東西到如今也沒過期,有興趣能夠去細讀一下。linux

至於 linux 下 gcc 是怎樣作的,網上充斥着各類文檔,不少但也比較雜,我這兒就簡單把我這幾天看到的,想到的,理解了的,不理解的,作個簡單的總結,歡迎指正。ios

異常拋出後,發生了什麼事情?

根據 c++ 的標準,異常拋出後若是在當前函數內沒有被捕捉(catch),它就要沿着函數的調用鏈繼續往上拋,直到走完整個調用鏈,或者在某個函數中找到相應的 catch。若是走完調用鏈都沒有找到相應的 catch,那麼std::terminate() 就會被調用,這個函數默認是把程序 abort,而若是最後找到了相應的 catch,就會進入該 catch 代碼塊,執行相應的操做。c++

程序中的 catch 那部分代碼有一個專門的名字叫做:Landing pad(不十分準確),從拋異常開始到執行 landing pad 裏的代碼這中間的整個過程叫做 stack unwind,這個過程包含了兩個階段:windows

1)從拋異常的函數開始,對調用鏈上的函數逐個往前查找 landing pad。數據結構

2)若是沒有找到 landing pad 則把程序 abort,不然,則記下 landing pad 的位置,再從新回到拋異常的函數那裏開始,一幀一幀地清理調用鏈上各個函數內部的局部變量,直到 landing pad 所在的函數爲止。框架

簡而言之,正常狀況下,stack unwind 所要作的事情就是從拋出異常的函數開始,沿着調用鏈向上找到 catch 所在的函數,而後從拋異常的地方開始,清理調用鏈上各棧幀內已經建立了的局部變量。ide

void func1()
{
  cs a; // stack unwind時被析構。
  throw 3;
}

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

void func3()
{
  cs c;
  try 
  {
    func2();
  }
  catch (int)
  {
    //進入這裏以前, func1, func2已經被unwind.
  }
}

能夠看出,unwind 的過程能夠簡單當作是函數調用的逆過程,這個過程在實現上由一個專門的 stack unwind 庫來進行,在 intel 平臺上,它屬於 Itanium ABI 接口中的一部分,且與具體的語言無關,由系統提供實現,任何上層語言均可以在這個接口的基礎上實現各自的異常處理,GCC 就基於這個接口來實現c++的異常處理。函數

Itanium C++ ABI

Itanium ABI 定義了一系列函數及相應的數據結構來創建整個異常處理的流程及框架,主要的函數包括如下這些:測試

_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind

其中的 _Unwind_RaiseException() 函數用於進行stack unwind,它在用戶執行 throw 時被調用,主要功能是從當前函數開始,對調用鏈上每一個函數幀都調用一個叫做 personality routine 的函數(__gxx_personality_v0),該函數由上層的語言定義及提供實現,_Unwind_RaiseException() 會在內部把當前函數棧的調用現場重建,而後傳給 personality routine,personality routine則主要負責作兩件事情:ui

1)檢查當前函數是否含有相應 catch 能夠處理上面拋出的異常。

2)清掉調用棧上的局部變量。

顯然,咱們能夠發現 personality routine 所作的這兩件事情和前面所說的 stack unwind 所要經歷的兩個階段一一對應起來了,所以也能夠說,stack unwind 主要就是由 personality routine 來完成,它至關於一個 callback。

_Unwind_Reason_Code (*__personality_routine)
        (int version,
         _Unwind_Action actions,
         uint64 exceptionClass,
         struct _Unwind_Exception *exceptionObject,
         struct _Unwind_Context *context);

注意上面的第二個參數,它是用來告訴 personality routine,當前是處於 stack unwind 的哪一個階段的,其它的參數則主要用來傳遞與異常相關的信息及當前函數的上下文。前面一直所說的 stack unwind 包含兩個階段,具體到調用鏈上的函數來講,就是每一個函數在 unwind 的過程當中都會被 personality routine 遍歷兩次。

下面的僞代碼展現了 _Unwind_RaiseException() 內部的大概實現,算是對前面的一個總結:

_Unwind_RaiseException(exception)
{
    bool found = false;
    while (1)
     {
         // 創建上個函數的上下文
         context = build_context();
         if (!context) break;
         found = personality_routine(exception, context, SEARCH);
         if (found or reach the end) break;
     }

    while (found)
    {
        context = build_context();
        if (!context) break;
        personality_routine(exception, context, UNWIND);
        if (reach_catch_function) break;
    }
}

 ABI中的函數使用到了兩個自定義的數據結構,用於傳遞一些內部的信息。

struct _Unwind_Context;

struct _Unwind_Exception {
  uint64     exception_class;
  _Unwind_Exception_Cleanup_Fn exception_cleanup;
  uint64     private_1;
  uint64     private_2;
};

根據接口的介紹,_Unwind_Context 是一個對調用者透明的結構,用於表示程序運行時的上下文,主要就是一些寄存器的值,函數返回地址等,它由接口實現者來定義及建立,但我沒在接口中找到它的定義,只在 gcc 的源碼裏找到了一份它的定義

struct _Unwind_Context
{
  void *reg[DWARF_FRAME_REGISTERS+1];
  void *cfa;
  void *ra;
  void *lsda;
  struct dwarf_eh_bases bases;
  _Unwind_Word args_size;
};

至於 _Unwind_Exception,顧名思義,它在 unwind 庫內用於表示一個異常。

C++ ABI.

基於前面介紹的 Itanium ABI,編譯器層面也定義了一系列的 ABI 來與之交互。當咱們在代碼中寫下 "throw xxx" 時,編譯器會分配一個數據結構來表示該異常,該異常有一個頭部,定義以下:

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; };

注意其中最後一個變量:_Unwind_Exception unwindHeader,這個變量就是前面 Itanium 接口裏提到的接口內部用的結構體。當用戶 throw 一個異常時,編譯器會幫咱們調用相應的函數分配出以下一個結構:

 

其中 _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)_Unwind_RaiseException() 對調用鏈上的函數進行 unwind 時,調用 personality routine。

5)若是該異常如能被處理(有相應的 catch),則 personality routine 會依次對調用鏈上的函數進行清理。

6)_Unwind_RaiseException() 將控制權轉到相應的catch代碼。

7) unwind 完成,用戶代碼繼續執行。

從 c++ 的角度看,一個完整的異常處理流程就完成了,固然,其中省略了不少的細節,其中最讓人以爲神祕的也許就是 personality routine了,它是怎麼知道當前 Unwind 的函數是否有相應的 catch 語句呢?又是怎麼知道該怎樣去清理這個函數內的局部變量呢?具體實現這兒先不細說,只須要大概明白,其實它也不知道,只有編譯器知道,所以在編譯階段編譯器會創建創建一些表項來保存相應的信息,使得 personality routine 能夠在運行時經過這些事先創建起來的信息進行相應的查詢。

從源碼看Unwind的過程

 unwind 的過程是從 __cxa_throw() 裏開始的,請看以下源碼:

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 ();
}

咱們能夠看到 __cxa_throw 最終調用了 _Unwind_RaiseException(),stack unwind 就此開始,如前面所說,unwind 分爲兩個階段,分別進行搜索 catch 及清理調用棧,其相應的代碼以下:

/* 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;
}

如上兩個函數分別對應了 unwind 過程當中的這兩個階段,注意其中的:

uw_init_context()
uw_frame_state_for()
uw_update_context()

這幾個函數主要是用來重建函數調用現場的,它們的實現涉及到一大堆的細節,這兒賣個關子先不細說,大概原理就是,對於調用鏈上的函數來講,它們的很大一部分上下文是能夠從堆棧上恢復回來的,如 ebp, esp, 返回地址等。編譯器爲了讓 unwinder 能夠從棧上獲取這些信息,它在編譯代碼的時候,創建了不少表項用於記錄每一個能夠拋異常的函數的相關信息,這些信息在重建上下文時將會指導程序怎麼去搜索棧上的東西。

作點有意思的事情

說了一大堆,下面寫個測試的程序簡單回顧一下前面所說的關於異常處理的大概流程:

#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;
}

上面的程序運行起來後,咱們能夠在 __gxx_personality_v0 裏下一個斷點。

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)

從這個調用棧能夠看出,異常拋出後,咱們的程序都作了些什麼。若是你以爲好玩,你甚至能夠嘗試去 hook 掉其中某些函數,從而改變異常處理的行爲,這種 hack 的技巧在某些時候是頗有用的,好比說我如今用到的一個場景:

咱們使用了一個第三庫,這個庫裏有一個消息循環,它是放在一個 try/catch 裏面的。

void wxEntry()
{
    try
    {
       call_user_func();
    }
    catch(...)
    {
       unhandled_exception();
    }
}

call_user_func() 會調用一系列的函數,其中涉及咱們本身寫的代碼,在某些時候咱們的代碼拋異常了,並且咱們沒有捕捉住,所以 wxEntry 最終會 catch 住這些異常,而後調用 unhandled_exception(), 這個函數默認會調用一些清理函數,而後把程序 abort,而在調用清理函數的時候,因爲咱們的代碼已經行爲不正常了,在種狀況下去清理一般又會引出不少其它的奇奇怪怪的錯誤,最後就算獲得了coredump 也很難判斷出咱們的程序哪裏出了問題。因此咱們但願當咱們的代碼拋出異常且沒有被咱們本身處理而最後在 wxEntry() 中被捕捉了的話,咱們能夠把拋異常的地方的調用棧給打出來。一開始咱們嘗試把 __cxa_throw 給 hook 了,也就是每當有人一拋異常,咱們就把當時的調用棧給打出來,這個方案能夠解決問題,可是問題很明顯,它影響了全部拋異常的代碼的執行效率,畢竟收集調用棧相對來講是比較費時的。

其實咱們並不必對每一個 throw 都去處理,問題的關鍵就在於咱們能不能識別出咱們所想要處理的異常。

在這個案例中,咱們偏偏能夠,由於全部沒被處理的異常,最終都會統一上拋到 wxEntry 中,那麼咱們只要 hook 一下 personality routine,看看當前 unwind 的是否是 wxEntry 不就能夠了嗎!

#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; }

上面的代碼中,personality routine 返回_URC_HANDLER_FOUND 則意味着當前函數幀裏找到相應的 landing pad,而後咱們就嘗試判斷一下,該函數是否就是咱們的目標函數,若是是則立刻進行相應的處理。這個作法顯然比 hook __cxa_throw 要好一些,畢竟只針對一個函數作了處理,固然,與原生的異常處理相比,這裏仍是有必定的效率損失的,就看怎麼取捨了,追求方便 debug 是必然要付出些代價的。

相關文章
相關標籤/搜索