幾周前寫了一篇文章詳細介紹stack unwinding。 今天介紹C++ exception handling,stack unwinding的一個應用。Exception handling有多種ABI(interoperability of C++ implementations),其中應用最廣泛的是Itanium C++ ABI: Exception Handling
Itanium C++ ABI: Exception Handling
簡化的exception處理流程(從throw到catch):
- 調用
__cxa_allocate_exception分配空間存放exception object和exception header__cxa_exception - 跳轉到
__cxa_throw,設置__cxa_exception字段後跳轉到_Unwind_RaiseException _Unwind_RaiseException執行search phase,調用personality查找匹配的try catch(類型匹配)_Unwind_RaiseException執行cleanup phase:調用personality查找包含out-of-scope變量的stack frames,對於每個stack frame,跳轉到其landing pad執行destructors。該landing pad用_Unwind_Resume跳轉回cleanup phase_Unwind_RaiseException執行的cleanup phase跳轉到匹配的try catch對應的landing pad- 該landing
pad調用
__cxa_begin_catch,執行catch代碼,然後調用__cxa_end_catch __cxa_end_catch銷毀exception object
注意:每個棧幀的personality routine可以不同。實踐中多個棧幀使用同一個personality routine是很常見的。
其中_Unwind_RaiseException負責stack
unwinding,是語言無關的。而stack unwinding中的語言相關概念(catch
block、out-of-scope variable)用personality解釋/封裝。
這是一個核心思想,使得該ABI可以應用與其他語言並允許其他語言和C++混用。
因此,Itanium C++ ABI: Exception Handling分成Level 1 Base ABI and
Level 2 C++ ABI兩部分。Base ABI描述了語言無關的stack
unwinding部分,定義了_Unwind_* API。常見實現是:
- libgcc:
libgcc_s.so.1andlibgcc_eh.a - 多個名稱爲libunwind的庫(
libunwind.so或libunwind.a)。使用Clang的話可以用--rtlib=compiler-rt --unwindlib=libunwind選擇鏈接libunwind,可以用llvm-project/libunwind或nongnu.org/libunwind
C++ ABI則和C++語言相關,定義了__cxa_*
API(__cxa_allocate_exception, __cxa_throw,
__cxa_begin_catch等)。常見實現是:
- libsupc++,libstdc++的一部分
- llvm-project中的libc++abi
llvm-project中的C++標準庫實現libc++可以接入libc++abi、libcxxrt或libsupc++,推薦使用libc++abi。
Level 1 Base ABI
Data structures
主要數據結構是:
1 | // Level 1 |
1 | int main() { |
exception_class和exception_cleanup是Level
2拋出exception的API設置的。Level 1
API不處理exception_class,只是把它傳遞給personality
routine。Personality routine用該值區分native和foreign exceptions。
libc++abi
__cxa_throw會設置exception_class爲表示"CLNGC++\0"的uint64_t。libsupc++則使用表示"GNUCC++\0"的uint64_t。ABI要求低位包含"C++\0"。 libstdc++拋出的exceptions會被libc++abi當作foreign exceptions。只有catch (...)可以捕獲foreign exceptions。
Exception propagation實現機制會用另一個
exception_class標識符來表示dependent exceptions。
exception_cleanup存放這個exception object的destroying
delete函數,被__cxa_end_catch用來銷毀一個foreign
exception。
private_1和private_2是Level
1私有的,不應被personality使用。
Unwind操作需要的信息(對於給定的IP/SP,如何獲取上一層棧幀的IP/SP等寄存器信息)是實現相關的,Level
1 ABI沒有定義。
在ELF系統裏,.eh_frame和.eh_frame_hdr(PT_EH_FRAME
program header)存儲unwind信息。 參見Stack
unwinding。
Level 1 API
_Unwind_Reason_Code _Unwind_RaiseException(_Unwind_Exception *obj);執行用於exception的stack
unwinding。 它正常情況下是noreturn的,會像longjmp那樣把控制權交給matched
catch handler(catch block)或non-catch
handlers(需要執行destructors的代碼塊)。 它是個two-phase
process,分爲phase 1 (search phase)和phase 2 (cleanup phase)。
- search phase查找matched catch handler,把stack
pointer記錄在
private_2中- 根據IP/SP及其他保存的寄存器追溯調用鏈
- 對於每個棧幀,如果沒有personality
routine則跳過;有則調用(actions設置爲
_UA_SEARCH_PHASE) - 若personality返回
_URC_CONTINUE_UNWIND,繼續搜索 - 若personality返回
_URC_HANDLER_FOUND,表示找到了一個matched catch handler or unmatched exception specification,停止搜索
- cleanup phase跳轉到non-catch handlers(通常是local variable
destructors),再把控制權交給phase 1定位的matched catch handler
- 根據IP/SP及其他保存的寄存器追溯調用鏈
- 對於每個棧幀,如果沒有personality
routine則跳過;有則調用(actions設置爲
_UA_CLEANUP_PHASE,search phase標記的棧幀還會設置_UA_HANDLER_FRAME) - 若personality返回
_URC_CONTINUE_UNWIND,表示沒有landing pad,繼續unwind - 若personality返回
_URC_INSTALL_CONTEXT,表示有landing pad,跳轉到landing pad - 對於search phase沒有標記的中間棧幀,landing
pad執行清理工作(一般是destructors of out-of-scope
variables),會調用
_Unwind_Resume跳轉回cleanup phase - 對於被search phase標記的棧幀,landing
pad調用
__cxa_begin_catch,然後執行catch block中的代碼,最後調用__cxa_end_catch銷毀exception object
1 | static _Unwind_Reason_Code unwind_phase1(unw_context_t *uc, _Unwind_Context *ctx, |
C++不支持resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised),所以two-phase process不是必需的,但two-phase允許C++和其他語言共存於call stack上。
_Unwind_Reason_Code _Unwind_ForcedUnwind(_Unwind_Exception *obj, _Unwind_Stop_Fn stop, void *stop_parameter);執行forced
unwinding: 跳過search phase,執行稍微不同的cleanup
phase。private_2被用作stop function的參數。
這個函數很少用到。
void _Unwind_Resume(_Unwind_Exception *obj);繼續phase
2的unwind過程。它類似longjmp,是noreturn的,是唯一被編譯器直接調用的Level
1 API。編譯器通常在non-catch handlers末尾調用該函數。
void _Unwind_DeleteException(_Unwind_Exception *obj);銷毀指定的exception
object。它是唯一處理exception_cleanup的Level 1
API,被__cxa_end_catch調用。
很多實現提供擴展:_Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn callback, void *ref);是另一種特殊的unwind過程:忽略personality,將棧幀信息通知一個外部callback。
Level 2 C++ ABI
這一部分處理C++的throw、catch block、out-of-scope variable destructors等語言相關概念。
Data structures
每個thread有一個全局exception棧,caughtExceptions存儲棧頂(最新)的exception,__cxa_exception::nextException指向棧中下一個exception。
1
2
3
4struct __cxa_eh_globals {
__cxa_exception *caughtExceptions;
unsigned uncaughtExceptions;
};
1 | int main() { |
__cxa_exception的定義如下,其末尾存放Base
ABI定義的_Unwind_Exception。__cxa_exception在_Unwind_Exception基礎上添加了C++語義信息。
1 | // Level 2 |
處理exception需要的信息(對於給定的IP,是否在try catch中、是否有需要執行的out-of-scope variable destructors、是否有dynamic exception specification)叫作language-specific data area (LSDA),是實現相關的,Level 2 ABI沒有定義。
Landing pad
Landing pad是text section中的一段和exception相關的代碼,它有三種:
- cleanup clause:通常調用destructors of out-of-scope
variables或
__attribute__((cleanup(...)))註冊的callbacks,然後用_Unwind_Resume跳轉回cleanup phase - 捕獲exception的catch clause:調用destructors of out-of-scope
variables,然後調用
__cxa_begin_catch,執行catch代碼,最後調用__cxa_end_catch - rethrow:調用destructors of out-of-scope variables in the catch
clause,然後調用
__cxa_end_catch,接着用_Unwind_Resume跳轉回cleanup phase
如果一個try有多個catch,那麼language-specific data
area裏會有多個串聯的action table entries,但landing pad描述合併的catch
clauses。 Personality在轉移控制權給landing
pad前,會調用_Unwind_SetGP設置__buitin_eh_return_data_regno(1)存放switchValue,告知landing
pad哪一個類型匹配了。
Rethrow是在執行catch代碼中間被__cxa_rethrow觸發的,需要destruct
catch clause定義的局部變量,調用__cxa_end_catch抵消catch
clause開頭調用的__cxa_begin_catch。
.gcc_except_table
ELF系統裏language-specific data
area通常存儲在.gcc_except_table
section中。該section被__gxx_personality_v0和__gcc_personality_v0解析。它的結構很簡單:
- header(@LPStart、@TType和call sites的編碼,action records的起始偏移)
- call site table: 描述每個call site(一個地址區間)應執行的landing pad offset (0 if not exists)和action record offset (biased by 1, 0 for no action)
- action table
- type table (referennced by postive switch values)
- dynamic exception specification (deprecated in C++, so rarely used) (referenced by negative switch values)
下面是一個例子:
1 | .section .gcc_except_table,"a",@progbits |
每個call site record除了call site offset和length外還有兩個值landing pad offset和action record offset。
- landing pad offset爲0。action record offset也應爲0。沒有landing pad
- landing pad offset非0。有landing pad
- action record
offset爲0,也叫做cleanup("cleanup"這個描述有些歧義,因爲Level 1有clean
phase的術語),通常描述local variable
destructors和
__attribute__((cleanup(...))) - action record offset非0。action record offset指向action table中一條action record。catch or noexcept specifier or exception specification
- action record
offset爲0,也叫做cleanup("cleanup"這個描述有些歧義,因爲Level 1有clean
phase的術語),通常描述local variable
destructors和
每個action record有兩個值:
- switch value (SLEB128): 正數表示catch的類型的TypeInfo在type table中的下標;負數表示type table中一個exception specification的offset;0表示cleanup action,效果類似於call site record中action record offset爲0
- offset to next action record: 須要處理的下一個action record,0表示結束。這種單鏈表形式可以描述串聯的多個catch,或exception specification list
offset to next action record不僅可以用作單鏈表,也可用作trie,但幾乎碰不到可以用上trie性質的場景。
程序中不同區域對應的landing pad offset/action record offset取值:
- 無local variable
destructor的非try區域:
landing_pad_offset==0 && action_record_offset==0 - 有local variable
destructor的非try區域:
landing_pad_offset!=0 && action_record_offset==0。phase 2應停下調用cleanup - 有
__attribute__((cleanup(...)))的變量的非try區域:landing_pad_offset!=0 && action_record_offset==0。同上 - try區域:
landing_pad_offset!=0 && action_record_offset!=0。landing pad指向catch拼接得到的代碼塊。action record爲大於0的type filter描述一個catch - try區域,含
catch (...):同上。action record爲大於0的type filter指向type table中一個值0的項(表示catch any) - 在一個含noexcept specifier的函數可能propagate
exception到caller的區域:
landing_pad_offset!=0 && action_record_offset!=0。landing pad指向調用std::terminate的代碼塊。action record爲大於0的type filter指向type table中一個值0的項(表示catch any) - 在一個含exception specifier的函數可能propagate
exception到caller的區域:
landing_pad_offset!=0 && action_record_offset!=0。landing pad指向調用__cxa_call_unexpected的代碼塊。action record爲小於0的type filter描述一個exception specifier list
Level 2 API
void *__cxa_allocate_exception(size_t thrown_size);。編譯器爲throw A();生成該函數的調用,分配一段內存存放__cxa_exception和A
object。__cxa_exception緊挨在A object左側。
下面這個函數說明了程序操作的exception
object的地址和__cxa_exception的關係: 1
2
3static void *thrown_object_from_cxa_exception(__cxa_exception *exception_header) {
return static_cast<void *>(exception_header + 1);
}
void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *));調用上述函數找到__cxa_exception
header,填充各個字段(referenceCount, exception_class, unexpectedHandler, terminateHandler, exceptionType, exceptionDestructor, unwindHeader.exception_cleanup)後調用_Unwind_RaiseException。這個函數是noreturn的。
void *__cxa_begin_catch(void *obj);。編譯器在catch
block的開頭生成該函數的調用。對於native exception:
- 加
handlerCount - 壓入該thread的全局exception棧,減少
uncaught_exception值 - 返回adjusted pointer of the exception object
對於foreign exception(不一定有__cxa_exception
header):
- 該thread的全局exception棧爲空的話則push,否則執行
std::terminate(不知道是否有類似__cxa_exception::nextException的字段) - 返回
static_cast<_Unwind_Exception *>(obj) + 1(假設_Unwind_Exception緊挨着thrown object)
簡化實現: 1
2
3
4
5
6
7
8
9
10
11
12void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)) {
__cxa_exception *hdr = (__cxa_exception *)thrown - 1;
hdr->exceptionType = tinfo; hdr->destructor = destructor;
hdr->unexpectedHandler = std::get_unexpected();
hdr->terminateHandler = std::get_terminate();
hdr->unwindHeader.exception_class = ...;
__cxa_get_globals()->uncaughtExceptions++;
_Unwind_RaiseException(&hdr->unwindHeader);
// Failed to unwind, e.g. the .eh_frame FDE is absent.
__cxa_begin_catch(&hdr->unwindHeader);
std::terminate();
}
void __cxa_end_catch();在catch
block末尾或rethrow時被調用。對於native exception:
- 從該thread的全局exception棧上獲取當前exception,減少
handlerCount handlerCount到〇則pop該thread的全局exception棧- 如果是native
exception:
handlerCount減少到0時調用__cxa_free_exception(有dependent exception時得減少referenceCount,到0時調用__cxa_free_exception)
對於foreign exception:
- 調用
_Unwind_DeleteException - 執行
__cxa_eh_globals::uncaughtExceptions = nullptr;(由於__cxa_begin_catch性質,棧中有恰好一個exception)
void __cxa_rethrow();會標註exception
object,使handlerCount被__cxa_end_catch減低到0時不會被銷毀,因爲這個object會被_Unwind_Resume恢復的cleanup
phase復用。
注意,除了__cxa_begin_catch和__cxa_end_catch,多數__cxa_*函數無法處理foreign
exceptions(沒有__cxa_exception header)。
實例
對於如下代碼: 1
2
3
4
5
6
struct A { ~A(); };
struct B { ~B(); };
void foo() { throw 0xB612; }
void bar() { B b; foo(); }
void qux() { try { A a; bar(); } catch (int x) { puts(""); } }
編譯得到的彙編概念上長這樣:
1 | void foo() { |
運行流程:
- qux調用bar,bar調用foo,foo拋出exception
- foo動態分配內存塊,存放拋出的int和
__cxa_exceptionheader,然後執行__cxa_throw __cxa_throw填充__cxa_exception的其他字段,調用_Unwind_RaiseException
接下來_Unwind_RaiseException驅動Level 1的two-phase
process。
_Unwind_RaiseException執行phase 1: search phase- 對於bar,以
_UA_SEARCH_PHASE爲actions參數調用personality,返回_URC_CONTINUE_UNWIND(沒有catch handler) - 對於qux,以
_UA_SEARCH_PHASE爲actions參數調用personality,返回_URC_HANDLER_FOUND(有catch handler) - 標記qux的棧幀的stack
pointer會被標記(保存在
private_2中),並停止搜索
- 對於bar,以
_Unwind_RaiseException執行phase 2: cleanup phase- bar的棧幀不是search
phase標記的,以
_UA_CLEANUP_PHASE爲actions參數調用personality,返回_URC_INSTALL_CONTEXT - 跳轉到bar的棧幀的landing pad
- landing pad清理b之後用
_Unwind_Resume回到cleanup phase - qux的棧幀是search
phase標記的,以
_UA_CLEANUP_PHASE|_UA_HANDLER_FRAME爲actions參數調用personality,返回_UA_INSTALL_CONTEXT - 跳轉到qux棧幀的landing pad
- landing
pad調用
__cxa_begin_catch,執行catch代碼,然後調用__cxa_end_catch
- bar的棧幀不是search
phase標記的,以
__gxx_personality_v0
Personality routine被Level 1 phase 1和phase 2調用,用於提供語言相關處理。不同的語言、實現或架構可能使用不同的personality routines。常見的personality如下:
__gxx_personality_v0: C++__gxx_personality_sj0: sjlj__gcc_personality_v0: C-fexceptions,用於__attribute__((cleanup(...)))__CxxFrameHandler3: Windows MSVC__gxx_personality_seh0: MinGW-w64-fseh-exceptions__objc_personality_v0: MacOSX環境ObjC
C++在ELF系統上的實現最常用的是__gxx_personality_v0,其實現在:
- GCC:
libstdc++-v3/libsupc++/eh_personality.cc - libc++abi:
src/cxa_personality.cpp
_Unwind_Reason_Code (*__personality_routine)(int version, _Unwind_Action action, uint64 exceptionClass, _Unwind_Exception *exceptionObject, _Unwind_Context *context);
沒有錯誤的情況下:
- For
_UA_SEARCH_PHASE, returns_URC_CONTINUE_UNWIND: no lsda, or there is no landing pad, there is a non-catch handler or a matched exception specification_URC_HANDLER_FOUND: there is a matched catch handler or an unmatched exception specification
- For
_UA_CLEANUP_PHASE, returns_URC_CONTINUE_UNWIND: no lsda, or there is no landing pad, or (not produced by a compiler) there is no cleanup action_URC_INSTALL_CONTEXT: the other cases
Personality轉移控制權給landing
pad前,會調用_Unwind_SetGP設置兩個寄存器(架構相關,__buitin_eh_return_data_regno(0)和__buitin_eh_return_data_regno(1))存放_Unwind_Exception *和switchValue。
代碼:
1 | _unwind_Reason_Code __gxx_personality_v0(int version, _Unwind_Action actions, uint64_t exceptionClass, _Unwind_Exception *exc, _Unwind_Context *ctx) { |
對於native exception,search phase
personality返回_URC_HANDLER_FOUND時會緩存該棧幀的LSDA相關信息。在cleanup
phase再度調用personality時actions == (_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME),personality知道可以讀取緩存,不需要解析.gcc_except_table。
在剩下三種情況下會調用scan_eh_tab解析.gcc_except_table:
actions & _UA_SEARCH_PHASEactions & _UA_CLEANUP_PHASE && actions & _UA_HANDLER_FRAME && !is_native: foreign exception可以被catch (...)捕獲,但遇到exception specification則應terminateactions & _UA_CLEANUP_PHASE && !(actions & _UA_HANDLER_FRAME): non-catch handlers and unmatched catch handlers, matched exception specification。還有一種可能是_Unwind_ForcedUnwind的phase 2
1 | static void scan_eh_tab(...) { |
__gcc_personality_v0
libgcc and
compiler-rt/lib/builtins實現了這個函數來處理__attribute__((cleanup(...)))。
默認的實現在search
phase沒有返回_URC_HANDLER_FOUND,所以cleanup
handler不能用作catch handler。 然而,我們可以提供自己的實現在search
phase返回_URC_HANDLER_FOUND...
在x86-64上,__buitin_eh_return_data_regno(0)是RAX。我們可以讓cleanup
handler傳遞RAX給landing pad。
1 | // a.cc |
1 | % clang -c -fexceptions a.cc b.c |
Rethrow
前面Landing pad節簡述了rethrow執行的代碼。通常caught
exception會在__cxa_end_catch銷毀,因此__cxa_rethrow會標記exception
object並增加handlerCount。
C++11 引入了Exception Propagation (N2179;
std::rethrow_exception
etc),libstdc++中使用__cxa_dependent_exception實現。
設計參見https://gcc.gnu.org/legacy-ml/libstdc++/2008-05/msg00079.html
1 | struct __cxa_dependent_exception { |
std::current_exception和std::rethrow_exception會增加引用計數。
在libstdc++裏,
__cxa_rethrow調用GCC擴展_Unwind_Resume_or_Rethrow(能resume
forced unwinding)。
LLVM IR
待補充
- nounwind: cannot unwind
- unwtables: force generation of the unwind table regardless of nounwind
1 | if uwtables |
編譯器行爲
-fno-exceptions -fno-asynchronous-unwind-tables: neither.eh_framenor.gcc_except_tableexists-fno-exceptions -fasynchronous-unwind-tables:.eh_frameexists,.gcc_except_tabledoesn't-fexceptions: both.eh_frameand.gcc_except_tableexist- In GCC, for a
noexceptfunction, a possibly-throwing call site unhandled by a try block does not get an entry in the.gcc_except_tablecall site table. If the function has no try block, it gets a header-only.gcc_except_table(4 bytes) - In Clang, there is a call site entry calling
__clang_call_terminate. The size overhead is larger than GCC's scheme. Improving this requires LLVM IR work
- In GCC, for a
如果某個exception將要propagate到一個function的caller時:
- no
.eh_frame:_Unwind_RaiseExceptionreturns_URC_END_OF_STACK.__cxa_throwcallsstd::terminate .eh_framewithout.gcc_except_table: pass-through (local variable destructors are not called). This is the case of-fno-exceptions -fasynchronous-unwind-tables..eh_framewith empty.gcc_except_table:__gxx_personality_v0callsstd::terminatesince no call site code range matches.eh_framewith proper.gcc_except_table: unwind
結合上述描述,某個exception將要propagate到一個noexcept function的caller時:
-fno-exceptions -fno-asynchronous-unwind-tables: propagating through a function callsstd::terminate-fno-exceptions -fasynchronous-unwind-tables: pass-through. Local variable destructors are not called. This behavior is unexpected.-fexceptions: propagating through anoexceptfunction callsstd::terminate
When std::terminate is called, there is a diagnostic
looking like
terminate called after throwing an instance of 'int'
(libstdc++; libc++ has a smiliar one). There is no stack trace. If the
process installs a SIGABRT signal handler, the handler may
get a stack trace and symbolize the addresses.
Catching exceptions while unwinding through -fno-exceptions code is a proposal to improve the diagnostics.
Personality and typeinfo encoding
.eh_frame contains information about the unwind
operation. See Stack
unwinding for its format.
In -fpie/-fpic mode, the personality and type info
encodings have the DW_EH_PE_indirect|DW_EH_PE_pcrel bits on
most targets. 1
2
3
4
5
6void raise() { throw 42; }
bool foo() {
try { raise(); } catch (int) { return true; }
return false;
}
int main() { foo(); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25_Z3foov:
.cfi_startproc
.cfi_personality 155, DW.ref.__gxx_personality_v0
.cfi_lsda 27, .Lexception0
...
.section .gcc_except_table,"a",@progbits
...
# >> Catch TypeInfos <<
.Ltmp3: # TypeInfo 1
.long .L_ZTIi.DW.stub-.Ltmp3
.Lttbase0:
.data
.p2align 3, 0x0
.L_ZTIi.DW.stub:
.quad _ZTIi
.hidden DW.ref.__gxx_personality_v0
.weak DW.ref.__gxx_personality_v0
.section .data.DW.ref.__gxx_personality_v0,"aGw",@progbits,DW.ref.__gxx_personality_v0,comdat
.p2align 3, 0x0
.type DW.ref.__gxx_personality_v0,@object
.size DW.ref.__gxx_personality_v0, 8
DW.ref.__gxx_personality_v0:
.quad __gxx_personality_v0
In the example, .eh_frame contains a PC-relative
relocations referencing DW.ref.__gxx_personality_v0
.gcc_except_table contains a PC-relative relocation
referencing .L_ZTIi.DW.stub. The relocations are link-time
constants, so .eh_frame can remain readonly.
DW.ref.__gxx_personality_v0 and
.L_ZTIi.DW.stub reside in writable sections which will
contain dynamic relocations if __gxx_personality_v0 and
_ZTIi are defined in a shared object - which is often the
case.
For -fno-pic code, different targets have different
ideas. AArch64 and RISC-V use
DW_EH_PE_indirect|DW_EH_PE_pcrel as well. On x86,
.cfi_personality refers to
__gxx_personality_v0. This will lead to a canonical PLT if
__gxx_personality_v0 is defined in a shared object (e.g.
libstdc++.so.6). I sent a patch https://gcc.gnu.org/PR108622 to use
DW_EH_PE_indirect|DW_EH_PE_pcrel.
R_MIPS_32
and R_MIPS_64 personality encoding
https://github.com/llvm/llvm-project/issues/58377
1 | void foo() { try { throw 1; } catch (...) {} } |
mips64el-linux-gnuabi64-g++ -fpic and
clang++ --target=mips64el-unknown-linux-gnuabi64 -fpic use
DW_EH_PE_absptr | DW_EH_PE_indirect to encode personality
routine pointers. Using DW_EH_PE_absptr instead of
DW_EH_PE_pcrel is wrong. GNU ld works around the compiler
design problem by converting DW_EH_PE_absptr to
DW_EH_PE_pcrel. ld.lld does not support this and will
report an error: 1
2
3
4
5
6% clang++ --target=mips64el-linux-gnuabi -fpic -fuse-ld=lld -shared ex.cc
ld.lld: error: relocation R_MIPS_64 cannot be used against symbol 'DW.ref.__gxx_personality_v0'; recompile with -fPIC
>>> defined in /tmp/ex-40a996.o
>>> referenced by ex.cc
>>> /tmp/ex-40a996.o:(.eh_frame+0x13)
...
R_MIPS_32 for 32-bit builds is similar.
Potentially-throwing
__cxa_end_catch
__cxa_end_catch is potentially-throwing because it may
destroy an exception object with a potentially-throwing destructor (e.g.
~C() noexcept(false) { ... }). 1
2
3
4
5
6
7
8struct A { ~A(); };
void opaque();
void foo() {
A a;
// The exception object has an unknown type and may throw. The landing pad
// then needs to call A::~A for `a` before jumping to _Unwind_Resume.
try { opaque(); } catch (...) { }
}
To support an exception object with a potentially-throwing destructor, Clang generates conservative code for a catch-all clause or a catch clause matching a record type:
- assume that the exception object may have a throwing destructor
- emit
invoke void @__cxa_end_catch(as the call is not marked as thenounwindattribute). - emit a landing pad to destroy local variables and call
_Unwind_Resume
Per C++ [dcl.fct.def.coroutine], a coroutine's function body implies
a catch (...). Clang's code generation pessimizes even
simple code, like: 1
2
3
4
5
6
7UserFacing foo() {
A a;
opaque();
co_return;
// For `invoke void @__cxa_end_catch()`, the landing pad destroys the
// promise_type and deletes the coro frame.
}
Throwing destructors are typically discouraged. In many environments, the destructors of exception objects are guaranteed to never throw, making our conservative code generation approach seem wasteful.
Furthermore, throwing destructors tend not to work well in practice:
- GCC does not emit call site records for the region containing
__cxa_end_catch. This has been a long time, since 2000. - If a catch-all clause catches an exception object that throws, both GCC and Clang using libstdc++ leak the allocated exception object.
To avoid code generation pessimization, I added -fassume-nothrow-exception-dtor
for Clang 18 to assume that __cxa_end_catch calls have the
nounwind attribute. This requires that thrown exception
objects' destructors will never throw.
To detect misuses, diagnose throw expressions with a
potentially-throwing destructor. Technically, it is possible that a
potentially-throwing destructor never throws when called transitively by
__cxa_end_catch, but these cases seem rare enough to
justify a relaxed mode.
其他
使用libc++和libc++abi
On Linux, compared with clang, clang++
additionally links against libstdc++/libc++ and libm.
Dynamically link against libc++.so (which depends on libc++abi.so)
(additionally specify -pthread if threads are used):
1 | clang++ -stdlib=libc++ -nostdlib++ a.cc -lc++ -lc++abi |
If compile actions and link actions are separate
(-stdlib=libc++ passes -lc++ but its position
is undesired, so just don't use it):
1 | clang++ -nostdlib++ a.cc -lc++ -lc++abi |
Statically link in libc++.a (which includes the members of
libc++abi.a). This requires a
-DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=on build:
1 | clang++ -stdlib=libc++ -static-libstdc++ -nostdlib++ a.cc -pthread |
Statically link in libc++.a and libc++abi.a. This is a bit inferior because there is a duplicate -lc++ passed by the driver.
1 | clang++ -stdlib=libc++ -static-libstdc++ -nostdlib++ a.cc -Wl,--push-state,-Bstatic -lc++ -lc++abi -Wl,--pop-state -pthread |
libc++abi和libsupc++
值得注意的是,libc++abi提供的<exception> <stdexcept>類型佈局(如logic_error
runtime_error等)都是特意和libsupc++兼容的。 GCC
5的libstdc++拋棄ref-counted
std::string後libsupc++仍使用__cow_string用於logic_error等。libc++abi也使用了類似的ref-counted
string。
libsupc++和libc++abi不使用inline
namespace,有衝突的符號名,因此通常一個libc++/libc++abi應用無法使用某個動態鏈接libstdc++.so的shared
object(ODR violation)。
如果花一些工夫,還是能解決這個問題的:編譯libstdc++中非libsupc++的部分得到自製libstdc++.so.6。可執行檔鏈接libc++abi提供libstdc++.so.6需要的C++
ABI符號。
Monolithic
.gcc_except_table
Clang 12之前採用monolithic
.gcc_except_table。和其他很多metadata
sections一樣,monolithic設計的主要問題是無法被linker garbage collect。
對於RISC-V -mrelax和basic block
sections則會有更大的問題:.gcc_except_table有指向text
sections local symbols的relocations。 如果指向的text sections在COMDAT
group中被丟棄,則這些relocations會被linker拒絕(error: relocation refers to a symbol in a discarded section)。
解決方案就是採用fragmented .gcc_except_table(https://reviews.llvm.org/D83655)。
但實際部署沒有那麼簡單:)LLD先處理--gc-sections(尚不明確哪些.eh_frame
pieces是live的),後處理(包括GC).eh_frame。
--gc-sections時,所有.eh_frame
pieces是live的。它們會標記所有.gcc_except_table.* live。
根據section
group的GC規則,一個.gcc_except_table.*會標註同一section
group的其他sections(包含.text.*) live。 結果就是所有section
groups中的.text.*無法被GC,導致輸入大小增大。
https://reviews.llvm.org/D91579修復了這個問題:對於.eh_frame,不要標註section
group中的.gcc_except_table。
-fbasic-block-sections=
使用basic block sections時,可以選擇每個basic block
section獲得其專屬的.gcc_except_table,或者讓一個函數的所有basic
block
sections使用同一個.gcc_except_table。LLVM實現選擇了後者,有幾個好處:
- No duplicate headers
- Sharable type table
- Sharable action table (this only matters for the deprecated exception specification)
使用同一個.gcc_except_table就只有一個LPStart,得保證所有landing
pads到LPStart的offsets均可以用relocations表示。
因爲多數架構沒有表示差的relocation type,因此把landing
pads放在同一個section是最合適的表示方式。
Exception handling ABI for the ARM architecture
整體結構和Itanium C++ ABI: Exception
Handling相同,數據結構、_Unwind_*等有些許差異。
https://maskray.me/blog/2020-11-08-stack-unwinding含有少量註記。
Compact Exception Tables for MIPS ABIs
用.eh_frame_entry和.gnu_extab描述。
設計理念:
- Exception code ranges are sorted and must be linearly searched. Therefore it would be more compact to specify each relative to the previous one, rather than relative to a fixed base.
- The landing pad is often close to the exception region that uses it. Therefore it is better to use the end of the exception region as the reference point, than use the function base address.
- The action table can be integrated directly with the exception region definition itself. This removes one indirection. The threading of actions can still occur, by providing an offset to the next exception encoding of interest.
- Often the action threading is to the next exception region, so optimizing that case is important.
- Catch types and exception specification type lists cannot easily be encoded inline with the exception regions themselves. It is necessary to preserve the unique indices that are automatically created by the DWARF scheme.
使用和ARM EH類似的compact unwind descriptors。Builtin PR1表示沒有language-dependent data,Builtin PR2用於C/C++