你一定有興趣:
c++ exception handling (2) - 使用 g++ 5.4.0
|
fig 0. 金字塔知識門檻 |
c++ exception handling 還真不是普通的複雜, 我目前僅僅知道其實作原理, 但實作細節太複雜, 沒能搞懂。面試 c++ 時常會看到 virtaul function 如何實作的考題, 但卻沒看過問 c++ exception handling 怎麼實作, 沒有別的原因, 就是因為它難到只有很少人才知道怎麼實作, 不知道怎麼實做 exception handling 一點都不丟臉, 因為連
cfront 也搞不定
挑戰這麼難的東西, 又沒有什麼經濟效益, 我一定是阿達了。廢話不多說, 來看看 gcc 怎麼實作 c++ exception handling。
vc 和 gcc 有不同的作法, 我研究的是 gcc 的作法。
看了不少參考資料, 本篇文章以 binary hacks 繁體中文版 item 38, 39, 40, 41 為主, 因為有個小程式可以用來實驗以及說明 exception handle。
下面這 3 個函式是最主要的關鍵:
1 __cxa_throw
2 _Unwind_RaiseException
3 __gxx_personality_v0 (int version, _Unwind_Action actions, _Unwind_Exception_Class exception_class, struct _Unwind_Exception *ue_header, struct _Unwind_Context *context)
這些函式的 source code 在 gcc libgcc 目錄下, libgcc 是一個很神秘的 library, 裡頭幾乎是 gcc 特異功能的實做。unwind, 軟體浮點數 ... 都是在這裡。
gcc-3.4.4/gcc
gcc-5.4.0/libgcc
_Unwind_SjLj_RaiseException
_Unwind_RaiseException | gcc-5.4.0/libgcc/unwind-sjlj.c
gcc-5.4.0/libgcc/unwind.inc |
#define PERSONALITY_FUNCTION __gxx_personality_v0
PERSONALITY_FUNCTION (int version,
_Unwind_Action actions,
_Unwind_Exception_Class exception_class,
struct _Unwind_Exception *ue_header,
struct _Unwind_Context *context) |
/gcc-5.4.0/libstdc++-v3/libsupc++/eh_personality.cc |
__cxa_throw
extern "C" void __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo, void (_GLIBCXX_CDTOR_CALLABI *dest) (void *)) | gcc-5.4.0/libstdc++-v3/libsupc++/eh_throw.cc |
a.cpp L116 throw 100;
會轉成呼叫 (ref a.cpp L118 ~ 120)
__cxa_allocate_exception()
__cxa_throw()
__cxa_throw() 發動時的流程:
->
執行的是 _Unwind_SjLj_RaiseException
#ifdef _GLIBCXX_SJLJ_EXCEPTIONS
_Unwind_SjLj_RaiseException (&header->exc.unwindHeader);
#else
_Unwind_RaiseException (&header->exc.unwindHeader);
#endif |
|
|
|-> __gxx_personality_sj0
|
|
|-> uw_install_context
uw_install_context 會呼叫 longjmp 回到上一個函式, 以 a.cpp 來說, 就是 func1()。
__gxx_personality_sj0 是幹麻用的? 搜尋是不是有對應的 catch statement, 或是有那個物件需要解構, 得去執行解構函式, 要跳去的那個位址有個很厲害的術語叫做 landing_pad, source code 會看到 landing_pad = info.LPStart + cs_lp;, 就是用來找到要去執行解構函式或是 catch statement 的位址, 一旦 uw_install_context 執行之後, 就會跳去那個位址。
像 func1() 有個物件需要解構, __gxx_personality_sj0 知道這件事情, 所以才要讓 _Unwind_RaiseException 往 func1 跳, 很神奇是吧! 一但 func1() 拿掉 a.cpp L128 那個 Obj obj, 就不會跳回 func1()。
那 __gxx_personality_sj0 怎麼知道這些事情的, 這個就很複雜, 得靠 g++ 在編譯的時候塞入 dwarf 裡頭的資訊, 而要怎麼取出這些資訊也很神秘, 和 CIE 及 FDE 有關, 不過我不知道這 2 個是什麼東西, 也不知道怎麼取出來, 就算讀了 source code, 也還是看不懂。
另外 __gxx_personality_sj0 會比對丟出的例外物件和 catch 的例外物件, 如果一樣, landing_pad 才會往那個 catch 指定, 這就是為什麼 exception handle 需要 rtti 的支援, rtti 的 type_info 物件, 就是拿來比對這 2 個例外物件有沒有一致。
bt.cpp 只有模擬一半的功能, 使用 setjmp/longjmp, back_to_func 可以回到前一個 function, sjlj 就是用類似的方法串起這些 jmp_buf; 不過我不知道怎麼使用 .eh_frame, .gcc_except_table section 裡頭的資料來得知是不是有那個解構函式需要執行, 是不是有符合的 catch statement。
binary hacks 繁體中文版 item 38, 39, 40, 41 是用 gcc 3.4.4 講解, 雖然過時了, 但基本原理是一樣的, 就先從 gcc 3.4.4 的建構開始吧。
g++ 使用 setjmp/longjmp, dwarf 這兩種來支援 c++ exception handle, 目前的 gcc 5 似乎不使用 --enable-sjlj-exceptions, 我比較熟悉 setjmp/longjmp 的作法, dwarf2 太苦了, 我不想走這條路, 先以 --enable-sjlj-exceptions 來建構 gcc 3.4.4。
我以熟悉的 setjmp/long 來學習, 編譯 gcc 3.4.4 加上 --enable-sjlj-exceptions, 即使用以 setjmp/longjmp 實做的 exception handle。
setjmp/longjmp, dwarf 是用來處理 unwind, 就是從目前的函式回到上一個函式, 類似 bt.cpp 做的事情, dwarf 的作法需要去理解 dwarf 格式, 聽說是不得了的複雜, 我不想花時間在上頭, 而 setjmp/longjmp 我已經知道其實作原理, 不需要在花額外的功夫。
另外一個需要的能力就是知道要回到那一個 function, 這就是靠神秘的 LSDA 的內容來得知, g++ 會在 .gcc_except_table section 插入某些資訊, 讓 __gxx_personality_sj0 可以用來判斷要回到那個函式。
env:
32 bit debian
編譯時可能會遇到一些 header 的問題, 我把 /usr/include/i386-linux-gnu/* link 到 /usr/include
root@debian32:/usr/include# ls -l sys
lrwxrwxrwx 1 root root 32 Dec 26 15:42 sys -> /usr/include/i386-linux-gnu/sys
a.cpp 是 binary hack 書上提供的範例, 提供了對照, try/catch/throw 是怎麼轉成一般的 c++ 程式碼, 看上去就清楚了, 最麻煩的就是那個 lsda 到底是怎麼樣的資料結構, 可惜書上也沒寫得很清楚, 看來只能看第 0 手資料了。
list 1 的結果可以成功呼叫解構函式, 以及跑到正確的 catch 程式碼。可以用 gdb 跑跑看, exception handle 的神秘感解除了一半, 另外一半還在 libunwind, libgcc 裡頭的函式。
a.cpp L166 就是 L118 ~ 120 那 3 行; a.cpp L137 ~ 145 就是 L149 ~ 167 那麼多行。
objdump -d a 看不到詳細的反組譯程式碼, 我使用 gdb 來反組譯, 這是意外的收穫。
list 2 L21, L31 有 2 個 dtor, 很奇怪吧, L21 是給 exception handle 用的, 當從 throw 回到 func1 時, 會莫名的抵達這裡, 事實上是回到 L13 0x8048b53 這裡, 然後在執行 L27 回到上一個 stack frame (本例來說就是 main); L31 則是給正常執行流程呼叫的 dtor, L12 有個狡猾的 jmp, 真是機關算盡。
list 3 是 g++ 3.4.4 的反組譯版本, 更清楚了, 我應該早點想到的, 它不只為我解除了 2 個 dtor 的疑惑, 還把莫名會抵達 func1() 的原因也找了出來, 甚至連那個 Lsda 也幫我釐清了, 也因為知道 Lsda 的內容, 我連帶改出 g++ 5.4.0 的版本了。
list 3 是使用 try/catch/throw 的版本, list 3 L302, 303, 是不是和自己填入 a.cpp L151 ~ 153 一樣呢?
list 3 L303, L387 就是那個該死的 lsda, 從 list 3 L387 ~ L402, 在 .gcc_except_table section (就是 LSDA - Language Specific Data Area), 又是另外一個狡猾的地方。
至於 g++ 5.4.0 我怎麼改出來的呢? 就是用 g++ 5.4.0 去反組譯 try/catch/throw 的版本, 把 .gcc_except_table section, 填到那個 lsda 就好了, 果然還真的不同。
再來是那個莫名回到 func1 的動作是怎麼作到的呢? 這個困擾我好久, 用 gdb 追也找不出所以然, 照理說應該要有一個 setjmp 在這裡, 才能透過 longjmp 回到這, 但我就一直找不到哪裡有 call setjmp, 直到我用 g++ -S 之後才看到, 原來 g++ 在 func1 安插了類似 setjmp 的程式碼, 這才讓 _Unwind_RaiseException 有能力回到 func1。
list 3 L182 _Unwind_SjLj_Register 的動作類似 bt.cpp 那個 map<string, jmp_buf>, 把每一個 fuction 要回來的位置記起來, 它的參數 SjLj_Function_Context 裡頭有 jmp_buf, 得先把 jmp_buf 填好才行, 讓 uw_install_context 的 longjmp 回到這裡。
由於是 g++ 插入的 code, 得從組合語言去看出來才行, 還真是難。L177 的 .L18 就是 setjmp 紀錄起來的值, 這裡就是在填上面說的 jmp_buf 的部份, 但並不是產生呼叫 setjmp 的程式碼, 而是填入那個 jmp_buf 所需要的值就可以了, 所以 _Unwind_RaiseException 發動 uw_install_context, 就會回到 L202, 和 gdb 的顯示是一樣的。
把 func1() Obj obj; 拿掉, 再看 g++ 產生的 a.s, 就會發現那個 func1 和 c 的長相一樣, 不會被偷偷插入那麼多程式碼了。
#define uw_install_context(CURRENT, TARGET) \
do \
{ \
_Unwind_SjLj_SetContext ((TARGET)->fc); \
longjmp ((TARGET)->fc->jbuf, 1); \
} \
while (0)
list 3 L172 ~ 173 是不是有類似的行為, 塞入 __gxx_personality_sj0, lsda 這些資料, lsda 是我目前還無法突破的部份。
由於用到 typeinfo 來判斷型別, 這是為什麼 exception handle 需要有 rtti 支援的原因。
從 global object, static object, virtaul function, rtti 到 exception handle, 現在你知道 c++ 有那麼多的黑魔法, c++ 真是不簡單, 這也是為人所詬病的一個特性, 太黑箱了。
在 c++ 這麼多的特性, 我最有興趣的是 virtual function 和 exception handle 的實作, 我已經找了多年的資料, 有點收穫真是開心。
typeid ref:
- typeid详解
- 執行時期型態資訊(RTTI)
- A General-Purpose Run-Time Type Information System for C++
- http://www.cs.rug.nl/~alext/SOFTWARE/RTTI/rtti_doc.html
- https://pdfs.semanticscholar.org/ca44/58d8cb126fe6eae8f19cab6efb9b9fe47c88.pdf
ref:
Visual C++ 的 exception handle:
dwarf:
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。