ELF 标准翻译

可移植格式规格说明,版本 1.1。
来自工具接口标准 (TIS)
注:已有 TIS 1.2版,但ELF文件格式这部分与1.1版本差别不大。

前言

ELF:可重定位/链接格式

ELF文件格式作为应用程序二进制接口(ABI),最初由Unix系统实验室(USL)编写并发布。工具接口标准委员会(TIS)选择改进后的ELF标准作为在32位Intel架构上的可移植目标文件格式,适用于多种操作系统。

ELF标准使软件开发变得简单,它为开发者提供了跨多平台的二进制接口定义。这将减少不同接口实现的数目,从而减少重新编码和重新编译的需要。

关于本文档

本文档的预期读者是为多种32位系统环境开发目标文件或可执行文件的开发人员。

文档分为以下几个部分:

  1. “目标文件”描述了3种主要的ELF目标文件格式[1]。
  2. “程序装载和动态链接”描述了在创建运行时程序时目标文件信息和操作系统的执行过程。
  3. “C库”列举了libsys(即标准C(ANSI C)和libc库实例)包含的符号,以及libc库实例需要的全局数据符号。

注:X86架构参考文档变为Intel架构。

1 目标文件

介绍

第一部分描述了ABI目标文件格式——ELF。它主要有三种目标文件类型:

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

在介绍性的材料之后,第一部分聚焦于文件格式以及它和构建程序的关系。第二部分也描述了部分目标文件,该部分聚焦于执行程序所需的必要信息。

文件格式

目标文件参与程序链接(构建程序)和程序执行(运行程序)的过程。考虑到方便性和效率,目标文件格式提供了并行的多种视角来描述文件内容,反映出了不同程序活动的需要。图1-1展示了一个目标文件的组织结构。

图1-1:目标文件格式

- 链接视角
	- ELF头
	- 程序头表(可选)
	- 第1节(section)
	- ...
	- 第n节
	- ...
	- ...
	- 节头表

- 执行视角
	- ELF头
	- 程序头表
	- 第1段(segment)
	- 第2段
	- ...
	- 节头表(可选)

ELF头存在于目标文件开头,相当于描述文件组织结构的“路线图”。在链接视角中,构成了目标文件信息主体:指令,数据,符号表和重定位信息等等。第一部分后面将对特殊节进行描述。第二部分讨论以及文件的程序执行视角。

程序头表(如果有)告诉系统怎样创造进程镜像。用于构建进程镜像(执行程序)的文件必须有一个程序头表;可重定位文件不需要有。

节头表包含了描述文件节的信息。每一个节在表中都占有一项;每一项都给出了如节名,节大小等信息。链接期间使用的文件必须有一个节头表;其他目标文件则可有可没有。

注:虽然图1-1中程序头表紧跟在ELF头后面,节头表跟在节后面,但实际文件可能有所差异。另外,节和段没有限定顺序。只有ELF头的位置是固定的。

数据表示

目标文件格式支持多种符合8比特一个字节和32位架构标准的处理器。然而,它可以扩展到更大(或更小)的架构上。因此,目标文件使用独立于机器的格式来表示一些控制数据,这允许以统一的方式识别目标文件并解释它们的内容。目标文件中的剩余数据使用目标处理器上的编码方式,与创建文件的机器无关。

图1-2:32位数据类型

名称 长度 对齐方式 用途
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不使用位域。

ELF文件头

有些目标文件控制结构可以增长,因为ELF头包括他们的实际大小。如果目标文件格式改变,程序可能会遇到比预期大或者小的控制结构。因此,程序可能忽略“额外”信息。对待“丢失”信息的方式取决于背景环境,也会在定义了扩展时被指定。

图1-3:ELF头 [2]

#define EI_NIDENT	16

typedef struct {
	unsigned char	e_ident[EI_NIDENT];
	ELF32_Half		e_type;
	ELF32_Half		e_machine;
	ELF32_Word		e_version;
	ELF32_Addr		e_entry;
	ELF32_Off		e_phoff;
	ELF32_Off		e_shoff;
	ELF32_Word		e_flags;
	ELF32_Half		e_ehsize;
	ELF32_Half		e_phentsize;
	ELF32_Half		e_phnum;
	ELF32_Half		e_shentsize;
	ELF32_Half		e_shnum;
	ELF32_Half		e_shstrndx;
} Elf32_Ehdr;

e_ident

开始的字节标志这个文件是一个目标文件,并提供用于解码和解释文件内容的机器无关的数据。完整的描述在后面“ELF标识符”一节。

e_type

这一项标识目标文件类型。

名称 意义
ET_NONE 0 无文件类型
ET_REL 1 可重定位文件
ET_EXEC 2 可执行文件
ET_DYN 3 共享目标文件
ET_CORE 4 核心转储文件
ET_LOPROC 0xff00 处理器指定
ET_HIPROC 0xffff 处理器指定

虽然核心转储文件的内容没有限定,但ET_CORE还是被保留用于标志此类文件。从ET_LOPROCET_HIPROC(包括边界)值被保留用于处理器指定场景。其他值被保留,在未来必要时可能被赋予新的目标文件类型。

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

其他值被保留并在未来必要时用于赋予新的机器。 特定处理器的ELF名称使用机器名称来区分。例如,下面的标志(flag)使用前缀EF_;在EM_XYZ机器上名叫WIDGET的标志将被叫作EF_XYZ_WIDGET

e_version

这一项指定目标文件的版本。

名称 意义
EV_NONE 0 无效版本
EV_CURRENT 1 当前版本

值1表示初始文件格式;未来扩展(extensions)将用更大的数字创建新的版本。虽然在上面值EV_CURRENT为1,但是为了反映当前版本号,它可能会改变。

e_entry

这一项给出了系统开始进程时要把控制权转交到的虚拟地址。如果文件没有相关的入口项,则这一项为0。

e_phoff

这一项给出了程序头表在文件中的字节偏移。如果文件中没有程序头表,则本项值为0。

e_shoff

这一项给出了节头表在文件中的字节偏移。如果文件中没有节头表,则本项值为0。

e_flags

这一项给出了文件中特定处理器相关的标志。标志命名方式为EF_machine_flag。有关标志定义的内容,请参考“机器信息”部分。

e_ehsize

这一项给出了ELF头的字节长度。

e_phentsize

这一项给出了程序头表中一个项所占的字节长度。程序头表中所有项长度相同。

e_phnum

这一项给出了程序头表中的项数。因此,本项(e_phnum)与e_phentsize项的乘积即为程序头表的字节长度。如果文件中没有程序头表,则本项值为0。

e_shentsize

这一项给出了节头的字节长度。一个节头就是节头表中的一项;节头表中所有项长度相同。

e_shnum

这一项给出了节头表中的项数。因此,本项(e_shnum)与e_shentsize项的乘积即为节头表的字节长度。如果文件中没有节头表,则本项值为0.

e_shstrndx

这一项给出了节头表在节名字符串表中的索引值。如果文件中没有节名字符串表,则本项值为SHN_UNDEF。更多相关信息,请参考后面的“节”和“字符串表”部分。

ELF标识符

如上所述,ELF提供了一个目标文件框架来支持多种处理器,多种编码格式以及多种类型的机器。为了支持各种目标文件,文件的初始字节指定了解释文件的方式,处理器无关特性和文件剩余内容的独立性。

ELF头(目标文件)的初始字节指的是e_ident项。

图1-4:e_ident[]标识符索引表

名称 用途
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[]长度

这些索引指向保存相关值的字节处。

EI_MAG0 ~ EI_MAG3

文件的头4个字节,被称作“魔数”,标识该文件是一个ELF目标文件。

名称 位置
ELFMAG0 0x7f e_ident[EI_MAG0]
ELFMAG1 ‘E’ e_ident[EI_MAG1]
ELFMAG2 ‘L’ e_ident[EI_MAG2]
ELFMAG3 ‘F’ e_ident[EI_MAG3]

EI_CLASS

e_ident[EI_MAG3]的下一个字节,e_ident[EI_CLASS],标识文件的类型或容量。

名称 意义
ELFCLASSNONE 0 无效类型
ELFCLASS32 1 32位文件
ELFCLASS64 2 64位文件

文件格式被设计成能够在多种字节长度的机器之间移植,而不需要强制规定机器的最长字节长度和最短字节长度。ELFCLASS32类型支持文件大小和虚拟地址空间上限为4GB的机器;它是上述定义中的基本类型。

ELFCLASS64类型被保留用于64位架构。它出现在这里表明目标文件可能会改变,但是64位格式目前还没有限定。其他类型在未来必要时会被定义,并附带有不同的基本类型和目标文件数据大小。

EI_DATA

e_ident[EI_DATA]字节给出了特定处理器数据在目标文件中的编码方式。下面是目前已定义的编码:

名称 意义
ELFDATANONE 0 无效数据编码
ELFDATA2LSB 1 参考下文
ELFDATA2MSB 2 参考下文

更多关于编码的信息在后面给出。其他值被保留,在未来必要时赋予新的编码。

EI_VERSION

e_ident[EI_DATA]字节给出了ELF头的版本号。目前来说,这个值必须是EV_CURRENT,即之前已经给出的e_version项。

EI_PAD

这个值标识了e_ident中未使用字节的开始。这些字节被保留并置为0;处理目标文件的程序应该忽略它们。如果目前未使用的字节被赋予新的意义,EI_PAD的值在未来可能会改变。

文件数据编码方式限定了对于文件中基本量的解析方式。如之前所述,ELFCLASS32类型文件使用占据1,2和4字节的量。在已定义的编码方式下,量的表示如下图1-5/1-6。字节号在左上角。

ELFDATA2LSB编码限定补码值最低有效位占用最低地址。

图1-5:数据编码 ELFDATA2LSB

elf-1-5

ELFDATA2MSB编码限定补码值最高有效位占用最低地址。

图1-6:数据编码 ELFDATA2LSB

elf-1-6

机器信息

对于e_ident中的文件标识符,32位Intel架构要求下面的值。

图1-7

位置
e_ident[EI_CLASS] ELFCLASS32
e_ident[EI_DATA] ELFDATA2LSB

处理器标识位于ELF头的e_machine项,并且值必须是EM_386

ELF头的e_flags项保存有文件相关的比特位标志。32位Intel架构没有定义任何标志;所以这一项为0。

节(Sections)

目标文件的节头表帮助定位文件中的所有节。节头表是一个ELF32_Shdr结构体类型的数组,在后面给出。节头表索引是引用数组中元素的下标。ELF头中的e_shoff项给出了从文件开头到节头表位置的字节偏移;e_shnum给出了节头表包含的项数;e_shentsize给出了每一项的字节长度。

图1-8:特殊节索引

名称
SHN_UNDEF 0
SHN_LORESERVE 0xff00
SHN_LOPROC 0xff00
SHN_HIPROC 0xff1f
SHN_ABS 0xfff1
SHN_COMMON 0xfff2
SHN_HIRESERVE 0xffff

SHN_UNDEF

这个值标志未定义的,丢失的,不相关的或者其他没有意义的节引用。例如,与节号SHN_UNDEF相关的符号“defined”就是一个未定义符号。

注:虽然0号索引被保留用于未定义值,节头表也包含索引0的项。也就是说,如果ELF头的e_shnum项表明某文件的节头表中有6个项,那么索引应该为0~5。初始项的内容在本节后面会提到。

SHN_LORESERVE

这个值指定了保留索引值范围的下界。

SHN_LOPROCSHN_HIPROC

在这个闭区间范围内的值被保留用于特定处理器语义。

SHN_ABS

这个值指定了相关引用的绝对值。例如,与节号SHN_ABS关联的定义符号拥有绝对值,不受重定位的影响。

SHN_COMMON

与这一节相关的定义符号是通用符号,例如FORTRAN COMMON或者C语言中未分配的外部变量。

SHN_HIRESERVE

这个值指定了保留索引值范围的上界。系统保留在SHN_LORESERVESHN_HIRESERVE之间(包含边界)的索引值;这些值不在节头表中引用。也就是说,节头表不包含保留索引项。

一个节头是一个结构体。

图1-9:节头[3]

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;

sh_name

这一项给出了节名。它的值是“节头字符串表”节中的索引[参照后面“字符串表”部分],指向一个带有尾零的字符串位置。

sh_type

这一项给出了节的分类。分类依据是节的内容和语义。后文将详解节类型和对它们的描述。

sh_flags

节带有每个长为1比特的标志来表示各种属性。标志定义见后文。

sh_addr

如果某节在进程的内存镜像中会出现,则这一项给出了该节第一个字节应在的地址。否则,该项为0.

sh_offset

这一项给出了从文件开头到该节首字节的字节偏移量。对于下文将要提到的SHT_NOBITS节类型,属于这个类型的节在文件中不占用空间,它的sh_offset属性指出了该节在文件总体布局中的位置。

sh_size

这一项给出了节的字节长度。除非节类型是SHT_NOBITS,否则该节在文件中占用的长度就是sh_size。属于SHT_NOBITS的节长度可能不为零,但是它在文件中不占空间。[4]

sh_link

这一项给出了节头表索引链接,它的解释取决于节类型。图1-13描述了这些值。

sh_info

这一项给出了额外信息,它的解释取决于节类型。图1-13描述了这些值。

sh_addralign

某些节有地址对齐的限制。例如,如果某节中有一个双字,系统必须保证整个节的双字对齐。也就是说,sh_addr的值模sh_addralign必须等于0。目前,只有0和2的正整数次幂是允许的。值0和1表示节没有对齐要求。

sh_entsize

有些节带有具有固定长度项的表,如符号表。对于这样的节,sh_entsize给出了表中项的字节长度。如果节中不存在如前所述的表,则本项值为0。

节头的sh_type项指定了节的含义。

图1-10:节类型sh_type

名称
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
SHT_REL 9
SHT_SHLIB 10
SHT_DYNSYM 11
SHT_LOPROC 0x70000000
SHT_HIPROC 0x7fffffff
SHT_LOUSER 0x80000000
SHT_HIUSER 0xffffffff

SHT_NULL

节头闲置,没有相对应的节,节头其他项的值是未定义的。

SHT_PROGBITS

该节具有程序定义的信息。这些信息的格式和意义均由该程序决定。

SHT_SYMTABSHT_DYNSYM

这些节有符号表。目前,一个目标文件中属于每个类型的节只有一个,但是在未来这一限制可能会消除。具有代表性的是SHT_SYMTAB类型的节为链接编辑提供了符号,它也可能被用于动态链接。作为一个完整的符号表,它可能包含许多对于动态链接来说不必要的符号。因此,目标文件中可能也会包含SHT_DYNSYM节,为了节省空间,该节中保存了最小的用于动态链接的符号集合。详细内容,参照“符号表”部分。

SHT_STRTAB

这种节包含字符串表。一个目标文件可能有多个字符串表节。详细内容,请参照“字符串表”部分。

SHT_RELA

这种节给出了带有明确加数的重定位项,例如32位类型的目标文件对应的ELF32_Rela类型。一个目标文件可能有多个重定位节。详细内容,参照“重定位”部分。

SHT_HASH

这种节给出了符号哈希表。所有参与动态链接的节必须包含一个符号哈希表。目前,一个目标文件中可能只有一个哈希表,但是这一限制在未来可能消除。详细内容,参照第二部分的“哈希表”。

SHT_DYNAMIC

这一节给出了动态链接的信息。目前,一个目标文件可能只有一个动态节,但是这一限制在未来可能消除。详细内容,参照第二部分的“动态节”。

SHT_NOTE

某种程度上来说,这一节给出了标记文件的信息。详细内容,参照第二部分的“记录节”。

SHT_NOBITS

这种节在文件中不占用空间,但是很像SHT_PROGBITS类型节。虽然这种节不包含字节,但是sh_offset量指出了该节在文件总体布局中的位置。

SHT_REL

这种节给出了不带有明确加数的重定位项,例如32位类型的目标文件对应的ELF32_Rel类型。一个目标文件可能有多个重定位节。详细内容,参照“重定位”部分。

SHT_SHLIB

这种节被保留,但是没有特定含义。带有这种节的程序不符合ABI定义。

SHT_LOPROCSHT_HIPROC

闭区间范围上的值被保留用于特定处理器语义。

SHT_LOUSER

指定了应用程序可使用的索引下界。

SHT_HIUSER

指定了应用程序可使用的索引上界。在SHT_LOUSERSHT_HIUSER之间的节类型可以被应用程序使用,不会与目前或者未来的系统定义节类型冲突。

其他节类型值被保留。如上所述,索引为0(SHN_UNDEF)的节头也存在,虽然这个索引标记的是未定义节引用。这一项的信息如图1-11.

图1-11:节头表项:索引0

名称 记录
sh_name 0 无名称
sh_type SHT_NULL 闲置
sh_flags 0 无标志
sh_addr 0 无地址
sh_offset 0 无文件偏移
sh_size 0 无长度
sh_link SHN_UNDEF 无链接信息
sh_info 0 无辅助信息
sh_addralign 0 无对齐
sh_entsize 0 无项

节头中sh_flags项包含每个长为1比特位的标志来描述节属性。已定义值如下,其他值保留。

图1-12:节属性标志,sh_flags

名称
SHF_WRITE 0x1
SHF_ALLOC 0x2
SHF_EXECINSTR 0x4
SHF_MASKPROC 0xf0000000

如果sh_flags中某个标志位被置1,则节具有该属性,否则不具有或没有应用。未定义属性被置0。

SHF_WRITE

这种节包含进程运行时应该被写入的数据。

SHF_ALLOC

这种节在进程运行时占用内存。有些控制节不占用目标文件的内存镜像空间,对于这样的节本属性处于关闭状态(off)。

SHF_EXECINSTR

这样的节包含可执行的机器指令。

SHF_MASKPROC

这一掩码中的所有比特用于特定处理器语义。

sh_linksh_info两项具有特殊信息,这取决于节类型。

图1-13:sh_linksh_info解释

sh_type sh_link sh_info
SHT_DYNAMIC 节中项用到的字符串表的节头索引 0
SHT_HASH 哈希表应用到的符号表的节头索引 0
SHT_REL/SHT_RELA 相关符号表的节头索引 重定位应用到的节的节头索引
SHT_SYMTAB/SHT_DYNSYM 相关字符串表的节头索引 比最后一个局部符号(与STB_LOCAL绑定)的符号表索引大1
other SHN_UNDEF 0

特殊节

许多节包含有程序和控制信息。下列节被系统使用,具有下述对应类型和属性。

图1-14:特殊节

名称 类型 属性
.bss SHT_NOBITS SHF_ALLOC+SHF_WRITE
.comment SHT_PROGBITS
.data SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.data1 SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.debug SHT_PROGBITS
.dynamic SHT_DYNAMIC 参照下文
.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
.interp SHT_PROGBITS see below
.line SHT_PROGBITS
.note SHT_NOTE
.plt SHT_PROGBITS 参照下文
.relname SHT_REL 参照下文
.relaname SHT_RELA 参照下文
.rodata SHT_PROGBITS SHF_ALLOC
.rodata1 SHT_PROGBITS SHF_ALLOC
.shstrtab SHT_STRTAB
.strtab SHT_STRTAB 参照下文
.symtab SHT_SYMTAB 参照下文
.text SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR

.bss

包含占用程序内存镜像空间的未定义数据。程序开始运行时,系统把这些数据初始化为0。但是本节不占用文件空间,正如SHT_NOBITS类型所述。

.comment

包含了版本控制信息。

.data.data1

包含了占用程序内存镜像空间的已定义数据。

.debug

包含符号调试信息。这些内容是未指定的。

.dynamic

包含动态链接信息。本节属性包括SHF_ALLOC比特位。而是否有SHF_WRITE属性则依赖于处理器。更多信息,参照第二部分。

.dynstr

包含动态链接所需的字符串,这些字符串大多是与符号表项相关的名字。更多信息,参照第二部分。

.dynsym

包含动态链接符号表,正如“符号表”部分所述。更多信息,参照第二部分。

.fini

包含进程结束需要的可执行指令。也就是说,当一个程序正常结束时,系统会安排执行这一节的代码。

.got

包含全局偏移表。更多信息,参照第一部分“特殊节”部分和第二部分“全局偏移表”部分。

.hash

包含符号哈希表。更多信息,参照第二部分“哈希表”部分。

.init

包含进程初始化时需要的可执行指令。也就是说,如果一个程序开始运行,系统会在调用主程序入口点(对于C语言程序来说是main)安排运行这一节的代码。

.interp

包含程序解释器的路径名。如果一个文件具有包含此节的载入段,则此节的属性将会包括SHF_ALLOC标志位;否则,不具备该属性。更多信息,参照第二部分。

.line

包含符号调试的行号信息,描述了源程序和机器指令之间的一致性。内容未限定。

.note

包含格式遵循第二部分“记录节”部分所描述那样的信息。

.plt

包含过程链接表。更多信息,参照第一部分“特殊节”部分和第二部分“过程链接表”部分。

.relname.relaname

包含重定位信息,如“重定位”部分所述。如果文件具有包含重定位的载入段,则此节的属性将会包括SHF_ALLOC标志位;否则,不具备该属性。按照惯例,name由重定位应用到的节来提供。因此,.text的重定位节通常有名字.rel.text或者.rela.text

.rodata.rodata1

包含属于程序内存镜像中不可写入段的只读数据。更多信息,参照第二部分“程序头”部分。

.shstrtab

包含节名。

.strtab

包含字符串。这些字符串大多代表与符号表项相关的名字。如果文件具有包含符号字符串表的载入段,则此节的属性将会包括SHF_ALLOC标志位;否则,不具备该属性。

.symtab

包含符号表,如“符号表”部分所述。如果文件具有包含符号表的载入段,则此节的属性将会包括SHF_ALLOC标志位;否则,不具备该属性。

.text

包含一个程序的“文本”,或可执行指令。

虽然应用程序使用带有点(.)前缀的节名是可以的,如果它们的意义符合要求,但这样的节名是系统保留名。程序最好使用不带前缀的名字,避免和系统节发生冲突。目标文件格式允许自定义不在上述列表中的节。一个目标文件允许多个节拥有相同节名。

处理器架构保留的节名的命名形式是在节名前加处理器名缩写。这个名字应该是从e_machine处得来。例如,.FOO.psect是由FOO架构定义的psect节。目前的扩展名按照它们历史名来叫。

业已存在的扩展名
.sdata
.sbss
.lit8
.gptab
.conflict
.tdesc
.lit4
.reginfo
.liblist

字符串表

字符串表节包含带尾零的字符序列(即一般所说的字符串)。目标文件使用这些字符串来表示符号和节名。通过字符串表节中的索引来引用字符串。第一个字节,索引0,被定义为只含一个尾零。同样地,字符串表最后一个字节也被定义为只含一个尾零。一个索引为0的字符串要么无名,要么是空名(null name),这取决于上下文。字符串表为空是被允许的;它的节头中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            

图1-15:字符串表索引

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

正如上例所示,字符串表索引可以引用该节中的任何字节。一个字符串可以出现多次;对子串的引用也存在;一个字符串可能被引用多次。未被引用的字符串也允许存在。

符号表

目标文件的符号表包含了程序中对符号定义和引用进行定位和重定位所需的信息。符号表索引是数组下标。索引0既指出了表中第一项,也作为未定义符号的索引。初始项的内容在本节后面介绍。

名称
STN_UNDEF 0

一个符号表项是下面的格式。

图1-16:符号表项 [5]

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

这一项指定了符号类型和具有的属性。其值和意义列于下文。下面的代码展示了如何控制它的值。

#define ELF32_ST_BIND(i)	((i)>>4)
#define ELF32_ST_TYPE(i)	((i)&0xf)
#define ELF32_ST_BIND(i)	(((b)<<4)+((t)&0xf))

st_other

这一项目前是0,无意义。

st_shndx

每一个符号项都在相关的节中被定义;这一项包含相关节头表的索引。如图1-7和相关文字所述,有些节索引具有特殊的意义。

符号绑定决定了链接过程的可见性和行为。

图1-17:符号绑定,ELF32_ST_BIND

名称
STB_LOCAL 0
STB_GLOBAL 1
STB_WEAK 2
STB_LOPROC 13
STB_HIPROC 15

STB_LOCAL

局部符号在包含它们的定义的目标文件之外是不可见的。同名局部符号可能存在于多个文件中,但是互相之间不干扰。

STB_GLOBAL

全局符号对于所有被结合的目标文件可见。一个文件对于一个全局符号的定义会与令一个文件中对于同一符号的未定义引用保持一致。

STB_WEAK

弱符号与全局符号相像,但是他们的定义具有低优先级。

STB_LOPROCSTB_HIPROC

在闭区间内的值保留用于特定处理器环境。

全局符号与弱符号主要在两方面区别:

在每一个符号表中,所有与STB_LOCAL绑定的符号出现在弱符号和全局符号的前面。如之前的“节”部分所述,一个符号表节的sh_info项所对应的节头项包含了符号表中第一个非局部符号的索引。

符号类型给出了相关项的一般分类。

图1-18:符号类型,ELF32_ST_TYPE

名称
STT_NOTYPE 0
STT_OBJECT 1
STT_FUNC 2
STT_SECTION 3
STT_FILE 4
STT_LOPROC 13
STT_HIPROC 15

STT_NOTYPE

该符号类型未指定。

STT_OBJECT

该类型符号与数据量相关,例如变量,数组等。

STT_FUNC

该类型符号与函数或者其他可执行代码有关。

STT_SECTION

该类型符号与节有关。该类型符号在符号表中的项主要用于重定位,通常有STB_LOCAL绑定。

STT_FILE

按照惯例,该符号名给出了产生目标文件的源文件名。如果文件符号存在,则它有STB_LOCAL绑定,节索引是SHN_ABS,且优先级比其他STB_LOCAL符号高。

STT_LOPROCSTT_HIPROC

闭区间内的值保留用于特定处理器。

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

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

SHN_ABS

该类型符号具有绝对的值,不会因为重定位而改变。

SHN_COMMON

该符号标注一个尚未被分配的一般块。符号值给出了字节对齐限制,类似于节的sh_addralign项。也就是说,链接器将为该符号在地址为st_value倍数的地方分配空间。符号长度指出了需要的字节数。

SHN_UNDEF

这个节表索引表示符号未定义。当链接器合并这个目标文件和另一个定义了上述符号的目标文件时,文件中对于该符号的引用将会链接到那个实际定义处。

如上所述,符号表项中索引0的项STN_UNDEF被保留;它具有下列值。

图1-19:符号表项:索引0

名称 注解
st_name 0 无名
st_value 0 值0
st_size 0 无长度
st_info 0 无类型,局部绑定
st_other 0  
st_shndx SHN_UNDEF 无节

符号值

不同类型目标文件的符号表项对于st_value项的解释有细小差异。

虽然符号表值在不同目标文件中有相似意义,但是允许合适的程序去更有效率地获取数据.

重定位

重定位是把符号引用和符号定义连接起来的过程。例如,当程序调用函数时,相关的调用指令必须把控制权转到恰当的执行地址。另一方面,可重定位文件必须包含描述怎样修改它们节内容的信息,只有这样可执行文件和共享目标文件才能够在创建进程镜像时掌握正确信息。重定位项是下面这些数据。

图1-20:重定位项

typedef struct {
    Elf32_Addr        r_offset;
    Elf32_Word       r_info;
} Elf32_Rel;

typedef struct {
    Elf32_Addr     r_offset;
    Elf32_Word    r_info;
    Elf32_Sword    r_addend;
} Elf32_Rela;

r_offset

包含重定位操作实施的位置。对于可重定位文件来说,这个值是从该节开始到受到重定位影响的存储单元的字节偏移。对于可执行文件或者共享目标文件来说,这个值是受到重定位影响的存储单元的虚拟地址。

r_info

包含重定位必须实施到的符号表索引和实施的重定位类型。例如,一个调用指令的重定位项将包含它所调用的函数的符号表索引。如果索引是STN_UNDEF,即未定义的符号表索引,则重定位用0当做“符号值”。重定位类型依赖于特定处理器。当文章中涉及到重定位项的重定位类型或者符号表索引时,它指的是将ELF32_R_TYPE或者ELF32_R_SYM应用到r_info项的结果。

#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))

r_addend

指定了计算存储到可重定位域的值时用到的常量加数。

如上所示,只有ELF32_Rela项包含一个准确加数。Elf32_Rel类型项在待修改位置存储了不准确的加数。由于处理器架构的不同,两种格式都可能需要,或者为了方便需要更多。因此,在某个特定机器的实现可能使用另一个格式另外的格式或者两个格式之一,这取决于上下文。

一个重定位节了引用另外两个节:一个符号表,一个待修改的节。如“节”部分所属,节头的sh_infosh_link项指定了这些关系。不同目标文件的重定位项对于r_offset项的解释可能有细微差别。

虽然为了相关程序的方便,r_offset的解释对于不同目标文件来说有差异,但是重定位类型的意义则相同。

重定位类型

重定位项描述了怎样修改相关的指令和数据域(比特数在图中下角落内)。

图1-21:可重定位域

elf-1-21

word32

它指定了一个32比特域,占用4个字节,对齐方式任意。这些值使用32位Intel架构上其他字变量的值相同的比特顺序。

elf-1-21-1

下面的计算假设这些动作是把一个可重定位文件转换为可执行文件或者共享目标文件。从概念上讲,链接器把多个可重定位文件混合来得到输出文件。它首先要决定怎样结合并放置这些输入文件,然后更新符号表的值,最后重定位。应用于可执行文件或者共享 目标文件的重定位是相似的,并且会达到相同的结果。后面的描述采用如下记号。

A

用于计算重定位域的值的加数。

B

在执行时一个共享目标被装载进内存的基址。一般来说,共享目标文件的虚拟基地址为0,但是执行地址却不同。

G

在执行时重定位项的符号在全局偏移表中的偏移。更多信息,参照第二部分“全局偏移表”。

GOT

全局偏移表的地址。更多信息,参照第二部分“全局偏移表”。

L

符号的过程链接表项的位置(节偏移或者地址)。过程链接表项将函数调用重定向到合适地址。链接器创建初始的过程链接表,在程序执行过程中,动态链接器修改过程链接表项。更多信息,参照第二部分“过程链接表”。

P

被重定位(通过r_offset计算)的存储单元的位置(节偏移或地址)。

S

索引在重定位项中的符号的值。

重定位项的r_offset值指出了第一个受影响的存储单元的偏移量或者虚拟地址。重定位类型指定了要改变哪些比特位以及如何计算它们的值。SYSTEM V架构仅仅使用Elf32_Rel重定位项,加数保留在将被重定位的域中。在所有情况下,加数和计算所得结果使用统一比特序。

图1-22:重定位类型

名称 计算
R_386_NONE 0
R_386_32 1 word32 S + A
R_386_PC32 1 word32 S + A - P
R_386_GOT32 1 word32 G + A - P
R_386_PLT32 1 word32 L + A - P
R_386_COPY 5
R_386_GLOB_DAT 6 word32 S
R_386_JMP_SLOT 7 word32 S
R_386_RELATIVE 8 word32 B + A
R_386_GOTOFF 9 word32 S + A - GOT
R_386_GOTPC 10 word32 S + A - P

一些重定位类型除了简单计算外还有以下语义。

R_386_GOT32

该重定位类型计算了从全局偏移表基址到符号的全局偏移表项的距离。另外,它还命令链接器创建一个全局偏移表。

R_386_PLT32

该重定位类型计算了符号的过程连接表项地址。另外,它还命令链接器创建一个过程链接表。

R_386_COPY

该重定位类型由链接器为动态链接过程创建。它的偏移项指向可写段中的位置。该符号表索引指定了一个既应存在于当前目标文件又该存在于一个共享目标文件的符号。在执行过程中,动态链接器将与该共享目标符号相关的数据复制到由上述偏移量指定的位置。

R_386_GLOB_DAT

该重定位类型用于设置一个指向特定符号地址的全局偏移表项。这个特殊的重定位类型允许确定符号和全局偏移表项之间的联系。

R_386_JMP_SLOT

该重定位类型由链接器为动态链接过程创建。它的偏移项给出了一个过程链接表项的位置。动态链接器修改过程链接表,从而把程序控制权转移到上述指出的符号地址。[参照第二部分“过程链接表”]

R_386_RELATIVE

该重定位类型由链接器为动态链接过程创建。它的偏移项给出了共享目标中的一个包含了某个代表相对地址的值的位置。动态链接器通过把共享目标装载到的虚拟地址与上述相对地址相加来计算对应虚拟地址。这种类型的重定位项必须在符号表索引中指定0。

R_386_GOTOFF

该重定位类型计算了符号值与全局偏移表地址之间的差。另外,它还命令链接器创建一个全局偏移表。

R_386_GOTPC

该重定位类型与R_386_PC32相像,不过它在计算过程中使用的是全局偏移表的地址。正常情况下在该重定位中被引用的符号是_GLOBAL_OFFSET_TABLE_,它也会命令链接器创建一个全局偏移表。

2 程序装载与动态链接

介绍

第二部分讲述了目标文件信息和创建运行程序时的系统行为。有些内容适用于所有系统;有些则依赖于处理器。

在静态时,可执行文件和和共享目标文件代表程序。为了执行这样的程序,系统使用这些文件来创建动态程序,或者进程镜像。进程镜像有包含代码,数据,栈和其他东西的段。本部分主要讲述下面内容。

注:特定处理器范围的ELF常量具有命名惯例。如DT_PT_之类用于特定处理器扩展,包含有处理器的名称:例如DT_M32_SPECIAL。已存在的不使用这个惯例的处理器扩展也将被支持。

已存在扩展
DT_JMP_REL

程序头

可执行文件或共享目标文件的程序头表是一个结构体数组,每一项描述了一个段或者其他系统准备执行程序时需要的信息。一个目标文件的包含一个或多个,如后文“段内容”描述的那样。程序头只对可执行文件和共享目标文件有意义。一个文件的ELF头中的e_phentsizee_phnum项指定了它的程序头大小。【参照第一部分“ELF头”】

图2-1:程序头

typedef struct {
	ELF32_Word	p_type;
	ELF32_Off	p_offset;
	ELF32_Addr	p_vaddr;
	ELF32_Addr	p_paddr;
	ELF32_Word	p_filesz;
	ELF32_Word	p_memsz;
	ELF32_Word	p_flags;
	ELF32_Word	p_align;
} Elf32_Phdr;

p_type

这一项指出该项数组元素描述的段种类或者如何解释该数组元素的信息。类型值和意义见后文。

p_offset

这一项给出了从文件开头到该段第一个字节的偏移量。

p_vaddr

这一项给出了在内存中该段第一个字节所在的虚拟地址。

p_paddr

在与物理寻址有关的系统上,该项保留用于段的物理地址。由于”System V”忽略了应用程序的物理寻址,可执行文件和共享目标文件的该项内容未限定。

p_filesz

这一项给出了文件镜像中的该段字节数;可以为零。

p_memsz

这一项给出了内存镜像中的该段字节数;可以为零。

p_flags

这一项给出了与段相关的标志位。已定义标志值见后文。

p_align

如本部分后文“程序装载”所述,可装载进程段必须有一致的p_vaddrp_offset值(模页大小后相等)。这一项给出了该段在内存中和在文件中应该如何对齐。值为0和1意味着无对齐要求。否则,p_align应该为一个正的2的幂次值,p_vaddrp_offsetp_alig同余。

有些项描述了进程段;有些给出了补充信息,并不作用于进程镜像。除了后文给出的顺序外,段项还可能以任意顺序出现。已定义的类型值如下;其他值保留用于未来需要。

图2-2:段类型:p_type

名称
PT_NULL 0
PT_LOAD 1
PT_DYNAMIC 2
PT_INTERP 3
PT_NOTE 4
PT_SHLIB 5
PT_PHDR 6
PT_LOPROC 0x70000000
PT_HIPROC 0x7fffffff

PT_NULL

该数组元素不使用;其他项的值未定义。该类型允许程序头表有可忽略的项。

PT_LOAD

该数组元素指定了一个可装载段,其由p_fileszp_memsz描述。文件中的字节被映射到内存段的起始位置。如果段的内存长度(p_memsz)大于文件长度(p_filesz),则按照定义,“额外”的字节值为0,跟在段的已初始化数据后面。文件长度不会超过内存长度。在程序头表中,可装载段项按照p_vaddr升序排列。

PT_DYNAMIC

该数组元素指定动态链接信息。更多信息,参照“动态链接”节。

PT_INTERP

该数组元素指定了一个以尾零结束的路径名的位置和长度,以此来调用解释器。该段类型只对可执行文件有意义(不过它也会在共享目标文件中出现);它不会在同一个文件中出现多次。如果它存在,则它在所有可装载段项之前。更多信息,参照“程序解释器”部分。

PT_NOTE

该数组元素指定了附加信息的位置和长度。更多信息,参照“记录节”。

PT_SHLIB

该段类型保留,但没有特定语义。包含该类型数组元素的程序不符合ABI标准。

PT_PHDR

如果存在的话,该数组元素指定了程序头表本身的位置和长度,无论是在内存镜像中还是在文件中。它不会在同一个文件中出现多次。另外,只要程序头表是该程序内存镜像中的一部分,它就会出现。如果它存在,则它在所也有可装载段项之前。更多信息,参照“程序解释器”。

PT_LOPROCPT_HIPROC

在该闭区间上的值保留用于特定处理器环境。

注:除非有特别需求,否则所有程序头段类型都是可选的。也就是说,一个文件的程序头表可能只包含和它的内容有关的元素。

基址

可执行文件和共享目标文件有基址,它是与该程序内存镜像相联系的最低虚拟地址。基址的一个作用是在动态链接过程中进行重定位。

可执行文件或共享目标文件的基址在执行时使用三个值来计算:内存装载地址,最大页长度,程序可装载段的最低虚拟地址。正如本章中“程序装载”部分所述,程序头中中的虚拟地址不一定就是程序内存镜像的实际虚拟地址。为了计算基址,首先要确定与p_vaddr值最小的PT_LOAD段相关的内存地址。之后通过把内存地址缩小到最近的最大页长度整数倍处。内存地址可能与p_vaddr一样,也可能不一样,这取决于装载进内存的文件类型。

如第一部分的“节”所述,.bss节的类型是SHT_NOBITS。虽然它不占用文件空间,但是它占用段内存镜像空间。正常情况下,这些未定义数据在段的末尾,因此使得在相应的程序头项中p_memsz的值大于p_filesz的值。

记录节(Note Section)

有时供应商或者系统制造者需要用特殊信息标记一个目标文件,使其他程序能够检验一致性和兼容性等等。SHT_NOTE类型节和PT_NOTE类型的程序头元素就是用于此。节和程序头元素中的记录信息包含若干项,每一项都是一个以目标处理器格式组织的4字节字型数组。下面的标签帮助解释记录信息的组织结构,但它们不是规格的一部分。

图2-3:记录信息

labels
namesz
descsz
type
name . . .
desc . . .

nameszname

name中的第一个namesz长度包含一个以尾零结尾的字符串,代表项的拥有者或者发起者。没有避免名称冲突的正式机制。按照惯例,供应商使用他们自己的名字,例如“XYZ 计算机公司”,作为标识符。如果没有名字,namesz为0。填充块是可选的,它是为了在必要时满足描述符的4字节对齐需求。填充位不算在namesz中。

descszdesc

desc中的第一个descsz长度包含记录描述符。ABI对于描述符的内容没有限制。如果没有描述符,则descsz为0。填充块是可选的,它是为了在必要时满足下一个记录项的4字节对齐需求。填充位不算在descsz中。

type

这个字给出了描述符的解释。每一个发起者控制它自己的类型;对于一个类型值的多种解释可能存在。因此,一个程序为了理解描述符,必须同时认出名称和类型。目前,类型必须是非负数。ABI没有定义描述符的意义。

为了说明,下面的记录段包含两项。

图2-4:记录段例子

elf-2-4

注:

程序装载

系统在创建或者扩大一个进程镜像时,正常来说,它把每个文件段拷贝到一个虚拟内存段。有时系统对文件的读取依赖于程序的执行行为,例如系统装载。[6]除非一个进程在执行时引用了逻辑页,否则它不需要物理页,进程通常会使许多页处于未引用状态。因此为了增强系统性能,延迟的物理读取常常避免它们。为了在实际中获得效率,可执行文件和共享目标文件必须有文件偏移和虚拟地址模页长度同余的段镜像。

SYSTEM V架构中段的虚拟地址和文件偏移模4 KB(0x1000)或这更大的2的幂次同余。因为4 KB是最大页长度,所以不管物理页长度是多少,文件的大小对于分页来说都合适。

图2-5:可执行文件

elf-2-5

图2-6:程序头段

Text Data
p_type PT_LOAD PT_LOAD
p_offset 0x100 0x2bf00
p_vaddr 0x8048100 0x8074f00
p_paddr unspecified unspecified
p_filesz 0x2be00 0x4e00
p_memsz 0x2be00 0x5e24
p_flags PF_R+PF_X PF_R+PF_W+PF_X
p_align 0x1000 0x1000

虽然示例中的代码段和数据段的文件偏移和虚拟地址模4 KB同余,最多用4个文件页就可以装下不纯的代码或者数据(这取决于页长度和文件系统块长度)。

从逻辑上说,系统强制要求内存的许可对每个段看起来好像是完整的且隔离的;段的地址被调整,以保证每个地址空间上的逻辑页有单独的一套许可。在上面例子中,文件代码段末尾和数据段开头可能被映射两次:在代码段的虚拟地址处和数据段的虚拟地址处。

数据段末尾需要对未初始化数据的特殊处理,系统定义它们为0。因此如果一个文件的最后一个数据页包含不存在逻辑内存页中的信息,额外的数据必须被置0,而非可执行文件的未知内容。从逻辑上说,其他三个页的“不纯性”不是进程镜像的一部分;系统是否删去它们不受限制的。该程序的内存镜像如下,假设4 KB(0x1000)一页。

图2-7:进程镜像段

elf-2-7

可执行文件和共享目标文件的段装载在一个方面有所不同。可执行文件段很典型地包含绝对代码。为了让程序正确执行,段必须存在于用来创建可执行文件的虚拟地址处。因此系统使用未改变的p_vaddr值当做虚拟地址。

然而,共享目标文件段典型地包含位置无关代码。这使得一个段的虚拟地址在不同进程之间可以改变,而没有无效的执行行为。虽然系统为某个进程选择虚拟地址,但是它将段保持在相对位置上。因为位置无关的代码在段之间使用相对寻址,内存中虚拟地址之间的差异必须与文件中虚拟地址的差异一致。下表给出了共享目标文件的虚拟地址在不同进程中分配的可能情况,这表明了它固定的相对位置。下表也描述了基地址的计算。

Sourc Text Data Base Address
文件 0x200 0x2a400 0x0
进程 1 0x80000200 0x8002a400 0x80000000
进程 2 0x80081200 0x800ab400 0x80081000
进程 3 0x900c0200 0x900ea400 0x900c0000
进程 4 0x900c6200 0x900f0400 0x900c6000

动态链接

程序解释器

可执行文件可能有一个PT_INTERP程序头元素。在exec(BA_OS)期间,系统会检索来自PT_INTERP段的路径名称,并从解释器文件段来创建初始的进程镜像。也就是说,系统为解释器构成一个内存镜像,而非使用初始的可执行文件的段镜像。之后系统把控制权交给解释器,解释器为应用程序提供环境。

解释器以下面两种方式之一接过控制权。第一,它可能在最开始位置接收到一个文件描述符,用来读取可执行文件。它可以用这个文件描述符来读取并/或将该可执行文件的段映射到内存中。第二,由于可执行文件格式的不同,系统可能会直接将可执行文件加载进内存,而不是给解释器提供一个打开的文件描述符。在具有文件描述符的可能例外情况下,解释器的初始进程状态与可执行文件接收到的一致。解释器本身不再需要第二个解释器。一个解释器可能是一个共享目标文件或者可执行文件。

动态连接器

在构建使用了动态链接的可执行文件时,链接器会给可执行文件添加一个PT_INTERP类型的程序头元素,告诉系统调用动态链接器作为程序解释器。

注:系统提供的动态链接器的位置是与特定处理器有关的。

Exec(BA_OS)和动态链接器协同创建程序的进程镜像,需要下面几步:

链接器也会创建各种数据来帮助动态链接器处理可执行文件和共享目标文件。如之前在“程序头”部分展示的,这些数据在可装载段中,这使得它们在执行过程中可以被访问懂啊。(重申一次,要知道具体准确的段内容是与特定处理器有关的。更多信息,参照处理器补充[7])

由于所有符合ABI的程序都会通过共享目标库导入基本的系统服务,动态链接器会参与到每一个符合ABI的程序的执行中。

如“程序装载”部分解释的,在处理器补充中,共享目标文件可能占用的是与记录在文件程序头表中不同的虚拟内存地址。动态链接器重定位内存镜像,在应用程序获得控制权之前更新绝对地址。虽然如果库文件恰好在程序头表中制定的地址处加载,绝对地址的值可能是正确的,但正常情况下不是这样的。

如果进程环境【参考exec(BA_OS)】包含一个叫做LD_BIND_NOW的变量,且其值非空,则动态链接器会在转交控制权给程序之前处理所有的重定位项目。例如,下面这些环境变量项都会指定这一行为:

否则,LD_BIND_NOW可能要么不存在于当前环境中,要么是空值。动态链接器被允许惰性地计算过程链接表项,这会避免对未被调用的函数进行符号解析和重定位。更多信息,参照“过程链接表”。

动态节

如果一个目标文件参与到动态链接过程,它的程序头表中将有PT_DYNAMIC类型的元素。这个“段”包含.dynamic节。一个特殊的符号,_DYNAMIC,标识了该节,该节包含一个结构体数组。结构体如下。

图2-9:动态结构体

typedef struct {
	Elf32_Sword		d_tag;
	union {
		Elf32_Word	d_val;
		Elf32_Addr	d_ptr;
	} d_un;
} Elf32_Dyn;

extern Elf32_Dyn_DYNAMIC[];

对于该类型的量,d_tag控制了d_un的解释。

d_val

这些Elf32_Word量代表有不同含义的整数值。

d_ptr

这些Elf32_Addr量代表程序虚拟地址。如之前所述,一个文件的虚拟地址可能与执行过程中的内存虚拟地址不一致。当解释地址包含在动态结构体中时,动态链接器基于原始文件值和内存基址来计算实际地址。为了一致性,文件不包含用于“纠正”动态结构体中地址的重定位项。

下表总结了可执行文件可共享目标文件的标签需求。如果一个标签被标记为“强制”,则遵循ABI的文件对应的动态链接数组一定有一个此类型的项。相似地,“可选”意味着该标签对应的项可能出现,但不是必要的。

图2-10:动态数组标签,d_tag

名称 d_un 可执行文件 共享目标文件
DT_NULL 0 忽略 强制 强制
DT_NEEDED 1 d_val 可选 可选
DT_PLTRELSZ 2 d_val 可选 可选
DT_PLTGOT 3 d_ptr 可选 可选
DT_HASH 4 d_ptr 强制 强制
DT_STRTAB 5 d_ptr 强制 强制
DT_SYMTAB 6 d_ptr 强制 强制
DT_RELA 7 d_ptr 强制 可选
DT_RELASZ 8 d_val 强制 可选
DT_RELAENT 9 d_val 强制 可选
DT_STRSZ 10 d_val 强制 强制
DT_SYMENT 11 d_val 强制 强制
DT_INIT 12 d_ptr 可选 可选
DT_FINI 13 d_ptr 可选 可选
DT_SONAME 14 d_val 忽略 可选
DT_RPATH 15 d_val 可选 忽略
DT_SYMBOLIC 16 忽略 忽略 可选
DT_REL 17 d_ptr 强制 可选
DT_RELSZ 18 d_val 强制 可选
DT_RELENT 19 d_val 强制 可选
DT_PLTREL 20 d_val 可选 可选
DT_DEBUG 21 d_ptr 可选 忽略
DT_TEXTREL 22 忽略 可选 可选
DT_JMPREL 23 d_ptr 可选 可选
DT_LOPROC 0x70000000 未指定 未指定 未指定
DT_HIPROC 0x7fffffff 未指定 未指定 未指定

DT_NULL

一个带有DT_NULL标签的相合标志着_DYNAMIC数组的结束。

DT_NEEDED

这个元素包含了字符串表中一个尾零结尾的字符串的偏移,给出了需要的库的名称。偏移是记录在DT_STRTAB项中的表的索引。关于这些名称的更多信息,参照“共享文件依赖性”部分。动态数组可能包含多个此类型的项。虽然它们与其类型项之间的关系是无意义的,但这些项之间的相对顺序是有意义的。

DT_PLTRELSZ

这个元素包含了与过程链接表有关的重定位项的总字节长度。如果DT_JMPREL类型的项存在,则DT_PLTRELSZ总会出现。

DT_PLTGOT

这一项包含了与过程链接表和/或全局偏移表关联的地址。更详细的内容,参照处理器补充说明中的这部分。

DT_HASH

这一项包含了“哈希表”部分描述的符号哈希表的地址。哈希表参考DT_SYMTAB元素引用的符号表。

DT_STRTAB

这一项包含了第一部分描述的字符串表的地址。符号名称,库名称和其他字符串均在此表中。

DT_SYMTAB

这一项包含了第一部分描述的符号表的地址,带有32位文件类的Elf32_Sym项。

DT_RELA

这一项包含了第一部分描述的的重定位表的地址。该表中的项有明确的加数,例如32位文件类的Elf32_Rela。一个目标文件可能包含多个重定位节。当为一个可执行文件或者共享目标文件创建重定位表时,链接器把这些节连接起来,形成一个表。虽然这些节在目标文件中是保持独立的,但是动态链接器把它们当做一个表。当动态链接器为可执行文件创建进程镜像或添加一个共享目标到进程镜像中时,它读取重定位表并执行相关操作。如果该元素存在,动态结构体必须包含DT_RELASZDT_RELAENT元素。当重定位对于一个文件是“强制”时,DT_RELA或者DT_REL都可能发生(两者都出现是允许的,但不是必要的)。

DT_RELASZ

这一项包含了DT_RELA重定位表的总字节长度。

DT_RELAENT

这一项包含了DT_RELA重定位项的字节长度。

DT_STRSZ

这一项包含了字符串表的字节长度。

DT_SYMENT

这一项包含了符号表项的字节长度。

DT_INIT

这一项包含了后面“初始化和终止函数”部分讨论的初始化函数的地址。

DT_FINI

这一项包含了后面“初始化和终止函数”部分讨论的终止函数的地址。

DT_SONAME

这一项包含了字符串表中一个以尾零结尾的字符串的偏移,给出了共享目标的名称。偏移是记录在DT_STRTAB项中的表的索引。关于名称的更多信息,参照后面“共享文件依赖性”部分。

DT_RPATH

这一项包含了字符串表中一个以尾零结尾的搜索库搜索路径字符串的偏移,在“共享文件依赖性”部分有讨论。偏移是记录在DT_STRTAB项中的表的索引。

DT_SYMBOLIC

这一项在共享文件库中的出现改变了动态链接器对于该库中引用的符号解析算法。动态链接器从共享文件本身开始搜索,而不是从可执行文件开始搜索符号。如果共享文件不能提供被引用的符号,动态链接器将继续正常搜索可执行文件和其他共享目标文件。

DT_REL

这一项与DT_RELA很项,除了它的表中包含的是不明确的加数,例如对于32位文件类的Elf32_Rel。如果该项存在,动态结构体必须包含DT_RELSZDT_RELENT`元素。

DT_RELSZ

这一项包含了DT_REL重定位表的总字节长度。

DT_RELENT

这一项包含了DT_REL重定位项的字节长度。

DT_PLTREL

这一项指定了过程链接表参考的重定位项的类型。d_val成员酌情包含DT_REL或者DT_RELA。过程链接表中的所有重定位项必须使用相同的重定位。

DT_DEBUG

这一项用于调试。它的内容不受ABI限定;包含该项的程序不符合ABI标准。

DT_TEXTREL

这一项的缺失表示没有重定位项应该导致一个不可写段的更改,如程序头表中段许可限制的那样。如果该项存在,则一个或多个重定位项可能需要修改一个不可写段,并且动态链接器会做相应准备。

DT_JMPREL

如果存在,该项的d_ptr成员包含了与过程链接表单独关联的重定位项的地址。这些重定位项的分离使得动态链接器在进程初始化阶段忽略了它们(如果惰性绑定生效)。如果这一项存在,则DT_PLTRELSZDT_PLTREL类型的相关项必须存在。

DT_LOPROC~DT_HIPROC

在这个闭区间上的值保留用于特定处理器语义。

除了在数组末尾的DT_NULL项,和DT_NEEDED项之间的相对顺序,项能够以任意顺序出现。上表中未出现的标签值被保留。

共享文件依赖性

当链接器处理一个文件库[10]时,它会提取库成员,并把它们复制到输出的目标文件中。这些静态链接服务在程序执行时是可访问的,不需要动态链接器参与。共享目标文件也提供服务,并且在执行时动态链接器必须把适当的共享目标文件与进程镜像关联起来。因此可执行文件和共享目标文件描述了它们指定的依赖关系。

注:当一个共享目标在依赖列表中多次被引用时,动态链接器只把该文件与进程链接一次。

依赖列表中的名称要么是DT_SONAME字符串的复制,要么是在创建目标文件时共享目标路径名称的复制。例如,如果链接器在创建一个可执行文件时只使用了lib1中带有DT_SONAME项的共享目标和另一个路径名为/usr/lib/lib2的共享目标库,该可执行文件将在它的依赖列表中包含lib1/usr/lib/lib2

如果一个共享目标名称包含一个或多个斜杠(/)字符,例如上面的/usr/lib/lib2或者目录/文件,动态链接器把它直接当做路径名称使用。如果一个名称没有斜杠,例如上面的lib1,有三个机制来指定共享目标的路径搜索,按照优先级排列如下。

所有来自LD_LIBRARY_PATH的目录会在来自DT_RPATH的目录后面被搜索。虽然有些程序(比如链接器)把分号前后的列表区别对待,动态链接器却不会。动态链接器接受分号表示法,语义如上。

注:为了安全性,对于带有set-userset-group标识的程序,动态链接器忽略了环境变量搜索(例如LD_LIBRARY_PATH)。它仅仅搜索DT_RPATH指定的目录和/usr/lib

全局偏移表

通常来说,位置无关代码不能包含绝对虚拟地址。全局偏移表在私有数据中包含绝对地址,因此使得这些地址可以与位置无关性和程序代码段的共享性兼容。一个程序引用它的使用位置无关寻址的全局偏移表并且提取绝对的值,因此可以把位置无关引用重定位到绝对位置上。

初始地,全局偏移表包含它的重定位项需要的信息【参照第一部分“重定位”内容】。在系统为一个可装载目标文件创建进程段后,动态链接器处理重定位项,这些项中的一些将会是全局偏移表中的R_386_GLOB_DAT类型。动态链接器决定相关的符号值,计算它们的绝对地址,并把相关的内存表项设定为合适的值。虽然在链接器创建目标文件时绝对地址还是未知的,但是动态链接器知道所有内存段的地址,因此可以计算它们包含的符号的绝对地址。

如果一个程序需要对一个符号的绝对地址的直接访问,这个符号将拥有一个全局偏移表项。因为可执行文件和共享目标文件有单独的全局偏移表,一个符号的地址可能出现在多个表中。动态链接器在把控制权转移给进程镜像中的任何代码之前会处理所有的全局偏移表的重定位,因此保证了绝对地址在执行期间是可访问的。

全局偏移表第零项保留用于存储动态结构体的地址,以符号_DYNAMIC引用。这允许一个程序,例如动态链接器,在没有处理它的重定位项的情况下找到它自己的动态结构体。这对于动态链接器非常重要,因为它必须在没有其他程序帮助重定位它的内存镜像的情况下初始化自身。在32位Intel架构上,全局偏移表的第一项和第二项也保留。后面的“过程链接表”部分描述了它们。

系统可能为同一个共享目标在不同程序中选择不同的内存段地址;它甚至可能对同一个程序不同时间执行选择不同的库地址。然而,一旦进程镜像被创建,内存段地址不会改变。只要进程存在,它的内存段总会存在于固定的虚拟地址处。

一个全局偏移表的格式和解释与特定处理器有关的。对于32位Intel架构来说,_GLOBAL_OFFSET_TABLE_符号可能被用来访问这个表。

过程(Procedure)链接表

和全局偏移表重定位位置无关地址到绝对位置在很大程度上相像,过程链接表把位置无关的函数调用重定位到绝对位置。链接器不能解决从一个可执行文件或者共享目标文件到另一个之间的执行转换(例如函数调用)问题。因此,链接器安排程序将控制权转给过程链接表中的项。在SYSTEM V架构上,过程链接表存在于共享代码段中,但是它们使用私有全局偏移表中的地址。动态链接器决定目的地的绝对地址并且相应地修改全局偏移表的内存镜像。因此,动态链接器可以在没有影响位置无关性和程序代码段的共享性的情况下重定向这些项。可执行文件和共享目标文件有独立的过程链接表。

图2-12:绝对过程链接表

.PLT0:pushl	got_plus_4
      jmp	*got_plus_8
	  nop; nop
	  nop; nop
.PLT1:jmp	*name1_in_GOT
      pushl	$offset@PC
.PLT2:jmp	*name2_in_GOT
      push	$offset
	  jmp	.PLT0@PC
	  ...

图2-13:位置无关过程链接表

.PLT0:pushl	4(%ebx)
      jmp	*8(%ebx)
	  nop; nop
	  nop; nop
.PLT1:jmp	*name1_in_GOT(%ebx)
      pushl	$offset
.PLT2:jmp	*name2_in_GOT(%ebx)
      push	$offset
	  jmp	.PLT0@PC
	  ...

注:如同上图展示的,过程链接表指令对绝对代码和位置无关代码使用不同的操作数寻址模式。然而,它们给动态链接器的接口是一样的。

按照下面的步骤,动态链接器和程序“合作”来解决过程链接表和全局偏移表中的符号引用问题。

  1. 在第一次创建程序的内存镜像时,动态链接器将全局偏移表中的第二项和第三项设置为特殊值。后面的步骤对这些值进行了更多解释。
  2. 如果过程链接表是位置无关的,全局偏移表的地址必须存在于(%ebx)中。进程中的每个共享目标文件有它自己的过程链接表,控制权只会在同一个目标文件中转换给过程链接表。因此,调用函数负责在调用过程链接表项之前设置全局偏移表的基寄存器。
  3. 为了描述方便,假定程序叫做name1,把控制权转给标签.PLT1
  4. 第一条指令跳转到name1对应的全局偏移表项的地址处。初始地,全局偏移表包含后面的的pushl指令地址,而不是name1的真实地址。
  5. 因此,程序将重定位偏移 (offset)压入栈。重定位偏移是一个在重定位表中生效的32位非负字节偏移量。指定的重定位项将是R_386_JMP_SLOT类型,它的偏移量将指定之前在jmp指令中使用的全局偏移表项。重定位项也包含一个符号表索引,告诉动态链接器哪一个符号被引用了,本例中是name1
  6. 在压入重定位偏移后,程序跳转到.PLT0,即过程链接表中的第一项。pushl指令把第二个全局偏移表项的值(got_plus_44(%ebx))放在栈上,给动态链接器一个识别信息。程序接着跳转到全局偏移表中的第三项(got_plus_88(%ebx)),这样就把控制权交给了动态链接器。
  7. 当动态链接器收到控制权后,它进行出栈操作,检查指定的重定位项,寻找符号值,把name1的“真正”地址放到它的全局偏移表项中,并把控制权转交给预期的目的地址。
  8. 之后的过程链接表项的执行将会直接把控制权转交给name1,不再调用动态链接器。也就是说,.PLT1中的jmp指令将跳转到name1,而不是“下落”到pushl指令。

LD_BIND_NOW环境变量可以改变动态链接器的行为。如果它的值是非空的,那么动态链接器会在把控制权转交给程序之间计算过程链接表项。也就是说,动态链接器在进程初始化时处理R_386_JMP_SLOT类型的重定位项。否则,动态链接器惰性处理过程链接表项,将符号解析和重定位推迟到第一个表项执行时进行。

注:惰性绑定(Lazy binding)通常会在总体上改进一个应用程序的表现,因为不使用的符号不会导致动态链接。然而,由于两个特性,惰性绑定对于一些应用程序是不适用的。第一,对于一个共享目标函数的初始引用比后续的调用耗时更长,因为动态链接器为了解析符号拦截了调用。一些应用不能容忍这种不可预测性。第二,如果发生错误,动态链接器不能够解析符号,动态链接器将终止当前程序。在惰性绑定情况下,这随时可能发生。有些应用程序不能容忍这种不可预测性。通过关闭惰性绑定,动态链接器强制失败在应用程序收到控制权之前,在进程初始化时发生。

哈希表

Elf32_Word对象的哈希表提供了对符号表的访问。下面的标签用于解释哈希表组织,但是它们不是规定的一部分。

图2-14:符号哈希表

labels
nbucket
nchain
bucket[0]
bucket[nbucket-1]
chain[0]
chain[nchain-1]

bucket数组包含nbucket数目的项,chain数组包含nchain数目的项;索引从0开始。bucketchain包含符号表索引。链表项(Chain table entries)与符号表是并行的。符号表项的数目应该等于nchain;所以符号表索引也可以作链表项索引。哈希函数(后面展示)接受一个符号名称,返回一个用于计算bucket索引的值。因此,如果哈希函数接收某些名称并且返回x,则bucket[x%nbucket]给出了一个索引,y,用于在符号表中和链表中定位。如果符号表项不是期望的,则chain[y]给出了下一个拥有相同哈希值的符号表项。我们可以通过chain链来寻找直到找到包含期望名称的符号表项或者chain项包含STN_UNDEF值。

图2-15:哈希函数

unsigned long
elf_hash(const unsigned char *name)
{
	unsigned long	h = 0, g;

	while (*name)
	{
		h = (h << 4) + *name++;
		if(g = h & 0xf0000000)
			h ^= g >> 24;
		h &= ~g;
	}
	return h;
}

初始化和终止函数

在动态链接器创建进程镜像并且重定位后,每个共享目标文件都有机会去执行一些初始化代码。这些初始化函数的调用没有一个规定的顺序,但是所有的共享对象初始化在可执行文件获得控制权之前就发生了。

类似地,共享对象也有终止函数,它们在基进程开始它的终止化后与atexit(BA_OS)[9]机制一起执行。同样地,动态链接器调用终止函数的顺序是不限定的。

共享对象通“动态节”部分所述的动态结构体中的DT_INITDT_FINI项来指定它们的初始化和终止函数。典型地,这些函数的代码存在于.init节和.fini节,这些在第一部分“节”中有提到。

注:虽然正常情况下,atexit(BA_OS)将终止进程,但是不保证在进程结束时它已经被执行。尤其是当进程调用了_exit(BA_OS)[参照 exit(BA_OS)]而不执行该终止例程或者这个进程因为接收到一个既没有捕捉又没有忽略的信号而停止时。

3 C 库

C 库

C库,libc,包含了所有包含在libsys中的符号,另外,也包含了在下列两个表中列出的例程。第一个表列出了ANSI C 标准中的例程。

图3-1:libc内容,名称(无同义词)

名称 名称 名称 名称 名称
abort fputc isprint putc strncmp
abs fputs ispunct putchar strncpy
asctime fread isspace puts strpbrk
atof freopen isupper qsort strrchr
atoi frexp lsxdigit raise strspn
atol fscanf labs rand strsrt
bsearch fseek ldexp rewind strtod
clearerr fsetpos ldiv scanf strtok
clock ftell localtime setbuf strtol
ctime fwrite longjmp setjmp strtoul
difftime getc mblen setvbuf tmpfile
div getchar mbstowcs sprintf tmpnam
fclose getenv mbtowc srand tolower
feof gets memchr sscanf toupper
ferror gmtime memcmp strcat ungetc
fflush isalnum memcpy strchr vfprintf
fgetc isalpha memmove strcmp vprintf
fgetpos iscntrl memset strcpy vsprintf
fgets isdigit mktime strcspn wcstombs
fopen isgraph perror strlen wctomb
fprintf islower printf strncat  

另外,libc包含下面服务。

图3-2:libc内容,名称(无同义词)

名称 名称 名称 名称 名称
__assert getdate lockf ~ sleep tell ~
cfgetispeed getopt lsearch strdup tempnam
cfgetospeed getpass memccpy swab tfind
cfsetispeed getsubopt mkfifo tcdrain toascii
cfsetospeed getw mktemp tcflow _tolower
ctermid hcreate monitor tcflush tsearch
cuserid hdestroy nftw tcgetattr _toupper
dup2 hsearch nl_langinfo tcgetpgrp twalk
fdopen isascii pclose tcgetsid tzset
__filbuf isatty popen tcsendbreak _xftw
fileno isnan putenv tcsetattr  
__flsbuf isnand ~ putw tcsetpgrp  
fmtmsg ~ lfind setlabel tdelete  

标注~的函数在SVID Issue 3中是2级,所以在ABI中是2级。

在上述表中的符号外,以_name形式存在的同义词没有列出。例如,libc包含getopt也包含_getopt

在上述例程中,下面3个在别处没有定义。

int __filbuf(FILE *f);

这个函数返回f的下一个输入字符,恰当地填充它的缓冲区。如果出现错误,返回EOF

int __flsbuf(int x, FILE *f);

这个函数立即输出f的输出字符,就像putc(x,f)被调用,然后把x的值添加到输出流中。如果发生错误,返回EOF,否则返回x。

int _xftw(int, char *, int (*)(char *, struct stat *, int), int);

在应用程序被编译时,对ftw(BA_LIB)函数的调用被映射到这个函数上。这个函数与ftw(BA_LIB)相同,除了_xftw()要求插入第一个参数,其值必须是2.

更多关于SVID,ANSI C和POSIX的信息,参照本章的其他库部分。更多信息,参考本章末的“系统数据接口”。[8]

全局数据符号

libc库需要一些全局外部数据符号的定义来保证它的例程正常工作。除了后表中给出的,libsys库需要的所有数据符号必须由libc提供。

关于这些符号代表的数据对象的正式声明,参考System V 接口定义,第三版或者System V ABI对应的适当处理器补充的第六章中”数据定义“部分。

在后表中符合name- _name形式的项,每一对中的每个符号都代表相同数据。带有下划线的同义词用来满足ANSI C标准的需要。

图3-3:libc内容,全局外部数据符号

名称 名称
getdate_err optarg
_getdate_err opterr
__iob optind
  optopt

索引

索引

译者注

elf-header