文章目錄
- 前言
- 一、LSB
- 二、The .eh_frame section
- 2.1 簡介
- 2.2 The Common Information Entry Format
- 2.1.1 Augmentation String Format
- 2.3 The Frame Description Entry Format
- 三、The .eh_frame_hdr section
- 四、libunwind
- 五、基于Frame Pointer和基于unwind 形式的棧回溯比較
- 參考資料
前言
基于FP的棧回溯請參考:
Linux x86_64 基于FP棧回溯
Linux ARM64 基于FP棧回溯
基于FP棧回溯需要一個專門寄存器RBP來保存frame poniter。
gcc優化選項 -O 默認使用-fomit-frame-pointer編譯標志進行優化,省略幀指針。將寄存器RBP作為一個通用的寄存器來使用。
-fomit-frame-pointer 是GCC編譯器的一個編譯選項。當啟用該選項時,它告訴編譯器在不需要基指針的函數中省略基指針。通過省略基指針,編譯器避免了保存、設置和恢復基指針的指令,從而使生成的代碼更小、更快。
省略基指針還提供了一個額外的通用寄存器可供使用,這對于具有有限通用寄存器數量的架構(如x86)非常有用。
這樣就不能基于FP來進行棧回溯了,Linux通過.eh_frame節可以來進行棧回溯,.eh_frame節通常由編譯器(如GCC)在編譯可執行文件或共享庫時生成。調試器(如GDB)能夠讀取并解析這些節,以提供強大的調試功能。
在x86_64體系架構上,大多數軟件在編譯的時采用了gcc的默認選項,而gcc的默認選項不啟用函數幀指針FP,而是把RBP寄存器作為一個通用的寄存器,以及無法進行FP進行棧回溯,因此對于用戶空間程序,通常使用.eh_frame section 來進行棧回溯。
.eh_frame段中存儲著跟函數入棧相關的關鍵數據。
當函數執行入棧指令后,在該段會保存跟入棧指令一一對應的編碼數據,
根據這些編碼數據,就能計算出當前函數棧大小和cpu的哪些寄存器入棧了,在棧中什么位置。
無論是否有-g選項,gcc默認都會生成.eh_frame和.eh_frame_hdr section。
一、LSB
Linux Standard Base(LSB)定義了編譯應用程序的系統接口和支持安裝腳本的最小環境。其目的是為符合LSB的大規模應用程序提供統一的行業標準環境。
LSB規范由兩個基本部分組成:一個通用部分,描述了在LSB的所有實現中保持不變的接口部分;以及一個特定于體系結構的部分,描述了根據處理器體系結構而變化的接口部分。通用部分和特定于體系結構的部分共同為具有相同硬件體系結構的系統上的編譯應用程序提供了完整的接口規范。
LSB包含一組應用程序接口(API)和應用程序二進制接口(ABI)。API可以出現在可移植應用程序的源代碼中,而該應用程序的編譯二進制文件可以使用更大的一組ABIs。符合規范的實現提供了這里列出的所有ABIs。編譯系統可以通過替換(例如通過宏定義)某些API,將其調用轉換為一個或多個底層二進制接口的調用,并根據需要插入對二進制接口的調用。
LSB是由Linux Foundation組織架構下的多個Linux發行版共同參與的項目,旨在標準化軟件系統結構,包括文件系統層次結構(Filesystem Hierarchy Standard)。LSB基于POSIX規范、Single UNIX Specification(SUS)和其他幾個開放標準,但在某些領域進行了擴展。
根據LSB:
LSB的目標是開發和推廣一組開放標準,增加Linux發行版之間的兼容性,并使軟件應用程序能夠在任何符合標準的系統上運行,即使是以二進制形式。此外,LSB還將協調努力,吸引軟件供應商為Linux操作系統移植和編寫產品。
二、The .eh_frame section
2.1 簡介
在Linux系統中,.eh_frame節是一種特殊的節(section),用于存儲程序的調試信息和堆棧回溯相關的信息。
這個節通常在可執行文件或共享庫中存在,以支持運行時的調試和異常處理。
當程序在Linux系統中進行異常處理和堆棧展開時,會使用到.eh_frame節。.eh_frame節是基于DWARF(Debugging With Attributed Record Formats)調試格式的一部分。
.eh_frame節的主要作用是提供運行時支持,用于正確展開函數調用堆棧。它存儲了一系列編碼的調用幀信息,這些信息在異常處理或進行堆棧回溯時起到關鍵作用。
在異常發生或需要進行堆棧回溯時,運行時系統會利用.eh_frame節中的信息來展開堆棧。它會遵循編碼的CFI(Call Frame Information)指令序列,逐層遍歷堆棧幀,獲取返回地址,并找到對應的異常處理程序或回溯信息。
# readelf -S a.out
共有 30 個節頭,從偏移量 0x1930 開始:節頭:[號] 名稱 類型 地址 偏移量大小 全體大小 旗標 鏈接 信息 對齊......[16] .eh_frame_hdr PROGBITS 00000000004005c0 000005c0000000000000003c 0000000000000000 A 0 0 4[17] .eh_frame PROGBITS 0000000000400600 000006000000000000000114 0000000000000000 A 0 0 8......
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings), I (info),L (link order), O (extra OS processing required), G (group), T (TLS),C (compressed), x (unknown), o (OS specific), E (exclude),l (large), p (processor specific)
A (alloc)
.eh_frame帶有SHF_ALLOC flag(標志一個section是否應為內存中鏡像的一部分)。
.eh_frame節包含了用于棧回溯和異常處理的數據結構,其中包括編碼了調用幀信息、異常處理表和其他相關數據的指令序列。這些信息用于在程序運行時進行堆棧展開(stack unwinding),即在異常發生時回溯函數調用堆棧以查找異常處理程序。這些結構可以在程序運行時被調試器或其他工具使用。它們提供了關于函數調用鏈、寄存器狀態和局部變量等信息的詳細描述,以便進行調試和錯誤診斷。
當程序中包含異常處理機制(如C++異常)或使用與堆棧相關的特性(如backtrace函數)時,編譯器會生成和使用.eh_frame節。這些信息允許運行時系統在異常處理期間正確地展開函數調用堆棧,并將控制權傳遞給適當的異常處理程序。
盡管.eh_frame增加了可執行文件的大小,但它提供了重要的運行時支持和調試功能。然而,對于某些嵌入式系統或特定的應用程序,可能需要最小化可執行文件的大小,并且不需要異常處理和調試功能。在這種情況下,可以使用編譯器選項(如-fno-asynchronous-unwind-tables)來禁用.eh_frame的生成,以減少可執行文件的大小。
.eh_frame中的數據結構通常使用一種稱為DWARF(Debugging With Arbitrary Record Formats)的格式進行編碼。
DWARF是一種調試信息格式,廣泛用于Linux系統和其他類Unix系統中。它定義了一組規范,用于描述程序的調試信息,包括函數、類型、變量、源代碼映射等。
通過解析.eh_frame節中的DWARF數據,調試器可以還原函數調用堆棧,獲取函數的參數和局部變量值,以及跟蹤函數調用的路徑。這對于調試復雜的程序、分析錯誤和優化代碼非常有幫助。
.eh_frame節應包含一個或多個調用幀信息(CFI - Call Frame Information)記錄。存在的記錄數量應由節頭中包含的節大小確定。每個CFI記錄包含一個通用信息條目(CIE - Common Information Entry)記錄,后面跟著一個或多個幀描述條目(FDE - Frame Description Entry)記錄。CIE和FDE都應對齊到地址單元大小的邊界。
Call Frame Information Format:
----------------------------------
Common Information Entry Record
----------------------------------
Frame Description Entry Record(s)
----------------------------------
如下圖所示:
2.2 The Common Information Entry Format
Common Information Entry Format:
Length | Required |
Extended Length | Optional |
CIE ID | Required |
Version | Required |
Augmentation String | Required |
Code Alignment Factor | Required |
Data Alignment Factor | Required |
Return Address Register | Required |
Augmentation Data Length | Optional |
Augmentation Data | Optional |
Initial Instructions | Required |
Padding |
// libunwind/include/dwarf.htypedef struct dwarf_cie_info{unw_word_t cie_instr_start; /* start addr. of CIE "initial_instructions" */unw_word_t cie_instr_end; /* end addr. of CIE "initial_instructions" */unw_word_t fde_instr_start; /* start addr. of FDE "instructions" */unw_word_t fde_instr_end; /* end addr. of FDE "instructions" */unw_word_t code_align; /* code-alignment factor */unw_word_t data_align; /* data-alignment factor */unw_word_t ret_addr_column; /* column of return-address register */unw_word_t handler; /* address of personality-routine */uint16_t abi;uint16_t tag;uint8_t fde_encoding;uint8_t lsda_encoding;unsigned int sized_augmentation : 1;unsigned int have_abi_marker : 1;unsigned int signal_frame : 1;}
dwarf_cie_info_t;
(1)Length
一個4字節的無符號值,表示CIE結構的長度(以字節為單位),不包括Length字段本身。如果Length字段的值為0xffffffff,則長度包含在Extended Length字段中。如果Length字段的值為0,則此CIE應被視為終止符,并且處理將結束。
(2)Extended Length
這個8字節的無符號值表示CIE結構的字節長度,不包括長度字段和擴展長度字段本身。除非長度字段包含值0xffffffff,否則該字段不存在。
(3)CIE ID
這個4字節的無符號值用于區分CIE(Common Information Entry)記錄和FDE(Frame Description Entry)記錄。該值應始終為0,表示該記錄是一個CIE。
static inline int
is_cie_id (unw_word_t val, int is_debug_frame)
{/* The CIE ID is normally 0xffffffff (for 32-bit ELF) or0xffffffffffffffff (for 64-bit ELF). However, .eh_frameuses 0. */if (is_debug_frame)return (val == (uint32_t)(-1) || val == (uint64_t)(-1));elsereturn (val == 0);
}
(4)Version
這個1字節的值用于標識幀信息結構的版本號。該值應為1。
/* Read the return-address column either as a u8 or as a uleb128. */if (version == 1){if ((ret = dwarf_readu8 (as, a, &addr, &ch, arg)) < 0)return ret;dci->ret_addr_column = ch;}
(5)Augmentation String
這個值是一個以NUL(空字符)結尾的字符串,用于標識與該CIE或與該CIE關聯的FDE的增強信息。如果字符串長度為零,則表示沒有增強數據存在。增強字符串是區分大小寫的,并且應按照下面的描述進行解釋。
/* read and parse the augmentation string: */memset (augstr, 0, sizeof (augstr));for (i = 0;;){if ((ret = dwarf_readu8 (as, a, &addr, &ch, arg)) < 0)return ret;if (!ch)break; /* end of augmentation string */if (i < sizeof (augstr) - 1)augstr[i++] = ch;}
(6)Code Alignment Factor
這個值是一個無符號的LEB128編碼值,它被從與該CIE或其FDE關聯的所有"advance location"指令中分解出來。該值應與"advance location"指令的增量參數相乘,以獲得新的位置值。
(7)Data Alignment Factor
這個值是一個帶符號的LEB128編碼值,它被從與該CIE或其FDE關聯的所有偏移指令中分解出來。該值應與偏移指令的寄存器偏移參數相乘,以獲得新的偏移值。
if ((ret = dwarf_read_uleb128 (as, a, &addr, &dci->code_align, arg)) < 0|| (ret = dwarf_read_sleb128 (as, a, &addr, &dci->data_align, arg)) < 0)return ret;
(8)Augmentation Length
這個值是一個無符號的LEB128編碼值,用于表示增強數據的字節長度。只有當增強字符串中包含字符’z’時,該字段才存在。
(9)Augmentation Data
這是一個數據塊,其內容由增強字符串中的內容定義,具體描述如下。只有當增強字符串中包含字符’z’時,該字段才存在。該數據塊的大小由增強長度(Augmentation Length)指定。
(9)Initial Instructions
這是初始的調用幀指令集。指令的數量由CIE記錄中剩余的空間確定。
(10)Padding
這些額外的字節用于將CIE結構對齊到地址單元大小邊界。
2.1.1 Augmentation String Format
增強字符串指示了一些可選字段的存在以及如何解釋這些字段。該字符串區分大小寫。CIE中增強字符串中的每個字符的解釋如下:
‘z’:
字符串的第一個字符可以是’z’。如果存在,則增強數據字段也必須存在。增強數據的內容將根據增強字符串中的其他字符進行解釋。
‘L’:
字符串的第一個字符是’z’時,可以在任何位置上出現’L’。如果存在,它表示CIE的增強數據中存在一個參數,并且FDE的增強數據中也存在相應的參數。CIE的增強數據中的參數是1字節,表示用于FDE的增強數據中的參數的指針編碼,該參數是指向特定語言數據區(LSDA)的地址。LSDA指針的大小由使用的指針編碼指定。
‘P’:
字符串的第一個字符是’z’時,可以在任何位置上出現’P’。如果存在,它表示CIE的增強數據中存在兩個參數。第一個參數是1字節,表示用于第二個參數的指針編碼,該參數是指向人格例程處理程序的地址。人格例程用于處理特定語言和供應商的任務。系統解旋庫接口通過指向人格例程的指針訪問特定語言的異常處理語義。個性例程沒有ABI-specific的名稱。個性例程指針的大小由使用的指針編碼指定。
‘R’:
字符串的第一個字符是’z’時,可以在任何位置上出現’R’。如果存在,則增強數據中應包含一個1字節的參數,該參數表示FDE中使用的地址指針的指針編碼。
i = 0;if (augstr[0] == 'z'){dci->sized_augmentation = 1;if ((ret = dwarf_read_uleb128 (as, a, &addr, &aug_size, arg)) < 0)return ret;i++;}for (; i < sizeof (augstr) && augstr[i]; ++i)switch (augstr[i]){case 'L':/* read the LSDA pointer-encoding format. */if ((ret = dwarf_readu8 (as, a, &addr, &ch, arg)) < 0)return ret;dci->lsda_encoding = ch;break;case 'R':/* read the FDE pointer-encoding format. */if ((ret = dwarf_readu8 (as, a, &addr, &fde_encoding, arg)) < 0)return ret;break;case 'P':/* read the personality-routine pointer-encoding format. */if ((ret = dwarf_readu8 (as, a, &addr, &handler_encoding, arg)) < 0)return ret;if ((ret = dwarf_read_encoded_pointer (as, a, &addr, handler_encoding,pi, &dci->handler, arg)) < 0)return ret;break;case 'S':/* This is a signal frame. */dci->signal_frame = 1;/* Temporarily set it to one so dwarf_parse_fde() knows thatit should fetch the actual ABI/TAG pair from the FDE. */dci->have_abi_marker = 1;break;default:Debug (1, "Unexpected augmentation string `%s'\n", augstr);if (dci->sized_augmentation)/* If we have the size of the augmentation body, we can skipover the parts that we don't understand, so we're OK. */goto done;elsereturn -UNW_EINVAL;}
2.3 The Frame Description Entry Format
Frame Description Entry Format:
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)Length
一個4字節的無符號值,表示FDE(Frame Description Entry)結構的長度(以字節為單位),不包括Length字段本身。如果Length字段的值為0xffffffff,則長度包含在Extended Length字段中。如果Length字段的值為0,則該FDE應被視為終止器,并且處理過程應該結束。
(2)Extended Length
一個8字節的無符號值,表示FDE(Frame Description Entry)結構的長度(以字節為單位),不包括Length字段或Extended Length字段本身。除非Length字段的值為0xffffffff,否則該字段不會出現。
(3)CIE Pointer
一個4字節的無符號值,從當前FDE中的CIE指針的偏移量中減去,得到關聯CIE的起始偏移量。該值永遠不應為0。
(4)PC Begin
一個編碼值,表示與該FDE關聯的初始位置的地址。編碼格式在增強數據(Augmentation Data)中指定。
(5)PC Range
一個絕對值,表示與該FDE關聯的指令字節數。
(6)Augmentation Length
一個無符號 LEB128 編碼值,表示增強數據的字節長度。只有在關聯的CIE中的增強字符串包含字符 ‘z’ 時,該字段才存在。
(7)Augmentation Data
一個數據塊,其內容由關聯CIE中的增強字符串的內容所定義,如上所述。只有當關聯CIE中的增強字符串包含字符 ‘z’ 時,該字段才存在。該數據塊的大小由增強長度(Augmentation Length)給出。
(8)Call Frame Instructions
一組調用幀指令(Call Frame Instructions)。
(9)Padding
用于將FDE(Frame Description Entry)結構對齊到一個地址單元大小邊界的額外字節。
三、The .eh_frame_hdr section
.eh_frame_hdr 段包含有關 .eh_frame 段的額外信息。該段中包含了指向 .eh_frame 數據起始位置的指針,以及可選的指向 .eh_frame 記錄的二進制搜索表。
定位一個pc所在的FDE需要從頭掃描.eh_frame,找到合適的FDE(pc是否落在initial_location和address_range表示的區間),所花時間和掃描的CIE和FDE記錄數相關。 .eh_frame_hdr包含binary search index table描述(initial_location, FDE address) pairs。
.eh_frame_hdr Section Format:
Encoding | Field |
---|---|
unsigned byte | version |
unsigned byte | eh_frame_ptr_enc |
unsigned byte | fde_count_enc |
unsigned byte | table_enc |
encoded | eh_frame_ptr |
encoded | fde_count |
binary search table |
(1)version
.eh_frame_hdr 格式的版本。該值應為 1。
(2)eh_frame_ptr_enc
eh_frame_ptr字段的編碼格式。
(3)fde_count_enc
fde_count 字段的編碼格式。DW_EH_PE_omit 的值表示二進制搜索表不存在。
(4)table_enc
二進制搜索表中條目的編碼格式。DW_EH_PE_omit 的值表示二進制搜索表不存在。
(5)eh_frame_ptr
指向.eh_frame部分開頭的指針的編碼值。
(6)fde_count
二進制搜索表中條目數的編碼值。
(7)binary search table
一個包含 fde_count 個條目的二進制搜索表。每個表條目包含兩個編碼值,即初始位置和地址。這些條目按照初始位置的值按升序排序。
四、libunwind
libunwind 是一個可移植且高效的 C API,用于確定 ELF 程序線程的當前調用鏈,并可以在該調用鏈的任何點上恢復執行。該 API 支持本地(同一進程)和遠程(其他進程)操作。用于顯示引發問題的調用鏈的回溯信息,或用于性能監控和分析。
libunwind的使用比較簡單:
#define UNW_LOCAL_ONLY#include <libunwind.h>
#include <stdio.h>
#include <stdlib.h>#define panic(...) \{ fprintf (stderr, __VA_ARGS__); exit (-1); }static void do_backtrace (void)
{unw_cursor_t cursor;unw_word_t ip, sp;unw_context_t uc;int ret;unw_getcontext (&uc);if (unw_init_local (&cursor, &uc) < 0)panic ("unw_init_local failed!\n");do{unw_get_reg (&cursor, UNW_REG_IP, &ip);unw_get_reg (&cursor, UNW_REG_SP, &sp);char fname[64];unw_word_t offset;unw_get_proc_name(&cursor, fname, sizeof(fname), &offset);printf ("(ip=%016lx) (sp=%016lx): (%s+0x%x) [%p]\n", (long) ip, (long) sp, fname, offset, (long) sp);ret = unw_step (&cursor);if (ret < 0){unw_get_reg (&cursor, UNW_REG_IP, &ip);panic ("FAILURE: unw_step() returned %d for ip=%lx\n",ret, (long) ip);}}while (ret > 0);
}void func_c(void)
{do_backtrace();
}void func_b(void)
{func_c();
}void func_a(void)
{func_b();
}int main (int argc, char **argv)
{func_a ();return 0;
}
# gcc 1.c -lunwind
# ./a.out
(ip=0000000000400897) (sp=00007fffc131ce40): (do_backtrace+0x1a) [0x7fffc131ce40]
(ip=00000000004009f0) (sp=00007fffc131d660): (func_c+0x9) [0x7fffc131d660]
(ip=00000000004009fb) (sp=00007fffc131d670): (func_b+0x9) [0x7fffc131d670]
(ip=0000000000400a06) (sp=00007fffc131d680): (func_a+0x9) [0x7fffc131d680]
(ip=0000000000400a1c) (sp=00007fffc131d690): (main+0x14) [0x7fffc131d690]
(ip=00007ff1df9a2555) (sp=00007fffc131d6b0): (__libc_start_main+0xf5) [0x7fffc131d6b0]
(ip=00000000004007b9) (sp=00007fffc131d770): (+0xf5) [0x7fffc131d770]
五、基于Frame Pointer和基于unwind 形式的棧回溯比較
(1)基于Frame Pointer - fp寄存器的棧回溯:
優點:棧回溯比較快,理解簡單。相對較簡單:基于Frame Pointer寄存器的棧回溯通常比解析unwind節更簡單直接。
缺點:gcc添加了優化選項 -O 就會省略掉省略基指針。這樣就不能都通過這種形式進行棧回溯了。
-fomit-frame-pointer編譯標志進行優化:避免將%rbp用作棧幀指針,把FP當作一個通用寄存器,這樣就提供了一個額外的通用寄存器,提高程序運行效率。
通用寄存器用來暫存數據和參與運算。通過load\store指令操作。
如果把fp寄存器當作棧幀寄存器,那就不能參與指令數據運算,CPU寄存器是很寶貴的,多一個寄存器對加快指令數據運算是有積極意義的。
(2)基于unwind 形式的棧回溯:
優點:只是將入棧相關的指令的編碼保存到unwind段中,不用把無關的寄存器保存到棧中,也不用浪費fp寄存器。
把FP當作一個通用寄存器,這樣就提供了一個額外的通用寄存器,提高程序運行效率。
更準確:unwind節中的調試信息提供了更詳細的函數調用和棧幀信息,可以更準確地還原函數調用鏈和參數傳遞。
不受優化影響:unwind節通常包含了編譯器生成的準確信息,不受編譯器優化選項的影響。
提供更多調試功能:unwind節提供了豐富的調試信息,可以用于更深入的調試和錯誤診斷。
缺點:棧回溯的速度肯定比fp形式棧回溯慢,理解難度要比fp形式大很多。
復雜性:解析和使用unwind節的調試信息可能需要更多的工具和技術知識。
參考資料
https://refspecs.linuxfoundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html#EHFRAME
https://github.com/libunwind/libunwind