c++ 異常處理(2)

前面一篇博文簡單介紹了 c++ 異常處理的流程,但在一些細節上一帶而過了,好比,_Unwind_RaiseException 是怎樣重建函數現場的,Personality routine 是怎樣清理棧上變量的等,這些細節涉及到不少與語言層面無關的東西,本文嘗試介紹一下這些細節的具體實現。html

相關的數據結構

如前所述,unwind 的進行須要編譯器生成必定的數據來支持,這些數據保存了與每一個可能拋異常的函數相關的信息以供運行時查找,那麼,編譯器都保存了哪些信息呢?根據 Itanium ABI 的定義,主要包括如下三類linux

1)unwind table,這個表記錄了與函數相關的信息,共三個字段:函數的起始地址,函數的結束地址,一個 info block 指針。ios

2)unwind descriptor table,這個列表用於描述函數中須要unwind的區域的相關信息。c++

3)語言相關的數據(language specific data area),用於上層語言內部的處理。git

以上數據結構的描述來自 Itanium ABI 的標準定義,但在具體實現時,這些數據是怎麼組織以及放到了哪裏則是由編譯器來決定的,對於 GCC 來講,全部與 unwind 相關的數據都放到了 .eh_frame 及 .gcc_except_table 這兩個 section 裏面了,並且它的格式與內容和標準的定義稍稍有些不一樣。github

.eh_frame區域

.eh_frame 的格式與 .debug_frame 是很類似的(不徹底相同),屬於 DWARF 標準中的一部分。全部由 GCC 編譯生成的須要支持異常處理的程序都包含了 DWARF 格式的數據與字節碼,這些數據與字節碼的主要做用有兩個:網絡

1)描述函數調用棧的結構(layout)數據結構

2)異常發生後,指導 unwinder 怎麼進行 unwind。app

DWARF 字節碼功能很強大,它是圖靈完備的,這意味着僅僅經過 DWARF 就能夠作幾乎任何事情(therotically)。可是從數據的組織上來看,DWARF 實在略顯複雜晦澀,所以不多有人願意去碰,本文也只是簡單介紹其中與異常處理相關的東西。本質上來講,eh_frame 像是一張表,它用於描述怎樣根據程序中某一條指令來設置相應的寄存器,從而返回到當前函數的調用函數中去,它的做用能夠用以下表格來形象地描述。ide

program counter CFA ebp  ebx eax return address
0xfff0003001 rsp+32 *(cfa-16) *(cfa-24) eax=edi *(cfa-8) 
0xfff0003002 rsp+32 *(cfa-16)   eax=edi *(cfa-8)
0xfff0003003 rsp+32 *(cfa-16) *(cfa-32) eax=edi *(cfa-8

上表中,CFA(canonical frame address) 表示一個基地址,用於做爲當前函數中的其它地址的起始地址,使得其它地址能夠用與該基地址的偏移來表示,因爲這個表可能要覆蓋不少程序指令,所以這個表的體積有多是很大的,甚至比程序自己的代碼量還要大。而在實際中,爲了減小這個表的體積,GCC 一般會對它進行壓縮編碼,以及儘量減小要覆蓋的指令的數量,好比,只對會拋異常的函數裏的特定區域指令進行記錄。

具體的實現上,eh_frame 由一個CIE (Common Information Entry) 及多個 FDE (Frame Description Entry) 組成,它們在內存中是連續存放的:

 CIE 及 FDE 格式的定義能夠參看以下:

 CIE結構: 

Length

Required
Extended Length Optional
CIE ID Required
Version Required
Augmentation String Required
EH Data Optional
Code Alignment Factor Required
Data Alignment Factor Required
Return Address Register Required
Augmentation Data Length Optional
Augmentation Data Optional
Initial Instructions Required
Padding  

FDE結構:

Length Required
Extended Length Optional
CIE Pointer Required
PC Begin Required
PC Range Required
Augmentation Data Length Optional
Augmentation Data Optional
Call Frame Instructions Required
Padding  

注意其中標註紅色的字段:

1)Initial Instructions,Call Frame Instructions 這兩字段裏放的就是所謂的 DWARF 字節碼,好比:DW_CFA_def_cfa R OFF,表示經過寄存器 R 及位移 OFF 來計算 CFA,其功能相似於前面的表格中第二列指明的內容。

2)PC begin,PC range,這兩個字段聯合起來表示該 FDE 所能覆蓋的指令的範圍,eh_frame 中全部的 FDE 最後會按照 pc begin 排序進行存放。

3)若是 CIE 中的 Augmentation String 中包含有字母 "P",則相應的 Augmentation Data 中包含有指向 personality routine 的指針。

4)若是 CIE 中的 Augmentation String 中包含有有字母「L」,則 FDE 中 Aumentation Data 包含有 language specific data 的指針。

 

對一個elf文件經過以下命令:readelf -Wwf xxx,能夠讀取其中關於 .eh_frame 的數據:

The section .eh_frame contains: 00000000 0000001c 00000000 CIE Version: 1 Augmentation: "zPL" Code alignment factor: 1 Data alignment factor: -8 Return address column: 16 Augmentation data: 00 d8 09 40 00 00 00 00 00 00 DW_CFA_def_cfa: r7 ofs 8 ##如下爲字節碼 DW_CFA_offset: r16 at cfa-8

00000020 0000002c 00000024 FDE cie=00000000 pc=00400ac8..00400bd8 Augmentation data: 00 00 00 00 00 00 00 00 
#如下爲字節碼 DW_CFA_advance_loc:
1 to 00400ac9 DW_CFA_def_cfa_offset: 16 DW_CFA_offset: r6 at cfa-16 DW_CFA_advance_loc: 3 to 00400acc DW_CFA_def_cfa_reg: r6 DW_CFA_nop DW_CFA_nop DW_CFA_nop

對於由 GCC 編譯出來的程序來講,CIE, FDE 是其在 unwind 過程當中恢復現場時所依賴的所有東西,並且是完備的,這裏所說的恢復現場指的是恢復調用當前函數的函數的現場,好比,func1 調用 func2,而後咱們能夠在 func2 裏經過查詢 CIE,FDE 恢復 func1 的現場。CIE,FDE 存在於每個須要處理異常的 ELF 文件中,當異常發生時,runtime 根據當前 PC 值調用 dl_iterate_phdr() 函數就能夠把當前程序所加載的全部模塊輪詢一遍,從而找到該 PC 所在模塊的 eh_frame。

for (n = info->dlpi_phnum; --n >= 0; phdr++) { if (phdr->p_type == PT_LOAD) { _Unwind_Ptr vaddr = phdr->p_vaddr + load_base; if (data->pc >= vaddr && data->pc < vaddr + phdr->p_memsz) match = 1; } else if (phdr->p_type == PT_GNU_EH_FRAME) p_eh_frame_hdr = phdr; else if (phdr->p_type == PT_DYNAMIC) p_dynamic = phdr; }

找到 eh_frame 也就找到 CIE,找到了 CIE 也就能夠去搜索相應的 FDE,找到FDE及CIE後,就能夠從這兩數據表中提取相關的信息,並執行DWARF 字節碼,從而獲得當前函數的調用函數的現場,參看以下用於重建函數幀的函數:

static _Unwind_Reason_Code uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs) { struct dwarf_fde *fde; struct dwarf_cie *cie; const unsigned char *aug, *insn, *end; memset (fs, 0, sizeof (*fs)); context->args_size = 0; context->lsda = 0; // 根據context查找FDE。
  fde = _Unwind_Find_FDE (context->ra - 1, &context->bases); if (fde == NULL) { /* Couldn't find frame unwind info for this function. Try a target-specific fallback mechanism. This will necessarily not provide a personality routine or LSDA. */ #ifdef MD_FALLBACK_FRAME_STATE_FOR MD_FALLBACK_FRAME_STATE_FOR (context, fs, success); return _URC_END_OF_STACK; success: return _URC_NO_REASON; #else
      return _URC_END_OF_STACK; #endif } fs->pc = context->bases.func; // 獲取對應的CIE.
  cie = get_cie (fde); // 提取出CIE中的信息,如personality routine的地址。
  insn = extract_cie_info (cie, context, fs); if (insn == NULL) /* CIE contained unknown augmentation. */
    return _URC_FATAL_PHASE1_ERROR; /* First decode all the insns in the CIE. */ end = (unsigned char *) next_fde ((struct dwarf_fde *) cie); // 執行dwarf字節碼,從而恢復相應的寄存器的值。
 execute_cfa_program (insn, end, context, fs); // 定位到fde的相關數據
  /* Locate augmentation for the fde. */ aug = (unsigned char *) fde + sizeof (*fde); aug += 2 * size_of_encoded_value (fs->fde_encoding); insn = NULL; if (fs->saw_z) { _Unwind_Word i; aug = read_uleb128 (aug, &i); insn = aug + i; } // 讀取language specific data的指針
  if (fs->lsda_encoding != DW_EH_PE_omit) aug = read_encoded_value (context, fs->lsda_encoding, aug, (_Unwind_Ptr *) &context->lsda); /* Then the insns in the FDE up to our target PC. */
  if (insn == NULL) insn = aug; end = (unsigned char *) next_fde (fde); // 執行FDE中的字節碼。
 execute_cfa_program (insn, end, context, fs); return _URC_NO_REASON; }

經過如上的操做,unwinder 就已經把調用函數的現場給重建起來了,這些現場信息包括:

struct _Unwind_Context { void *reg[DWARF_FRAME_REGISTERS+1];  //必要的寄存器。
    void *cfa; // canoniacl frame address, 前面提到過,基地址。
    void *ra;// 返回地址。
    void *lsda;// 該函數對應的language specific data,若是存在的話。
    struct dwarf_eh_bases bases; _Unwind_Word args_size; };

實現 Personality routine 

Peronality routine 的做用主要有兩個:

1)檢查當前函數是否有相應的 catch 語句。

2)清理當前函數中的局部變量。

十分不巧,這兩件事情僅僅依靠運行時也是無法完成的,必須依靠編譯器在編譯時創建起相關的數據進行協助。對於 GCC 來講,這些與拋異常的函數具體相關的信息所有放在 .gcc_except_table 區域裏去了,這些信息會做爲Itanium ABI 接口中所謂的 language specific data 在 unwinder 與 c++ ABI 之間傳遞,根據前面的介紹,咱們知道在 FDE 中保存有指向 language specific data 的指針,所以 unwinder 在重建現場的時候就已經把這些數據讀取了出來,c++ 的 ABI 只要調用 _Unwind_GetLanguageSpecificData() 就能夠獲得指向該數據的指針。

關於 GCC 下 language specific data 的格式,在網上幾乎找不到什麼權威的文檔,我只在 llvm 的官網上找到一個相關的連接,這個文檔對 gcc_except_table 做了很詳細的說明,我對比了一下 GCC 源碼裏的 personality routine 的相關實現,發現二者仍是有些許出入,所以本文接下來的介紹主要基於對 GCC 相關源碼的我的解讀,若有錯誤歡迎指正。

 

下圖來源於網絡,展現了gcc_except_table 及 language specific data 的格式:

  

由上圖所示,LSDA 主要由一個表頭,及其後緊跟着的三張表組成。

1.LSDA Header:

該表頭主要用來保存接下來三張表的相關信息,如編碼,及表的位移等,該表頭主要包含六個域:

1)Landing pad 起始地址的編碼方式,長度爲一個字節。

2)landing pad 起始地址,這是可選的,只有當前面指明的編碼方式不等於 DW_EH_PE_omit 時,這個字段才存在,此時讀取這個字段就須要根據前面指定的編碼方式進行讀取,長度不固定,若是這個字段不存在,則 landing pad 的起始地址須要經過調用 _Unwind_GetRegionStart() 來得到,獲得其實就是當前模塊加載的起始地址,這是最多見的形式。

3)type table 的編碼方式,長度爲一個字節。

4)type table 的位移,類型爲 unsigned LEB128,這個字段是可選的,只有3)中編碼方式不等於 DW_EH_PE_omit 時,這個才存在。

5)call site table 的編碼方式,長度爲一個字節。

6)call site table 的長度,一個 unsigned LEB128 的值。

2.call site table

LSDA 表頭以後緊跟着的是 call site table,該表用於記錄程序中哪些指令有可能會拋異常,表中每條記錄共有4個字段:

1)可能會拋異常的指令的地址,該地址是距 Landing pad 起始地址的偏移,編碼方式由 LSDA 表頭中第一個字段指明。

2)可能拋異常的指令的區域長度,該字段與 1)一塊兒表示一系列連續的指令,編碼方式與 1)相同。

3)用於處理上述指令的 Landing pad 的位移,這個值若是爲 0 則表示不存在相應的 landing pad。

4)指明要採起哪些 action,這是一個 unsigned LEB128 的值,該值減1後做爲下標獲取 action table 中相應記錄。

call site table 中的記錄按第一個字段也就是指令起始地址進行排序存放,所以 unwind 的時候能夠加快對該表的搜索,unwind 的過程當中,若是當前 pc 的值不在 call site table 覆蓋的範圍內的話,搜索就會返回,而後就調用std::terminate() 結束程序,這一般來講是不正常的行爲。

若是在 call site table 中有對應的處理,但 landing pad 的位移倒是 0 的話,代表當前函數既不存在 catch 語句,也不須要清理局部變量,這是一種正常狀況,unwinder 應該繼續向上 unwind,而若是 landing pad 不爲0,則代表該函數中有 catch 語句,可是這些 catch 可否處理拋出的異常則還要結合 action 字段,到 type table 中去進一步加以判斷:

1)若是 action 字段爲 0,則代表當前函數沒有 catch 語句,但有局部變量須要清理。

2)若是 action 字段不爲 0,則代表當前函數中存在 catch 語句,又由於 catch 是可能存在多個的,怎麼知道哪一個可以 catch 當前的異常呢?所以須要去檢查 action table 中的表項。

3. Action table

action table 中每一條記錄是一個二元組,表示一個 catch 語句所對應的異常,或者表示當前函數所容許拋出的異常 (exception specification),該列表每條記錄包含兩個字段:

1)filter type,這是一個 unsigned LEB128 的數值,用於指向 type table 中的記錄,該值有多是負數。

2)指向下一個 action table 中的下一條記錄,這是當函數中有多個 catch 或 exception specification 有多個時,將各個 action 記錄連接起來。

4. Type Table

type table 中存放的是異常類型的指針:

std::type_info* type_tables[];

這個表被分紅兩部分,一部分是各個 catch 所對應的異常的類型,另外一部分是該函數容許拋出的異常類型:

void func() throw(int, string)
{
}

type table中這兩部分分別經過正負下標來進行索引:

有了如上這些數據,personality routine 只須要根據當前的 pc 值及當前的異常類型,不斷在上述表中查找,最後就能找到當前函數是否有 landing pad,若是有則返回 _URC_INSTALL_CONTEXT,指示 unwinder 跳過去執行相應的代碼。

什麼是Landing pad

在前面一篇博文裏,咱們簡單提到了Landing pad:指的是可以 catch 當前異常的 catch 語句。這個說法其實不確切,準確來講,landing pad 指的是 unwinder 以外的「用戶代碼」:

1)用於 catch 相應的 exception,對於一個函數來講,若是該函數中有 catch 語句,且可以處理當前的異常,則該 catch 就是 landing pad。

2)若是當前函數沒有 catch 或者 catch 不能處理當前 exception,則意味着異常還要從當前函數繼續往上拋,於是 unwind 當前函數時有可能要進行相應的清理,此時這些清理局部變量的代碼就是 landing pad。

從名字上來看,顧名思議,landing pad 指的是程序的執行流程在進入當前函數後,最後要轉到這裏去,很恰當的描述。當 landing pad 是 catch 語句時,這個比較好理解,前面咱們一直說清理局部變量的代碼,這是什麼意思呢?這些清理代碼又放在哪裏?爲了說明這個問題,咱們看一下以下代碼:

#include <iostream>
#include <stddef.h>
using namespace std;

class cs
{
    public:

        explicit cs(int i) :i_(i) { cout << "cs constructor:" << i << endl; }
        ~cs() { cout << "cs destructor:" << i_ << endl; }

    private:

        int i_;
};

void test_func3()
{
    cs c(33);
    cs c2(332);

    throw 3;

    cs c3(333);
    cout << "test func3" << endl;
}

void test_func3_2()
{
    cs c(32);
    cs c2(322);

    test_func3();

    cs c3(323);

    test_func3();
}

void test_func2()
{
    cs c(22);

    cout << "test func2" << endl;
    try
    {
        test_func3_2();

        cs c2(222);
    }
    catch (int)
    {
        cout << "catch 2" << endl;
    }
}

void test_func1()
{
    cout << "test func1" << endl;
    try
    {
        test_func2();
    }
    catch (...)
    {
        cout << "catch 1" << endl;
    }
}

int main()
{
    test_func1();
    return 0;
}

對於函數 test_func3_2() 來講,當 test_func3() 拋出異常後,在 unwind 的第二階段,咱們知道 test_func3_2() 中的局部變量 c 及 c2 是須要清理的,而 c3 則不用,那麼編譯器是怎麼生成代碼來完成這件事情的呢?當異常發生時,運行時是沒有辦法知道當前哪些變量是須要清理的,由於這個緣由編譯器在生成代碼的時候,在函數的末尾設置了多個出口,使得當異常發生時,能夠直接跳到某一段代碼就能清理相應的局部變量,咱們看看 test_func3_2() 編譯後生成的對應的彙編代碼:

void test_func3_2()
{
  400ca4:    55                     push   %rbp
  400ca5:    48 89 e5               mov    %rsp,%rbp
  400ca8:    53                     push   %rbx
  400ca9:    48 83 ec 48            sub    $0x48,%rsp
    cs c(32);
  400cad:    48 8d 7d e0            lea    0xffffffffffffffe0(%rbp),%rdi
  400cb1:    be 20 00 00 00         mov    $0x20,%esi
  400cb6:    e8 9f 02 00 00         callq  400f5a <_ZN2csC1Ei>
    cs c2(322);
  400cbb:    48 8d 7d d0            lea    0xffffffffffffffd0(%rbp),%rdi
  400cbf:    be 42 01 00 00         mov    $0x142,%esi
  400cc4:    e8 91 02 00 00         callq  400f5a <_ZN2csC1Ei>

    test_func3();
  400cc9:    e8 5a ff ff ff         callq  400c28 <_Z10test_func3v>

    cs c3(323);
  400cce:    48 8d 7d c0            lea    0xffffffffffffffc0(%rbp),%rdi
  400cd2:    be 43 01 00 00         mov    $0x143,%esi
  400cd7:    e8 7e 02 00 00         callq  400f5a <_ZN2csC1Ei>

    test_func3();
  400cdc:    e8 47 ff ff ff         callq  400c28 <_Z10test_func3v>
  400ce1:    eb 17                  jmp    400cfa <_Z12test_func3_2v+0x56>
  400ce3:    48 89 45 b8            mov    %rax,0xffffffffffffffb8(%rbp)
  400ce7:    48 8b 5d b8            mov    0xffffffffffffffb8(%rbp),%rbx
  400ceb:    48 8d 7d c0            lea    0xffffffffffffffc0(%rbp),%rdi #c3的this指針
  400cef:    e8 2e 02 00 00         callq  400f22 <_ZN2csD1Ev>
  400cf4:    48 89 5d b8            mov    %rbx,0xffffffffffffffb8(%rbp)
  400cf8:    eb 0f                  jmp    400d09 <_Z12test_func3_2v+0x65>
  400cfa:    48 8d 7d c0            lea    0xffffffffffffffc0(%rbp),%rdi #c3的this指針
  400cfe:    e8 1f 02 00 00         callq  400f22 <_ZN2csD1Ev>
  400d03:    eb 17                  jmp    400d1c <_Z12test_func3_2v+0x78>
  400d05:    48 89 45 b8            mov    %rax,0xffffffffffffffb8(%rbp)
  400d09:    48 8b 5d b8            mov    0xffffffffffffffb8(%rbp),%rbx
  400d0d:    48 8d 7d d0            lea    0xffffffffffffffd0(%rbp),%rdi #c2的this指針
  400d11:    e8 0c 02 00 00         callq  400f22 <_ZN2csD1Ev>
  400d16:    48 89 5d b8            mov    %rbx,0xffffffffffffffb8(%rbp)
  400d1a:    eb 0f                  jmp    400d2b <_Z12test_func3_2v+0x87> 
  400d1c:    48 8d 7d d0            lea    0xffffffffffffffd0(%rbp),%rdi #c2的this指針
  400d20:    e8 fd 01 00 00         callq  400f22 <_ZN2csD1Ev>
  400d25:    eb 1e                  jmp    400d45 <_Z12test_func3_2v+0xa1>
  400d27:    48 89 45 b8            mov    %rax,0xffffffffffffffb8(%rbp)
  400d2b:    48 8b 5d b8            mov    0xffffffffffffffb8(%rbp),%rbx
  400d2f:    48 8d 7d e0            lea    0xffffffffffffffe0(%rbp),%rdi #c的this指針
  400d33:    e8 ea 01 00 00         callq  400f22 <_ZN2csD1Ev>
  400d38:    48 89 5d b8            mov    %rbx,0xffffffffffffffb8(%rbp)
  400d3c:    48 8b 7d b8            mov    0xffffffffffffffb8(%rbp),%rdi
  400d40:    e8 b3 fc ff ff         callq  4009f8 <_Unwind_Resume@plt>  #c的this指針
  400d45:    48 8d 7d e0            lea    0xffffffffffffffe0(%rbp),%rdi
  400d49:    e8 d4 01 00 00         callq  400f22 <_ZN2csD1Ev>
}
  400d4e:    48 83 c4 48            add    $0x48,%rsp
  400d52:    5b                     pop    %rbx
  400d53:    c9                     leaveq 
  400d54:    c3                     retq   
  400d55:    90                     nop    


注意其中標紅色的代碼,_ZN2csD1Ev 便是類 cs 的析構函數,_Unwind_Resume() 則是當清理完成時,用來從 landing pad 返回的代碼。test_func3_2() 中只有 3 個 cs 對象,但調用析構函數的代碼卻出現了 6 次。這裏其實就是設置了多個出口函數,分別對應不一樣狀況下,處理各個局部變量的析構,對於咱們上面的代碼來講,test_func3_2() 函數中的 landing pad 就是從地址:400d09 開始的,這些代碼作了以下事情:

1)先析構 c2,而後 jump 到 400d2b 析構 c.

2)最後調用 _Unwind_Resume()

因而可知當程序中有多個可能拋異常的地方時,landing pad 也相應地會有多個,該函數的出口將更復雜,這也算是異常處理的一個 overhead 了。

總結

至此,關於 GCC 處理異常的具體流程及方式,各個細節都已寫完,涉及不少比較瑣碎的東西,只有反覆閱讀源碼及相關文檔才能搞明白,也不容易,只是古人說的好,紙上得來終覺淺,爲了加深印象及驗證所學的內容,我根據前面瞭解的這些知識,簡單仿着 GCC 寫了一個簡化版的 c++ ABI,代碼放到了 github 上這裏,有興趣的讀者們能夠參考一下,本來是打算把 unwinder 也寫一遍的,但 DWARF 的格式實在太過複雜,已經超出了異常處理這個範圍,就做罷了。

 

【引用】:

http://www.intel.com/content/dam/www/public/us/en/documents/guides/itanium-software-runtime-architecture-guide.pdf

http://mentorembedded.github.io/cxx-abi/abi-eh.html

http://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html

https://www.opensource.apple.com/source/gcc/gcc-5341/gcc/

http://www.cs.dartmouth.edu/~sergey/battleaxe/hackito_2011_oakley_bratus.pdf

http://mentorembedded.github.io/cxx-abi/exceptions.pdf

http://www.airs.com/blog/archives/464

相關文章
相關標籤/搜索