ELF文件结构解析

可执行与可链接格式 (英语:Executable and Linkable Format,缩写为ELF),常被称为ELF格式,在计算机科学中,是一种用于可执行文件、目标文件、共享库和核心转储(core dump)的标准文件格式。

1999年,被86open项目选为x86架构上的类Unix操作系统的二进制文件格式标准,用来取代COFF。因其可扩展性与灵活性,也可应用在其它处理器、计算机系统架构的操作系统上

—— 来自维基百科

在下文的讲解中,我们会以下面的代码的编译和运行在的docker系统是ubuntu:21.04, docker file在这里 代码在这里

结构

上面是ELF文件的结构图,我们可以分为四个部分

从结构图我们可以看到中间的内容区域被PHT和SHT同时引用到,为什么会有这样的情况呢?因为ELF文件有两种用途,用于代码编译过程中的链接和用于程序运行中的运行。站在不同的用途,ELF文件被使用到的结构各有不同,一般我们称为链接视图执行视图

链接视图中的程序头表是可以忽略的,相应的在执行视图中节头表是可以忽略的。

ELF头部表

我们先编译一下main.c

/************main.c****************/
#include <stdio.h>

int main() {
    printf("hello world!\n");
    return 0;
}
➜  ELFLearning git:(master) gcc -c main.c -o main.o

然后用readelf工具读取头部信息

readelf工具可以读取ELF文件中的各个区块的信息,具体可以用readelf -h查看各个参数的用法 同时,在这里我们也会使用objdump工具来查看ELF文件的二进制内容,具体查看objdump -H

➜  ELFLearning git:(master) ✗ rea readelf -h main.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          784 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         14
  Section header string table index: 13

在文件起始处,有4个标识字节。 在ASCII代码0x7f字符之后, 接下来是字符E(0x45) 、 L(0x4c) 、 F(0× 46) 的ASCII码值。这使得所有处理ELF的工具都可以识别文件是否是所要的格式。还有一些与 具体体系结构相关的信息,在本例中,ubuntu的64位系统。类别标识( ELF64)正确 地表明这是一台64位机器(现在很少能找到32位的机器了) 文件类型是REL,意味着文件是个可重定位文件。 Version字段用于区分ELF标准的各个修订版本。但 因为版本1仍然是最新的,目前还不需要这个特性。另外还包括ELF文件的各个部分的长度和索引位置 信息(稍后会更详细地讨论)。详细的格式定义可以参考这篇“ELF Header”

节头

上文提到了ELF的结构有两种视图,这里我们先站在编译的角度,从链接视图分析。 链接视图最重要的概念是节(section), 节头表(section header table)详细的给出了各个节的信息。 下面我们用readelf再分析一下main.o的节头表

➜  ELFLearning git:(master) ✗ readelf -S main.o
There are 14 section headers, starting at offset 0x310:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000001b  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000250
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000005b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000005b
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000068
       000000000000002b  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000093
       0000000000000000  0000000000000000           0     0     1
  [ 8] .note.gnu.propert NOTE             0000000000000000  00000098
       0000000000000020  0000000000000000   A       0     0     8
  [ 9] .eh_frame         PROGBITS         0000000000000000  000000b8
       0000000000000038  0000000000000000   A       0     0     8
  [10] .rela.eh_frame    RELA             0000000000000000  00000280
       0000000000000018  0000000000000018   I      11     9     8
  [11] .symtab           SYMTAB           0000000000000000  000000f0
       0000000000000138  0000000000000018          12    10     8
  [12] .strtab           STRTAB           0000000000000000  00000228
       0000000000000028  0000000000000000           0     0     1
  [13] .shstrtab         STRTAB           0000000000000000  00000298
       0000000000000074  0000000000000000           0     0     1
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)

数据结构

下面是section header table在linux源码中数据结构的定义

 typedef struct {
   Elf32_Word    sh_name;
   Elf32_Word    sh_type;
   Elf32_Word    sh_flags;
   Elf32_Addr    sh_addr;
   Elf32_Off     sh_offset;
   Elf32_Word    sh_size;
   Elf32_Word    sh_link;
   Elf32_Word    sh_info;
   Elf32_Word    sh_addralign;
   Elf32_Word    sh_entsize;
 } Elf32_Shdr;

 typedef struct elf64_shdr {
   Elf64_Word sh_name;           /* Section name, index in string tbl */
   Elf64_Word sh_type;           /* Type of section */
   Elf64_Xword sh_flags;         /* Miscellaneous section attributes */
   Elf64_Addr sh_addr;           /* Section virtual addr at execution */
   Elf64_Off sh_offset;          /* Section file offset */
   Elf64_Xword sh_size;          /* Size of section in bytes */
   Elf64_Word sh_link;           /* Index of another section */
   Elf64_Word sh_info;           /* Additional section information */
   Elf64_Xword sh_addralign;     /* Section alignment */
   Elf64_Xword sh_entsize;       /* Entry size if section holds table */
 } Elf64_Shdr;

name(4字节)

4个字节,这里的name字段记录了当前节点的名字的字符串在字符串表(有一个字符串表节点,专门存储ELF文件中用到的字符串)中的索引值

type(4字节)

4个字节,节点类型,字段值和解释如下

| 类型值 | 名字 | 解释 | |—— | — | - | |0x0| SHT_NULL| 节表头入口,没有类型| |0x1| SHT_PROGBITS|程序数据, 该类型节区包含程序定义的信息,它的格式和含义都由程序来决定。| |0x2| SHT_SYMTAB|该类型节区包含一个符号表(Symbol Table)。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。 | |0x3| SHT_STRTAB|该类型节区包含字符串表( STRing TABle )。| |0x4| SHT_RELA| 该类型节区包含显式加数(后续重定位会讲到)的重定位项( RELocation entry with Addends ),例如,64位目标文件中的 Elf64_Rela 类型。此外,目标文件可能拥有多个重定位节区。 | |0x5| SHT_HASH| 符号哈希表,用于快速查找符号| |0x6| SHT_DYNAMIC| 动态链接信息| |0x7| SHT_NOTE| 该类型节区包含以某种方式标记文件的信息| |0x8| SHT_NOBITS| 该类型节区不占用文件的空间,其它方面和 SHT_PROGBITS 相似。尽管该类型节区不包含任何字节,其对应的节头成员 sh_offset 中还是会包含概念性的文件偏移。一般是.bss节使用| |0x9| SHT_REL| 该类型节区包含重定位表项(RELocation entry without Addends),不过并没有加数(后续重定位会讲到)。例如,64 位目标文件中的 Elf64_rel 类型。目标文件中可以拥有多个重定位节区| |0xA| SHT_SHLIB| 保留| |0xB| SHT_DYNSYM| 作为一个完整的符号表,它可能包含很多对动态链接而言不必 要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。| |0xE| SHT_INIT_ARRAY| 构造器列表, ELF可以定义自己的构造器,在加载/动态链接的时候,加载器会优先执行| |0xF| SHT_FINI_ARRAY| 解构器列表| |0x10| SHT_PREINIT_ARRAY| Array of pre-constructors| |0x11| SHT_GROUP|Section group | |0x12| SHT_SYMTAB_SHNDX| Extended section indices| |0x13| SHT_NUM| Number of defined types.| |0x60000000| SHT_LOOS| Start OS-specific.| |…|…|…| ####flag(4字节/8字节) 用于标记节点的权限,比如修改,可执行,如下

类型值 名字 解释
0x1 SHF_WRITE 这种节包含了进程运行过程中可以被写的数据。
0x2 SHF_ALLOC 这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭状态 (off)。
0x3 SHF_EXECINSTR 这种节包含可执行的机器指令(EXECutable INSTRuction)。
0x20 SHF_STRINGS 包含了以NULL(’\0’)结尾的字符串,一般用于定义字符串节
0x40 SHF_INFO_LINK ‘sh_info’ contains SHT index
0x40 SHF_TLS 保存了TLS(Thread-Local storage data, 线程局部存储数据)

address(4字节/8字节)

如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0。

offset(4字节/8字节)

节区在文件中的偏移值

size(4字节/8字节)

节区在文件中的大小(单位byte),可以是0

link&info(4字节+4字节)

这个信息的解释依赖于节点的类型

addralign(4字节/8字节)

对齐大小,必须是2的整数倍

entsize

某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。反之,此成员取值为 0。

节区

节区包含目标文件中除了 ELF 头部、程序头部表、节区头部表的所有信息。节区满足以下条件

许多在 ELF 文件中的节都是预定义的,它们包含程序和控制信息。这些节被操作系统使用,但是对于不同的操作系统,同一节区可能会有不同的类型以及属性。

可执行文件是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析引用(不同文件中的子例程的引用以及数据的引用,调整对象文件中的绝对引用)并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节区中,比如 .dynamic 。

每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种

类型 描述
静态链接 静态链接的文件中所使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了。
动态链接 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件执行时使用到相应函数时,相应的函数地址才会被解析。

有一些特殊的节可以支持调试,比如说 .debug 以及 .line 节;支持程序控制的节有 .bss,.data, .data1, .rodata, .rodata1。

note相关节区

有时候生产厂商或者系统构建者可能需要使用一些特殊的信息来标记ELF文件,从而其它程序可以来检查该ELF文件的一致性以及兼容性。节区类型为 SHT_NOTE 或者程序头部类型为 PT_NOTE 的元素用于来实现这个目的,它们中对象的表项可能包含一到多个,每一个表项都是目标处理器格式的 4 字节数组。下面给出了一些可能的注释信息。但是这并不在 ELF 文件的规范内。

下面给出一个简单的例子来说明一下

这里包含了两个表项。

在 Linux 中,与 Note 相关的节包含了 ELF 文件中的一些注释信息,主要包含两个节

.strtab

该节区描述默认的字符串表,包含了一系列的以 NULL 结尾的字符串。ELF 文件使用这些字符串来存储程序中的符号名,包括

该节在运行的过程中不需要加载,只需要加载对应的子集 .dynstr 节。

一般通过对字符串的首个字母在字符串表中的下标来索引字符串。

字符串表的首尾字节都是NULL。此外,索引为0的字符串要么没有名字,要么就是名字为空,其解释依赖于上下文。字符串表也可以为空,相应的,其节区头部的 sh_size 成员将为0。在空字符串表中索引大于 0 的下标显然是非法的。

一个节区头部的 sh_name 成员的值为其相应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了一个包含 25 个字节的字符串表,以及与不同索引相关的字符串。

索引 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9
0 \0 n a m e . \0 V a r
10 i a b l e \0 a b l e
20 \0 \0 x x \0          

其中包含的字符串有

索引 字符串
0 none
1 name.
7 Variable
11 able
16 able
24 空字符串

可以看出

这部分信息在进行 strip 后就会消失。

.dynstr

此节区包含用于动态链接的字符串,格式和.strtab类似

.symtab: 符号表

概述

每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。

此外,链接本质就是把多个不同的目标文件相互“粘”在一起,实际上,目标文件相互粘合是目标文件之间对地址的引用,即函数和变量的地址的相互引用。而在粘合的过程中,符号就是其中的粘合剂。

目标文件中的符号表包含了一些通用的符号,这部分信息在进行了 strip 操作后就会消失。包括

符号表其实是一个数组,数组中的每一个元素都是一个结构体,具体如下

typedef struct {
    Elf32_Word      st_name;
    Elf32_Addr      st_value;
    Elf32_Word      st_size;
    unsigned char   st_info;
    unsigned char   st_other;
    Elf32_Half      st_shndx;
} Elf32_Sym;

每个字段的含义如下

字段 说明
st_name 符号在字符串表中对应的索引。如果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。
st_value 给出与符号相关联的数值,具体取值依赖于上下文,可能是一个正常的数值、一个地址等等。
st_size 给出对应符号所占用的大小。如果符号没有大小或者大小未知,则此成员为0。
st_info 给出符号的类型和绑定属性。之后会给出若干取值和含义的绑定关系。
st_other 目前为0,其含义没有被定义。
st_shndx 如果符号定义在该文件中,那么该成员为符号所在节在节区头部表中的下标;如果符号不在本目标文件中,或者对于某些特殊的符号,该成员具有一些特殊含义。

其中,符号表中下标 0 存储了符号表的一个元素,同时这个元素也相对比较特殊,作为所有未定义符号的索引,具体如下

名称 取值 说明
st_name 0 无名称
st_value 0 0 值
st_size 0 无大小
st_info 0 无类型,局部绑定
st_other 0 无附加信息
st_shndx 0 无节区
st_value

在 Linux 的 ELF 文件中,具体说明如下

  1. 该符号对应着一个变量,那么表明该变量在内存中的偏移。我们可由这个值获取其文件偏移
    1. 获取该符号对应的 st_shndx,进而获取到相关的节区。
    2. 根据节区头元素可以获取节区的虚拟基地址和文件基地址。
    3. value-内存基虚拟地址=文件偏移-文件基地址
  2. 该符号对应着一个函数,那么表明该函数在文件中的起始地址。
st_info

st_info 中包含符号类型和绑定信息,这里给出了控制它的值的方式具体信息如下

#define ELF32_ST_TYPE(i)    ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))
Symbol Type

可以看出 st_info 的低 4 位表示符号的类型,具体定义如下

名称 取值 说明
STT_NOTYPE 0 符号的类型没有定义。
STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等。
STT_FUNC 2 符号与某个函数或者其他可执行代码相关。
STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定。
STT_FILE 4 一般情况下,符号的名称给出了生成该目标文件相关的源文件的名称。如果存在的话,该符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS 且优先级比其他STB_LOCAL符号高。
STT_LOPROCSTT_HIPROC 13~15 保留用于特定处理器

共享目标文件中的函数符号有比较特殊,当另一个目标文件从共享目标文件中引用一个函数时,链接器自动为被引用符号创建过程链接表项。共享目标中除了STT_FUNC , 其它符号将不会通过过程链接表自动被引用。

如果一个符号的值指向节内的特定位置,则它的节索引号 st_shndx,包含了它在节头表中的索引。当一个节在重定位过程中移动时,该符号值也做相应改变,对该符号的引用继续指向程序中的相同位置。有些特定节索引值具有其他语义。

Symbol Binding

根据 #define ELF32_ST_BIND(i) ((i)>>4) 可以看出 st_info 的高 4 位表示符号绑定的信息。而这部分信息确定了符号的链接可见性以及其行为,具体的取值如下

名称 取值 说明
STB_LOCAL 0 表明该符号为局部符号,在包含该符号定义的目标文件以外不可见。相同名称的局部符号可以存在于多个文件中,互不影响。
STB_GLOBAL 1 表明该符号为全局符号,对所有将被组合在一起的目标文件都是可见的。一个文件中对某个全局符号的定义将满足另一个文件对相同全局符号的未定义引用。我们称初始化非零变量的全局符号为强符号,只能定义一次。
STB_WEAK 2 弱符号与全局符号类似,不过它们的定义优先级比较低。
STB_LOPROC ~STB_HIPROC 13 这个范围的取值是保留给处理器专用语义的。

在每个符号表中,所有具有 STB_LOCAL 绑定的符号都优先于弱符号和全局符号。符号表节区中的 sh_info 项所对应的头部的成员包含第一个非局部符号的符号表索引。

此外,全局符号与弱符号的主要区别如下:

符号取值

不同的目标文件类型对符号表项中 st_value 成员的解释不同:

符号表取值在不同的目标文件中具有相似的含义,可以有适当的程序可以采取高效的方法来访问数据。

st_shndx

特殊的索引及其意义如下

如何定位

那么对于一个符号来说如何定位其对应字符串的地址呢?具体步骤如下

  1. 根据 Section Header Table 中符号节头中的 sh_link 获取该符号节中对应符号字符串节在 Section Header Table 中的下标。进而我们就可以获取对应符号节的地址。
  2. 根据该符号的定义中的 st_name 获取该符号的偏移,即在对应符号节中的偏移。
  3. 根据上述两者就可以定位一个符号对应的字符串的地址了。

关于定位我们会在下章节重定位再详细讲解

.gnu.hash

注:本部分主要参考https://blogs.oracle.com/ali/gnu-hash-elf-sections。

在 ELF 良好地可扩展性的帮助下, GNU 为 ELF 对象添加了一个新的哈希节,这个节的性能相比于原有的 SYSV hash 会好很多。该节用于快速根据符号名获取对应符号表中的索引。

.bss

未初始化的全局变量对应的节。此节区不占用 ELF 文件空间,但占用程序的内存映像中的空间。当程序开始执行时,系统将把这些数据初始化为 0。bss其实是block started by symbol的简写,说明该节区中单纯地说明了有哪些变量。

.dataSection

这些节区包含初始化了的数据,会在程序的内存映像中出现。

.rodata

这些节区包含只读数据,这些数据通常参与进程映像的不可写段。

init & .init_array

此节区包含可执行指令,是进程初始化代码的一部分。程序开始执行时,系统会在开始调用主程序入口(通常指 C 语言的 main 函数)前执行这些代码。

.text

此节区包含程序的可执行指令。

.fini & .fini_array

此节区包含可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将执行这里的代码。

.interp section

一般来说,可执行文件具有一个 PT_INTERP 类型的程序头元素,以便于来加载程序中的段。这个节包含了程序对应的解释器。在 exec (BA_OS) 过程中,系统会从该节中提取对应解释器的路径,并根据解释器文件的段创建初始时的程序镜像。也就是说,系统并不使用给定的可执行文件的镜像,而会首先为解释器构造独立的内存镜像。因此,解释器需要从系统处获取控制权,然后为应用程序提供执行环境。

解释器可能有两种方式获取控制权。

其余节区

名称 类型 属性 含义
.comment SHT_PROGBITS   包含版本控制信息。
.debug SHT_PROGBITS   此节区包含用于符号调试的信息。gcc使用-g参数生成,有这个节区才能使用gdb调试
.dynamic SHT_DYNAMIC SHF_ALLOC SHF_WRITE 此节区包含动态链接信息。SHF_WRITE 位设置与否是否被设置取决于具体的处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数 情况下这些字符串代表了与符号表项相关的名称。结构和.strtab相同
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含动态链接符号表。
.got SHT_PROGBITS   此节区包含全局偏移表。动态链接章节会讲到相关知识
.line SHT_PROGBITS   此节区包含符号调试的行号信息,描述了源程序与机器指令之间的对应关系,其内容是未定义的。
.plt SHT_PROGBITS   此节区包含过程链接表(procedure linkage table)。动态链接章节会讲到
.relname SHT_REL   这些节区中包含重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含SHF_ALLOC位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.relaname SHT_RELA    
.shstrtab SHT_STRTAB   此节区包含节区名称。结构和.strtab一样
.text SHT_PROGBITS SHF_ALLOC, SHF_EXECINSTR 此节区包含可执行的代码段
.rela.text SHT_RELA   此节区包含可执行的代码段的重定位表, 每个需要重定位的节区都会有一个重定位表,比如.rela.dyn/.rela.plt。这个一个带加数的重定位表

注意: