编译器编译源代码后生成的文件叫做目标文件,而目标文件经过编译器链接之后得到的就是可执行文件。那么目标文件到底是什么?它和可执行文件又有什么区别?链接到底又做了什么呢?接下来,我们将探索一下目标文件的本质。

目标文件的格式

目前,PC 平台流行的 可执行文件格式(Executable) 主要包含如下两种,它们都是 COFF(Common File Format) 格式的变种。

  • Windows 下的 PE(Portable Executable)
  • Linux 下的 ELF(Executable Linkable Format)

目标文件就是源代码经过编译后但未进行链接的那些中间文件(Windows 的.obj和 Linux 的.o),它与可执行文件的格式非常相似,所以一般跟可执行文件格式一起采用同一种格式存储。在 Windows 下采用PE-COFF文件格式;Linux 下采用ELF文件格式。

事实上,除了可执行文件外,动态链接库(DDL,Dynamic Linking Library)静态链接库(Static Linking Library) 均采用可执行文件格式存储。它们在 Window 下均按照 PE-COFF 格式存储;Linux 下均按照 ELF 格式存储。只是文件名后缀不同而已。

  • 动态链接库:Windows 的.dll、Linux 的.so
  • 静态链接库:Windows 的.lib、Linux 的.a

下面,我们将以 ELF 文件为例进行介绍。

ELF 文件结构

注意:SegmentSection 的区别。很多地方对两者有所混淆。段是程序执行的必要组成,当多个目标文件链接成一个可执行文件时,会将相同权限的节合并到一个段中。相比而言,节的粒度更小。

如图所示,为 ELF 文件的基本结构,其主要由四部分组成:

  • ELF Header
  • ELF Program Header Table
  • ELF Section Header Table
  • ELF Sections

从图中,我们就能看出它们各自的数据结构以及相互之间的索引关系。下面我们依次进行介绍。

ELF Header

我们可以使用 readelf 工具来查看 ELF Header。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ readelf -h hello.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:          672 (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:         13
  Section header string table index: 10

ELF 文件结构示意图中定义的Elf_Ehdr的各个成员的含义与 readelf 具有对应关系。如下表所示:

成员 含义
e_ident Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2’s complement, little end
Version: 1(current)
OS/ABI: UNIX - System V
ABI Version: 0
e_type Type: REL (Relocatable file)
ELF 文件类型
e_machine Machine: Advanced Micro Devices X86-64
ELF 文件的 CPI 平台属性
e_version Version: 0x1
ELF 版本号。一般为常数 1
e_entry Entry point address: 0x0
入口地址,规定 ELF 程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位指令一般没有入口地址,则该值为 0
e_phoff Start of program headers: 0(bytes into file)
e_shoff Start of section headers: 672 (bytes into file)
Section Header Table 在文件中的偏移
e_word Flags: 0x0
ELF 标志位,用来标识一些 ELF 文件平台相关的属性。
e_ehsize Size of this header: 64 (bytes)
ELF Header 本身的大小
e_phentsize Size of program headers: 0 (bytes)
e_phnum Number of program headers: 0
e_shentsize Size of section headers: 64 (bytes)
单个 Section Header 大小
e_shnum Number of section headers: 13
Section Header 的数量
e_shstrndx Section header string table index: 10
Section Header 字符串表在 Section Header Table 中的索引

ELF 魔数

每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头 4 个字节,通常被称为魔数(Magic Number)。通过对魔数的判断可以确定文件的格式和类型。如:ELF 的可执行文件格式的头 4 个字节为0x7Felf;Java 的可执行文件格式的头 4 个字节为cafe;如果被执行的是 Shell 脚本或 perl、python 等解释型语言的脚本,那么它的第一行往往是#!/bin/sh#!/usr/bin/perl#!/usr/bin/python,此时前两个字节#!就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序路径。

ELF 文件类型

ELF 文件主要有三种类型,可以通过 ELF Header 中的e_type成员进行区分。

  • 可重定位文件(Relocatable File)ETL_REL。一般为.o文件。可以被链接成可执行文件或共享目标文件。静态链接库属于可重定位文件。

  • 可执行文件(Executable File)ET_EXEC。可以直接执行的程序。

  • 共享目标文件(Shared Object File):ET_DYN。一般为 .so 文件。有两种情况可以使用。

    • 链接器将其与其他可重定位文件、共享目标文件链接成新的目标文件;
  • 动态链接器将其与其他共享目标文件、结合一个可执行文件,创建进程映像。

ELF Section Header Table

ELF 节头表是一个节头数组。每一个节头都描述了其所对应的节的信息,如节名、节大小、在文件中的偏移、读写权限等。编译器、链接器、装载器都是通过节头表来定位和访问各个节的属性的。

我们可以使用 readelf 工具来查看节头表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ readelf -S hello.o

There are 13 section headers, starting at offset 0x2a0:

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
       0000000000000015  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000001f0
       0000000000000030  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000055
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  00000055
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000062
       0000000000000035  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000097
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  00000098
       0000000000000038  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000220
       0000000000000018  0000000000000018   I      11     8     8
  [10] .shstrtab         STRTAB           0000000000000000  00000238
       0000000000000061  0000000000000000           0     0     1
  [11] .symtab           SYMTAB           0000000000000000  000000d0
       0000000000000108  0000000000000018          12     9     8
  [12] .strtab           STRTAB           0000000000000000  000001d8
       0000000000000013  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

ELF 文件结构示意图中定义的Elf_Shdr的各个成员的含义与 readelf 具有对应关系。如下表所示:

成员 含义
sh_name 节名
节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过 Section Header 索引到)。sh_name 的值实际上是其节名字符串在.shstrtab中的偏移值
sh_type 节类型
sh_flags 节标志位
sh_addr 节地址:节的虚拟地址
如果该节可以被加载,则 sh_addr 为该节被加载后在进程地址空间中的虚拟地址;否则 sh_addr 为 0
sh_offset 节偏移
如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如 sh_offset 对于 BSS 节来说是没有意义的
sh_size 节大小
sh_link、sh_info 节链接信息
sh_addralign 节地址对齐方式
sh_entsize 节项大小
有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize 表示每个项的大小。如果为 0,则表示该节不包含固定大小的项。

节类型(sh_type)

节名是一个字符串,只是在链接和编译过程中有意义,但它并不能真正地表示节的类型。对于编译器和链接器来说,主要决定节的属性是节的类型(sh_type)和节的标志位(sh_flags)。

节的类型相关常量以SHT_开头,上述readelf -S命令执行的结果省略了该前缀。常见的节类型如下表所示:

常量 含义
SHT_NULL 0 无效节
SHT_PROGBITS 1 程序节。代码节、数据节都是这种类型。
SHT_SYMTAB 2 符号表
SHT_STRTAB 3 字符串表
SHT_RELA 4 重定位表。该节包含了重定位信息。
SHT_HASH 5 符号表的哈希表
SHT_DYNAMIC 6 动态链接信息
SHT_NOTE 7 提示性信息
SHT_NOBITS 8 表示该节在文件中没有内容。如.bss
SHT_REL 9 该节包含了重定位信息
SHT_SHLIB 10 保留
SHT_DNYSYM 11 动态链接的符号表

节标志位(sh_flag)

节标志位表示该节在进程虚拟地址空间中的属性。如是否可写、是否可执行等。相关常量以SHF_开头。常见的节标志位如下表所示:

常量 含义
SHF_WRITE 1 表示该节在进程空间中可写
SHF_ALLOC 2 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。
SHF_EXECINSTR 4 表示该节在进程空间中可以被执行

节链接信息(sh_link、sh_info)

如果节的类型是与链接相关的(无论是动态链接还是静态链接),如**重定位表、符号表、**等,则sh_linksh_info两个成员所包含的意义如下所示。其他类型的节,这两个成员没有意义。

sh_type sh_link sh_info
SHT_DYNAMIC 该节所使用的字符串表在节头表中的下标 0
SHT_HASH 该节所使用的符号表在节头表中的下标 0
SHT_REL 该节所使用的相应符号表在节头表中的下标 该重定位表所作用的节在节头表中的下标
SHT_RELA 该节所使用的相应符号表在节头表中的下标 该重定位表所作用的节在节头表中的下标
SHT_SYMTAB 操作系统相关 操作系统相关
SHT_DYNSYM 操作系统相关 操作系统相关
other SHN_UNDEF 0

ELF Sections

节的分类

上述 ELF Section Header Table 部分已经简单介绍了节类型。接下来我们来介绍详细一些比较重要的节。

.text 节

.text节是保存了程序代码指令的代码节一段可执行程序,如果存在 Phdr,则.text节就会存在于text段中。由于.text节保存了程序代码,所以节类型为SHT_PROGBITS

.rodata 节

rodata节保存了只读的数据,如一行 C 语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能在text段(不是data段)中找到.rodata节。由于.rodata节是只读的,所以节类型为SHT_PROGBITS

.plt 节(过程链接表)

.plt节也称为过程链接表(Procedure Linkage Table)其包含了动态链接器调用从共享库导入的函数所必需的相关代码。由于.plt节保存了代码,所以节类型为SHT_PROGBITS

.data 节

.data节存在于data段中,其保存了初始化的全局变量等数据。由于.data节保存了程序的变量数据,所以节类型为SHT_PROGBITS

.bss 节

.bss节存在于data段中,占用空间不超过 4 字节,仅表示这个节本省的空间。.bss节保存了未进行初始化的全局数据。程序加载时数据被初始化为 0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,所以节类型为SHT_NOBITS

.got.plt 节(全局偏移表-过程链接表)

.got节保存了全局偏移表.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。由于.got.plt节与程序执行有关,所以节类型为SHT_PROGBITS

.dynsym 节(动态链接符号表)

.dynsym节保存在text段中。其保存了从共享库导入的动态符号表。节类型为SHT_DYNSYM

.dynstr 节(动态链接字符串表)

.dynstr保存了动态链接字符串表,表中存放了一系列字符串,这些字符串代表了符号名称,以空字符作为终止符。

.rel.*节(重定位表)

重定位表保存了重定位相关的信息,这些信息描述了如何在链接或运行时,对 ELF 目标文件的某部分或者进程镜像进行补充或修改。由于重定位表保存了重定位相关的数据,所以节类型为SHT_REL

.hash 节

.hash节也称为.gnu.hash,其保存了一个用于查找符号的散列表。

.symtab 节(符号表)

.symtab节是一个ElfN_Sym的数组,保存了符号信息。节类型为SHT_SYMTAB

.strtab 节(字符串表)

.strtab节保存的是符号字符串表,表中的内容会被.symtabElfN_Sym结构中的st_name引用。节类型为SHT_STRTAB

.ctors 节和.dtors 节

.ctors构造器)节和.dtors析构器)节分别保存了指向构造函数和析构函数的函数指针,构造函数是在 main 函数执行之前需要执行的代码;析构函数是在 main 函数之后需要执行的代码

符号表

节的分类中我们介绍了.dynsym节和.symtab节,两者都是符号表。那么它们到底有什么区别呢?存在什么关系呢?

符号是对某些类型的数据或代码(如全局变量或函数)的符号引用,函数名或变量名就是符号名。例如,printf()函数会在动态链接符号表.dynsym中存有一个指向该函数的符号项(以Elf_Sym数据结构表示)。在大多数共享库和动态链接可执行文件中,存在两个符号表。即.dynsym.symtab

.dynsym保存了引用来自外部文件符号的全局符号。如printf库函数。.dynsym保存的符号是.symtab所保存符合的子集,.symtab中还保存了可执行文件的本地符号。如全局变量,代码中定义的本地函数等。

既然.dynsym.symtab的子集,那为何要同时存在两个符号表呢?

通过readelf -S命令可以查看可执行文件的输出,一部分节标志位(sh_flags)被标记为了A(ALLOC)、WA(WRITE/ALLOC)、AX(ALLOC/EXEC)。其中,.dynsym被标记为 ALLOC,而.symtab则没有标记。

ALLOC 表示有该标记的节会在运行时分配并装载进入内存,而.symtab不是在运行时必需的,因此不会被装载到内存中。.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需的唯一符号.dynsym对于动态链接可执行文件的执行是必需的,而.symtab只是用来进行调试和链接的。

img
img

上图所示为通过符号表索引字符串表的示意图。符号表中的每一项都是一个Elf_Sym结构,对应可以在字符串表中索引得到一个字符串。该数据结构中成员的含义如下表所示:

成员 含义
st_name 符号名。该值为该符号名在字符串表中的偏移地址。
st_value 符号对应的值。存放符号的值(可能是地址或位置偏移量)。
st_size 符号的大小。
st_other 0
st_shndx 符号所在的节
st_info 符号类型及绑定属性

使用 readelf 工具我们也能够看到符号表的相关信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ readelf -s hello.o

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

字符串表

类似于符号表,在大多数共享库和动态链接可执行文件中,也存在两个字符串表。即.dynstr.strtab,分别对应于.dynsymsymtab。此外,还有一个.shstrtab的节头字符串表,用于保存节头表中用到的字符串,可通过sh_name进行索引。

ELF 文件中所有字符表的结构基本一致,如上图所示。

重定位表

重定位就是将符号定义和符号引用进行连接的过程。可重定位文件需要包含描述如何修改节内容的相关信息,从而使可执行文件和共享目标文件能够保存进程的程序镜像所需要的正确信息。

重定位表是进行重定位的重要依据。我们可以使用 objdump 工具查看目标文件的重定位表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ objdump -r hello.o


hello.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE
0000000000000005 R_X86_64_32       .rodata
000000000000000a R_X86_64_PC32     puts-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE
0000000000000020 R_X86_64_PC32     .text

重定位表是一个Elf_Rel类型的数组结构,每一项对应一个需要进行重定位的项。 其成员含义如下表所示:

成员 含义
r_offset 重定位入口的偏移。
对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于节起始的偏移
对于可执行文件或共享对象文件来说,这个值是该重定位入口所要修正的位置的第一个字节的虚拟地址
r_info 重定位入口的类型和符号
因为不同处理器的指令系统不一样,所以重定位所要修正的指令地址格式也不一样。每种处理器都有自己的一套重定位入口的类型。
对于可执行文件和共享目标文件来说,它们的重定位入口是动态链接类型的。

重定位是目标文件链接成为可执行文件的关键。我们将在后面的进行介绍。

对象文件格式

对象文件

首先,你需要知道的是所谓对象文件(Object files)有三个种类:

  • 可重定位的对象文件(Relocatable file)

  • 可执行的对象文件(Executable file)

  • 可被共享的对象文件(Shared object file)

  • 可重定位的对象文件(Relocatable file)

    适于链接的可重定位文件(relocatable file),包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件。

    这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和 JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。

  • 可执行的对象文件(Executable file)

    适于执行的可执行文件(executable file),包含可以直接拷贝进行内存执行的二进制代码和数据。用于提供程序的进程映像,加载的内存执行。

    这我们见的多了。文本编辑器 vi、调式用的工具 gdb、播放 mp3 歌曲的软件 mplayer 等等都是 Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如 shell 脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。

  • 可被共享的对象文件(Shared object file)

    共享目标文件(shared object file),一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进行动态链接。连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

    这些就是所谓的动态库文件,也即 .so 文件。如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空间;另外如果拿它们放到 Linux 系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程中,必须经过两个步骤:

    1. 链接编辑器(link editor)拿它和其他 Relocatable object file 以及其他 shared object file 作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。
    2. 在运行时,动态链接器(dynamic linker)拿它和一个 Executable file 以及另外一些 Shared object file 来一起处理,在 Linux 系统里面创建一个进程映像。

文件格式

本质上,对象文件只是保存在磁盘文件中的一串字节,每个系统的文件格式都不尽相同:

  • Bell 实验室的第一个 Unix 系统使用 a.out 格式。
  • System V Unix 的早期版本使用 Common Object File Format(COFF)。
  • Windows NT 使用 COFF 的变种,叫做 Portable Executable(PE)。
  • 现代 Unix 系统,包括 Linux、新版 System V、BSD 变种、Solaris 都使用 Executable and Linkable Format(ELF)。

ELF 对象文件格式

ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。它自最早在 System V 系统上出现后,被 xNIX 世界所广泛接受,作为缺省的二进制文件格式来使用。可以说,ELF 是构成众多 xNIX 系统的基础之一。

ELF 代表 Executable and Linkable Format。他是一种对可执行文件、目标文件和库使用的文件格式。

他在 Linux 下成为标准格式已经很长时间, 代替了早年的 a.out 格式。ELF 一个特别的优点在于, 同一文件格式可以用于内核支持的几乎所有体系结构上, 这不仅简化了用户空间工具程序的创建, 也简化了内核自身的程序设计。例如, 在必须为可执行文件生成装载程序例程时。

但是文件格式相同并不意味着不同系统上的程序之间存在二进制兼容性, 例如, FreeBSD 和 Linux 都使用 ELF 作为二进制格式。尽管二者在文件中组织数据的方式相同。但在系统调用机制以及系统调用的语义方面, 仍然有差别。这也是在没有中间仿真层的情况下, FreeBSD 程序不能在 linux 下运行的原因(反过来同样是如此)。

有一点是可以理解的, 二进制程序不能在不同体系结构交换(例如, 为 Alpha CPU 编译的 Linux 二进制程序不能在 Sparc Linux 上执行), 因为底层的体系结构是完全不同的。但是由于 ELF 的存在, 对所有体系结构而言, 程序本身的相关信息以及程序的各个部分在二进制文件中编码的方式都是相同的。

Linux 不仅将 ELF 用于用户空间程序和库, 还用于构建模块。内核本身也是 ELF 格式。

ELF 文件标准历史

ELF 是一种开放格式, 其规范可以自由获取。

在 ELF 格式出来之后,TISC(Tool Interface Standard Committee)委员会定义了一套 ELF 标准。你可以从这里(http://refspecs.freestandards.org/elf/)找到详细的标准文档

20 世纪 90 年代,一些厂商联合成立了一个委员会(TISC 委员会),起草并发布了一个 ELF 文件格式标准供公开使用,并且希望所有人能够遵循这项标准并且从中获益。1993 年,委员会发布了 ELF 文件标准。当时参与该委员会的有来自于编译器的厂商,如 Watcom 和 Borland;来自 CPU 的厂商如 IBM 和 Intel;来自操作系统的厂商如 IBM 和 Microsoft。1995 年,委员会发布了 ELF 1.2 标准,自此委员会完成了自己的使命,不久就解散了。所以 ELF 文件格式标准的最新版本为 1.2。

文件类型 etype 成员表示 ELF 文件类型,即前面提到过的 3 种 ELF 文件类型,每个文件类型对应一个常量。系统通过这个常量来判断 ELF 的真正文件类型,而不是通过文件的扩展名。相关常量以“ET”开头,

TISC 委员会前后出了两个版本,v1.1 和 v1.2。两个版本内容上差不多,但就可读性上来讲,我还是推荐你读 v1.2 的。因为在 v1.2 版本中,TISC 重新组织原本在 v1.1 版本中的内容,将它们分成为三个部分(books): a) Book I 介绍了通用的适用于所有 32 位架构处理器的 ELF 相关内容 b) Book II 介绍了处理器特定的 ELF 相关内容,这里是以 Intel x86 架构处理器作为例子介绍 c) Book III 介绍了操作系统特定的 ELF 相关内容,这里是以运行在 x86 上面的 UNIX System V.4 作为例子介绍

值得一说的是,虽然 TISC 是以 x86 为例子介绍 ELF 规范的,但是如果你是想知道非 x86 下面的 ELF 实现情况,那也可以在http://refspecs.freestandards.org/elf/中找到特定处理器相关的Supplment文档。比方ARM相关的,或者MIPS相关的等等。另外,相比较UNIX系统的另外一个分支BSD Unix,Linux 系统更靠近 System V 系统。所以关于操作系统特定的 ELF 内容,你可以直接参考 v1.2 标准中的内容。

本文所使用的测试程序结构

add.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

// 不指定寄存器实现两个整数相加
int Add(int a, int b)
{
    __asm__ __volatile__
    (
        //"lock;\n"
        "addl %1,%0;\n"
        : "=m"(a)
        : "r"(b), "m"(a)
      //  :
    );

    return a;
}

sub.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

// 不指定寄存器实现两个参数相减
int Sub(int a, int b)
{
    __asm__ __volatile__
    (
        "subl %1, %0;"
        : "=m"(a)
        : "r"(b), "m"(a)
 //       :
    );

    return a;
}

testelf.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int a = 3, b = 5;

    printf("%d + %d = %d\n", a, b, Add(a, b));
    printf("%d - %d = %d\n", a, b, Sub(a, b));


    return EXIT_SUCCESS;
}

Makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
target=testelf_normal testelf_dynamic testelf_static

MAIN_OBJS=testelf.o
SUBS_OBJS=add.o sub.o

DYNA_FILE=libtestelf.so
STAT_FILE=libtestelf.a

all:$(target)

%.o : %.c
    $(CC) -c $^ -o $@

clean :
    rm -rf $(MAIN_OBJS) $(SUBS_OBJS)
    rm -rf $(DYNA_FILE) $(STAT_FILE)
    rm -rf $(target)


# Complie the execute
testelf_normal:$(MAIN_OBJS) $(SUBS_OBJS)
    gcc $^ -o $@

testelf_dynamic:$(MAIN_OBJS) $(DYNA_FILE)
    gcc  $^ -o $@ -L./ -ltestelf

testelf_static:$(MAIN_OBJS) $(STAT_FILE)
    gcc  testelf.o -o $@ -static -L./ -ltestelf



# Complie the Dynamic Link Library libtestelf.so
libtestelf.so:$(SUBS_OBJS)
    gcc -fPCI -shared $^ -o $@

# Complie the Static Link Library libtestelf.so
libtestelf.a:$(SUBS_OBJS)
    ar -r $@ $^

我们编写了两个库函数分别实现 add 和 sub 的功能, 然后编写了一个测试代码 testelf.c 调用了 Add 和 Sub.

然后我们的 Mmakefile 为测试程序编写了 3 分程序

  1. 普通的程序 testelf_normal, 由 add.o sub.o 和 testelf.o 直接链接生成
  2. 动态链接程序 testelf_dynamic, 将 add.o 和 sub.o 先链接成动态链接库 libtestelf.so, 然后再动态链接生成 testelf_dynamic
  3. 静态链接程序 testelf_static, 将 add.o 和 sub.o 先静态链接成静态库 libtestelf.a, 然后再静态链接生成可执行程序 testelf_staticke

我们在源代码目录执行 make 后会完成编译, 编译完成后

  • add.o, sub.o 和 testelf.o 是可重定位的对象文件(Relocatable file)
  • libtestelf.so 是可被共享的对象文件(Shared object file)
  • testelf_normal, testelf_dynamic 和 testelf_static 是可执行的对象文件(Executable file)

如下图所示

对象文件
对象文件

ELF 可执行与链接文件格式详解

布局和结构

ELF 文件由各个部分组成。

为了方便和高效,ELF 文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度

elf文件的布局和结构
elf文件的布局和结构

首先图的左边部分,它是以链接视图来看待 elf 文件的, 从左边可以看出,包含了一个 ELF 头部,它描绘了整个文件的组织结构。它还包括很多节区(section)。这些节有的是系统定义好的,有些是用户在文件在通过.section 命令自定义的,链接器会将多个输入目标文件中的相同的节合并。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等等。除此之外,还包含程序头部表(可选)和节区 头部表,程序头部表,告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,可重定位文件不需要这个表。而节区头部表(Section Heade Table)包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表,其他目标文件可以有,也可以没有这个表。

需要注意地是:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。

右半图是以程序执行视图来看待的,与左边对应,多了一个段(segment)的概念,编译器在生成目标文件时,通常使用从零开始的相对地址,而在链接过程中,链接器从一个指定的地址开始,根据输入目标文件的顺序,以段(segment)为单位将它们拼装起来。其中每个段可以包括很多个节(section)。

  • elf 头部

    除了用于标识 ELF 文件的几个字节外, ELF 头还包含了有关文件类型和大小的有关信息, 以及文件加载后程序执行的入口点信息

  • 程序头表(program header table)

    程序头表向系统提供了可执行文件的数据在进程虚拟地址空间中组织文件的相关信息。他还表示了文件可能包含的段数据、段的位置和用途

  • 段 segment

    各个段保存了与文件爱你相关的各种形式的数据, 例如,符号表、实际的二进制码、固定值(如字符串)活程序使用的数值常数

  • 节头表 section

    包含了与各段相关的附加信息。

ELF 基本数据类型定义

在具体介绍 ELF 的格式之前,我们先来了解在 ELF 文件中都有哪些数据类型的定义:

ELF 数据编码顺序与机器相关,为了使数据结构更加通用, linux 内核自定义了几种通用的数据, 使得数据的表示与具体体系结构分离

但是由于 32 位程序和 64 位程序所使用的数据宽度不同, 同时 64 位机必须兼容的执行 32 位程序, 因此我们所有的数据都被定义为 32bit 和 64bit 两个不同类型的数据

常规定义在include/uapi/linux中, 各个结构也可以按照需求重新定义

32 位机器上的定义

名称 常规定义 大小 对齐 目的
Elf32_Addr __u32 4 4 无符号程序地址
Elf32_Half __u16 2 2 无符号中等整数
Elf32_Off __u32 4 4 无符号文件偏移
Elf32_SWord __u32 4 4 有符号大整数
Elf32_Word __u32 4 4 无符号大整数
unsigned char 1 1 无符号小整数

*64 位机器上的定义**

名称 常规定义 大小 对齐 目的
Elf64_Addr __u64 8 8 无符号程序地址
Elf64_Half __u16 2 2 无符号小整数
Elf64_SHalf __s16 2 2 无符号小整数
Elf64_Off __u64 8 8 无符号文件偏移
Elf64_Sword __s32 4 4 有符号中等整数
Elf64_Word __u32 4 4 无符号中等整数
Elf64_Xword __u64 8 8 无符号大整数
Elf64_Sxword __s64 8 8 有符号大整数
unsigned char 1 1 无符号小整数

ELF 头部 Elfxx_Ehdr

elf 头部用 Elfxx_Ehdr 结构(被定义在linux/uapi/linux/elf.h来表示, Elf32_Ehdr(32bit)Elf64_Ehdr(64bit)

数据成员

内部成员, 如下

成员 类型 描述
e_ident[EI_NIDENT] unsigned char 目标文件标识信息, EI_NIDENT=16, 因此共占用 128 位
e_type Elf32_Half/Elf64_Half 目标文件类型
e_machine Elf32_Half/Elf64_Half 目标体系结构类型
e_version Elf32_Word/Elf64_Word 目标文件版本
e_entry Elf32_Addr/Elf64_Addr 程序入口的虚拟地址,若没有,可为 0
e_phoff Elf32_Off/Elf64_Off 程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为 0
e_shoff Elf32_Off/Elf64_Off 节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为 0
e_flags Elf32_Word/Elf64_Word 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag 的格式
e_ehsize Elf32_Half/Elf64_Half ELF 头部的大小(以字节计算)
e_phentsize Elf32_Half/Elf64_Half 程序头部表格的表项大小(按字节计算)
e_phnum Elf32_Half/Elf64_Half 程序头部表格的表项数目。可以为 0
e_shentsize Elf32_Half/Elf64_Half 节区头部表格的表项大小(按字节计算)
e_shnum Elf32_Half/Elf64_Half 节区头部表格的表项数目。可以为 0
e_shstrndx Elf32_Half/Elf64_Half 节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF

ELF 魔数 e_ident

魔数

很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)。根据这几个字节的内容就可以确定文件类型,因此这几个字节的内容被称为魔数 (magic number)。此外在一些程序代码中,程序员常常将在代码中出现但没有解释的数字常量或字符串称为魔数 (magic number)或魔字符串。

ELF 魔数 我们可以从前面 readelf 的输出看到,最前面的”Magic”的 16 个字节刚好对应“Elf32_Ehdr”的 e_ident 这个成员。这 16 个字节被 ELF 标准规定用来标识 ELF 文件的平台属性,比如这个 ELF 字长(32 位/64 位)、字节序、ELF 文件版本

最开始的 4 个字节是所有 ELF 文件都必须相同的标识码,分别为 0x7F、0x45、0x4c、0x46 第一个字节对应 ASCII 字符里面的 DEL 控制符, 后面 3 个字节刚好是 ELF 这 3 个字母的 ASCII 码。这 4 个字节又被称为 ELF 文件的魔数,几乎所有的可执行文件格式的最开始的几个字节都是魔数。

比如 a.out 格式最开始两个字节为 0x01、0x07;

PE/COFF 文件最开始两个个字节为 0x4d、0x5a,即 ASCII 字符 MZ。

这种魔数用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载。

接下来的一个字节是用来标识 ELF 的文件类的,0x01 表示是 32 位的,0x02 表示是 64 位的;第 6 个字是字节序,规定该 ELF 文件是大端的还是小端的(见附录:字节序)。第 7 个字节规定 ELF 文件的主版本号,一般是 1,因为 ELF 标准自 1.2 版以后就再也没有更新了。后面的 9 个字节 ELF 标准没有定义,一般填 0,有些平台会使用这 9 个字节作为扩展标志。

各种魔数的由来

a.out 格式的魔数为 0x01、0x07,为什么会规定这个魔数呢?

UNIX 早年是在 PDP 小型机上诞生的,当时的系统在加载一个可执行文件后直接从文件的第一个字节开始执行,人们一般在文件的最开始放置一条跳转(jump)指令,这条指令负责跳过接下来的 7 个机器字的文件头到可执行文件的真正入口。而 0x01 0x07 这两个字节刚好是当时 PDP-11 的机器的跳转 7 个机器字的指令。为了跟以前的系统保持兼容性,这条跳转指令被当作魔数一直被保留到了几十年后的今天。

计算机系统中有很多怪异的设计背后有着很有趣的历史和传统,了解它们的由来可以让我们了解到很多很有意思的事情。这让我想起了经济学里面所谓的“路径依赖”,其中一个很有意思的叫“马屁股决定航天飞机”的故事在网上流传很广泛,有兴趣的话你可以在 google 以“马屁股”和“航天飞机”作为关键字搜索一下。 其中需要注意地是 e_ident 是一个 16 字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如 byte0 的下标 0 别名为 EI_MAG0,具体如下:

名称 元素下标值 含义
EI_MAG0 0 文件标识
EI_MAG1 1 文件标识
EI_MAG2 2 文件标识
EI_MAG3 3 文件标识
EI_CLASS 4 文件类
EI_DATA 5 数据编码
EI_VERSION 6 文件版本
EI_PAD 7 补齐字节开始处
EI_NIDENT 16 e_ident[]大小

e_ident[EI_MAG0]~e_ident[EI_MAG3]即 e_ident[0]~e_ident[3]被称为魔数(Magic Number),其值一般为 0x7f,’E’,’L’,’F’

e_ident[EI_CLASS](即 e_ident[4])识别目标文件运行在目标机器的类别,取值可为三种值:

名称 元素下标值 含义
ELFCLASSNONE 0 非法类别
ELFCLASS32 1 32 位目标
ELFCLASS64 2 64 位目标

e_ident[EI_DATA](即 e_ident[5]):给出处理器特定数据的数据编码方式。即大端还是小端方式。取值可为 3 种:

名称 元素下标值 含义
ELFDATANONE 0 非法数据编码
ELFDATA2LSB 1 高位在前
ELFDATA2MSB 2 低位在前

目标文件类型 e_type

e_type 表示 elf 文件的类型

文件类型 etype 成员表示 ELF 文件类型,即前面提到过的 3 种 ELF 文件类型,每个文件类型对应一个常量。系统通过这个常量来判断 ELF 的真正文件类型,而不是通过文件的扩展名。相关常量以“ET”开头

如下定义:

名称 取值 含义
ET_NONE 0 未知目标文件格式
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件
ET_CORE 4 Core 文件(转储格式)
ET_LOPROC 0xff00 特定处理器文件
ET_HIPROC 0xffff 特定处理器文件
ET_LOPROC~ET_HIPROC 0xff00~0xffff 特定处理器文件

目标体系结构类型 e_machine

e_machine 表示目标体系结构类型

名称 取值 含义
EM_NONE 0 未指定
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel 80386
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 7 Intel 80860
EM_MIPS 8 MIPS RS3000
others 9~ 预留

ELF 版本 e_version

这个用来区分 ELF 标准的各个修订版本, 但是前面提到 ELF 最新版本就是 1(1.2), 仍然是最新版, 因此目前不需要这个特性

另外 ELF 头还包括了 ELF 文件的各个其他部分的长度和索引位置信息。因为这些部分的长度可能依程序而不同。所以在文件头部必须提供相应的数据.

readelf -h 查看 elf 头部

可重定位的对象文件(Relocatable file)

1
readelf -h add.o1

REL的elf文件头
REL的elf文件头

文件类型是, 说明是可重定位文件, 其代码可以移动至任何位置.

该文件没有程序头表, 对需要进行链接的对象而言, 程序头表是不必要的, 为此所有长度都设置为 0

可执行的对象文件(Executable file)

1
readelf -h testelf_dynamic1

exec的elf文件头
exec的elf文件头

可被共享的对象文件(Shared object file)

1
readelf -h libtestelf.so1

dynamic的elf文件头
dynamic的elf文件头

程序头部 Elf32_phdr

以程序运行的角度看 ELF 文件, 就需要程序头表,即要运行这个 elf 文件,需要将哪些东西载入到内存镜像。而节区头部表是以 elf 资源的角度来看待 elf 文件的,即这个 elf 文件到底存在哪些资源,以及这些资源之间的关联关系,

程序头部是一个表,它的起始地址在 elf 头部结构中的 e_phoff 成员指定,数量由 e_phnum 表示,每个程序头部表项的大小由 e_phentsize 指出。

可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的”段”包含一个或者多个”节区”,也就是” 段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件有意义。

下面来看程序头号部表项的数据结构

成员 类型 描述
p_type Elf32_Word/Elf64_Word 段类型
p_offset Elf32_Off/Elf64_Off 段位置
p_vaddr Elf32_Addr/Elf64_Addr 给出段的第一个字节将被放到内存中的虚拟地址
p_paddr Elf32_Addr/Elf64_Addr 仅用于与物理地址相关的系统中
p_filesz Elf32_Word/Elf64_Word 给出段在文件映像中所占的字节数
p_memsz Elf32_Word/Elf64_Word 给出段在内存映像中占用的字节数
p_flags Elf32_Word/Elf64_Word 与段相关的标志
p_align Elf32_Word/Elf64_Word 对齐

段类型 p_type


名称 取值 说明
PT_NULL 0 此数组元素未用。结构中其他成员都是未定义的
PT_DYNAMIC 2 数组元素给出动态链接信息
PT_INTERP 3 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面
PT_NOTE 4 此数组元素给出附加信息的位置和大小
PT_SHLIB 5 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI 不符
PT_PHDR 6 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面
PT_LOPROC~PT_HIPROC 0x70000000~0x7fffffff 此范围的类型保留给处理器专用语义

readelf -l 查看程序头表

在程序头表之后, 列出了6个段, 这些组成了最终在内存中执行的程序.

其还提供了各段在虚拟地址空间和物理空间的大小, 位置, 标志, 访问权限和对齐方面的信息. 还指定了 yui 个类型来更精确的描述段.

各段的语义如下

描述
PHDR 保存了程序头表
INTERP 指定在程序已经从可执行文件映射到内存之后, 必须调用的解释器. 在这里解释器并不意味着二进制文件的内容必须解释执行(比如 Java 的字节码需要 Java 虚拟机解释).它指的是这样一个程序:通过链接其他库, 来满足未解决的引用. 通常/lib/ld-linux.so.2, /lib/ld-linux-ia-64.so.2 等库, 用于在虚拟地址空间中国插入程序运行所需的动态库. 对几乎所有的程序来说, 可能 C 标准库都是必须映射的.还需要添加的各种库, 如 GTK, QT, 数学库 math, 线程库 pthread 等等
LOAD 表示一个需要从二进制文件映射到虚拟地址的段. 其中保存了常量数据(如字符串), 程序的目标代码等
DYNAMIC 该段保存了由动态链接器(即, INTERP 中指定的解释器)使用的信息
NOTE 保存了专有信息

可重定位的对象文件(Relocatable file)


1
readelf -l add.o1

rel的elf程序头表
rel的elf程序头表

可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

可执行的对象文件(Executable file)

1
readelf -l testelf_dynamic1

exec的elf程序头表
exec的elf程序头表

可被共享的对象文件(Shared object file)

1
readelf -l libtestelf.so1

dyn的elf程序头表
dyn的elf程序头表

虚拟地址空间中的各个段, 填充了来自 ELF 文件中特定的段的数据. 因而 readelf 输出的第二部分指定了那些节载入到哪些段(节段映射).

物理地址信息讲被忽略, 因为该信息是由内盒根据物理页帧到虚拟地址空间中相应位置的映射情况动态分配的.只有在没有 MMU(因而没有虚拟内存)的系统上该信息才是由意义的

节区(Sections)


节区中包含目标文件中的所有信息

除了:ELF 头部、程序头部表格、节区头部表格。

节区满足以下条件:

  1. 目标文件中的每个节区都有对应的节区头部描述它,反过来,有节区头部不意味着有节区。
  2. 每个节区占用文件中一个连续字节区域(这个区域可能长度为 0)。
  3. 文件中的节区不能重叠,不允许一个字节存在于两个节区中的情况发生。
  4. 目标文件中可能包含非活动空间(INACTIVE SPACE)。这些区域不属于任何头部和节区,其内容未指定。

节区头部表格


ELF 文件在描述各段的内容时, 是指定了哪些节的数据映射到段中. 因此需要一个结构来管理各个节的内容, 即节头表

节区头部表是以 elf 资源的角度来看待 elf 文件的,即这个 elf 文件到底存在哪些资源,以及这些资源之间的关联关系,而前面提到的程序头部表,则以程序运行来看 elf 文件的,即要运行这个 elf 文件,需要将哪些东西载入到内存镜像。

ELF 头部中,

e_shoff 成员给出从文件头到节区头部表格的偏移字节数;

e_shnum 给出表格中条目数目;

e_shentsize 给出每个项目的字节数。

从这些信息中可以确切地定位节区的具体位置、长度。

从之前的描述中可知,每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间,每一项元素为一结构体

那么这个节区头部由 elfxx_shdr(定义在include/uapi/linux/elf.h), 32 位 elf32_shdr, 64 位 elf64_shdr

结构体的成员如下

成员 类型 描述
sh_name Elf32_Word/Elf64_Word 节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串
sh_type Elf32_Word/Elf64_Word 节区类型
sh_flags Elf32_Word/Elf64_Word 节区标志
sh_addr Elf32_Addr/Elf64_Addr 如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0
sh_offset Elf32_Off/Elf64_Off 此成员的取值给出节区的第一个字节与文件头之间的偏移
sh_size Elf32_Word/Elf64_Word 此成员给出节区的长度(字节数)
sh_link Elf32_Word/Elf64_Word 此成员给出节区头部表索引链接。其具体的解释依赖于节区类型
sh_info Elf32_Word/Elf64_Word 此成员给出附加信息,其解释依赖于节区类型
sh_addralign Elf32_Word/Elf64_Word 某些节区带有地址对齐约束
sh_entsize Elf32_Word/Elf64_Word 某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数

节区类型 sh_type


sh_type 的取值如下:

名称 取值 说明
SHT_NULL 0 此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义
SHT_PROGBITS 1 此节区包含程序定义的信息,其格式和含义都由程序来解释
SHT_SYMTAB 2 此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化 一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接
SHT_STRTAB 3 此节区包含字符串表。目标文件可能包含多个字符串表节区
SHT_RELA 4 此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区
SHT_HASH 5 此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除
SHT_DYNAMIC 6 此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制
SHT_NOTE 7 此节区包含以某种方式来标记文件的信息
SHT_NOBITS 8 这种类型的节区不占用文件中的空间 , 其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员 sh_offset 中还是会包含概念性的文件偏移
SHT_REL 9 此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区
SHT_SHLIB 10 此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容
SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间
SHT_LOPROC X70000000 这一段(包括两个边界),是保留给处理器专用语义的
SHT_HIPROC OX7FFFFFFF 这一段(包括两个边界),是保留给处理器专用语义的
SHT_LOUSER 0X80000000 此值给出保留给应用程序的索引下界
SHT_HIUSER 0X8FFFFFFF 此值给出保留给应用程序的索引上界

节区标志 sh_flags


sh_flag 标志着此节区是否可以修改,是否可以执行,如下定义:

名称 取值 含义
SHF_WRITE 0x1 节区包含进程执行过程中将可写的数据
SHF_ALLOC 0x2 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0
SHF_EXECINSTR 0x4 节区包含可执行的机器指令
SHF_MASKPROC 0xF0000000 所有包含于此掩码中的四位都用于处理器专用的语义

sh_link 和 sh_info 字段的具体含义依赖于 sh_type 的值

sh_type sh_link sh_info
SHT_DYNAMIC 此节区中条目所用到的字符串表格的节区头部索引 0
SHT_HASH 此哈希表所适用的符号表的节区头部索引 0
SHT_REL SHT_RELA 相关符号表的节区头部索引 重定位所适用的节区的节区头部索引
SHT_SYMTAB SHT_DYNSYM 相关联的字符串表的节区头部索引 最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一
其它 SHN_UNDEF 0

特殊节区


有些节区是系统预订的,一般以点开头号,因此,我们有必要了解一些常用到的系统节区。

名称 类型 属性 含义
.bss SHT_NOBITS SHF_ALLOC +SHF_WRITE 包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间
.comment SHT_PROGBITS (无) 包含版本控制信息
.data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
.data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中
.debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息
.dynamic SHT_DYNAMIC 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表
.fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码
.got SHT_PROGBITS 此节区包含全局偏移表
.hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表
.init SHT_PROGBITS SHF_ALLOC +SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码
.interp SHT_PROGBITS 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0
.line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的
.note SHT_NOTE (无) 此节区中包含注释信息,�9C� 独立的格式。
.plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)
.relname .relaname SHT_REL SHT_RELA 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text
.rodata .rodata1 SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段
.shstrtab SHT_STRTAB 此节区包含节区名称
.strtab SHT_STRTAB 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含 SHF_ALLOC 位,否则该位为 0
.symtab SHT_SYMTAB 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含 SHF_ALLOC 位,否则该位置为 0
.text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令

readelf -S 查看节区头表


可重定位的对象文件(Relocatable file)


1
readelf -S add.o1

rel的elf节区头表
rel的elf节区头表

可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

可执行的对象文件(Executable file)


1
readelf -S testelf_dynamic1

rel的elf节区头表
rel的elf节区头表

可被共享的对象文件(Shared object file)


1
readelf -S libtestelf.so1

dyn的elf程序头表
dyn的elf程序头表

字符串表

首先要知道,字符串表它本身就是一个节区,从第二章描述中可知,每一个节区都存在一个节区头部表项与之对应,所以字符串表这个节区也存在一个节区头部表项对应,而在 elf 文件头部结构中存在一个成员 e_shstrndx 给出这个节区头部表项的索引位置。因此可以通过

1
shstrab  = (rt_uint8_t *)module_ptr +shdr[elf_module->e_shstrndx].sh_offset;1

来得到字符串表的起始位置。 字符串表节区包含以 NULL(ASCII 码 0)结尾的字符序列,通常称为字符串。ELF 目标文件通常使用字符串来表示符号和节区名称。对字符串的引用通常以字符串在字符 串表中的下标给出。

一般,第一个字节(索引为 0)定义为一个空字符串。类似的,字符串表的最后一个字节也定义为 NULL,以确保所有的字符串都以 NULL 结尾。索引为 0 的字符串在 不同的上下文中可以表示无名或者名字为 NULL 的字符串。

允许存在空的字符串表节区,其节区头部的 sh_size 成员应该为 0。对空的字符串表而言,非 0 的索引值是非法的。

例如:对于各个节区而言,节区头部的 sh_name 成员包含其对应的节区头部字符串表节区的索引,此节区由 ELF 头的 e_shstrndx 成员给出。下图给出了包含 25 个字节的一个字符串表,以及与不同索引相关的字符串。

字符
字符

那么上面字符串表包含以下字符串:

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

Symbol Table

首先,符号表同样本身是一节区,也存在一对应节区头部表项。

目标文件的符号表中包含用来定位、重定位程序中符号定义和引用的信息。

符号表索引是对此数组的索引。索引 0 表示表中的第一表项,同时也作为未定义符号的索引。

符号表是由一个个符号元素组成,用 elfxx_sym 来结构来表示, 定义在include/uapi/linux/elf.h, 同样 32 位为 elf32_sym, 64 位对应 elf64_sym

每个元素的数据结构如下定义:

成员 类型 描述
st_name Elf32_Word/Elf64_Word 名称,索引到字符串表
st_value Elf32_AddrElf64_Addr 给出相关联的符号的取值。依赖于具体的上下文
st_size Elf32_Word/Elf64_Word 相关的尺寸大小
st_info unsigned char 给出符号的类型和绑定属性
st_other unsigned char 该成员当前包含 0,其含义没有定义
st_shndx Elf32_Half/Elf64_Half 给出相关的节区头部表索引。某些索引具有特殊含义

st_info 给出符号的类型和绑定属性


st_info 中包含符号类型和绑定信息,操纵方式如:

1
2
3
#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf))123

st_info 的高四位(ELF32_ST_BIND(i))

表示符号绑定,用于确定链接可见性和行为。具体的绑定类型如:

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

全局符号与弱符号之间的区别主要有两点:

  1. 当链接编辑器组合若干可重定 位 的 目 标 文 件 时 , 不 允 许 对 同 名 的 STB_GLOBAL 符号给出多个定义。另一方面如果一个已定义的全局符号已经存在,出现一个同名的弱符号并不会产生错误。链接编辑器尽关心全局符号,忽略弱符号。类似地,如果一个公共符号(符号的 st_shndx 中包含 SHN_COMMON),那么具有相同名称的弱符号出现也不会导致错误。链接编辑器会采纳公共定义,而忽略弱定义。
  2. 当链接编辑器搜索归档库(archive libraries)时,会提取那些包含未定义全局符号的档案成员。成员的定义可以是全局符号,也可以是弱符号。连接编辑器不会提取档案成员来满足未定义的弱符号。未能解析的弱符号取值为 0。

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

st_info 的低四位 ELF32_ST_TYPE(i)

定义如下

名称 取值 说明
STT_NOTYPE 0 符号的类型没有指定
STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等
STT_FUNC 2 符号与某个函数或者其他可执行代码相关
STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定位,通常具有 STB_LOCAL 绑定
STT_FILE 4 传统上,符号的名称给出了与目标文件相关的源文件的名称。文件符号具有 STB_LOCAL 绑定,其节区索引是 SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号(如果有的话)
STT_LOPROC~STT_HIPROC 13~15 此范围的符号类型值保留给处理器专用语义用途

在共享目标文件中的函数符号(类型为 STT_FUNC)具有特别的重要性。当其他目标文件引用了来自某个共享目标中的函数时,链接编辑器自动为所引用的符号创建过 程链接表项。类型不是 STT_FUNC 的共享目标符号不会自动通过过程链接表进行引用。

如果一个符号的取值引用了某个节区中的特定位置,那么它的节区索引成员(st_shndx)包含了其在节区头部表中的索引。当节区在重定位过程中被移动时,符号的取值也会随之变化,对符号的引用始终会“指向”程序中的相同位置。

st_shndx

如前面所述,st_shndx 给出相关的节区头部表索引。但其值也存在一些特殊值,具有某些特殊的含义:

名称 取值 说明
SHN_ABS 符号具有绝对取值,不会因为重定位而发生变化
SHN_COMMON 符号标注了一个尚未分配的公共块。符号的取值给出了对齐约束,与节区的 sh_addralign 成员类似。就是说,链接编辑器将为符号分配存储空间,地址位于 st_value 的倍数处。符号的大小给出了所需要的字节数
SHN_UNDEF 此节区表索引值意味着符号没有定义。当链接编辑器将此目标文件与其他定义了该符号的目标文件进行组合时,此文件中对该符号的引用将被链接到实际定义的位置

st_value

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

  1. 在可重定位文件中,st_value 中遵从了节区索引为 SHN_COMMON 的符号的对齐约束。
  2. 在可重定位的文件中,st_value 中包含已定义符号的节区偏移。就是说,st_value 是从 st_shndx 所标识的节区头部开始计算,到符号位置的偏移。
  3. 在可执行和共享目标文件中,st_value 包含一个虚地址。为了使得这些文件的符号对动态链接器更有用,节区偏移(针对文 件的解释)让位于虚拟地址(针对内存的解释),因为这时与节区号无关。

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

nm 查看符号表

1
nm *.o1

rel的elf符号表
rel的elf符号表

可重定向文件, 是一个需要链接的对象, 程序头表对其而言不是必要的, 因此这类文件一般没有程序头表

可执行的对象文件(Executable file)


1
nm testelf_dynamic1

exec的elf符号表
exec的elf符号表

可被共享的对象文件(Shared object file)


1
nm libtestelf.so1

dyn的elf符号表
dyn的elf符号表

重定位信息


重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。

重定位表项


可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映像的正确信息。重定位表项就是这样一些数据。

可重定位表项的数据结构如下定义:

Elf32_Rel

成员 类型 描述
r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型

Elf32_Rela

成员 类型 描述
r_offset Elf32_Addr/Elf64_Addr 给出了重定位动作所适用的位置
r_info Elf32_Word/Elf64_Word 给出要进行重定位的符号表索引,以及将实施的重定位类型
r_addend Elf32_Word 给出一个常量补齐,用来计算将被填充到可重定位字段的数值

重定位节区会引用两个其它节区:符号表、要修改的节区。节区头部的 sh_info 和 sh_link 成员给出这些关系。不同目标文件的重定位表项对 r_offset 成员具有略微不同的解释。 r_info 通常分为高 8 位和低 8 位,分别表示不同的含义:

1
2
3
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))123

高 8 位用作要进行重定位的符号表索引,通过它可以得出一个符号表项,而低 8 位表示将实施的重定位类型,它是和处理器相关的。

ELF32_R_TYPE(i)


重定位表项描述如何修改后面的指令和数据字段。一般,共享目标文件在创建时,其基本虚拟地址是 0,不过执行地址将随着动态加载而发生变化。

参考