1 简介(Introduction)
ELF(Executable and Linkable Format),即可执行与可链接格式,是可执行文件(Executable Files)、可重定位文件(Relocatable Files)、共享目标文件(Shared Object File)和核心转储(Core Dumps)的通用标准文件格式。首次发布在名为System V Release 4(SVR4)的Unix操作系统版本的应用程序二进制接口(Application Binary Interface,ABI)规范中,随后在工具接口标准(Tool Interface Standard,TIS)中发布,很快就被Unix系统的不同供应商所接受。在1999年,它被86open项目选定为x86处理器上Unix和类Unix系统的标准二进制文件格式,用来取代通用目标文件格式(Common Object File Format,COFF)。
通过设计,ELF格式是灵活的,可扩展的和跨平台的。例如,它支持不同的字节序和地址大小,因此它不排除任何特定的中央处理单元(CPU)或指令集体系结构。这使得它可以被许多不同硬件平台上的许多不同操作系统所采用。
ELF文件的扩展名[^2]:
none
: ELF文件可以没有扩展名。.bin
: BIN文件是以二进制格式存储数据的文件。编译的计算机程序就是典型的例子。它与基于文本的文件不同,后者可以在文本编辑器中编辑,而前者不可以在文本编辑器中编辑,但可以在16进制编辑器中进行编辑。磁盘映像(Disk Images)通常是二进制文件,尽管它们经常使用其他文件扩展名。BIN文件也可以用于固件更新[^4]。.axf
: AXF文件是由ARM的RealView编译器(Keil的ARM-MDK的一部分)生成的目标文件格式,并且包含目标代码和调试信息。在调试器中,虽然仅将目标代码加载到目标本身,但是代码和调试信息都会加载到开发主机的内存中。通过JTAG,SWD或其他连接进行调试时(任何类型的-不仅仅是崩溃),代码都需要在主机上可用,并且调试信息需要将该代码与原始源代码相关联。通过调试连接,仅传输了诸如寄存器值之类的最少数据,因此,例如,调试器将获取程序计数器值,并能够使用AXF中的调试数据显示主机上可用的汇编程序和源代码[^4]。.o
: 可重定位文件(Relocatable File)。见下方。.elf
:可执行文件(Executable File)。见下方。.so
: 共享目标文件(Shared Object File)。见下方。.ko
: 内核对象(Kernel Object)。内核模块。.prx
: 索尼的PRX(PSP Relocation eXecutable?)格式是基于标准ELF格式的重定位可执行文件。它具有自定义的程序头(Program Headers),非标准MIPS重定位节(Relocation Sections)和唯一的ELF类型,从而与普通的ELF文件区分开。.puff
:.mod
:
ELF格式的目标文件(Object Files)主要有以下三种类型:
可重定位文件
(Relocatable File): 包含由编译器生成的代码以及数据。链接器会将它与其它目标文件链接起来从而创建可执行文件或者共享目标文件。在Linux系统中,这种文件的后缀一般为.o
。可执行文件
(Executable File): 就是我们通常在Linux中执行的程序。共享目标文件
(Shared Object File): 包含代码和数据,这种文件是我们所称的库文件,一般以.so
结尾。一般情况下,它有以下两种使用情景:链接器
(Link eDitor, ld)可能会将它和其它可重定位文件以及共享目标文件一起处理,以生成另外一个目标文件。动态链接器
(Dynamic Linker)将它与可执行文件以及其它共享目标文件组合在一起,以生成进程镜像。
目标文件由汇编器(Assembler)和链接器(Link Editor)创建,是文本程序(源代码)的二进制形式,可以直接在处理器上运行。那些需要虚拟机才能够执行的程序(例如:Java)不属于这一范围。
本文章使用的Linux Kernel代码:
/linux/include/elf.h.html,Copyright (C) 1995-2018
1.1 文件格式(File Format)
目标文件既会参与程序链接(构建程序)又会参与程序执行(运行程序)。出于方便性和效率考虑,根据过程的不同,目标文件格式提供了其内容的两种并行视图,如下:
Figure 1-1:Object File Format
注意
:
尽管该图显示程序头表(Program Header Table)紧跟在ELF头(ELF Header)之后,节头表(Section Header Table)紧跟在各节(Sections)之后,但实际文件可能会有所不同。此外,节(Sections)和段(Segments)没有指定的顺序,只有ELF头(ELF Header)在文件中具有固定的位置。
节(Section)表示可以在ELF文件中处理的最小不可分割的单元。段(Segment)是节(Section)的集合。段(Segment)表示可由exec(2)或运行时链接器映射到内存映像的最小单元[^6]。
这里给出一个关于链接视图和执行视图比较形象的展示:
Figure 1-2:Linking Execution View
1.1.1 链接视图(Linking View)
文件开始处是ELF头(ELF Header),它给出了整个文件的组织情况。
如果程序头表(Program Header Table)存在,它会告诉系统如何创建进程映像。用于构建进程映像(执行程序)的文件必须具有程序头表。但是可重定位文件(Relocatable File)不需要这个表。
节(Sections)区域包含了在链接视图中要使用的大部分信息:指令、数据、符号表、重定位信息等等。
节头表(Section Header Table)包含了描述文件节(Section)的信息,每个节在节头表中都有一个表项,会给出节的名称、节的大小等信息。用于链接的目标文件必须有节头表,其它目标文件则无所谓,可以有,也可以没有。
1.1.2 执行视图(Execution View)
对于执行视图来说,其主要的不同点在于没有了节(Section),而有了多个段(Segment)。其实这里的段(Segment)大都是来源于链接视图中的节(Section)。
1.2 数据表示(Data Representation)
目标文件格式支持具有8位/32位/64位架构的各种处理器。当然,这种格式是可以扩展的,也可以支持更小的或者更大位数的处理器架构。因此,目标文件会包含一些控制数据,这部分数据以机器无关的格式进行表示,从而有可能以通用的方式识别目标文件并解释其内容。目标文件中的其它数据采用目标处理器的格式进行编码,与在何种机器上创建没有关系。这里其实想表明的意思是目标文件可以进行交叉编译,我们可以在x86平台生成ARM平台的可执行代码。
目标文件格式定义的所有数据结构均遵循相关类的“自然”大小和对齐准则。如有必要,数据结构包含显式填充以确保4字节对象的4字节对齐,从而强制使数据结构的大小为4字节的整数倍,依此类推。文件开头的数据也具有适当的对齐方式,因此,例如,包含Elf32_Addr成员的结构将在文件内的4字节边界上对齐,包含Elf64_Addr成员的结构将在文件内的8字节边界上对齐。出于可移植性的原因,ELF文件格式没有使用位域。
Table 1-1:32-Bit Data Types
名称 | 大小 | 对齐方式 | 用途 |
---|---|---|---|
Elf32_Addr | 4 | 4 | 无符号程序地址 |
Elf32_Half | 2 | 2 | 无符号半整型 |
Elf32_Off | 4 | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 4 | 有符号整型 |
Elf32_Word | 4 | 4 | 无符号整型 |
unsigned char | 1 | 1 | 无符号小整型 |
Table 1-2:64-Bit Data Types
名称 | 大小 | 对齐方式 | 用途 |
---|---|---|---|
Elf64_Addr | 8 | 8 | 无符号程序地址 |
Elf64_Half | 2 | 2 | 无符号半整型 |
Elf64_Off | 8 | 8 | 无符号文件偏移 |
Elf64_Sword | 4 | 4 | 有符号整型 |
Elf64_Word | 4 | 4 | 无符号整型 |
Elf64_Sxword | 8 | 8 | 有符号长整型 |
Elf64_Xword | 8 | 8 | 无符号长整型 |
unsigned char | 1 | 1 | 无符号小整型 |
1.3 字符表示(Character Representations)
本节描述默认的ELF字符表示,并定义外部文件使用的标准字符集,这些文件应该可以在系统之间移植。几种外部文件格式用字符表示控制信息。这些单字节字符使用7位ASCII字符集。换句话说,当ELF接口文档提到字符常量时,例如“/”或“\n”,它们的数值应该遵循7位ASCII准则。对于前面的字符常量,单字节值分别为47和10。
根据字符编码,0~127范围之外的字符值可能占用一个或多个字节。应用程序可以控制它们自己的字符集,根据需要为不同的语言使用不同的字符集扩展。尽管工具接口标准(Tool Interface Standard,TIS)一致性不限制字符集,但它们通常应遵循一些简单的准则。
- 0~127之间的字符值应对应于7位ASCII代码。也就是说,编码超过127的字符集应该包括7位ASCII代码字符集,作为其子集。
- 字符值大于127的多字节字符编码应该仅包含值在0到127范围之外的字节。也就是说,每个字符使用超过一个字节的字符集不应该在多字节、非ASCII字符中“嵌入”类似于7位ASCII字符的字节。
- 多字节字符应该是自识别的。例如,这允许在任何多字节字符对之间插入任何多字节字符,而不会改变字符的解释。
这些注意事项与多语言(多国语言)应用程序特别相关。
注意:ELF常量有指定处理器范围的命名约定。用于处理器特定扩展的名称,例如:DT、PT,包含了处理器种类的名称,例如:DT_M32_SPECIAL。但是,不使用此约定的现有处理器扩展将受到支持。例如:DT_JMP_REL。
2 ELF头(ELF Header)
某些目标文件控制结构可以增长,因为ELF头(ELF Header)包含它们的实际大小。如果目标文件格式发生变化,程序可能会遇到比预期更大或更小的控制结构。因此,程序可能会忽略“额外”的信息。“缺失”的信息的处理取决于上下文,并将在定义扩展时指定。
ELF头(ELF Header)描述了ELF文件的概要信息,利用这个数据结构可以索引到ELF文件的全部信息,数据结构如下:
1 |
|
其中每个成员都是以“e_”开头的,应该都是ELF的缩写。每个成员具体的说明如下。
e_ident
: ELF头(ELF Header)或目标文件的初始字节将文件标记为目标文件,并提供与机器无关的数据,用于解码和解释文件的内容。e_type
: e_type标识目标文件类型。e_machine
: e_machine指定了当前文件可以运行的处理器架构(指令集)。e_version
: e_version标识目标文件的版本。e_entry
: 这一项为系统转交控制权给ELF中相应代码的虚拟地址,从而启动进程。如果ELF文件没有相关的入口点(Entry Point),则该成员保持为0。e_phoff
: 这一项给出程序头表(Program Header Table)在文件中的字节偏移(Program Header table OFFset)。如果文件中没有程序头表,则为0。e_shoff
: 这一项给出节头表(Section Header Table)在文件中的字节偏移(Section Header table OFFset)。如果文件中没有节头表,则为0。e_flags
: 这一项给出文件中与特定处理器相关的标志,这些标志命名格式为EF_machine_flag。e_ehsize
: 这一项给出ELF头(ELF Header)的字节长度(ELF Header Size)。e_phentsize
: 这一项给出程序头表(Program Header Table)中每个表项的字节长度(Program Header ENTry SIZE)。每个表项的大小相同。e_phnum
: 这一项给出程序头表的项数(Program Header entry NUMber)。因此,e_phnum与e_phentsize的乘积即为程序头表的字节长度。如果文件中没有程序头表,则该项值为0。
如果程序头(Program Header Table)的数量大于65534,则该成员的值为PN_XNUM(0xFFFF)。程序头表(Program Header Table)条目的实际数量包含在索引0处的节头(Section Header)的sh_info字段中。否则,初始节头条目的sh_info成员值为0。请参见“扩展的节头”[^6]。e_shentsize
: 这一项给出节头表(Section Header Table)中每个表项的字节长度(Section Header ENTry SIZE)。节头表中所有项占据的空间大小相同。e_shnum
: 这一项给出节头表的项数(Section Header NUMber)。因此,e_shnum与e_shentsize的乘积即为节头表的字节长度。如果文件中没有节头表,则该项值为0。
如果节的数量大于65279,则e_shnum的值为0。节头表(Section Header Table)条目的实际数量包含在索引0处的节头(Section Header)的sh_size字段中。否则,初始节头条目的sh_size成员的值为0。请参见“扩展的节头”[^6]。e_shstrndx
: 这一项给出节头表(Section Header Table)中与节名称字符串表(Section Header String Table)相关的表项的索引值(Section Header STRing table iNDeX)。如果文件中没有节名称字符串表,则该项值为SHN_UNDEF。关于细节的介绍,请参考后面的“节(Sections)”和“字符串表(String Table)”部分。
如果节名称字符串表(Section Header String Table)的节索引大于65279,则该成员的值为SHN_XINDEX(0xFFFF),并且节名称字符串表节的实际索引包含在索引0处的节头的sh_link字段中。否则,初始节头条目的sh_link成员的值为0。请参见“扩展的节头”[^6]。
2.1 初始数组(e_ident[EI_NIDENT])
如上所述,ELF提供了一个目标文件框架来支持多处理器、多数据编码以及多种类型的机器。为了支持这个目标文件族,文件的初始字节指定了如何解释机器无关的文件内容,并且独立于文件的剩余内容。
ELF头(ELF Header)或目标文件的初始字节对应于e_ident成员。这个数组对于不同的下标的含义如下:
Table 2-1:e_ident[] Identification Indexes
宏名称 | 值(数组下标) | 用途 |
---|---|---|
EI_MAG0 | 0 | 文件标识字节0的索引 |
EI_MAG1 | 1 | 文件标识字节1的索引 |
EI_MAG2 | 2 | 文件标识字节2的索引 |
EI_MAG3 | 3 | 文件标识字节3的索引 |
EI_CLASS | 4 | 文件类型字节的索引 |
EI_DATA | 5 | 数据编码字节的索引 |
EI_VERSION | 6 | 文件版本字节的索引 |
EI_OSABI | 7 | 操作系统ABI标识字节的索引 |
EI_ABIVERSION | 8 | ABI版本字节的索引 |
EI_PAD | 9 | 填充字节的起始字节的索引 |
EI_NIDENT | 16 | e_ident[]数组的大小 |
2.1.1 e_ident[EI_MAG0] ~ e_ident[EI_MAG3]
这些索引访问到的字节保存一下的值。e_ident[EI_MAG0]到e_ident[EI_MAG3],即文件的头4个字节,被称作“魔数(Magic Number)”,标识该文件是一个ELF目标文件。至于开头为什么是0x7f,并没有仔细去查过。
Table 2-2:Magic Number
宏名称 | 值 | 含义 |
---|---|---|
ELFMAG0 | 0x7f | e_ident[EI_MAG0]的字节值,魔数字节0 |
ELFMAG1 | ‘E’ | e_ident[EI_MAG1]的字节值,魔数字节1 |
ELFMAG2 | ‘L’ | e_ident[EI_MAG2]的字节值,魔数字节2 |
ELFMAG3 | ‘F’ | e_ident[EI_MAG3]的字节值,魔数字节3 |
2.1.2 e_ident[EI_CLASS]
e_ident[EI_CLASS]为e_ident[EI_MAG3]的下一个字节,标识文件的类型或容量。
Table 2-3:e_ident[EI_CLASS] Value
宏名称 | 值 | 含义 |
---|---|---|
ELFCLASSNONE | 0 | 无效的类型 |
ELFCLASS32 | 1 | 32位ELF文件 |
ELFCLASS64 | 2 | 64位ELF文件 |
ELFCLASSNUM | 3 | 类型的数量 |
ELF文件的设计使得它可以在多种字节长度的机器之间移植,而不需要强制规定机器的最长字节长度和最短字节长度。文件的类型定义了对象文件容器的数据结构所使用的基本类型。包含在目标文件节(Section)中的数据可以遵循不同的编程模型[^6]。
ELFCLASS32类型支持文件大小和虚拟地址空间上限为4GB的机器,例如:x86;它使用上述定义中的基本类型。
ELFCLASS64类型指的是64位架构,例如:64位的SPARC和x86-x64。
它出现在此处,展示了目标文件可能会如何更改。其他类型将根据需要定义,目标文件数据具有不同的基本类型和大小。
2.1.3 e_ident[EI_DATA]
e_ident[EI_DATA]字节给出了目标文件中的特定处理器数据的编码方式。这将影响从偏移量0x10开始的多字节字段的解释。下面是目前已定义的编码:
Table 2-4:e_ident[EI_DATA] Value
宏名称 | 值 | 含义 |
---|---|---|
ELFDATANONE | 0 | 无效数据编码 |
ELFDATA2LSB | 1 | 小端序(Little-endian) |
ELFDATA2MSB | 2 | 大端序(Big-endian) |
ELFDATANUM | 3 | 数据编码种类的数量 |
有关这些编码的更多信息如下所示。其他值被保留并将根据需要分配给新的编码。
文件数据编码方式表明了文件内容的解析方式。正如之前所述,ELFCLASS32类型文件和ELFCLASS64类型文件使用了占用1,2,4和8字节的整数来表示偏移量,地址和其他信息。对于已定义的不同的编码方式,其表示如下所示,其中字节号在左上角。
编码ELFDATA2LSB使用2的补码,最低有效字节(Least Significant Byte)占据最低地址。这种编码通常被非正式地称为小端序(Little Endian)。
Figure 2-1:Data Encoding ELFDATA2LSB
编码ELFDATA2MSB使用2的补码,最高有效字节(Most Significant Byte)占据最低地址。这种编码通常被非正式地称为大端序(Big Endian)。
Figure 2-2:Data Encoding ELFDATA2MSB
2.1.4 e_ident[EI_VERSION]
字节e_ident[EI_VERSION]指定ELF头(ELF Header)的版本号。当前,此值必须为EV_CURRENT,如介绍e_version时所说的那样。
2.1.5 e_ident[EI_OSABI]
字节e_ident[EI_OSABI]指示此ELF文件能够运行的目标操作系统ABI编号。其他ELF结构中的一些字段具有操作系统或ABI特定含义的标志和值,这些字段的解释由该字节的值决定。无论目标平台是什么,它通常都被设置为0。目前定义的不同操作系统ABI对应的编号如下:
Table 2-5:e_ident[EI_OSABI] Value
宏名称 | 值 | 含义 |
---|---|---|
ELFOSABI_NONE | 0 | No extensions or unspecified |
ELFOSABI_SYSV | 0 | UNIX System V ABI |
ELFOSABI_HPUX | 1 | HP-UX ABI |
ELFOSABI_NETBSD | 2 | NetBSD ABI |
ELFOSABI_LINUX | 3 | Linux ABI |
ELFOSABI_HURD | 4 | GNU Hurd ABI |
ELFOSABI_SOLARIS | 6 | Sun Solaris ABI |
ELFOSABI_AIX | 7 | IBM AIX ABI |
ELFOSABI_IRIX | 8 | SGI Irix ABI |
ELFOSABI_FREEBSD | 9 | FreeBSD ABI |
ELFOSABI_TRU64 | 10 | Compaq TRU64 UNIX ABI |
ELFOSABI_MODESTO | 11 | Novell Modesto ABI |
ELFOSABI_OPENBSD | 12 | OpenBSD ABI |
ELFOSABI_OPENVMS | 13 | OpenVMS ABI |
ELFOSABI_NSK | 14 | NonStop Kernel ABI |
ELFOSABI_AROS | 15 | AROS ABI |
ELFOSABI_FENIXOS | 16 | Fenix OS ABI |
ELFOSABI_CLOUDABI | 17 | CloudABI |
ELFOSABI_ARM_AEABI | 64 | ARM EABI |
ELFOSABI_ARM | 97 | ARM ABI |
ELFOSABI_STANDALONE | 255 | Standalone (embedded) application |
2.1.6 e_ident[EI_ABIVERSION]
e_ident[EI_ABIVERSION]用于进一步指定ABI的版本。此字段用于区分ABI的不兼容版本。它的解释取决于能够运行此文件的目标操作系统ABI,也就是EI_OSABI字段标识的ABI。如果没有为目标文件的EI_OSABI字段指定值,或者没有为由EI_OSABI字节的特定值确定的ABI指定版本值,则使用0用于指示未指定[^6]。
Linux内核(至少在版本2.6之后)没有定义它,所以它被静态链接的可执行文件忽略。在这种情况下,EI_PAD的值为8。在glibc 2.12+中,如果e_ident[EI_OSABI]==3,则将此字段视为动态链接器的ABI版本:它定义了动态链接器的特性列表,将e_ident[EI_ABIVERSION]作为共享对象(可执行程序库或动态库)请求的特性级别,并在请求未知特性时拒绝加载它,即e_ident[EI_ABIVERSION]大于最大的已知特性[^2]。
2.1.7 EI_PAD
EI_PAD为e_ident[]中未使用字节的起始索引。这些字节被保留并置为0;处理目标文件的程序应该忽略它们。如果之后这些字节被使用,EI_PAD的值就会改变。
2.2 目标文件类型(e_type)
e_type标识目标文件类型。
Table 2-6:e_type Value
宏名称 | 值 | 含义 |
---|---|---|
ET_NONE | 0 | 无文件类型 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 共享目标文件 |
ET_CORE | 4 | 核心转储文件 |
ET_NUM | 5 | 定义的文件类型数量 |
ET_LOOS | 0xFE00 | 特定操作系统目标文件类型值范围的下限 |
ET_HIOS | 0xFEFF | 特定操作系统目标文件类型值范围的上限 |
ET_LOPROC | 0xFF00 | 特定处理器目标文件类型值范围的下限 |
ET_HIPROC | 0xFFFF | 特定处理器目标文件类型值范围的上限 |
虽然核心转储文件的内容没有被详细说明,但ET_CORE还是被保留用于标识此类文件。从ET_LOPROC到ET_HIPROC(包括边界)被保留用于特定处理器的场景。其他值被保留,并在未来根据需要分配给新的目标文件类型。
2.3 处理器架构(e_machine)
e_machine指定了当前文件可以运行的处理器架构(指令集)。下面列出一些常见处理器架构(我见过的)对应的编码:
Table 2-7:e_machine Value
宏名称 | 值 | 含义 |
---|---|---|
EM_NONE | 0 | 无机器类型 |
EM_M32 | 1 | AT&T WE 32100 |
EM_SPARC | 2 | SUN SPARC |
EM_386 | 3 | Intel 80386(x86) |
EM_68K | 4 | Motorola 68000(M68k) |
EM_88K | 5 | Motorola 88000(M88k) |
EM_860 | 7 | Intel 80860 |
EM_MIPS | 8 | MIPS R3000 big-endian |
EM_MIPS_RS3_LE | 10 | MIPS R3000 little-endian |
EM_MIPS_RS4_BE | 10 | MIPS R4000 big-endian |
EM_PPC | 20 | PowerPC |
EM_PPC64 | 21 | PowerPC 64-bit |
EM_ARM | 40 | ARM 32-bit(up to ARMv7/Aarch32) |
EM_SPARCV9 | 43 | SPARC v9 64-bit |
EM_IA_64 | 50 | HP/Intel IA-64 |
EM_X86_64 | 62 | AMD x86-64 |
EM_MSP430 | 105 | Texas Instruments msp430 |
EM_ALTERA_NIOS2 | 113 | Altera Nios II |
EM_AARCH64 | 183 | ARM 64-bit(ARMv8/Aarch64) |
EM_AVR32 | 185 | Amtel 32-bit microprocessor |
EM_STM8 | 186 | STMicroelectronics STM8 |
EM_RISCV | 243 | RISC-V |
EM_BPF | 247 | Linux BPF – in-kernel virtual machine |
其中“EM”应该是“ELF Machine”的简写。
其他值被保留并将根据需要分配给新机器。此外,特定处理器的ELF名称使用机器名称来进行区分。例如,下面提到的标志使用前缀EF_(ELF Flag)。在EM_XYZ机器上名叫WIDGET的标志将被称为EF_XYZ_WIDGET。
2.4 目标文件版本(e_version)
e_version标识目标文件的版本。
Table 2-8:e_version Value
宏名称 | 值 | 含义 |
---|---|---|
EV_NONE | 0 | 无效版本 |
EV_CURRENT | >=1 | 当前版本 |
1表示原始文件格式;未来扩展(extensions)新的版本的时候将使用更大的数字。虽然在上面EV_CURRENT的值为1,但是为了反映当前版本号,它可能会改变,比如ELF到现在也就是1.2版本。
3 程序头表(Program Header Table)
可执行文件(Executable File)或共享目标文件(Shared Object File)的程序头表(Program Header Table)是一个结构体数组,每一个元素的类型是Elfxx_Phdr,描述了系统准备执行程序时所需的一个段(Segment)的信息或其他信息。一个目标文件的段包含一个或多个节(Section)。程序头仅对可执行文件和共享目标文件有意义。一个文件用ELF头(ELF Header)中的e_phentsize和e_phnum成员指定它自己的程序头表表项的大小和程序头表的项数。
可以说,程序头表(Program Header Table)就是专门为ELF文件运行时中的段(Segment)所准备的。程序头表表项所使用的结构体Elfxx_Phdr的定义如下:
1 |
|
各个字段的含义如下:
p_type
: 这个成员告诉该数组元素描述了什么样的段(Segment)或者如何解释该数组元素的信息。其类型值及其含义见下方。p_offset
: 该成员给出了从文件开始到该段(Segment)开头的第一个字节的偏移量。p_vaddr
: 该成员给出了该段(Segment)第一个字节在内存中的虚拟地址(Virtual Address)。p_paddr
: 该成员仅用于物理地址寻址相关的系统中,该成员保留用于段(Segment)的物理地址。由于“System V”忽略了应用程序的物理寻址,可执行文件(Executable File)或共享目标文件(Shared Object File)的该项内容并未被限定。p_filesz
: 该成员给出了文件映像中该段(Segment)的大小,可能为0。p_memsz
: 该成员给出了内存映像中该段(Segment)的大小,可能为0。p_flags
: 该成员给出了与段(Segment)相关的标记。详细内容见下方。p_align
: 可加载的程序的段(Segment)的p_vaddr以及p_offset的大小必须是page的整数倍。该成员给出了段在文件以及内存中的对齐方式。如果该值为0或1的话,表示不需要对齐。除此之外,p_align应该是2的整数指数次方,并且p_vaddr与p_offset在模p_align的情况下,结果应该相等。
PN_XNUM(0xFFFF)为e_phnum的特殊值。这表明程序头(Program Header Table)的实际数量太大而无法放入e_phnum,因此实际值位于节头表(Section Header Table)第0个节头(Section Header)的sh_info字段中。
3.1 段类型(p_type)
一些条目描述了进程段(Process Segments),另外一些条目则提供补充信息,但与进程映像没有关系(Process Image)。
Table 3-1:Segment Types
宏名称 | 值 | 含义 |
---|---|---|
PT_NULL | 0 | 该数组元素未使用。除p_type外,其他结构体成员的值都是未定义的。这种类型可以使程序头表(Program Header Table)忽略此条目。 |
PT_LOAD | 1 | 此类型段为一个可加载的段,大小由p_filesz和p_memsz描述。文件中该段的内容被映射到相应内存段的开始处。如果p_memsz大于p_filesz,“剩余”的字节都要被置为0并跟踪段的初始化区域。p_filesz不能大于p_memsz。可加载的段在程序头表中按照p_vaddr升序排列。 |
PT_DYNAMIC | 2 | 此类型段给出动态链接信息,具体参见ELF手册Book III。 |
PT_INTERP | 3 | 此类型段给出了一个以Null结尾的字符串的位置和长度,该字符串将被当作解释器的路径名进行调用。这种段类型仅对可执行文件有意义(也可能出现在共享目标文件中)。此外,这种段在一个文件中最多出现一次。而且该段类型的数组元素存在的话,它必须在所有可加载段条目的前面。 |
PT_NOTE | 4 | 此类型段给出附加信息的位置和大小。 |
PT_SHLIB | 5 | 该段类型被保留,不过语义未指定。而且,包含这种类型数组元素的程序不符合Unix System V的ELF标准,具体参见ELF手册Book III。 |
PT_PHDR | 6 | 该段类型的数组元素如果存在的话,则给出了程序头表自身在文件和程序内存映像中的的位置和大小。此类型的段在文件中最多出现一次。此外,只有程序头表是程序内存映像的一部分时,该段类型的数组元素才会存在。如果该段类型的数组元素存在,则必须在所有可加载段条目的前面。 |
PT_TLS | 7 | 该段类型的数组元素给出线程本地存储段(TLS)的信息。 |
PT_LOOS | 0x60000000 | 特定操作系统段类型值的下限。 |
PT_GNU_EH_FRAME | 0x6474E550 | 该段类型数组元素指定异常处理信息的位置和大小(由.eh_frame_hdr节定义)[^5]。 |
PT_GNU_STACK | 0x6474E551 | 该段类型数组元素中的p_flags成员指定包含栈的段的权限,并用于指示栈是否应该是可执行的。没有此段类型的数组元素,则表示该栈将是可执行的[^5]。 |
PT_GNU_RELRO | 0x6474E552 | 该段类型数组元素指定了一个在重定位后可以被置为只读的段的位置和大小[^5]。 |
PT_GNU_PROPERTY | 0x6474E553 | 该段类型数组元素指定.note.gnu.property节的位置和大小。 |
PT_HIOS | 0x6FFFFFFF | 特定操作系统段类型值的上限。 |
PT_LOPROC | 0x70000000 | 特定处理器段类型值的下限。 |
PT_HIPROC | 0x7FFFFFFF | 特定处理器段类型值的上限。 |
注意:除非其他地方特别要求,所有程序头段类型都是可选的。也就是说,文件的程序头表可能只包含与其内容相关的那些元素。
3.2 段权限(p_flags)
被系统加载到内存中的程序至少有一个可加载的段(PT_LOAD)。当系统为可加载的段创建内存映像时,它会按照p_flags为段(Segments)设置相应的访问权限。
Table 3-2:Segment Flag Bits, p_flags
宏名称 | 值 | 含义 |
---|---|---|
PF_X | 0x1 | 段具有可执行权限 |
PF_W | 0x2 | 段具有写权限 |
PF_R | 0x4 | 段具有读权限 |
PF_MASKOS | 0x0FF00000 | 为特定操作系统预留 |
PF_MASKPROC | 0xF0000000 | 为特定处理器预留 |
其中,所有在PF_MASKOS中的比特位都是被保留用于与操作系统相关的语义信息。所有在PF_MASKPROC中的比特位都是被保留用于与处理器相关的语义信息。如果指定了含义,则操作系统和处理器补充说明它们。
如果权限位为0,则拒绝该类型的访问。实际的内存权限取决于相应的内存管理单元,这可能因系统而异。尽管所有的权限组合都是可以的,但是系统一般会授予比请求更多的权限。在任何情况下,除非明确说明,否则段不会有写权限。下表显示了确切的标志解释和允许的标志解释。符合工具接口标准(Tool Interface Standard,TIS)的系统可以设置任何一种。
Table 3-3:Segment Permissions
宏名称 | 值 | 准确的权限 | 允许的权限 |
---|---|---|---|
none | 0 | 拒绝所有访问 | 拒绝所有访问 |
PF_X | 1 | 只执行 | 读,执行 |
PF_W | 2 | 只写 | 读,写,执行 |
PF_W + PF_X | 3 | 写,执行 | 读,写,执行 |
PF_R | 4 | 只读 | 读,执行 |
PF_R + PF_X | 5 | 读,执行 | 读,执行 |
PF_R + PF_W | 6 | 读,写 | 读,写,执行 |
PF_R + PF_W + PF_X | 7 | 读,写,执行 | 读,写,执行 |
例如,一般来说,代码段一般具有读和执行权限,但是不会有写权限。数据段一般具有写,读,以及执行权限。
3.3 段内容(Segment Contents)
一个目标文件段(Segment)可能包括一到多个节(Section),尽管这对程序头(Program Header)是透明的。文件的段(Segment)是否包含一个或多个节(Section)并不会影响程序的加载。尽管如此,我们也必须需要各种各样的数据来使得程序可以执行以及动态链接等等。下面会给出一般情况下的段(Segment)的内容。对于不同的段(Segment)来说,它内部的节(Section)的顺序以及所包含的节(Section)的个数有所不同。此外,与处理相关的约束可能会改变对应的段(Segment)的结构。
如下所示,代码段(Text Segment)包含只读的指令以及数据,通常包括以下节(Section)。其他节(Section)也可以驻留在可加载的段(Segment)中。当然这个例子并没有给出所有的可能的段(Segment)内容。
Figure 3-1:Text Segment
数据段(Data Segment)包含可写的数据以及指令,通常包括以下节(Section)。
Figure 3-2:Data Segment
程序头表(Program Header Table)的PT_DYNAMIC类型的元素指向.dynamic节(Section)。其中,.got节和.plt节包含与地址无关代码和动态链接相关的信息。尽管在上面给出的例子中,.plt节出现在代码段(Text Segment),但它可以驻留在代码段(Text Segment)或数据段(Data Segment)中,这取决于处理器。
.bss节的类型为SHT_NOBITS,虽然它不占用文件中的空间,但它占用段的内存映像的空间。通常情况下,没有初始化的数据在段的尾部,因此,p_memsz才会比p_filesz大。
注意:不同的段(Segment)的内容可能会有所重合,即不同的段(Segment)可能包含相同的节(Section)。
3.4 基地址(Base Address)
程序头(Program Header)中的虚拟地址可能并不是程序内存映像中实际虚拟地址。通常来说,可执行文件(Executable File)都会包含绝对地址的代码。为了使得程序可以正常执行,段(Segments)必须在用于构建可执行文件的虚拟地址中。另一方面,共享目标文件(Shared Object File)通常包含与地址无关的代码。这可以使得共享目标文件(Shared Object File)可以被多个进程加载,同时保持程序执行的正确性。尽管系统会为不同的进程选择不同的虚拟地址,但它维护了这些段(Segments)的相对位置。因为地址无关代码使用段(Segments)之间的相对地址来进行寻址,内存中的虚拟地址之间的差值必须与文件中的虚拟地址之间的差值相匹配。内存中任何段(Segments)的虚拟地址与文件中对应的虚拟地址之间的差值对于给定进程中的任何一个可执行文件(Executable File)或共享目标文件(Shared Object File)来说是一个单一常量值。这个差值就是基地址(Base Address),基地址的一个用途就是在动态链接期间重新定位程序的内存映像。
可执行文件(Executable File)或者共享目标文件(Shared Object File)的基地址是在执行过程中由以下三个数值计算得到的:
- 虚拟内存加载地址
- 最大页面大小
- 程序可加载段的最低虚拟地址
要计算基地址(Base Address),首先要确定与PT_LOAD段的最小p_vaddr相关联的内存虚拟地址。然后把该内存虚拟地址调整为与之最接近的最大页面的整数倍,即是基地址(Base Address)。相应的p_vaddr值本身也被调整为与之最接近的最大页面的整数倍。基地址(Base Address)就是调整后的内存虚拟地址和调整后的p_vaddr值之间的差值。根据要加载到内存中的文件的类型,内存虚拟地址可能与p_vaddr相同也可能不同。
4 节头表(Section Header Table)
节头表(Section Header Table)在ELF文件的尾部(为什么要放在文件尾部呢?),但是为了讲解方便,这里将这个表放在这里进行讲解。
该结构用于定位ELF文件中的每个节(Section)的具体位置。节头表(Section Header Table)是一个结构体数组,每一个元素的类型是Elfxx_Shdr,描述了一个节(Section)的概要内容。节头表索引是该数组的下标。ELF头(ELF Header)中的e_shoff成员给出了从文件开头到节头表(Section Header Table)的字节偏移。e_shnum告诉了我们节头表包含的表项数;e_shentsize给出了每一表项的大小,以字节为单位。
如果节(Section)的数量大于或等于SHN_LORESERVE(0xFF00),则e_shnum的值为SHN_UNDEF(0)。节头表(Section Header Table)条目的实际数量包含在索引0处的节头的sh_size字段中。否则,初始条目的sh_size成员的值为0。请参见扩展的节头(Extended Section Header)[^6]。
1 | typedef struct |
sh_name
: 此成员指定节(Section)的名称,它的值是节头字符串表节(.shstrtab)中内容距节头字符串表节起始的偏移量(以字节为单位),因此该字段实际是一个数值。字符串表中的具体内容是以Null结尾的字符串。sh_type
: 此成员对该节(Section)的内容和语义进行分类。节(Section)的类型及其说明会在后面进行介绍。sh_flags
: 每一比特代表不同的标志,描述节(Section)是否可写,可执行,需要分配内存等属性。sh_addr
: 如果该节(Section)将出现在进程的内存映像中,则此成员将给出该节(Section)的第一个字节应该在进程映像中的位置。否则,此字段为0。sh_offset
: 此成员给出从文件开头到该节(Section)的第一个字节的偏移量。SHT_NOBITS类型的节(Section)不占用文件的空间,因此其sh_offset成员给出的是概念性的偏移。sh_size
: 此成员给出节(Section)的字节大小。除非节(Section)的类型是SHT_NOBITS,否则该节(Section)占用文件中的sh_size字节。类型为SHT_NOBITS的节大小可能非零,不过却不占用文件中的空间。sh_link
: 此成员给出节头表(Section Header Table)索引链接,其具体的解释依赖于节(Section)类型。sh_info
: 此成员给出附加信息,其解释依赖于节类型。sh_addralign
: 某些节(Section)的地址需要对齐。例如,如果一个节(Section)有一个doubleword类型的变量,那么系统必须保证整个节(Section)按双字对齐。也就是说,sh_addr%sh_addralign=0。目前它仅允许为0,以及2的正整数幂。0和1表示该节(Section)没有对齐约束。sh_entsize
: 某些节(Section)中存在具有固定大小的表项的表,如符号表(Symbol Table)。对于这类节,该成员给出每个表项的字节大小。反之,此成员取值为0。
4.1 特殊的节头表索引(Special Section Indexes)
一些节头表索引被保留,目标文件(Object Files)将不会有这些特殊索引对应的节(Section)。
在索引大小受限的上下文中保留了一些节头表索引。例如,符号表(Symbol Table)条目的st_shndx成员以及ELF头(ELF Header)的e_shnum和e_shstrndx成员。在这种情况下,保留值不代表目标文件中的实际节(Section)。同样在这样的上下文中,溢出值指示实际的节索引将在其他地方,在更大的字段中找到[^6]。
Table 4-1:Special Section Indexes
宏名称 | 值 | 含义 |
---|---|---|
SHN_UNDEF | 0 | 标志未定义的,丢失的,不相关的或者其它没有意义的节引用。例如,与节号SHN_UNDEF相关的“defined”的符号就是一个未定义符号。 |
SHN_LORESERVE | 0xFF00 | 保留的索引值范围的下限。 |
SHN_LOPROC | 0xFF00 | 保留用于特定处理器语义索引值范围的下限。 |
SHN_BEFORE | 0xFF00 | 弃用。在其他节之前的节。与SHF_LINK_ORDER和SHF_ORDERED节标志一起提供初始节排序。 |
SHN_AFTER | 0xFF01 | 弃用。在其他节之后的节。与SHF_LINK_ORDER和SHF_ORDERED节标志一起提供最终节排序。 |
SHN_AMD64_LCOMMON | 0xFF02 | x64专用通用块标签。这个标签类似于SHN_COMMON,但是提供了标识大型通用块的功能[^6]。 |
SHN_HIPROC | 0xFF1F | 保留用于特定处理器语义索引值范围的上限。 |
SHN_LOOS | 0xFF20 | 保留用于特定操作系统语义索引值范围的下限。 |
SHN_HIOS | 0xFF3F | 保留用于特定操作系统语义索引值范围的上限。 |
SHN_ABS | 0xFFF1 | 该值指定相关引用的绝对值。例如,相对于节号SHN_ABS定义的符号具有绝对值并且不受重定位的影响。 |
SHN_COMMON | 0xFFF2 | 相对于本节定义的符号是通用符号,例如FORTRAN COMMON或未分配的C外部变量。 |
SHN_XINDEX | 0xFFFF | 一个溢出值,指示实际的节头索引太大而无法放入包含字段。索引在额外的表中(SHT_SYMTAB_SHNDX类型节)。 |
SHN_HIRESERVE | 0xFFFF | 保留的索引值范围的上限。 |
虽然0号索引被保留用于未定义值,但节头表(Section Header Table)仍然包含索引为0的项。也就是说,如果ELF头(ELF Header)的e_shnum为6,那么索引应该为0~5。更加详细的内容在后面会说明。
系统预留SHN_LORESERVE和SHN_HIRESERVE之间的索引值(含边界),这些值不引用节头表(Section Header Table)。也就是说,节头表(Section Header Table)不包含保留索引值对应的条目。
正如之前所说,节头表索引为0(SHN_UNDEF)的节头存在,此索引标记的是未定义的节引用。此条目包含以下内容。
Table 4-2:Section Header Table Entry: Index 0
名称 | 值 | 含义 |
---|---|---|
sh_name | 0 | 无名称 |
sh_type | SHT_NULL | 没有关联的节(Section) |
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 | 不存在具有固定大小的表项的表 |
4.2 节类型(sh_type)
节头(Section Header)的sh_type成员指定了节的类型。节类型目前有下列可选范围,其中“SHT”是“Section Header Table”的简写。
Table 4-3:Section Types, sh_type
宏名称 | 值 | 含义 |
---|---|---|
SHT_NULL | 0 | 该值将节头(Section Header)标记为非活动的;它没有关联的节(Section)。节头(Section Header)的其他成员具有未定义的值。 |
SHT_PROGBITS | 1 | 该节(Section)包含由程序定义的信息,其格式和含义完全由程序确定。 |
SHT_SYMTAB | 2 | 该类型节(Section)包含一个符号表(SYMbol TABle)。目前目标文件对每种类型的节(Section)都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB节(Section)为链接器提供符号,尽管也可用来实现动态链接。作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。 |
SHT_STRTAB | 3 | 该类型节(Section)包含字符串表(STRing TABle)。 |
SHT_RELA | 4 | 该类型节(Section)包含显式指定加数(r_addend)的重定位项(RELocation entry with Addends),例如,32位目标文件中的Elf32_Rela类型节。此外,目标文件可能拥有多个重定位节。 |
SHT_HASH | 5 | 该类型节(Section)包含一个符号哈希表(symbol HASH table)。标准Hash表(Standard Hash Table)(.hash)。 |
SHT_DYNAMIC | 6 | 该类型节(Section)包含用于动态链接的信息(DYNAMIC linking)。 |
SHT_NOTE | 7 | 该类型节(Section)包含以某种方式标记文件的信息(NOTE)。 |
SHT_NOBITS | 8 | 该类型节(Section)不占用文件的空间,其它方面和SHT_PROGBITS相似。尽管该类型节(Section)不包含任何字节,其对应的节头成员sh_offset中还是会包含概念性的文件偏移。 |
SHT_REL | 9 | 该类型节(Section)包含重定位项(RELocation entry without Addends),不过并没有指定加数(r_addend)。例如,32位目标文件中的Elf32_rel类型。目标文件中可以拥有多个重定位节。 |
SHT_SHLIB | 10 | 该类型此节(Section)被保留,不过其语义尚未被定义。 |
SHT_DYNSYM | 11 | 目标文件也可以包含一个类型为SHT_DYNSYM的节(Section),其中保存动态链接符号的一个最小集合,以节省空间。 |
SHT_INIT_ARRAY | 14 | 标识包含指向初始化函数的指针数组的节(Section)。数组中的每个函数指针都被视为具有void返回值的无参数函数[^6]。 |
SHT_FINI_ARRAY | 15 | 标识包含指向终止函数的指针数组的节(Section)。数组中的每个函数指针都被视为具有void返回值的无参数函数[^6]。 |
SHT_PREINIT_ARRAY | 16 | 标识包含指向在所有其他初始化函数之前调用的函数的指针数组的节(Section)。数组中的每个指针都被视为具有void返回值的无参数函数[^6]。 |
SHT_GROUP | 17 | 标识节组(Section Group)。节组(Section Group)标识一组相关的节(Section),链接器必须将这些节(Section)视为一个单元。SHT_GROUP类型的节(Section)只能出现在可重定位的对象中[^6]。 |
SHT_SYMTAB_SHNDX | 18 | 标识包含与符号表(Symbol Table)关联的扩展节头表索引(Extended Section Indexes)的节。如果符号表(Symbol Table)引用的任何节头表索引(Section Header Table Indexes)包含溢出值SHN_XINDEX(0xFFFF),则需要关联的SHT_SYMTAB_SHNDX条目。 |
SHT_NUM | 19 | 其值为定义的节类型数量,不同版本的Linux内核中,此值可能不同,因为可能会对已定义的节类型删除或者增加新的节类型。 |
SHT_LOOS | 0x60000000 | 此值为保留用于特定操作系统的节类型值的范围的下限。 |
SHT_GNU_ATTRIBUTES | 0x6FFFFFF5 | 对象属性。 |
SHT_GNU_HASH | 0x6FFFFFF6 | GNU风格Hash表(.gnu.hash)。 |
SHT_GNU_LIBLIST | 0x6FFFFFF7 | Prelink库列表。 |
SHT_GNU_verdef | 0x6FFFFFFD | 版本定义节(.gnu.version_d)。 |
SHT_GNU_verneed | 0x6FFFFFFE | 版本请求节(.gnu.version_r)。 |
SHT_GNU_versym | 0x6FFFFFFF | 符号版本节(.gnu.version)。 |
SHT_HIOS | 0x6FFFFFFF | 此值为保留用于特定操作系统的节类型值的范围的上限。 |
SHT_LOPROC | 0x70000000 | 此值为保留用于特定处理器的节类型值的范围的下限。 |
SHT_HIPROC | 0x7FFFFFFF | 此值为保留用于特定处理器的节类型值的范围的上限。 |
SHT_LOUSER | 0x80000000 | 此值为保留用于应用程序的节类型值的范围的下限。 |
SHT_HIUSER | 0x8FFFFFFF | 此值为保留用于应用程序的节类型值的范围的上限。SHT_LOUSER和SHT_HIUSER可以被应用程序使用,而不会与当前或将来系统定义的节类型冲突。 |
4.3 节标志(sh_flags)[^6]
节头(Section Header)中sh_flags字段的每一个比特位都可以定义相应的标志信息,其定义了对应节的内容是否可以被修改、被执行等信息。如果sh_flags中的一个标志位被设置,则该位取值为1,未定义的位都为0。目前已定义标志位如下,其他值保留。
Table 4-4:Section Attribute Flags, sh_flags
宏名称 | 值 |
---|---|
SHF_WRITE | 0x1 |
SHF_ALLOC | 0x2 |
SHF_EXECINSTR | 0x4 |
SHF_MERGE | 0x10 |
SHF_STRINGS | 0x20 |
SHF_INFO_LINK | 0x40 |
SHF_LINK_ORDER | 0x80 |
SHF_OS_NONCONFORMING | 0x100 |
SHF_GROUP | 0x200 |
SHF_TLS | 0x400 |
SHF_COMPRESSED | 0x800 |
SHF_MASKOS | 0x0FF00000 |
SHF_MASKPROC | 0xF0000000 |
SHF_AMD64_LARGE | 0x10000000 |
SHF_ORDERED | 0x40000000 |
SHF_EXCLUDE | 0x80000000 |
SHF_WRITE
: 标识在进程执行期间可写的节(Section)。SHF_ALLOC
: 标识在进程执行期间占用内存的节(Section)。某些控制节(Section)不驻留在目标文件的内存映像中。对于这些节(Section),不设置此属性。SHF_EXECINSTR
: 标识包含可执行机器指令的节(Section)。SHF_MERGE
: 标识包含可以合并以消除重复的数据的节(Section)。除非还设置了SHF_STRINGS标志,否则该节(Section)中的数据元素具有统一的大小。每个元素的大小在节头(Section Header)的sh_entsize字段中指定。如果还设置了SHF_STRINGS标志,则数据元素由以Null结尾的字符串组成。每个字符的大小在节头(Section Header)的sh_entsize字段中指定。SHF_STRINGS
: 标识由以Null结尾的字符串组成的节(Section)。每个字符的大小在节头(Section Header)的sh_entsize字段中指定。SHF_INFO_LINK
: 该节头(Section Header)的sh_info字段包含一个节头表索引(Section Header Table Index)。SHF_LINK_ORDER
: 本节(Section)为链接器添加了特殊的排序要求。这些要求适用于由本节头(Section Header)的sh_link字段标识的引用节。如果此节(Section)与输出文件中的其他节(Section)组合,则该节(Section)相对于这些节(Section)必须以相同的相对顺序出现,就像被引用节(Section)相对于与其组合的节(Section)出现一样。链接到的节(Section)必须是无序的,并且不能反过来指定SHF_LINK_ORDER或SHF_ORDERED。
此标志的典型用途是构建按地址顺序引用文本或数据节(Section)的表。
除了添加排序要求之外,SHF_LINK_ORDER还指示该节(Section)包含描述引用的节(Section)的元数据。当执行未使用的节消除时,链接器确保该节和引用的节被一起保留或丢弃。从SHF_LINK_ORDER节(Section)到其引用的节(Section)的重定位本身,并不表示使用了引用的节(Section)。
在没有sh_link排序信息的情况下,来自单个输入文件的节(Section)组合在输出文件的一个节(Section)中时,它们是连续的。这些节(Section)与输入文件中的节(Section)具有相同的相对顺序。来自多个输入文件的节(Section)以链接行顺序(link-line order)出现。
注意:特殊的sh_link值SHN_BEFORE和SHN_AFTER意味着排序后的节(Section)将分别排在排序集中所有其他节(Section)的前面或后面。如果有序集中的多个节(Section)具有这些特殊值之一,则保留输入文件链接行顺序。SHN_BEFORE和SHN_AFTER与使用扩展节索引的对象不兼容。他们已被弃用。
SHF_OS_NONCONFORMING
: 本节(Section)需要在标准链接规则之外进行特定于操作系统的特殊处理,以避免错误行为。如果此节(Section)具有sh_type值或包含了这些字段在特定操作系统范围内的sh_flags位,并且链接器无法识别这些值,则包含此节(Section)的目标文件将因错误而被拒绝。SHF_GROUP
: 此节(Section)是节组(Section Group)的成员,可能是唯一的成员。该节(Section)必须由SHT_GROUP类型的节(Section)引用。 SHF_GROUP标志只能为包含在可重定位对象中的节(Section)设置。SHF_TLS
: 此节(Section)包含线程本地存储(Thread-Local Storage)。进程内的每个线程都有该数据的不同实例。SHF_COMPRESSED
: 标识包含压缩数据的节(Section)。SHF_COMPRESSED仅适用于不可分配的节(Section),不能与SHF_ALLOC一起使用。此外,SHF_COMPRESSED不能应用于SHT_NOBITS类型的节。SHF_MASKOS
: 此掩码中包含的所有位都保留用于特定操作系统的语义。SHF_MASKPROC
: 此掩码中包含的所有位都保留用于特定处理器的语义。SHF_AMD64_LARGE
: x64的默认编译模型仅提供32位偏移量(Displacements)。这种偏移量将节(Section)的大小和最终段(Segment)的大小限制为2GB。此属性标志标识可以容纳超过2GB的节(Section)。此标志允许链接使用不同代码模型的目标文件。
使用小代码模型的对象可以自由引用不包含 SHF_AMD64_LARGE属性标志的x64对象文件节(Section)。包含此标志的节(Section)只能由使用较大代码模型的对象引用。例如,x64中等代码模型对象可以引用包含此属性标志的节(Section)和不包含此属性标志的节(Section)中的数据。但是,x64小代码模型对象只能引用不包含此标志的节(Section)中的数据。SHF_ORDERED
: SHF_ORDERED是SHF_LINK_ORDER提供的功能的旧版本,已被SHF_LINK_ORDER取代。不再支持SHF_ORDERED。
注意:特殊的sh_info值SHN_BEFORE和SHN_AFTER意味着排序后的节(Section)将分别排在排序集中所有其他节(Section)的前面或后面。如果有序集中的多个节(Section)具有这些特殊值之一,则保留输入文件链接行顺序。SHN_BEFORE和SHN_AFTER与使用扩展节索引的对象不兼容。他们已被弃用。
SHF_EXCLUDE
: 此节(Section)不包含在可执行文件或共享对象的链接输入中。如果还设置了SHF_ALLOC标志,或者该节(Section)存在重定位,则忽略此标志。
4.4 sh_link & sh_info
节头(Section Header)中的两个成员sh_link和sh_info包含特殊信息,具体取决于节类型。
Table 4-5:ELF sh_link and sh_info Interpretation
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 与此节(Section)关联的字符串表(String Table)的节头表(Section Header Table)索引。 | 0 |
SHT_HASH | 与此节(Section)关联的符号表(Symbol Table)的节头表(Section Header Table)索引。 | 0 |
SHT_REL/SHT_RELA | 与此节(Section)关联的符号表(Symbol Table)的节头表(Section Header Table)索引。 | 重定位应用到的节(Section)的节头表(Section Header Table)索引,否则为0。 |
SHT_SYMTAB/SHT_DYNSYM | 与此节(Section)关联的字符串表(String Table)的节头表(Section Header Table)索引。 | 最后一个本地符号(STB_LOCAL)的符号表(Symbol Table)索引加1。 |
SHT_GROUP | 与此节(Section)关联的符号表(Symbol Table)的节头表(Section Header Table)索引。 | 相关符号表(Symbol Table)中某一表项的符号表索引。指定符号表表项的名称为节组(Section Group)提供了签名(名称)。 |
SHT_SYMTAB_SHNDX | 与此节(Section)关联的符号表(Symbol Table)的节头表(Section Header Table)索引。 | 0 |
4.5 扩展的节头表(Extended Section Header Table)[^6]
标准ELF数据结构可以表示的节(Section)数量有限制的。
ELF头(ELF Header)的e_shnum和e_shstrndx元素都被限制为能够表示65535个节(Section)。
此外,符号表(Symbol Table)条目可以使用st_shndx元素引用与其关联的节(Section),该元素仅限于能够表示65279个节(Section)。尽管此元素的大小可以表示65535个节(Section),但为特殊符号类型保留了一系列值,SHN_LORESERVE(0xFF00) - SHN_HIRESERVE(0xFFFF)。
为了允许ELF对象包含超过65279个节(Section),提供了许多特殊定义和特殊节类型。由此生成的ELF对象被认为包含扩展节头(Extended Section Header)信息。
在标准ELF对象中,第一个节头(Section Header)是用0填充的。当节数超过ELF头(ELF Header)中相应数据(e_shnum)的大小时,第一个节头(Section Header)元素用于定义扩展ELF头属性。下表显示了这些值。
Table 4-6:Extended Section Header Table Entry: Index 0
名称 | 值 | 含义 |
---|---|---|
sh_name | 0 | 无名称 |
sh_type | SHT_NULL | 没有关联的节(Section) |
sh_flags | 0 | 无标志 |
sh_addr | 0 | 无进程映像中的虚拟地址 |
sh_offset | 0 | 无文件偏移 |
sh_size | e_shnum | 节头表(Section Header Table)中的条目数 |
sh_link | e_shstrndx | 与节头字符串表(Section Header String Table)相关联的节头表(Section Header Table)条目的索引 |
sh_info | e_phnum | 程序头表(Program Header Table)中的条目数 |
sh_addralign | 0 | 无对齐要求 |
sh_entsize | 0 | 不存在具有固定大小的表项的表 |
当使用这个节头0(Section Header 0)信息时,ELF头(ELF Header)的e_shnum元素应该设置为0,e_shstrndx元素应该设置为SHN_XINDEX(0xFFFF)。
SHT_SYMTAB_SHNDX节是一个Elf32_Word值数组。每个值与符号表(Symbol Table)条目一一对应,并以与符号表(Symbol Table)条目相同的顺序出现。这些值表示定义符号表(Symbol Table)条目的节头索引。只有当符号表(Symbol Table)条目的st_shndx字段包含溢出值SHN_XINDEX(0xFFFF)时,相应的 SHT_SYMTAB_SHNDX条目才保存实际的节头索引。否则,SHT_SYMTAB_SHNDX条目必须是SHN_UNDEF(0)。
如果一个ELF文件需要超过65534个程序头(Program Header),则节头表(Section Header Table)的节头0的sh_info元素用于定义程序头(Program Header)的数量,ELF头(ELF Header)的e_phnum元素包含PN_XNUM(0xFFFF)。
5 节(Sections)
节(Sections)包含目标文件中除了ELF头、程序头表、节头表之外的所有信息。此外,目标文件的节(Sections)满足以下几个条件:
- 目标文件中的每一个节(Sections)都只有一个对应的节头(Section Header)来描述它。但是,可能存在没有对应节(Section)的节头(Section Header)。
- 每个节(Sections)在目标文件中是连续的,但是大小可能为0。
- 任意两个节(Sections)不能重叠,即一个字节不能同时存在于两个节(Sections)中。
- 目标文件中可能会有闲置空间(Inactive Space),各种头和节不一定会覆盖到目标文件中的所有字节,闲置区域的内容是未指定的。
许多在ELF文件中的节(Section)都是预定义的,它们包含程序和控制信息。这些节(Section)被操作系统使用,但是对于不同的操作系统,同一节(Section)可能会有不同的类型以及属性。
可执行文件(Executable files)是由链接器将一些单独的目标文件以及库文件链接起来而得到的。其中,链接器会解析不同目标文件之间的引用(子例程的引用以及数据的引用),调整目标文件中的绝对引用,并且重定位指令。加载与链接过程需要目标文件中的信息,并且会将处理后的信息存储在一些特定的节(Section)中,比如.dynamic。
每一种操作系统都会支持一组链接模型,但这些模型都大致可以分为两种:
静态链接(Static)
: 静态链接的文件中所使用的库文件或者第三方库都被静态绑定了,其引用已经被解析了,是一个完全自包含的可执行文件。动态链接(Dynamic)
: 动态链接的文件中所使用的库文件或者第三方库只是单纯地被链接到可执行文件中。当可执行文件执行的时候使用到相应的函数时,相应的函数地址才会被解析。加载此可执行文件时,必须在系统中提供其他共享资源和动态库,才能使程序成功运行。
有一些特殊的节(Section)可以支持调试,比如说.debug以及.line节;支持程序控制的节有.bss,.data,.data1,.rodata,.rodata1。
Table 5-1:
名称 | 类型 | 属性 | 含义 |
---|---|---|---|
.comment | |||
.debug | |||
.dynamic | |||
.dynstr | |||
.dynsym | |||
.got | |||
.line | |||
.plt | |||
5.1 Note Related Sections
1 | typedef struct |
有时候供应商或者系统构建者可能需要使用一些特殊的信息来标记ELF文件,从而其它程序可以来检查该ELF文件的一致性以及兼容性。SHT_NOTE类型的节(Section)和PT_NOTE类型的程序头(Program Header)元素可用于来实现这个目的。节和程序头元素中的Note信息包含任意数量的表项,每一个表项都是目标处理器格式的4字节的字数组。下面出现的标签有助于解释注释信息的组织形式,但是这并不在ELF文件的规范内。
Figure 5-1:Note Information
namesz和name
: name的前namesz字节包含了一个以Null结尾的字符串,这表示该项的拥有者或者发起人。但是目前并没有避免冲突的格式化的命名机制。一般来说,供应商会使用他们自己公司的名字,例如“XYZ Computer Company”作为标识符。如果没有任何名字的话,namesz应该是0。如有必要,需要进行填充,以使name区域4字节对齐。这样的填充大小不包含在namesz中。descsz和desc
: desc的前descsz字节包含了注释(Note)的描述。ELF文件对于描述的内容没有任何约束。如果没有任何描述的话,descsz应该为0。如有必要,需要进行填充,以使desc区域4字节对齐。这样的填充大小不包含在descsz中。type
: 这个字段给出了描述的解释,每个发起者控制自己的类型。对于同一类型来说,有可能有多个描述与其对应。因此,发起者必须能够识别名称和类型以便于来理解对应的描述。目前来说,类型值必须为非负值,ELF文件的规范里并不定义描述符的意思。这也是为什么type在前面。
为了进一步说明,下面给出一个简单的例子。以下注释段(Note Segment)包含两个条目。
Figure 5-2:Example Note Segment
以上注释段(Note Segment)包含两个注释信息(Note Information),第一个注释信息的descsz为0,说明不存在desc。第二个注释信息是一个完整的注释信息。
注意:
1、系统保留没有name(namesz==0)和name长度为0(name[0]==’\0’)的注释信息(Note Information),但目前没有定义类型。所有其他names必须至少有一个非空字符。
2、注释信息(Note Information)是可选的。注释信息(Note Information)的存在不会影响程序的工具接口标准(Tool Interface Standard,TIS)的一致性,前提是该信息不影响程序的执行行为。否则,程序不符合TIS ELF规范并具有未定义的行为。
5.1.1 .note
此节(Section)存储各种注释信息。此节(Section)的节类型是SHT_NOTE。不使用任何节标志(sh_flags)。[^11]
5.1.2 .note.ABI-tag
此节(Section)存储运行此ELF文件的目标操作系统的ABI信息。
此节(Section)用于声明ELF映像的预期运行时API。它可能包括操作系统名称及其运行时版本。此节(Section)的节类型是SHT_NOTE。唯一使用的节标志(sh_flags)是SHF_ALLOC。[^11]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* Defined note types for GNU systems(为GNU Systems定义注释类型). */
/* ABI information. The descriptor consists of words:
word 0: OS descriptor
word 1: major version of the ABI
word 2: minor version of the ABI
word 3: subminor version of the ABI
*/
// Known OSes. These values can appear in word 0 of an NT_GNU_ABI_TAG note section entry.
// 已定义的OS描述符。这些值可以出现在NT_GNU_ABI_TAG注释节条目的desc的第0个字中。
下面是一个.note.ABI-tag节的例子,取自libc6_2.23-0ubuntu11.2_i386的libc-2.23.so:1
2
3
4
5
6
7
8
9
10
11
12
1300000198 _note_ABI_tag dword 'DATA'
00000198 cs:_note_ABI_tag
00000198 ;org 198h
00000198 dd 4 ; namesz
0000019C dd 10h ; descsz
000001A0 dword_1A0 dd 1 ; DATA XREF: sub_E90A0+16B↓o
000001A0 ; type, ELF_NOTE_ABI = 0x1
000001A4 aGnu_0 db 'GNU',0 ; name, ELF_NOTE_GNU = "GNU"
000001A8 dd 0 ; OS: Linux
000001AC dd 2 ; ABI Version: 2.6.32
000001B0 dd 6
000001B4 dd 32
000001B4 _note_ABI_tag ends
你也可以使用readelf对此节(Section)进行查看:1
2
3
4
5
6
7
8$ readelf -n libc-2.23.so
[...]
Displaying notes found at file offset 0x00000198 with length 0x00000020:
Owner Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 2.6.32
[...]
ABI Version: 2.6.32指的是运行编译器的主机上的Glibc构建时使用的内核头文件版本。从广义上讲,它显示了可执行文件能够运行的系统的最小内核版本。如果程序在小于2.6.32的内核上运行,则会显示内核太旧警告。
参考
:linux command “file” shows “for GNU/Linux 2.6.24”
5.1.3 .note.gnu.build-id
此节(Section)存储此ELF文件的Build ID信息。Build ID被用来唯一标识一个链接文件。
此节(Section)用于保存唯一标识ELF映像内容的ID。具有相同build ID的不同文件应该包含相同的可执行内容。有关更多详细信息,请参阅GNU链接器(ld (1))的–build-id选项。此节(Section)的节类型是SHT_NOTE。唯一使用的节标志(sh_flags)是SHF_ALLOC。[^11]
ld --build-id=style
: ld
请求创建“.note.gnu.build-id”ELF注释节(Section)。注释的内容是标识此链接文件的唯一标识。标识的样式可以是使用128个随机位的“uuid”,对输出内容的标准部分使用160位SHA1散列的“sha1”,对输出内容的标准部分使用128位md5散列的“md5”,或者使用指定为偶数个十六进制数字的选定位串的”0x hexstring”(忽略数字对之间的”-“和”:”字符)。如果省略样式,则使用“sha1”。
“md5”和“sha1”样式产生的标识符在相同的输出文件中总是相同的,但在所有不相同的输出文件中是唯一的。它不打算作为文件内容的校验和进行比较。链接文件稍后可能会被其他工具更改,但标识原始链接文件的Build ID位字符串不会更改。
为style传递“none”会禁用命令行前面任何“–build-id”选项的设置。
1 | /* Build ID bits as generated by ld --build-id. The descriptor consists of any nonzero number of bytes. */ |
下面是一个.note.gnu.build-id节的例子,取自libc6_2.23-0ubuntu11.2_i386的libc-2.23.so:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17root@kali:~/Desktop# file libc-2.23.so
libc-2.23.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so , BuildID[sha1]=bde4e8b0230b1b474cd8a1ca6e9f81bb2b438914, for GNU/Linux 2.6 , stripped
00000174 _note_gnu_build_id dword 'DATA'
00000174 cs:_note_gnu_build_id
00000174 ;org 174h
00000174 dword_174 dd 4 ; DATA XREF: .program.header:off_DC↑o
00000174 ; namesz
00000178 dd 14h ; descsz
0000017C dd 3 ; DATA XREF: .rodata:0014CAE0↓o
0000017C ; type, NT_GNU_BUILD_ID = 0x3
00000180 aGnu db 'GNU',0 ; name, ELF_NOTE_GNU = "GNU"
00000184 ; desc, BuildID[sha1]=bde4e8b0230b1b474cd8a1ca6e9f81bb2b438914
00000184 byte_184 db 0BDh, 0E4h, 0E8h, 0B0h, 23h, 0Bh, 1Bh, 47h, 4Ch, 0D8h
00000184 ; DATA XREF: .eh_frame:off_177E90↓o
00000184 db 0A1h, 0CAh, 6Eh, 9Fh, 81h, 0BBh, 2Bh, 43h, 89h, 14h
00000184 _note_gnu_build_id ends
你也可以使用readelf对此节(Section)进行查看:1
2
3
4
5
6
7$ readelf -n libc-2.23.so
Displaying notes found at file offset 0x00000174 with length 0x00000024:
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: bde4e8b0230b1b474cd8a1ca6e9f81bb2b438914
[...]
5.1.4 .note.gnu.property
binfmt_elf: Extract .note.gnu.property from an ELF file
1 | /* Program property. */ |
5.1.5 .note.GNU-stack
.note.GNU-stack的内容告诉系统将ELF加载到内存时如何控制堆栈。
此节(Section)用于在Linux目标文件(Odject File)中声明堆栈属性。此节(Section)的节类型是SHT_PROGBITS。唯一使用的节标志(sh_flags)是SHF_EXECINSTR。这向GNU链接器表明目标文件(Odject File)需要一个可执行堆栈。[^11]
参考
:
1、Hardened/GNU stack quickstart
2、Why data and stack segments are executable?
5.1.6 .note.stapsdt
LTTng是Linux的开源跟踪框架。
使用LTTng的用户空间动态跟踪通过Hook到代码中的预定位置来工作,这些位置可以是ELF函数(ELF functions)或SDT探针(Statically Defined Tracing Probes)。
ELF文件的函数列在文件元数据的符号表(Symbol Table)中。您无需执行任何特殊操作即可使这些函数可以用于跟踪(Tracing)。
另一方面,SDT探针(SDT probes)由其作者单独插入到应用程序(Apps)和库(Libraries)中,并且需要额外的工具链支持。SDT探针(SDT probes)用于许多Linux发行版上的各种库和应用程序,它们是通过在源代码中使用STAP_PROBE*系列宏的任何位置插入nop指令来实现的。使用nop指令作为占位符允许在附加探针时插入其他可执行指令,但在禁用它们时几乎不会影响性能。
无论您是在跟踪ELF函数(ELF functions)还是SDT探针(SDT probes),实际的探针插入和事件记录都由Linux内核的uprobe接口处理,并且事件记录在内核的缓冲区中。这意味着您需要一个以root身份运行的会话守护程序来加载所需的内核模块并使用此功能。
附加到一个SDT探针
:
与跟踪库函数不同,开发人员明确插入SDT探针以帮助调试和跟踪,这意味着它们被战略性地(Strategically)放置在代码中您可能需要附加到探针的位置。例如,libc:memory_sbrk_more探针位于libc的malloc()中,当需要分配更多内存来为内存请求提供服务时,会记录sbrk()系统调用的参数。
在附加到SDT探针之前,您需要知道您正在使用的应用程序库中有哪些探针可用。您可以通过使用readelf查看ELF注释节(Notes Section)来发现探针列表。例如,这里是readelf显示的libc.so.6库的ELF注释节(Notes Section)的一部分。
1 | $ readelf -n /lib/x86_64-linux-gnu/libc.so.6 |
您感兴趣的是stapsdt拥有的带有NT_STAPSDT描述的注释部分。这些注释部分显示了库或应用程序中的所有探针,以及每个条目中最重要的部分,Provider和Name - 附加到探针时您将需要这两个部分。
参考
:The new dynamic user space tracing feature in LTTng
5.1.7 .stapsdt.base
SystemTap是一个Linux非常有用的调试(跟踪/探测)工具,常用于Linux内核或者应用程序的信息采集,比如:获取一个函数里面运行时的变量、调用堆栈,甚至可以直接修改变量的值,对诊断性能或功能问题非常有帮助。SystemTap提供非常简单的命令行接口和很简洁的脚本语言,以及非常丰富的tapset和例子。
Systemtap使用编译器宏来注册它的SDT探针,因此不可能在运行时注册探针。下面显示了一个示例,我们将一个名为Probe的探针注册到一个名为Provider的提供者。
1 |
|
此代码生成的二进制文件将有一个名为.stapsdt.base的新的ELF节(Section),位于代码之后(通常是.text节)。此基址(Base)与帮助跟踪工具在二进制文件加载到内存后计算任何探针的内存地址相关。
它还将有一个ELF注释节(Notes Section),其中将存储所有探针数据[名称(name)、地址(address)、信号量(semaphores)、参数(arguments)]以供任何跟踪工具稍后读取。编译器还将用函数调用替换我们的DTRACE_PROBE宏,这就是探针指向的地方,从而可以轻松地将参数传递给探针。此函数是空操作(no-op)。
1 | Displaying notes found at file offset 0x00001064 with length 0x0000003c: |
这里有更多关于Systemtap如何实现SDT探针的信息。
参考
:libstapsdt - How Systemtap SDT works
5.1.8 .note.openbsd.ident
OpenBSD原生可执行文件通常包含此节(Section)来标识自己,因此内核可以在加载文件时绕过任何兼容性ELF二进制仿真测试。[^11]
5.2 Hash Related Sections
高版本GCC编译完的程序,一般只包含.gnu.hash节。我们可以使用如下参数编译程序,使其拥有两种或两种中的一种Hash表。
1 | gcc test.c -Wl,--hash-style=sysv -o test # 只有.hash |
上面的命令行参数的意义如下:
-Wl,<options\>
:传递以逗号分隔的选项给链接器(Linker)。--hash-style=
:链接器(Linker)选项,设置链接器(Linker)给编译后的ELF文件添加的Hash表的类型。
5.2.1 Hash函数
符号名中@及其后的所有字符都不参与Hash计算。
使用下列命令,可以查看某种Hash算法在某一对象中的表现情况:
1 | $ readelf -I libc-2.23-64.so |
Length表具有相同(hash % nBucket)值的符号对应的Chains[]表元素形成的Chain中的元素数量,Number为某一Chain的数量,也是相应Buckets[]元素的数量(其保存Chain首元素的索引)。
5.2.1.1 .hash Hash函数[^1]
.hash使用的Hash函数是ELF Hash(Sysv Hash)。
1 | unsigned long elf_hash(const unsigned char *name) |
5.2.1.2 .gnu.hash Hash函数[^8]
.gnu.hash使用的Hash函数是DJB Hash,该Hash算法由Bernstein(Daniel J Bernstein)教授在多年以前发表在comp.lang.c新闻组(usenet newsgroup):
1 | uint32_t dl_new_hash(const char *s) |
如果你在线搜索这个算法,你会发现表达式“h = h * 33 + c”
以另一种方式表示:
1 | h = ((h << 5) + h) + c |
它们是等价的语句,将整型乘法运算替换成成本更低的移位和加法运算。实际中是否能够降低成本取决于使用的CPU。对旧机器来说这两种形式将会有显著差异,但是对当代机器来说整型乘法运算相当的快。
该算法的另一种变化是将返回值剪切为31位:
1 | return h & 0x7fffffff; |
然而,GNU Hash使用完整的32位无符号结果。
GNU binutils实现使用uint_fast32_t类型来计算哈希。此类型被定义为当前系统上能够表示至少32位的最快的可用整数型机器类型。由于可能使用更宽的类型实现,结果在返回之前被显式地剪切为32位的无符号值。
1 | static uint_fast32_t dl_new_hash(const char *s) |
5.2.2 .hash
5.2.2.1 .hash表结构
标准哈希表(.hash)由链接器(Link Editor)构建。此节(Section)的节类型是SHT_HASH,使用的属性是SHF_ALLOC。动态节(Dynamic Section)中此节的节标记为DT_HASH。它的结构非常简单:
1 | // Pseudo-C |
哈希表(Hash Table)由Elf32_Word或Elf64_Word对象组成,其提供对符号表(Symbol Table)的访问。与哈希表(Hash Table)中哈希相关联的符号表(Symbol Table)在哈希表的节头(Section Header)的sh_link条目中指定。下图中使用标签来帮助解释哈希表(Hash Table)的组织形式,但这些标签不是此规范的一部分。
Figure 5-3:Symbol Hash Table
Buckets表包含nBucket个项,Chains表包含nChain个项。索引从0开始。Buckets和Chains都保存符号表索引。Chains表项与符号表(Symbol Table)项是平行的。符号表项的数量应等于nChain,因此符号表索引也可以索引Chains表项。
一个接受符号名称的Hash函数,返回一个值来计算Bucket表索引。因此,如果Hash函数返回某个符号名称的hash值,通过Buckets[hash % nBucket]可以得到一个索引index。该索引是符号表和Chain表的索引。如果符号表项不包含所需的符号名称,Chains[index]给出具有相同(hash % nBucket)值的下一个符号的符号表项索引。
可以跟踪Chain链,直到所选符号表项包含所需名称,或者Chain表项包含值STN_UNDEF。这个特殊的索引值表示Chain的结尾,意味着在Chain表中找不到更多的符号表索引。[^6]
总结如下
:Buckets[hash % nBucket]
:给出动态符号表(Dynamic Symbol Table)符号项的索引或Chains数组的初始索引。根据动态链接符号的数量来获得。(binutils/bfd/elflink.c有具体细节)Chains[index]
:给出Chains[]中的下一个项的索引(与前一项具有相同的(hash % nBucket)值)。index
:STN_UNDEF标记Chain的结尾(具有相同Hash值的符号对应的Chains[]元素形成的Chain)。
参考
:binutils/bfd/elflink.c有具体细节,bfd_elf_size_dynsym_hash_dynstr()。
查找过程是这样的:
1 | const Elf64_Sym* lookup_symbol( |
5.2.2.2 符号查找实例
首先我们写一个计算符号名Hash的程序:
1 | // gcc hash.c -o hash |
这里使用libc6_2.23-0ubuntu11.2_amd64的libc-2.23-64.so(我重命名了)来做测试,使用64位的程序,主要是为了证明不管是32位程序还是64位程序,通过ELF Hash来进行符号查找,Hash的位数都是32位,而且.hash表中的内容由Elf32_Word(32位)或Elf64_Word(32位)对象组成。我们随便找一个示例程序中的符号来进行测试,以“putwchar”为例:
1 | $ readelf -s libc-2.23-64.so |
从上面的输出信息中,我们可以知道动态符号表(Dynamic Symbol Table)总共有2245项,符号“putwchar”的索引为10。通过上面计算Hash的函数进行计算“putwchar”的Hash:
1 | $ ./hash putwchar |
接下来,我们需要计算Bucket表索引,使用Bucket[hash % nBucket]公式。所以我们需要知道nBucket的值。我们将libc-2.23-64.so使用IDA打开,找到.hash节所在位置,IDA不能很好地识别.hash,所以需要我们自己添加一个Section,并使用IDA Python脚本对.hash节的内容进行解析,结果如下:
1 | .hash:001BC8A8 _hash segment byte public '' use32 |
我们可以知道nBucket为0x3F9=1017,nChain为0x8C5=2245。nChain等于动态符号表(Dynamic Symbol Table)的项数。接下来我们计算Bucket表索引,并得到对应Bucket表项的值:
1 | Bucket[hash % nBucket] = Bucket[0xCBD99F2 % 0x3F9] = Bucket[0x107] = 0x0Ah = 10 |
我们可以看到Bucket数组相应表项的内容就是所查找符号在动态符号表(Dynamic Symbol Table)中的索引。
我们可以使用如下命令查看符号表中符号索引与Buckets[]数组索引的对应关系:
1 | $ readelf -sD libc-2.23-64.so |
Num列为动态符号表(.dynsym)索引,Buc列为Buckets[]数组索引。我们可以看到有三个符号都与Buckets[0]相关联,这三个符号计算出的Hash值对nBucket求模,结果都是0。我们可以通过以下方法访问到这三个符号:
1 | Buckets[0](469) --> Chains[469](949) --> Chains[949](1604) |
5.2.3 .gnu.hash
GNU哈希表(.gnu.hash)是标准哈希表(.hash)的更有效的替代方案。两者都可以出现在同一个ELF文件中,但现代GNU ELF文件通常只包含GNU哈希表。此节(Section)的节类型是SHT_GNU_HASH,动态节(Dynamic Section)中此节的节标记为DT_GNU_HASH。
主要区别
:
- 它添加了一个Bloom Filter以加速无效查找(Negative Lookups)。无效查找是常见的情况,因为符号是按顺序在不同的ELF文件中搜索的。
- 它在Hash Chain的每个条目中保存哈希值以避免无用的字符串比较。(比较Hash值比比较字符串更快)
- 通过避免在哈希表内存中跳转,它对缓存更加友好。
- 它使用DJB Hash函数。
.gnu.hash节由四个独立的部分组成,顺序如下:
Header
:一个提供节参数的32位(4字节)字数组。Bloom Filter
:此Filter用于快速拒绝在对象中不存在的符号的查找请求。Hash Buckets
:一个拥有nbuckets个32位Hash Bucket的数组。第N个Bucket字的内容为动态符号表(Dynamic Symbol Table)中具有相同(hash % nbuckets == N)的符号项的最低索引。Hash Chains(Hash Values)
:GNU Hash节的最后一部分包含(dynsymcount - symndx)个32位字,动态符号表(Dynamic Symbol Table)的第二部分中的每个符号对应一个条目。
.gnu.hash表的结构如下所示:
1 | // Pseudo-C |
Header、Bloom Filter、Buckets和Chains始终是32位字,而Bloom Filter可以是32位或64位字,具体取决于ELF对象的类别。这意味着ELFCLASS32类型的.gnu.hash节仅包含32位字,因此将它们的节头中的sh_entsize字段设置为4。ELFCLASS64类型的.gnu.hash节既包含32位字,也包含64位字,因此将sh_entsize设置为0。
假设.gnu.hash节已正确对齐以访问ELFCLASS大小的字,则在Bloom Filter之前紧接着的32位字(4个)确保了Filter Mask字始终正确对齐并可直接在内存中访问。
5.2.3.1 Dynamic Section Requirements[^8]
GNU Hash节(.gnu.hash)对动态符号表(Dynamic Symbol Table)的内容设置了一些额外的排序要求。这与标准SVR4 Hash节(.hash)形成了对比,后者允许符号按ELF标准允许的任何顺序排列。
一个标准的SVR4哈希表(.hash)包含动态符号表(Dynamic Symbol Table)中的所有符号。然而,其中一些符号永远不会通过.gnu.hash中查找到:
- 本地符号(LOCAL Symbols):除非被重定位引用(在某些架构上)
- 文件符号(FILE Symbols)
- 对于共享对象(Sharable Objects):所有UNDEF符号
- 对于可执行文件(Executables):任何未被PLT引用的UNDEF符号
- 特殊索引0对应的符号(UNDEF的一种特殊情况)
上面所说都是针对.gnu.hash节的,我经过测试libc6_2.23-0ubuntu11.2_amd64的libc-2.23-64.so(我重命名了)中的符号“_rtld_global”是UNDEF的,但是可以在.hash表中找到其在动态符号表(Dynamic Symbol Table)中的索引。
从哈希表节(Hash Table Section)省略这些符号不会影响正确性,并且会导致更少的哈希表拥塞(Congestion)、更短的哈希链(Chains)以及相应地更好的哈希性能。
使用GNU哈希(.gnu.hash),动态符号表(Dynamic Symbol Table)被分为两部分。第一部分接收可以从哈希表(.hash)中省略的符号。GNU哈希(.gnu.hash)不会对动态符号表(Dynamic Symbol Table)的这一部分中的符号强加任何特定的顺序。
动态符号表(Dynamic Symbol Table)的后半部分接收可从.gnu.hash访问的符号。这些符号需要使用上述GNU Hash函数进行升序排序(hash % nBuckets)。Hash Buckets的数量(nBuckets)记录在GNU Hash节中,如下所述。因此,在单个哈希链(Hash Chain)中找到的符号的符号表项和Chains[]数组元素在内存中都是相邻的,从而获得更好的缓存性能。
5.2.3.2 Header
Header中包含4个32位字的节参数:
nBuckets
:Buckets[]中元素的数量。根据动态链接符号的数量来获得(最小值为2)。Symndx
:动态符号表(.dynsym)具有dynsymCount个符号,Symndx是动态符号表(.dynsym)中可通过.gnu.hash表访问的第一个符号的索引。这意味着可通过.gnu.hash表访问(dynsymCount - Symndx)个符号。Maskwords
:.gnu.hash节中Bloom Filter部分中ELFCLASS大小的掩码字(Mask Word)的数量。该值必须为非0,并且必须是2的幂,如下所述。请注意,值0可以解释为.gnu.hash中不存在Bloom Filter。然而,GNU链接器(GNU linkers)不会这样做 —— .gnu.hash节总是包含至少1个掩码字(Mask Word)。Shift2
:用于Bloom Filter产生第二个Hash函数的移位计数。细节见下方。
参考
:binutils/bfd/elflink.c有具体细节,bfd_elf_size_dynsym_hash_dynstr()。
5.2.3.3 Bloom Filter
GNU Hash节包括一个Bloom Filter。Bloom Filter是概率性的(Probabilistic),这意味着可能会出现误报(False Positives),但不会出现漏报(False Negatives)(换句话说,就是通过Bloom Filter的不一定在Hash表中,但不通过的一定不在Hash表中)。此Bloom Filter用于快速拒绝在对象中不存在的符号的查找请求,从而避免更耗时的哈希查找操作。通常,一个进程中只有一个对象具有给定的符号。跳过对所有其他对象的哈希操作可以大大加快符号查找速度。
Bloom Filter由Maskwords个掩码字(Mask Word)组成,每个字是32位(ELFCLASS32)或64位(ELFCLASS64),具体取决于ELF对象的类别。在下面的讨论中,C将用于代表一个掩码字(Mask Word)的大小(以bit为单位)。掩码字(Mask Word)共同组成一个拥有(C * Maskwords)个位的逻辑位掩码(Logical Bitmask)。
GNU Hash使用k=2的Bloom Filter,这意味着每个符号使用两个独立的Hash函数。Bloom Filter参考包含以下语句:
对于较大的k,不需要设计k个不同的独立Hash函数。对于具有宽输出的良好Hash函数,此类Hash的不同位域(bit-fields)之间应该几乎没有相关性,因此这种类型的Hash可用于通过将其输出切片为多个位域(bit-fields)来生成多个“不同”的Hash函数。
GNU Hash所使用的Hash函数具有此属性。利用这一事实,可以从上面描述的单个Hash函数中产生Bloom Filter所需的两个Hash函数:
1 | H1 = dl_new_hash(name); |
如上所述,链接器(Link Editor)确定要使用多少个掩码字(Maskwords个),以及第一个Hash结果右移以产生第二个Hash结果的移位数(Shift2)。使用的掩码字(Mask Word)越多,Hash节越大,但误报率(False Positives)越低。我在私人电子邮件中被告知,GNU链接器(GNU Linker)首先从输入到哈希表(Hash Table)中的符号数(dynsymCount - Symndx)的以2为底的对数得到Shift2,对于ELFCLASS32的最小值为5,对于ELFCLASS64的最小值为6。这些值明确记录在哈希节(Hash Section)中,以便链接器(Link Editor)在将来出现更好的启发式方法时可以灵活地更改它们。
Bloom Filter掩码为两个Hash值分别设置一位。根据Bloom Filter参考,要设置的bit所在的掩码字(Mask Word)和要设置的具体bit将按如下计算方法计算:
1 | N1 = ((H1 / C) % Maskwords); // 要设置的bit所在的掩码字(Mask Word) |
在构建Filter时设置位:
1 | Bloom[N1] |= (1 << B1); |
然后对Filter进行测试:
1 | (Bloom[N1] & (1 << B1)) && (Bloom[N2] & (1 << B2)) |
GNU Hash与上述内容有很大的不同。不是分别计算N1和N2,而是使用单个掩码字(Mask Word),对应于上面的N1。这是GNU Hash开发人员为优化缓存行为而做出的合理的(Conscious)决定:
这使得Bloom Filter的2个Hash函数比使用两个不同的N时更加依赖,但在我们的测试中,拒绝应该被拒绝的查找的比率仍然非常好,并且对缓存更加友好。在查找期间尽可能少地接触缓存行(Cache Lines)是非常重要的。
因此,在GNU Hash中,单个掩码字(Mask Word)实际上通过如下方式进行计算:
1 | N = ((H1 / C) % Maskwords); |
设置Bloom Filter掩码字N中的两个位的方式如下所示:
1 | BITMASK = (1 << (H1 % C)) | (1 << (H2 % C)); |
链接器(Link-Editor)设置这些位的方式如下:
1 | Bloom[N] |= BITMASK; |
5.2.3.3.1 Bit Fiddling: 为何maskwords应为2的幂
通常,Bloom Filter可以使用任意数量的字(Words)来构造。但是,如上所述,GNU Hash要求Maskwords是2的幂(Maskwords只有1位为1)。这个要求允许下面的取模操作写成一个简单的掩码操作:
1 | N = ((H1 / C) % Maskwords); |
注意(Maskwords - 1)可以预先计算一次,然后用于每一个Hash:
1 | MASKWORDS_BITMASK = Maskwords - 1; |
5.2.3.3.2 Bloom Filter特殊情况
Bloom Filter有两个有趣的特殊情况:
- 当Bloom Filter的所有位都设置时,所有测试都会产生一个True值(接受符号查找请求)。GNU链接器(GNU Linker)利用这一点,当它想要“禁用”Bloom Filter时,.gnu.hash节包含一个所有位都设置的1个掩码字(Mask Word)的Bloom Filter。Bloom Filter仍然存在,并且仍在使用,开销最小,但它让一切都通过。
- 掩码字(Mask Word)中的任何一位都没有设置的Bloom Filter在所有情况下都将返回False。这种情况在ELF文件中比较少见,因为不导出符号的对象的应用有限。然而,有时对象是以这种方式构建的,其依赖于.init/.fini节来运行来自对象的代码。
5.2.3.4 Buckets[]
Bloom Filter之后是nBuckets个32位字。第N个字的内容为动态符号表(Dynamic Symbol Table)中具有相同(hash % nBuckets)结果的符号项的最低索引,其中:
1 | (dl_new_hash(symname) % nBuckets) == N |
由于动态符号表(Dynamic Symbol Table)按相同的键(hash % nBuckets)排序,因此dynsym[Buckets[N]]是包含所需符号(如果存在)的Hash Chain的第一个符号。
如果Hash Table中没有给定值N的符号,则Bucket[]数组元素将包含索引0。由于dynsym的索引0是保留值,该索引不会出现在有效符号中,因此是没有歧义的。
5.2.3.5 Chains[]
GNU Hash节的最后一部分Chains[]包含(dynsymCount - Symndx)个32位字,动态符号表(Dynamic Symbol Table)的第二部分中的每个符号对应一个Chains[]条目。每个字的高31位包含相应符号Hash的高31位。最低有效位用作停止位。当一个符号是给定Hash Chain中的最后一个符号时,它被设置为1:
1 | lsb = (N == dynsymCount - 1) || |
由于Chains[]的元素中保存符号的Hash值,此节也被称作Values[]。
5.2.3.6 使用.gnu.hash节查找符号
下面显示了如何使用.gnu.hash节在对象中查找符号。我们将假设存在包含所需信息的内存记录:
1 | typedef struct { |
为了简化问题,我们省略了处理不同ELFCLASS的细节。在上面,Word是一个32位无符号值,BloomWord是32位还是64位取决于ELFCLASS,而Sym是Elf32_Sym或Elf64_Sym。
给定一个包含上述对象信息的变量,以下伪代码返回一个指向所需符号的符号表项的指针(如果该符号存在于对象中),否则返回NULL。
1 | Sym *symhash(obj_state_t *os, const char *symname) |
5.2.3.7 符号查找实例
首先我们写一个计算符号名Hash的程序:
1 | // gcc gnu_hash.c -o gnu_hash |
这里依然使用libc6_2.23-0ubuntu11.2_amd64的libc-2.23-64.so(我重命名了)来做测试,我们随便找一个示例程序中的符号来进行测试,以“__gethostname_chk”为例:
1 | $ readelf -s libc-2.23-64.so |
从上面的输出信息中,我们可以知道动态符号表(Dynamic Symbol Table)总共有2245项,符号“__gethostname_chk”的索引为12。通过上面计算Hash的函数进行计算“__gethostname_chk”的Hash:
1 | $ ./gnu_hash __gethostname_chk |
要查找符号,我们需要知道.gnu.hash节的Header中的4个元素(nBuckets、Symndx、Maskwords、Shift2),以及Bloom[]、Buckets[]、Chains[]的位置。我们将libc-2.23-64.so使用IDA打开,找到.gnu.hash节所在位置,IDA可以很好地识别.gnu.hash,结果如下:
1 | 00000000000002B8 _gnu_hash segment para public '' use64 |
接下来我们需要通过Bloom Filter来验证当前对象中是否存在“__gethostname_chk”符号,首先通过计算出的Hash值得到Bloom Filter中要验证的掩码字在Bloom[]中的索引,以及改掩码字中相应的bit:
1 | H1 = dl_new_hash(“__gethostname_chk”) = 0x8ADCAD37 |
由上面的计算可知“__gethostname_chk”对应的Bloom Filter掩码字为Bloom[0xB4],其中的第55位和第50位都已经被设置了,说明“__gethostname_chk”可能存在于当前对象中。然后,我们通过相应的Bucket[]元素得到符号表索引/Chains[]索引:
1 | Buckets[H1 % nBuckets] = Buckets[0x8ADCAD37 % 0x3F3] = Buckets[1] = 0xA = 10 |
由此可知,当前符号表索引对应的符号并不是要查找的符号,我们接下来对Chain进行遍历:
1 | Symndx = 10 |
如上可知,我们在具有相同(hash % nBuckets)的Chain中,找到最后一个,终于找到了待查找符号“__gethostname_chk”所对应的Chains[]表元素。
我们可以使用如下命令查看符号表中符号索引与Buckets[]数组索引的对应关系:
1 | $ readelf -sD libc-2.23-64.so |
Num列为动态符号表(.dynsym)索引,Buc列为Buckets[]数组索引。我们可以看到有三个符号都与Buckets[1]相关联,这三个符号计算出的Hash值对nBucket求模,结果都是1。如前所述,此对象中不存在(hash % nBuckets)为0的符号,所以Buckets[0]为0。我们还可以看到一个规律,就是Num和Buc都是按顺序排列的,这是因为动态符号表(.dynsym)中的符号使用(hash % nBuckets)进行升序排序。这样就会使具有相同(hash % nBuckets)的动态符号表符号项和相应的Chains[]表表项在内存中是相邻的,查找符号时,我们只需要得到具有相同(hash % nBuckets)的动态符号表符号项的最小索引对应的符号项的指针,以及相应的Chain的头元素的指针,然后通过增加指针的值,就可以不用在Chains[]表中来回跳转。
5.3 Version Releated Sections[^14][^15]
本部分介绍符号版本控制机制(Symbol Versioning Mechanism)。所有ELF对象都可能提供或依赖于版本化的符号(Versioned Symbols)。符号版本控制由3个节类型(Section Types)实现:SHT_GNU_versym(.gnu.version)、SHT_GNU_verdef(.gnu.version_d)和SHT_GNU_verneed(.gnu.version_r)。
动态库定义的符号版本信息保存在.gnu.version_d节,可执行程序和动态库引用其它动态库所定义的符号版本信息保存在.gnu.version_r节。
以下描述和代码片段中的前缀Elfxx代表“Elf32”或“Elf64”,具体取决于ELF对象运行的处理器架构。
版本由字符串描述。用于符号版本的结构还包含一个成员,该成员保存字符串的ELF Hash值。这允许更有效的处理。
符号的版本控制机制并没有被广泛应用,主要使用在GNU的C库中,用来提供符号的版本信息,以实现共享库的后向兼容。简单的说,一个符号可能有一个或多个版本的实现,多个实现在源码层次就有多段不同的代码,但在链接层次只向外提供一个相同的符号名,然后用版本信息(如@GLIBC_2.0,两个@表示默认版本)来区别不同的实现。至于程序执行时使用哪个版本的实现跟该程序在编译时所引用的版本有关。
.gnu.version、.gnu.version_d和.gnu.version_r等对应的节头(Section Header)中sh_link和sh_info值的意义,如下所示:
1 | $ readelf -S libc-2.23.so |
其中,sh_link是与此节相关联的节的索引值,如下图所示:
Table 5-2:符号版本相关节的节头中sh_link和sh_info的含义
sh_type | sh_name | sh_link | sh_info |
---|---|---|---|
SHT_GNU_versym | .gnu.version | 与此节(Section)关联的符号表(.dynsym)的节头表(Section Header Table)索引。 | 0 |
SHT_GNU_verdef | .gnu.version_d | 与此节(Section)关联的字符串表(.dynstr)的节头表(Section Header Table)索引。 | 本节中Elfxx_Verdef结构的数量。 |
SHT_GNU_verneed | .gnu.version_r | 与此节(Section)关联的字符串表(.dynstr)的节头表(Section Header Table)索引。 | 本节中Elfxx_Verneed结构的数量。 |
5.3.1 .gnu.version
具有SHT_GNU_versym节类型的特殊节.gnu.version应包含符号版本表(Symbol Version Table)。该节(Section)应与.dynsym节中的动态符号表(Dynamic Symbol Table)具有相同数量的条目,并且一一对应。
.gnu.version节应包含一个Elfxx_Versym类型的元素数组(版本信息索引表),每个表项的长度为sizeof(Elf32_Half),2个字节,每个条目指定为动态符号表(Dynamic Symbol Table)中的相应符号定义(Defined)或要求(Required)的版本。
1 | /* Type for version symbol information. */ |
符号版本表(Symbol Version Table)中的值特定于它们所在的对象。这些值是由Elfxx_Vernaux结构的vna_other成员或Elfxx_Verdef结构的vd_ndx成员提供的符号版本标识符,一个索引。
值0和1被保留。
- 0 - 该符号是局部的,在对象之外不可用。
- 1 - 该符号在此对象中定义并且全局可用。
所有其他值用于标识位于其他符号版本节(Symbol Version Sections)中定义或所需的符号版本。值本身不是与符号关联的符号版本。由值标识的字符串定义了符号的版本。
Table 5-3:符号版本表内容的一些特殊值
宏名称 | 值 | 含义 |
---|---|---|
VER_NDX_LOCAL | 0 | 局部符号。 |
VER_NDX_GLOBAL | 1 | 全局符号。 |
VER_NDX_LORESERVE | 0xff00 | 保留条目的开始。 |
VER_NDX_ELIMINATE | 0xff01 | 被淘汰的符号。 |
我们可以通过readelf命令查看此节的内容:
1 | $ readelf -V libc-2.23.so |
5.3.2 .gnu.version_d
具有SHT_GNU_verdef节类型的特殊节.gnu.version_d应包含符号版本定义(Symbol Version Definitions)。本节中的条目数应包含在动态节.dynamic的节标记为DT_VERDEFNUM的条目中。
该节应包含一个Elfxx_Verdef结构数组,每一个Elfxx_Verdef结构,可选地后跟一个Elfxx_Verdaux结构数组。
.gnu.version_d节只存在于动态库文件中,表示自身所定义的符号的版本信息。
我们可以通过readelf命令查看此节的内容:
1 | $ readelf -V libc-2.23.so |
5.3.2.1 Elfxx_Verdef
1 | typedef struct { |
vd_version
:修订版本(Version Revision)。该字段应设置为1。vd_flags
:版本信息标志(Version Information Flag)位掩码。一般为0。vd_ndx
:节类型为SHT_GNU_versym的节(.gnu.version)中引用的符号版本的标识符,一个索引值。与Elfxx_Vernaux结构中的vna_other含义差不多。将.gnu.version表条目与相应的版本定义相关联。vd_cnt
:与此符号版本定义相关联的Elfxx_Verdaux结构数组条目的数量。同一符号可能有多个版本。vd_hash
:版本名称哈希值(用ELF Hash函数计算)。与此符号版本定义相关联的Elfxx_Verdaux结构数组的第一个Elfxx_Verdaux之中的vda_name对应的版本名称的哈希值。vd_aux
:Elfxx_Verdaux结构数组中第1个条目距当前Elfxx_Verdef结构起始的字节偏移量(一般为Elfxx_Verdef结构的字节大小)。Elfxx_Verdaux结构数组的第一个条目必须存在。此条目(Elfxx_Verdaux)包含指向此结构(Elfxx_Verdef)定义的符号版本的版本字符串。可以存在附加条目(Elfxx_Verdaux)。条目的数量由vd_cnt值给出。这些条目代表此符号版本定义(Elfxx_Verdef)的依赖关系。这些依赖项中的每一个都有自己的符号版本定义结构(Elfxx_Verdef)。vd_next
:下一个Elfxx_Verdef结构距当前Elfxx_Verdef结构起始的偏移量(Elfxx_Verdef结构大小+vd_cnt个Elfxx_Verdaux的大小),以字节为单位。0表示当前Elfxx_Verdef结构为最后1个。
Table 5-4:合法的vd_version值
宏名称 | 值 | 含义 |
---|---|---|
VER_DEF_NONE | 0 | 无版本。 |
VER_DEF_CURRENT | 1 | 当前版本。 |
VER_DEF_NUM | 2 | 给定版本号。 |
Table 5-5:合法的vd_flags值
宏名称 | 值 | 含义 |
---|---|---|
VER_FLG_BASE | 0x1 | 文件本身的版本定义。 |
VER_FLG_WEAK | 0x2 | 弱版本标识符。 |
当符号版本定义(Version Definitions)或符号自动缩减(Symbol Autoreduction)已应用于文件时,基本符号版本定义(Base Version Definition)始终存在。基本符号版本(Base Version)为文件保留符号提供了默认符号版本。弱符号版本定义(Weak Version Definition)没有与符号版本关联的符号。
5.3.2.2 Elfxx_Verdaux
1 | typedef struct { |
vda_name
:符号版本或依赖项名称字符串的偏移量(.dynstr),以字节为单位。GLIBC_2.2.5、libc.so.6。vda_next
:到下一个Elfxx_Verdaux条目的偏移量(也就是Elfxx_Verdaux结构的大小),以字节为单位。
5.3.3 .gnu.version_r
.gnu.version_r节存在于可执行文件和动态库(动态库也会引用其他动态库中的符号)中,表示所引用的符号的版本信息。
具有SHT_GNU_verneed节类型的特殊节.gnu.version_r应包含所需的符号版本定义(Required Symbol Version Definitions)。本节中的条目数应包含在动态节.dynamic的节标记为DT_VERNEEDNUM的条目中。
本节通过指示这些依赖项(Dependencies)所需的版本定义(Version Definitions)来补充文件的动态依赖项要求(Dynamic Dependency Requirements)。仅当依赖项包含符号版本定义时,才会在此节进行记录。
该节应包含一个Elfxx_Verneed结构数组,每一个Elfxx_Verneed结构可选地后跟一个Elfxx_Vernaux结构数组。
在.gnu.version_r中,可执行文件或动态库引用了多少个其他动态库文件则有多少个Elfxx_Verneed结构体,其他动态库文件中有多少个版本信息被引用则紧随其后就有多少个Elfxx_Vernaux结构体。
我们可以通过readelf命令查看此节的内容:
1 | $ readelf -V libc-2.23.so |
Version为符号版本的版本标识符,一个索引值。对应于Elfxx_Verdef.vd_ndx。在同一动态库中,Elfxx_Vernaux.vna_other和Elfxx_Verdef.vd_ndx的值是连续的,并且没有重复值。
5.3.3.1 Elfxx_Verneed
1 | typedef struct { |
vn_version
:结构的版本。此值当前设置为1,如果版本控制实现发生了不兼容的更改,则将重置该值。vn_cnt
:与此Elfxx_Verneed结构相关联的Elfxx_Vernaux结构数组条目数。vn_file
:此依赖项的文件名字符串的偏移量(.dynstr),以字节为单位。此依赖项的文件名与文件中找到的.dynamic依赖项之一匹配。ld-linux-x86-64.so.2。vn_aux
:此文件依赖项(File Dependency)所需的Elfxx_Vernaux结构版本定义数组中第1个条目距当前Elfxx_Verneed结构起始的字节偏移量(一般为Elfxx_Verneed结构的字节大小)。必须至少存在一个版本依赖项。可以存在其他版本依赖项,数量由vn_cnt值指示。vn_next
:下一个Elfxx_Verneed结构距当前Elfxx_Verneed结构起始的偏移量(Elfxx_Verneed结构大小+vn_cnt个Elfxx_Vernaux的大小),以字节为单位。0表示当前Elfxx_Verneed结构为最后1个。
Table 5-6:合法的vn_version值
宏名称 | 值 | 含义 |
---|---|---|
VER_NEED_NONE | 0 | 无版本。 |
VER_NEED_CURRENT | 1 | 当前版本。 |
VER_NEED_NUM | 2 | 给定版本号。 |
5.3.3.2 Elfxx_Vernaux
1 | typedef struct { |
vna_hash
:版本依赖项名称字符串的哈希值(用ELF Hash函数计算)。vna_flags
:版本依赖信息标志(Dependency Information Flag)位掩码。通常为0。vna_other
:在.gnu.version符号版本数组中使用的符号版本的版本标识符,一个索引值。与Elfxx_Verdef结构中的vd_ndx含义差不多。第15位控制对象是否被隐藏。如果设置了此位,对象就不能使用,静态链接器(Static Linker)将忽略该符号在对象中的存在。vna_name
:此版本依赖项的名称字符串的偏移量(.dynstr),以字节为单位。GLIBC_2.3。vna_next
:从当前Elfxx_Vernaux条目开始到下一个Elfxx_Vernaux条目的偏移量(Elfxx_Vernaux结构大小),以字节为单位。
Table 5-7:合法的vna_flags值
宏名称 | 值 | 含义 |
---|---|---|
VER_FLG_WEAK | 0x2 | 弱版本标识符。 |
弱符号版本依赖(Weak Version Dependency)表示对弱符号版本定义(Weak Version Definition)的原始绑定。
5.3.4 启动顺序(Startup Sequence)
当加载一个可共享对象时,系统应分析来自加载对象的版本定义数据(Version Definition Data),以确保它满足调用对象(Calling Object)的版本要求(Version Requirements)。此步骤称为定义测试(Definition Testing)。动态加载器(Dynamic Loader)应检索调用者的Elfxx_Verneed数组中的条目,并尝试在加载对象的.gnu.version_d表中找到匹配的符号版本定义信息(Definition Information)。
应该依次测试每个对象和依赖项。如果缺少符号定义并且未设置vna_flags的VER_FLG_WEAK位,则加载器将返回错误并退出。如果在Elfxx_Vernaux条目中设置了vna_flags的VER_FLG_WEAK位,加载器将发出警告并继续操作。
当找到被加载对象中未定义符号引用的版本时,将验证版本可用性。测试无误完成,对象应可用。
5.3.5 符号解析(Symbol Resolution)
当在对象中使用符号版本控制(Symbol Versioning)时,重定位将定义测试(Definition Testing)扩展了符号名称字符串的简单匹配:符号名称引用的版本字符串也应等于定义的符号版本名称。
在符号表中使用的索引也可以在节类型为SHT_GNU_versym的节中使用,然后使用该索引的值来获取名称数据。从Elfxx_Verneed数组中检索相应的需求对象文件名字符串(Requirement String),同样,从Elfxx_Verdef表中检索相应的版本定义字符串(Definition String)。
如果设置了符号版本的版本标识符的高位(第15位),则无法使用该对象,静态链接器(Static Linker)应忽略该符号在对象中的存在。
当一个带有引用(Reference)的对象和一个带有定义(Definition)的对象被链接时,以下规则将控制结果:
1、带有引用(Reference)的对象和带有定义(Definition)的对象都使用版本控制。在这种情况下处理所有描述的匹配。如果在Elfxx_Verneed条目中vn_name元素引用的对象中找不到匹配的定义,将触发致命错误。
2、带有引用(Reference)的对象不使用版本控制,而带有定义(Definition)的对象使用。在这种情况下,只有索引号为1和2的符号定义将用于引用匹配,静态链接器将其标识为基本定义。在未使用静态链接器的情况下,例如在调用dlopen()时,如果没有基本定义索引的版本是唯一定义该符号的版本,则该版本是可接受的。
3、带有引用(Reference)的对象使用版本控制,但带有定义(Definition)的对象没有指定。在这种情况下应接受匹配的符号。如果所需符号列表中的损坏隐藏了旧式的目标文件,并导致与Elfxx_Verneed条目中的目标文件名匹配,则将触发致命错误。
4、带有引用(Reference)的对象和带有定义(Definition)的对象都不使用版本控制。这种情况下的行为将默认为已存在的符号规则。
5.4 Strings Related Sections
以下是各个字符串表保存的符号的范围。
- .strtab:保存完整符号表.symtab中的符号名称。
- .dynstr:保存动态符号表.dynsym中的符号名称。
- .shstrtab:保存目标文件中拥有的节(Section)的名称。
对字符串表的引用:
- 用于保存节名(Section Name)的字符串表(.shstrtab)的节索引由ELF头的e_shstrndx字段指示。
- 许多带有字符串引用的节(Section)使用节头(Section Header)中的sh_link字段来给出它们使用的字符串表的节索引。
- 在.dynamic节中,动态字符串表(.dynstr)对于文件的偏移量位于DT_STRTAB条目中。动态字符串表(.dynstr)的大小位于DT_STRSZ条目中。
5.4.1 .strtab
此节保存字符串,最常见的是表示与符号表(Symbol Table)条目相关联的名称的字符串。如果文件具有包含符号字符串表(Symbol String Table)的可加载段,则该段的属性将包括SHF_ALLOC位。否则,该位将不设置。此节属于SHT_STRTAB类型。
本节介绍默认字符串表。字符串表节(String Table Sections)保存以Null字符结尾的字符序列,通常称为字符串。目标文件(Object File)使用这些字符串来表示符号(Symbol)和节名称(Section Names)。一般通过对字符串的首个字母在字符串表中的下标来索引字符串。字符串表的首尾字节都是Null,以确保所有字符串以Null终止。此外,索引为0的字符串要么没有名字(No Name),要么就是名字为空(Null Name),具体取决于上下文。字符串表也可以为空,相应的,它的节头的sh_size成员将为0。非0索引对于空字符串表无效。
节头的sh_name成员的值是节头字符串表节(.shstrtab)中内容距节头字符串表节起始的偏移量/索引值(以字节为单位),节头字符串表节(.shstrtab)在节头表中的索引由ELF头的e_shstrndx成员指定。下图显示了一个包含25个字节的字符串表以及与各种索引关联的字符串。
Figure 5-4:String Table Example
该字符串表中的字符串与索引的关系如下:
Table 5-8:String Table Indexes
Index | String |
---|---|
0 | none |
1 | name. |
7 | Variable |
11 | able |
16 | able |
24 | null string |
如示例所示:
- 字符串表索引可以引用该节中的任何字节。
- 一个字符串可能出现多次。
- 可以存在对子字符串的引用。
- 同一个字符串可以被引用多次。
- 字符串表中也可以存在未引用的字符串。
我们可以使用readelf对.strtab表的内容进行查看:
1 | $ readelf -p 30 hash |
这个节将会在使用GNU Binutils的strip命令对目标文件去除符号之后被丢弃。同样被丢弃的还有.symtab。通过去除符号,可以减小可执行文件和共享库文件的大小,也可以增加逆向的难度。
5.4.2 .dynstr
此节包含动态链接(Dynamic Linking)所需的字符串,最常见的是表示与符号表条目关联的名称的字符串。此节属于SHT_STRTAB类型。使用的属性类型是SHF_ALLOC。此节的结构与.strtab节类似。
我们可以使用readelf对.dynstr表的内容进行查看:
1 | $ readelf -p .dynstr libc-2.23.so |
5.4.3 .shstrtab
此节包含节名称(Section Names)。此节属于SHT_STRTAB类型。不使用任何属性类型。此节的结构与.strtab节类似。
我们可以使用readelf对.shstrtab表的内容进行查看:
1 | $ readelf -p .shstrtab libc-2.23.so |
5.5 Symbols Related Sections
每个目标文件都会有一个符号表,熟悉编译原理的就会知道,在编译程序时,必须有相应的结构来管理程序中的符号以便于对函数和变量进行重定位。
此外,链接本质就是把多个不同的目标文件相互“粘”在一起,实际上,目标文件相互粘合是目标文件之间对地址的引用,即函数和变量的地址的相互引用。而在粘合的过程中,符号就是其中的粘合剂。
目标文件中的符号表包含了一些通用的符号,这部分信息在进行了strip操作后就会消失。包括变量名和函数名。
5.5.1 .symtab
目标文件的符号表包含定位(Locate)和重定位(Relocate)程序的符号的定义(Definitions)和引用(References)时所需的信息。符号表索引是该数组的下标。索引0既指定表中的第一个条目,又用作未定义符号索引。本节稍后将指定初始条目的内容。
Table 5-9:Index 0 of Symbol Table
Name | Value |
---|---|
STN_UNDEF | 0 |
符号表表项具有以下格式(这里需要注意一点,Elf32_Sym和Elf64_Sym结构体成员的顺序不同):
1 | typedef struct { |
各个字段的含义如下:
st_name
:如果该值非零,该成员保存该符号的符号名字符串在相应字符串表中的节偏移量/索引,字符串表保存符号名称的字符串。否则,符号表项没有名称。注:外部C符号在C语言和目标文件的符号表中具有相同的名称。st_value
:该成员给出相关符号的值。根据上下文,这可能是绝对值、地址等。详情见下方。st_size
:许多符号都有相应的大小。例如,数据对象的大小是对象中包含的字节数。如果符号没有大小或大小未知,则该成员为0。st_info
:该成员指定符号的类型(Symbol Type)和绑定属性(Binding Attributes)。st_other
:该成员定义了符号的可见性(Visibility)。st_shndx
:每个符号表条目都被“定义”为与某个节相关,该成员保存相关节的节头表索引。如Table 4-1和相关文本所述,一些节索引具有特殊含义。
其中,符号表中索引为0(STN_UNDEF)的表项存储了符号表的一个元素,同时这个元素也相对比较特殊,作为所有未定义符号的索引,具体如下:
Table 5-10:Symbol Table Entry: Index 0
名称 | 值 | 含义 |
---|---|---|
st_name | 0 | 无符号名字符串。 |
st_value | 0 | 无符号值。 |
st_size | 0 | 无符号大小。 |
st_info | 0 | 无符号类型,本地绑定。 |
st_other | 0 | 默认符号可见性规则。 |
st_shndx | SHN_UNDEF | 无对应的节。 |
我们可以使用readelf对.symtab表的内容进行查看:
1 | $ readelf -s hash |
5.5.1.1 st_value
不同目标文件类型的符号表条目对st_value成员的解释略有不同。
- 在可重定位文件中,st_value保存节索引为SHN_COMMON的符号的对齐约束。
- 在可重定位文件中,st_value保存已定义符号的节偏移量。也就是说,st_value是从st_shndx标识的节的开头的偏移量。
- 在可执行文件和共享目标文件中,st_value保存一个虚拟地址。为了使这些文件的符号对动态链接器(Dynamic Linker)更有用,节偏移量(文件层面解释)让位于与节号无关的虚拟地址(内存层面解释)。
尽管st_value对于不同的目标文件具有相似的含义,但允许适当的程序高效地访问这些数据。
如果可执行文件中包含对与其相关联的共享对象中定义的函数的引用,则该文件的符号表节将包含该符号的条目。该符号表条目的st_shndx成员将包含SHN_UNDEF。这向动态链接器(Dynamic Linker)发出信号,该函数的符号定义不包含在可执行文件本身中。如果该符号已在可执行文件中分配了一个过程链接表(Procedure Linkage Table)条目,并且该符号表条目的st_value成员不为零,则该值将包含该过程链接表条目的第一条指令的虚拟地址。否则,st_value成员为0。动态链接器(Dynamic Linker)在解析对函数地址的引用时使用此过程链接表表项的地址。
在Linux的ELF文件中,具体说明如下:
1、该符号对应着一个变量,那么st_value的值为该变量在内存中的偏移。我们可由这个值获取其文件偏移。
- a. 获取该符号对应的st_shndx,进而获取到相关的节。
- b. 根据节头成员可以获取节的内存基地址和文件基地址。
- c. st_value - 内存基地址 = 文件偏移 - 文件基地址
2、该符号对应着一个函数,那么st_value的值为该函数在文件中的偏移。
可执行文件和与其关联的共享目标文件对函数地址的引用可能不会解析为相同的值。来自共享目标文件的引用通常由动态链接器(Dynamic Linker)解析为函数本身的虚拟地址。可执行文件中对共享目标文件中定义的函数的引用通常由链接器(Link Editor)解析为可执行文件中该函数的过程链接表(Procedure Linkage Table)条目的地址。
为了允许函数地址的比较按预期工作,如果可执行文件引用共享目标文件中定义的函数,链接器将把该函数的过程链接表(Procedure Linkage Table)条目的地址放置在其关联的符号表条目中。动态链接器(Dynamic Linker)会特别对待此类符号表条目。如果动态链接器正在搜索一个符号,并且在可执行文件中遇到该符号的符号表条目,则它通常遵循规则如下:
- 1、如果符号表条目的st_shndx成员不是SHN_UNDEF,则动态链接器(Dynamic Linker)已找到该符号的定义,并将其的st_value成员用作该符号的地址。
- 2、如果st_shndx成员是SHN_UNDEF,并且符号是STT_FUNC类型,并且st_value成员不为0,则动态链接器(Dynamic Linker)会将此条目识别为特殊项,并使用st_value成员作为符号的地址。
- 3、否则,动态链接器(Dynamic Linker)认为该符号在可执行文件中未定义并继续处理。
5.5.1.2 st_info
st_info指定符号类型及绑定属性。st_info的低4位表示符号类型,高4位表示绑定属性。符号类型的宏定义以STT开头,符号绑定的宏定义以STB开头。
1 | /* st_info字段中符号类型和绑定属性的提取 */ |
5.5.1.2.1 Symbol Type
下面是一些合法的符号类型:
Table 5-11:Symbol Types
宏名称 | 值 | 含义 |
---|---|---|
STT_NOTYPE | 0 | 未指定符号的类型。 |
STT_OBJECT | 1 | 该符号与数据对象相关联,如变量、数组等。 |
STT_FUNC | 2 | 该符号与一个函数或其他可执行代码相关联。 |
STT_SECTION | 3 | 该符号与一个节相关联。这种类型的符号表表项主要是为了重定位而存在的,通常具有STB_LOCAL绑定。 |
STT_FILE | 4 | 通常,符号的名称给出与目标文件相关联的源文件的名称。一个文件符号具有STB_LOCAL绑定,它的节索引是SHN_ABS,如果存在的话,它在文件的其他STB_LOCAL符号之前。节类型为SHT_SYMTAB的符号表(.symtab)的索引1的符号项表示目标文件的STT_FILE符号。通常,此符号跟在STT_SECTION符号之后,然后是已简化(Reduced)为局部符号的任何全局符号。 |
STT_COMMON | 5 | 该符号标记一个未初始化的公共块(Common Block)。这个符号和STT_OBJECT完全一样。 |
STT_TLS | 6 | 该符号是线程本地数据对象。定义后,该符号给出了符号的分配偏移量,而不是实际地址。对于可分配的节,类型为STT_TLS的符号只能由特殊的线程本地存储重定位引用。线程本地存储重定位只能引用STT_TLS类型的符号,或者引用的节具有SHF_TLS标志和STT_SECTION类型的符号。从不可分配节对STT_TLS类型符号的引用没有此限制。 |
STT_NUM | 7 | 定义类型的数量。 |
STT_LOOS | 10 | 保留用于特定操作系统语义符号类型值范围的下限。 |
STT_GNU_IFUNC | 10 | 该符号是间接的代码对象。 |
STT_HIOS | 12 | 保留用于特定操作系统语义符号类型值范围的上限。 |
STT_LOPROC | 13 | 保留用于特定处理器语义符号类型值范围的下限。 |
STT_HIPROC | 15 | 保留用于特定处理器语义符号类型值范围的上限。 |
共享目标文件中的函数符号(类型为STT_FUNC的符号)具有特殊意义。当另一个目标文件从共享目标文件引用一个函数时,链接器(Link Editor)会自动为被引用的符号创建一个过程链接表(Procedure Linkage Table)条目。目标文件不会通过过程链接表(Procedure Linkage Table)自动引用类型不是STT_FUNC的其它共享目标符号。
5.5.1.2.2 Symbol Binding
符号的绑定决定了符号的链接可见性(Linkage Visibility)和行为(Behavior)。
下面是一些合法的符号绑定:
Table 5-12:Symbol Binding
宏名称 | 值 | 含义 |
---|---|---|
STB_LOCAL | 0 | 局部符号。这种符号在包含其定义的目标文件之外不可见。同名的局部符号可以存在于多个文件中,互不干扰。 |
STB_GLOBAL | 1 | 全局符号。这种符号对所有被合并的目标文件都是可见的。一个文件对全局符号的定义将满足另一个文件对同一全局符号的未定义引用。 |
STB_WEAK | 2 | 弱符号。这种符号类似于全局符号,但它们的定义具有较低的优先级。 |
STB_NUM | 3 | 定义的类型的数量。 |
STB_LOOS | 10 | 保留用于特定操作系统语义绑定类型值范围的下限。 |
STB_HIOS | 12 | 保留用于特定操作系统语义绑定类型值范围的上限。 |
STB_LOPROC | 13 | 保留用于特定处理器语义绑定类型值范围的下限。 |
STB_HIPROC | 15 | 保留用于特定处理器语义绑定类型值范围的上限。 |
在每个符号表中,所有带有STB_LOCAL绑定的符号都位于弱符号和全局符号之前。符号的类型为关联的实体提供了一般的分类。符号表节(Symbol Table Section)的节头成员sh_info保存着符号表中第一个非局部符号的符号表索引。
全局符号和弱符号在两个主要方面有所不同:
- 当链接器(Link Editor)链接多个可重定位目标文件时,它不允许定义多个具有相同名称的STB_GLOBAL符号。另一方面,如果存在一个已定义的全局符号,则同名弱符号的存在不会导致错误。链接器(Link Editor)会优先选择全局符号定义,忽略弱符号定义。类似的,如果存在公共符号(即st_shndx字段为SHN_COMMON的符号),同名弱符号的出现也不会导致错误。链接器(Link Editor)会优先选择公共符号(Common Symbol)定义,忽略弱符号定义。
- 当链接器(Link Editor)搜索静态库(Archive Libraries)时,它将提取包含未定义的(Undefined)和暂定的(Tentative)全局符号定义的归档成员。成员的定义可以是全局符号或弱符号。默认情况下,链接器(Link Editor)不会提取归档成员来解析未定义的弱符号。未解析的弱符号具有0值。使用“-z weakextract”会覆盖此默认行为。此选项允许链接器(Link Editor)提取归档成员来解析未定义的弱符号。
注:弱符号主要用于系统软件。不鼓励在应用程序中使用它们。
5.5.1.3 st_other
该成员定义了符号的可见性(Visibility)。下面的代码展示了如何操作32位对象和64位对象的st_other的值。其他位设置为零,因为其不包含任何含义。
1 | /* How to extract and insert information held in the st_other field. */ |
可以在可重定位对象中指定此可见性。此可见性定义了该符号成为动态对象的一部分后如何访问该符号。
下面是一些合法的符号可见性:
Table 5-13:ELF Symbol Visibility
宏名称 | 值 | 含义 |
---|---|---|
STV_DEFAULT | 0 | 具有STV_DEFAULT属性的符号的可见性由符号的绑定类型指定。全局符号和弱符号在定义它们的动态对象之外可见。局部符号被隐藏。全局符号和弱符号也可以被抢占(Preempted)。这些符号可以通过另一个组件中的同名定义插入。 |
STV_INTERNAL | 1 | 这个可见性属性的解释与STV_HIDDEN相同。 |
STV_HIDDEN | 2 | 如果当前组件中定义的符号名称对其他组件不可见,则该符号将隐藏。这样的符号必然受到保护。该属性用于控制组件的外部接口。由这样的符号命名的对象如果其地址被传递到外部,仍然可以被另一个组件引用。当对象包含在动态对象中时,可重定位对象中包含的隐藏符号将被删除或转换为STB_LOCAL绑定。 |
STV_PROTECTED | 3 | 如果当前组件中定义的符号在其他组件中可见,则该符号将受到保护,但不能被抢占(Preempted)。在定义组件中对此类符号的任何引用都必须解析为该组件中的定义。即使符号定义存在于由默认规则插入(Interpose)的另一个组件中,也必须进行此解析。具有STB_LOCAL绑定的符号将不具有STV_PROTECTED可见性。 |
STV_EXPORTED | 4 | 此可见性属性可确保符号保持全局性。这种可见性不能被任何其他符号可见性技术降级(Demoted)或消除(Eliminated)。具有STB_LOCAL绑定的符号将不具有STV_EXPORTED可见性。 |
STV_SINGLETON | 5 | 此可见性属性确保符号保持全局性,并且符号定义的单个实例绑定到进程内的所有引用。这种可见性不能被任何其他符号可见性技术(Demoted)或消除(Eliminated)。具有STB_LOCAL绑定的符号将不具有STV_SINGLETON可见性。不能直接绑定到STV_SINGLETON。 |
STV_ELIMINATE | 6 | 此可见性属性扩展STV_HIDDEN。在当前组件中定义为消除(Eliminate)的符号对其他组件不可见。该符号不会写入使用该组件的动态对象的任何符号表中。 |
STV_SINGLETON可见性属性会影响链接(Link-Editing)期间可执行文件或共享目标文件中的符号解析。一个进程中的任何引用只能绑定到一个单独的(Singleton)实例。
STV_SINGLETON可以与STV_DEFAULT可见性属性结合使用,STV_SINGLETON优先。STV_EXPORT可以与STV_DEFAULT可见性属性结合使用,STV_EXPORT优先。STV_SINGLETON或STV_EXPORT可见性不能与任何其他可见性属性结合使用。这样的事件对于链接(Link-Edit)来说是致命(Fatal)的。
在链接(Link-Editing)期间,其他可见性属性不会影响动态对象内符号的解析。这种解析由绑定类型控制。一旦链接器选择了它的解析,这些属性就强加了两个要求。这两个要求都基于这样一个事实,即被链接的代码中的引用可能已经过优化以利用这些属性。
- 所有非默认可见性属性,当应用于符号引用时,意味着必须在被链接的对象中提供满足该引用的定义。如果这种类型的符号引用在被链接的对象中没有定义,则该引用必须具有STB_WEAK绑定。在这种情况下,该引用被解析为0。
- 如果对名称的任何引用或名称的定义是具有非默认可见性属性的符号,则可见性属性将被传递(Propagated)到正在链接的对象中的解析符号。如果为符号的不同实例指定了不同的可见性属性,则将约束(Constraining)最大的可见性属性传递到正在链接的对象中的解析符号。这些属性,从约束最少到最多排序,是STV_PROTECTED、STV_HIDDEN和 STV_INTERNAL。
5.5.1.4 st_shndx
如果一个符号的值指向一个节中的特定位置,它的节索引成员st_shndx将保存一个节头表表项的索引。随着在重定位期间节的移动,符号的值也会发生变化,并且对符号的引用继续“指向”程序中的同一位置。一些特殊的节索引值给出了其他语义。
ELF目标文件中的符号向链接器(Linker)和加载器(Loader)传达特定信息。
Table 5-14:Special Section Index of Symbol
宏名称 | 值 | 含义 |
---|---|---|
SHN_UNDEF | 0 | 此节索引表示符号未定义(在此目标文件中未定义,可能在其他目标文件中)。当链接器(Link Editor)将此目标文件与另一个定义指定符号的目标文件组合在一起时,此目标文件对符号的引用将被链接到实际定义。 |
SHN_ABS | 0xFFF1 | 符号具有绝对值,不会因重定位而改变。 |
SHN_COMMON | 0xFFF2 | 该符号标记尚未分配的公共块(Common Block)。符号的值给出了对齐约束(Alignment Constraints),类似于节头的sh_addralign成员。也就是说,链接器(Link Editor)将在st_value倍数的地址上为符号分配存储空间。st_size表明需要多少字节。 |
如果此成员包含SHN_XINDEX,则实际的节头表索引太大而无法放入此字段。实际值包含在类型为SHT_SYMTAB_SHNDX的相关节中。
5.5.2 .symtab_shndx
当符号表表项Elfxx_Sym的成员st_shndx包含SHN_XINDEX(0xFFFF)时,表示实际的节头表索引太大而无法放入st_shndx字段。实际值保存在.symtab_shndx节中。本节属于SHT_SYMTAB_SHNDX类型。
此节保存特殊符号表节索引数组,如.symtab所述。如果关联的符号表节包含SHF_ALLOC位,则该节的属性也包括SHF_ALLOC位。否则,不包括。
5.5.3 .dynsym
本节保存动态链接符号表,与.symtab的结构相同,包含.symtab表中支持动态链接所需的符号的子集。此节属于SHT_DYNSYM类型。使用的属性是SHF_ALLOC。因此这个符号表在进程执行期间会占用内存,可以在进程的内存映像中使用。
.dynsym表以标准的Null符号开始,然后是文件的全局符号。STT_FILE符号通常不会出现在这个符号表中。如果重定位表项需要STT_SECTION符号,则可能会出现。
需要注意的是.dynsym表的内容是运行时所需的,ELF文件中export/import的符号信息全在这里。但是,.symtab节中存储的信息是编译时的符号信息,它们在strip之后会被删除掉。
我们主要关注动态符号表表项中的两个成员:
- st_name,该成员保存着动态符号在.dynstr表(动态字符串表)中的偏移。
- st_value,如果这个符号被导出,这个符号保存着对应的虚拟地址。
我们可以使用readelf对.dynsym表的内容进行查看:
1 | $ readelf -s libc-2.23.so |
5.5.3.1 符号版本
动态符号的版本信息保存在.gnu.version节中,该节(Section)应与.dynsym节中的动态符号表(Dynamic Symbol Table)具有相同数量的条目,并且一一对应。其是由Elfxx_Versym结构构成的数组,每个数组元素是一个16位的整数,这个整数是由Elfxx_Vernaux结构的vna_other成员或Elfxx_Verdef结构的vd_ndx成员提供的符号版本标识符,一个索引。
在这样的情况下,动态链接器使用从Elfxx_Rel结构体的成员r_info中获得的下标同时作为.dynsym节和.gnu.version节的下标。这样就可以一一对应到每一个符号到底是那个版本的了。
5.7 Data Related Sections
5.7.1 .bss
此节保存未初始化的数据(全局变量),占用程序内存映像空间,但不占用ELF文件空间。根据定义,当程序开始运行时,系统将这些数据初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,因此此节属于SHT_NOBITS类型。属性类型为SHF_ALLOC和SHF_WRITE。
5.7.2 .tbss
此节保存未初始化的线程本地数据,占用程序内存映像空间,但不占用ELF文件空间。根据定义,当为每个新执行流实例化数据时,系统将这些数据初始化为0,在程序执行期间可以进行赋值。由于.tbss节未保存实际的数据,因此此节属于SHT_NOBITS类型。属性类型为SHF_ALLOC、SHF_WRITE和SHF_TLS。
5.7.3 .data & .data1
这些节保存初始化的数据(全局变量),占用程序内存映像空间,也占用ELF文件空间。由于其保存了程序的变量数据,因此这些节属于SHT_PROGBITS类型。属性类型为SHF_ALLOC和SHF_WRITE。
5.7.4 .tdata & .tdata1
这些节保存初始化的线程本地数据,占用程序内存映像空间,也占用ELF文件空间。系统为每个新的执行流都实例化其内容的一个副本。由于其保存了程序的变量数据,因此这些节属于SHT_PROGBITS类型。属性类型为SHF_ALLOC、SHF_WRITE和SHF_TLS。
5.7.5 .rodata & .rodata1
这些节保存只读数据,这些数据通常会在进程内存映像中形成不可写的段(Segment)。这些节属于SHT_PROGBITS类型。使用的属性是SHF_ALLOC。
5.8 Common Code Section[^6]
术语
:Link-Editor
:链接器
链接器ld(1),连接(Concatenate)并解释(Interpret)来自一个或多个输入文件的数据。这些文件可以是可重定位目标文件(Relocatable Objects)、共享目标文件(Shared Objects)或静态库(Archive Libraries)。从这些输入文件创建一个输出文件。此文件可以是动态可执行文件(Dynamic Executable)、位置无关可执行文件(Position-Independent Executable)、可重定位目标文件(Relocatable Object)或共享目标文件(Shared Objects)。链接器(Link-Editor)通常作为编译环境的一部分被调用。
Runtime Linker
:运行时链接器/加载器
运行时链接器ld.so.1(1),在运行时处理动态可执行文件(Dynamic Executable)、位置无关可执行文件(Position-Independent Executable)和共享目标文件(Shared Objects),将可执行文件(Executable)和共享目标文件(Shared Objects)绑定在一起以创建可运行的进程。
Shared Objects
:共享目标文件/共享对象/共享库
共享目标文件(Shared Objects)是链接编辑(Link-Edit)阶段的一种输出形式。共享目标文件有时称为共享库(Shared Libraries)。共享目标文件对于创建强大、灵活的运行时环境很重要。
Object Files
:目标文件/对象文件
链接器(Link-Editor)、运行时链接器(Runtime Linker)和相关工具处理符合可执行与可链接格式(Executable and Linkable Format,也称为ELF)的文件。
5.8.1 初始化和终止节
动态目标文件(Dynamic Objects)可以提供用于运行时初始化(Runtime Initialization)和终止处理(Termination Processing)的代码。每次在进程中加载动态目标文件时,动态目标文件的初始化代码执行一次。每次从进程卸载动态目标文件或在进程终止时,动态目标文件的终止代码执行一次。这段代码可以封装在两种节类型中的一种,函数指针数组或单个代码块。每一种节类型的节都是由输入的可重定位目标文件(Relocatable Objects)中的类似节串联(Concatenation)得到的。
.preinit_array、.init_array和.fini_array节分别提供了运行时预初始化(Pre-Initialization)、初始化(Initialization)和终止(Termination)函数的指针数组。在创建动态目标文件(Dynamic Objects)时,链接器(Link-Editor)会相应地使用.dynamic节的DT_PREINIT_[ARRAY/ARRAYSZ]、DT_INIT_[ARRAY/ARRAYSZ]和DT_FINI_[ARRAY/ARRAYSZ]“标签对”来标识这些数组。这些标签标识相关联的节,以便运行时链接器(Runtime Linker)可以调用这些节。预初始化数组(Pre-Initialization Array)仅适用于可执行文件。
注释:分配给这些数组的函数必须由正在构建的目标文件提供。
.init和.fini节分别提供了运行时初始化(Initialization)和终止(Termination)代码块。编译器驱动程序(Compiler Drivers)通常会为.init和.fini节提供它们添加到输入文件列表(Input File List)开头和结尾的文件。这些编译器提供的文件具有将.init和.fini节中的代码从可重定位目标文件(Relocatable Objects)封装(Encapsulating)到单个函数中的作用。
这些函数分别由保留符号名称_init和_fini标识。在创建动态目标文件(Dynamic Objects)时,链接器(Link-Editor)会相应地使用.dynamic的DT_INIT和DT_FINI标签来标识这些符号。这些标签标识相关联的节,以便运行时链接器(Runtime Linker)可以调用它们。
链接器(Link-Editor)可以使用“-z initarray”和“-z finiarray”选项直接执行初始化(Initialization)和终止(Termination)函数的注册。例如,以下命令将foo()的地址放在.init_array数组的元素中,将bar()的地址放在.fini_array数组的元素中。
1 | $ cat main.c |
可以使用汇编器(Assembler)直接创建初始化(Initialization)和终止(Termination)节。然而,大多数编译器提供特殊的原语(Special Primitives)来简化它们的声明。例如,可以使用以下“#pragma”定义重写前面的代码示例。这些定义导致对foo()的调用被放置在.init节,对bar()的调用被放置在.fini节。
1 | $ cat main.c |
初始化(Initialization)和终止(Termination)代码分布(Spread Through)在多个可重定位目标文件(Relocatable Objects)中,当包含在静态库(Archive Library)或共享目标文件/动态库(Shared Object)中时,可能会导致不同的行为。使用静态库(Archive)的应用程序的链接编辑(Link-Edit)可能只提取静态库中包含的一小部分对象,这些对象可能仅提供分布在静态库成员中的一部分初始化和终止代码。在运行时,只执行这部分代码。当依赖项在运行时被加载时,基于共享目标文件/动态库(Shared Object)构建的同一应用程序将执行所有累积的(Accumulated)初始化和终止代码。
在运行时确定进程内执行初始化(Initialization)和终止(Termination)代码的顺序是一个涉及依赖性分析(Dependency Analysis)的复杂问题。限制初始化和终止代码的内容以简化此分析。简化的、自包含的、初始化和终止代码提供可预测的运行时行为。
如果初始化代码涉及动态目标文件(Dynamic Object),其内存可以使用dldump(3C)转储,则数据初始化应该是独立的。
5.8.2 初始化和终止例程
在将控制转移到应用程序之前,运行时链接器(Runtime Linker)会处理应用程序中找到的任何初始化节(Initialization Sections)以及任何加载的依赖项(Loaded Dependencies)。如果在进程执行期间加载了新的动态目标文件(Dynamic Objects),则它们的初始化节将作为加载对象的一部分进行处理。初始化节.preinit_array、.init_array和.init是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。
运行时链接器(Runtime Linker)执行地址包含在.preinit_array和.init_array节中的函数。这些函数的执行顺序与其地址在数组中的出现顺序相同。运行时链接器(Runtime Linker)将.init节作为单独的函数执行。如果一个对象同时包含.init和.init_array节,则.init节在执行该对象的.init_array节定义的函数之前被执行。
可执行文件可以在.preinit_array节中提供预初始化函数(Pre-Initialization Functions)。这些函数在运行时链接器(Runtime Linker)构建进程映像并执行重定位之后,但在任何其他初始化函数(Initialization Functions)执行之前执行。共享目标文件/动态库(Shared Objects)中不允许使用预初始化函数(Pre-Initialization Functions)。
注释:可执行文件中的任何.init节都是由编译器驱动程序(Compiler Driver)提供的进程启动机制(Process Startup Mechanism)从应用程序调用的。在执行所有依赖项初始化节(Dependency Initialization Sections)之后,最后调用可执行文件中的.init节。
动态目标文件(Dynamic Objects)还可以提供终止节。终止节.fini_array和.fini是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。
任何终止节都将传递给atexit(3C)。当进程调用exit(2)时会调用这些终止例程。当使用dlclose(3C)从正在运行的进程中删除对象时,也会调用终止节。
运行时链接器(Runtime Linker)执行地址包含在.fini_array节中的函数。这些函数的执行顺序与它们的地址在数组中出现的顺序相反。运行时链接器(Runtime Linker)将.fini节作为单独的函数执行。如果一个对象同时包含.fini和.fini_array节,则.fini_array节定义的函数在该对象的.fini节执行之前被执行。
注释:可执行文件中的任何.fini节都是由编译器驱动程序(Compiler Driver)提供的进程终止机制(Process Termination Mechanism)从应用程序调用的。在执行所有依赖项终止节(Dependency Termination Sections)之前,首先调用可执行文件的.fini节。
5.8.2.1 初始化和终止代码的限制和陷阱
ELF初始化、终止节和例程(Routines)在对象生命周期(Life Cycle)的敏感点(Sensitive Point)执行。在初始化(Initialization)期间,对象已加载到内存中,但尚未完全初始化。在终止(Finalization)期间,对象仍然加载在内存中,但使用不再安全,并且可能会部分地(Partially)从进程状态(Process State)中移除。在任何一种情况下,进程状态都不是完全一致的,并且对于代码可以安全执行的操作有很大的限制。常见的陷阱(Pitfalls)包括但不限于以下内容:
- 循环依赖(Cyclic Dependencies)导致死锁(Deadlock),其中一个对象的初始化代码会触发另一个对象的加载,而另一个对象又调用回初始对象。
- 在多线程应用程序中使用共享目标文件/动态库(Shared Object)时线程序列化(Serialization)失败。两个线程可能会尝试同时访问延迟加载的库(Lazily Loaded Library)。首先到达那里的线程将导致运行时链接器(Runtime Linker)加载对象并开始运行初始化代码。程序员经常错误地认为,运行时链接器(Runtime Linker)可以在ELF初始化和终止代码运行时阻止多个线程同时访问给定对象,但事实并非如此。一旦初始化代码正在运行,运行时链接器(Runtime Linker)就无法阻止其他线程尝试访问该库。因此,第二个线程可能以不一致(Inconsistent)的状态访问对象。对象有责任通过提供必要的锁(Locks)或要求调用者这样做来序列化此类访问。
ELF初始化、终止节和例程(Routines)允许执行任意代码,给人的错觉(Illusion)是它们能够执行在正常上下文(Normal Context)中运行的代码可能执行的任何操作。从这个角度来看,这样的代码似乎只是一种无需显式函数调用(Explicit Function Calls)即可进行初始化或清理的便捷方式(Convenient Way)。这种误解(Misconception)会导致难以诊断(Diagnose)的故障。
程序员在使用ELF初始化和终止代码时应谨慎(Cautious),并限制操作的范围(Scope)和复杂性(Complexity)。链接器(Link-Editor)和运行时链接器(Runtime Linker)无法识别此类代码的内容或用途,也无法诊断(Diagnose)或预防(Prevent)不安全代码。小型自包含(Self Contained)操作是安全的。涉及访问其他对象或进程状态的操作可能不会。库不应该在初始化和终止代码中尝试复杂的操作,而应该为它们的调用者提供显式的(Explicit)初始化和终止函数,并记录这样做的需求。
5.8.2.2 初始化和终止顺序
在运行时确定进程内执行初始化(Initialization)和终止(Termination)代码的顺序是一个涉及依赖性分析(Dependency Analysis)的复杂过程。这个过程从初始化和终止节的最初的实现(Original Inception)有了很大的发展。此过程试图满足现代语言和当前编程技术的期望(Expectations)。但是,可能存在难以满足用户期望的场景。通过理解这些场景并限制初始化代码和终止代码的内容,可以实现灵活的、可预测的运行时行为。
初始化节(Initialization Section)的目标是在引用同一对象中的任何其他代码之前执行一小段代码。终止节(Termination Section)的目标是在对象执行完毕后执行一小段代码。自包含(Self Contained)的初始化节和终止节可以轻松满足这些要求。
但是,初始化节(Initialization Section)通常更复杂,并且会引用其他对象提供的外部接口。因此,在从其他对象引用之前,必须先执行一个对象的初始化节,这样就建立了依赖关系(Dependency)。应用程序可以建立广泛的依赖层次结构(Dependency Hierarchy)。此外,依赖关系可以在其层次结构中创建循环。如果初始化节加载额外的对象,或者改变已经加载的对象的重定位模式,情况可能会变得更加复杂(Complicated)。这些问题导致了各种试图满足这些节的原始目标的排序(Sorting)和执行(Execution)技术。
运行时链接器(Runtime Linker)构造一个已加载对象的拓扑排序列表(Topologically Sorted List)。此列表是根据每个对象表示的依赖关系(Dependency Relationship)以及所表示依赖关系之外的任何符号绑定(Symbol Bindings)构建的。
初始化节以依赖项的反向拓扑顺序(Reverse Topological Order)执行。如果发现了循环依赖关系(Cyclic Dependencies),则不能对构成循环的对象进行拓扑排序(Topologically Sorted)。任何循环依赖项的初始化节都以其反向的加载顺序(Reverse Load Order)执行的。类似地,终止节按依赖关系的拓扑顺序调用。任何循环依赖项的终止节都按其加载顺序执行。
通过使用带有-i选项的ldd(1)命令可以获得对对象依赖项的初始化顺序的静态分析。例如,下面的动态目标文件(Dynamic Objects)显示了一个循环依赖关系。
1 | $ elfdump -d B.so.1 | grep NEEDED |
前面的分析完全来自显式依赖关系(Explicit Dependency Relationships)的拓扑排序(Topological Sorting)。但是,经常会创建未定义其所需依赖项(Required Dependencies)的对象。出于这个原因,符号绑定(Symbol Bindings)也被纳入为依赖分析的一部分。符号绑定与显式依赖的结合有助于产生更准确(Accurate)的依赖关系。通过使用带有-i和-d选项的ldd(1)命令可以获得更准确的初始化顺序的静态分析。
加载对象的最常见模型是使用延迟绑定(Lazy Binding)。使用此模型,在初始化处理之前仅处理直接引用(Immediate Reference)符号绑定。来自延迟引用(Lazy References)的符号绑定可能仍处于挂起状态(Pending)。这些绑定可以扩展迄今为止建立的依赖关系。通过使用带有-i和-r选项的ldd(1)命令可以获得包含所有符号绑定的初始化顺序的静态分析。实际上,大多数应用程序使用延迟绑定(Lazy Binding)。因此,在计算初始化顺序之前实现的依赖分析遵循使用“ldd -i -d”的静态分析。但是,由于这种依赖关系分析可能不完整(Incomplete),而且可能存在循环依赖关系,所以运行时链接器(Runtime Linker)提供了动态初始化。
动态初始化(Dynamic Initialization)尝试在调用同一对象中的任何函数之前执行对象的初始化节(Initialization Section)。在延迟符号绑定(Lazy Symbol Binding)期间,运行时链接器(Runtime Linker)确定是否已调用绑定到的对象的初始化节(Initialization Section)。如果没有,运行时链接器(Runtime Linker)在从符号绑定过程(Symbol Binding Procedure)返回之前执行初始化节。
ldd(1)命令不能显示动态初始化(Dynamic Initialization)。但是,通过将LD_DEBUG环境变量设置为包含init令牌,可以在运行时观察到初始化调用的确切序列(Exact Sequence)。通过添加调试令牌细节,可以捕获大量运行时初始化信息和终止信息。此信息包括依赖项列表(Dependency Listings)、拓扑处理(Topological Processing)和循环依赖项(Cyclic Dependencies)的识别。
动态初始化(Dynamic Initialization)仅在处理延迟引用(Lazy References)时可用。这种动态初始化通过以下方式规避(Circumvented):
- 使用环境变量LD_BIND_NOW。
- 使用“-z now”选项构建的对象。
- 使用dlopen(3C)以RTLD_NOW模式加载的对象。
到目前为止所描述的初始化技术可能仍然不足(Insufficient)以应对一些动态活动(Dynamic Activities)。初始化节可以加载其他对象,可以显式地使用dlopen(3C),也可以隐式地通过延迟加载(Lazy Loading)和使用过滤器(Filters)。初始化节(Initialization Sections)还可以促进(Promote)现有对象的重定位。如果使用具有RTLD_NOW模式的dlopen(3C)引用同一对象,则已加载以使用延迟绑定(Lazy Binding)的对象将解析这些绑定。这种重定位提升有效地抑制了动态解析函数调用时可用的动态初始化功能。
每当加载新对象或提升(Promoted)现有对象的重定位时,就会启动这些对象的拓扑排序(Topological Sort)。实际上,在建立新的初始化要求并执行关联的初始化节时,暂停了原始初始化执行。该模型试图确保新引用的对象被适当地初始化以供原始初始化节使用。但是,这种并行化(Parallelization)可能会导致不必要的递归(Unwanted Recursion)。
在处理采用延迟绑定(Lazy Binding)的对象时,运行时链接器(Runtime Linker)可以检测某些级别的递归(Recursion)。这个递归可以通过设置LD_DEBUG=init来显示。例如,执行foo.so.1的初始化节可能会导致调用另一个对象。如果此对象随后引用foo.so.1中的接口,则创建一个循环。作为将延迟函数引用绑定到foo.so.1的一部分,运行时链接器(Runtime Linker)可以检测此递归(Recursion)。
1 | $ LD_DEBUG=init prog |
运行时链接器(Runtime Linker)无法检测到通过已重定位的引用发生的递归(Recursion)。
递归可能是耗时(Expensive)且有问题的(Problematic)。减少可由初始化节触发的外部引用(External References)和动态加载活动(Dynamic Loading Activities)的数量,以消除递归。
对于使用dlopen(3C)添加到正在运行的进程中的任何对象,都会重复初始化处理。对于由于调用dlclose(3C)而从进程中卸载的任何对象,也会执行终止处理。
前面的部分描述了各种用于以满足用户期望的方式执行初始化和终止节的技术。但是,还应采用编码风格(Coding Style)和链接编辑实践(Link-Editing Practices)来简化依赖项之间的初始化和终止关系。这种简化有助于使初始化处理和终止处理可预测,同时不太容易受到意外依赖顺序(Unexpected Dependency Ordering)的任何副作用。
将初始化和终止节的内容保持在最低限度。通过在运行时初始化对象来避免全局构造函数(Global Constructors)。减少初始化和终止代码对其他依赖项的依赖。定义所有动态目标文件(Dynamic Objects)的依赖需求。不要表达非必需的依赖关系。避免循环依赖。不要依赖于初始化或终止序列的顺序。对象的排序会受到共享对象和应用程序开发的影响。
5.8.3 .preinit_array
此节包含一个函数指针数组,这些函数是在此可执行文件(Executable File)中所有其他初始化函数调用之前调用的函数。数组中的每个指针都被视为具有void返回值的无参数函数。.preinit_array节是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。.dynamic节中的DTPREINIT[ARRAY/ARRAYSZ]“标签对”来标识这个节。此节属于SHT_PREINIT_ARRAY类型。使用的属性是SHF_ALLOC和SHF_WRITE。
可执行文件可以在.preinit_array节中提供预初始化函数(Pre-Initialization Functions)。这些函数在运行时链接器(Runtime Linker)构建进程映像并执行重定位之后,但在任何其他初始化函数(Initialization Functions)执行之前执行。共享目标文件/动态库(Shared Objects)中不允许使用预初始化函数(Pre-Initialization Functions)。
5.8.4 .init
此节为包含该节的动态目标文件(Dynamic Object)保存帮助进程初始化的单个初始化函数的可执行指令。当程序开始运行时,系统会在调用主程序入口点(在C程序中称为main)之前执行本节中的代码。.init节是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。.dynamic节中的DT_INIT“标签”来标识这个节。此节属于SHT_PROGBITS类型。使用的属性是SHF_ALLOC和SHF_EXECINSTR。如果一个对象同时包含.init和.init_array节,则.init节在执行该对象的.init_array节定义的函数之前被执行。
5.8.5 .init_array
此节包含一个函数指针数组,用于为包含该节的动态目标文件(Dynamic Object)提供初始化函数的指针数组。.init_array节是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。.dynamic节中的DTINIT[ARRAY/ARRAYSZ]“标签对”来标识这个节。此节属于SHT_INIT_ARRAY类型。使用的属性是SHF_ALLOC和SHF_WRITE。如果一个对象同时包含.init和.init_array节,则.init节在执行该对象的.init_array节定义的函数之前被执行。
5.8.6 .text
此节保存程序的“文本(text)”或可执行指令。此节属于SHT_PROGBITS类型。使用的属性是SHF_ALLOC和SHF_EXECINSTR。
5.8.7 .fini_array
此节包含一个函数指针数组,用于为包含该节的动态目标文件(Dynamic Object)提供终止函数的指针数组。.fini_array节是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。.dynamic节中的DTFINI[ARRAY/ARRAYSZ]“标签对”来标识这个节。此节属于SHT_FINI_ARRAY类型。使用的属性是SHF_ALLOC和SHF_WRITE。如果一个对象同时包含.fini和.fini_array节,则.fini_array节定义的函数在该对象的.fini节执行之前被执行。
5.8.3 .fini
此节为包含该节的动态目标文件(Dynamic Object)保存帮助进程终止的单个终止函数的可执行指令。当程序正常退出时,系统执行本节中的代码。.fini节是在构建动态目标文件(Dynamic Objects)时由链接器(Link-Editor)创建的。.dynamic节中的DT_FINI“标签”来标识这个节。此节属于SHT_PROGBITS类型。使用的属性是SHF_ALLOC和SHF_EXECINSTR。如果一个对象同时包含.fini和.fini_array节,则.fini_array节定义的函数在该对象的.fini节执行之前被执行。
5.9 Dynamic Related Sections
动态链接器
在构建使用动态链接(Dynamic Linking)的可执行文件时,链接器(Link Editor)会在可执行文件中添加类型为PT_INTERP的程序头元素,告诉系统调用动态链接器(Dynamic Linker)作为程序解释器(Program Interpreter)。
注释:系统提供的动态链接器的位置是特定于处理器的。
可执行文件(Executable File)和动态链接器(Dynamic Linker)合作为程序创建进程映像,这需要以下操作:
- 将可执行文件的内存段(Memory Segments)添加到进程映像(Process Image)中;
- 向进程映像(Process Image)添加共享目标文件内存段(Shared Object Memory Segments);
- 对可执行文件及其共享目标文件执行重定位;
- 关闭用于读取可执行文件的文件描述符(File Descriptor),如果已提供给动态链接器;
- 将控制权转移给程序,使程序看起来好像是直接从可执行文件中获得控制权的。
链接器(Link Editor)还为可执行文件(Executable Files)和共享目标文件(Shared Object Files)构建各种数据来帮助动态链接器(Dynamic Linker)。如上面“程序头(Program Header)”中所示,这些数据驻留在可加载段(Loadable Segments)中,使它们在执行期间可用。(请注意,确切的段内容是特定于处理器的。)
- 节类型为SHT_DYNAMIC的.dynamic节保存各种数据。位于该节开头的结构保存其他动态链接信息的地址。
- 节类型为SHT_HASH的.hash节包含一个符号哈希表(Symbol Hash Table)。节类型为SHT_GNU_HASH的.gnu.hash节包含一个GNU风格的符号哈希表。
- 节类型为SHT_PROGBITS的.got和.plt节包含两个单独的表:全局偏移表(Global Offset Table)和过程链接表(Procedure Linkage Table)。程序对位置无关(Position-Independent)的代码使用全局偏移表。下面的部分解释了动态链接器(Dynamic Linker)如何使用和更改表来为目标文件创建内存映像(Memory Images)。
因为每个符合UNIX System V的程序都从共享目标库(Shared Object Library)中导入基本系统服务(Basic System Services),所以动态链接器参与每个符合TIS ELF的程序执行。
共享目标文件(Shared Objects)可能占用与文件程序头表(Program Header Table)中记录的地址不同的虚拟内存地址。动态链接器(Dynamic Linker)重定位内存映像(Memory Image),在应用程序获得控制之前更新绝对地址(Absolute Addresses)。如果库是在程序头表(Program Header Table)中指定的地址处加载的,那么绝对地址值是正确的,但通常情况并非如此。
如果进程环境包含一个名为LD_BIND_NOW且具有非空值(non-null)的变量,则动态链接器(Dynamic Linker)会在将控制权转移到程序之前处理所有重定位。例如,以下所有环境条目都将指定此行为:
- LD_BIND_NOW=1
- LD_BIND_NOW=on
- LD_BIND_NOW=off
否则,LD_BIND_NOW要么不会出现在环境中,要么具有空值。动态链接器(Dynamic Linker)被允许延迟地计算过程链接表(Procedure Linkage Table)条目,从而避免符号解析(Symbol Resolution)和未调用函数的重定位开销。
共享目标文件依赖
当链接器(Link Editor)处理静态库(Archive Library)时,它提取库成员并将它们复制到输出目标文件中。这些静态链接的服务在执行期间可用,而无需涉及到动态链接器(Dynamic Linker)。共享目标文件(Shared Objects)也提供服务,动态链接器必须将适当的共享目标文件附加到进程映像(Process Image)以供执行。因此,可执行文件和共享目标文件描述了它们特定的依赖关系。
当动态链接器为目标文件创建内存段(Memory Segments)时,依赖关系(记录在.dynamic节的DT_NEEDED条目中)告诉动态链接器需要哪些共享目标文件来提供程序服务。通过重复连接(Repeatedly Connecting)引用的共享目标文件及其依赖项,动态链接器构建完整的进程映像(Process Image)。解析符号引用(Symbolic References)时,动态链接器使用广度优先搜索(Breadth-First Search)检查符号表。也就是说,它首先查看可执行程序本身的符号表,然后查看DT_NEEDED条目所指示的共享目标文件的符号表(按顺序),然后查看第二级DT_NEEDED条目,依此类推。共享目标文件必须可由进程读取;不需要其他权限。
注释:即使在依赖项列表(Dependency List)中多次引用共享目标文件(Shared Object),动态链接器也只会将目标文件连接到进程一次。
依赖项列表中的名称是DT_SONAME字符串或用于构建目标文件的共享目标文件的路径名的副本。例如,如果链接器使用一个共享目标文件(DT_SONAME项为lib1)和另一个共享目标文件(路径名为/usr/lib/lib2)构建一个可执行文件,则该可执行文件将包含lib1和/usr/lib/lib2在它的依赖列表中。
如果共享目标文件名称在名称中的任何位置包含一个或多个斜杠(/)字符,例如上面的/usr/lib/lib2或目录/文件,则动态链接器直接使用该字符串作为路径名。如果名称没有斜线(/),例如上面的lib1,则有三个条件(Facilities)指定了共享目标文件路径搜索,其优先级如下:
- 首先,.dynamic数组的标签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 ID程序的环境搜索规范(如:LD_LIBRARY_PATH)。但是,它会搜索DT_RPATH目录和/usr/lib。同样的限制也适用于在已安装扩展安全系统的系统上拥有超过最小权限的进程。
5.9.1 .interp
此节包含程序解释器(Program Interpreter)的路径名。如果文件具有包含该节的可加载段(Loadable Segment),则该节的属性将设置SHF_ALLOC位。否则,该位将不被设置。此节属于SHT_PROGBITS类型。
5.9.1.1 程序解释器
一般来说,参与动态链接(Dynamic Linking)的可执行文件应具有一个段类型为PT_INTERP的程序头元素。在exec(BA_OS)期间,系统从PT_INTERP段提取对应解释器的路径名,并从解释器文件的段创建初始进程映像(Initial Process Image)。也就是说,系统为解释器(Interpreter)构建一个内存映像(Memory Image),而不是使用原始的可执行文件的段映像(Segment Images)。然后解释器负责从系统接收控制,并为应用程序提供执行环境。
解释器(Interpreter)可能有两种方式获取控制权:
- 首先,它可能会接收一个指向文件开头的文件描述符(File Descriptor)来读取可执行文件。它可以使用这个文件描述符来读取(Read)和/或映射(Map)可执行文件的段(Segments)到内存中。
- 其次,根据可执行文件格式的不同,系统可能会将可执行文件加载到内存中,而不是给解释器一个打开的文件描述符。虽然文件描述符可能会出现异常,但是解释器的初始进程状态(Initial Process State)仍然会与可执行文件可能接收到的状态相匹配。解释器本身可能不需要再有一个解释器。
解释器可以是共享目标文件(Shared Object)或可执行文件(Executable File)。
- 共享目标文件(Shared Object)(正常情况)解释器被加载为位置无关(Position Independent),地址可能因进程而异;系统在mmap(KE_OS)和相关服务使用的动态段区域(Dynamic Segment Area)中创建解释器的段。因此,共享目标文件解释器通常不会与原始可执行文件的原始段地址冲突。
- 可执行文件(Executable File)解释器一般会被加载到固定地址(Fixed Addresses);系统使用程序头表(Program Header Table)中的虚拟地址(p_vaddr)创建它的段。因此,可执行文件解释器的虚拟地址可能与第一个可执行文件发生冲突;解释器负责解决冲突。
5.9.2 .dynamic
此节保存动态链接信息。该节的属性将设置SHF_ALLOC位。是否设置SHF_WRITE位是特定于处理器的。此节属于SHT_DYNAMIC类型。
如果一个目标文件(Object File)参与到动态链接(Dynamic Linking)的过程中,它的程序头表(Program Header Table)将有一个类型为PT_DYNAMIC的元素。这个“段(Segment)”包含.dynamic节。一般使用特殊符号_DYNAMIC标记包含以下结构体数组的节:
1 | typedef struct { |
其中,d_tag的取值决定了该如何解释d_un。
- d_val:这个字段表示一个整数值,可以有多种解释。
- d_ptr:这个字段表示程序虚拟地址(Program Virtual Addresses)。如前所述,文件的虚拟地址(File’s Virtual Addresses)在执行期间可能与内存的虚拟地址(Memory Virtual Addresses)不匹配。当解释动态结构(Dynamic Structure)中包含的地址时,动态链接器(Dynamic Linker)根据原始文件值(Original File Value)以及内存的基地址(Memory Base Address)计算实际地址。为了保持一致性,文件不包含“纠正”动态结构中的地址的重定位条目。
下表总结了可执行文件(Executable Files)和共享目标文件(Shared Object Files)的标签(d_tag)要求。如果标签(d_tag)被标记为“强制(Mandatory)”,则符合TIS ELF的文件的动态链接数组(Dynamic Linking Array)必须具有该类型的条目。同样,“可选(Optional)”意味着动态链接数组的该标签条目可能出现但不是必需的。
宏名称 | 值 | d_un | 可执行文件 | 共享目标文件 |
---|---|---|---|---|
DT_NULL | 0 | Ignored | Mandatory | Mandatory |
DT_NEEDED | 1 | d_val | Optional | Optional |
DT_PLTRELSZ | 2 | d_val | Optional | Optional |
DT_PLTGOT | 3 | d_ptr | Optional | Optional |
DT_HASH | 4 | d_ptr | Mandatory | Mandatory |
DT_STRTAB | 5 | d_ptr | Mandatory | Mandatory |
DT_SYMTAB | 6 | d_ptr | Mandatory | Mandatory |
DT_RELA | 7 | d_ptr | Mandatory | Optional |
DT_RELASZ | 8 | d_val | Mandatory | Optional |
DT_RELAENT | 9 | d_val | Mandatory | Optional |
DT_STRSZ | 10 | d_val | Mandatory | Mandatory |
DT_SYMENT | 11 | d_val | Mandatory | Mandatory |
DT_INIT | 12 | d_ptr | Optional | Optional |
DT_FINI | 13 | d_ptr | Optional | Optional |
DT_SONAME | 14 | d_val | Ignored | Optional |
DT_RPATH | 15 | d_val | Optional | Optional |
DT_SYMBOLIC | 16 | Ignored | Ignored | Optional |
DT_REL | 17 | d_ptr | Mandatory | Optional |
DT_RELSZ | 18 | d_val | Mandatory | Optional |
DT_RELENT | 19 | d_val | Mandatory | Optional |
DT_PLTREL | 20 | d_val | Optional | Optional |
DT_DEBUG | 21 | d_ptr | Optional | Ignored |
DT_TEXTREL | 22 | Ignored | Optional | Optional |
DT_JMPREL | 23 | d_ptr | Optional | Optional |
DT_BIND_NOW | 24 | Ignored | Optional | Optional |
DT_INIT_ARRAY | 25 | d_ptr | Optional | Optional |
DT_FINI_ARRAY | 26 | d_ptr | Optional | Optional |
DT_INIT_ARRAYSZ | 27 | d_val | Optional | Optional |
DT_FINI_ARRAYSZ | 28 | d_val | Optional | Optional |
DT_RUNPATH | 29 | d_val | Optional | Optional |
DT_FLAGS | 30 | d_val | Optional | Optional |
DT_ENCODING | 32 | Unspecified | Unspecified | Unspecified |
DT_PREINIT_ARRAY | 32 | d_ptr | Optional | Ignored |
DT_PREINIT_ARRAYSZ | 33 | d_val | Optional | Ignored |
DT_SYMTAB_SHNDX | 34 | d_ptr | Optional | Optional |
DT_MAXPOSTAGS | 34 | Unspecified | Unspecified | Unspecified |
DT_NUM | 35 | Unspecified | Unspecified | Unspecified |
DT_LOOS | 0x6000000D | Unspecified | Unspecified | Unspecified |
DT_HIOS | 0x6FFFF000 | Unspecified | Unspecified | Unspecified |
DT_VALRNGLO | 0x6FFFFD00 | Unspecified | Unspecified | Unspecified |
DT_GNU_PRELINKE | 0x6FFFFDF5 | d_val | Optional | Optional |
DT_GNU_CONFLICTSZ | 0x6FFFFDF6 | d_val | Optional | Optional |
DT_GNU_LIBLISTSZ | 0x6FFFFDF7 | d_val | Optional | Optional |
DT_CHECKSUM | 0x6FFFFDF8 | d_val | Optional | Optional |
DT_PLTPADSZ | 0x6FFFFDF9 | d_val | Optional | Optional |
DT_MOVEENT | 0x6FFFFDFA | d_val | Optional | Optional |
DT_MOVESZ | 0x6FFFFDFB | d_val | Optional | Optional |
DT_FEATURE_1 | 0x6FFFFDFC | d_val | Optional | Optional |
DT_POSFLAG_1 | 0x6FFFFDFD | d_val | Optional | Optional |
DT_SYMINSZ | 0x6FFFFDFE | d_val | Optional | Optional |
DT_SYMINENT | 0x6FFFFDFF | d_val | Optional | Optional |
DT_VALRNGHI | 0x6FFFFDFF | Unspecified | Unspecified | Unspecified |
DT_ADDRRNGLO | 0x6FFFFE00 | Unspecified | Unspecified | Unspecified |
DT_GNU_HASH | 0x6FFFFEF5 | d_ptr | Optional | Optional |
DT_TLSDESC_PLT | 0x6FFFFEF6 | d_ptr | Optional | Optional |
DT_TLSDESC_GOT | 0x6FFFFEF7 | d_ptr | Optional | Optional |
DT_GNU_CONFLICT | 0x6FFFFEF8 | d_ptr | Optional | Optional |
DT_GNU_LIBLIST | 0x6FFFFEF9 | d_ptr | Optional | Optional |
DT_CONFIG | 0x6FFFFEFA | d_ptr | Optional | Optional |
DT_DEPAUDIT | 0x6FFFFEFB | d_ptr | Optional | Optional |
DT_AUDIT | 0x6FFFFEFC | d_ptr | Optional | Optional |
DT_PLTPAD | 0x6FFFFEFD | d_ptr | Optional | Optional |
DT_MOVETAB | 0x6FFFFEFE | d_ptr | Optional | Optional |
DT_SYMINFO | 0x6FFFFEFF | d_ptr | Optional | Optional |
DT_ADDRRNGHI | 0x6FFFFEFF | Unspecified | Unspecified | Unspecified |
DT_VERSYM | 0x6FFFFFF0 | d_ptr | Optional | Optional |
DT_RELACOUNT | 0x6FFFFFF9 | d_val | Optional | Optional |
DT_RELCOUNT | 0x6FFFFFFA | d_val | Optional | Optional |
DT_FLAGS_1 | 0x6FFFFFFB | d_val | Optional | Optional |
DT_VERDEF | 0x6FFFFFFC | d_ptr | Optional | Optional |
DT_VERDEFNUM | 0x6FFFFFFD | d_val | Optional | Optional |
DT_VERNEED | 0x6FFFFFFE | d_ptr | Optional | Optional |
DT_VERNEEDNUM | 0x6FFFFFFF | d_val | Optional | Optional |
DT_LOPROC | 0x70000000 | Unspecified | Unspecified | Unspecified |
DT_SPARC_REGISTER | 0x70000001 | d_val | Optional | Optional |
DT_AUXILIARY | 0x7FFFFFFD | d_val | Unspecified | Optional |
DT_USED | 0x7FFFFFFE | d_val | Optional | Optional |
DT_FILTER | 0x7FFFFFFF | d_val | Unspecified | Optional |
DT_HIPROC | 0x7FFFFFFF | Unspecified | Unspecified | Unspecified |
DT_NULL
:带有DT_NULL标签的_DYNAMIC[]数组条目标志着_DYNAMIC[]数组的结束。DT_NEEDED
:d_val。此元素保存所需库(Needed Library)的名称字符串(以Null结尾的字符串)在字符串表(.dynstr)中的字节偏移量。偏移量(Offset)是记录在DT_STRTAB条目中的字符串表(.dynstr)的索引。有关这些名称的更多信息,请参阅“共享目标文件依赖”。_DYNAMIC[]数组可能包含多个具有此类型的条目。这些条目的相对顺序很重要,但它们与其他类型条目的关系并不重要。DT_PLTRELSZ
:d_val。此元素保存与过程链接表(Procedure Linkage Table)关联的重定位条目的总大小(以字节为单位),也就是.rel.plt节的大小。如果存在DT_JMPREL类型的条目,则必须附带一个DT_PLTRELSZ类型的条目。DT_PLTGOT
:d_ptr。此元素保存与过程链接表(Procedure Linkage Table)和/或全局偏移表(Global Offset Table)相关联的节(.got.plt)的地址。DT_HASH
:d_ptr。此元素保存符号哈希表(Symbol Hash Table,.hash)节的地址。此哈希表引用DT_SYMTAB元素引用的符号表(.dynsym)。DT_STRTAB
:d_ptr。此元素保存字符串表(.dynstr)的地址。符号名称(Symbol Names)、库名称(Library Names)和其他字符串位于该表中。DT_SYMTAB
:d_ptr。此元素保存符号表(.dynsym)的地址。DT_RELA
:d_ptr。此元素保存重定位表(.rela.dyn)的地址。表中的条目具有显式加数(Explicit Addends),例如:32位ELF文件使用Elf32_Rela结构。一个目标文件(Object File)可能有多个重定位节(Relocation Sections)。当为可执行文件(Executable Files)或共享目标文件(Shared Object Files)构建重定位表(Relocation Table)时,链接器(Link Editor)将这些节连接起来以形成单个表。尽管这些节在目标文件中保持独立,但动态链接器(Dynamic Linker)看到的是单个表。当动态链接器为可执行文件创建进程映像(Process Image)或向进程映像添加共享目标文件时,它会读取重定位表并执行相关操作。如果存在此元素,则_DYNAMIC[]数组还必须具有DT_RELASZ和DT_RELAENT元素。当重定位对于文件是“强制的(Mandatory)”时,DT_RELA和DT_REL元素只有一个会存在(两者都是允许的,但不是必需的)。DT_RELASZ
:d_val。此元素保存DT_RELA元素指向的重定位表(.rela.dyn)的总大小,以字节为单位。DT_RELAENT
:d_val。此元素保存DT_RELA元素指向的重定位表(.rela.dyn)的重定位条目(Elfxx_Rela)的大小,以字节为单位。DT_STRSZ
:d_val。此元素保存字符串表(.dynstr)的大小,以字节为单位。DT_SYMENT
:d_val。此元素保存符号表条目(Elfxx_Sym)的大小,以字节为单位。DT_INIT
:d_ptr。此元素保存初始化函数(Initialization Function)的地址,也是.init节的地址。DT_FINI
:d_ptr。此元素保存终止函数(Termination Function)的地址,也是.fini节的地址。DT_SONAME
:d_val。此元素保存此共享对象的名称(以Null结尾的字符串)在字符串表(.dynstr)中的字节偏移量。偏移量(Offset)是记录在DT_STRTAB条目中的字符串表(.dynstr)的索引。有关这些名称的更多信息,请参阅“共享目标文件依赖”。DT_RPATH
:d_val。此元素保存搜索库搜索路径字符串(Search Library Search Path String)(以Null结尾的字符串)在字符串表(.dynstr)中的字节偏移量。在“共享目标文件依赖”中讨论过。偏移量(Offset)是记录在DT_STRTAB条目中的字符串表(.dynstr)的索引。DT_SYMBOLIC
:此元素在共享目标库(Shared Object Library)中的存在改变了动态链接器(Dynamic Linker)对库内引用的符号的解析算法。动态链接器不是从可执行文件(Executable Files)开始符号搜索,而是从共享目标文件本身开始。如果共享目标文件未能提供引用的符号,则动态链接器会像往常一样搜索可执行文件和其他共享目标文件。DT_REL
:d_ptr。此元素保存重定位表(.rel.dyn)的地址。此元素与DT_RELA类似,但其表中的条目具有隐式加数(Implicit Addends),例如:32位ELF文件使用Elf32_Rel结构。如果此元素存在,则_DYNAMIC[]数组还必须具有DT_RELSZ和DT_RELENT元素。DT_RELSZ
:d_val。此元素保存DT_REL元素指向的重定位表(.rel.dyn)的总大小,以字节为单位。DT_RELENT
:d_val。此元素保存DT_REL元素指向的重定位表(.rel.dyn)的重定位条目(Elfxx_Rel)的大小,以字节为单位。DT_PLTREL
:d_val。此元素保存过程链接表(Procedure Linkage Table)所引用的重定位条目的类型。d_val成员根据需要保存DT_REL或DT_RELA的值。过程链接表(Procedure Linkage Table)中的所有重定位必须使用相同的重定位。DT_DEBUG
:d_ptr。此元素用于调试。本文件中未规定其内容。DT_TEXTREL
:此元素的缺失意味着任何重定位表项都不应该导致对不可写段(Non-Writable Segment)的修改,如程序头表(Program Header Table)中的段权限所指定的那样。如果此元素存在,一个或多个重定位条目可能会请求对不可写段进行修改,并且动态链接器(Dynamic Linker)可以相应地进行准备。DT_JMPREL
:d_ptr。此元素如果存在,此条目的d_ptr成员保存仅与过程链接表(Procedure Linkage Table)关联的重定位表[.rel(a).plt]的地址。如果启用了延迟绑定(Lazy Binding),则分离这些重定位条目可以让动态链接器(Dynamic Linker)在进程初始化期间忽略它们。如果此元素存在,则DT_PLTRELSZ和DT_PLTREL类型的相关元素也必须存在。DT_BIND_NOW
:此元素如果存在于共享目标文件(Shared Object Files)或可执行文件(Executable Files)中,则该元素指示动态链接器(Dynamic Linker)在将控制转移到程序之前处理包含该元素的目标文件的所有重定位。当通过环境或通过dlopen(BA_LIB)指定时,此元素的存在优先于对该目标文件使用延迟绑定(Lazy Binding)的指示(Directive)。此元素的使用已被DF_BIND_NOW标志取代。DT_INIT_ARRAY
:d_ptr。此元素保存指向初始化函数的指针数组(.init_array)的地址。此元素还要求存在DT_INIT_ARRAYSZ元素。DT_FINI_ARRAY
:d_ptr。此元素保存指向终止函数的指针数组(.fini_array)的地址。此元素还要求存在DT_FINI_ARRAYSZ元素。DT_INIT_ARRAYSZ
:d_val。此元素保存DT_INIT_ARRAY数组(.init_array)的总大小,以字节为单位。DT_FINI_ARRAYSZ
:d_val。此元素保存DT_FINI_ARRAY数组(.fini_array)的总大小,以字节为单位。DT_RUNPATH
:d_val。此元素保存库搜索路径字符串(Library Search Path String)(以Null结尾的字符串)在DT_STRTAB指示的字符串表(.dynstr)中的字节偏移量。DT_FLAGS
:d_val。此元素保存特定于此目标文件的标志值。DT_ENCODING
:大于等于DT_ENCODING,小于等于DT_LOOS的动态标签值(Dynamic Tag Values),遵循d_un联合体的解释规则。DT_PREINIT_ARRAY
:d_ptr。此元素保存指向预初始化函数的指针数组(.preinit_array)的地址。此元素还要求存在DT_PREINIT_ARRAYSZ元素。仅处理包含在可执行文件(Executable Files)中的此数组。如果此数组包含在共享目标文件(Shared Object Files)中,则忽略此数组。DT_PREINIT_ARRAYSZ
:d_val。此元素保存DT_PREINIT_ARRAY数组(.preinit_array)的总大小,以字节为单位。DT_SYMTAB_SHNDX
:d_ptr。此元素保存扩展的符号表节索引节(.symtab_shndx)的地址。DT_MAXPOSTAGS
:正的动态数组标记值(Dynamic Array Tag Values)的数量。DT_NUM
:动态数组标记值(Dynamic Array Tag Values)的数量。DT_LOOS
:保留用于特定的操作系统语义的动态数组标记值(Dynamic Array Tag Values)范围的下限。这些值都遵循d_un联合体的解释规则。DT_HIOS
:保留用于特定的操作系统语义的动态数组标记值(Dynamic Array Tag Values)范围的上限。这些值都遵循d_un联合体的解释规则。DT_VALRNGLO
:用于Elfxx_Dyn结构的d_un.d_val字段的值的范围的下限。DT_GNU_PRELINKE
:d_val。此元素保存Prelinking的时间戳。DT_GNU_CONFLICTSZ
:DT_GNU_LIBLISTSZ
:DT_CHECKSUM
:DT_PLTPADSZ
:DT_MOVEENT
:DT_MOVESZ
:DT_FEATURE_1
:DT_POSFLAG_1
:DT_SYMINSZ
:DT_SYMINENT
:DT_VALRNGHI
:用于Elfxx_Dyn结构的d_un.d_val字段的值的范围的上限。DT_ADDRRNGLO
:DT_GNU_HASH
:DT_TLSDESC_PLT
:DT_TLSDESC_GOT
:DT_GNU_CONFLICT
:DT_GNU_LIBLIST
:DT_CONFIG
:DT_DEPAUDIT
:DT_AUDIT
:DT_PLTPAD
:DT_MOVETAB
:DT_SYMINFO
:DT_ADDRRNGHI
:DT_VERSYM
:DT_RELACOUNT
:DT_RELCOUNT
:DT_FLAGS_1
:DT_VERDEF
:DT_VERDEFNUM
:DT_VERNEED
:DT_VERNEEDNUM
:DT_LOPROC
:DT_SPARC_REGISTER
:DT_AUXILIARY
:DT_USED
:DT_FILTER
:DT_HIPROC
:
5.10 Relocation Related Sections
5.10.1 .rel(a).dyn
5.10.2 .rel(a).plt
.rel(a).xxxx
可重定位文件中
.rel.text
.rel.data
5.11 Global Offset Table
5.11.1 .got
5.11.2 .got.plt
5.12 Procedure Linkage Table
5.12.1 .plt
5.12.2 .plt.got
5.13 .eh_frame Related
5.13.1 .eh_frame_hdr
5.13.2 .eh_frame
Thread-Local Storage Section
.tdata & .tdata1
.tbss
.debug
Prelink Related Sections
Prelink是一种旨在加速ELF程序在各种Linux架构上的动态链接(Dynamic Linking)的工具。
1995年,Linux将其二进制格式从a.out更改为ELF。a.out二进制格式非常不灵活,而且很难构建共享库(Shared Libraries)。Linux在a.out中的共享库是位置相关(Position Dependent)的,并且在链接时必须为每个共享库分配一个唯一的虚拟地址空间槽(Virtual Address Space Slot)。即使只有几个共享库,维护这些分配也非常困难,过去有一个由人以文本文件形式维护的集中的地址注册表(Central Address Registry),但是,当有成千上万个不同的共享库,它们的大小、版本和导出符号都在不断变化时,这肯定是不可能做到的。另一方面,为了加载这些共享库,动态链接器(Dynamic Linker)只需要做最少量的工作,因为重定位处理(Relocation Handling)和符号查找(Symbol Lookup)仅在链接时完成。动态链接器使用uselib系统调用,它只是将命名库(Named Library)映射到地址空间[没有段(Segment)或节(Section)保护差异,整个映射是可写和可执行的]。
ELF二进制格式是最灵活的二进制格式之一,它的共享库(Shared Libraries)易于构建,并且不需要集中分配(Central Assignment)虚拟地址空间槽(Virtual Address Space Slot)。共享库与位置无关(Position Independent),重定位处理(Relocation Handling)和符号查找(Symbol Lookup),部分在创建可执行文件时完成,部分在运行时完成。通过预加载(Preloading)一个定义这些符号的新共享库,或者没有通过添加符号到之前在符号查找时搜索到的共享库来重新链接可执行文件,或者通过向程序使用的库中添加新的依赖共享库,可以在运行时覆盖共享库中的符号。所有这些改进都有其代价,即程序启动速度较慢、每个进程具有更多的非共享内存,以及与共享库中的位置无关代码(Position Independent Code)相关的运行时成本。
ELF程序的程序启动比使用共享库的a.out程序的启动慢,因为动态链接器(Dynamic Linker)在调用程序入口点之前有更多的工作要做。加载库(Loading Libraries)的成本稍大一些,因为ELF共享库通常具有单独的只读段(Read-Only Segments)和可写段(Writable Segments),因此动态链接器必须为每个段使用不同的内存保护。主要的区别在于重定位处理(Relocation Handling)和相关联的符号查找(Associated Symbol Lookup)。在a.out格式中,运行时没有重定位处理或符号查找。在ELF格式中,这一成本比过去在Linux中从a.out到ELF过渡期间要重要得多,特别是现在GUI程序不断增长,并开始使用越来越多的共享库。5年前,使用超过10个共享库的程序非常少见,如今大多数GUI程序链接到大约40个或更多共享库,在极端情况下,程序甚至使用90多个共享库。每个共享库都会将其动态重定位集添加到成本中并扩大符号搜索范围,因此除了执行更多符号查找之外,应用程序必须执行的每一次符号查找的平均代价都更高。另一个增加成本的因素是在共享库的符号哈希表(Symbol Hash Table)中查找符号时必须比较符号名称的长度。C++库往往具有极长的符号名称,不幸的是,新的C++ ABI将命名空间(Namespaces)和类名(Class Names)放在最前面,方法名(Method Names)放在最后,因此符号名称通常仅在很长名称的最后几个字节上有所不同。
每次应用重定位时,包含要写入的地址的整个内存页都必须加载到内存中。操作系统执行写时复制(Copy-on-Write)操作,这也导致内存页的物理内存(Physical Memory)不能再与其他进程共享。使用ELF,通常所有程序的全局偏移表(Global Offset Table),包含指向共享库中对象的指针的常量和变量等都在动态链接器(Dynamic Linker)将控制权交给程序之前写入。
在大多数架构上(除了一些例外,如AMD64架构)位置无关代码(Position Independent Code)要求一个寄存器需要专门用作PIC寄存器,因此不能在函数中用于其他目的。这尤其会降低寄存器匮乏(Register-Starved)架构(如:IA-32)的性能。此外,需要一些代码来设置PIC寄存器,要么作为函数序言(Function Prologues)的一部分调用,要么在调用序列中使用函数描述符(Function Descriptors)时调用。
Prelink是一个工具,它与相应的动态链接器(Dynamic Linker)和链接器(Linker)更改一起尝试将一些a.out的优势(例如:速度和更少的COW页面)带到ELF二进制格式,同时保留其所有的灵活性。它还以有限的方式尝试减少由重定位创建的不可共享页面(Non-Shareable Pages)的数量。Prelink与GNU C库中的动态链接器密切配合,但是将它移植到其他一些使用动态链接器可以以类似方式修改的平台的ELF中可能并不太难。
Prelink Design
Prelink的设计使其需要尽可能少的ELF扩展。它不应该绑定到特定的架构,而应该适用于所有ELF架构。在程序启动期间,它应该避免所有符号查找,如上所示,这是非常耗时的。它需要在共享库和可执行文件不断变化的环境中工作,无论是因为安全更新(Security Updates)还是功能增强(Feature Enhancements)。它应该避免动态链接器(Dynamic Linker)和工具之间的大量代码重复。即使在非预链接(Non-Prelinked)的可执行文件中,或者当共享库(Shared Libraries)之一升级并且可执行文件(Executable Files)的预链接(Prelinking)尚未更新时,预链接的共享库(Prelinked Shared Libraries)也需要可用。
为了最大限度地减少启动期间执行的重定位次数,需要尽可能多地重定位共享库和可执行文件。对于相对重定位(Relative Relocations),这意味着库需要始终加载到相同的基地址,对于其他重定位,这意味着必须始终加载具有这些重定位解析到的定义的所有共享库(通常包括库或可执行文件所依赖的所有共享库)到相同的地址。ELF可执行文件(位置无关可执行文件除外)在链接期间已经固定了它们的加载地址。对于共享库,Prelink需要类似于a.out虚拟地址空间槽(Virtual Address Space Slot)注册表的东西。在所有安装中维护这样的注册表并不能很好地扩展,因此Prelink在查看它应该加速的所有可执行文件及其所有依赖的共享库后,会动态分配这些虚拟地址空间槽。下一步是将共享库实际重定位到分配的基地址。
完成后,共享库的实际预链接(Prelinking)就可以完成了。首先,所有依赖的共享库都需要预链接(Prelink不支持共享库之间的循环依赖,只会警告它们而不是Prelinking循环依赖的库),然后,对于共享库中的每个重定位,Prelink都需要查找共享库的自然符号搜索范围(Natural Symbol Search Scope)内的符号(首先是共享库本身,然后是所有依赖的共享库的广度优先搜索),并将重定位应用于符号的目标节(Symbol’s Target Section)。动态链接器(Dynamic Linker)中的符号查找代码相当复杂和庞大,因此为了避免重复所有这些,Prelink选择使用动态链接器来进行符号查找。动态链接器通过一个特殊的环境变量被告知它应该打印所有执行的符号查找及其类型,并且Prelink通过管道(Pipe)读取此输出。因为其中一个要求是,即使是Non-Prelinked的可执行文件,Prelink的共享库必须是可用的[复制所有的共享库,这样就有了原始的(Pristine)和预链接的(Prelinked)副本,这将对RAM的使用非常不友好],Prelink必须确保通过应用重定位不会丢失任何信息,因此可以在Non-Prelinked可执行文件的启动时廉价地完成重定位处理。对于RELA架构,这更容易,因为在处理重定位时不需要重定位目标内存的内容。对于REL架构,情况并非如此。Prelink尝试了一些后面描述的技巧,如果失败,需要将REL重定位节转换为RELA格式,其中加数(Addend)存储在重定位节而不是重定位目标的内存中。
当一个可执行文件(直接或间接)依赖的所有共享库都被Prelink时,可执行文件中的重定位处理类似于共享库中的重定位。不幸的是,在共享库的自然符号搜索范围(Natural Symbol Search Scope)中查找时(即在共享库Prelink时完成)和在应用程序的全局符号搜索范围(Global Symbol Search Scope)中查找时,并不是所有的符号都有相同的解析。此类符号在本文中被称为冲突(Conflicts),对这些符号的重定位被称为冲突的重定位(Conflicting Relocations)。冲突取决于可执行文件、及其依赖的所有共享库和它们各自的顺序。它们仅适用于链接到可执行文件的共享库(DT_NEEDED动态标记中提到的库和它们递归依赖的共享库)。Prelink无法预测通过dlopen(3)加载的共享库集,也无法预测这些库加载的顺序,以及它们被卸载的时间。当动态链接器(Dynamic Linker)打印在可执行文件中完成的符号查找时,它也会打印冲突(Conflicts)。然后Prelink针对这些符号进行所有重定位,并构建一个带有冲突修复(Conflict Fixups)的特殊RELA节,并将其存储到Prelink的可执行文件中。此外,所有依赖共享库的列表(按它们在符号搜索范围中出现的顺序)及其校验(Checksums)和和预链接时间(Times of Prelinking)存储在另一个特殊节中。
动态链接器(Dynamic Linker)首先检查它本身是否是Prelink的。如果是,就可以避免它的初步重定位处理(这个是在搜索范围内只用动态链接器本身完成的,这样动态链接器中的所有例程都可以轻松使用,没有太多限制)。当它即将启动一个程序时,它首先查看由Prelink创建的库列表节(如果有),并检查它们是否以相同的顺序出现在符号搜索范围(Symbol Search Scope)内,自Prelink以来没有被修改,并且没有加载任何新的共享库。如果满足所有这些条件,则可以使用Prelink。在这种情况下,动态链接器处理修复部分(Fixup Section)并跳过所有正常的重定位处理。如果不满足一个或多个条件,动态链接器将继续在可执行文件和所有共享库中进行正常的重定位处理。
6 Reference
[^1]: Executable and Linking Format(ELF) Specification v1.2
[^2]: WikiPedia - Executable and Linkable Format
[^3]: CTF Wiki - ELF 文件格式
[^4]: 关于嵌入式的 bin、hex、axf、map - 2020
[^5]: Linux Standard Base Core Specification 3.1 - 2005
[^6]: Oracle® Solaris 11.3 Linkers and Libraries Guide - 2018
[^7]: Gabriel Corona - The ELF file format - 2015
[^8]: Oracle Solaris Blog - GNU Hash ELF Sections - 2008
[^9]: r00tk1t - GNU Hash ELF Sections(译) - 2017
[^10]: Kevin’s Attic - ELF Sections for Exception Handling - 2017
[^11]: Format of Executable and Linking Format(ELF) files
[^12]: icefireelf - 经典字符串hash函数介绍及性能比较 - 2010
[^13]: jmpews’s Blog - Pwn之ELF解析 - 2016
[^14]: 程序的本质之三 - ELF文件中与符号(Symbol)相关的Section的定义 - 2019
[^15]: Linux Standard Base Core Specification 5.0