在我們理解異常的路程上,我們發現重擔在libstdc++裡完成,如c++ abi說明的那樣。閱讀了一些鏈結器錯誤,我們上次推斷要處理異常我們需要c++ abi的輔助;我們建立了乙個丟擲異常的c++程式,把它與乙個c程式鏈結,發現編譯器有時把我們的throw指令翻譯為某些現在呼叫幾個libstdc++函式的物件來實際丟擲異常。已經迷失了?你可以在我的github repo
裡檢查這個專案的源**。
無論如何,我們希望確切理解異常是如何丟擲的,因此我們將嘗試實現我們自己的小abi,能夠丟擲異常。要做到這,需要許多rtfm,不過在這裡
可以找到乙個用於llvm的完整abi介面。讓我們先回憶一下這些缺少的函式是什麼開始:
2. throw.o: in function `foo()':
3. throw.cpp:4: undefined reference to `__cxa_allocate_exception'
4. throw.cpp:4: undefined reference to `__cxa_throw'
5. throw.o:(.rodata._zti9exception[typeinfo for exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
6. collect2: ld returned 1 exit status
__cxa_allocate_exception
我覺得名字不言自明。__cxa_allocate_exception接受乙個size_t,分配足夠的記憶體來儲存要丟擲的異常。這比你想象的要複雜得多:在要丟擲乙個異常時,棧會發生一些神奇的事情,因此在這裡分配資源不是乙個好主意。不過在堆上分配記憶體也不是乙個好主意,因為如果我們耗盡記憶體,我們可能要丟擲異常。靜態分配同樣不是好主意,因為我們需要這是執行緒安全的(否則兩個執行緒同時訪問同樣悲劇)。鑑於這些限制,絕大多數實現看起來在乙個區域性執行緒儲存(堆)上分配記憶體,如果記憶體耗盡轉向乙個緊急儲存(大概是靜態的)。當然我們不希望操心那些醜陋的細節,因此如果願意我們可以只有乙個靜態緩衝。
__cxa_throw
這個函式執行所有的丟擲魔術!根據abi文獻,一旦建立了異常,__cxa_throw將被呼叫。這個函式將負責啟動棧回滾。這的乙個重要後果是:__cxa_throw從不預期會返回。它要麼把執行委託給正確的catch塊來處理異常,要麼(預設)呼叫std::terminate,但它從不返回。
用於__cxxabiv1::__class_type_info的vtable
一件離奇的事……__class_type_info顯然是某種rtti,但它究竟是什麼?現在這是不容易回答的,並且對我們的小abi而言它不是特別重要;我們把它放在附錄裡,留待我們完成丟擲異常過程分析之後,現在我們只說這是abi定義的入口,以(在執行時)知曉兩個型別是否相同。這是呼叫來確定乙個catch(父親)是否能處理乙個throw孩子的函式。目前我們將關注在基礎:我們需要給它乙個位址用於鏈結器(即定義它是不足夠的,我們需要具現它),並且它必須有乙個vtable(即,它必須有虛函式)。
在這些函式上發生了很多事情,但讓我們嘗試實現盡可能簡單的異常丟擲器:當乙個異常丟擲時,呼叫exit。我們的應用程式幾乎沒有問題,但缺少某些abi內容,因此讓我們建立乙個mycppabi.cpp。閱讀我們的abi規範,可以得出__cxa_allocate_exception與__cxa_throw的署名:
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
26
27
28
29
30
31
32
33
34
35
36
#include
#include
#include
namespace __cxxabiv1
} ti;
}
#define exception_buff_size 255
char exception_buff[exception_buff_size];
extern "c"
void __cxa_free_exception(void *thrown_exception);
#include
void __cxa_throw(
void* thrown_exception,
struct type_info *tinfo,
void (*dest)(void*))
} // extern "c"
備註:你可以從我的github repo
如果我們現在編譯mycppabi.cpp並把它與其他兩個.o檔案鏈結,我們將得到乙個可工作的二進位制檔案,它將列印「alloc ex 1\nthrow」,然後退出。相當簡單,但這是乙個驚人的壯舉:我們設法丟擲乙個異常而沒有呼叫libc++。我們已經編寫了c++ abi乙個(非常小的)部分!
通過建立我們自己的小abi,我們獲得的另乙個重要的知識:throw關鍵字被編譯為libstdc++的兩個函式。這裡沒有雙關語,它實際上是相當簡單的翻譯。我們甚至可以反彙編我們的丟擲函式來驗證它。讓我們執行這個命令「g++ -s throw.cpp」。
1
2
3
4
5
6
7
8
9
seppuku:
.lfb3:
[...]
call __cxa_allocate_exception
movl $0, 8(%esp)
movl $_zti9exception, 4(%esp)
movl %eax, (%esp)
call __cxa_throw
[...]
更神奇的事情發生了:在throw關鍵字被翻譯為這個兩個呼叫,編譯器甚至不知道怎樣處理異常。因為libstdc++是定義__cxa_throw及其朋友的地方,且libstdc++是在執行時動態鏈結的,在第一次執行我們的可執行檔案時,可以選擇異常處理方法。
現在我們看到了一些進展,但我們仍然有很長的路要走。我們的abi僅能丟擲異常。我們可以擴充套件它來處理乙個捕捉嗎?我們下一節來看。
C 異常的幕後(1)
每個人都知道良好的異常處理是困難的。在異常 生命期 的每個層面,出現這種情況的原因有許多 編寫異常安全的 是困難的,異常可能從不期望的位置丟擲 雙關語 理解設計不良的異常架構是複雜的,因為幕後發生了許多巫術,它是慢的 因為不正確地丟擲異常可能導致呼叫不可原諒的std terminate,它是危險的。...
C 異常的幕後11 閱讀CFI表
nicolasbrailo 要從我們已經為我們的abi實現的personality函式裡正確處理異常,我們需要閱讀lsda 語言特定資料區 來了解哪個呼叫幀 即哪個函式 可以處理哪個異常,以及了解 可以找到著陸墊 catch塊 lsda是cfi格式的,我們將在本章裡學習如何讀它。讀cfi資料是相當直...
C 異常的幕後8 兩階段處理
nicolasbrailo 上一章以新增乙個 unwind 能夠呼叫的personality函式而結束。它沒做什麼,但它在那裡。我們已經實現的abi現在可以丟擲異常,捕捉也已經完成一半,但需要正確選擇catch塊 著陸墊 的personality函式目前有點傻。我們通過嘗試理解personality...