ELF文件结构


[TOC]

简介

ELF (Executable and Linkable Format)文件,也就是在 Linux 中的目标文件,从结构上讲,它是已经编译后的可执行文件格式,但是还没有经过链接的过程,和真正的可执行文件在结构上稍有不同。但目标文件一般和可执行文件格式一起采用一种格式存储,从广义上看,可以把目标文件与可执行文件看成是一种类型的文件,主要有以下四种种类型:

ELF文件类型 说明 示例
可重定位文件(Relocatable) 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库可可以归为这一类。 Linux的.o
可执行文件(Excutable File) 这类文件包含了可直接执行的程序,比如ELF可执行文件,一般都没有扩展名。 比如/bin/bash文件
共享目标文件(Shared Object File) 这类文件包含了代码和数据,这种文件是我们所称的库文件。 Linux的.so,如/lib/glibc-2.5.so
核心转储文件(Core Dump File) 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件。 Linux下的core dump

对于共享目标文件:

  • 共享目标文件(Shared Object File),包含代码和数据,这种文件是我们所称的库文件,一般以 .so 结尾。一般情况下,它有以下两种使用情景:
    • 链接器(Link eDitor, ld)可能会处理它和其它可重定位文件以及共享目标文件,生成另外一个目标文件。
    • 动态链接器(Dynamic Linker)将它与可执行文件以及其它共享目标组合在一起生成进程镜像。

目标文件由汇编器和链接器创建,是文本程序的二进制形式,可以直接在处理器上运行。那些需要虚拟机才能够执行的程序 (Java) 不属于这一范围。

ELF文件格式

目标文件既会参与程序链接又会参与程序执行。出于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图,如下

img

首先,我们来关注一下链接视图

文件开始处是 ELF 头部( ELF Header),它给出了整个文件的组织情况。

如果程序头部表(Program Header Table)存在的话,它会告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。

节区部分包含在链接视图中要使用的大部分信息:指令、数据、符号表、重定位信息等等。

节区头部表(Section Header Table)包含了描述文件节区的信息,每个节区在表中都有一个表项,会给出节区名称、节区大小等信息。用于链接的目标文件必须有节区头部表,其它目标文件则无所谓,可以有,也可以没有。

这里给出一个关于链接视图比较形象的展示:

img

对于执行视图来说,其主要的不同点在于没有了 section,而有了多个 segment。其实这里的 segment 大都是来源于链接视图中的 section。

注意:

尽管图中是按照 ELF 头,程序头部表,节区,节区头部表的顺序排列的。但实际上除了 ELF 头部表以外,其它部分都没有严格的顺序。

SimpleSection.o目标文件解析

我们通过调试来学习ELF的文件结构:

/* SimpleSction.c
Linux :
	编译命令:gcc -c SimpleSection.c -m32
*/
int printf(const char* format , ...);

int global_init_var = 84;
int global_uninit_var2;

int func(int i){
        printf("%d\n", i);
}

int main(void){
        static int static_init_var = 85;
        static int static_uninit_var2;
        int a = 1;
        int b;
        func(static_init_var + static_uninit_var2 + a + b);
        return 0;
}

通过objdump这个工具查看一下编译后的.o文件


simpleSection.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
  1 .text         00000081  00000000  00000000  0000003c  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 .data         00000008  00000000  00000000  000000c0  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000008  00000000  00000000  000000c8  2**2
                  ALLOC
  4 .rodata       00000004  00000000  00000000  000000c8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .text.__x86.get_pc_thunk.ax 00000004  00000000  00000000  000000cc  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  6 .comment      0000002c  00000000  00000000  000000d0  2**0
                  CONTENTS, READONLY
  7 .note.GNU-stack 00000000  00000000  00000000  000000fc  2**0
                  CONTENTS, READONLY
  8 .eh_frame     0000007c  00000000  00000000  000000fc  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

可以看到.text段、.data段、.bss段、只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)、gcc处理异常产生的段(.eh_frame),.group段。几个重要的段的属性:

属性名 作用
size 段的大小
File Offset 段在文件中的偏移
CONTENTS 表示改段在文件中存在

通过objdump可以看到bss段标注的是ALLOC,说明bss段不是在文件中的,堆栈提示段的size大小为0,也认为他不在ELF文件中。

代码段 程序源代码编译后的机器指令 .code /.text段
数据段 已初始化的全局变量和局部静态变量 .data段
BSS段 未初始化的全局变量和局部变量 .bss段

文件头–ELF Header

可以使用: readelf -h simpleSection.o查看ELF Header

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1012 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         15
  Section header string table index: 14

从输出结果可以看到ELF的文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序入口和长度、段表的位置和长度及段的数量等。

数据形式

ELF 文件格式支持 8 位 / 32 位体系结构。当然,这种格式是可以扩展的,也可以支持更小的或者更大位数的处理器架构。因此,目标文件会包含一些控制数据,这部分数据表明了目标文件所使用的架构,这也使得它可以被通用的方式来识别和解释。目标文件中的其它数据采用目的处理器的格式进行编码,与在何种机器上创建没有关系。这里其实想表明的意思目标文件可以进行交叉编译,我们可以在 x86 平台生成 arm 平台的可执行代码。

目标文件中的所有数据结构都遵从 “自然” 大小和对齐规则。如下

名称 长度 对齐方式 用途
Elf32_Addr 4 4 无符号程序地址
Elf32_Half 2 2 无符号半整型
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword 4 4 有符号大整型
Elf32_Word 4 4 无符号大整型
unsigned char 1 1 无符号小整型

如果必要,数据结构可以包含显式地补齐来确保 4 字节对象按 4 字节对齐,强制数据结构的大小是 4 的整数倍等等。数据同样适用是对齐的。因此,包含一个 Elf32_Addr 类型成员的结构体会在文件中的 4 字节边界处对齐。

为了具有可移植性,ELF 文件不使用位域。

节(段)表–Section Header Table

前面讲了ELF文件中有很多的段,但是如果没有人管他们就会很乱套,这个时候这些段的头头段表(Section Header Table)出现了,段表就是确保这些段的基本属性的结构。段表是ELF文件除头文件意外最重要的结构,描述了ELF的各个段的信息,比如每个段的段名、长度、在文件中的偏移、读写权限等。编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。前面使用objdump列出了simpleSction.o的主要段,其实是不全的,可以使用readelf工具查看完整的段结构:

![1](./ELF%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84/1.png)![1](./ELF%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84/1.png)There are 15 section headers, starting at offset 0x3f4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .group            GROUP           00000000 000034 000008 04     12  12  4
  [ 2] .text             PROGBITS        00000000 00003c 000081 00  AX  0   0  1
  [ 3] .rel.text         REL             00000000 000310 000048 08   I 12   2  4
  [ 4] .data             PROGBITS        00000000 0000c0 000008 00  WA  0   0  4
  [ 5] .bss              NOBITS          00000000 0000c8 000008 00  WA  0   0  4
  [ 6] .rodata           PROGBITS        00000000 0000c8 000004 00   A  0   0  1
  [ 7] .text.__x86.[...] PROGBITS        00000000 0000cc 000004 00 AXG  0   0  1
  [ 8] .comment          PROGBITS        00000000 0000d0 00002c 01  MS  0   0  1
  [ 9] .note.GNU-stack   PROGBITS        00000000 0000fc 000000 00      0   0  1
  [10] .eh_frame         PROGBITS        00000000 0000fc 00007c 00   A  0   0  4
  [11] .rel.eh_frame     REL             00000000 000358 000018 08   I 12  10  4
  [12] .symtab           SYMTAB          00000000 000178 000100 10     13   9  4
  [13] .strtab           STRTAB          00000000 000278 000098 00      0   0  1
  [14] .shstrtab         STRTAB          00000000 000370 000082 00      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),
  D (mbind), p (processor specific)

readelf输出的结果就是ELF文件段表的内容,段表是一个以”Elf32_Shdr“结构体为元素的数组。数组元素的个数等于段的个数,每个”Elf32_Shdr“结构体对应一个段。”Elf32_Shdr“又被称为段描述符(Section Descriptor),”Elf32_Shdr“的各个成员的含义如下图:

Elf32_Shdr结构体

sh_type

段的类型(sh_type)。段名只是在链接和编译过程中有意义,但它并不能真正表示段的类型。而且段名是可以通过代码更改的,对于编译器和链接器来说,主要决定短的属性的是段的类型(sh_type)和标志位(sh_flag),段的类型相关常量以SHT_开头,其中 SHT 是 Section Header Table 的简写:

名称 取值 说明
SHT_NULL 0 该类型节区是非活动的,这种类型的节头中的其它成员取值无意义。
SHT_PROGBITS 1 该类型节区包含程序定义的信息,它的格式和含义都由程序来决定。
SHT_SYMTAB 2 该类型节区包含一个符号表(SYMbol TABle)。目前目标文件对每种类型的节区都只 能包含一个,不过这个限制将来可能发生变化。 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。
SHT_STRTAB 3 该类型节区包含字符串表( STRing TABle )。
SHT_RELA 4 该类型节区包含显式指定位数的重定位项( RELocation entry with Addends ),例如,32 位目标文件中的 Elf32_Rela 类型。此外,目标文件可能拥有多个重定位节区。
SHT_HASH 5 该类型节区包含符号哈希表( HASH table )。
SHT_DYNAMIC 6 该类型节区包含动态链接的信息( DYNAMIC linking )。
SHT_NOTE 7 该类型节区包含以某种方式标记文件的信息(NOTE)。
SHT_NOBITS 8 该类型节区不占用文件的空间,其它方面和 SHT_PROGBITS 相似。尽管该类型节区不包含任何字节,其对应的节头成员 sh_offset 中还是会包含概念性的文件偏移。
SHT_REL 9 该类型节区包含重定位表项(RELocation entry without Addends),不过并没有指定位数。例如,32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB 10 该类型此节区被保留,不过其语义尚未被定义。
SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必 要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC 0X70000000 此值指定保留给处理器专用语义的下界( LOw PROCessor-specific semantics )。
SHT_HIPROC OX7FFFFFFF 此值指定保留给处理器专用语义的上界( HIgh PROCessor-specific semantics )。
SHT_LOUSER 0X80000000 此值指定保留给应用程序的索引下界。
SHT_HIUSER 0X8FFFFFFF 此值指定保留给应用程序的索引上界。

sh_flags

节头中 sh_flags 字段的每一个比特位都可以给出其相应的标记信息,其定义了对应的节区的内容是否可以被修改、被执行等信息。如果一个标志位被设置,则该位取值为 1,未定义的位都为 0。目前已定义值如下,其他值保留。

名称 说明
SHF_WRITE 0x1 这种节包含了进程运行过程中可以被写的数据。
SHF_ALLOC 0x2 这种节在进程运行时占用内存。对于不占用目标文件的内存镜像空间的某些控制节,该属性处于关闭状态 (off)。
SHF_EXECINSTR 0x4 这种节包含可执行的机器指令(EXECutable INSTRuction)。
SHF_MASKPROC 0xf0000000 所有在这个掩码中的比特位用于特定处理器语义。

当节区类型的不同的时候,sh_link 和 sh_info 也会具有不同的含义,如果段的类型是链接相关的(不论是动态链接还是静态链接),比如重定位表、符号表等,那么sh_link和sh_info这两个成员所包含的意义如下,对于其他类型的段,这两个成员没有意义:。

sh_type sh_link sh_info
SHT_DYNAMIC 节区中使用的字符串表的节头索引 0
SHT_HASH 此哈希表所使用的符号表的节头索引 0
SHT_REL/SHT_RELA 与符号表相关的节头索引 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 操作系统特定信息,Linux 中的 ELF 文件中该项指向符号表中符号所对应的字符串节区在 Section Header Table 中的偏移。 操作系统特定信息
other SHN_UNDEF 0

重定位表

在通过前面使用readelf查看时,发现有一个叫做.rel.text的段,类型是”SHT_REL“,是一个重定位表(Relocation Table)。链接器在处理目标文件时,需要对目标文件中的的位置进行重定位,即代码段和数据段中那些绝对地址的引用位置。这些重定位信息都记录在ELF文件的重定位表中。每个段都有一个重定位表,例如”.rel.text“是”text“段的重定位表。

一个重定位表是ELF中的一个段,这个段的类型是”SHT_REL“类型,”sh_link“表示符号表的下标,它的”sh_info“表示它作用于哪个段。比如”.rel.text“作用于”.text“段,而”.text“段的下标为”1“,那么”.rel.text“的”sh_info“为”1“。

字符串表

ELF文件中用到很多字符串,如段名、变量名、函数名等。但是字符串长度不一,所以将这些字符串存放到一个表中,然后使用字符串在表中的偏移来引用:

偏移 +0 +1 +2 +3 +4 +5 +6 +7 +8 +9
+0 \0 h e l l o w o r l
+10 d \0 M y v a r i a b
+20 l e \0
对应偏移的字符串为:

偏移 字符串
0 空字符串
1 helloworld
6 world
12 Myvariable
这样在ELF文件中,引用字符串只需给出一个数字下标就行,单个字符串都以\0结尾,所以不需要考虑长度问题。ELF中两个表为:

  • 字符串表(String Table):常见段名为”.strtab“
  • 段表字符串表(Section Header String Table):常见段名为”.shstrtab“

通过分析ELF文件头,可以得到段表和段表字符串表的位置。

链接的接口——符号

链接过程本质是把多个不同的文件通过函数和变量引用的方式链接起来

举个栗子:目标文件B需要用到目标文件A中的函数”func“,那么成目标文件A定义(define)了函数”func“,目标文件B引用(reference)了目标文件A中的函数”func“。这两个概念同样适用于变量。在连接中,将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)

在链接过程中可以将符号看做粘合剂,所以符号需要统一管理,**每个目标文件中都会有一个相应的符号表(Symbol Table),表中记录了目标文件所用到的所有符号,每个定义的符号有一个对应的值,叫做符号值(Symbol Value),符号是就是变量和函数的地址**,下面是符号表中所有符号的分类:

  • 定义在本目标文件的全局符号,可以被其他目标引用,例如SimpleSection.o中的”func1“、”main“和”global_init_var“。
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做外部符号(External Symbol),例如:SimpleSection.o中的”printf“
  • 段名,这种符号往往由编译器产生,它的值就是该段的起始地址,例如SimpleSection.o中的”.text“、”.data“等
  • 局部符号,这类符号只在编译单元内部可见。例如:SimpleSection.o里面的”static_var“和”static_var2“
  • 行号信息,即目标文件指令与源代码中代码行对应关系,可选

值得关注的是前两个,在链接过程中只关心全局符号的链接,局部符号、段名、行号都是次要的。

ELF符号表结构

ELF文件中的符号表往往是文件中的一个段,段名一般叫”.symtab“。表结构是一个Elf32_Sym结构的数组,每个Elf32_Sym结构对应一个符号。Elf32_Sym结构定义如下:

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

成员定义如下:

  • st_name:符号名。这个成员包含了该符号名在字符串表中的下标
  • st_value:符号相对应的值。这个值和符号有关,可能是一个绝对值,也可能是一个地址等,不同的符号对应的值含义不同
  • st_size:符号大小,对于包含数据的符号,这个值是该数据类型的大小,比如一个double类型的符号占用8个字节,如果该值为0,则表示该符号大小为0或位置
  • st_info:符号类型和绑定信息
  • st_other:该成员目前为0,没用
  • st_shndx:符号所在段

文章作者: XiaozaYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaozaYa !
  目录