ELF Format: 程序加载和动态链接

来源:互联网 发布:金华职业技术学院网络 编辑:程序博客网 时间:2024/06/06 00:16

Refer to: http://www.skyfree.org/linux/references/ELF_Format.pdf


前一篇文讲了ELF format相关的东西,这篇翻下ELF文件的程序加载和动态链接知识


1. 介绍

可执行文件和共享目标文件实际就是静态的程序,要执行程序,系统需要创建这些文件对应的动态程序,就是进程映像(Process Image)。进程映像内放置了段(segments)来存储文本(text)、数据(data)、栈(stack)等。本文主要讨论以下几个方面:

程序头(Program Header):程序头是描述与程序执行直接相关的主要目标文件结构,用于定位文件中的段映像(segment images),并且包含创建进程映像所必需的信息。

程序加载(Program Loading):将目标文件加载到内存中的过程

动态链接(Dynamic linking):某些程序加载完一个可执行文件还不完善,还必须进一步解析里面的符号引用(这些符号的定义在其他的共享目标文件中),这个过程通过动态链接完成。


2. 程序头(Program Header)

可执行文件或者共享目标文件的程序头表(Program Header Table)是一个固定结构(就是Program Header)的列表,每个列表条目描述一个用于程序执行的段或其他信息。目标文件的每个段都包含一个或多个节(sections)。程序头表只对可执行文件或共享目标文件有意义,重定位文件不会用到程序头表。ELF头(ELF header)中的e_phoff, e_phentsize和e_phnum成员描述了该文件中程序头的位置和大小信息。

p_type: 这个条目描述的段类型,或者怎样解析此条目中的信息

p_offset: 从文件开始到段的第一个字节的偏移量

p_vaddr: 段在内存中的虚拟地址

p_paddr: 在使用物理地址的系统中,这个成员用于存储段的物理地址

p_filesz: 段在文件中的大小,可能为0

p_memsz: 段在内存中的大小,可能为0

p_flags: 段相关的标志

p_align: 可加载进程中的段必须与p_vaddr和p_offset匹配,并且是页大小的整数倍。这个成员描述了段在内存和文件中的对齐数值。0和1表示没有对齐要求,否则p_align必须为2的正整数次方,并且p_vaddr等于p_offset且模p_align等于0。


2.1 段类型(Segment Types)

程序头表中某些条目描述进程段,另一些只提供额外的信息而不会加载到进程映像。段条目除一些特殊的条目外没有固定顺序。以下为段类型:


PT_NULL: 未使用的列表条目,该条目中其他成员的值是未定义的

PT_LOAD: 可加载的段,将文件中p_filesz字节加载到对应内存段中的的开始位置,文件段不能小于内存段,如果内存段大小(p_memsz)大于文件段大小,多余空间填充为0。程序头表中的可加载段根据p_vaddr值升序排列。

PT_DYNAMIC: 动态链接相关信息

PT_INTERP: 指定一个可调用解释器的字符串路径名(成员中存的是该路径的位置和大小)。只有可执行文件才有这种段(共享文件也可以有。。),并且只能有一个。此段必须放在所有的可加载段条目之前。

PT_NOTE: 附加信息的位置和大小

PT_SHLIB: 预留类型,无意义

PT_PHDR: 指出该程序头表(program header table)自身在文件和内存映像中的位置和大小,一个文件只能有一个这种类型,并且只有当程序头表是进程内存映像一部分时才有。此类型条目必须在所有可加载段条目之前。

PT_LOPROC - PT_HIPROC: 预留给处理器相关语义


2.2 基地址(Base Address)

可执行和共享目标文件都有一个基地址,该地址是目标程序映像在内存中的最小虚拟地址。基地址被用于动态链接中重定位内存映像。

基地址是在执行过程中由以下3个值计算出:内存加载地址、最大内存页大小、程序可加载段的最小虚拟地址。程序头(program headers)中的虚拟地址不代表程序内存映像中的实际虚拟地址,当计算基地址时,需要找出所有可加载段中的最小的p_vaddr值,然后根据最大页大小计算出基地址。对于有些类型的文件,内存地址可能不等于p_vaddr。

.bss Section是一个系统section,它的类型为SHT_NOBITS。这个section虽然不占用文件空间,但是却会影响对应段的内存映像。通常这些未初始化数据都被放在段尾,因此需要在相应段头条目中将p_memsz设为大于p_filesz的值。


2.3 注解节(Note Section)

有些系统或者供应商需要在目标文件中标记特殊信息以供其他程序识别或者兼容,类型为SHT_NOTE的section和类型为PT_NOTE的程序头条目就是做这件事的。section或者程序头条目中的Note信息存储一些条目,每个条目中包含是一个序列(4-byte word)。下图是一个例子

namesz and name: name中的头namesz个字节是一个表示所有者或者开发者的字符串

descsz and desc: desc中的头descsz个字节是描述信息

type: 用于解析描述信息


如下是一个Note段实例:


3. 程序加载(Program Loading)

当系统创建一个进程映像时,它只是逻辑上将文件中的段拷贝到对应的虚拟内存段,而实际上只有当执行是需要用到某个逻辑页时,它才会被加载到内存中,一个进程中有大量的没有被使用的逻辑页。因此延迟物理读取可以提高系统性能。要取得这样的效率,我们就要让可执行文件和共享目标文件中段映像的偏移和虚拟地址模页尺寸相等(就是全等模页尺寸,congruent modulo the page size)


上图是一个目标文件实例,Text和Data段的的偏移和虚拟地址都全等模4KB,需要4个文件页来存储text或者data段

第一个text页包括ELF header、程序头表(program header table)、和其他信息

最后一个text页持有data段开始部分的拷贝

第一个data页存有text段尾部分拷贝

最后一个data也可能包含与运行中的进程无关的文件信息


下图是程序头段(Program Header Segments)


系统在逻辑上将每个段看成是完整且独立的,并以此来设置内存权限;段的地址会被调整来保证地址空间中每个逻辑页都有一个单独的权限集。上例中,文件中存储text段尾部分和data段首部分的区域会被映射两次:text段的虚拟地址和一个data段的虚拟地址

data段尾部分对未初始化数据需要进行特殊处理,系统会将这部分填充为0。因此如果文件的最后一个data页包含了逻辑内存页中不存在的信息,那这些额外的数据会被设置为0(没太明白。。)。另外3个页中的’杂质‘逻辑上不在进程映像中,未明确规定系统是否清除他们。该文件对应的内存映像如下(假设页尺寸为4KB,0x1000):



可执行文件与共享目标文件在段的加载方面有一点不同。可执行文件的段一般包含绝对代码(absolute code),为了使进程正确执行,这些段必须位于用来构建可执行文件的虚拟地址中。所以系统直接使用p_vaddr当作段在内存中的虚拟地址。

而共享目标文件的段一般包含位置无关代码,对于不同进程,这些段的虚拟地址会变动。虽然系统对于每个单独的进程使用虚拟地址,但是对于这些段却使用相对地址。因为地址无关代码使用段与段之间的相对地址,因此文件虚拟地址之间的差异必须与内存虚拟地址之间的差异相同。下表展示了一个共享目标文件在不同进程中的虚拟地址,不同进程中虚拟地址虽然不同,但虚拟地址之间的差是相同的,此例同样展示了基地址的计算:


4. 动态链接(Dynamic Linking)

4.1 程序解释器(Program Interpreter)

每个可执行文件都可能包含一个PT_INTERP类型的程序头条目,在执行exec(BA_OS)期间,系统从PT_INTERP段中获取解释器(其实也是一个可执行/共享目标文件)路径,并且用改解释器文件的段来创建初始进程映像。系统使用解释器来组建内存映像而不是使用原来那个可执行文件,这种情况下解释器就要负责从系统那得到控制权,并为应用程序提供运行环境。

解释器有两种获取控制权的方式。第一种是获得一个读取可执行文件的文件描述符(指向文件开头位置),它可以使用这个文件描述符读取(或者映射)可执行文件的段到内存中。第二种是系统根据可执行文件格式(特定格式),将可执行文件加载到内存中。解释器本身是一个可执行文件或者共享 目标文件:

    如果解释器是共享目标文件,那该文件会以位置无关代码加载(加载地址在不同进程里可能不同);系统在动态段区域中创建它的段,共享目标类型解释器通常不会与原本的可执行文件的段地址产生冲突。

    如果解释器是可执行文件,那解释器会被加载到固定地址。系统使用程序头表中的虚拟地址来创建解释器的段,可执行文件类型解释器的虚拟地址可能与原本的可执行文件产生冲突,解释器要负责解决冲突。


4.2 动态链接器(Dynamic Linker)

使用动态链接构建一个可执行文件时,链接器会把一个PT_INTERP类型的程序头条目(program header element)插入可执行文件,以使系统将动态链接器当成程序解释器来调用。

Exec(BA_OS)与动态链接器一起创建程序的内存映像,包括以下步骤:

    将可执行文件的内存段添加到进程映像

    将共享目标的内存段添加到进程映像

    为可执行文件和它的共享目标文件进行重定位

    如果没有其他进程使用(除动态链接器外),关闭读取可执行文件的文件描述符

    将控制权交给程序,使其看起来就像是直接从exec(BA_OS)获得的控制权一样


连接器还会为辅助可执行文件与共享目标文件的动态链接构建一些数据,在上面的’程序头(Program Header)‘中,这些数据在可加载(loadable)段中。

    类型为SHT_DYNAMIC的.dynamic section存储了其中一些数据记录了其他动态链接信息的地址,这些数据位于section的开始位置

    类型为SHT_HASH的.hash section存储了一个符号哈希表

    类型为SHT_PROGBITS的.got和.plt sections分别存有两个表:全局偏移表(the global offset table)和进程链接表(the procedure linkage table)。下面将介绍动态连接器怎样使用这些表来建立目标文件的内存映像。


由于ABI(Application Binary Interface)标准的程序都需要从共享目标库中引入一些基础的系统服务,所以动态链接器将参与到每个ABI程序的执行中。

如’程序加载(Program Loading)’中介绍,共享目标占有的虚拟内存地址可能与文件程序头表(program header table)中记录的地址不一样,动态链接器会重定位内存映像,在应用程序获得控制前更新绝对地址。

如果进程环境【见exec(BA_OS)】中包含一个名为LD_BIND_NOW的非null变量,动态链接器会在将控制交给程序之前处理所有的重定位。比如,以下所有的环境变量都会引起此行为:

LD_BIND_NOW = 1

LD_BIND_NOW = on

LD_BIND_NOW = off

动态链接器可以使用惰性策略来获取进程链接表条目(procedure linkage table entries),这样可避免解析和重定位那些没有被调用的函数。


4.3 动态Section(Dynamic Section)

如果一个目标文件参与了动态链接,那此文件的程序头表(program header table)中就会存在一个类型为PT_DYNAMIC的段。此段中包含.dynamic section。这个section被一个特殊的符号 _DYNAMIC 标记,包含下列结构的一个数组:


d_tag的值决定了d_un的意义:

dval: 类型为Elf32_Word的联合变量代表一个整数,不同值有不同意义

d_ptr: 类型为Elf32_Addr的联合变量代表程序虚拟地址。一个文件的虚拟地址可能与执行时的内存实际虚拟地址不同,当解析动态结构中的地址时,动态链接器会根据文件中的值和内存基地址来计算实际地址。为统一起见,文件的动态结构中不包含存有”正确“地址的重定位条目。

下表是d_tag的可能取值,如果tag标记为”mandatory"(强制),则ABI文件的动态链接链表必须有一个该类型的条目。如果标记为“optional”(可选),则这种类型的条目则不是必要的:


表续:


DT_NULL:tag为DT_NULL的条目标记_DYNAMIC列表结尾

DT_NEEDED:这个条目存储指向一个字符串表中一个字符串,表明一个需要的(needed)库的名字。动态列表(dynamic array)可能包含多个这种类型的条目,这种条目之间的相对位置很重要,与其他类型的条目的相对位置则无所谓。

DT_PLTRELSZ:此条存储与进程链接表(procedure linkage table)关联的所有重定位条目总的大小(字节)。如果存在DT_JMPREL类型的条目,则DT_PLTRELSZ条目也必须存在

DT_HASH:符号哈希表(symbol hash table)的地址,此哈希表与DT_SYMTAB条目指向的符号表相对应

DT_STRTAB:字符串表的地址,符号名、库名,以及其他字符串都存在这个表中

DT_SYMTAB:符号表地址

DT_RELA:重定位表的地址。一个目标文件可保护多个重定位sections,为一个可执行文件或共享目标文件创建重定位表时,连接器会把所有这些sections连成一个表。虽然文件中这些sections相互独立,但是动态链接器值看到一个表。

DT_RELASZ:DT_RELA重定位表的总大小(字节)

DT_RELAENT:DT_RELA重定位表中每个表项的大小(字节)

DT_STRSZ:字符串表的大小(字节)

DT_SYMENT:符号表中每个表项的大小(字节)

DT_INIT:初始化函数的地址

DT_FINI:终止函数的地址

DT_SONAME:字符串表中的字符串偏移,共享目标文件的名字。

DT_RPATH:字符串表中的字符串偏移,搜索库的路径

DT_SYMBOLIC:这个元素在共享目标库中可以改变动态链接器对自身的符号解析算法,动态链接器将首先在共享目标文件自身中搜索,而不是先在可执行文件中搜索。如果没找到,才会开始搜索可执行文件和其他共享目标文件。

DT_REL:与DT_RELA相似,不过它的表只有隐式的附加数(addends)。如果这种tag的条目存在,那动态结构中也必需有DT_RELSZ和DT_RELENT条目

DT_RELSZ:DT_REL重定位表的总大小(字节)

DT_RELENT:DT_REL重定位表中每一项的大小(字节)

DT_PLTREL:指明进程链接表(procedure linkage table)引用的重定位条目类型。d_val存储DT_REL或者DT_RELA,进程链接表中的重定位必需是同样的类型

DT_DEBUG:用于调试,它的内容不是ABI标准,使用这个条目的程序不符合ABI标准

DE_TEXTREL:如果动态列表中没有这种类型,则表明重定位条目不应该修改任何一个不可写段(段权限定义在程序头表【program header table】中)。如果存在这种tag类型的条目,则至少有一个重定位条目可能会更改某个不可写段,动态链接器据此要做一些准备。

DT_JMPREL:如果存在,这个条目的d_ptr成员存储那些只关联到进程链接表(procedure linkage table)的重定位条目的地址。如果使用了延迟绑定(lazy binding),分开这些重定位条目会使动态链接器在进程初始化时忽略他们。

DT_LOPROC to DT_HIPROC:预留给处理器相关语义

除了DT_NULL必须在列表尾部,以及DT_NEEDED条目之间的相对顺序,其他条目之间可以任意顺序存储。


4.4 共享目标依赖(Shared Object Dependencies)

当链接器处理归档库(经过压缩之类的库)时,它首先提取出库成员并将它们拷贝到输出目标文件。执行期间,这些静态链接服务不需要动态链接器就是可用的。共享目标文件也会提供服务,动态链接器必须将那些适当的共享目标文件加载到进程映像中执行。所以可执行文件和共享目标文件会描述他们的依赖关系。

当动态链接器创建一个目标文件的内存段时,这些依赖(存在动态结构的DT_NEEDED条目中)指出需要哪些共享目标文件来提供支持。通过重复连接这些被引用的共享目标文件和他们的依赖,动态链接器就可以创建一个完整的进程映像了。解析符号引用时,动态链接器使用宽度优先搜索检查符号表,它首先查看可执行文件本身的符号表,然后在DT_NEEDED条目指定的库中查找(按条目顺序),然后在DT_NEEDED条目库中的DT_NEEDED库中查找,一直进行下去。进程必须拥有共享目标文件的可读权限。

依赖列表中的名字要么拷贝自DT_SONAME字符串,要么拷贝自用来构建目标文件的共享目标文件的路径名。比如,如果链接器使用了一个共享目标文件(含有DT_SONAME条目lib1)和共享目标文件(路径名为/usr/lib/lib2),那么这个可执行文件的依赖列表就包括了lib1和/usr/lib/lib2。

如果一个共享目标文件名包含反斜杠(\),比如上面的/usr/lib/lib2,那动态链接器就会将它直接当成路径名(包含路径和文件名)使用。如果没有包含反斜杠,则通过以下3中方式搜索:

    第一,动态列表标志DT_RPATH可能提供一个包含路径列表的字符串,以冒号分隔。比如字符串,/home/dir/lib:/home/dir2/lib: 告诉动态链接器先搜索/home/dir/lib,再搜索/home/dir2/lib,然后是当前目录。

    第二,有一个名为LD_LIBRARY_PATH的环境变量可能存有像上面那样的目录列表,此列表以分号结尾(;),后面可以跟其他目录列表。例子如下:

        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


4.5 全局偏移表(Global Offset Table)

位置无关代码通常不能保护绝对虚拟地址。全局偏移表在私有数据中存有绝对地址,让这些地址即使在和程序的位置无关性和共享性不协调的情况下也可用。程序通过位置无关地址引用它的全局便宜表,并且从中提取绝对地址,以此将位置无关地址重定向到绝对位置。

开始时,全局偏移表存储了它的重定位条目需要的信息,当系统为可加载目标文件创建了内存段之后,动态链接器就会处理重定位条目,其中一些类型为R_386_GLOB_DAT的条目指向全局偏移表。动态链接器决定相关的符号值、计算它们的绝对地址、并且将适当的内存表条目设置为合适的值。虽然链接器构建目标文件时绝对地址是未知的,但是由于动态链接器知道所有内存段的地址,所以它可以计算出里面包含的符号的绝对地址。

如果一个程序需要使用一个符号的绝对地址,这个符号就会有一个全局偏移表条目。由于可执行文件和共享目标文件各自有自己的全局偏移表,同一个符号的地址可能会出现同时出现在几个表中。动态链接器会在将控制权交给进程映像之前处理所有全局偏移表的重定位,保证执行期间所有绝对地址都可用。

表的第零个条目(在位置0上的条目)是预留来存储动态结构(dynamic structure)的地址,动态结构被符号_DYNAMIC引用。这样程序(比如动态链接器)就可以在处理重定位条目前找到自己的动态结构了。这对于动态链接器来说尤其重要,因为它必须独立地初始化自己来重定位它的内存映像。在32-bit因特尔体系结构中,全局偏移表的第一和第二个条目也是预留的。

系统在不同的程序中可能为相同的共享目标选择不同的内存段地址,甚至可能在同一个程序的不同的执行中选择不同的库地址。然而,一旦进程映像创建完成,内存段地址就不会改变了。

全局偏移表的格式和意义根据处理器不同而不同,32bit因特尔体系结构,可能用符号_GLOBAL_OFFSET_TABLE_来定位此表,这个符号可能位于.got section的中间位置。


4.6 进程链接表(Procedure Linkage Table)

与全局偏移表用于将位置无关地址转化为绝对地址相似,进程链接表用来将位置无关的函数调用转化为绝对位置。链接器不能解析转移自其他可执行文件或共享目标文件的函数调用,因此链接器就将这些程序转移控制安排到进程链接表的条目中。在SYSTEM V体系结构中,进程链接表位于共享文本中,但是它使用私有全局偏移表中的地址。动态链接器确定目标的绝对地址并且更改相应全局偏移表的内存映像。动态链接器因此可以直接重定向这些条目,而不用管程序文本的位置无关性和共享性。可执行文件和共享目标文件都有自己的进程链接表。

下图为绝对位置进程链接表


下图为位置无关进程链接表:



动态链接器根据以下步骤与程序一起‘合作’通过进程链接表和全局偏移表来解析符号引用

A. 当开始创建程序内存映像时,动态链接器将全局偏移表的第二和第三项设置为特殊值,下面详细讲解

B. 如果进程链接表是位置无关的,全局偏移表必须位于%ebx寄存器中。内存映像中的每个共享目标文件都有自己的进程链接表,控制之后从目标文件中转移至同一目标文件中的进程链接表条目。因此,调用函数(调用其他函数的函数)要在调用进程链接表条目之前负责设置全局偏移表的基址寄存器。

C. 比如程序调用name1,将控制转移至标签.PLT1处

D. 第一个指令跳转至name1对应的全局偏移表条目的地址,开始时全偏移表存储的是push1指令的地址,而非name1的真正地址。

E. 然后程序将重定位偏移压入栈中,重定位偏移是重定位表中一个32bit、非负的字节偏移。这个重定位条目类型为R_386_JMP_SLOT,它的偏移指明了前一个jmp指令中使用的全局偏移表条目。这个重定位条目也包含了一个符号表索引,告诉动态链接器被应用的符号是什么(这个例子中是name1)。

F. 把重定位偏移压入栈之后,程序就跳转到.PLT0位置,这是进程链接表的第一个条目。push1指令将全局偏移表的第二个条目(got_plus_4或者4(%ebx))压入栈中,将一个字的定位信息提供给动态链接器。然后程序跳转到全局偏移表的第三个地址(got_plus_8或许8(%ebx)),将控制交给动态链接器。

G. 当动态链接器获得控制后,就展开栈(unwind the stack),在指定的重定位条目中查找符号值,将name1的‘真实’地址存入它的全局偏移表条目中,并把控制转交给指定的目的地址。

H. 接下来进程链接表条目的执行会直接转移到name1,动态链接器不会再被调用。也就是说,.PLT1位置上的jmp指令转移到name1,而不是'陷入'(fall through)压栈指令中。


LD_BIND_NOW环境变量可以改变动态链接器的行为。如果它的值非null,动态链接器就会在将控制转交给程序前解析进程链接表条目。就是说,动态链接器在进程初始化过程中处理类型为R_386_JMP_SLOT的重定位条目。否则,动态链接器就使用延迟策略来解析进程连接表条目,在表的任何一个条目执行之前不进行符号解析和重定位。


4.6 哈希表(Hash Table)

Elf32_Word对象的哈希表支持符号表,以下标签显示了哈希表的结构,但是这不是规范(spec)的一部分


bucket数组包含nbucket个条目,chain数组包含nchain个条目,从0开始索引。bucket和chain都存有符号表索引,chain表项与符号表一一对应,符号表条目数应等于nchain,所以符号表的索引页可以用来选择chain表的条目。哈希函数接受一个符号名并且返回一个值用于计算bucket里的索引。如果哈希函数(算法)为某个名字返回值x,那么bucket[x%nbucket]也是一个索引y,此索引可用于chain和符号表中。如果得到的符号表条目(symbol[x]不是想要的那个,chain[y]就是具有相同哈希值的下一个符号表条目。我们可以一直在chain表中找下去,直到找到想要的条目或者chain表项的值为STN_UNDEF。


4.7 初始化和终结函数(Initialization and Termination Functions)

当动态链接器创建了进程映像并且执行了重定位后,每个共享目标文件都会执行一些初始化代码。这些初始化函数的调用没有特定顺序,但是所有共享目标文件的初始化都要在可执行文件获得控制前完成。

同样,共享目标文件也可能含有终结函数,它会在基进程开始终结过程后,与atexit(BA_OS)机制一起执行。终结函数的顺序也不定。

共享目标文件通过动态结构(dynamic structure)中的DT_INIT和DT_FINI条目来指定初始化和终结函数。通常这些函数的代码位于.init和.fini sections中。


0 0