理解 ELF

01. ELF 文件的静态结构

静态格式.svg

1.1 介绍

1.1.1 它是什么

名字的由来:ELF 即 Executable and Linking Format,可执行 可链接格式。

主要有三种类型:

  • 可重定位文件
  • 共享目标文件
  • 可执行文件

1.1.2 它怎么来的

汇编器 和 链接器 生成,一种字节流形式的文件

1.2 它是怎么组织起来的

类似 TLV(type length value) 这种组织形式,猜测肯定有类似表示类型长度值的各种字段。事实上也类似,我们需要建立起下面几个概念:ELF 文件头、程序/节头表、段/节

1、ELF 文件头想当然的是包含文件的结构信息,具体指什么后面讲

2、节 专用于连接过程,包含 指令、符号、重定位数据等

3、程序头表 包含创建进程镜像的一些信息

4、节头表:每个节都必须在节头表中有一个注册项(描述名字大小),每个都要在节头表中对应注册项,以描述名字大小等

5、段 描述缺失

1.2.1 它怎么表示数据

  • 控制数据:ELF 定义了自己的数据结构所以不依赖机器的字长(需要机器去适配?)
  • 其他数据:使用目标处理器数据格式
1.2.1.1 怎么表示文本字符

文本字符应当数据 1.2.1 描述的其他数据。

TIS 委员会没有限制所使用的字符集,但必须

  • 与 0~127 的 ASCII 字符兼容
  • 无论单/多 字节,每个字节都不能在 0~127 之间
  • 多字节字符必须 自识别:每个字符识别不依赖于其他条件

1.3 ELF 文件头具体包含哪些信息

前面提到包含了结构信息,具体指啥?详细的在文档中有一个结构体,这里摘录几个字段:

  • e_ident: 识别标志及解码数据,因为不同体系结构和编码格式,ELF文件内容会截然不同,所以这16个字节会位置固定且通用。
    • 它包含了文件标志(4 byte)、文件类别、编码格式、文件版本、补齐字节开始位置 等
    • e_ident[5] = 1, 表示是小头编码(LSB), 权值较小的在前
  • e_type: 文件类型,实践表明 a.out 属于 动态链接库文件 而非 可执行文件(除非静态链接)
  • e_machine: 我的AMD 是 0x3e
  • e_entry: 怎么试都是 0

1.4 怎么定义,它是个啥??

前面知道它在节头表中被登记, 且目标文件有很多节,节头表每个表项是个 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;
特别说明下 sh_type 类型字段:
  • SHT_SYMTAB/SHT_DYNSYM 两类都包含符号表,SHT_SYMTAB包含完整的, SHT_DYNSYM 包含一个较小的专门用于动态连接
  • SHT_RELA/SHT_REL: 含带明确加数的重定位项,32位的是 Elf32_Rela, 这个是用来干什么的?
  • SHT_DYNAMIC: 动态连接信息

1.5 特殊的节

  1. 指定代码/控制信息 是预定好的,不同的操作系统这些节的类型和属性会不一样!!

每个操作系统有自己的连接模型,总的来说还是两类:

  • 静态连接:动静态库被静态绑定??,所有符号被解析
  • 动态连接:略,如果解析动态连接进来的符号引用,不同系统有不同方式
  1. .debug .line 包含程序控制信息
  2. .bss .data .data1 .rodata .rodata1 包含程序控制信息
  3. 有些含有程序控制信息,大多用于连接过程,动态连接过程由下面的节提供:
    .dynsym、.dynstr、.interp、.hash、.dynamic、.rel、.rela、.got、.plt
    (.plt和.got 依处理器而不同,但支持同样的连接模型)
  4. .init .fini 用于进程初始化和终止过程
  5. 具体说明略,见文档

1.6 特殊节中的特殊节 --> 字符串表

也就是 前文提到的 .strtab 节, (Elf32_Shdr)obj.sh_type=SHT_STRTAB,
(Elf32_Shdr)obj.sh_entsize=???

包含若干以 ‘null’ 结尾的字符序列,需要引用时提供序号即可(如果忘记用法需要再读一下文档相关章节)

1.7 特殊节中的特殊节 --> 符号表

也就是 前文提到的 .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 就是第一天指令的地址,否则地址被动态连接器用来解析函数地址 – 不是很明白.

对于 (Elf32_Sym)obj.st_info : 本地在前,全局在后
  • 符号绑定(高四位)
    • 0-STB_LOCAL: 本地符号,只出现在本文件,文件外无效
    • 1-STB_GLOBAL: 全局符号
    • 2-STB_WEAK:
      • 类似全局,优先级更低
      • 在查找符号时,连接编辑器不会去提取弱符号存档成员
    • 13~15-STB_LOPROC ~ STB_HIPROC: 保留
  • 符号类型(低四位)
    • 0: 未指定
    • 1:数据对象,如变量数组
    • 2:函数对象
    • 3:与节相关,用于重定位
    • 4: 文件相关
    • 13~15:保留
  • 符号信息:b 是个啥?
#defineELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf)))
对于 (Elf32_Sym)obj.st_shndx :

符号表项 与 节 的关系:前者一定与一个后者相联系。前者指明相关联的节,重定位时根据节位置的改变而改变。对下面三种特殊的节有特别的意义:

  • SHN_ABS: 值是绝对的,有常量属性
  • SHN_COMMON: 因为是没有分配的公共节,所以此值规定对齐规则
  • SHN_UNDEF: 本符号不在当前目标文件中定义,在链接时找

另外提一下,符号表的首项与其他不同.

1.8 重定位

重定位(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)

对于 (Elf32_rel)obj.r_info
  • 所这用的符号表索引:高24位?
  • 类型:低8位,各个处理器定义不同

一个重定位节需要引用另外两个节:

  • 符号表节, 引用关系由 sh_info 指定
  • 被修改节,引用关系由 sh_link 指定

不同目标文件里 r_offset 成员的含义不同:

  • 重定位文件: r_offset 成员是一个偏移量,即 重定位节描述如何如何修改文件中的另一个节的内容,指向了另一个节中的存储单元地址
  • 可执行或共享目标文件中:r_offset 是符号定义在进程空间的虚拟地址
重定位项用于描述如何修改下面的指令和数据域

引入一个概念:被重定位域,32位

重定位文件转换为可执行或so文件的过程:

  1. 首先决定如何组装
  2. 定位符号
  3. 更新符号值
  4. 实现重定位

有下面几种运算

  • A: 表示用于计算重定位域值的加数
  • B:表示在程序运行期,共享目标被装入内存时的基地址。一般来说,共享目标文件在构建时基地址为0,但在运行时则不是
  • G:表示可重定位项在全局偏移量表中的位置,这里存储了此重定位项在运行期间的地址。更多信息参见下文"全局偏移量表"
  • GOT:表示全局偏移量表的地址
  • L:表示一个符号的函数连接表项的所在之处,可能是节内偏移量,或者是内存地址。
  • P:表示被重定位的存储单元在节内的偏移量或者内存地址,由 r_offset 计算得到
  • S:表示重定位项中某个索引值所代表的符号的值

一个重定位项的 r_offset 值指定了被重定位的数据在节内的偏移量或者在进程空间内的虚拟地址
重定位类型指定了哪些位需要被修改以及如何算计它们的值。如 R_386_GOT32 需要计算 G+A

重定位有哪些类型,在哪个字段被指定

重定位有下面几种类型:

  • R_386_GLOB_DAT: 这种重定位类型用于把指定的符号地址设置为一个全局偏移量表项。这种重定位类型在符号与全局偏移量表项之间建立起了联系
  • R_386_JMP_SLOT: 连接编辑器创建这种重定位类型,用于动态连接
  • R_386_RELATIVE: 连接编辑器创建这种重定位类型,主要是用于动态连接, 此类型相应的 offset 成员给出了共享目标内的一个位置,这个位置含有一个代表相对地址的值。把共享目标被加载的地址加上这个相对地址,动态连接器就可以计算得到真正需要的虚拟地址。这种类型的重定位项必须为符号表指定0值。
  • R_386_GOTOFF: 这种重定位类型计算符号值与全局偏移量表地址之间的差值。它还指示连接编辑器去构建全局偏移量表。
  • R_386_GOTPC: 这种重定位类型与R_386_PC32很相似,只不过在计算中它使用的是全局偏移量表的地址。一般来说,在这种类型的重定位中所引用的符号是_GLOBAL_OFFSET_TABLE_,它还指示连接编辑器去构建全局偏移量表。

2. ELF文件的装载与动态连接

2.1 介绍

描述将 ELF 与 动态链接库装载到进程空间过程的系统行为:

  • 装载:把目标文件载入内存
  • 连接:解析目标文件的符号引用

2.2 程序头

2.2.1 程序头的结构

可执行或.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;

2、 ELF 文件的装载与动态连接

2.1 介绍

本章描述用于创建程序的目标文件信息系统行为. 可执行文件和共享目标文件(动态连接库)是程序的静态存储形式。要执行一个程序,系统要先把相应的 可执行文件和动态连接库 装载到进程空间中,这样形成一个可运行的 进程的内存空间布局,也可以称它为 "进程镜像"。一个已装载完成的进程空间会包含多个不同的"段(segment)",比如代码段(text segment),数据段(data segment),堆栈段(stack segment)等等。

2.2 程序头

2.2.1 程序头结构

一个可执行文件或共享目标文件的 程序头表(program header table) 是一个 数组,数组中的每一个元素称为 "程序头(program header)",每一个 程序头 描述了一个"段(segment)" 或者一块用于准备执行程序的信息。一个目标文件中的 "段(segment)" 包含一个或者多个 "节(section)"。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。在目标文件的文件头(elf header)中,e_phentsizee_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;
p_type

此数据成员说明了本程序头所描述的段的类型,或者如何解析本程序头的信息。

  • PT_NULL: 此类型表明本程序头是未使用的
  • PT_LOAD:此类型表明本程序头指向一个可装载的段,段的内容会被从文件中拷贝到内存中
  • PT_DYNAMIC:此类型表明本段指明了动态连接的信息
  • PT_INTERP:本段指向了一个以"null"结尾的字符串,这个字符串是一个ELF解析器的路径
  • PT_NOTE:本段指向了一个以"null"结尾的字符串,这个字符串包含一些附加的信息
  • 其他略
p_offset

此数据成员给出本段内容在 文件中的位置,即段内容的开始位置相对于文件开头的偏移量

p_vaddr

此数据成员给出本段内容的开始位置在 进程空间中的虚拟地址

p_paddr

此数据成员给出本段内容的开始位置在 进程空间中的物理地址

p_filesz

此数据成员给出本段内容在 文件中的大小,单位是字节,可以是0

p_memsz

此数据成员给出本段内容在 内容镜像中的大小,单位是字节,可以是0。

p_flags

此数据成员给出了本段内容的属性。具体有哪些标志位请参见下文

p_align

对于可装载的段来说,其p_vaddr和p_offset的值至少要向内存页面大小对齐

2.2.2 基地址

可执行文件中需要含有绝对的地址, 比如变量地址,函数地址等,为了让程序正确地执行,“段” 中出现的虚拟地址必须在创建可执行程序时被重新计算. 另一方面,出于ELF通用性的要求,目标文件的段中又不能出现绝对地址,其代码是不应依赖于具体存储位置的,即同一个段在被加载到两个不同的进程中时,它的地址可能不同,但它的行为不能表现出不一样。

在被加载到进程空间里时,尽管段会被分配到一个不确定的地址,但相对位置是确定的。

一个可执行文件或共享目标文件的 基地址 是在运行期间由以下三个值计算出来的:内存加载地址最大页面大小程序可装载段的最低地址zl。为计算基地址,首先找出类型为PT_LOAD(即可加载)而且p_vaddr(段地址)最低的那个段,把这个段在内存中的地址与最大页面大小相除,得到一个段地址的余数;再把p_vaddr与最大页面大小相除,得到一个p_vaddr的余数。基地址就是段地址的余数与p_vaddr的余数之差。

2.2.3 段权限

可读可写通用,可写是最高的

2.3 段内容

代码段(.text)可能包含这些节:.text .rodata .hash .dynsym .dynstr .plt .rel.got
数据段(.data)可能包含这些节: .data .dynamic .got .bss

.bss 类型为 SHT_NOBITS, 在目标文件不占空间,但在段中(程序中)会占,一般在段末尾,所以 p_memsz 可能会比 p_filesz 大

2.4 段注释

类型为 PT_NOTE, 用于给其他程序检查目标文件的一致性和兼容性

readelf a.out --string-dump=.comment

组织方式略

2.5 程序装载

定义:操作系统创建或扩充进程镜像的过程

逻辑上,需要把文件中的段复制到虚拟内存,但这样效率很低,实际在需要访问时才映射。这要求 ELF 文件中段的镜像在文件中的偏移量或内存虚拟地址必须向页面大小对齐(Intel 是4KB), 这样便于整页的换进换出。

ELF 中可能包含不纯的代码和数据,这可能导致映射到内存两次。

可执行文件与共享目标的段的装载不同:

  • 可执行文件当然要包含绝对地址,用p_vaddr来记录不变的虚拟地址。
  • 共享目标文件中,当然要使用地址无关的代码。给不同程序用,so 被加载的地址也不同,所以只能根据段间的相对位置来访问
    • 在进程的内存中,两段的虚拟地址之差与它们在文件中的位置偏移量之差相等

2.6 动态连接

定义:解析符号引用的过程。
发生时间:进程初始化与进程运行期间

2.6.1 程序解析器

需要动态库的可执行文件会有一个 PT_INTERP 类型的程序头项,该段包含一个路径字符串指名ELF解析器,执行程序时系统函数 exec 会去初始化该解析器的进程镜像,把进程空间暂时借给解析器,然后解析器继续执行:

  • 解析器可以取得可执行文件的描述符,读取并映射可执行程序的段到内存
  • 也可系统直接将文件内容载入内存

关于解析器:

  • 一般是一个共享目标,且段内容位置不相关,一般系统会用mmap 在动态区域为解析器创建段镜像
  • 它也可以是独立的 exe, 那系统就要按照此exe的程序头来加载它

2.6.2 动态连接器

需要动态链接库的时候,链接编辑器会在elf程序头中加一个 PT_INTERP 项, 可执行文件与动态连接器一起创建了进程的镜像的过程包含了下面的活动:

  • 添加 exe 的段到进程空间
  • 添加 so 的段到共享空间
  • 为 exe 和 so 进行重定位
  • 关闭 exe 的文件描述符
  • 把控制权交给程序
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, 那连接器需要在程序运行前把所有重定位都处理完。否则重定位工作可以推后(引用时)

2.6.3 动态段

若 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 的含义:

  • d_val 表示一个整数值,解释有很多
  • d_ptr 表示虚拟地址,如前面描述,可能不匹配,需加上基地址

下面表格总结了标志的要求:
表22动态项标志说明

d_tag名称数值d_un可执行共享目标说明
DT_NULL0忽略必需必需标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数组的末端。
DT_NEEDED1d_val可选可选此元素包含一个 NULL 结尾的字符串的字符串表偏移,该字符串给出某个需要的库的名称。所使用的字符串表根据 DT_STRTAB 项目中记录的内容确定。所谓的偏移即是指在该表中的下标。动态数组中可以包含多个这种类型的条目。这些条目的相对顺序很重要,尽管他们与其他类型条目间的顺序没有很大关系。
DT_PLTRELSZ2d_val可选可选此元素给出了与过程链接表(PLT)相关联的重定位项的总计大小(按字节)。如果存在 DT_JMPREL 类型的条目,必须有与之配合的 DT_PLTRELSZ 条目。
DT_PLTGOT3d_ptr可选可选此元素给出一个与过程链接表(PLT)与/或全局偏移表相关联的一个地址。
DT_HASH4d_ptr必需必需此元素包含符号哈希表的地址。此哈希表指的是被DT_SYMTAB
DT_STRTAB5d_ptr必需必需此元素包含字符串表的地址,符号名、库名、和其他字符串都包含在此表中。
DT_SYMTAB6d_ptr必需必需此元素包含符号表的地址。对 32 位的文件而言,这个符号表中的条目是 Elf32_Sym 类型。
DT_RELA7d_ptr必需可选此元素包含重定位表的地址。此表中的元素包含显式的补齐,例如 32 位文件中的 Elf32_Rela。目标文件可能有多个重定位节区。在为可执行文件或者共享目标文件构造重定位表时,连接编辑器将这些节区连接起来,形成一个表格。尽管在目标文件中这些节区保持相互独立,动态链接器所看到的仍然是一个表。在动态链接器为可执行文件创建进程映像或者向一个进程映像中添加某个共享目标时,要读取重定位表,并执行相关的动作。如果此元素存 在 , 动 态 结 构 必 须 也 包 含 DT_RELASZ 和DT_RELAENT 元素。如果对于某个文件来说,重定位能力是必需的,那么 DT_RELA 或者 DT_REL 都可能存在(二者都是允许存在但不要求存在的)。
DT_RELASZ8d_val必需可选此元素包含 DT_RELA 重定位表的大小(按字节数计算)。
DT_RELAENT9d_val必需可选此元素包含 DT_RELA 重定位项的大小(按字节计算)。
DT_STRSZ10d_val必需必需此元素给出字符串表的大小,按字节数计算。
DT_SYMENT11d_val必需必需此元素给出符号表项的大小,按字节数计算。
DT_INIT12d_ptr可选可选此元素包含初始化函数的地址。
DT_FINI13d_ptr可选可选此元素包含结束函数(Termination
DT_SONAME14d_val忽略可选此元素给出一个 NULL 结尾的字符串的字符串表偏移,字符串是某个共享目标的名称。该偏移实际上是 DT_STRTAB 项目所记录的表格的索引。
DT_RPATH15d_val可选忽略此元素包含 NULL 结尾的字符串的字符串表偏移,字符串是搜索库时使用的搜索路径。该偏移实际上是 DT_STRTAB 项目所记录的表格的索引。
DT_SYMBOLIC16忽略忽略可选此元素出现于某个共享目标库中时,将改变动态链接器在该库中解析引用时使用的符号解析算法。动态链接器不再从可执行文件中开始搜索符号,而是从共享目标中开始搜索。如果共享目标未能提供所引用的符号,动态链接器才会和平常一样搜索可执行文件和其他共享目标。
DT_REL17d_ptr必需可选此元素与 DT_RELA 类似,只是其表格中包含隐式的补齐,对 32 位文件而言,就是 Elf32_Rel。如果文件中包含此元素,那么动态结构中也必须包含 DT_RELSZ 和 DT_RELENT 元素。
DT_RELSZ18d_val必需可选此元素包含 DT_REL 重定位表的总计大小,按字节数计算。
DT_RELENT19d_val必需可选此元素包含 DT_REL 重定位项的大小,按字节数计算。
DT_PLTREL20d_val可选可选此成员给出过程链接表所引用的重定位项的类型。根据具体情况,d_val 成员包含 DT_REL 或者DT_RELA。过程链接表中的所有重定位都必须采用相同的重定位方式。
DT_DEBUG21d_ptr可选忽略此成员用于调试。ABI 未规定其内容,访问这些条目的程序与 ABI 不兼容。
DT_TEXTREL22忽略可选可选如果文件中不包含此成员,则表示没有任何重定位表项能够引起对不可写段的修改,正如程序头部表中段许可所规定的。如果存在此成员,则存在若干重定位项要求对不可写段进行修改,动态链接器因此可以作相应的准备。
DT_JMPREL23d_ptr可选可选如果存在这种成员,则表示条目的 d_ptr 成员包含了某个重定位项的地址,并且该重定位项仅与过程链接表相关。把重定位项分开有利于让动态链接器在进程初始化时忽略它们,当然后期绑定必须可行。如果存在此成员,相关的 DT_PLTRELSZ 和 DT_PLTREL 必须也存在。
DT_LOPROC0x70000000未指定未指定未指定这个范围的表项,包括 DT_LOPROC 和 DT_HIPROC 都是保留给处理器特定的语义的
DT_HIPROC0x7fffffff未指定未指定未指定同上

注:

  • 没有出现在此表中的标记值是保留的。
  • 除了数组末尾的 DT_NULL 元素以及 DT_NEEDED 元素的相对顺序约束以外,其他项目可以以任意顺序出现。

2.6.4 共享目标的依赖关系

处理存档库时:

  • 静态连接:连接编辑器 提取库成员并拷贝到输出文件
  • 动态连接:动态连接器 要把共享目标也载入到进程空间

假设 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 查找
  • 搜索 /usr/lib

2.6.5 全局偏移表( global offset table – .got )

.got 表选择在私有数据中包含绝对地址,没有牺牲独立性

全局偏移表中最初包含其重定位项中要求的信息。在系统为可加载目标创建内存段以后,动态链接器要处理重定位项,其中有一些重定位项的类型是 R_386_GLOB_DAT,是对全局偏移表的引用。动态链接器确定相关的符号取值,计算其绝对地址,并将相应的内存表格项目设置为正确的数值。尽管在链接编辑器构造一个目标文件时还无法知道绝对地址,动态链接器清楚所有内存段的地址,因而能够计算其中所包含的符号的绝对地址。

如果程序需要直接访问某个符号的绝对地址,那么该符号就会具有一个全局偏移表项。由于可执行文件和共享目标具有独立的全局偏移表,一个符号的地址可能出现在多个表中。动态链接器在将控制交给进程映像中任何代码之前,要处理所有的全局偏移表重定位,因而确保了执行过程中绝对地址信息可用。

表项0是保留的,用来存放动态结构的地址,可以用符号_DYNAMIC引用之。这样,类似动态链接器这种程序能够在尚未处理其重定位项的时候先找到自己的动态结构。对于动态链接器而言这点很重要,因为它必须能够在不依赖其他程序来对其内存映像进行重定位的前提下,初始化自己。在32位 Intel 体系结构下,全局偏移表中的表项1和2也是保留的。

系统可能在不同的程序中为相同的共享目标选择不同的内存段地址,甚至为统一程序的两次执行选择不同的库地址。尽管如此,一旦进程映像被建立起来,内存段不会改变其地址。只有进程存在,其内存段都位于固定的虚地址。

全局偏移表的格式和解释都是和处理器相关的。对于 64 位 Intel 体系结构而言,符号 _GLOBAL_OFFSET_TABLE_ 可以用来访问该表。

extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];

2.6.6 函数地址

.exe 和 .so 文件引用同一个函数时,地址不相同。 .so 文件被正常解析为所在虚拟地址, .exe 中被动态连接器定向到函数连接表的一个表项。

为了在比较地址时出现逻辑错误,当 .exe 引用一个在 .so 中定义的函数时,连接编辑器 就把这个函数的函数连接表项的地址放到其相应的符号表项中去。动态连接器 会特别对待这种符号表项。在 .exe 中,如果 动态连接器 查找一个符号时遇到了这种符号表项,就会按照以下规则行事:

  • 1、如果符号表项的 st_shndx 成员不是 SHN_UNDEF,动态连接器就找到了一个符号的定义,把表项的st_value成员作为符号的地址。
  • 2、如果符号表项的st_shndx成员是 SHN_UNDEF,并且符号类型是STT_FUNC,st_value成员又非0的话,动态连接器就认定这是一个特殊的项,把 st_value 成员作为符号的地址
  • 3、否则,动态连接器认为这个符号是在可执行文件中未定义的。

也有些 重定位函数连接表项 有关,这些表项用于给函数调用做 定向,而不是引用函数地址, 但这种不能像上面描述的那样处理,因为 动态连接器 不可以把 函数连接表项 重定向到它们自己.

2.6.7 函数连接表( procedure linkage table – .plt )

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 中的指令使用的 操作数寻址方式 不同。但是它们给 动态连接器的接口 都是相同的。

2.6.8 解析符号

在以下的这些步骤中,动态连接器程序 合作来 解析 PLTGOT 中所有的 符号引用

  • 1、最开始创建内存镜像时,动态连接器 把 GOT 中的二三项设为特定值,下面的步骤去解析特定值
  • 2、若 函数连接表(PLT) 位置独立,则 GOT 地址必须存在 %ebx, 进程空间的每一个 .so 都有自己的 .plt, 每个表都用于文件内的函数调用。所以主调函数需要负责在调用 .plt 之前设置 .got
  • 3、假设要调用函数 name1, 与之对应的 .plt 是 .PLT1
  • 4、第一条指令跳转到 name1 所在 .got 中的地址。一开始,.got 中持有的是 “push1” 指令的地址,而不是 “name1” 的地址。
  • 5、接下来,程序将 重定位偏移(offset) 压栈。重定位偏移是一个 32 位非负数,是在重定位表中的字节偏移量。指定的重定位表项的类型为 R_386_JMP_SLOT,其偏移将给出在前面的 jmp 指令中使用的 GOT 项。重定位项也包含一个符号表索引,借以告诉动态链接器被引用的符号是什么,在这里是 name1。
  • 6、在将重定位偏移压栈后,程序会跳转到 .PLT0 ,也就是过程链接表的第一项。pushl 指令把第二个全局偏移表项(got_plus_4 或者 4(%ebx))压入堆栈,因而为动态链接器提供了识别信息的机会。程序然后跳转到第三个 GOT 表项内保存的地址(got_plus_8 或者 8(%ebx)),后者将控制传递给动态链接器
  • 7、当动态链接器得到控制后,它恢复堆栈,查看指定的重定位项,寻找符号的值,将 name1 的 “真实” 地址存储于 .got 中,并将控制传递给期望的目的地
  • 8、.plt 的后续执行将把控制直接传递给 name1,不会再次调用动态链接器。就是说 .PLT1 处的 jmp 将控制传递给 name1,而不会执行后面的 pushl 指令

环境变量 LD_BIND_NOW 可以更改动态链接行为。如果其取值非空,动态链接器会在控制传递给程序之前,对 plt 进行计算。就是说动态链接器会在进程初始化的过程中处理类型为 R_386_JMP_SLOT 的重定位项。否则,动态链接器会对过程链接表实行懒惰计算,延迟符号解析和重定位,直到某个表项的第一次执行。 懒惰绑定通常会提供整体的应用性能,因为未使用的符号不会引入额外的动态链接开销。尽管如此,有些应用情形会使得 懒惰绑定 不太合适。首先,对 .so 函数的第一次引用花的时间会超出后续调用,因为动态链接器要截获调用以便解析符号。一些应用不能容忍这种不可预测性。第二,如果发生了错误,动态链接器无法解析某个符号,动态链接器会终止程序。在懒惰绑定下,这类事情可能会发生任意多次。某些应用也可能无法容忍这种不可预测性。通过关闭懒惰绑定,动态链接器会迫使所有错误都发生在进程初始化期间,而不是应用程序接收控制以后。

2.7 哈希表

至少 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,说明这个符号表中并不含有此符号。

这种解冲突的方法是 链式地址法 ?

2.8 初始化和终止函数

在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行某些初始化代码的机会。这些初始化函数的被调用顺序是不一定的,不过所有共享目标初始化都会在可执行文件得到控制之前发生。类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit() 机制执行。动态链接器对终止函数的调用顺序是不确定的。 共享目标通过动态结构中的DT_INITDT_FINI条目指定初始化/终止函数。通常这些代码放在.init.fini节区中

注意:尽管 atexit() 终止处理通常会被执行,在进程消亡时并不能保证被执行。特别地,如果进程调用了 _exit 或者进程因为收到某个它既未捕捉又未忽略的信号而终止时,不会执行终止处理。

2.9 程序解析器

/usr/lib/libc.so.6 竟然就是一个程序解析器 ???


遗留问题:

  1. Computed goto 在 elf 中是怎么存在的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值