ELF格式详解
Executable and Linkable Format (ELF)
可执行和可链接格式(ELF)
Executable and Linkable
Format (ELF)
可执行和可链接格式(ELF)
ELF: Executable and Linking Format
可执行且可链接格式最初由UNIX系统实验室(UNIX System Laboratories (USL))设计并发布,作为Application Binary
Interface (ABI)的一部分。工具接口委员会(Tool
Interface Standards (TIS))委员会选择不断发展的ELF标准作为可移植的目标文件格式,使之可工作在32位Intel体系架构的多种操作系统上。
ELF标准试图通过为开发者提供一系列的二进制接口,来实现流水线式的软件开发流程。这些接口定义了如何扩展到不同的操作系统环境中。这就减少了不同实现的接口的数量,也就避免了重新编码和实现。
关于这篇文档
这篇文档面向在32位操作系统上创建目标文件和可执行文件的开发人员。本文档分为三个部分:
n 第一章,“目标文件”:介绍了3种主要目标文件类型的ELF目标文件格式
n 第二章,“程序加载与动态链接”:介绍了目标文件的一些补充信息,以及系统运行程序时的动作。
n 第三章,“C库”:列出了libsys中包含的符号(symbols),标准的ANSI C,libc例程以及libc例程需要的全局数据符号。
注:X86架构改称为Intel架构。
1
目标文件
1.1
简介
本章介绍应用程序二进制接口(ABI,
Application binary Interface)规定的目标文件格式ELF(Executable and Linking
Format),有三种主要类型的目标文件:
1)
可重定位(relocatable)文件:保存着代码和数据,这些代码和数据可以同其他目标文件链接在一起,生成一个可执行文件或共享目标文件。
2)
可执行(executable)文件:保存着能够执行的程序,这个文件告诉exec(BA_OS)如何创建一个程序的进程映像(program’s process image)。
3)
共享目标(shared object)文件:保存着能够在不同上下文环境中链接的代码和数据。首先,链接编辑器(link editor)[参见ld (SD_CMD)]可能将它与其他的可重定位文件和共享目标文件链接到一起,生成另外一个目标文件。然后,动态链接器(dynamic linker)将它与一个可执行文件和其他的共享目标文件链接到一起,生成一个新的进程映像。
由汇编器(assembler)和链接编辑器(link editor)创建的目标文件,以二进制的形式表示直接在处理器上运行的程序。而需要其他抽象机支持的程序,比如shell脚本,不在本文讨论范围内。
在这节介绍性的材料之后,第1章主要介绍文件的格式以及如何利用它们创建应用程序。 第二章补充描述了目标文件的几个组成部分(主要是程序头部(program header)),但主要是介绍应用程序的执行。
1.1.1
目标文件的格式
程序头部表(program header table)告诉系统如何创建一个进程映像。要创建一个进程映像(执行一个程序),必须有一个程序头部表,而可重定位文件不需要有(所以这个程序头部表是属于可选的)。Section头部表(section header table)包含描述文件section的信息。每个section对应表中的一个表项,每个表项包含了一些信息,比如section名字、section大小等。链接时使用的文件必须有一个section头部表,其他情况下使用的目标文件不需要有。
注:虽然图中的程序头部表紧跟在ELF头部之后,section头部表在section之后,但实际的文件可能有所不同。而且,除了ELF头部固定在文件的起始位置之外,sections和segments可能采用任意的顺序排列。
1.1.2
数据表示
目标文件格式支持多种类型的处理器,比如8位的、32位的体系架构。而且,它还试图扩展到更大或更小的体系架构上运行。因此,目标文件为控制数据使用一些与机器无关的格式,从而可以使用通用方法来描述目标文件的内容。目标文件中使用的其他数据,则需要采用目标处理器的编码规则进行编码,与创建文件的机器架构无关。
目标文件格式定义的所有数据结构遵循其对应类型的固有大小和对齐标准。对于需要4字节对齐的对象,数据结构中需要显式包含4字节对齐的填充字段,从而使数据结构的大小是4的倍数。数据在文件中的偏移也需要适当的对齐,比如,一个包含了Elf32_Addr成员的数据结构在文件中将会对齐到4字节边界上。
出于移植性方面的考虑,ELF不使用位字段(bit-field)。
1.2
ELF头部 ELF Header
有些目标文件中数据结构的大小是能够增长的,所以ELF头部包含了它们的实际大小。 如果目标文件格式发生改变,程序可能会碰到比预期或大或小它们不能识别的数据结构,因此程序可能会忽略“额外”的信息。处理这些“missing”的信息依赖于上下文,并且需要在定义扩展数据结构时指出它们。
l e_ident:位于文件起始部分的字节,把文件标记为一个目标文件,并提供机器无关的数据,用于解码和解释这个文件的内容。完整的介绍参见后面的“ELF标识”一节。
l e_type:该成员标识目标文件的类型。
虽然core类型的文件内容没有具体的规定,但ET_CORE还是留作标识此类型的文件。ET_LOPROC与ET_HIPROC(含)之间的取值留作处理器相关的语义。其他的取值留作以后新类型的目标文件使用。
l e_machine:指出目标文件要运行在哪种体系架构上。
其他的取值留作新的体系架构使用。处理器相关的ELF标识通过机器名称来区分,比如,后面提到的一个以EF_为前缀的标识:WIDGET,如果是针对XYZ这种机器的,那么它的标识应为:EF_XYZ_WIDGET。
l e_version:指定了目标文件的版本。
1表示原始的文件格式。如果需要扩展,将使用更大的数字创建新的版本号。EV_CURRENT虽然在上图中取值为1,但可以通过改变他的取值来反映当前的版本号。
l e_entry:该成员表示虚拟地址,是系统执行文件的入口(系统转移控制权到该成员给出的虚拟地址),从而启动进程。如果目标文件没有相关入口点,那么取值为0。
l e_phoff:该成员保存程序头部表(program header table)在目标文件中的偏移,如果不存在这个表,那么取值为0。
l e_shoff:该成员保存section头部表(section header table)在目标文件中的偏移,如果文件中不存在这个表,那么取值为0。
l e_flags:该成员保存处理器相关的标志(processor-specific
flags),标志的命名规则是EF_machine_flag。参见“机器信息”一节。
l e_ehsize:该成员保存ELF头部(ELF Header)的大小。
l e_phentsize:该成员保存程序头部表的表项的大小,所有表项的大小相同。
l e_phnum:该成员保存程序头部表的表项的数目。由e_phentsize和e_phnum就可以计算出表的大小。如果文件中不存在程序头部表,那么该成员取值为0。
l e_shentsize:该成员保存section 头部(section header)的大小。一个section头部(section header)即section头部表中的一个表项,所有表项的大小相同。
l e_shnum:该成员保存section头部表的表项的数目。由e_shentsize e和e_shnum就可以计算出表的大小。如果文件中不存在section头部表,那么该成员取值为0。
l e_shstrndx:该成员保存section头部表中与section 名称字符串表相关的表项的索引。如果目标文件中没有section名称字符串表,那么此成员的取值为SHN_UNDEF。参见后面的“Sections”和“字符串表”查看更多信息。(This member holds
the section header table index of the entry associated with the section name
string table. If the file has no section name string table, this member holds
the value SHN_UNDEF. See ‘‘Sections’’ and ‘‘String Table’’ below for more
information.)
1.2.1
ELF 标识 ELF Identification
正如上面提到的,ELF这种目标文件结构可以支持不同的处理器类型、数据编码和机器类型。为了实现这一点,文件起始部分的字节规定了如何解释此文件,这和文件所在的处理器无关,也和文件后面的内容无关。
ELF头部(目标文件)中的起始字节用e_ident成员保存。
这些通过索引访问的字节保存如下的取值:
l EI_MAG0至EI_MAG3:最初的4个字节定义了“magic number”,指明这个文件是一个ELF目标文件。
l EI_CLASS:下一个字节,e_ident[EI_CLASS]这个字节表明了文件的类别。
ELF文件格式被设计成可在各种不同大小的机器间移植,避免了把64位的文件运行在32位的机器上。ELFCLASS32支持4GBytes虚地址空间的文件,它的数据类型采用前面介绍的基本类型。ELFCLASS64用于64位的体系架构,本文中对64位的文件格式未作说明。如果需要,还可定义其他的种类,以及对应的基本数据类型和大小。
l EI_DATA:e_ident[EI_DATA]字节规定了处理器相关的数据编码。下表中列出了目前定义的编码。
编码的更多信息,后面会详细介绍。其他的取值留作新的编码方式。
l EI_VERSION:e_ident[EI_VERSION]字节标明了ELF头部(ELF header)的版本号。目前,其取值必须为EV_CURRENT,同前面介绍的e_version一致。
l EI_PAD:填充e_ident数组中未被使用的字节。是保留字节,取值为0。程序解析目标文件时需要忽略他们。如果目前未使用的字节将来使用了,这些字节的取值可能发生变化。
文件的数据编码规定了如何解释文件中的对象。ELFCLASS32文件中的对象占用1、2或4个字节。下图展示了不同的编码下,对象是如何被表示的,字节序号标示在左上角。
ELFDATA2LSB下的二进制补码值,LSB(least
significant byte)占用低地址。(译者:小端字节)
ELFDATA2MSB下的二进制补码值,MSB(most
significant byte)占用低地址。(译者:大端字节)
1.2.2
机器信息 Machine Information
对于32位的Inter架构,其e_ident取值如下所示:
ELF头部中e_machine成员存放的处理器标识必须为EM_386。
ELF头部中e_flags成员存放目标文件相关的标志位。32位的Intel架构没有定义标志位,所以这个成员的取值为0。
1.3
Sections
目标文件的section 头部表(secition header table)存放着所有的section。如下所述,section 头部表是一个Elf32_Shdr结构的数组,通过下标来索引。ELF头部(ELF header)中的e_shoff成员给出了section 头部表在文件中的偏移,e_shnum给出了表中共含多少表项,e_shentsize给出了每个表项的大小。
section头部表(section header table)的一些索引是保留的,目标文件将不会存在这些索引对应的section。
l SHN_UNDEF:表示一个未定义的、丢失的、不相关的、或无意义的section引用。比如对应于SHN_UNDEF的“defined”符号是一个未定义的符号。
注:虽然索引0代表未定义,section 头部表中仍会包含一个索引为0的表项。这就意味着,如果ELF头部中的e_shnum规定section 头部表中含有6个表项,它们的索引取值是0至5。索引为0的表项稍后会介绍。
l SHN_LORESERVE:保留索引值的下限(This value specifies the lower bound of the range of reserved
indexes)。
l SHN_LOPROC至SHN_HIPROC:这个范围内的取值留作特定处理器相关的语义。
l SHN_ABS:相应引用的绝对值(This value specifies
absolute values for the corresponding reference)。比如对应为SHN_ABS的符号,是一个绝对值,不受重定位影响(For example,symbols defined relative to section number SHN_ABS have
absolute values and are not affected by relocation.)。
l SHN_COMMON:这个section定义的符号是通用符号,比如FORTRAN COMMON和未分配的C external变量(Symbols defined relative to this
section are common symbols, such as FORTRANCOMMON or unallocated C external
variables)。
l SHN_HIRESERVE:保留索引值的上限(This value specifies the upper bound of the range of reserved
indexes)。系统保留了SHN_LORESERVE和SHN_HIRESERVE(含)之间的索引值。这些值不会对应到section 头部表,即表中不会含有是这些索引值的表项。
section包含一个目标文件的除ELF头部、程序头部表和section 头部表之外的所有信息。目标文件的section要满足几个条件:
1)
目标文件中的每个section都只对应一个section头部。而一个section header可以不对应任何section(Every section in an object file has exactly one section header
describing it. Section headers may exist that do not have a section)。
2)
文件中的每个section占用连续的字节序列(可能为空)。
3)
文件中的各section不能重叠,即不存在属于多个section的字节。
4)
目标文件可以有不活动的空间。各种头部和sections(The various headers and the sections)可能没有“覆盖”目标文件的所有字节。本文未规定不活动空间的数据内容。
section头部(section header)的数据结构如下图所示:
l sh_name:该成员指定section的名称。它的值是section头部字符串表section的索引,是一个以空字符结尾的字符串的位置。参见后面的“字符串表”一节。(This
member specifies the name of the section. Its value is an index into the
section header string table section [see ‘‘String Table’’ below], giving the
location of a null-terminated string)
l sh_type:该成员对section的内容和语义进行分类(即section类型)。参见后面的图表。
l sh_flags:section标志位,描述各种section的属性。参见后面的图表。
l sh_addr:如果section将会出现在进程的内存映像中,这个成员给出的地址是该section首个字节驻留在内存中的地址。否则,此成员的取值为0。
l sh_offset:section在文件中的偏移。后面会介绍一种SHT_NOBITS类型的section,不占用文件中的字节,此成员仅代表概念上的偏移。
l sh_size:section的大小。除非section类型是SHT_NOBITS,否则section将占用文件中的sh_size个字节。SHT_NOBITS类型的section可能其sh_size不为0,但是它不占用文件中的任何空间。
l sh_link: 该成员保存section 头部表的索引。不同类型的section,它有不同的含义,参见后面的图表(This member holds a section header table index link, whose
interpretation depends on the section type. A table below describes the values)。
l sh_info:该成员保存辅助信息。不同类型的section,它有不同的含义,参见后面的图表。
l sh_addralign:有些section有地址对齐方面的约束条件。比如section中有个doubleword型的数据,系统就必须保证整个section是doubleword对齐的,即sh_addr对sh_addralign取模为0。目前,此成员的取值只允许是0和2的幂数。取值0和1代表该section没有对齐方面的约束。
l sh_entsize:有些section保存固定大小条目的表,比如符号表。对于这种表,此成员给出了表项的大小。如果表项大小不固定,此成员取值为0。
sh_type成员的取值如下图所示:
l SHT_NULL:表示此section头部(section header)是不活动的,不对应一个section。Section头部中其他成员的取值均未定义。
l SHT_PROGBITS:section存放的数据由程序定义,它们的格式和含义也都由程序决定。
l SHT_SYMTAB和SHT_DYNSYM:section存放的是一个符号表。目前,一个目标文件只能包含一个此类型的section,但是这一限制以后可能放宽。通常情况下,SHT_SYMTAB提供了链接编辑(link editing)时用到的符号,同时它也可能被动态链接用到。作为一个完整的符号表,它可能包含许多动态链接用不到的符号。因此,目标文件还可能包含一个SHT_DYNSYM section,用于存放动态链接用到的符号,以节省空间,参见后面的“符号表”一节。
l SHT_STRTAB:section保存的是一个字符串表(string table)。一个目标文件可以有多个字符串表section。参见后面的“字符串表”一节。
l SHT_RELA:section通过显式的加数保存重定位条目(The section holds
relocation entries with explicit addends)。比如ELF32_Rela对应32位的目标文件类型。一个目标文件可以有多个重定位section。参见后面的“重定位”一节。
l SHT_HASH:section保存的是一个符号哈希表(The section holds a
symbol hash table)。所有参与动态链接的对象必须包含一个符号哈希表。目前一个目标文件只能包含一个哈希表,但这个限制以后可能放宽。参见后面的“哈希表”一节。
l SHT_DYNAMIC:section保存动态链接的相关信息。目前,一个目标文件只能有一个此类型的section,但是这个限制以后可能放宽。参见后面的“动态链接section”一节。
l SHT_NOTE:section保存文件的一些注释信息(The section holds
information that marks the file in some way)。参见第2章“注释section”一节。
l SHT_NOBITS:此类型的section不占用文件的空间,其他方面类似于SHT_PROGBITS。虽然这种类型的section不包含字节,sh_offset成员仍规定了概念上的文件偏移。
l SHT_REL:section保存没有显示加数的重定位条目(The section holds
relocation entries without explicit addends)。比如ELF32_Rel对应32位的目标文件。一个目标文件可能有多个重定位的section。参见后面的“重定位”一节。
l SHT_SHLIB:该section类型被保留,未定义其含义(This section type is
reserved but has unspecified semantics.)。含有这种类型的section的程序不符合ABI规范。
l SHT_LOPROC至SHT_HIPROC:这个范围内的取值留作处理器相关的语义。
l SHT_LOUSER:预留给应用程序使用的索引值的下限。
l SHT_HIUSER:预留给应用程序使用的索引值的上限。应用程序可以使用SHT_LOUSER和SHT_HIUSER之间的section类型,而不会和目前或未来系统定义的section类型冲突。
其他的section类型都是保留的。正如前面提到的,虽然索引为0(SHN_UNDEF)代表未定义的section引用,但是仍然存在这样的section头部其保存的值如下所示:
Section头部的sh_flags成员保存用于描述section的属性。已定义的取值如下所示,其他为保留值。
如果设置了sh_flags中的一个flag比特位,那么section的这个属性就是“打开”的状态。否则,这个属性是“关闭”的状态。未定义的属性位设置为0。
l SHF_WRITE:section包含的数据在进程执行期间是可写的。
l SHF_ALLOC:在进程执行期间section占用内存。某些控制用section并不占用目标文件在内存中的映像,这类section的这一属性是关闭的状态。
l SHF_EXECINSTR:section包含可执行的机器指令。
l SHF_MASKPROC:这个掩码中的所有比特位留作处理器相关的语义。
Section头部(section header)的两个成员:sh_link和sh_info,依据section的不同类型,保存特殊的信息。
Section类型为SHT_DYNAMIC时,sh_link表示此section中条目所用到的字符串表的section头部索引。sh_link表示The section header index of the
string table used by entries in the section。sh_info为0。
Section类型为SHT_HASH时,sh_link表示此哈希表所适用的符号表的section头部索引。sh_link表示The section header index of the symbol
table to which the hash table applies。sh_info为0。
Section类型为SHT_REL和SHT_RELA时,sh_link表示相关符号表的section头部索引。sh_info表示重定位所适用的section的section头部索引。sh_link表示The section header index of the
associated symbol table。sh_info表示The section header index of the section to which the relocation
applies。
Section类型为SHT_SYMTAB和SHT_DYNSYM时,sh_link表示相关联的字符串表的section头部索引。sh_info表示最后一个局部符号(绑定STB_LOCAL)的符号表索引值加一。sh_link表示The section header index of the
associated string table。sh_info表示One greater than the symbol table index of the last local symbol
(binding STB_LOCAL)。
Section类型为其他时,sh_link为SHN_UNDEF,sh_info为0
1.3.1
特殊的section
各种不同的section保存程序和控制信息,下表中列出了系统使用的section,并标明了它们的类型和属性。
l .bss:保存程序内存映像中未初始化的数据。当程序开始运行时,系统把这些数据初始化为0。这个section是SHT_NOBITS类型的,即不占用目标文件空间。属性为SHF_ALLOC和SHF_WRITE。
l .comment:保存版本控制信息。section类型为SHT_PROGBITS,没有属性。
l .data和.data1:保存程序的内存映像中已经初始化的数据。section类型为SHT_PROGBITS。属性为SHF_ALLOC和SHF_WRITE。
l .debug:保存符号的调试信息。其内容并未规定。section类型为SHT_PROGBITS。没有属性
l .dynamic:保存动态链接信息。其属性包含SHF_ALLOC比特位。SHF_WRITE比特位是否被置位与处理器相关。参见第二章。section类型为SHT_DYNAMIC。
l .dynstr:保存动态链接需要的字符串。通常,这些字符串代表符号表表项的名字。参见第二章。section类型为SHT_STRTAB。属性为SHF_ALLOC。
l .dynsym:保存动态链接符号表。参见”符号表”一节,参见第二章。section类型为SHT_DYNSYM。属性为SHF_ALLOC。
l .fini:保存进程退出代码所执行的指令。即当一个程序正常退出时,系统执行此section中的代码。section类型为SHT_PROGBITS。属性为SHF_ALLOC和SHF_EXECINSTR。
l .got:保存全局偏移表。参见“全局偏移表”一节。section类型为SHT_PROGBITS。
l .hash:保存符号哈希表。参见“哈希表”一节。section类型为SHT_HASH。属性为SHF_ALLOC。
l .init:保存进程初始化代码所执行的指令。即当一个程序开始运行时,在main程序入口(main for C programs)之前,系统执行此section中的代码。section类型为SHT_PROGBITS。属性为SHF_ALLOC和SHF_ EXECINSTR。
l .interp:保存程序解释器的路径名。如果文件存在包含此section的可加载的segment,section的属性将包含SHF_ALLOC比特位。否则该比特位是关闭状态。参见第二章。section类型为SHT_PROGBITS。
l .line:保存符号调试用到的行号,指明了源代码和机器码之间的对应关系。其内容未规定。section类型为SHT_PROGBITS。没有属性。
l .note:按照一定格式保存注释信息。参见第二章“注释section”一节。section类型为SHT_NOTE。没有属性。
l .plt:保存过程链接表(procedure linkage table)。参见第二章“过程链接表”一节。section类型为SHT_PROGBITS。
l .relname和.relaname:保存重定位的相关信息。参见后面的“重定位”一节。如果文件存在包含此section的可加载的segment,section的属性将包含SHF_ALLOC比特位,否则该比特位将是关闭状态。通常情况下,name是此重定位section对应的原section的名字。比如一个重定位section对应.text,那么它的名字通常为.rel.text或.rela.text。
l .rodata和.rodata1:保存进程映像中不可写segment里的只读数据。参见第二章“程序头部”一节。section类型为SHT_PROGBITS。属性为SHF_ALLOC。
l .shstrtab:保存section名字的字符串表section。(This section holds section names)。section类型为SHT_STRTAB。没有属性。
l .strtab:保存字符串表,这些字符串通常代表符号表中表项的名字。如果文件存在包含符号字符串表的可加载的segment,此section的属性应该包含SHF_ALLOC比特位,否则该位为关闭状态。section类型为SHT_STRTAB。
l .symtab:保存一个符号表。参见后面“符号表”一节。如果文件存在包含符号表的可加载的segment,此section的属性应该包含SHF_ALLOC比特位,否则该位为关闭状态。section类型为SHT_SYMTAB。
l .text:保存程序中的”文本”,和其他可执行的指令。
Section名字前面的“.”前缀代表此section是系统保留的。如果这些section的功能满足需要,程序可以直接使用它们。程序也可以定义自己的section名字,不要加这个前缀,以避免和系统section冲突。目标文件是允许自定义上面列表之外的section的。一个目标文件可能包含具有相同名字的多个section。
体系结构相关的Section名字具有特定的格式,需要将体系结构的缩写放在section名字前面。缩写应该来自于e_machine中保存的体系结构名。比如.FOO.psect是FOO这个体系结构定义的一个psect section。现有的一些扩展使用它们的历史名称来命名:
1.4
字符串表 String Table
字符串表section(译者:section类型为SHT_STRTAB的section)保存着一些以空字符结尾的字符串。目标文件用这些字符串表示符号名称和section的名称。可以通过索引引用字符串表中的字符串。首字节的索引为0,用来存储一个空字符。同样的,字符串表的末字节也存储一个空字符,从而确保所有的字符串均以空字符结尾。如果字符串索引为0,意味着这是一个没有名字或是一个空名字,这由上下文决定。允许存在空的字符串表section,它的section头部的sh_size成员取值为0,针对空字符串表的非0索引是无效的。
Section头部(Elf32_Shdr)的sh_name成员保存着一个索引,用这个索引可在section 头部字符串表中找到对应的名字,这个表是由ELF 头部(Elf32_Ehdr)中的e_shstrndx成员指定。下图展示了一个25字节长的字符串表:
通过上面的例子可以看出,字符串表的索引可以指向此字符串表section的任一字节。一个字符串可能出现多次,可以引用子字符串,一个字符串可以被引用多次,还可能存在未被引用的字符串。
1.5
符号表 Symbol Table
目标文件的符号表保存的信息,用于定位和重定位一个程序的符号定义和引用。符号表通过索引作为数组的下标,索引为0的表项表示这是符号表的第一个表项,同时还用来表示一个未定义的符号。第一个表项的内容我们稍后介绍。符号表是由一个个符号表项组成的。每个符号表项的结构是Elf32_Sym。
符号表表项的数据结构Elf32_Sym如下图所示:
l st_name:该成员保存目标文件的符号字符串表的索引,符号字符串表中存放着符号名称。如果取值非0,那么该索引对应字符串表中的符号名字。否则,此符号表项没有名称。
注:外部的C符号在C文件和目标文件符号表中的名字是一样的。
l st_value:相关联的符号的值。依赖于上下文,可能是一个绝对值,一个地址,后面会详细介绍。
l st_size:很多符号有大小,比如:一个数据对象的大小就是这个对象所包含的字节数。如果符号没有大小或是大小未知,那么取值为0。
l st_info:指定了符号的类型和约束属性。后面的表格指出了它可能的取值和意义,下面的代码展示了如何操作它的取值。
l st_other:目前取值为0,未定义其语义。
l st_shndx:每个符号表项都在某个section中给出了其定义。这个成员保存着对section 头部表中相关section的索引。就像图1-7所示,某些section的索引代表特殊的含义。
一个符号的约束属性ELF32_ST_BIND(由上图可知是st_info右移4位,因此是st_info的高4位)决定了链接的可见性和行为(A symbol binding determines
the linkage visibility and behavior.)。
l STB_GLOBAL:Global符号对要链接到一起的所有目标文件都是可见的。如果一个文件中定义了一个Global符号,那么另一个文件中无需定义,可直接引用此Global符号。
l STB_WEAK:Weak符号类似于Global符号,只是它约束的定义优先级较低。
l STB_LOPROC与STB_HIPROC之间:此范围内的取值留作处理器相关的语义。
Global和Weak符号主要有2个不同点:
1)
当链接编辑器(liink editor)链接几个可重定位的目标文件时,不允许STB_GLOBAL符号有多个相同名字的定义。然而,如果已经存在一个Global符号的定义,那么一个相同名字的Weak符号的定义不会引发错误。链接编辑器将使用Global符号定义,而忽略Weak符号的定义。类似的,如果存在一个公共符号(比如一个符号,其st_shndx字段为SHN_COMMON),那么一个相同名字的Weak符号的定义不会引发错误。链接编辑器使用公共符号定义,而忽略Weak符号。
2)
当链接编辑器(liink editor)搜索档案库(archive libraries)时,将把包含未定义Global符号定义的档案库文件解压,定义可能是一个Global符号或Weak符号。但是,链接编辑器并不去搜索未定义Weak符号的定义。未识别出的Weak符号值为0。
2)的另一版本翻译:
当链接编辑(liink editor)搜索档案库(archive libraries)时,会提取那些包含未定义Global符号的档案成员,成员的定义可以是Global符号,也可以是Weak符号。链接编辑器不会提取档案成员去解析未定义Weak符号。未能解析的Weak符号值为0。
在所有的符号表中,STB_LOCAL符号在Weak和Global符号之前。As ‘‘Sections’’ above describes,符号表section的sh_info成员保存着符号表中第一个非Local符号的索引。
可以依据符号的类型将符号分类(st_info的低4位)。
l STT_NOTYPE:未指定符号类型。
l STT_OBJECT:这种类型的符号对应一个数据对象,比如变量、数组等。
l STT_FUNC:这种类型的符号对应一个函数或其它可执行的代码。
l STT_SECTION:这种类型的符号对应一个section(与某个section相关)。这种类型的符号表项位于符号表最前面,这种类型的符号表项主要用于重定位,通常具有STB_LOCAL约束。
l STT_FILE:通常,此符号的名称也就是目标文件对应源文件的名称。File类型的符号具有STB_LOCAL约束,它的section索引是SHN_ABS,它的位置在所在源文件中其他STB_LOCAL符号之前。
l STT_LOPROC至STT_HIPROC:此范围间的取值留作处理器相关的语义。
共享目标文件(shared
object file)中STT_FUNC类型的符号有特殊的含义。当目标文件引用一个来自共享目标文件的函数时,链接编辑器自动的为被引用的STT_FUNC符号创建一个过程链接表表项(procedure linkage table
entry)。而共享目标文件中的其他类型的符号,将不能自动的通过过程链接表引用。
如果符号的值(st_value)指向一个section中指定的位置,那么它的section索引成员st_shndx保存着section头部表中的索引。由于section在重定位过程中可能会被移动,符号的值也随之变化,而所有指向该符号的指针则没有变化,仍指向这个符号,即指向该符号在程序中的位置。
下面列出了一些特殊语义的section索引值:(正如前面提到的,目标文件将不会存在这些索引对应的section)
l SHN_ABS:符号是一个绝对值,重定位时不会改变。
l SHN_COMMON:此符号标识一个还没有分配空间的公共块。符号的值(st_value)给出了对齐约束条件,类似于section的sh_addralign成员。这就意味着,链接编辑器将为此符号分配存储空间,地址位于st_value整数倍处。符号的大小代表它需要多少个字节。
l SHN_UNDEF:表示此符号未定义。当链接编辑器将此目标文件和其他文件链接到一起时,如果那个其他文件中定义了这个符号,那么此文件对这个符号的引用将和实际的定义链接到一起。
前面提到过,符号表中索引为0的表项(STN_UNDEF)是保留的:
1.5.1
符号值 Symbol Values
不同类型目标文件的符号表项对st_value成员的解释略有不同。
1)
可重定位文件:如果符号的对应section的索引是SHN_COMMON,那么st_value保存的是符号的对齐约束条件。
2)
可重定位文件:如果是一个已经定义的符号,st_value保存着该符号在对应section中的偏移,即从st_shndx标识的section开始处的一个偏移量。
3)
可执行文件和共享目标文件:为了使文件中的符号对动态链接器(dynamic linker)来说更加有用,st_value保存着一个虚拟地址。section偏移(文件中的解释)让位给虚拟地址(内存中的解释),因为在这种情况下,section偏移计数已经不重要了。
除了上面提到的,符号表的取值(st_value)含义对不同的目标文件来说是类似的,程序也就能够采用高效的方法来访问数据。
1.6
重定位 Relocation。(.text对应.rela.text和.rel.text .data对应.rela.data和.rel.data)
重定位是将符号引用(symbolic references)和符号定义(symbolic definitions)链接到一起的过程。比如,当一个程序调用一个函数,相关的调用指令(the
associated call instruction)在执行时必须把控制权传递到正确的目的地址。换句话说,就是可重定位文件必须包含一些信息,用来描述怎样修改它们的section内容,从而使可执行文件和共享目标文件掌握正确的信息用于创建进程的程序映像(process’s program image)。重定位表项就是这样一些数据。重定位表项的数据结构(Elf32_Rel,Elf32_Rela)如下图所示:
l r_offset:该成员指出应用重定位操作的位置(重定位操作应用于哪个位置),对于可重定位文件,它的值是从section的开始处到被重定位作用的存储单元之间的字节偏移量。对于可执行文件或共享目标文件,它的值是被重定位作用的存储单元的虚拟地址。(译者:这就是重定位涉及的原section)
l r_info:指出被重定位作用的符号在符号表中的索引和重定位的类型。比如,一个调用指令的重定位表项将保存被调用函数的符号表索引,如果索引是STN_UNDEF,即未定义的符号表索引,那么重定位使用0作为该符号的值。重定位的类型是处理器相关的。当程序代码访问重定位表项的重定位类型和符号表索引时,需要将ELF32_R_TYPE宏和ELF32_R_SYM(r_info的高8位)宏分别作用于重定位表项的r_info成员。(译者:这就是重定位涉及的符号表section)
l r_addend:指定一个常量加数(constant addend),用于计算存储到可重定位字段的值。
前面图1-20中表明,只有Elf32_Rela类型的表项包含一个显式的加数。Elf32_Rel类型的表项在需要修改的位置存储一个隐式的加数。依据处理器的体系结构,可能有必要选取其中的一种方式。因此,对于一种特定的机器而言,可能仅采用其中一种方式,或是两者均采用,这与上下文有关。
一个重定位section引用到另外两个section:符号表section和要修改的原section。Section头部的sh_info和sh_link成员在“Sections”一节中介绍过了,指定了这些联系。不同的目标文件对重定位表项r_offset成员的解释略有不同。
1)
在可重定位文件中,r_offset保存了section偏移。重定位section描述了如何修改此文件中的原section,重定位偏移指定了在原section中的一个存储单元。
2)
在可执行文件和共享目标文件中,r_offset保存了虚拟地址,以使这些文件的重定位表项对动态链接器更有用,section偏移(文件中的解释)让位于虚拟地址(内存中的解释)。
不同类型的目标文件对于r_offset有不同的解释,这就使相关的程序能够更方便的访问这些文件。重定位类型的含义对不同文件来说是一致的。
1.6.1
重定位类型
重定位表项规定了如何改变下面的指令和数据字段(比特位下标标示在图框的下角)。
l word32:指定了一个32位的占用4个字节的任意字节对齐的类型。采用32位Intel架构的字节序。(译者:Intel架构是小端字节)
下面举一个转换重定位文件到可执行文件或共享目标文件的例子。从概念上来讲,链接编辑器链接一个或多个可重定位文件形成一个输出文件时,它首先决定如何合并和定位输入的文件,然后更新符号值,最后进行重定位。重定位作用于可执行文件和共享目标文件时,过程与之类似,并且输出同样的结果。下面的描述使用如下表示法:
l A:用于计算重定位字段的值的加数。This means the
addend used to compute the value of the relocatable field。
l B:表示在执行期间,被加载进内存的共享目标的基地址(base address)。通常,共享目标文件的虚拟基地址为0,但执行时的地址将与之不同。
l G:表示全局偏移表的一个偏移,重定位表项中符号的地址在执行期间会驻留在这个表项中。(全局偏移表中保存的是重定位表项中符号的地址)参见第二章“全局偏移表”一节。
l GOT:全局偏移表的地址。参见第二章“全局偏移表”一节。
l L:过程链接表表项的位置(section偏移或地址),该表项对应一个符号。过程链接表表项把一个函数调用重定位到正确的目的地址。链接编辑器创建最初的过程链接表,而动态链接器在执行期间修改它的表项。参见第二章“过程链接表”一节。
l P:重定位后的存储单元的位置(section偏移或地址,通过r_offset计算)。
l S:符号的值,该符号的索引驻留在重定位项中。This means the value of the symbol whose index resides in the
relocation entry。
重定位表项r_offset的值指定了受影响存储单元首字节的偏移或虚拟地址。重定位类型指定了要改变哪些位以及如何计算它们的值。SYSTEM V体系架构只使用了Elf32_Rel类型的重定位表项,重定位作用的字段保存了加数。在所有情况下,加数和计算结果使用相同的字节序。
有些重定位类型的语义不仅仅是单纯的计算:
l R_386_GOT32:该重定位类型计算从全局偏移表(global offset table)的基址到符号所在的全局偏移表表项的距离,并且通知链接编辑器建立全局偏移表。(G+A-P)
l R_386_PLT32:该重定位类型计算符号的过程链接表表项地址,并且通知链接编辑器构建过程链接表。(L+A-P)
l R_386_COPY:链接编辑器创建此重定位类型是用于动态链接的。它的r_offset成员对应一个可写segment中的位置。符号表索引指定了一个在当前目标文件和共享目标文件中都存在的符号。在执行期间,动态链接器拷贝与共享目标的符号相关的数据到偏移指定的位置。(none)
l R_386_GLOB_DAT:用于给指定符号的地址设置一个全局偏移表表项。该特殊重定位类型确定符号和全局偏移表项之间的对应关系。(S)
l R_386_JMP_SLOT:链接编辑器创建此重定位类型是用于动态链接的,它的偏移给出了过程链接表表项的位置。动态链接器通过修改过程链接表表项,把控制权传递给指定符号的地址,参见第二章“过程链接表”一节。(S)
l R_386_RELATIVE:链接编辑器创建此重定位类型是用于动态链接的,它的偏移成员给出了共享目标中的一个位置,这个位置包含了一个表示相对地址的值。动态链接器通过把共享目标被加载的虚拟地址与相对地址相加,计算出对应的虚拟地址。此重定位类型的重定位表项必须把符号表索引置为0。(B+A)
l R_386_GOTOFF:此类型的重定位计算符号值和全局偏移表地址的差值,并且通知链接编辑器构建全局偏移表。(S+A-GOT)
l R_386_GOTPC:此类型类似于R_386_PC32。只是它在计算中使用全局偏移表的地址。这种类型的重定位引用的通常是_GLOBAL_OFFSET_TABLE_DE类型的符号,并且通知链接编辑器构建全局偏移表。(GOT+A-P)
2
程序加载与动态链接
2.1
简介
第2章介绍了目标文件信息和系统运行程序所需的动作,这里的有些信息适用于所有系统,有些则是处理器相关的。
可执行文件和共享目标文件都是静态的表示程序。为了执行这样的程序,系统使用这些文件来创建动态的程序表示,即进程映像。一个进程映像通过segment保存文本、数据、堆栈等。本章主要讨论下面的内容:
1)
程序头部:这一节是第1章的补充,介绍了目标文件结构中与程序执行直接相关的部分,涉及的主要数据结构是程序头部表(program header
table),保存着文件中的segment映像,以及创建程序内存映像时使用的其他一些必要信息。
2)
程序加载:系统必须将目标文件加载到内存中才能使程序运行。
3)
动态链接:系统加载完程序后,必须解析目标文件中所有的符号引用,才能组成一个完整的进程映像,从而构建一个进程。
注:ELF常量的命名有相关的规范,表示特殊的处理器范围。比如DT_和PT_这样的名字,如果需要针对特定处理器进行扩展,那么就需要和处理器的名字联合起来。比如DT_M32_SPECIAL。有些已经存在的处理器扩展名不符合这个规范,但也是被支持的:
2.2
程序头部 Program Header
可执行文件和共享目标文件的程序头部表(program header table)是一个结构数组,每个元素描述了一个segment,以及系统准备执行程序时所需的其他信息。目标文件的segment包含一个或多个section,后面会介绍。程序头部只对可执行文件和共享目标文件有意义。目标文件通过ELF头部中的e_phentsize成员和e_phnum成员指定了程序头部表的大小,参见第一章“ELF头部”一节。Elf32_Phdr结构如下所示:
l p_type:说明了这个数组元素描述了一个什么种类的segment,以及如何解释这个数组元素的信息。Type的取值和含义后面会介绍。
l p_offset:从文件开头到segment的第一个字节的偏移。
l p_vaddr:segment第一个字节在内存中的虚拟地址。
l p_paddr:在某些系统上此成员保存segment的物理地址。由于System V忽略应用程序的物理地址,这个成员的取值在可执行文件和共享目标文件中没有定义。
l p_filesz:segment在文件映像中的字节数,可能为0。
l p_memsz:segment在内存映像中的字节数,可能为0。
l p_flags:与segment相关的标志。后面会介绍。
l p_align:后面“程序加载”一节会介绍,可加载的进程segment其p_vaddr和p_offset模页面大小(page size)必须是同余的(译者:p_vaddr和p_offset除以页面大小,余数必须相同)。这个成员给出了segment在内存和文件中的对齐值。取值为0和1表示没有对齐方面的要求。否则,p_align应该是一个正整数,并且是2的幂数。p_vaddr和p_offset应该是模p_align同余的。
有的表项描述了进程segment,其他的则描述了一些进程映像中并不包含的辅助信息。Segment表项除了有明确规定的,可以采用任意的顺序。下面列出了segment类型的取值,其他的取值留作以后使用。
l PT_NULL:表示此数组元素未被使用,其他成员的值未定义。这个类型使程序头部表可以包含一些无关的表项。
l PT_LOAD:表示这是一个可加载的segment,通过p_filesz和p_memsz描述。目标文件中的字节将被映射到内存segment的开始部分去。如果segment的内存大小(p_memsz)大于文件大小(p_filesz),在segment已初始化数据区后面“额外”多出来的部分将填充值为0的字节。文件大小不可以大于内存大小。程序头部表中可加载的segment表项依据p_vaddr成员按升序排列。
l PT_DYNAMIC:保存了动态链接的相关信息。参见后面“动态section”一节。
l PT_INTERP:指定了一个以空字符结尾的字符串的位置和长度,这个字符串是需调用的解释器的路径。此类型的segment只对可执行文件有效(虽然也可能存在于共享目标文件中)。在一个文件中只能有一个此类型的segment,如果有,那么它必须位于所有可加载的segment表项之前。更多信息参见“程序解释器”一节。
l PT_NOTE:指定了辅助信息的位置和大小。更多信息参见“注释section”一节。
l PT_SHLIB:保留的,未定义语义。包含此类型segment头部的程序不符合ABI规范。
l PT_PHDR:如果有,指定了程序头部表自身在文件和内存映像中的位置和大小。文件中只能有一个此类型的程序头部。而且,仅当程序头部表是程序的内存中映像的一部分时,才会出现。此类型的表项必须位于所有表项之前。更多信息参见后面的“程序解释器”一节。
l PT_LOPROC至PT_HIPROC:此范围内的取值留作处理器相关的语义。
注:除非在其他地方有特殊的需求,所有程序头部的segment类型都是可选的,即文件的程序头部表可能只包含与其内容有关的程序头部。
2.2.1
基地址 Base Address。
可执行文件和共享目标文件有一个基地址,这个地址是程序目标文件的内存映像的起始虚拟地址。基地址的一个作用是在动态链接时重定位程序的内存映像。
一个可执行文件或共享目标文件的基地址是在执行期间由三个值计算出来的:内存加载地址、最大页尺寸(maximum page size)、程序的可加载segment的起始虚拟地址。正如“程序加载”一节中描述的那样,程序头部(program
header)中的虚拟地址(p_vaddr)可能并不代表程序的内存映像中实际的虚拟地址。为了计算基地址,需要首先确定与PT_LOAD类型的segment p_vaddr对应的内存地址,然后截去地址的低位部分,使地址为最接近的最大页面大小的整数倍的地址,这就是基地址。依据加载进内存的文件的类型,内存地址可能与p_vaddr的值匹配,也可能不匹配。
第1章中介绍的.bss section是SHT_NOBITS型的,虽然它不占据文件空间,但对segment在内存中的映像还是有影响的。通常,这些未初始化的数据驻留在segment的尾部,因此使得程序头部中的p_memsz比p_filesz大。
2.2.2
注释section Note Section
有时厂商或系统构建者需要用特定的信息注释目标文件,程序利用这些信息进行一致性和兼容性等检查。SHT_NOTE类型的section和PT_NOTE类型的程序头部就是用于这个目的。Section与程序头部中的注释信息由任意数目的表项组成,每个表项都是与目标处理器格式相关的4字节数组。下图展示了一种注释信息的组织结构,但只是一个例子,并不是规范。
l namesz和name:name字段的前namesz个数的字节包含一个以空字符结尾的字符串,代表表项的所有者和创建者。没有正式的机制来避免名称冲突。按照惯例,厂商使用他们自己的名称作为标识符,比如“XYZ计算机公司”。如果不包含name字段,namesz为0。如果需要确保后面的desc字段是4字节对齐的,name字段会包含填充字节,填充字节的长度不计算进namesz。
l descsz和desc:desc字段的前descsz个数的字节包含注释的描述信息。ABI没有规定描述信息的内容。如果不包含描述信息,descsz为0。如果需要确保下一条注释表项是4字节对齐的,desc字段会包含填充字节,填充字节的长度不计算进descsz。
l type:指定了如何解释描述信息。每个创建者控制它自己的类型,所以相同的类型值可能会有不同的解释。因此,程序必须同时识别name和type字段,才能理解描述信息的含义。目前type的取值必须是非负的。ABI没有规定描述信息的含义。
举例说明,下图中的注释segment包含2个表项:
注:系统保留了空名字(namesz==0)和0长度的名字(name[0]==’\0’ ) 的注释信息,但目前没有定义其对应的类型。所有其他的名称都必须至少包含一个非空字符。
注:注释信息是可选的。注释信息的存在不会影响到程序的ABI规范,也不会影响到程序的执行行为。否则的话,程序就存在未定义的行为,是不符合ABI规范的。
2.3
程序加载 Program Loading
系统创建或增补一个进程映像时,只是逻辑上拷贝文件中的segment到虚拟内存中的segment。系统何时、是否在物理上访问这个文件,取决于程序的执行行为,比如系统加载等。执行进程的过程中,只有引用到逻辑页时才会请求一个物理页。进程中通常会有很多未引用的页,这种延迟物理读的方式就将这些未引用的页排除了,提高了系统的性能。在实际应用中,如果想实现这种方式,就必须使可执行文件和共享目标文件的segment映像在文件中的偏移及其虚拟地址是模页面大小同余的。
对SYSTEM V体系结构而言,其segment的虚拟地址和文件偏移是模4KB(0x1000)(或更大的2的幂数)同余的。由于4KB是最大的页面尺寸,目标文件适用于分页,而不用考虑物理页面的大小。
例子中文本segment和数据segment的文件偏移、虚拟地址都是模4KB同余的,但是有4个页面(依据页面大小和系统文件块大小)包含的不全是文本或数据。
1)
第一个文本页包含ELF头部,程序头部表和其他信息。
2)
最后一个文本页包含一份数据segment开始部分的拷贝。
3)
第一个数据页包含一份文本segment结尾部分的拷贝。
4)
最后一个数据页可能包含与进程运行不相关的文件信息。
逻辑上,系统使每个segment看起来都好像是完整和独立的,以便为它们所在的内存赋予访问权限。这就需要调整segment的地址,以确保地址空间中的每个逻辑页有一组独立的权限。在上面的例子中,文件中保存文本结尾和数据开头的区域将被映射两次,一个虚拟地址对应文本,另一个不同的虚拟地址对应数据。
数据segment的结尾处需要为未初始化的数据做特殊的处理,系统将这部分数据的值置为0。如果文件的最后一个数据页包含其他信息,这些信息不会映射到逻辑内存页中,必须将内存页剩余部分的数据置为0,这一点与可执行文件允许有未知的内容不同。其他三个页面的不纯信息在逻辑上并不是进程映像的一部分,系统是否需要除去其他三个页面中的不纯信息(Impurities)未作规定。这个程序对应的内存映像如下图所示,假设页面大小是4KB(0x1000)。
可执行文件和共享目标文件加载segment时有所不同,通常可执行文件的segment包含的是绝对代码。为了使进程正确执行,segment必须驻留在可执行文件中指定的虚拟地址。因此,系统不会改变p_vaddr的值,直接将它作为虚拟地址。
而共享目标文件的segment包含的是位置无关的代码,这就意味着,虽然其虚拟地址在不同进程间是不同的,但却不会导致执行行为无效。尽管系统为每个进程单独选择虚拟地址,它仍然维护这些segment的相对位置。由于位置无关代码在segment中使用相对地址,内存虚拟地址间的偏移必须与文件中的虚拟地址的偏移保持一致。下图举了不同进程中共享目标虚拟地址分配的一个例子,展示了相对位置的不变性,也展示了如何计算基地址。
2.4
动态链接
2.4.1
程序解释器 Program Interpreter
可执行文件可能包含一个PT_INTERP类型的程序头部。在exec(BA_OS)期间,系统从PT_INTERP segment中检索一个路径名,然后利用解释器文件的segment创建初始的进程映像。也就是说系统并没有使用最初的可执行文件的segment映像,而是创建了一个解释器的内存映像,然后解释器负责从系统接收控制权并为应用程序提供运行环境。
解释器可通过两种方式获得控制权。第1种方式,它(解释器)得到一个文件描述符以读取可执行文件,从文件的开始部分开始读。使用这个文件描述符读取、映射可执行文件的segment到内存中。第2种方式,通过解析可执行文件格式,系统可能已经将可执行文件加载进内存,而不是将已经打开的文件描述符交给解释器。由于文件描述符可能存在异常,解释器的初始进程状态也就与可执行文件已接收的状态保持一致。解释器本身可能不需要另外一个解释器。解释器可能是一个共享目标文件或可执行文件:
1)
共享目标文件(通常情况):以位置无关方式加载,即不同的进程使用不同的地址。系统在mmap(KE_OS)和相关服务使用的动态segment区域创建了解释器的segment。因此共享目标解释器通常不会和要处理的可执行文件的segment发生地址冲突。
2)
可执行文件:被加载到固定地址,系统使用程序头部表中的虚拟地址创建它的segment。因此,可执行文件解释器的虚拟地址可能和要处理的可执行文件的地址发生冲突,解释器负责解决这个冲突。
2.4.2
动态链接器 Dynamic Linker
当用动态链接技术构建一个可执行文件时,链接编辑器会在可执行文件中添加一个PT_INTERP类型的程序头部,告知系统调用指定的动态链接器作为程序解释器。
注:系统提供动态链接器的位置是处理器相关的。
Exec(BA_OS)和动态链接器相互配合为程序创建了进程映像,步骤如下:
1)
把可执行文件的内存segment添加到进程映像中。
2)
把共享目标的内存segment添加到进程映像中。
3)
重定位可执行文件及其共享目标。
4)
如果动态链接器收到了用于读取可执行文件的文件描述符,则关闭它。
5)
把控制权传递给程序,这看起来好像是程序已经直接从exec(BA_OS)接收了控制权。
链接编辑器构建了多种数据,用来帮助动态链接器处理可执行文件和共享目标文件。参见“程序头部”一节。这些数据驻留在可加载的segment中,执行程序时可以访问它们。(再一次重申,调用这些segment的内容是处理器相关的,参见处理器的说明文档以获得更完整的信息。)
l SHT_DYNAMIC类型的.dynamic section保存这些数据。此section的存放的是动态链接表,保存动态链接时用到的地址信息。
l SHT_HASH类型的.hash section保存一张符号哈希表。
l SHT_PROGBITS类型的.got和.plt section保存两张独立的表:全局偏移表和过程链接表。后面将讲解动态链接器如何使用、更新这些表,从而创建目标文件的内存映像。
由于每个符合ABI规范的程序都引用了共享目标库中的基本系统服务,动态链接器也就参与了每个符合ABI规范的程序的执行过程。(译者:每个符合ABI规范的程序都需要动态链接器才能完成对基本系统服务的调用)
正如“程序加载”一节中关于处理器相关补充内容所描述的那样,共享目标在内存中的虚拟地址可能与文件程序头部表中的地址不同。动态链接器会重定位内存映像,在应用程序获得控制权之前更新绝对地址。如果加载库时使用的是程序头部表中指定的地址,那么这些绝对地址就能够正常工作,不过通常不是这样。
如果进程的环境变量[参见exec(BA_OS)]包含名为LD_BIND_NOW的变量,并且值非空,动态链接器在将控制权交给程序之前,就会执行所有重定位操作。比如,下面列出的环境变量都指定了这个行为。
l LD_BIND_NOW=1
l LD_BIND_NOW=on
l LD_BIND_NOW=off
否则的话,如果环境变量中不包含LD_BIND_NOW,或者它的值为空,那么将允许动态链接器惰性的(lazily)处理过程链接表。从而避免了为那些还没有调用到的函数解析符号和重定位。参见“过程链接表”一节。
2.4.3
动态section Dynamic Section
如果一个目标文件参与了动态链接,它的程序头部表(program header table)将有一个PT_DYNAMIC类型的头部。其对应的segment包含.dynamic section。一个特殊的符号“_DYNAMIC”标识出了这个section,它包含一个下图所示Elf32_Dyn结构的数组。
这种类型的数据对象,d_tag决定d_un中哪个成员有效:
l d_val:有不同含义的整数值。
l d_ptr:程序的虚拟地址。前面提到过,在执行期间文件的虚拟地址可能和内存的虚拟地址不同。当解释包含在动态链接表表项中的地址(d_ptr)时,动态链接器通过文件中的原始地址和内存基地址计算实际地址。出于一致性方面的考虑,文件不包含用于修正动态链接表项中的地址的重定位表项。
动态链接表的表项类型,通过d_tag成员保存。下表列出了可执行文件和共享目标文件关于d_tag的相关要求。如果标示d_tag为“mandatory”(强制),那么动态链接器链接符合ABI规范的文件时,动态链接表必须包含一个该类型的表项。与之相对的,“optional”意味着该类型的表项是可选的。
l DT_NULL:一个DT_NULL类型的表项标识出了_DYNAMIC数组的尾部。
l DT_NEEDED:保存了字符串表中一个以空字符结尾的字符串的偏移,即所需的库的名字。这个偏移是保存在DT_STRTAB表项中的表的索引,参见“共享目标依赖”一节。动态链接表可能包含多个此类型的表项,这些表项的相对顺序是有意义的,但是它们与其他类型表项的相对顺序是无意义的。
l DT_PLTRELSZ:保存与过程链接表相关的重定位表项的总大小,以字节为单位。如果存在一条DT_JMPREL类型的表项,那么就必须有一条DT_PLTRELSZ类型的表项与之对应。
l DT_PLTGOT:保存过程链接表、全局偏移表的地址。更多信息请参阅处理器相关的文档。
l DT_HASH:保存符号哈希表的地址,参见“哈希表”一节。该哈希表与DT_SYMTAB中指定的符号表相对应。(This hash table refers
to the symbol table referenced by the DT_SYMTAB element.)
l DT_STRTAB:保存一个字符串表的地址,参见第1章。表中存放了符号名、库名、其他的字符串。
l DT_SYMTAB:保存符号表的地址,参见第1章。32位文件对应Elf32_Sym类型的表项。
l DT_RELA:保存一个重定位表的地址,参见第1章。重定位表中的表项存放显式的加数(explicit addends),比如32位文件的Elf32_Rela数据类型。一个目标文件可能包含多个重定位section,当为可执行文件和共享目标文件创建重定位表时,链接编辑器将这些section链接到一起,组成一张重定位表。虽然目标文件中的section仍然是各自独立的,但是动态链接器却只看到一张重定位表。当动态链接器为可执行文件创建进程映像时,或是将一个共享目标添加到进程映像中时,它读取重定位表,并执行重定位操作。如果存在此类型的表项,那么必须同时包含DT_RELASZ和DT_RELAENT类型的表项。当一个文件包含重定位操作时,动态链接表中必须包含DT_RELA和DT_REL中的一种(也允许同时存在,但不是必须的)。
l DT_RELASZ:保存DT_RELA类型重定位表的总大小,以字节为单位。
l DT_RELAENT:保存DT_RELA类型重定位表的表项的大小,以字节为单位。
l DT_STRSZ:保存字符串表的大小,以字节为单位。
l DT_SYMENT:保存符号表表项的大小,以字节为单位。
l DT_INIT:保存初始化函数的地址。参见后面的“初始化和终止函数”一节。
l DT_FINI:保存终止函数的地址。参见后面的“初始化和终止函数”一节。
l DT_SONAME:保存字符串表中以空字符结尾的字符串的偏移,给出了共享目标的名字。此偏移是保存在DT_STRTAB表项中的表的索引。更多信息参见后面的“共享目标依赖”一节。
l DT_RPATH:保存字符串表中以空字符结尾的检索库、检索路径字符串的偏移,将在“共享目标依赖”一节中讨论。此偏移是保存在DT_STRTAB表项中的表的索引。
l DT_SYMBOLIC:存在于共享目标库中的这个表项,将改变动态链接器对引用自此库中的符号的解析算法。动态链接器将从共享目标本身开始查找符号,而不是在可执行文件中查找。如果共享目标中没有找到符号的引用,动态链接器才像往常那样继续查找可执行文件和其他共享目标。
l DT_REL:与DT_RELA类似,不同之处在于它对应的表存有隐式的加数。比如32位文件的Elf32_Rel数据类型。如果存在此类型的表项,还必须同时包含DT_RELSZ和DT_RELENT类型的表项。
l DT_RELSZ:保存DT_REL类型重定位表的总大小,以字节为单位。
l DT_RELENT:保存DT_REL类型重定位表的表项的大小,以字节为单位。
l DT_PLTREL:指定了过程链接表所引用的重定位表项的类型。根据具体情况,d_val成员保存DT_REL或DT_RELA。过程链接表中的所有重定位必须采用同样类型的重定位。
l DT_DEBUG:用于调试。它的内容在ABI中并无规定。访问这个表项的程序不符合ABI规范。
l DT_TEXTREL:如果没有这个成员,意味着不存在可以修改不可写segment的重定位表项,segment权限在程序头部表中指定。如果存在此类型的表项,意味着存在一或多个重定位表项,它们可能需要修改不可写的segment,动态链接器也就能够做出相应的准备。
l DT_JMPREL:如果存在此类型的表项,该表项的d_ptr成员保存仅与过程链接表相关的重定位表项的地址。之所以区分出这些重定位表项,是为了如果动态链接器使用了惰性绑定,那么在执行初始化时可以忽略它们。如果存在此类型的表项,必须同时存在相关的DT_PLTRELSZ和DT_PLTREL类型的表项。
l DT_LOPROC至DT_HIPROC:此范围内的取值留作处理器相关的语义。
除了动态链接表末尾的DT_NULL表项,以及顺序相关的DT_NEEDED类型的表项,其他表项可以任意排列。表中没有列出的类型取值是保留的。
2.4.4
共享目标依赖 Shared Object
Dependencies
当链接编辑器处理一个档案库(archive
library)时,它提取库成员并将其复制到输出的目标文件中。在执行时即可获得这些静态的链接服务,而不用调用动态链接器。共享目标也提供这样的服务,但是为了执行,动态链接器必须把正确的共享目标文件链接到进程映像。因些,可执行文件和共享目标文件描述了它们特有的依赖。
当动态链接器为目标文件创建内存segment时,依赖(保存在动态链接表DT_NEEDED类型的表项中)指出了需要什么样的共享目标来支持程序的业务。通过链接所引用的共享目标和它们的依赖,动态链接器构建了一个完整的进程映像。当解析符号引用时,动态链接器使用广度优先算法查找符号表。即首先查找可执行程序本身的符号表,然后查找DT_NEEDED表项(按顺序排列)的符号表,然后再查找下一级的DT_NEEDED表项的符号表,如此类推。共享目标文件对于进程来说必须是可读的,不需要其他的权限。
注:即使多次引用了一个共享目标,动态链接器也只将目标链接到进程一次。
表项中保存的依赖的名字不是DT_SONAME字符串的拷贝,就是共享目标文件路径名的拷贝,这些共享目标文件用于构建目标文件。比如,如果链接编辑器创建了一个可执行文件,使用一个共享目标,对应一个DT_SONAME类型lib1的表项,和另外一个共享目标库,路径名是/usr/lib/lib2,可执行文件的依赖表项中将包含lib1和/usr/lib/lib2。
如果共享目标名字的任何位置有一个或多个斜线(/)字符,比如前面用到的/usr/lib/lib2或其他目录名、文件名,动态链接器直接用这个字符串作为路径名。如果没有斜线,比如前面用到的lib1,依据下面介绍的优先级,有三种方式用于指定共享目标的搜索路径:
1)
第一种方式:动态链接表中DT_RPATH类型的表项会给出一个包含一系列目录的字符串,这些字符串有冒号(:)分开。比如:/home/dir/lib:/home/dir2/lib:告诉动态链接器首先搜索第一个目录/home/dir/lib、然后是/home/dir2/lib、最后是当前目录来寻找依赖。
2)
第二种方式:在进程的环境变量[参见exec(BA_OS)]中有一个LD_LIBRARY_PATH的变量可能包含像上面那样的一系列目录,可以在后面加上分号(;),然后是另一个目录列表。比如:
n LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:
n LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib:
n LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:;
所有的LD_LIBRARY_PATH目录都在DT_RPATH保存的目录之后检索。虽然一些程序(比如链接编辑器)对待分号前后路径名的方式不同,动态链接器不是这样的。然而,动态链接器仅在前面描述的语义下,才接受分号这个符号。
3)
第三种方式:如果前面这两组目录都没有成功定位所需的库,动态链接器将搜索/usr/lib。
注:出于安全方面的考虑,对于设置用户和组ID的程序,动态链接器将不会搜索环境变量(比如LD_LIBRARY_PATH)。但它还是要搜索DT_RPATH保存的目录和/usr/lib。
2.4.5
全局偏移表
GOT,Global Offset Table。通常,位置无关代码不能包含绝对虚拟地址。但全局偏移表却保存着绝对地址,可直接使用这些地址而不会危及位置无关性和程序文本的共享性。程序通过位置无关的代码访问全局偏移表并提取绝对地址,从而把位置无关的引用转换到绝对位置。
最初,全局偏移表保存着重定位表项请求的信息,参见第一章“重定位”一节。在系统为可加载的目标文件创建内存segment后,动态链接器将处理重定位表项,一些R_386_GLOB_DAT类型的表项涉及到全局偏移表。动态链接器确定相关符号的值,计算它们的绝对地址,并为其对应的内存表表项设置正确的值。尽管当链接编辑器构建一个目标文件时绝对地址是未知的,动态链接器却知道所有内存segment的地址并能据此计算出符号的绝对地址。
如果程序需要直接访问符号的绝对地址,那么这个符号将对应一个全局偏移表的表项。由于可执行文件和共享目标文件都包含各自的全局偏移表,一个符号的地址可能出现在几个表中。动态链接器在将控制权交给进程映像中的任何代码之前,已经重定位了所有全局偏移表,从而确保了执行期间绝对地址是可用的。
全局偏移表中索引为0的表项留作保存动态链接表的地址,通过符号_DYNAMIC标识。这就允许一个程序,比如动态链接器,无需处理重定位表,即可访问自己的动态链接表。这对动态链接器来说非常重要,因为它必须不依赖于其他程序重定位它的内存映像,就能够初始化自己。在32位的Intel架构上,全局偏移表索引为1和2的表项也是保留的,后面“过程链接表”一节介绍了它们。
系统在不同的程序中可能为相同的共享目标选择不同的内存segment地址,甚至可能为同一个程序中引用同一个库的不同执行位置选择不同的地址。然而,一旦进程映像构建完毕,内存segment地址就不会改变。只要进程存续,它的内存segment就会驻留在一个固定的虚拟地址。
全局偏移量表的格式和解释是处理器相关的。对于32位的Intel架构而言,可使用符号_GLOBAL_OFFSET_TABLE_访问全局偏移表。
符号_GLOBAL_OFFSET_TABLE_可能驻留在.got section的中间位置,允许通过负的或正的数组下标来访问它。
2.4.6
过程链接表
PLT,Procedure Linkage Table。就像全局偏移表把位置无关地址重定向到绝对位置一样,过程链接表把位置无关的函数调用重定向到绝对位置。链接编辑器不能解析从一个可执行或共享目标文件到另一个文件的执行传递(如函数调用)。因此,链接编辑器只能让程序把控制权传递到过程链接表中的表项中。在SYSTEM V架构上,过程链接表驻留在共享文本区,但使用文件私有的全局偏移表中的地址。动态链接器首先决定目的地的绝对地址,然后修改内存映像中全局偏移表的绝对地址。动态链接器因此能够将表项重定向,而不会危及程序文本的位置无关性和共享性。可执行文件和共享目标文件包含各自独立的过程链接表。
注:如上图所示,对于绝对代码和位置无关代码,过程链接表指令使用不同的操作数寻址方式。然而,他们面向动态链接器的接口是相同的。
按照下面的步骤,动态链接器和程序相互配合,通过过程链接表和全局偏移表解析符号的引用。
1)
第一次创建程序的内存映像时,动态链接器将全局偏移表的第二、三条表项设为特殊值。后面的步骤解释了这些值的含义。
2)
如果过程链接表是位置无关的,全局偏移表的地址必须在%ebx中。进程映像中的每个共享文件都有它自己的过程链接表,过程链接表表项只能向同一个目标文件内的另一个表项传递控制权。调用函数负责在调用过程链接表表项之前设置全局偏移表的基地址寄存器。
3)
假定程序调用了name1,把控制权传输到标签.PLT1。
4)
第一条指令跳转到全局偏移表中name1表项的地址。初始时,全局偏移表保存的是下面pushl指令的地址,而不是name1的真正地址。
5)
程序将重定位偏移(offset)入栈。重定位偏移是重定位表的一个32位、非负的字节偏移。这个重定位表项将会是R_386_JMP_SLOT类型,而它保存的偏移值将指定先前jmp指令使用的全局偏移表的表项。重定位表项还包含一个符号表索引,告知动态链接器所引用符号的名字,我们这个例子中是name1。
6)
在重定位偏移入栈后,程序跳转到.PLT0,也就是过程链接表的第一个表项。pushl指令把第二个过程链接表表项(got_plus_4或4(%ebx))的值入栈。因此给了动态链接器一个字的辨别信息。然后程序跳转到全局偏移表第三个表项(got_plus_8或8(%ebx))中的地址,也就将控制权传递到了动态链接器。
7)
当动态链接器接收到控制权,它展开堆栈,查看指定的重定向表项,找出符号值,在全局偏移表表项中存储name1的真实地址,然后把控制权传给请求的目的地址。
8)
过程链接表表项再一次执行时将直接传送name1,而不用再次调用动态链接器,也就是说.PLT1处的jmp指令会直接传递到name1,而不会到后面的pushl指令。
LD_BIND_NOW这个环境变量可以改变动态链接器的行为,如果它的值非零,动态链接器在把控制权传给程序之前计算过程链接表表项。也就是说,动态链接器在进程初始化时处理R_386_JMP_SLOP类型的重定位表项。否则,动态链接器不会计算过程链接表表项,直到表项第一次执行时才进行符号解析和重定位。
注:惰性绑定(lazy
binding)通常提升了应用程序总体的性能,因为动态链接器不会提前链接未使用的符号。然而,在两种情况下惰性绑定不适用于某些应用程序。第一种,由于动态链接器需要中途接管控制权,以解析符号,所以共享目标程序的首次引用比后续调用所需的时间要长,某些应用程序不能忍受这种不可预测性。第二种,如果发生了错误,动态链接器无法解析符号,并将终止这个调用。在惰性绑定下,这可能发生任意次,某些应用程序同样无法忍受这一不可预测性。如果关闭惰性绑定,动态链接器可以使错误发生在进程初始化阶段,在应用程序得到控制权之前。
2.4.7
哈希表
Hash Table。哈希表由Elf32_Word类型的对象组成,能加快对符号表的访问。下图是一个哈希表的组织方式,但只是一个例子,并不是规范的一部分。
bucket数组包含nbucket个条目,而chain数组包含nchain个条目,索引从0开始。bucket和chain都保存符号表的索引。chain表项的索引与符号表对应。符号表的表项数应等于nchain,所以使用符号表索引也可访问chain表表项。哈希函数(后面会介绍)接收一个符号名并返回一个值,这个值用于计算bucekt索引。因此,如果哈希函数为某个符号名返回值x,bucket[x%nbucket]给出一个索引y,用于访问符号表和chain表。如果符号表表项不是想要的,chain[y]给出具有相同哈希值的下一个符号表表项。可以沿着chain链一直查找,直到找到保存有所要名字的符号表表项,或者值为STN_UNDEF的chain表项。
2.4.8
初始化和终止函数
Initialization and Termination Functions。动态链接器构建了进程映像并执行重定位后,每个共享目标都有机会执行一些初始化代码。这些初始化函数的调用没有特定的顺序,但所有共享目标的初始化都发生在可执行文件获得控制权之前。
类似的,共享目标可能还有终止函数,通过atexit(BA_OS)机制在进程开始了它的终止序列之后被执行。同样的,动态链接器调用终止函数的顺序也不定。
共享目标通过动态链接表中的DT_INIT和DT_FINI表项定义它们的初始化和终止函数,参见前面的“动态section”一节。通常,这些函数的代码驻留在.init和.fini section,参见第一章“Sections”一节。
注:虽然atexit(BA_OS)通常会执行进程终止函数,但并不保证在进程死掉之前一定会执行终止函数。特殊情况下,如果进程调用的是_exit[参见exit(BA_OS)],或者进程因为收到一个不是它捕获的又不能忽略的信号而死掉,都不会执行终止函数,。
3
C库
3.1
C库
C库:libc,包含了libsys中所有的符号,而且包含下面两张表列出的例程。第一张表列出了ANSI C标准中的例程:
除此之外,libc还存放了如下服务:
+符号标识的程序在SVID Issue3 Level2中,因此也在ABI Level2中。
除了前面With
Synonyms表列出的符号之外,名叫name的表项存在_name格式的替代名,即名字开头以一个下划线开始,并没有列出这样的表项。举例来说,libc同时包含getopt和_getopt。
前面列出的例程中,有些地方可能没有定义下面的程序:
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的信息。更多信息参见“System Data Interfaces”。
3.2
全局数据符号
Global Data Symbols。Libc库需要一些全局外部数据符号以使它的例程能够正常工作。libsys库需要的所有这些数据符号必须通过libc提供,还有下表中列出的数据符号。
要想得到这些符号代表的数据对象的正式声明,参阅System V Interface Definition, Third Edition或是相关处理器System V ABI说明文档的第六章“Data Definitions”一节。
下表中name、_name格式的表项,每一对符号都表示同样的数据。提供下划线标识的替代名是为了满足ANSI C标准。
评论
发表评论