ELF是Executable and Linkable Format的缩写,Linkable:可链接,表示ELF文件是链接工作的重要参与者,Excutable:可执行,表示ELF将参与程序的执行工作,包括二进制程序的运行和.so文件的加载。
图1.1 ELF文件中的Linking View和Execution View示意图
通常情况下我们常见的ELF文件有:c/c++文件编译后得到的.o文件,动态库.so文件,.o和.so文件链接后得到的二进制可执行文件也是ELF文件。Art生成的可执行文件.oat是一种Android定制的ELF文件,所以理解ELF是理解oat文件的基础。
Linking view
如图1.2所示,ELF文件的开头是一个文件头,该文件头描述的是整个文件的文件属性,包括文件是否可以执行,是静态链接还是动态链接以及如果是可执行文件的话,入口地址在哪,目标硬件,操作系统等信息。(图1.1并没有把所有的segment都写进去)
图1.2 swap对应的ELF头文件信息
对于一般c语言的编译后执行语句都编译成机器码保存在.text段,对于已经初始化的全局变量和局部静态变量都保存在.data段,对于未初始化的全局变量和局部静态变量一般都放在.bss段中。.bss段没有内容,在文件中也不占据空间,只是给未初始化的全局变量和局部静态变量预留位置。.rodata包含只读数据的信息,比如printf里的字符串就属于这一类。.symtab段是ELF中存储符号表的段,主要用于编译链接,也可以参与动态库的加载。此外ELF中还另外专门定义了一个段.dynsym用来存储仅用与动态链接的符号信息。
从linking view的角度来看,程序源代码被编译之后主要分为程序指令(只读区域,如.text)程序数据(读写区域,如.data和.bss)。那么要将指令和数据分开放?原因有以下三个:1.提高安全性,程序数据在运行的时候会被修改,而如果把程序指令也设置可读写,那么就会增加不安全性;2.提高CPU读取缓存的效率;3.节约空间,当程序中运行着该程序的多个副本时,其指令都是一样的,内存只需要保存一份该程序的只读部分,其他每个部分的进程只需要另外保存读写区域的内容即可,这样可以大量节省内存。
Execution view
Execution view中的ELF需要包含Program Header Table,其内容与linking view的section不同,在可执行情况下,segment是用来描述一个即将被操作系统装在到虚拟内存空间的可执行文件。
Linux 在装载可执行文件的时候,会将这些 segment 映射到进程的地址空间中。Linux 将进程虚拟地址空间中的一个 segment 叫做虚拟内存区域 (Virtual memory area, VMA)。虚拟地址空间存储区的分布如图1.3所示:
图1.3 二进制文件在64为虚拟地址空间中的布局
ELF 将相同或者相似属性的 section 合并为一个 segment 并映射到一个 VMA中,是为了减少页面内部碎片,以节省内存空间的使用。因为在有了虚拟存储机制以后,装载的时候采用页映射的方式。Intel 系列的处理器,页尺寸最小是 4096个字节,也就是 4KB。当写的程序很小的时候,每个 section 可能只有几十或者几百个字节,如果每个 section 都占用一个页的话,对内存的浪费是海量的。所以在将目标文件链接成可执行文件的时候,链接器会尽量把相同或相似权限属性的section 分配在同一空间,在程序头表中,将一个或多个属性类似的 section 合并为一个 segment,然后在装载的时候,将这个 segment 映射到进程虚拟地址空间中的一个VMA中。
图1.4中上半部分为swap函数的program Headers表,下半部分为segment到section的映射关系。
图1.4 swap函数对应的section
表1.1 p_type的含义
总结:基于Linking view的方式:根据Section Header Table解析Section,这个时候按照基于文件的偏移量来读取不同的Section的内容,再根据Section中对应的数据来解析。
基于Execution view的方式,先打开文件,然后逐个将PH Table中的segment通过mmap映射到对应的虚拟内存空间,然后遍历segment的内容。
如果觉得《动态链接一:ELF文件格式》对你有帮助,请点赞、收藏,并留下你的观点哦!