C 語法上使用 goto 或是 setjmp/longjmp 實現例外處理。goto 只能做本地跳轉,亦即在同一函式內部進行跳轉。setjmp/longjmp 則能做到非本地跳轉。setjmp 會保留當前進程的狀態,以供之後的 longjmp 回到 setjmp 所在位置。setjmp/longjmp 並非以 stack unwind 實現1)。
All setjmp is doing is saving a bunch of registers including rsp (the stack pointer) and the return address into the jmp_buf array that was passed as a parameter.
C++ 語法上使用 try/catch/throw 實現例外處理,C++ 內部主要使用 table-driven 的方式實現。簡單來講,編譯和鏈結時期會建立一張表,一段範圍的計數器值會對應到例外處理語句。當執行時期發生例外時,運行期函式庫會表跳轉至對應的例外處理語句。
術語 stack unwinding,又稱棧回溯 (棧回退),此為一搜尋例外處理代碼的過程。當例外被丟出時,棧會以 callee 到 caller 的方向消退,以搜尋例外處理代碼處理該例外。棧回溯的過程中,當脫離某一個棧的範圍時,針對配置在該棧上的物件,運行期函式庫會呼叫其解構子。棧回溯的效果就如同除了例外處理代碼 (即 catch) 所在的函式以外,往下曾經被調用過的函式沒有作用,彷彿未被調用。
The objects allocated on the stack are "unwound" when the scope is exited (here the scope is of the function func.) This is done by the compiler inserting calls to destructors of automatic (stack) variables.
DWARF2 contains .debug_frame information for helping debuggers figure out how to unwind frames (i.e. how to restore the stack to the previous frame from any instruction executing using the current frame.
uw_frame_state_for
: 在 _Unwind_RaiseException
被調用。將當前 context 的 caller 更新至 fs (frame state)。/* Given the _Unwind_Context CONTEXT for a stack frame, look up the FDE for its caller and decode it into FS. This function also sets the args_size and lsda members of CONTEXT, as they are really information about the caller's frame. */ static _Unwind_Reason_Code uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs) { // 尋找 caller 的 fde。 fde = _Unwind_Find_FDE (context->ra + _Unwind_IsSignalFrame (context) - 1, &context->bases); // 找到 fde 基於的 cie,並執行 cie 中的指令。 cie = get_cie (fde); insn = extract_cie_info (cie, context, fs); /* First decode all the insns in the CIE. */ end = (const unsigned char *) next_fde ((const struct dwarf_fde *) cie); execute_cfa_program (insn, end, context, fs); // 執行 fde 中指令,更新 fs。 execute_cfa_program (insn, end, context, fs); }
uw_update_context
: 在 _Unwind_RaiseException
被調用。將 fs (frame state) 所表示 context 的 caller,更新至 context。/* CONTEXT describes the unwind state for a frame, and FS describes the FDE of its caller. Update CONTEXT to refer to the caller as well. Note that the args_size and lsda members are not updated here, but later in uw_frame_state_for. */ static void uw_update_context (struct _Unwind_Context *context, _Unwind_FrameState *fs) { // 主要由 uw_update_context_1 更新 context。 uw_update_context_1 (context, fs); if (fs->regs.reg[DWARF_REG_TO_UNWIND_COLUMN (fs->retaddr_column)].how == REG_UNDEFINED) /* uw_frame_state_for uses context->ra == 0 check to find outermost stack frame. */ context->ra = 0; else { // 將 context 的 caller 的 return address 更新至 context->ra。 // 至此,context 完全更新到原本 context 的 caller 的內容。 context->ra = __builtin_extract_return_addr (_Unwind_GetPtr (context, fs->retaddr_column)); } }
throw
會被轉成 __cxa_allocate_exception
和 __cxa_throw
。__cxa_throw
→ _Unwind_RaiseException
extern "C" void __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo, void (_GLIBCXX_CDTOR_CALLABI *dest) (void *)) { _Unwind_RaiseException (&header->exc.unwindHeader); // Some sort of unwinding error. Note that terminate is a handler. __cxa_begin_catch (&header->exc.unwindHeader); std::terminate (); }
catch
會被轉成 __cxa_begin_catch
和 __cxa_end_catch
,__cxa_begin_catch
和 __cxa_end_catch
又被稱作 landing pad。catch
可以處理該例外,跳到 __cxa_begin_catch
和 __cxa_end_catch
。處理例外之後,正常返回。catch
可以處理該例外,跳到 _Unwind_Resume
。__gxx_personality_v0
。__gxx_personality_v0
透過 LSDA 知道其所屬函式是否可以處理當前的例外。.gcc_except_table
段。_Unwind_RaiseException
調用 __gxx_personality_v0
檢查 .gcc_except_table
段中的 LSDA 得知該函式是否可以處理當前的例外。__gxx_personality_v0
。_Unwind_Reason_Code __gxx_personality_v0 ( int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception* unwind_exception, _Unwind_Context* context)
_Unwind_RaiseException
調用 personality function,檢查當前棧是否可以處理目前的例外。personality function 接受 _UA_SEARCH_PHASE
表示此為第一輪查找,返回 _URC_HANDLER_FOUND
表示當前棧可以處理目前的例外,返回 _URC_CONTINUE_UNWIND
表示繼續棧回朔。_Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE _Unwind_RaiseException(struct _Unwind_Exception *exc) { /* 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; /* Set up fs to describe the FDE for the caller of cur_context. The first time through the loop, that means __cxa_throw. */ 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. Usually 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; } /* Update cur_context to describe the same frame as fs. */ uw_update_context (&cur_context, &fs); }
_Unwind_RaiseException
第一次進入迴圈之時,其 caller 即為 __cxa_throw
。(gdb) bt #0 __gxx_personality_v0 (version=1, actions=1, exceptionClass=0, unwind_exception=0x555555756040 <completed>, context=0x7fffffffdc20) at mycppabi.cpp:76 #1 0x00007ffff7bce28b in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1 #2 0x0000555555554a99 in __cxa_throw (thrown_exception=0x555555756060 <exception_buff>, tinfo=0x555555755d88 <typeinfo for Exception>, dest=0x0) at mycppabi.cpp:54 #3 0x0000555555554901 in raise () at throw.cpp:8 #4 0x000055555555490f in try_but_dont_catch () at throw.cpp:14 #5 0x0000555555554977 in catchit () at throw.cpp:25 #6 0x0000555555554a12 in seppuku () at throw.cpp:37 #7 0x00005555555548d8 in main () at main.c:5
_URC_HANDLER_FOUND
,_Unwind_RaiseException
調用 _Unwind_RaiseException_Phase2
。personality function 接受 _UA_CLEANUP_PHASE
表示此為第二輪清理。static _Unwind_Reason_Code _Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc, struct _Unwind_Context *context, unsigned long *frames_p) { 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. */ gcc_assert (!match_handler); uw_update_context (context, &fs); frames++; }
_URC_HANDLER_FOUND
表示可處理例外的 catch 已被找到; 第二輪清理,應返回 _URC_INSTALL_CONTEXT
表示應當恢復執行。此時必須要指定於哪一個 catch (即 landing pad) 恢復執行。__gxx_personality_v0
透過 LSDA 知道其所屬函式是否可以處理當前的例外。_Unwind_Reason_Code __gxx_personality_v0 ( int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception* unwind_exception, _Unwind_Context* context) { if (actions & _UA_SEARCH_PHASE) { printf("Personality function, lookup phase\n"); return _URC_HANDLER_FOUND; } else if (actions & _UA_CLEANUP_PHASE) { printf("Personality function, cleanup\n"); const uint8_t* lsda = (const uint8_t*)_Unwind_GetLanguageSpecificData(context); uintptr_t ip = _Unwind_GetIP(context) - 1; uintptr_t funcStart = _Unwind_GetRegionStart(context); uintptr_t ipOffset = ip - funcStart; return _URC_INSTALL_CONTEXT; } else { printf("Personality function, error\n"); return _URC_FATAL_PHASE1_ERROR; } }
_Unwind_GetLanguageSpecificData
: 取得當前棧的 LSDA。我們可以在 LSDA 找到 landing pad。_Unwind_GetIP
: 取得當前 PC 位址。基本上就是調用丟出例外函式之後所在的位址。(lldb) expr -f hex -- ip (uintptr_t) $4 = 0x000000010000085c (lldb) dis -a ip -m 11 try { ** 12 raise(); 0x100000854 <+4>: subq $0x30, %rsp 0x100000858 <+8>: callq 0x100000820 ; raise at throw.cpp:6 0x10000085d <+13>: jmp 0x100000862 ; <+18> at throw.cpp:13
_Unwind_GetRegionStart
: 取得當前棧的起始位址。可以於 gdb 反匯編該位址,看到函式 try_but_dont_catch
。.gcc_except_table
段中的 LSDA,以得到該函式 landing pad 相關訊息。.LFE1: .globl __gxx_personality_v0 .section .gcc_except_table,"a",@progbits .align 4 .LLSDA1: .byte 0xff # @LPStart format (omit) .byte 0x9b # @TType format (indirect pcrel sdata4) .uleb128 .LLSDATT1-.LLSDATTD1 # @TType base offset .LLSDATTD1: .byte 0x1 # call-site format (uleb128) .uleb128 .LLSDACSE1-.LLSDACSB1 # Call-site table length .LLSDACSB1: .uleb128 .LEHB0-.LFB1 # region 0 start .uleb128 .LEHE0-.LEHB0 # length .uleb128 .L8-.LFB1 # landing pad .uleb128 0x1 # action .uleb128 .LEHB1-.LFB1 # region 1 start .uleb128 .LEHE1-.LEHB1 # length .uleb128 0 # landing pad .uleb128 0 # action .uleb128 .LEHB2-.LFB1 # region 2 start .uleb128 .LEHE2-.LEHB2 # length .uleb128 .L9-.LFB1 # landing pad .uleb128 0 # action .uleb128 .LEHB3-.LFB1 # region 3 start .uleb128 .LEHE3-.LEHB3 # length .uleb128 0 # landing pad .uleb128 0 # action .LLSDACSE1: .byte 0x1 # Action record table .byte 0 .align 4 .long DW.ref._ZTI14Fake_Exception-. .LLSDATT1: .text .size _Z18try_but_dont_catchv, .-_Z18try_but_dont_catchv .section .rodata
try_but_dont_catch
為例,LSDA 將其分成數個區域 (region)。每一個區域都有對應的 call site table entry。void try_but_dont_catch() { try { /* region 0 begin */ raise(); /* region 0 end */ } catch(Fake_Exception&) { /* region 2 begin */ printf("Caught a Fake_Exception!\n"); /* region 2 end */ } /* region 1 begin */ printf("try_but_dont_catch handled the exception\n"); /* region 1 end */ }
.gcc_except_table
段中的 LSDA。透過 _Unwind_SetIP
設置要執行的 landing pad (即 catch)。這裡因為沒有透過 _Unwind_SetGR
將例外傳給 landing pad,landing pad 只能告知 _Unwind
無法處理該例外。當前實現,_Unwind
會陷入尋找可以處理例外的 landing pad 的無窮迴圈。// lsda 是 LSDA 基地址,加上 LSDA header 和 call site table header,得到 call site table。 const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header) + sizeof(LSDA_Call_Site_Header); for (size_t i=0; i < cs_in_table; ++i) { const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)]; LSDA_Call_Site cs(offset); if (cs.cs_lp) { uintptr_t func_start = _Unwind_GetRegionStart(context); _Unwind_SetIP(context, func_start + cs.cs_lp); break; } }
while (lsda < lsda_cs_table_end) { LSDA_CS cs(&lsda); if (cs.lp) { int r0 = __builtin_eh_return_data_regno(0); int r1 = __builtin_eh_return_data_regno(1); _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception)); // Note the following code hardcodes the exception type; // we'll fix that later on _Unwind_SetGR(context, r1, (uintptr_t)(1)); uintptr_t func_start = _Unwind_GetRegionStart(context); _Unwind_SetIP(context, func_start + cs.lp); break; } }
_Unwind_SetGR
傳參給 personality function。第一個參數傳入例外,第二個參數傳入例外的型別。此處我們傳入的型別使得 catch(Fake_Exception&)
實際上變成 catch(…)
。void foo() { L0: try { do_something(); L1: } catch (const Exception1& ex) { ... } catch (const Exception2& ex) { ... } catch (const ExceptionN& ex) { ... } catch (...) { } L2: }
_Unwind_GetIP
取得) 落在 start 和 start + len 之間,我們可以知道當前棧可以處理例外。while (lsda < lsda_cs_table_end) { LSDA_CS cs(&lsda); // If there's no LP we can't handle this exception; move on if (not cs.lp) continue; uintptr_t func_start = _Unwind_GetRegionStart(context); // Calculate the range of the instruction pointer valid for this // landing pad; if this LP can handle the current exception then // the IP for this stack frame must be in this range uintptr_t try_start = func_start + cs.start; uintptr_t try_end = func_start + cs.start + cs.len; // Check if this is the correct LP for the current try block if (throw_ip < try_start) continue; if (throw_ip > try_end) continue; // We found a landing pad for this exception; resume execution int r0 = __builtin_eh_return_data_regno(0); int r1 = __builtin_eh_return_data_regno(1); _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception)); // Note the following code hardcodes the exception type; // we'll fix that later on _Unwind_SetGR(context, r1, (uintptr_t)(1)); _Unwind_SetIP(context, func_start + cs.lp); break; }
.gcc_except_table
段中有 call site table,action table 和 type table。type table 紀錄型別資訊。.LLSDACSE1: .byte 0x1 # Action record table .byte 0 .align 4 .long DW.ref._ZTI14Fake_Exception-. .LLSDATT1: # Type table 結尾。
DW.ref._ZTI14Fake_Exception-.
紀錄著 _ZTI14Fake_Exception
型別資訊所在位址。_ZTI14Fake_Exception: .quad _ZTVN10__cxxabiv117__class_type_infoE+16 .quad _ZTS14Fake_Exception .weak _ZTS14Fake_Exception .section .rodata._ZTS14Fake_Exception,"aG",@progbits,_ZTS14Fake_Exception,comdat .align 16 .type _ZTS14Fake_Exception, @object .size _ZTS14Fake_Exception, 17
@TType base offset
)。.LLSDA1: .byte 0xff # @LPStart format (omit) .byte 0x9b # @TType format (indirect pcrel sdata4) .uleb128 .LLSDATT1-.LLSDATTD1 # @TType base offset
.LLSDACSB1: .uleb128 .LEHB0-.LFB1 # region 0 start .uleb128 .LEHE0-.LEHB0 # length .uleb128 .L8-.LFB1 # landing pad .uleb128 0x1 # action
.LLSDACSE1: .byte 0x1 # Action record table .byte 0 .align 4 .long DW.ref._ZTI14Fake_Exception-. # catch 可以處理例外的型別。 .LLSDATT1:
__cxa_throw
會將例外的型別傳遞給 _Unwind_RaiseException
。extern "C" __cxa_refcounted_exception* __cxxabiv1:: __cxa_init_primary_exception(void *obj, std::type_info *tinfo, void (_GLIBCXX_CDTOR_CALLABI *dest) (void *)) _GLIBCXX_NOTHROW { __cxa_refcounted_exception *header = __get_refcounted_exception_header_from_obj (obj); header->exc.exceptionType = tinfo; return header; } extern "C" void __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo, void (_GLIBCXX_CDTOR_CALLABI *dest) (void *)) { __cxa_refcounted_exception *header = __cxa_init_primary_exception(obj, tinfo, dest); _Unwind_RaiseException (&header->exc.unwindHeader); }
_Unwind_RaiseException
調用 personality function 檢視 LSDA 中的型別,和當前例外的型別對照,決定 landing pad 是否能處理該例外。.gcc_except_table
中的 call site table,action table 和 type table 以找到 landing pad 可以處理的例外型別。_Unwind_Resume
。