赞
踩
名字的由来:ELF 即 Executable and Linking Format,可执行 可链接格式。
主要有三种类型:
汇编器 和 链接器 生成,一种字节流形式的文件
类似 TLV(type length value) 这种组织形式,猜测肯定有类似表示类型长度值的各种字段。事实上也类似,我们需要建立起下面几个概念:ELF 文件头、程序/节头表、段/节
1、ELF 文件头想当然的是包含文件的结构信息,具体指什么后面讲
2、节 专用于连接过程,包含 指令、符号、重定位数据等
3、程序头表 包含创建进程镜像的一些信息
4、节头表:每个节都必须在节头表中有一个注册项(描述名字大小),每个节
都要在节头表
中对应注册项,以描述名字大小等
5、段 描述缺失
文本字符应当数据 1.2.1 描述的其他数据。
TIS 委员会没有限制所使用的字符集,但必须
自识别
:每个字符识别不依赖于其他条件前面提到包含了结构信息,具体指啥?详细的在文档中有一个结构体,这里摘录几个字段:
节
怎么定义,它是个啥??前面知道它在节头表中被登记, 且目标文件有很多节,节头表每个表项是个 Elf32_Shdr 的结构,就是说节头表其实是一个结构体数组。
位置、数量、表项大小 分别有文件头中的 e_shoff e_shnum e_shentsize
表示
什么是节索引
: 文字为给出具体定义,只说明了节头表中保留的索引值都位于 SHN_LORESERVE(0xff00) ~ SHN_HIRESERVE(0xffff) 之间
节是文件中最大的部分,需要满足下面的条件:
typedef struct {
Elf32_Word sh_name; # 只是索引,指向`字符串表` 节中的位置
Elf32_Word sh_type; # 类型: 符号表、字符串表、重定位节、哈希表
Elf32_Word sh_flags;# 属性:可写/占内存/指令代码/保留
Elf32_Addr sh_addr; # 映射到进程空间起始地址
Elf32_Off sh_offset;# 本节所在位置
Elf32_Word sh_size; # 大小,单位字节
Elf32_Word sh_link; # 节头表中本节对应位置:这是双向链表?
Elf32_Word sh_info; # 附加信息
Elf32_Word sh_addralign; # 对齐参数
Elf32_Word sh_entsize; # 节是一张表:表示表项大小
} Elf32_Shdr;
每个操作系统有自己的连接模型,总的来说还是两类:
.debug .line
包含程序控制信息.bss .data .data1 .rodata .rodata1
包含程序控制信息.dynsym、.dynstr、.interp、.hash、.dynamic、.rel、.rela、.got、.plt
.init .fini
用于进程初始化和终止过程也就是 前文提到的 .strtab
节, (Elf32_Shdr)obj.sh_type=SHT_STRTAB,
(Elf32_Shdr)obj.sh_entsize=???
包含若干以 ‘null’ 结尾的字符序列,需要引用时提供序号即可(如果忘记用法需要再读一下文档相关章节)
也就是 前文提到的 .symtab
节, (Elf32_Shdr)obj.sh_type=SHT_SYMTAB/SHT_DYNSYM
(Elf32_Shdr)obj.sh_entsize=sizeof(Elf32_Sym) ???
它包含的信息用于 定位和重定位程序中的符号定义和引用
, 目标文件的其他部分通过索引来使用.
typedef struct {
Elf32_Word st_name; // 指向字符串表的索引值
Elf32_Addr st_value; // 可能是值/地址/字节对齐数
Elf32_Word st_size; //
unsigned char st_info;// 标识了符号绑定、符号类型、符号信息 三种属性
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
如果一个可执行文件有一个函数引用,且函数在 .so 中,那针对这个符号,符号表中应该含有这个函数符号,且表中的 (Elf32_Sym)obj.st_shndx 值为 SHN_UNDEF, 这表示函数符号并不在可执行文件中。如果这个函数表项的 st_value!=0,那 st_value 就是第一天指令的地址,否则地址被动态连接器用来解析函数地址 – 不是很明白.
#defineELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf)))
符号表项 与 节 的关系:前者一定与一个后者相联系。前者指明相关联的节,重定位时根据节位置的改变而改变。对下面三种特殊的节有特别的意义:
另外提一下,符号表的首项与其他不同.
重定位(relocation)就是符号引用与符号定义连接在一起的过程。
重定位文件必须知道如何修改其所包含的节,构建ELF时,把节中的符号换成在进程空间中的虚拟地址。包含这些转换信息的就是 – 重定位项(relocation entries)
readelf -r a.out
typedef struct {
Elf32_Addr r_offset;//给出重定位所作用的位置:偏移量或虚拟地址
Elf32_Word r_info;
// 这里没有r_addend, 隐含在被修改的位置里
} Elf32_Rel;
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend; // 用于计算重定位域值的加数
} Elf32_Rela;
在前面的1.4节,讲到如果一个节的类型是 SHT_REL/SHT_RELA, 那么一个节就是重定位节,且带有明确的加数(Elf32_Rel/Elf32_Rela)
一个重定位节需要引用另外两个节:
不同目标文件里 r_offset 成员的含义不同:
重定位节描述如何如何修改文件中的另一个节的内容
,指向了另一个节中的存储单元地址引入一个概念:被重定位域,32位
重定位文件转换为可执行或so文件的过程:
有下面几种运算
共享目标被装入内存时的基地址
。一般来说,共享目标文件在构建时基地址为0,但在运行时则不是可重定位项在全局偏移量表中的位置
,这里存储了此重定位项在运行期间的地址。更多信息参见下文"全局偏移量表"表示全局偏移量表的地址
函数连接表项的所在之处
,可能是节内偏移量,或者是内存地址。存储单元在节内的偏移量或者内存地址
,由 r_offset
计算得到索引值所代表的符号的值
一个重定位项的 r_offset
值指定了被重定位的数据在节内的偏移量
或者在进程空间内的虚拟地址
。
重定位类型指定了哪些位需要被修改以及如何算计它们的值。如 R_386_GOT32
需要计算 G+A
重定位有下面几种类型:
offset
成员给出了共享目标内的一个位置
,这个位置含有一个代表相对地址的值
。把共享目标被加载的地址
加上这个相对地址
,动态连接器就可以计算得到真正需要的虚拟地址
。这种类型的重定位项必须为符号表指定0值。描述将 ELF 与 动态链接库装载到进程空间过程的系统行为:
可执行或.so 文件的程序头表
是一个数组,每个元素称为程序头
。每个程序头描述一个段
或一块用于执行程序的信息。段包含多个节
.
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
本章描述用于创建程序的目标文件信息
和系统行为
. 可执行文件和共享目标文件(动态连接库)是程序的静态存储形式。要执行一个程序,系统要先把相应的 可执行文件和动态连接库
装载到进程空间中,这样形成一个可运行的 进程的内存空间布局
,也可以称它为 "进程镜像"
。一个已装载完成的进程空间会包含多个不同的"段(segment)",比如代码段(text segment),数据段(data segment),堆栈段(stack segment)等等。
一个可执行文件或共享目标文件的 程序头表(program header table)
是一个 数组
,数组中的每一个元素称为 "程序头(program header)"
,每一个 程序头
描述了一个"段(segment)"
或者一块用于准备执行程序的信息
。一个目标文件中的 "段(segment)"
包含一个或者多个 "节(section)"
。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。在目标文件的文件头(elf header)中,e_phentsize
和 e_phnum
成员指定了程序头的大小。
程序头结构:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
此数据成员说明了本程序头所描述的段的类型,或者如何解析本程序头的信息。
此数据成员给出本段内容在 文件中的位置
,即段内容的开始位置相对于文件开头的偏移量
此数据成员给出本段内容的开始位置在 进程空间中的虚拟地址
此数据成员给出本段内容的开始位置在 进程空间中的物理地址
此数据成员给出本段内容在 文件中的大小
,单位是字节,可以是0
此数据成员给出本段内容在 内容镜像中的大小
,单位是字节,可以是0。
此数据成员给出了本段内容的属性。具体有哪些标志位请参见下文
对于可装载的段来说,其p_vaddr和p_offset的值至少要向内存页面大小对齐
可执行文件中需要含有绝对的地址, 比如变量地址,函数地址等
,为了让程序正确地执行,“段” 中出现的虚拟地址必须在创建可执行程序时被重新计算. 另一方面,出于ELF通用性的要求,目标文件的段中又不能出现绝对地址,其代码是不应依赖于具体存储位置的,即同一个段在被加载到两个不同的进程中时,它的地址可能不同
,但它的行为不能表现出不一样。
在被加载到进程空间里时,尽管段会被分配到一个不确定的地址,但相对位置是确定的。
一个可执行文件或共享目标文件的 基地址
是在运行期间由以下三个值计算出来的:内存加载地址
,最大页面大小
,程序可装载段的最低地址
zl。为计算基地址,首先找出类型为PT_LOAD(即可加载)而且p_vaddr(段地址)最低的那个段,把这个段在内存中的地址与最大页面大小相除,得到一个段地址的余数;再把p_vaddr与最大页面大小相除,得到一个p_vaddr的余数。基地址就是段地址的余数与p_vaddr的余数之差。
可读可写通用,可写是最高的
代码段(.text)可能包含这些节:.text .rodata .hash .dynsym .dynstr .plt .rel.got
数据段(.data)可能包含这些节: .data .dynamic .got .bss
.bss 类型为 SHT_NOBITS, 在目标文件不占空间,但在段中(程序中)会占,一般在段末尾,所以 p_memsz 可能会比 p_filesz 大
类型为 PT_NOTE, 用于给其他程序检查目标文件的一致性和兼容性
readelf a.out --string-dump=.comment
组织方式略
定义:操作系统创建或扩充进程镜像的过程
逻辑上,需要把文件中的段复制到虚拟内存,但这样效率很低,实际在需要访问时才映射。这要求 ELF 文件中段的镜像在文件中的偏移量或内存虚拟地址必须向页面大小对齐(Intel 是4KB), 这样便于整页的换进换出。
ELF 中可能包含不纯的代码和数据,这可能导致映射到内存两次。
可执行文件与共享目标的段的装载不同:
定义:解析符号引用的过程。
发生时间:进程初始化与进程运行期间
需要动态库的可执行文件会有一个 PT_INTERP
类型的程序头项,该段包含一个路径字符串指名ELF解析器,执行程序时系统函数 exec 会去初始化该解析器的进程镜像,把进程空间暂时借给解析器,然后解析器继续执行:
关于解析器:
需要动态链接库的时候,链接编辑器会在elf程序头中加一个 PT_INTERP 项, 可执行文件与动态连接器一起创建了进程的镜像的过程包含了下面的活动:
typedef struct {
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
连接编辑器
会为 动态连接器
组织一些数据,下面几个 节
会在放到可装载段中以方便在运行是访问:
SHT_DYNAMIC
的 .dynamic
节包含很多动态连接信息,开始处的结构包含其他连接信息的地址SHT_HASH
的 .hash
节包含哈希符号SHT_PROGBITS
类型的 .got
(golbal offset table) 和 .plt
(procedure linkage table,函数链接表) 节各包含一张表 – 使用方法会在后面详述进程环境如果包含变量 LD_BIND_NOW
且不为空,如 LD_BIND_NOW=1/on/off
, 那连接器需要在程序运行前把所有重定位都处理完。否则重定位工作可以推后(引用时)
若 ELF 参与动态连接,则程序头表一定会包含一个 PT_DYNAMIC
表项,对应的段称为动态段(dynamic segment), 段名 .dynamic
, 作用是提供连接器需要的信息如:so文件名、动态连接符号表位置、动态连接重定位表位置…
动态段包含所有的动态节,由符号 _DYNAMIC
标记(how??), 包含下面结构体的数组:
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union {
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
extern Elf64_Dyn _DYNAMIC[];
对这种类型的对象,d_tag 控制 d_un 的含义:
下面表格总结了标志的要求:
表22动态项标志说明
d_tag名称 | 数值 | d_un | 可执行 | 共享目标 | 说明 |
---|---|---|---|---|---|
DT_NULL | 0 | 忽略 | 必需 | 必需 | 标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数组的末端。 |
DT_NEEDED | 1 | d_val | 可选 | 可选 | 此元素包含一个 NULL 结尾的字符串的字符串表偏移,该字符串给出某个需要的库的名称。所使用的字符串表根据 DT_STRTAB 项目中记录的内容确定。所谓的偏移即是指在该表中的下标。动态数组中可以包含多个这种类型的条目。这些条目的相对顺序很重要,尽管他们与其他类型条目间的顺序没有很大关系。 |
DT_PLTRELSZ | 2 | d_val | 可选 | 可选 | 此元素给出了与过程链接表(PLT)相关联的重定位项的总计大小(按字节)。如果存在 DT_JMPREL 类型的条目,必须有与之配合的 DT_PLTRELSZ 条目。 |
DT_PLTGOT | 3 | d_ptr | 可选 | 可选 | 此元素给出一个与过程链接表(PLT)与/或全局偏移表相关联的一个地址。 |
DT_HASH | 4 | d_ptr | 必需 | 必需 | 此元素包含符号哈希表的地址。此哈希表指的是被DT_SYMTAB |
DT_STRTAB | 5 | d_ptr | 必需 | 必需 | 此元素包含字符串表的地址,符号名、库名、和其他字符串都包含在此表中。 |
DT_SYMTAB | 6 | d_ptr | 必需 | 必需 | 此元素包含符号表的地址。对 32 位的文件而言,这个符号表中的条目是 Elf32_Sym 类型。 |
DT_RELA | 7 | d_ptr | 必需 | 可选 | 此元素包含重定位表的地址。此表中的元素包含显式的补齐,例如 32 位文件中的 Elf32_Rela。目标文件可能有多个重定位节区。在为可执行文件或者共享目标文件构造重定位表时,连接编辑器将这些节区连接起来,形成一个表格。尽管在目标文件中这些节区保持相互独立,动态链接器所看到的仍然是一个表。在动态链接器为可执行文件创建进程映像或者向一个进程映像中添加某个共享目标时,要读取重定位表,并执行相关的动作。如果此元素存 在 , 动 态 结 构 必 须 也 包 含 DT_RELASZ 和DT_RELAENT 元素。如果对于某个文件来说,重定位能力是必需的,那么 DT_RELA 或者 DT_REL 都可能存在(二者都是允许存在但不要求存在的)。 |
DT_RELASZ | 8 | d_val | 必需 | 可选 | 此元素包含 DT_RELA 重定位表的大小(按字节数计算)。 |
DT_RELAENT | 9 | d_val | 必需 | 可选 | 此元素包含 DT_RELA 重定位项的大小(按字节计算)。 |
DT_STRSZ | 10 | d_val | 必需 | 必需 | 此元素给出字符串表的大小,按字节数计算。 |
DT_SYMENT | 11 | d_val | 必需 | 必需 | 此元素给出符号表项的大小,按字节数计算。 |
DT_INIT | 12 | d_ptr | 可选 | 可选 | 此元素包含初始化函数的地址。 |
DT_FINI | 13 | d_ptr | 可选 | 可选 | 此元素包含结束函数(Termination |
DT_SONAME | 14 | d_val | 忽略 | 可选 | 此元素给出一个 NULL 结尾的字符串的字符串表偏移,字符串是某个共享目标的名称。该偏移实际上是 DT_STRTAB 项目所记录的表格的索引。 |
DT_RPATH | 15 | d_val | 可选 | 忽略 | 此元素包含 NULL 结尾的字符串的字符串表偏移,字符串是搜索库时使用的搜索路径。该偏移实际上是 DT_STRTAB 项目所记录的表格的索引。 |
DT_SYMBOLIC | 16 | 忽略 | 忽略 | 可选 | 此元素出现于某个共享目标库中时,将改变动态链接器在该库中解析引用时使用的符号解析算法。动态链接器不再从可执行文件中开始搜索符号,而是从共享目标中开始搜索。如果共享目标未能提供所引用的符号,动态链接器才会和平常一样搜索可执行文件和其他共享目标。 |
DT_REL | 17 | d_ptr | 必需 | 可选 | 此元素与 DT_RELA 类似,只是其表格中包含隐式的补齐,对 32 位文件而言,就是 Elf32_Rel。如果文件中包含此元素,那么动态结构中也必须包含 DT_RELSZ 和 DT_RELENT 元素。 |
DT_RELSZ | 18 | d_val | 必需 | 可选 | 此元素包含 DT_REL 重定位表的总计大小,按字节数计算。 |
DT_RELENT | 19 | d_val | 必需 | 可选 | 此元素包含 DT_REL 重定位项的大小,按字节数计算。 |
DT_PLTREL | 20 | d_val | 可选 | 可选 | 此成员给出过程链接表所引用的重定位项的类型。根据具体情况,d_val 成员包含 DT_REL 或者DT_RELA。过程链接表中的所有重定位都必须采用相同的重定位方式。 |
DT_DEBUG | 21 | d_ptr | 可选 | 忽略 | 此成员用于调试。ABI 未规定其内容,访问这些条目的程序与 ABI 不兼容。 |
DT_TEXTREL | 22 | 忽略 | 可选 | 可选 | 如果文件中不包含此成员,则表示没有任何重定位表项能够引起对不可写段的修改,正如程序头部表中段许可所规定的。如果存在此成员,则存在若干重定位项要求对不可写段进行修改,动态链接器因此可以作相应的准备。 |
DT_JMPREL | 23 | d_ptr | 可选 | 可选 | 如果存在这种成员,则表示条目的 d_ptr 成员包含了某个重定位项的地址,并且该重定位项仅与过程链接表相关。把重定位项分开有利于让动态链接器在进程初始化时忽略它们,当然后期绑定必须可行。如果存在此成员,相关的 DT_PLTRELSZ 和 DT_PLTREL 必须也存在。 |
DT_LOPROC | 0x70000000 | 未指定 | 未指定 | 未指定 | 这个范围的表项,包括 DT_LOPROC 和 DT_HIPROC 都是保留给处理器特定的语义的 |
DT_HIPROC | 0x7fffffff | 未指定 | 未指定 | 未指定 | 同上 |
注:
处理存档库时:
连接编辑器
提取库成员并拷贝到输出文件动态连接器
要把共享目标也载入到进程空间假设 a.so 引用 b.so, a.out 同时引用 a.so,b.so , 最后b.so 只被引用一次,原因:
动态结构中的 DT_NEEDED
项指名依赖库,动态连接器
会连接被引用的符号和它们依赖的库(反复执行)。解析符号引用时,动态连接器
会用一种广度优先的算法来查找符号,就是先查找自己的符号表,再下一层依赖库。即使一个 .so 被引用多次,只会连接一次
依赖关系列表中的名字,即可以是 DT_SONAME
字符串,也可是 .so 完整路径名, 比如依赖列表中可能存在 “lib1”、“/usr/lib/lib2”, 如果名字中带 ‘/’ 则直接把字符串当路径名,否则根据下面三个规则查找:
DT_RPATH
可能会给出一个含一些列目录名的字符串,冒号隔开如 “/home/dir:/home/dir2:”LD_LIBRARY_PATH
查找.got
表选择在私有数据中包含绝对地址,没有牺牲独立性
全局偏移表中最初包含其重定位项中要求的信息。在系统为可加载目标创建内存段以后,动态链接器要处理重定位项,其中有一些重定位项的类型是 R_386_GLOB_DAT
,是对全局偏移表的引用。动态链接器确定相关的符号取值,计算其绝对地址,并将相应的内存表格项目设置为正确的数值。尽管在链接编辑器构造一个目标文件时还无法知道绝对地址,动态链接器清楚所有内存段的地址,因而能够计算其中所包含的符号的绝对地址。
如果程序需要直接访问某个符号的绝对地址,那么该符号就会具有一个全局偏移表项。由于可执行文件和共享目标具有独立的全局偏移表,一个符号的地址可能出现在多个表中。动态链接器在将控制交给进程映像中任何代码之前,要处理所有的全局偏移表重定位,因而确保了执行过程中绝对地址信息可用。
表项0是保留的,用来存放动态结构的地址,可以用符号_DYNAMIC引用之。这样,类似动态链接器这种程序能够在尚未处理其重定位项的时候先找到自己的动态结构。对于动态链接器而言这点很重要,因为它必须能够在不依赖其他程序来对其内存映像进行重定位的前提下,初始化自己。在32位 Intel 体系结构下,全局偏移表中的表项1和2也是保留的。
系统可能在不同的程序中为相同的共享目标选择不同的内存段地址,甚至为统一程序的两次执行选择不同的库地址。尽管如此,一旦进程映像被建立起来,内存段不会改变其地址。只有进程存在,其内存段都位于固定的虚地址。
全局偏移表的格式和解释都是和处理器相关的。对于 64 位 Intel 体系结构而言,符号 _GLOBAL_OFFSET_TABLE_
可以用来访问该表。
extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];
.exe 和 .so 文件引用同一个函数时,地址不相同。 .so 文件被正常解析为所在虚拟地址, .exe 中被动态连接器定向到函数连接表的一个表项。
为了在比较地址时出现逻辑错误,当 .exe 引用一个在 .so 中定义的函数时,连接编辑器
就把这个函数的函数连接表项的地址放到其相应的符号表项
中去。动态连接器
会特别对待这种符号表项。在 .exe 中,如果 动态连接器
查找一个符号时遇到了这种符号表项,就会按照以下规则行事:
SHN_UNDEF
,并且符号类型是STT_FUNC,st_value成员又非0的话,动态连接器就认定这是一个特殊的项,把 st_value 成员作为符号的地址也有些 重定位
与 函数连接表项
有关,这些表项用于给函数调用做 定向
,而不是引用函数地址, 但这种不能像上面描述的那样处理,因为 动态连接器
不可以把 函数连接表项
重定向到它们自己.
PLT 的作用是把位置独立的函数重定向到绝对地址
。
连接编辑器
不能解析函数在不同目标文件之间的跳转, 它把对 其它目标文件 中 函数的调用 重定向 到一个 函数连接表项
中去(intel 架构它位于 共享代码段
),它使用 GOT 中的私有地址。动态连接器
决定目标的绝对地址,并会相应的修改 GOT 中的内存镜像。这样就实现了位置无关和共享的绝对地址定位。 .exe 和 .so 维护各自的函数链接表。
绝对地址的函数连接表(Absolute Procedure Linkage Table)
.PLT0: pushl got_plus_4
jmp *got_plus_8
nop; nop
nop; nop
.PLT1: jmp *name1_in_GOT
pushl $offset@PC
.PLT2: jmp *name2_in_GOT
pushl $offset
jmp .PLT0@PC
...
地址无关的函数连接表(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
中的指令使用的 操作数寻址方式
不同。但是它们给 动态连接器的接口
都是相同的。
在以下的这些步骤中,动态连接器
与 程序
合作来 解析 PLT
和 GOT
中所有的 符号引用
动态连接器
把 GOT 中的二三项设为特定值,下面的步骤去解析特定值函数连接表(PLT)
位置独立,则 GOT 地址必须存在 %ebx
, 进程空间的每一个 .so 都有自己的 .plt, 每个表都用于文件内的函数调用。所以主调函数需要负责在调用 .plt 之前设置 .got重定位偏移
(offset) 压栈。重定位偏移是一个 32 位非负数,是在重定位表中的字节偏移量。指定的重定位表项的类型为 R_386_JMP_SLOT,其偏移将给出在前面的 jmp 指令中使用的 GOT 项
。重定位项也包含一个符号表索引,借以告诉动态链接器被引用的符号是什么,在这里是 name1。环境变量 LD_BIND_NOW
可以更改动态链接行为。如果其取值非空,动态链接器会在控制传递给程序之前,对 plt 进行计算。就是说动态链接器会在进程初始化的过程中处理类型为 R_386_JMP_SLOT
的重定位项。否则,动态链接器会对过程链接表实行懒惰计算,延迟符号解析和重定位,直到某个表项的第一次执行。 懒惰绑定通常会提供整体的应用性能,因为未使用的符号不会引入额外的动态链接开销。尽管如此,有些应用情形会使得 懒惰绑定
不太合适。首先,对 .so 函数的第一次引用花的时间会超出后续调用
,因为动态链接器要截获调用以便解析符号。一些应用不能容忍这种不可预测性。第二,如果发生了错误,动态链接器无法解析某个符号,动态链接器会终止程序。在懒惰绑定下,这类事情可能会发生任意多次。某些应用也可能无法容忍这种不可预测性。通过关闭懒惰绑定,动态链接器会迫使所有错误都发生在进程初始化期间,而不是应用程序接收控制以后。
至少 Elf32_Word 目标组成的哈希表支持符号表的访问,下图解释了哈希表(但不是规范的一部分):
nbucket
---
nchain
---
bucket[0]
...
bucket[nbucket-1]
---
chain[0]
...
chain[nchain-1]
常见的哈希函数:;
unsigned longelf_hash(constunsignedchar *name) {
unsignedlongh = 0, g;
while (*name) {
h = (h << 4) + *name++ ; if (g = h & 0xf0000000) h ^= g >> 24;
h &= -g;
}
returnh;
}
Bucket数组中含有 nbucket 个项,chain数组中含有 nchain 个项,序号都从 0 开始。Bucket 和 chain 中包含的都是符号表中的索引。符号表中的项数必须等于 nchain,所以符号表中的索引号也可以用来索引 chain 表。如下所示的一个哈希函数输入一个符号名,输出一个值用于计算 bucket 索引。如果给出一个符号名,经哈希函数计算得到值x,那么 x%nbucket
是 bucket 表内的索引, bucket[x%nbucket]
给出一个符号表的索引值y,y同时也是 chain 表内的索引值。如果符号表内索引值为 y 的元素并不是所要的,那么 chain[y]
给出符号表中下一个哈希值相同的项的索引。如果所有哈希值相同的项都不是所要的,最后的一个 chain[y]
将包含值STN_UNDEF,说明这个符号表中并不含有此符号。
这种解冲突的方法是 链式地址法
?
在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行某些初始化代码的机会
。这些初始化函数的被调用顺序是不一定的,不过所有共享目标初始化都会在可执行文件得到控制之前发生
。类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit()
机制执行。动态链接器对终止函数的调用顺序是不确定的。 共享目标通过动态结构中的DT_INIT
和 DT_FINI
条目指定初始化/终止函数。通常这些代码放在.init
和.fini
节区中
注意:尽管 atexit()
终止处理通常会被执行,在进程消亡时并不能保证被执行。特别地,如果进程调用了 _exit
或者进程因为收到某个它既未捕捉又未忽略的信号而终止时,不会执行终止处理。
/usr/lib/libc.so.6
竟然就是一个程序解析器 ???
遗留问题:
Computed goto
在 elf 中是怎么存在的Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。