背景

        一个程序要想运行,首先要加载到内存中,程序的pc指针指向内存中的代码,代码在执行的时候会使用内存中的数据。所以ELF文件主要包含代码和数据。
数据可以分为两类:

  1. 静态数据。
  2. 动态数据。

什么是动态数据?,动态数据程序运行过程中产生,在堆或者栈上分配内存。而静态数据则不然,静态数据在代码编译完成后,就应该确定使用的地址和空间。
静态数据又分为两种,只读数据和可读写数据。

        一个库要想被其他程序调用,还要约定一种方式,使调用者可以找到被调用者的函数地址,所以运行程序还要保存动态链接相关的信息。另外程序还要告知连接器,如何将代码和数据加载到内存中,这就是程序的加载信息。ELF文件格式是类unix系统中常见的程序和库的文件格式,所以ELF要包含上述信息(代码,数据,加载信息和链接信息)。ELF怎么来组这这些信息呢,下面是ELF的数据格式图。

解析emf文件 elf文件解析_解析emf文件

程序的信息都保存在section(节)中,文件通过section header table(节头表)来描述每个节的信息,这些信息包括如下几项:

  • Name:节的名称。
  • Type:节的类型。
  • Addr:节映射到虚拟空间的位置(这个信息一般不用)。
  • Off :节在ELF文件中的偏移。
  • Size:节的大小。
  • ES Flg Lk Inf Al。

不同类型的节有不同的作用。

程序的加载信息则保存在program header table(程序段表)中,ELF被加载到内存中后用segment(段)表示,段的描述信息:

  • Type: 段类型。
  • Offset: 段对应的ELF文件偏移(也就是说该段用于映射ELF文件的偏移地址)。
  • VirtAddr: 段在虚拟内存中的偏移地址(注意:在PIC模式下段不一定被加载到该地址,可能会与该地址有个偏移)。
  • PhysAddr:段被加载到的物理地址(在mmu系统下不使用该地址)。
  • FileSiz:该段映射的文件大小。从offset—>(offset + FileSiz) 映射到内存的VirtAddr—>(VirtAddr + FileSiz)
  • MemSiz: 该段占用的内存大小(MemSiz >= FileSiz)。
  • Flg: 读写权限,该段在内存中是否具有读写执行权限。
  • Align:段的对齐要求。

程序的链接信息也保存在section(节)中,关于链接信息我们可以分为两部分,一部分为库导出的函数/变量,另一部分则是库依赖的其他库的函数/变量。对于描述导出的函数/变量是比较容易的,因为库自身知道函数/变量的名称和要加载到的内存位置,这部分信息(名字和地址)保存在类型为DYNSYM的节中。 对于要导入的函数/变量,在类型为RELA/REL的节中保存了引用其他库的函数/变量在符号表中的索引,符号表中则保存该函数/变量的名称,在类型为DYNAMIC的节中保存了程序依赖的库名称,链接器在链接的时候会去依赖的库中根据函数/变量的名称去寻找该函数/变量被加载到内存的位置,将找到的函数/变量地址写入到类型为PROGBITS(一般名称为.got的节) 节对应的位置,之后再使用函数/变量就直接从got表中获取地址即可。

ELF文件的映射及详解

解析emf文件 elf文件解析_加载_02


ELF的加载主要的依据为程序头表,程序头表中每一项描述的段(segment)的信息,一个段可能包含多个section(节)。

我们以上图中的程序头表的段信息来举例:

  • PHDR:程序头的作用主要用于加载的验证。
  • LOAD: load类型的头主要指导连接器如何分配内存和如何把elf映射到内存中,以及内存的权限。
  • DYNMIC: 这个头描述了动态链接信息被加载到内存的位置。也就是图中.dynmic在内存中的位置。
    对于上图两个LOAD段是比较重要的,第一个LOAD段对应ELF从头部到.rodata节结尾的部分,这部分被加载到内存后,设置权限为可读和可执行的。上图是arm gcc编译出来的elf程序,这部分其实有些内容是不需要执行权限的,说明arm gcc控制还是比较粗犷的。
    第二个LOAD段对应ELF文件从.data.rel.ro.local节开头到.bss节结尾的部分,这部分被加载到内存后,设置权限为开读和可写的。为什么右边内存中的读写区域会大于左边文件的区域呢,是因为.bss段只保存了全局为初始化数据(这些变量默认初值为0)需要的空间,而没有在文件中分配该空间,所以LOAD段会为这部分空间分配内存,填充内容为0。
    另外ELF的节头表和一些节是不需要加载到内存的。

动态链接

关于动态链接需要知道如下几点即可:

  1. 程序的导出符号和导出符号的地址在符号表节,符号名称在字符串表中。
  2. 程序依赖的库信息保存在DYNMIC类型的节中。
  3. 程序的函数/变量信息保存在类型为REL/RELA的节中,该节保存了该函数/变量在符号表中的位置,以及重定位的方法以及重定位后的函数地址保存在类型为PROGBIT节中的位置(一般在可读写内存区域的节如.got,.got.plt节,因为要后填写析函数/变量地址,所以该区域为读写区域)。
  4. 类型为HASH的节用于加快根据函数名称寻找函数地址的过程,也就是函数解析的过程。

关于链接脚本

EFL_load图中描述的是gcc连接器默认的链接脚本生成的ELF文件,节的名称都是gcc定义的。所以我们只要能满足elf连接器的要求,elf的节名称组织方式完全可以自己定义,也就是链接脚本的作用,我们甚至可以自己写加载器。