前言
译者注:
- 可移植格式规格说明,版本 1.1。
- 来自工具接口标准 (TIS)。
- 已有 TIS 1.2版,但ELF文件格式这部分与1.1版本差别不大。
ELF:可重定位/链接格式
ELF文件格式作为应用程序二进制接口(ABI),最初由Unix系统实验室(USL)编写并发布。工具接口标准委员会(TIS)选择改进后的ELF标准作为在32位Intel架构上的可移植目标文件格式,适用于多种操作系统。
ELF标准使软件开发变得简单,它为开发者提供了跨多平台的二进制接口定义。这将减少不同接口实现的数目,从而减少重新编码和重新编译的需要。
关于本文档
本文档的预期读者是为多种32位系统环境开发目标文件或可执行文件的开发人员。
文档分为以下几个部分:
- “目标文件”描述了3种主要的ELF目标文件格式[1]。
- “程序装载和动态链接”描述了在创建运行时程序时目标文件信息和操作系统的执行过程。
- “C库”列举了libsys(即标准C(ANSI C)和libc库实例)包含的符号,以及libc库实例需要的全局数据符号。
注:X86架构参考文档变为Intel架构。
1 目标文件
介绍
第一部分描述了ABI目标文件格式——ELF。它主要有三种目标文件类型:
- 可重定位文件包含了用来和其他目标文件链接起来生成可执行文件或共享目标文件的代码和数据。
- 可执行文件包含一个可执行的程序。它详定了
exec(BA_OS)
该怎样创建程序的进程镜像。 - 共享目标文件包含了在两种情境下用于链接的代码和数据。第一,链接[参照
ld(SD_CMD)]
可能将它和其他可重定位文件以及共享目标文件合起来生成另一个目标文件;第二,动态链接器把它和一个可执行文件以及其他共享目标文件结合,创造一个进程镜像。
目标文件由汇编器和链接器创建,是可以在处理器上直接运行的二进制程序。那些需要虚拟机才能够执行的程序,如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_LOPROC
到ET_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
ELFDATA2MSB
编码限定补码值最高有效位占用最低地址。
图1-6:数据编码 ELFDATA2LSB
机器信息
对于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_LOPROC
~ SHN_HIPROC
在这个闭区间范围内的值被保留用于特定处理器语义。
SHN_ABS
这个值指定了相关引用的绝对值。例如,与节号SHN_ABS
关联的定义符号拥有绝对值,不受重定位的影响。
SHN_COMMON
与这一节相关的定义符号是通用符号,例如FORTRAN COMMON
或者C语言中未分配的外部变量。
SHN_HIRESERVE
这个值指定了保留索引值范围的上界。系统保留在SHN_LORESERVE
到SHN_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_SYMTAB
与SHT_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_LOPROC
~SHT_HIPROC
闭区间范围上的值被保留用于特定处理器语义。
SHT_LOUSER
指定了应用程序可使用的索引下界。
SHT_HIUSER
指定了应用程序可使用的索引上界。在SHT_LOUSER
到SHT_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_link
和sh_info
两项具有特殊信息,这取决于节类型。
图1-13:sh_link
和sh_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
包含过程链接表。更多信息,参照第一部分“特殊节”部分和第二部分“过程链接表”部分。
.rel
name和.rela
name
包含重定位信息,如“重定位”部分所述。如果文件具有包含重定位的载入段,则此节的属性将会包括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_LOPROC
~STB_HIPROC
在闭区间内的值保留用于特定处理器环境。
全局符号与弱符号主要在两方面区别:
- 当链接器在结合多个可重定位目标文件时,不允许定义多个相同名字的
STB_GLOBAL
符号。另一方面,如果一个已定义全局符号存在,则同名的弱符号的存在不会引起错误。链接器会选择全局定义,忽略弱符号定义。类似的,如果一个公共符号存在(就是st_shndx
域为SHN_COMMON
的符号),则同名的弱符号的存在不会引起错误。链接器会选择公共定义,忽略弱符号定义。 - 当链接器寻找文件库时,它会提取包含未定义全局符号的文件。它可能是一个全局符号或者弱符号。链接器不会为了解决未定义的弱符号问题而提取文件。未定义的弱符号的值为0。
在每一个符号表中,所有与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_LOPROC
~STT_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
项的解释有细小差异。
- 在可重定位文件中,对于节索引为
SHN_COMMON
的符号,st_value
包含了字节对齐限制。 - 在可重定位文件中,对于已定义符号,
st_value
包含了节偏移地址。也就是说,st_value
是从st_shndx
标识节处开始的偏移量。 - 在可执行文件和共享目标文件中,
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_info
和sh_link
项指定了这些关系。不同目标文件的重定位项对于r_offset
项的解释可能有细微差别。
- 在可重定位文件中,
r_offset
包含了节偏移。这就是说,重定位节本身描述了怎样修改文件中的另外的节;重定位偏移指出了第二个节中的存储单元。 - 在可执行文件和共享目标文件中,
r_offset
包含虚拟地址。为了让这些文件的重定位项对动态链接器更有用,节偏移(文件角度)给出了一个虚拟地址(内存角度)。
虽然为了相关程序的方便,r_offset
的解释对于不同目标文件来说有差异,但是重定位类型的意义则相同。
重定位类型
重定位项描述了怎样修改相关的指令和数据域(比特数在图中下角落内)。
图1-21:可重定位域
word32
它指定了一个32比特域,占用4个字节,对齐方式任意。这些值使用32位Intel架构上其他字变量的值相同的比特顺序。
下面的计算假设这些动作是把一个可重定位文件转换为可执行文件或者共享目标文件。从概念上讲,链接器把多个可重定位文件混合来得到输出文件。它首先要决定怎样结合并放置这些输入文件,然后更新符号表的值,最后重定位。应用于可执行文件或者共享 目标文件的重定位是相似的,并且会达到相同的结果。后面的描述采用如下记号。
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_phentsize
和e_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_vaddr
和p_offset
值(模页大小后相等)。这一项给出了该段在内存中和在文件中应该如何对齐。值为0和1意味着无对齐要求。否则,p_align
应该为一个正的2的幂次值,p_vaddr
与p_offset
模p_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_filesz
和p_memsz
描述。文件中的字节被映射到内存段的起始位置。如果段的内存长度(p_memsz
)大于文件长度(p_filesz
),则按照定义,“额外”的字节值为0,跟在段的已初始化数据后面。文件长度不会超过内存长度。在程序头表中,可装载段项按照p_vaddr
升序排列。
PT_DYNAMIC
该数组元素指定动态链接信息。更多信息,参照“动态链接”节。
PT_INTERP
该数组元素指定了一个以尾零结束的路径名的位置和长度,以此来调用解释器。该段类型只对可执行文件有意义(不过它也会在共享目标文件中出现);它不会在同一个文件中出现多次。如果它存在,则它在所有可装载段项之前。更多信息,参照“程序解释器”部分。
PT_NOTE
该数组元素指定了附加信息的位置和长度。更多信息,参照“记录节”。
PT_SHLIB
该段类型保留,但没有特定语义。包含该类型数组元素的程序不符合ABI标准。
PT_PHDR
如果存在的话,该数组元素指定了程序头表本身的位置和长度,无论是在内存镜像中还是在文件中。它不会在同一个文件中出现多次。另外,只要程序头表是该程序内存镜像中的一部分,它就会出现。如果它存在,则它在所也有可装载段项之前。更多信息,参照“程序解释器”。
PT_LOPROC
~PT_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 . . . |
namesz
和name
在name
中的第一个namesz
长度包含一个以尾零结尾的字符串,代表项的拥有者或者发起者。没有避免名称冲突的正式机制。按照惯例,供应商使用他们自己的名字,例如“XYZ 计算机公司”,作为标识符。如果没有名字,namesz
为0。填充块是可选的,它是为了在必要时满足描述符的4字节对齐需求。填充位不算在namesz
中。
descsz
和desc
在desc
中的第一个descsz
长度包含记录描述符。ABI对于描述符的内容没有限制。如果没有描述符,则descsz
为0。填充块是可选的,它是为了在必要时满足下一个记录项的4字节对齐需求。填充位不算在descsz
中。
type
这个字给出了描述符的解释。每一个发起者控制它自己的类型;对于一个类型值的多种解释可能存在。因此,一个程序为了理解描述符,必须同时认出名称和类型。目前,类型必须是非负数。ABI没有定义描述符的意义。
为了说明,下面的记录段包含两项。
图2-4:记录段例子
注:
- 系统保留了无名(namesz==0)的和名字长度为0(name[0]==’\0’)的记录信息,但是目前没有定义类型。所有其他名字必须少含有一个非尾零字符。
- 记录信息是可选的。在记录信息不影响程序的执行行为时,注释信息是否存在不影响程序对于ABI的遵循。否则,程序不遵循ABI,并且有未定义行为。
程序装载
系统在创建或者扩大一个进程镜像时,正常来说,它把每个文件段拷贝到一个虚拟内存段。有时系统对文件的读取依赖于程序的执行行为,例如系统装载。[6]除非一个进程在执行时引用了逻辑页,否则它不需要物理页,进程通常会使许多页处于未引用状态。因此为了增强系统性能,延迟的物理读取常常避免它们。为了在实际中获得效率,可执行文件和共享目标文件必须有文件偏移和虚拟地址模页长度同余的段镜像。
SYSTEM V架构中段的虚拟地址和文件偏移模4 KB(0x1000)或这更大的2的幂次同余。因为4 KB是最大页长度,所以不管物理页长度是多少,文件的大小对于分页来说都合适。
图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个文件页就可以装下不纯的代码或者数据(这取决于页长度和文件系统块长度)。
- 第一个代码页包含ELF头,程序头表和其他信息。
- 最后一个代码页包含一份数据段开头的拷贝。
- 第一个数据页包含一份代码段末尾的拷贝。
- 最后一个数据页可能包含与进程运行不相关的文件信息。
从逻辑上说,系统强制要求内存的许可对每个段看起来好像是完整的且隔离的;段的地址被调整,以保证每个地址空间上的逻辑页有单独的一套许可。在上面例子中,文件代码段末尾和数据段开头可能被映射两次:在代码段的虚拟地址处和数据段的虚拟地址处。
数据段末尾需要对未初始化数据的特殊处理,系统定义它们为0。因此如果一个文件的最后一个数据页包含不存在逻辑内存页中的信息,额外的数据必须被置0,而非可执行文件的未知内容。从逻辑上说,其他三个页的“不纯性”不是进程镜像的一部分;系统是否删去它们不受限制的。该程序的内存镜像如下,假设4 KB(0x1000)一页。
图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
段的路径名称,并从解释器文件段来创建初始的进程镜像。也就是说,系统为解释器构成一个内存镜像,而非使用初始的可执行文件的段镜像。之后系统把控制权交给解释器,解释器为应用程序提供环境。
解释器以下面两种方式之一接过控制权。第一,它可能在最开始位置接收到一个文件描述符,用来读取可执行文件。它可以用这个文件描述符来读取并/或将该可执行文件的段映射到内存中。第二,由于可执行文件格式的不同,系统可能会直接将可执行文件加载进内存,而不是给解释器提供一个打开的文件描述符。在具有文件描述符的可能例外情况下,解释器的初始进程状态与可执行文件接收到的一致。解释器本身不再需要第二个解释器。一个解释器可能是一个共享目标文件或者可执行文件。
- 一个共享目标文件(正常情况下)在地址不同的各种进程中按照位置无关的方式装载;系统在
mmap(KE_OS)
及相关服务使用的动态链接段区域创建它的段。因此,一个共享目标解释器将不会与初始可执行文件的初始段地址发生冲突。 - 一个可执行文件在固定的地址装载;系统使用来自程序头表的虚拟地址创建它的段。因此,一个可执行文件解释器的虚拟地址可能会与第一个可执行文件发生冲突;解释器应该负责解决冲突。
动态连接器
在构建使用了动态链接的可执行文件时,链接器会给可执行文件添加一个PT_INTERP
类型的程序头元素,告诉系统调用动态链接器作为程序解释器。
注:系统提供的动态链接器的位置是与特定处理器有关的。
Exec(BA_OS)
和动态链接器协同创建程序的进程镜像,需要下面几步:
- 把可执行文件的内存段添加到进程镜像中;
- 把共享目标文件内存段添加到进程镜像中;
- 为可执行文件和共享目标文件重定位;
- 如果动态链接器收到过用于读取可执行文件的文件描述符,则关闭它;
- 把控制权交给程序,使这一切看起来像是程序直接从
exec(BA_OS)
处接过控制权。
链接器也会创建各种数据来帮助动态链接器处理可执行文件和共享目标文件。如之前在“程序头”部分展示的,这些数据在可装载段中,这使得它们在执行过程中可以被访问懂啊。(重申一次,要知道具体准确的段内容是与特定处理器有关的。更多信息,参照处理器补充[7])
SHT_DYNAMIC
类型的.dynamic
节包含各种数据。这个存在于节开头的结构包含有其他动态链接信息的地址。SHT_HASH
类型的.hash
节包含有符号哈希表。SHT_PROGBITS
类型的.got
、.plt
节包含两个独立的表:全局偏移表和过程链接表。后面部分会解释动态链接器如何使用并改变这些表来创建内存镜像。
由于所有符合ABI的程序都会通过共享目标库导入基本的系统服务,动态链接器会参与到每一个符合ABI的程序的执行中。
如“程序装载”部分解释的,在处理器补充中,共享目标文件可能占用的是与记录在文件程序头表中不同的虚拟内存地址。动态链接器重定位内存镜像,在应用程序获得控制权之前更新绝对地址。虽然如果库文件恰好在程序头表中制定的地址处加载,绝对地址的值可能是正确的,但正常情况下不是这样的。
如果进程环境【参考exec(BA_OS)
】包含一个叫做LD_BIND_NOW
的变量,且其值非空,则动态链接器会在转交控制权给程序之前处理所有的重定位项目。例如,下面这些环境变量项都会指定这一行为:
- LD_BIND_NOW = 1
- LD_BIND_NOW = on
- LD_BIND_NOW = off
否则,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_RELASZ
和DT_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_RELSZ和
DT_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_PLTRELSZ
和DT_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
,有三个机制来指定共享目标的路径搜索,按照优先级排列如下。
- 第一,动态数组标签
DT_RPATH
可能包含一个字符串,它包含了一个目录列表,由冒号(:)分隔。例如,字符串/home/dir/lib:/home/dir2/lib:
告诉了动态链接器首先在/home/dir/lib
目录搜索,然后再在/home/dir2/lib
搜索,最后在当前目录搜索。 - 第二,进程环境中一个叫做
LD_LIBRARY_PATH
的变量【参照exec(BA_OS)
】可能包含一个与上面所述相同的目录列表,可能由一个分号(;)结尾,后面跟着另外一个目录列表。- LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:
- LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib:
- LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:;
所有来自LD_LIBRARY_PATH
的目录会在来自DT_RPATH
的目录后面被搜索。虽然有些程序(比如链接器)把分号前后的列表区别对待,动态链接器却不会。动态链接器接受分号表示法,语义如上。
- 最后,如果上面两组目录无法定位期望的库,则动态链接器搜索
/usr/lib
。
注:为了安全性,对于带有set-user
和set-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
...
注:如同上图展示的,过程链接表指令对绝对代码和位置无关代码使用不同的操作数寻址模式。然而,它们给动态链接器的接口是一样的。
按照下面的步骤,动态链接器和程序“合作”来解决过程链接表和全局偏移表中的符号引用问题。
- 在第一次创建程序的内存镜像时,动态链接器将全局偏移表中的第二项和第三项设置为特殊值。后面的步骤对这些值进行了更多解释。
- 如果过程链接表是位置无关的,全局偏移表的地址必须存在于
(%ebx)
中。进程中的每个共享目标文件有它自己的过程链接表,控制权只会在同一个目标文件中转换给过程链接表。因此,调用函数负责在调用过程链接表项之前设置全局偏移表的基寄存器。 - 为了描述方便,假定程序叫做
name1
,把控制权转给标签.PLT1
。 - 第一条指令跳转到
name1
对应的全局偏移表项的地址处。初始地,全局偏移表包含后面的的pushl
指令地址,而不是name1
的真实地址。 - 因此,程序将重定位偏移 (offset)压入栈。重定位偏移是一个在重定位表中生效的32位非负字节偏移量。指定的重定位项将是
R_386_JMP_SLOT
类型,它的偏移量将指定之前在jmp
指令中使用的全局偏移表项。重定位项也包含一个符号表索引,告诉动态链接器哪一个符号被引用了,本例中是name1
。 - 在压入重定位偏移后,程序跳转到
.PLT0
,即过程链接表中的第一项。pushl
指令把第二个全局偏移表项的值(got_plus_4或4(%ebx)
)放在栈上,给动态链接器一个识别信息。程序接着跳转到全局偏移表中的第三项(got_plus_8或8(%ebx)
),这样就把控制权交给了动态链接器。 - 当动态链接器收到控制权后,它进行出栈操作,检查指定的重定位项,寻找符号值,把
name1
的“真正”地址放到它的全局偏移表项中,并把控制权转交给预期的目的地址。 - 之后的过程链接表项的执行将会直接把控制权转交给
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开始。bucket
和chain
包含符号表索引。链表项(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_INIT
和DT_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 |
索引
索引
略
译者注
- [1] object file 一词在原文档时而代指普遍的ELF文件(可重定位文件/可执行文件/共享文件)时而仅代指已编译但未链接的文件。一般不会引起混淆。
- [2] 你可以在
/usr/include/elf.h
找到这个结构体的定义。另外,使用readelf -h filename
可列出ELF header
,截图如下:
- [3] 你可以在
/usr/include/elf.h
找到这个结构体的定义。另外,使用readelf -S filename
可列出节头信息。 - [4] 举个这样的节的例子:
.bss
。 - [5] 你可以在
/usr/include/elf.h
找到这个结构体的定义。另外,使用readelf -s filename
可列出符号表信息。 - [6] 此句的意思可能我理解的不对,原句是“When——and if——the system physically reads the file depends on the program’s execution behavior, system load, etc.”个人觉得似乎有语法错误?
- [7] 原文为“processor supplement”,译者不清楚具体的含义是什么,姑且译为“处理器补充”,后文如出现,亦译如此。
- [8] 本文档中并没有文中提到的其他库部分或者”系统数据接口“部分,估计C库部分是摘自其他文档。
- [9]
atexit()
- [10] 原文是“archive library”,似乎台湾把archive叫做“存档”,但是意思与“文件”一样。