C 異常的幕後3 取悅鏈結器的ABI

2021-09-12 01:43:43 字數 4457 閱讀 4195

在我們理解異常的路程上,我們發現重擔在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_infovtable

一件離奇的事……__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...