前情提要
需求想写不落地加载dex的壳,但是想要加载dex的同时还可以oat就必须涉及到hook了,于是决定彻底扫一遍hook。但看了几篇文后发现….ELF文件格式都忘了差不多了、linker与exec还没看…..
还是因为当初看的时候手太懒了没有动手写啊……….
于是先从一篇完整的elf文件格式解析写起
readelf真的好用……
hook基本都是c++写的,因此解析也用c++写好点
因为我的机子都是64位的,因此解析android arm-64位下的so为目的
由于许多书籍对elf中的名词描述不同,因此采用如下翻译:
汉语 | 英语 |
---|---|
ELF文件头 | ELF header |
基地址 | base address |
动态连接器 | dynamic linker |
全局偏移量表 | global offset table |
初始化函数 | initialization function |
函数连接表 | procedure linkage table |
程序头 | program header |
节 | section |
段 | segment |
此外本篇将地址区分为三种:
逻辑地址:指由程序产生的与段相关的偏移地址部分。也就是段*16+的那个值。
虚拟地址/线性地址:为进程虚拟的地址空间
物理地址:实际地址空间
现在从逻辑地址到虚拟地址还要经过一次保护模式分段机制。简述就是保护模式将段的信息存于GDT与LDT中,段寄存器存其表的索引值。
32位Linux中逻辑地址等于线性地址,因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。
现在先认为elf中的都是虚拟地址。等搞清保护模式机制时阅读exec与linker再分析地址问题。64位下的机制貌似很不一样…….
ps:elf不是精灵的意思么….之后根据linker写elf壳的时候就叫darkelf好了….
概述
由于考虑到对各种不同硬件和操作系统的适用性和扩展性,ELF(v1.2)规范在制 定时,把 ELF格式分为了三个层次。
第一层是基本的部分,即格式中通用的部 分,这部分在各种处理器架构和操作系统上都是相同的;
第二层是处理器的扩展部 分,这部分会因处理器架构的不同而不同,规范中只定义了针对 Intel i386架构的内容,针对其它的处理器的内容由处理器厂商自己提供;
第三层是操作系统的扩展 部分,这部分内容在不同的操作系统下面也可能是不同的,规范中只定义了针对 UNIX System V Release 4操作系统的内容。
90年代发布的ELF1.2标准,因此本篇参考该标准解析。由于该文件只是规范,并且有根据不同厂商扩展的部分,实际一切以现有的ARM64-android为分析源
文件类型
ELF文件的类型:可重定位文件(.o .a)、可执行文件、共享文件(.so)、转存文件
可重定位文件
用于与其它目标文件进行连接以构建可执行文件或动态链接库。可重定位文件就是常说的目标文件,由源文件编译而成,但还 没有连接成可执行文件。在 UNIX系统下,一般有扩展名”.o”。之所以称其为“可 重定位”,是因为在这些文件中,如果引用到其它目标文件或库文件中定义的符号 (变量或者函数)的话,只是给出一个名字,这里还并不知道这个符号在哪里,其具体的地址是什么。需要在连接的过程中,把对这些外部符号的引用重新定位到其 真正定义的位置上,所以称目标文件为“可重定位”或者“待重定位”的。 .a是.o的集合
共享目标文件
即动态连接库文件。它在以下两种情况下被使用:第一,在连接过程中与其它动态链接库或可重定位文件一起构建新的目标 文件;第二,在可执行文件被加载的过程中,被动态链接到新的进程中,成为运行 代码的一部分。
可执行文件
经过连接的,可以执行的程序文件。
文件格式概观
ELF文件根据用途不同可被看成的内容不同
用于构建程序或者用于运行程序
注意实际上elf文件只有文件头的位置是固定的,其它内容的位置都可以变化
ELF文件头
位于文件的开始处,包含有整个文件的结构信息。
节(section)
是专用于连接过程而言的,在每个“节”中包含有指令数据、符号数据、重 定位数据等等。
程序头表(program header table)
在运行过程中是必须的,在连接过程中是可选的,因为它的作用是告诉系统 如何创建进程的镜像。
节头表 (section header table)
包含有文件中所有“节”的信息。在连接视图中,“节头表”是必须存在 的,文件里的每一个“节”都需要在“节头表”中有一个对应的注册项,这个注册 项描述了节的名字、大小等等。
数据类型
无符号:
自定义 | 字节 |
---|---|
Elf_Byte | 1 |
Elf64_Quarter | 2 |
Elf64_Half | 4 |
Elf64_Word | 4 |
Elf64_Addr | 8 |
Elf64_Off | 8 |
Elf64_Xword | 8 |
有符号:
自定义 | 字节 |
---|---|
Elf64_Shalf | 4 |
Elf64_Sword | 4 |
Elf64_Sxword | 8 |
不管平台如何,x86还是arm。linux还是win。编译或者加载64位elf的时候ELF文件都是按这个类型大小定义的。因此只要保证头文件数据类型的大小一致性就能移植编译使用
该字长由ELF自己定义,与主机无关
ELF的字符集采用ASCII,支持其它编码格式
ELF文件头
文件头描述整个文件的结构信息,描述可能与文件中实际不同,具体处理需要参考具体实现源码-linker与exec。加壳时寻找其bug
ident
前16个字节用于识别最基本的意义。
ELF文件开始的这一部分的格式是固定并通用的,在所有平台上都一样。 所有处理器都可能用固定的格式去读取这一部分的内容,从而获知这个 ELF文件 中接下来的内容应该如何读取和解析。
定义ident数组索引功能的宏:
MAG0-3
魔数部分,用于标识ELF文件,这四个字节内容固定
实际结果就是这个
CLASS
这个字节用于指明文件的类型
实际结果为2
DATA
指明了目标文件中的数据编码格式
决定大端小端的。
实际为小端,因此arm64下android原生为小端编码-方便与机器
VERSION
指明 ELF文件头的版本
current:当前
实际为1
OSABI
指明可运行的ABI
实际并没使用…为0
ABIVERSION
实际为0
PAD
从 e-ident[EI-PAD]到 e-ident[EI-NIDENT-1]之间的7个字节目前暂时不使 用,留作以后扩展,在实际的文件中应被填 0补充,其它程序在读取 ELF文 件头时应该忽略这些字节。如果以后 ELF文件头的内容被扩展,这 9个字节 中有一些被使用起来的话,EI_PAD将被定义得更大
type
此字段表明本目标文件属于哪种类型。
ET-LOPROC ~ ET-HIPROC (0xff00 ~ 0xffff)这一范围内的文件类型是为特定 处理器而保留的,如果需要为某种处理器专门设定文件格式,可以从这一范围内选 取一个做为标识。
在以上已定义范围外的文件类型均为保留类型,留做以后可能的扩展。
这个实际上arm64android下不管so还是加不加pie的可执行文件都是3
machine
此字段用于指定该文件适用的处理器体系结构
实际这里的值是B7。应该是android自己定的…
version
此字段指明目标文件的版本
实际为1,可以根据目标文件的版本变动
entry
指明程序入口的虚拟地址,对于非可执行文件该值为0
实际上so与可执行都有这个值…用ida分析后发现该值指向的文件偏移量就是start函数地址。
文件中的虚拟地址应该都是相对文件0的偏移。
phoff
此字段指明程序头表(program header table)开始处在文件中的偏移量。如果没 有程序头表,该值应设为 0
这些偏移都是从0开始的,并且偏移处就是开始。如果把整个文件看成数组,file[off]就是目标的开始第一个字节,说成数组/文件的第几个字节就是off+1
数数从1开始数,数组从0开始标
shoff
此字段指明节头表(section header table)开始处在文件中的偏移量。如果没有节头表,该值应设为 0。
flags
此字段含有处理器特定的标志位。标志的名字符合”EF-machine-flag”的格式。
实际为0
ehsize
此字段表明 ELF文件头的大小,以字节为单位。
phentsize
此字段表明在程序头表中每一个表项的大小,以字节为单位。
phnum
此字段表明程序头表中总共有多少个表项。如果一个目标文件中没有程序头 表,该值应设为 0
shentsize
此字段表明在节头表中每一个表项的大小,以字节为单位。
shnum
此字段表明节头表中总共有多少个表项。如果一个目标文件中没有节头表, 该值应设为 0
shstrndx
节头表中与节名字表相对应的表项的索引。如果文件没有节名字表,此值应 设置为 SHN-UNDEF。
这个就是节名字表在节表的下标
节
节头表
在目标文件中可以包含很多“节”(section),所有这些“节”都登记在一张称为 “节头表”(section header table)的数组里。节头表的每一个表项是一个 Elf64-Shdr 结构,通过每一个表项可以定位到对应的节
elf文件头中给出了其表头位置、表项数量、表项大小
某些表项的索引值被保留,有特殊的含义。ELF文件的节头表中不会出现索引值为以下各值的表项
通常,目标文件中含有众多的“节”,“节”区是文件中大的部分,它们需要满足下列这些条件:
- 目标文件中的每一个节一定对应有一个节头(section header),节头中有对节的描述信息;但有的节头可以没有对应的节,而只是一个空的头。
- 每一个节所占用的空间是连续的。
- 各个节之间不能互相重叠。
- 在目标文件中,节与节之间可能会存在一些多余的字节,这些字节不属于任何节。
|
|
name
本节的名字。整个名字的字符串并不存储在这里,它仅是一个索引号,指向 “字符串表”节中的某个位置,那里存储了一个以’\0’结尾的字符串
有一些定义好的:
功能见下特殊节
type
本节的类型。
0号的表头项不表达实际的内容,只是一个空的表项
NULL
此值表明本节头是一个无效的(非活动的)节头,它也没有对应的节。本节头中的其它成员的值也都是没有意义的。
PROGBITS
此值表明本节所含有的信息是由程序定义的,本节内容的格式和含义都由程序来决定。
SYMTAB
静态符号表
一般来说,SHT_SYMTAB提供的符号用于在创建目标文件的时候编辑连接,在运行期间也有可能会用于动态连接。SHT-SYMTAB包含完整的符号表,它往往会包含很多在运行期间(动态连接)用不到的符号。所以一个目标文件可以再有一个 SHT_DYNSYM节,它含有一个较小的符号表,专门用于动态连接。
STRTAB
此值表明本节是字符串表。目标文件中可以包含多个字符串表节。
RELA
重定位节,含加数
HASH
哈希表,连接使用,便于快速找到符号
DYNAMIC
动态连接信息的节
NOTE
此值表明本节包含的信息用于以某种方式来标记本文件。
NOBITS
此值表明这一节的内容是空的,节并不占用实际的文件空间。
REL
重定位节,不含加数
SHLIB
保留值,未定义
DYNSYM
动态符号表
LOPROC
为特殊处理器保留的节类型索引值的下边界。
HIPROC
为特殊处理器保留的节类型索引值的上边界。
LOUSER
为应用程序保留节类型索引值的下边界。
HIUSER
为应用程序保留节类型索引值的下边界。
flags
本节的一些属性,由一系列标志比特位组成,各个比特定义了节的不同属性,当某种属性被设置时相应的标志位被设为1,反之则设为0。未定义的标志位被全部置0。
|
|
WRITE
如果此标志被设置,表示本节所包含的内容在进程运行过程中是可写的
ALLOC
如果此标志被设置,表示本节内容在进程运行过程中要占用内存单元。并不是所有节都会占用实际的内存,有一些起控制作用的节,在目标文件映射到进程空间时,并不需要占用内存。
EXECINSTR
如果此标志被设置,表示此节内容是指令代码。
MASKPROC
所有被此值所覆盖的位都是保留做特殊处理器扩展用的。
addr
如果本节的内容需要映射到进程空间中去,此成员指定映射的起始地址;如果不需要映射,此值为0。
即指定虚拟地址。
实际上该值和文件偏移可能一样可能不同,但该值也是相对基址0的,在虚拟地址中可以相对装入,在文件中节连续但在内存中节之间可能留有很大空隙
offset
指明了本节所在的位置,该值是节的第一个字节在文件中的位置,即相对于文件开头的偏移量。单位是字节。如果该节的类型为 SHT-NOBITS的话,表明这一节的内容是空的,节并不占用实际的空间,这时 sh_offset只代表一个逻辑上的位置概念,并不代表实际的内容。
size
指明节的大小,单位是字节。如果该节的类型为 SHT-NOBITS,此值仍然可能为非零,但没有实际的意义。
指明的是该节在内存与文件中的大小,但是可被 NOBITS表示为仅指明内存中的大小。
link
此成员是一个索引值,指向节头表中本节所对应的位置。根据节的类型不同,本成员的意义也有所不同。
info
此成员含有此节的附加信息,根据节的类型不同,本成员的意义也有所不同
对于某些节类型来说,sh-link和 sh-info含有特殊的信息:
addralign
此成员指明本节内容如何对齐字节,即该节的地址应该向多少个字节对齐。
保证addr%(2 addralign )=0 为0或1表示没有对齐要求
entsize
有一些节的内容是一张表,其中每一个表项的大小是固定的,比如符号表。对于这种表来说,本成员指定其每一个表项的大小。如果此值为 0则表明本节内容不是这种表格
特殊节
在ELF文件中有一些特定的节是预定义好的,其内容是指令代码或者控制信息。这些节专门为操作系统使用,对于不同的操作系统,这些节的类型和属性有所不同。
.开头表示为系统保留的。应用程序可以构造自己的节,但最好不要重名。节名不具有唯一性,可以重复。
比如但不限于:
bss
本节中包含目标文件中未初始化的全局变量。一般情况下,可执行程序在开始运行的时候,系统会把这一段内容清零。但是,在运行期间的 bss段是由系统初始化而成的,在目标文件中.bss节并不包含任何内容,其长度为 0,所以它的节类型为 SHT-NOBITS。
其也指明了size
comment
版本控制信息
可见其中包含了编译器信息,没有alloc表示不在内存中分配空间
data/data1
这两个节用于存放程序中被初始化过的全局变量。在目标文件中,它们是占用实际的存储空间的,与.bss节不同。
debug
本节中含有调试信息,内容格式没有统一规定。所有以”.debug”为前缀的节名 字都是保留的
dynamic
本节包含动态连接信息,并且可能有 SHF-ALLOC和 SHF-WRITE等属性。 是否具有SHF-WRITE属性取决于操作系统和处理器。
保存了动态连接的基本信息,依赖对象、动态连接符号表、动态连接重定位表等
dynstr
此节含有用于动态连接的字符串,一般是那些与符号表相关的名字。
里面含有动态连接符号的字串
dynsym
此节含有动态连接符号表
fini
此节包含进程终止时要执行的程序指令。当程序正常退出时,系统会执行这一节中的代码。
got
此节包含全局偏移量表。
hash
本节包含一张符号哈希表。
init
此节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进 入主函数之前执行这一节中的代码。so中含有也会用于初始化
interp
此节含有 ELF程序解析器的路径名。如果此节被包含在某个可装载的段中,那么本节的属性中应置 SHF-ALLOC标志位,否则不置此标志
line
本节也是一个用于调试的节,它包含那些调试符号的行号,为程序指令码与源文件的行号建立起联系。其内容格式没有统一规定。
note
注释节
plt
函数连接表
relname/relaname
这两个节含有重定位信息。如果此节被包含在某个可装载的段中,那么本节的属性中应置 SHF-ALLOC标志位,否则不置此标志。注意,这两个节的名字 中”name”是可替换的部分,执照惯例,对哪一节做重定位就把”name”换成哪一节 的名字。比如,.text节的重定位节的名字将是.rel.text或.rela.text。
重定位是把符号引用和符号定义连接在一起的过程。使程序知道该跳转到哪里去
|
|
一个“重定位节(relocation section)”需要引用另外两个节:一个是符号表节,一个是被修改节。在重定位节中,节头的sh-info和 sh-link成员分别指明了引用关系。不同的目标文件中,重定位项的r-offset成员的含义略有不同。
- 在重定位文件中,r-offset成员含有一个节偏移量。也就是说,重定位节本身描述的是如何修改文件中的另一个节的内容,重定位偏移量 (r-offset)指向了另一个节中的一个存储单元地址。
- 在可执行文件或共享目标文件中,r-offset含有的是符号定义在进程空间中的虚拟地址。可执行文件和共享目标文件是用于运行程序而不是构建程序的,所以对它们来说更有用的信息是运行期的内存虚拟地址,而不是某个符号定义在文件中的位置。
表明了从哪个符号表找到符号去哪里如何修改地址
offset
本数据成员给出重定位所作用的位置。对于重定位文件来说,此值是受重定位作用的存储单元在节中的字节偏移量;对于可执行文件或共享目标文件来说,此值是受重定位作用的存储单元的虚拟地址。
info
|
|
低32位表示重定位类型,高32位表示重定位符号在符号表的下标。使用的符号表用link表示,作用的段用info表示
比如:
GLOB-DAT: 这种重定位类型用于把指定的符号地址设置为一个全局偏移量表项。这种重 定位类型在符号与全局偏移量表项之间建立起了联系。
addend
本成员指定了一个加数,这个加数用于计算需要重定位的域的值
rodata/rodata1
本节包含程序中的只读数据,在程序装载时,它们一般会被装入进程空间中 那些只读的段中去。
shstrtab
本节是“节名字表”,含有所有其它节的名字,属于字符串表的一种,它包含所有节的名字。每一个节头的 sh-name成员应该是一个索引值,它指向节名字表中的一个位置,从这个位置开始到接下来第一个’null’字符为止的这个字符串,正是这个节的名字。
可见并不用分配内存空间
strtab
本节用于存放字符串,主要是那些符号表项的名字。如果一个目标文件有一个可装载的段,并且其中含有符号表,那么本节的属性中应该有SHF-ALLOC。
以null开头,该节种存放多个以null结尾的字符序列。
索引时提供的序号不是字符串的序号。如果把整个字符串节看做字节数组的话该序号是数组的下标。从下标到null为被指向的字串。因此可以引用每个字符串靠后的部分。
实际上so与可执行中没有看见这个节,这个节一般在可重定位文件中(.o.a)
symtab
本节用于存放符号表。如果一个目标文件有一个可载入的段,并且其中含有 符号表,那么本节的属性中应该有 SHF-ALLOC。
目标文件中的“符号表(symbol table)”所包含的信息用于定位和重定位程序中的符号定义和引用。目标文件的其它部分通过一个符号在这个表中的索引值来使用该符号。索引值从0开始计数,但值为0的表项(即第一项)并没有实际的意义,它表示未定义的符号。这里用常量STN-UNDEF来表示未定义的符号。
定义:
该结构体32位与64位排列不同。64位的是:411288;32位的是:444112。应该是为了保证地址对齐
符号表的首项全0。实际上so与可执行文件中并没有此节,该节主要用于静态连接的
name
符号的名字。但它并不是一个字符串,而是一个指向字符串表的索引值,在字符串表中对应位置上的字符串就是该符号名字的实际文本。如果此值为非0,它代表符号名在字符串表中的索引值。如果此值为0,那么此符号无名字。
info
符号的类型和属性。st-info由一系列的比特位构成,标识了“符号绑定 (symbol binding)”、“符号类型(symbol type)”和“符号信息(symbol infomation)” 三种属性。下面几个宏分别用于读取这三种属性值。
符号绑定(Symbol Binding)
4位:
LOCAL
表明本符号是一个本地符号。它只出现在本文件中,在本文件外该符号无效。所以在不同的文件中可以定义相同的符号名,它们之间不会互相影响。
GLOBAL
表明本符号是一个全局符号。当有多个文件被连接在一起时,在所有文 件中该符号都是可见的。正常情况下,在一个文件中定义的全局符号,一定是在其它文件中需要被引用,否则无须定义为全局。
WEAK
类似于全局符号,但是相对于STB-GLOBAL,它们的优先级更低。
多个可重定位文件链接时,同名的GLOBAL不许出现多次,但当GLOBAL存在时,同名弱符号可以存在但会被忽略。比其优先级高的还有COMMON符号
LOPROC-HIPROC
为特殊处理器保留的属性区间。
在符号表中,不同属性的符号所在位置也有不同,本地符号(STB-LOCAL)排在前面,全局符号(STB-GLOBAL/STB-WEAK)排在后面。
符号类型(Symbol Types)
|
|
NOTYPE
本符号类型未指定。
OBJECT
本符号是一个数据对象,比如变量、数组等。
FUNC
本符号是一个函数,或者其它的可执行代码。函数符号在共享目标文件中有特殊的意义。当另外一个目标文件引用一个共享目标文件中的函数符号时,连接编辑器为被引用符号自动创建一个连接表项。非STT-FUNC类型的共享目标符号不会通过这种连接表项被自动引用。
SECTION
本符号与一个节相关联,用于重定位,通常具有STB_LOCAL属性。
FILE
本符号是一个文件名符号,它具有STB-LOCAL属性,它的节索引值是 SHN-ABS。在符号表中如果存在本类符号的话,它会出现在所有 STB-LOCAL类符号的前部。
L-H
特殊处理器保留
other
本数据成员目前暂未使用,在目标文件中一律赋值为 0。
shndx
任何一个符号表项的定义都与某一个“节”相联系,因为符号是为节而定义,在节中被引用。本数据成员即指明了相关联的节。本数据成员是一个索引值,它指向相关联的节在节头表中的索引。在重定位过程中,节的位置会改变,本数据成员的值也随之改变,继续指向节的新位置。
有三个特殊值:
ABS
符号的值是绝对的,具有常量性,在重定位过程中,此值不需要改变。
COMMON
本符号所关联的是一个还没有分配的公共节,本符号的值规定了其内容的字节对齐规则,与sh-addralign相似。也就是说,连接器会为本符号分配存储空间,而且其起始地址是向st-value对齐的。本符号的值指明了要分配的字节数。
UNDEF
当一个符号指向第 1节(SHN-UNDEF)时,表明本符号在当前目标文件中未定义,在连接过程中,连接器会找到此符号被定义的文件,并把这些文件连接在一起。本文件中对该符号的引用会被连接到实际的定义上去。
value
符号的值。这个值其实没有固定的类型,它可能代表一个数值,也可以是一个地址,具体是什么要看上下文。
- 在重定位文件中,如果一个符号对应的节的索引值是 SHN-COMMON,st-value值是这个节内容的字节对齐数。
- 在重定位文件中,如果一个符号是已定义的,那么它的 st-value值 是该符号的起始地址在其所在节中的偏移量,而其所在的节的索引由st-shndx给出。
- 在可执行文件和共享库文件中,st-value不再是一个节内的偏移量,而是一个虚拟地址,直接指向符号所在的内存位置。这种情况下,st-shndx就不再需要了。
这样的设计是为了在不同类型文件中访问数据更有效
size
符号的大小。各种符号的大小各不相同,比如一个对象的大小就是它实际占用的字节数。
text
本节包含程序指令代码
段
要执行一个程序,系统要先把相应的可执行文件和动态连接库装载到进程空间中,这样形成一个可运行的进程的内存空间布局,也可以称它为“进程镜像”。
权限相同的节分别映射容易浪费页,因为映射必须以页为单位才可以方便进行权限管理、换入换出等操作(段式现往往用于在页式前划分空间),因此将权限相同的节合并为段一起映射,用于加载执行。
这也就是前面看节表时发现如下具有W权限的节明显新分配到了一个页
文件映射到这俩个段时邻界部分会被映射俩次,文件是紧凑连续的,并作为源,分页映射到内存会按文件中段的要求映射内存,因此邻界区会有一页的重复
总体上看,文件在内存中被拉长了,但每个段内的相对位置不会变,内存内段内某个位置-程序头指定VA=文件中的这个位置-程序头指定文件偏移
举例:
dynamic中的:pltgot指定的VA为0x11f98
根据段头表指定的分段位于7号 GNU Read-only After Relocation段。该段头指定了:
节头中指定了:
因此满足:0x11f98-0x11D80=0x1f98-0x1d80
因此程序头实际上就是指定了应该把文件中的那一部分映射到内存中的那一部分。其中文件的部分用文件偏移表示,内存中的部分用VA表示,文件偏移相对0,内存VA也是相对基地址的(默认为0)。
操作系统先将文件按程序头的规定映射到内存中(实际是形成页表),再按程序头中的内容解析整个elf文件,因为此时解析都是读取的内存中的数据(从文件取太慢太多次了)因此程序头中的内容按VA表示更方便定位。所以elf可执行与so中的地址数据都是VA的!!!!!!!!
具体看exec的源码分析,其中有几个段指明的部分是重复的,是为了指明信息,如动态段的位置、连接器的路径、栈信息等
程序头
一个可执行文件或共享目标文件的程序头表(program header table)是一个数组, 数组中的每一个元素称为“程序头(program header)”,每一个程序头描述了一个 “段(segment)”或者一块用于准备执行程序的信息。一个目标文件中的“段 (segment)”包含一个或者多个“节(section)”。程序头只对可执行文件或共享目标文件有意义,对于其它类型的目标文件,该信息可以忽略。在目标文件的文件头 (elf header)中,e-phentsize和 e-phnum成员指定了程序头的大小。
有些程序头的内容是描述进程内的段,而有的是给出一些辅助信息,并不直接成为段的内容
type
此数据成员说明了本程序头所描述的段的类型,或者如何解析本程序头的信息。
除非有特别要求,否则所有程序头的段类型域 p-type 都是可选项,不是必须存在的。在所有程序头都不指定段类型的情况下,程序头表中所有的表项都不代表任何特别的类型,而只是作为一种索引,表明其相应的段的大小和位置。
NULL
此类型表明本程序头是未使用的,本程序头内的其它成员值均无意义。具有此种类型的程序头应该被忽略。
LOAD
此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到内存中。段在文件中的大小是p-filesz,在内存中的大小是p-memsz。如果 p-memsz大于 p-filesz,在内存中多出的存储空间应填0补充,也就是说,段在内存中可以比在文件中占用空间更大;而相反,p-filesz 永远不应该比 p-memsz大,因为这样的话,内存中就将无法完整地映射段的内容。在程序头表中,所有 PT-LOAD类型的程序头按照 p-vaddr的值做升序排列。
DYNAMIC
此类型表明本段指明了动态连接的信息。
INTERP
本段指向了一个以”null”结尾的字符串,这个字符串是一个 ELF解析器的路径。这种段类型只对可执行程序有意义,当它出现在共享目标文件中时, 是一个无意义的多余项。在一个ELF文件中它多只能出现一次,而且必须出现在其它可装载段的表项之前。
NOTE
本段指向了一个以”null”结尾的字符串,这个字符串包含一些附加的信息。
SHLIB
该段类型是保留的,而且未定义语法。
PHDR
此类型的程序头如果存在的话,它表明的是其自身所在的程序头表在文件或内存中的位置和大小。这样的段在文件中可以不存在,只有当所在程序头表所覆盖的段只是整个程序的一部分时,才会出现一次这种表项,而且这种表项一定出现在其它可装载段的表项之前。
L-H
系统与处理器保留
offset
此数据成员给出本段内容在文件中的位置,即段内容的开始位置相对于文件开头的偏移量。
vaddr
此数据成员给出本段内容的开始位置在进程空间中的虚拟地址。
在被加载到进程空间里时,尽管“段”会被分配到一个不确定的地址,但是不同的段之间会有确定的“相对位置(relative position)”。也就是说,在目标文件中存储的两个段,它们的位置之间有多少偏移,当它们被加载到内存中时,这两个段的位置之间仍然保持这么大的偏移(距离)。映射在不同进程中的同一so段间距离相等。
文件中写的虚拟地址与实际内存中的虚拟地址之差为基地址。基地址计算方法见源码分析
paddr
此数据成员给出本段内容的开始位置在进程空间中的物理地址。对于目前大多数现代操作系统而言,应用程序中段的物理地址事先是不可知的,所以目前这个成员多数情况下保留不用,或者被操作系统改作它用。
filesz
此数据成员给出本段内容在文件中的大小,单位是字节,可以是 0。
memsz
此数据成员给出本段内容在内容镜像中的大小,单位是字节,可以是 0。
flags
此数据成员给出了本段内容的属性。
实际的权限依据系统的内存管理器处理,可能比给的权限大
align
对于可装载的段来说,其 p-vaddr和 p-offset的值至少要向内存页面大小对 齐。此数据成员指明本段内容如何在内存和文件中对齐。如果该值为 0或 1,表明 没有对齐要求;否则,p-align应该是一个正整数,并且是 2 的幂次数。p-vaddr和 p-offset在对 p-align取模后应该相等。
段内容
一个段可以包含多个节,方便装载。程序头只关心段
注释段NOTE
NOTE的段往往会包含类型为 NOTE的节,NOTE节可 以为目标文件提供一些特别的信息,用于给其它的程序检查目标文件的一致性和兼容性。
注释节中包含三种信息:名字 (name/namesz)、类型(type)和描述(desc/descsz)。
名字用于区别不同的信息提供者, 比如不同的厂商,”ABC Computer Company”或”XYZ Computer Company”。
一个信息提供者可能会提供很多种信息,为了区别,把信息分类,即给每一种 信息加一个 ID,或者说是类型 type。
有了 name和 type的约束,desc的内容才有意义,才知道它所描述的是什么。
程序解析器/动调连接器INTERP
这个串指明了一个 ELF程序解析器,系统会转去初始化该解析器的进程镜像。也就是,在这时系统会暂停原来的工作,不是用待执行文件的段内容去初始化进程空间,而是把进程空间暂时“借”给解析器程序使用。然后,解析器程序将从系统手中接过控制权继续执行。
在 Intel架构下,系统中自带一种符合 ELF规范的程序解析器: /usr/lib/libc.so.1。
当创建一个可执行文件时,如果依赖其它的动态连接库,那么连接编辑器会在可执行文件的程序头中加入一个 PT-INTERP项,告诉系统这里需要使用动态连接器。
通过dynamic段取到需要的so,在程序加载到内存执行前完成加载so并重定位。
动态段DYNAMIC
如果一个目标文件参与动态连接的话,它的程序头表中一定会包含一个类型为PT-DYNAMIC的表项,其所对应的段称为动态段(dynamic segment),段的名字为.dynamic。动态段的作用是提供动态连接器所需要的信息,比如依赖于哪些共享目标文件、动态连接符号表的位置、动态连接重定位表的位置等等。
这个动态段中包含有动态节:
对于每一项,tag控制着对un的解析
tag
|
|
图:dynamic-so
图:dynamic-exe
NULL
用于标记DYNAMIC数组的结束。
NEEDED
此元素指明了一个所需的库的名字。不过此元素本身并不是一个字符串,它是一个指向由”DT-STRTAB”所标记的字符串表中的索引,在表中,此索引处是一个以’null’结尾的字符串,这个字符串就是库的名字。在动态数组中可以包含若干个此类型的项,这些项出现的相对顺序是不能随意调换的。
PLTRELSZ
此元素含有与函数连接表相关的所有重定位项的总大小,以字节为单位。如果数组中有DT-JMPREL项的话,DT-PLTRELSZ也必须要有。
PLTGOT
此元素包含与函数连接表或全局偏移量表相应的地址。
HASH
此元素含有符号哈希表的地址。
STRTAB
此元素包含字符串表的地址,此表中包含符号名、库名等等。
SYMTAB
此元素包含符号表的地址。
RELA
此元素包含一个重定位表的地址。就是前面的Rela,表明了从哪个符号表找到符号去哪里如何修改地址
RELASZ
此元素持有DT-RELA相应的重定位表的大小,以字节为单位。
RELAENT
此元素持有DT-RELA相应的重定位表项的大小,以字节为单位。
STRSZ
此元素持有字符串表的大小,以字节为单位。
SYMENT
此元素持有符号表项的大小,以字节为单位。
INIT
此元素持有初始化函数的地址。
FINI
此元素持有终止函数的地址。
SONAME
此元素持有一个字符串表中的偏移量,该位置存储了一个以’null’结尾的字符串,是一个共享目标的名字。相应的字符串表由DT-STRTAB指定。
自己的名字
RPATH
此元素持有一个字符串表中的偏移量,该位置存储了一个以’null’结尾的字符串,是一个用于搜索库文件的路径名。相应的字符串表由DT-STRTAB指定。
SYMBOLIC
在共享目标文件中,此元素的出现与否决定了动态连接器解析符号时所用的算法。先搜素可执行文件还是库文件
REL
此元素与DT-RELA相似,只是它所指向的重定位表中,“加数”是隐含的而不是显式的。与静态中的相同
RELSZ
此元素持有DT-REL相应的重定位表的大小,以字节为单位。
RELENT
此元素持有DT-REL相应的重定位表项的大小,以字节为单位。
PLTREL
本成员指明了函数连接表所引用的重定位项的类型。d-val成员含有DT-REL或DT-RELA。函数连接表中的所有重定位类型都是相同的。
DEBUG
本成员用于调试,格式未明确定义。
TEXTREL
如果此元素出现的话,在重定位过程中如果需要修改的是只读段的话,连接编辑器可以做相应的修改。
JMPREL
此类型元素如果存在的话,其d-ptr成员含有与函数连接表单独关联的重定位项地址。把多个重定位项分开可以让动态连接器在初始化的时候忽略它们,当然前提条件是“后期绑定”是激活的。如果此元素存在的话,DT-PLTRELSZ和DT-PLTREL也应该出现。
BIND-NOW
如果此元素存在的话,动态连接器必须在程序开始执行以前,完成所有包含此项的目标的重定位工作。如果此元素存在,即使程序应用了“后期绑定”,它对于此项所指定的目标也不适用,动态连接器仍需事先做好重定位。
L-H
处理器保留
共享目标的依赖关系
静态连接下,连接器提取库的成员拷贝到输出文件中。动态连接下需要找到so库并添加到进程中。详见linker源码分析
当动态连接器为一个目标文件创建内存段的时候,动态结构中的 DT-NEEDED 项会指明所依赖的库,动态连接器会连接被引用的符号和它们所依赖的库,这个过程会反复地执行,直到一个完整的进程镜像被构建好。当解析一个符号引用的时候,动态连接器以一种“广度优先”的算法来查找符号表。就是说,动态连接器首先查找可执行程序自己的符号表,然后是 DT-NEEDED项所指明的库的符号表, 再接下来是下一层依赖库的符号表,依次下去。共享目标文件必须是可读的,其它权限没有要求。
共享库加载后会记录其名字,只引用一次,名字可以是SONAME也可以是共享文件的路径名。
寻找时,如果共享库名字中含有/,则直接把字符串作为路径名,如果没有,则按如下顺序查找:
- RPATH可能给出了一个含有一系列目录名的字符串,各目录名以冒号”:”相隔。在这些目录下依次查找
- 进程的环境变量中会有一个
LD_LIBRARY_PATH
变量,它也含有一个目录名列表,各目录名以冒号”:”相隔,各目录名列表以分号”;” 相隔。 - 如果如上两组路径都无法找到所要的库,动态连接库就搜索/usr/lib。
全局偏移量表got
该表不通过处理器架构格式与解析方式不同。
可执行文件和共享目标文件有各自的全局偏移量表
详细分析见下文
函数连接表plt
该表不同处理器下实现不同
可执行文件和共享目标文件有各自的函数连接表。
详细分析见下文
解析符号
具体见linker分析
- 在一开始创建程序内存镜像的时候,动态连接器把全局偏移量表中的第 2和第 3个表项设为特定值。下面的步骤中会解析所设置的值。
- 如果函数连接表是位置独立的,全局偏移量表的地址必须存储在 %ebx中。进程空间中的每一个共享目标文件都有自己的函数连接表,每一个表都是用于本文件内的函数调用。那么,主调函数就要负责在调用函数连接表项之前设置全局偏移量表。
- 这里做个示例,假设程序要调用函数 name1,与之相应的函数连接表是.PLT1。
- 第一条指令跳转到 name1所在全局偏移量表项中的地址。一开始, 全局偏移量表中持有的是”push1”指令的地址,而不是”name1”的地址。
- 接下来,程序把一个重定位偏移量压入堆栈。重定位偏移量是一个 32位的非负数,它指向重定位表内的一项。被指定的重定位项类型为 R-386-JMP-SLOT,它的”offset”指定了前一个”jmp”指令所用到的一个全局 偏移表项。重定位项也包含一个符号表索引,为动态连接器指明了正被引用 的是什么符号,在本例是即”name1”。
- 把重定位偏移量压栈以后,程序就跳到.PLT0,即函数连接表的第一项。”push1”指令把全局偏移量表的第 2项(got-plus-4 or 4(%ebx))压入栈 顶,因此给了动态连接器一个字的确认信息。接下来,程序跳转到全局偏移 量表的第 3项(got-plus-8 or 8(%ebx))中的地址,控制权就又交给了动态连接器。
- 动态连接器接过控制权后,它弹出栈顶数据,查找指定的重定位项,找到符号的值,把”name1”真正的地址存储在全局偏移量表项中,并把控制权传给指定的目标。
- 接下来,函数连接表会把控制权直接转到 name1,不需要动态连接 器再次介入。也就是说,.PLT1处的 jmp指令会跳转到 name1。
这就是后期绑定
哈希表
图hash
不是规范的一部分,可能有不同实现
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,说明这个符号表中并不含有此符号。
初始化和终止函数
当动态连接器构建好进程镜像,并完成重定位后,每一个共享目标都有机会执 行一些初始化代码。所有共享目标的初始化都发生在程序开始执行前。
依赖需要先初始化,动态连接器保证其只调用一次,动态连接器只负责共享目标文件中的初始化和终止化,并不负责可执行文件。
示例
一个so的解析
源:
|
|
结果:
|
|
一个可执行文件的解析
源:
|
|
结果:
|
|
实现一个解析器
github:
https://github.com/imbaya2466/XFile
参考
程序员的自我修养
ELF格式解析-v1.2 赵 凤 阳
ELF 文件格式分析 -滕启明
ELF for the ARM® 64-bit Architecture (AArch64)
ELF for the ARM® Architecture
CSAPP