異常(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所在的函數為止。 復制代碼 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則主要負責做兩件事情: 1)檢查當前函數是否含有相應catch可以處理上面拋出的異常。 2)清掉調用棧上的局部變量。 需要注意的是,這兩件事情是分開來做的,首先從拋異常的地方開始,一幀一幀向上檢查,直到找到含有相應catch的函數(或發現到了盡頭則terminate),如果找到,則再次回到異常拋出所在的函數,一幀一幀地清理調用棧的變量,換而言之,_Unwind_RaiseException遍歷了兩次函數調用棧,這兩個階段可以大概用如下偽代碼表示: 復制代碼 _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; } } 復制代碼 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) 找到對應的catch代碼,調用__cxa_begin_catch() 5) 執行catch中的代碼。 6)調用__cxa_end_catch(). 7) 調用_Unwind_Resume(). 從c++的角度看,一個完整的異常處理流程就完成了,當然,其中省略了很多的細節。 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 (); } 復制代碼 如前面所說,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,返回值等。編譯器為了從棧上獲取這些信息,它在編譯代碼的時候,建立了很多表項用於記錄每個可以拋異常的函數的相關信息,這些信息就在重建上下文時能夠指導程序怎麼去搜索棧上的東西。 做點有意思的事情 說了一大堆,下面寫個測試的程序簡單回顧一下前面所說的關於異常處理的大概流程: 復制代碼 #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; }