4.2.3 Execution View下的ELF
图4-17 Program Header Table数据结构
Execution View中ELF必须包含Program Header Table(以后简称PH Table)。和Section Header Table相对应,PH Table描述的是segment的信息。图4-17所示为它的数据结构。
图4-17中Elf64_Phdr各个字段的介绍如下。
·p_type:segment的类型。
·p_flags:segment标记符。
·p_offset:该segment位于文件的起始位置。
·p_vaddr:该segment加载到进程虚拟内存空间时指定的内存地址。
·p_paddr:该segment对应的物理地址。对于可执行文件和动态库文件而言,这个值并没有意义,因为系统用的是虚拟地址,不会使用物理地址。
·p_filesz:该segment在文件中占据的大小,其值可以为0。因为segment是由section组成的,有些section在文件中不占据空间,比如前文提到的.bss section。
·p_memsz:该segment在内存中占据的空间,其值可以为0。
·p_align:segment加载到内存后其首地址需要按p_align的要求进行对齐。
下面通过一个例子来分析PH Table,示例依然使用图4-4所示的代码。通过"make exe"命令得到main.out可执行文件,接着使用"readelf-l"查看ELF文件中的PH Table,结果如图4-18所示。
图4-18中:
·首先显示的是PH Table的内容,该Table中每项元素描述了一个Segment。
·然后显示的是Section到Segment的映射关系。ELF文件物理上并不包含所谓的Segment,Segment其实是一个或多个Section按一定映射关系组合而来的。
接下来介绍一些更详细的和Segment有关的知识,首先是Elf64_Phdr中的p_type和p_flags。
4.2.3.1 p_type与p_flags介绍
p_type说明Segment的类型,其取值如表4-4所示。
图4-18 readelf-l main.out
表4-4 p_type取值和含义
下面来看pt_flags的取值。它和Segment在内存中的访问权限有关,如图4-19所示。
图4-19 pt_flags的取值说明
结合图4-18,我们看看其中的索引2号和3号Segment的读写权限,
·索引号为2的Segment:先观察它的映射关系,其Section组成包括.text,而.text section包含了程序的机器码。也就是说,该Segment是用于执行的,所以它的访问权限为只读和可执行。
·索引号为3的Segment:其Section组成包含.bss section,我们可以大胆猜测这个Segment应该是包含程序数据的。所以,它的访问权限为可读写,但是不能为可执行。
接下来重点看看Section到Segment的映射。
4.2.3.2 Section到Segment映射
Section到Segment的映射关系其实非常简单,区间落在[p_offset,p_offset+p_filesz]范围内的Section属于同一个Segment。本节将挑选几个代表性的Segment来看看Segment和Section的映射关系。
先来看PH Table的第一项,它描述的是PH Table自己的信息。以本例而言,其p_offest=0x40,大小是p_filesz=0x1F8,没有对应的映射section。这和ELF文件头结构里用来描述PH Table信息的几个成员取值完全一样,读者可对比图4-20。
图4-20 readelf-h main.out
在图4-20中:
·Start of program headers为64,它表示PH Table位于文件64(0x40)字节处。
·PH Table每项元素长56字节,一共9个元素(总长度为56×9=504,即0x1F8)。
再来观察有着最为复杂映射关系的索引号为2的Segment,它的映射关系可由图4-21来解释。
图4-21左边的内容来自于ELF文件的section信息,可通过"readelf--sections main.out"得到。图中箭头上的数字代表各模块在文件中的偏移量。图4-21右边浅色部分为PH Table[2]的值。通过比较可发现,二者在文件映射位置上是完全匹配的。
提示 注意,图4-21中.eh_frame和.eh_frame_hdr之间有空白区域,那是因为Section有字节对齐要求,空白区域用作对齐。映射到内存之后,这些空白区域在内存中都填0。
图4-21 PH Table[2]元素的映射关系
4.2.3.3 Execution View小结
在系统研究ELF文件之前,笔者也是经常接触它们,尤其是可执行文件和动态库文件。那时对ELF文件内部结构的了解也仅限于诸如.text section存储了指令,.bss section包含初值为0的数据之类的事情上。并且,最重要的问题是,笔者都是从Execution View的角度来看待ELF,并没有深入去了解Section的类型和作用。而通过本节的学习,读者会深刻感受到:
·ELF文件真正核心的内容其实是包含在一个个的Section中。了解Section的类型、内部对应的数据结构及功能对于掌握ELF有着至关重要的作用。
·当ELF文件加载到内存中时,ELF文件不同的Section会根据PH Table的映射关系映射到进程的虚拟内存空间中去。这时的Section就会以Segment的样子呈现出来。
另外,结合Linking View和本节的内容可知,要实现一个ELF文件的解析有两种方式。
·基于Linking View的方式:即根据Section Header Table解析Section。这时只要按基于文件的偏移量来读取不同Section的内容,再根据Section对应的数据结构来解析它就可以。这种方式适用于类似readelf这样的工具。
·基于Execution View的方式:即先打开文件,然后逐个将PH Table(Program Header Table)中的Segment映射到(利用mmap系统调用)对应的虚拟内存地址(p_vaddr)上去。然后就可以遍历Segment的内容。不过,由于Segment可由多个Section组成,在解析这样的Segment之时,不免还是需要借助Section Header Table。这种方法比基于Linking View的方式麻烦一些,但Android里的oatdump工具就是用这种方法解析oat文件的。
俗话说学以致用,现在我们将利用所学知识来研究现实世界中一个常见的案例。