第二部分 程序加载和动态链接
一、简介
第二部分介绍了目标文件信息和创建运行程序的系统操作,这里的有些信息适用于所有系统,有些则是处理器相关的。
可执行文件和共享目标文件都是静态表示程序。为了执行这样的程序,系统用这些文件来创建动态程序表示或是进程映像。一个进程映像有一些segment保存着代码、数据、堆栈等。
二、程序头
可执行文件和共享目标文件的程序头表是一个结构数组,每个结构描述了一个segment或系统准备执行程序时所须要的信息。目标文件的segment包含一个或多个section。程序头只对可执行文件和共享目标文件有意义。
Program Header
t y p e d e f s t r u c t {
E l f 3 2 _ W o r d p _ t y p e ;
E l f 3 2 _ O f f p _ o f f s e t ;
E l f 3 2 _ A d d r p _ v a d d r ;
E l f 3 2 _ A d d r p _ p a d d r ;
E l f 3 2 _ W o r d p _ f i l e s z ;
E l f 3 2 _ W o r d p _ m e m s z ;
E l f 3 2 _ W o r d p _ f l a g s ;
E l f 3 2 _ W o r d p _ a l i g n ;
} E l f 3 2 _ P h d r ;
p_type 这个成员说明了这个数组元素描述了一个什么种类的segment或者怎样解释这个数组元素的信息。
p_offset 从文件开头到segment第一个字节的偏移量
p_vaddr segment第一个字节在内存的虚拟地址
p_paddr segment的物理地址,这个成员在可执行文件和共享目标文件中还没指定内容
p_filesz segment在文件映像中的字节数
p_memsz segment在内存映像中的字节数
p_flags 与segment相关的标志
p_align segment在内存和文件中的对齐值
基地址
可执行文件和共享目标文件有一个基地址,这个地址是关于程序目标文件的内存映像的最低虚拟地址。基地址的一个作用是在动态链接时重定位内存映像。
一个可执行或目标文件的基地址是在执行期间由三个值计算出来的:内存加载地址、最大页尺寸和程序可加载segment的最低虚拟地址。
.bss 这个section是SHT_NOBITS型的,尽管他不占据文件在空间,但对segment的内存映像还是有作用的。通常未初始化的数据驻留在segment的结尾处,因此使得在相关程序头元素中p_memsz要比p_filesz大
注释section
有时vendor或系统构建者须要用指定的信息注释目标文件,这些信息供其他程序进行一致性和兼容性等检查。SHT_NOTE类型的section和PT_NOTE类型的程序头元素就是用于这个目的。
三、程序加载
随着系统创建或增加一个进程映像,理论上拷贝文件的segment到虚拟内存segment。一个进程只有在执行时引用逻辑页才会请求一个物理页,并且进程通常会留有很多未引用的页。因此频繁地延迟物理读可以避免这些未引用的页,提高性能。
有四个文件页包含不纯的文本或数据
1、 第一个文本页包含ELF头,程序头表和其他信息。
2、 最后一个文本页包含数据开始的一份拷贝
3、 第一个数据页有一份文本结尾的拷贝
4、 最后一个数据页可能包含运行进程时不相关的文件信息
理论上,系统强制每个segment的内存许可是完整独立的,segment的地址被调整,以确保地址空间中每个逻辑页有一组单独的许可。
可执行文件和共享目标文件在segment加载这方面是有所不同的。
可执行文件segment包含绝对代码。为使进程正确执行,segment必须驻留在用于构建可执行文件的虚拟地址。因此,系统使用不变的p_vaddr值作为虚拟地址。
另一方面,共享目标segment包含位置无关的代码。这使得虚拟地址从一个进程到另一个进程发生了改变却不会使执行行为无效。尽管系统为每个进程选择虚拟地址,它仍然维护这些segment的相对位置。因为位置无关代码在segment中使用相对编址。内存中虚拟地址间的不同必须符合文件中虚拟地址的不同。
四、动态链接
程序解释器
一个可执行文件将会有一个PT_INTERP程序头元素。在exec(BA_OS)期间,系统从PT_INTERP segment中检索一个路径名,并从解释器文件的segment中创建初始化进程映像。也就是说系统为解释器组合了一个内存映像,代替使用原来的可执行文件的segment映像。然后是解释器负责从系统接收控制并为应用程序提供环境。
解释器可以是共享目标文件或可执行文件
共享目标文件(通常情况都是这个)以位置无关方式加载,从一个进程到另一个进程将使它的地址改变。系统在动态segment区创建了它的segment。这些动态segment由mmap(KE_OS)和相关服务使用。因此,一个典型的共享目标解释器不和原先可执行文件的先前的segment地址冲突。
可执行文件被加载到固定地址;系统使用来自程序头表的虚拟地址创建它的segment。因些,一个可执行文件解释器的虚拟地址将会和第一个可执行文件的冲突;解释器负责解决这个冲突。
动态链接器
当用动态链接构建一个可执行文件时,链接编辑器在可执行文件中添加一个PT_INTERP类型的程序头元素,告诉系统调用动态链接器作为程序解释器。
exec(BA_OS)和动态链接器合作为程序创建了进程映像,这个必须有如下步骤:
1、 把可执行文件的内存segment添加到进程映像中
2、 把共享目标内存segment添加到进程映像
3、 为可执行文件和他的共享目标实现重定位
4、 如果有文件描述符给了动态链接器,则关闭用于读可执行文件的文件描述符
5、 把控制权传给程序,使得看起来好像程序已经直接从exec(BA_OS)接收了控制权
动态section
如果一个目标文件参与了动态链接,它的程序头表将有一个PT_DYNAMIC类型的元素。这个segment包含.dynamic section。一个特殊的符号,_DYNAMIC,给这个section加上标签,这个section包含了下面这个结构的数组。
Dynamic Structure
t y p e d e f s t r u c t {
E l f 3 2 _ S w o r d d _ t a g ;
u n i o n {
E l f 3 2 _ W o r d d _ v a l ;
E l f 3 2 _ A d d r d _ p t r ;
} d _ u n ;
} E l f 3 2 _ D y n ;
e x t e r n E l f 3 2 _ D y n _ D Y N A M I C [ ] ;
对于每个有这种类型的目标文件,d_tag控制着对d_un的解释
d_val 这些Elf32_Word目标代表着有不同解释的整数值
d_ptr 这些Elf32_Addr目标代表程序的虚拟地址。在执行期间,文件的虚拟地址可以和内存的虚拟地址不搭配。当解释包含在动态结构中的地址时,动态链接器基于前面文件的值和内存基地址计算实际地址。为了一致,文件在动态结构中不包含到“正确”地址的重定位入口。
共享目标依赖
当链接编辑器处理一个归档库时,它提取库成员并复制到输出目标文件。在执行时,这些静态链接服务是可得到的,而不用调用动态链接器。共享目标也提供服务,而且为了执行,动态链接器必须把正确的共享目标文件链接到进程映像。因些,可执行和共享目标文件描述了它们特有的依赖。
当动态链接器为目标文件创建内存segment时,依赖指出需要什么样的共享目标来支持程序的服务。通过重复连接引用的共享目标和它们的依赖,动态链接器构建了一个完整的进程映像。
在依赖列表中的名称是DT_SONAME字符串或共享目标路径名的拷贝,这些共享目标用于构建目标文件。
如果共享目标名在名称的任何位置有一个或多个斜线(/)字符,如/usr/lib/lib2或directory/file,动态链接器直接用这个字符串作为路径名。如果没有斜线,如lib1,依据下面的优先权,有三种可能来指定共享目标的路径搜索:
1、 动态数组标签DT_RPATH会给出一个包含一个系列目录的字符串,这些字符串有冒号(:)分开。如:/home/dir/lib:/home/dir2/lib:告诉动态链接器首先搜索第一个目录/home/dir/lib、然后是/home/dir2/lib、最后是当前目录来寻找依赖。
2、在进程环境中有一个变量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:;
1、 如果那两组目标没有成功定位需要的库,动态链接器将搜索/usr/lib
全局偏移量表(got)
通常,位置无关代码不能包含绝对虚拟地址。全局偏移量表在私有数据中保存着绝对地址,因此,使得这些地址可用而不会危及位置无关和程序文本的共享性。程序引用使用位置无关编址的全局偏移量表并提取绝对地址,因此把位置无关的引用改为绝对位置。
最初,全局偏移量表保存着重定位入口请求的信息。在系统为可加载目标文件创建内存segment后,动态链接器处理重定位入口,一些R_386_GLOB_DAT类型的入口涉及到全局偏移量表。动态链接器确定相关符号值,计算它们的绝对地址,并为适当的内存表入口设置正确的值。尽管在链接编辑器构建一个目标文件时绝对地址是未知的,动态链接器却知道所有内存segment的地址并因此能计算出那里符号的绝对地址。
如果程序请求直接访问符号的绝对地址,那个符号将有一个全局偏移量表入口。因为可执行和共享目标文件有各自的全局偏移量表,一个符号的地址可能出现在几个表中。动态链接器在将控制权交给进程映像中任何代码之前处理所有全局偏移量表的重定位,因此确保了在执行时绝对地址是可用的。
全局偏移量表的格式和解释是处理器相关的。
过程联接表(glt)
就像全局偏移量表把位置无关地址计算改变为绝对地址一样,过程联接表把位置无关的函数调用改为绝对位置。链接编辑器不能解析从一个可执行或共享目标文件到另一个的执行传输(如函数调用)。因此,链接编辑器安排让程序把控制权传输到过程联接表中的入口。
Position-Independent Procedure Linkage Table
.PLT0:pushl 4(%ebx)
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1:jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2:jmp *name2@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
...
按照下面的步骤,动态链接器和程序合作通过plt和got解析符号引用。
1、 第一次创建程序的内存映像时,动态链接器将got表中第二、第三次入口设为特殊值。
2、 如果plt表是位置无关的,got的地址必须在寄存器%ebx中。进程映像中的每个共享文件有它自己的plt,并只能从同一个目标文件向一个plt入口进行控制传输。因此,调用函数负责在调用plt入口前设置got基址寄存器(%ebx).
3、 为了说明,假定程序调用name1,把控制权传输到标签.PLT1。
4、 第一条指令跳转到got表中name1入口的地址。初始时,got表包含下面的pushl指令的地址,而不是name1的真正地址
5、 因此,程序将重定位偏移量(offset)入栈。重定位偏移量是重定位表中一个32位、非负字节偏移量。那个指定的重定位入口将会是R_386_JMP_SLOT类型,而它的偏移量将指定用于先前jmp指令的got入口。重定位入口还包含一个符号表索引,告诉动态链接器什么符号被引用,我们这种情况是name1.
6、 在重定位偏移量入栈后,程序跳转到.PLT0,也就是plt的第一个入口。pushl指令把第二个got表入口入栈。因此给了动态链接器一个字的辨别信息。然后程序跳转到第三个got入口地址,将控制权传输到动态链接器。
7、 当动态链接器接收到控制权,它展开堆栈,查看指定的重定们入口,找出符号值,在got入口存储name1的真实地址,然后把控制权传给想要的目的地址。
8、 后面对plt入口的执行将直接传送name1,而不用再次调用动态链接器,也就是说.PLT1处的jum指令传到name1,而不是到pushl指令。
LD_BIND_NOW这个环境变量可以改变动态链接器的行为,如果它的值是非零的,动态链接器在把控制权传给程序前求plt入口的值。也就是说,动态链接器在进程初始化时处理R_386_JMP_SLOP类型的重定位入口。否则,动态链接器不会求plt入口的值,直到表的入口第一次执行才进行符号解析和重定位。
哈希表
Elf32_Word对象的哈希表支持符号表访问。下面的标签帮助解释哈希表的组织,但不是规范的一部分
Symbol hash table
nbucket |
nchain |
bucket[0] |
. . . |
bucket[nbucket-1] |
chain[0] |
. . . |
chain[nchain-1] |
bucket数组包含nbucket入口,而chain数组包含nchain入口;索引从0开始。bucket和chain都包含符号表索引。chain表入口与符号表对应。符号表的入口数应等于nchain;所以符号表索引也选择了chain表入口。哈希函数接收一个符号名并返回一个值,这个值可能被用于计算bucekt索引。因此,如果哈希函数为某个符号名返回值x,bucket[x%nbucket]给出一个索引y,用于符号表和chain表。如果符号表入口不是想要的,chain[y]给出相具有同哈希值的下一个符号表入口。可以沿着chain链直到选到了包含想要的名称的符号表入口或者包含STN_UNDEF值的chain入口。
Hashing Function
u n s i g n e d l o n g
e l f _ h a s h ( c o n s t u n s i g n e d c h a r * n a m e )
{
u n s i g n e d l o n g h = 0 , g ;
w h i l e ( * n a m e )
{
h = ( h < < 4 ) + * n a m e + + ;
i f ( g = h & 0 x f 0 0 0 0 0 0 0 )
h ^ = g > > 2 4 ;
h & = ~ g ;
}
r e t u r n h ;
}
初始化和终止函数
在动态链接器构建了进程映像并执行重定位后,每个共享目标获得了执行一些初始化代码的机会。这些初始化函数的调用没有特定的顺序,但所有的共享目标初始化都发生在可执行文件获得控制权之前。
类似的,共享目标可能还有一个终止函数,它和atexit(BA_OS)机制在基本进程开始了它的终止序列后被执行。再则,动态链接器调用终止函数的顺序也未定。