文章目录
- 前言
- 一、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 000005c0
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400600 00000600
0000000000000114 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.h
typedef 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) or
0xffffffffffffffff (for 64-bit ELF). However, .eh_frame
uses 0. */
if (is_debug_frame)
return (val == (uint32_t)(-1) || val == (uint64_t)(-1));
else
return (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 that
it 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 skip
over the parts that we don't understand, so we're OK. */
goto done;
else
return -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