本章节描述SVR4的section规范。
SVR4规定了下述类型的Section:
SHT_SYMTAB,SHT_DYNSYM。存储符号表,当前一个目标文件每种类型只能有一个section,但是这个限制在将来可能放宽。典型地,SHT_SYMTAB提供静态链接的符号,但是也可用于动态链接,作为一个完全的符号表,可能包含许多对动态链接不必须要的符号。同样的,目标文件也可以包含SHT_DYNSYM section,仅动态链接符号。
SHT_STRTAB。一个目标文件,可能有多个字符串表。
SHT_HASH。所有参与动态链接的目标文件必须有符号哈希表。当前,一个目标文件只有一个哈希表,后续这个限制可能放宽。
SHT_DYNAMIC。一个目标文件只有一个dynamic section,后续这个限制可能放宽。
增加一种类型的解释,如下:
sh_type | sh_link | sh_info |
SHT_SYMTAB SHT_DYNSYM | 相关的字符串表的section索引 | 最后一个本地符号的所在section的索引+1 |
动态链接信息由这些section提供:.dynsym .dynstr .interp .hash .dynamic .rel .rela .got .plt。有些section的内容和处理器相关,但是他们支持相同的链接模型。.init和.fini用于进程初始化和结束代码。
Name | Type | Attributes |
.dynstr | SHT_STRTAB | SHF_ALLOC |
.dynsym | SHT_DYNSYM | SHF_ALLOC |
.fini | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR |
.init | SHT_PROGBITS | SHF_ALLOC + SHF_EXECINSTR |
.interp | SHT_PROGBITS | see below |
.relname | SHT_REL | see below |
relaname | SHT_RELA | see below |
.dynstr,字符串表,通常是.dynsym相关符号的字符串表。
.dynsym, 动态链接用的符号表。
.fini,包含可执行指令,在程序正常结束时执行。
.init,程序初始化的可执行指令,在调用主程序入口点(c的main函数)之前执行。
.interp,包含一个程序解释器的路径,通常这是一个动态链接器。如果有任何可加载段包含这个section,则属性包括SHF_ALLOC。
.relname/.relaname,包含可重定向信息,如果任何可加载段包含这个section,则属性包括SHF_ALLOC。约定,name是重定向目标section的名字,因此.text对应的重定向section名字为.rel.text或.rela.text。
如果st_name为非0,则他表示字符串表的索引,否则该符号没有名字。
外部c符号,在c和目标文件符号表中有相同的名字。
共享库中的函数符号有特殊的含义,链接器将自动创建一个PLT (procedure linkage table) 条目。
全局和弱符号有两点不同。
当链接器结合几个可重定向目标文件时,不允许多个同名的STB_GLOBAL符号。当有全局符号时,同名的弱符号不引起错误,链接器选择全局符号的定义。类似的,一个common符号存在,同名弱定义的符号存在也不导致错误,链接器选择common定义。
当链接库搜索archive库时,提取定义的和未定义的全局符号。但是链接器不提取文档成员来解析未定义的弱符号,未定义的弱符号有0值。
c标准没有和弱符号相关的规范。但是gcc提供了扩展。
1 2 | #pragma weak power2 int power2( int x); |
或
1 | int __attribute__((weak)) power2( int x); |
程序头中的虚拟地址可能并不能程序其在内存中的实际虚拟地址。可执行文件通常包含绝对代码(absolute code),为了让进程正确的执行,段必须使用编译时的虚拟地址驻留在内存。另一方面,共享目标(shared object)的段通常包含位置无关代码。这让段在不同进程中虚拟地址可以不同。尽管系统为每个进程的共享库段选择虚拟地址,但它维护段的相对位置(relative position)。因此位置无关代码在段间使用相对寻址(relative addressing)。内存中两个段之间虚拟地址的差(diffrence)必须匹配文件中虚拟地址的差。这个差就是base address。
段类型,如之前描述。p_paddr成员的值,是未定义的。p_align,可加载段p_vaddr和p_offset应该是协调的,都对齐到page_size。
系统可能赋予更多的权限,但是写权限是严格按照指定的赋予。
通常,包含两个可加载段:Text段和Data段。那些只读的section,通常放在Text段,如.text、.rodata。可写的section放在Data段,如.data。.bss通常放在Data段的最后。下面是一个例子:
Text Segment
.text |
.rodata |
.hash |
.dynsym |
.dynstr |
.plt |
.rel.got |
Data Segment
.data |
.dynamic |
.got |
.bss |
一个PT_DYNAMIC程序头指向.dynamic section。
需要动态链接的可执行文件应当有一个PT_INTERP程序头,其指向.interp section。如/bin/ls的elf信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1 |
在exec期间,系统从PT_INTERP段获取解释器的路径,从解释器文件的段创建一个初始的进程镜像。解释器负责接收控制,然后为应用程序准备环境。解释器有两种方式接收控制。
一:接收一个读取可执行文件的文件描述符,位置在文件开头。解释器可以用这个文件描述符来读取和(或)者映射可执行文件的段到内存。
二:根据可执行文件的格式,系统可以加载可执行文件到内存。
解释器本身可能不要求第二个解释器,一个解释器可以是共享目标文件,也可以是可执行文件。
一:shared object。由于共享目标通常是位置独立的,可以加载到可变的地址,因此共享目标解释器不会与原始可执行文件的段地址冲突。
二:executable file。可执行文件加载到固定的地址,系统使用程序头表中的虚拟地址。因此可执行文件解释器的虚拟地址可能和第一个可执行文件的地址冲突。解释器负责解决冲突。
当创建使用动态链接的可执行文件时,链接编辑器(link editor)添加一个PT_INERP类型的程序头到可执行文件,告诉系统调用调用动态链接器作为程序解释器。
exec和动态链接器协同合作、创建进程镜像。
添加可执行文件内存段到进程镜像
添加共享目标内存段到进程镜像
执行可执行文件和它的共享目标的重定向
关闭读取可执行文件的文件描述符。
转移控制给程序。
链接编辑器还创建各种数据来协助动态链接器。这些数据驻留在可加载段,使它们在运行时可用。
.dynamic,类型为SHT_DYNAMIC,包含各种数据。
.hash,类型为SHT_HASH,包含符号哈希表
.got和.plt,类型为SHT_PROGBITS,包含两个独立的表。global offset table和procedure linkage table。
动态链接器可以立即评估PLT表,也可以延后处理,依赖于LD_BIND_NOW的值。
如果一个目标文件参与动态链接,他的程序头表将包含PT_DYNAMIC类型,这个段包含一个.dynamic section。符号表中一个特殊的符号_DYNAMIC标注这个section,包含一个下列结构体的数组。
1 2 3 4 5 6 7 8 9 | typedef struct dynamic{ Elf32_Sword d_tag; union { Elf32_Sword d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC []; |
d_tag,类型,d_un的类型依赖于d_tag。
d_val,32位整数,含义根据类型有不同的解释。
d_ptr,表示程序虚拟地址。文件虚拟地址可能和内存虚拟地址不一致。动态链接器根据原文件的值和基址来计算真正的地址。基于一致性考虑,文件不包含重定向信息来纠正本地址。
d_tag的值如下表:
Name | Value | d_un | Executable | Shared Object |
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 | ignored |
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_LOPROC | 0x70000000 | unspecified | unspecified | unspecified |
DT_HIPROC | 0x7fffffff | unspecified | unspecified | unspecified |
DT_NULL,表示_DYNAMIC数组的结束。
DT_NEEDED,给出需要的库的名字,由字符串表的索引指定。字符串表由DT_STRTAB指定。可以有多个DT_NEEDED,而且顺序是有意义的。
DT_PLTRELSZ,所有plt表中重定向条目的大小。
DT_PLTGOT,地址。
DT_HASH,符号哈希表的地址。
DT_STRTAB,字符串表的地址。
DT_SYMTAB,符号表的地址。
DT_RELA,重定向表的地址。
DT_RELASZ,重定向表的全部字节大小。
DT_RELAENT,重定向表一个条目的大小,
DT_STRSZ,字符串表的大小。
DT_SYMENT,一个符号表条目的大小。
DT_INIT,初始化函数的地址。
DT_FINI,结束函数的地址。
DT_SONAME,共享目标的名字,字符串表的索引。(共享库自己的名字)
DT_RPATH,库搜索路径的名字,字符串表的索引。
DT_SYMBOLIC,共享库符号的搜索算法,从可执行文件开始还是共享库开始。
DT_REL/DT_RELSZ/DT_RELENT,和RELA相似。
DT_PLTREL,指定PLT的类型,是REL还是RELA。
DT_DEBUG,调试信息。
DT_TEXTREL,如果没有这个tag,则表示没有重定向条目修改不可写段,否则相反。根据这个tag,动态链接器可以做相应的准备。
DT_JMPREL,待研究。
DT_BIND_NOW,符号绑定时机。
当动态链接器创建目标文件的内存段时,DT_NEEDED指定的依赖告诉需要哪些共享库,通过不断加载共享库及他们的依赖,动态链接器创建完整的处理器镜像。
当解析符号引用时,它可执行宽度优先的搜索。创建可执行文件时DT_NEEDED的值要么是共享库的DT_SONAME,或者是共享库的路径。
如果共享目标名字由一个或多个slash(/)。动态链接器直接使用其作为路径名。如果没有反斜杠,按照下列方式搜索。
DT_RPATH,可能给出搜索路径列表,用colon(:)分开。
进程环境LD_LIBRARY_PATH指定的路径。可用colon或者semicolon(;)分开。
最后,/usr/lib
Global Offset Table包含私有数据的绝对地址,这样可以在不向位置独立代码和代码段可共享性妥协的情况下使地址可访问。
procedure linkage table重定向位置独立函数调用为绝对地址。
符号哈希表用来快速定位符号,是一个Elf32_word的表。
nbucket |
nchain |
bucket[0] ... bucket[nbucket-1] |
chain[0] ... chain[nchain-1] |
chain表并行与符号表,nchain应该和符号的数量相同。首先根据哈希函数,算出符号的哈希值x。然后bucket[x%nbucket]得到一个符号索引y。如果符号不匹配。则chain[y]给出了一下一个符号表条目,拥有相同的哈希值。一直继续下去直到找到符号或者STN_UNDEF。
哈希函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 | static unsigned long elf_hash( const unsigned char *name) { unsigned long h = 0, g; while (*name) { h = (h << 4) + *name++; if (g = h & 0xf0000000) h ^= g >> 24; h &= ~g; } return h; } |
动态链接器创建进程镜像、执行重定向后,每个共享目标都有一个机会执行一些初始代码。所有的共享目标初始化发生在可执行文件获得控制前。
目标文件A的初始化代码被调用前,所有的它的依赖的初始化代码已经被调用。
类似的,共享目标也有结束结束函数,在base process开始结束后,通过atexit机制执行。动态链接器调用结束函数的顺序和初始化完全相反。动态链接器保证初始和结束函数只执行一次。
共享目标通过dynamic section的DT_INIT和DT_FINI指定初始和结束函数,典型地,这些函数在.init和.fini section。
动态链接器不为调用可执行文件的.init负责,也不为通过atexit注册.fini负责。通过atexit定义的结束函数必须在任何共享目标的结束函数前执行。