C 語法上使用 goto 或是 setjmp/longjmp 實現例外處理。goto 只能做本地跳轉,亦即在同一函式內部進行跳轉。setjmp/longjmp 則能做到非本地跳轉。setjmp 會保留當前進程的狀態,以供之後的 longjmp 回到 setjmp 所在位置。setjmp/longjmp 並非以 stack unwind 實現1)。
-
- Deep Wizardry: Stack Unwinding
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.
- setjmp 主要保存被調用方保存 (callee-save) 暫存器、棧指針和返回位址於 jmp_buf。jmp_buf 可以是 unsigned char 陣列,大小足夠保存前述資訊即可。
- longjmp 從傳入的 jmp_buf 中回復暫存器和棧指針,載入 setjmp 設置的返回位址。
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.
系列文章
代碼
-
-
- .eh_frame_hdr: 指向 .eh_frame,讓 libstdc++ 和 libgcc 知道 .eh_frame 的位置 (What do the .eh_frame and .eh_frame_hdr sections store, exactly?)。
- .gcc_except_table: 用來得知例外何時可以被處理。規格可以參考 Exception Handling Tables。
- libgcc: unwind-dw2.c
- 關於 CIE 和 FDE,參考 debugger。
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)); } }
-
-
-
- C++ 的
throw
會被轉成__cxa_allocate_exception
和__cxa_throw
。- libstdc++: eh_alloc.cc和 eh_throw.cc。
- libgcc: unwind.inc
__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 (); }
-
- C++ 的
catch
會被轉成__cxa_begin_catch
和__cxa_end_catch
,__cxa_begin_catch
和__cxa_end_catch
又被稱作 landing pad。- libstdc++: eh_catch.cc 和 eh_personality.cc。
-
- 調用函式之後,有底下幾種情況:
- 正常返回。
- 丟出例外,有
catch
可以處理該例外,跳到__cxa_begin_catch
和__cxa_end_catch
。處理例外之後,正常返回。 - 丟出例外,沒有
catch
可以處理該例外,跳到_Unwind_Resume
。
- 有 try-catch 的函式,其開頭會有底下匯編指示符:
- .cfi_personality: 定義 personality function,一般為
__gxx_personality_v0
。 - .cfi_lsda: 定義 LSDA (Language Specific Data Area)。
__gxx_personality_v0
透過 LSDA 知道其所屬函式是否可以處理當前的例外。
-
- 有 try-catch 的函式,在緊鄰其結尾處,會有一個
.gcc_except_table
段。_Unwind_RaiseException
調用__gxx_personality_v0
檢查.gcc_except_table
段中的 LSDA 得知該函式是否可以處理當前的例外。
-
- libstdc++: eh_personality.cc 定義
__gxx_personality_v0
。_Unwind_Reason_Code __gxx_personality_v0 ( int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception* unwind_exception, _Unwind_Context* context)
- version/exceptionClass: 語言/ABI/編譯器相關版本。
- actions: 當前 personality 是執行第一查找 (lookup) 或是第二輪清理 (cleanup)。
- unwind_exception: 當前處理的例外。
- context: 當前棧相關資訊,如: LSDA。
-
_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); }
- 對於註解的理解,可以參考底下的 GDB 輸出。
_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
- 一但 personality function 返回
_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++; }
-
- personality function 於第一輪查找,應返回
_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
。
-
-
- personality function 讀取緊鄰函式之後,
.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 */ }
-
- personality function 讀取緊鄰函式之後,
.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; } }
- C++ exceptions under the hood 13: setting the context for a landing pad
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(…)
。
-
-
- LSDA 基本描述 try-catch 的範圍。
void foo() { L0: try { do_something(); L1: } catch (const Exception1& ex) { ... } catch (const Exception2& ex) { ... } catch (const ExceptionN& ex) { ... } catch (...) { } L2: }
- call site table entry 依序由底下欄位組成:
- start: try block 起始位址在函式中的偏移量,即 L0 – addr_of(foo)。
- len: try block 的長度,即 L1 - L0。
- lp: landing pad 起始位址在函式中的偏移量,即 L1 – addr_of(foo)。
- action: 例外型別相關資訊。
-
- 當 ip (透過
_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
-
- LSDA 標頭紀錄 type table 的偏移 (
@TType base offset
)。.LLSDA1: .byte 0xff # @LPStart format (omit) .byte 0x9b # @TType format (indirect pcrel sdata4) .uleb128 .LLSDATT1-.LLSDATTD1 # @TType base offset
- call site table entry 紀錄 action table 的索引 (非零值減去 1 才是真正的索引。0 表示沒有 action)。
.LLSDACSB1: .uleb128 .LEHB0-.LFB1 # region 0 start .uleb128 .LEHE0-.LEHB0 # length .uleb128 .L8-.LFB1 # landing pad .uleb128 0x1 # action
- 對於帶有 catch 的 landing pad,其 action table entry 紀錄 type table 的反向索引 (即要乘上 -1)。
.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 可以處理的例外型別。
-
- action table entry 的 type index 如果為 0,表示該 landing pad 負責清理 (例如執行解構子),最後調用
_Unwind_Resume
。
-