Linux的二进制表示格式—ELF

之前在解决项目中关于解析core文件中,了解了关于ELF的相关知识,当时还处于萌新(现在还处于萌新状态)对于ELF格式那是一脸懵,今天就对ELF做一个简单的了解。

ELF

首先一个文本文件只有经过编译、链接形成一个可执行文件,也就是0、1代码,才能被硬件设备所识别。如下图所示:
image

其中,Linux下二进制的程序有这个严格的格式,这个格式就叫做ELF,全称Executeable and Linkable Format,可执行与可链接格式。
这个格式会根据编译的结果不同,分成不同的格式。

ELF的第一个格式 -- 可重定位文件

在编译的时候,先做预处理工作(如宏展开、头文件嵌入到正文等),之后就是真正的编译过程,最终编译成.o文件,这就是ELF的第一种类型,可重定位文件(Relocatable File)

这个文件格式如下:
image

ELF 文件的头是描述整个文件的。这个文件格式在内核中有定义,分别是struct elf32_hdrstruct elf64_hdr

在编译好的二进制文件中,存在着代码、还有一些局部变量、静态变量等section。

  • .text:放编译好的二进制可执行代码

  • .data:已经初始化好的全局变量

  • .rodata:只读数据,例如字符串常量、const的变量

  • .bss:未初始化全局变量,运行时会置0

  • .symtab:符号表,记录的则是函数和变量

  • .strtab:字符串表、字符串常量和变量名

这些节的元数据也需要有一个地方保存,就是最后的节头部表(Section Header Table)。在这个表里,每一个section都有一项,在代码里也有定义struct elf32_shdrstruct elf64_shdr

为啥叫重定位呢?
因为一个.o中的函数将来被谁调用、在哪调用都是未知,.o里面的位置也就不确定了,但是又必须得是可重定位的,因为它将来是要做函数库的,哪里需要哪里搬 ,就需要重新定位这些代码、变量的位置。

在ELF section段中,有的section, 如.rel.text、.rel.data,就与重定位有关。

会在.rel.text里面标注,这个函数需要重兴定位

另,要想让某个函数作为库文件被重用,不能以.o的形式存在,而是要形成库文件,最简单的类型是静态链接库.a文件(Archives),仅仅将一系列对象文件(.o)归档为一个文件,使用a r 创建
如:

ar cr libstaticprocess.a process.o

虽然这个.a 里面只有一个.o,但实际上是可以有多个.o,当有程序要使用这个静态链接库的时候,会将.o文件提取出来,连接到程序中。

gcc -o staticcreateprocess createprocess.o -L. -lstaticprocess

在这个命令里,-L 表示在当前目录下找.a 文件,-lstaticprocess 会自动补全文件名,比如加前缀 lib,后缀.a,变成 libstaticprocess.a,找到这个.a 文件后,将里面的 process.o 取出来,和 createprocess.o 做一个链接,形成二进制执行文件 staticcreateprocess。

这个链接的过程,重定位就起作用了,原来 createprocess.o 里面调用了 create_process 函数,但是不能确定位置,现在将 process.o 合并了进来,就知道位置了。

形成的二进制文件叫可执行文件,是 ELF 的第二种格式。

ELF的第二种格式 -- 可执行文件

格式如下:
image

这个格式和.o 文件大致相似,还是分成一个个的 section,并且被节头表描述。只不过这些 section 是多个.o 文件合并过的。但是这个时候,这个文件已经是马上就可以加载到内存里面执行的文件了,因而这些 section 被分成了需要加载到内存里面的代码段、数据段和不需要加载到内存里面的部分,将小的 section 合成了大的段 segment,并且在最前面加一个段头表(Segment Header Table)。在代码里面的定义为 struct elf32_phdr 和 struct elf64_phdr,这里面除了有对于段的描述之外,最重要的是 p_vaddr,这个是这个段加载到内存的虚拟地址。

在 ELF 头里面,有一项 e_entry,也是个虚拟地址,是这个程序运行的入口。
当程序运行起来之后,就是下面这个样子:

# ./staticcreateprocess
# total 40
-rw-r--r--. 1 root root 1572 Oct 24 18:38 CentOS-Base.repo
......

静态链接库一旦链接进去,代码和变量的 section 都合并了,因而程序运行的时候,就不依赖于这个库是否存在。但是这样有一个缺点,就是相同的代码段,如果被多个程序使用的话,在内存里面就有多份,而且一旦静态链接库更新了,如果二进制执行文件不重新编译,也不随着更新。

因而就出现了另一种,动态链接库(Shared Libraries),不仅仅是一组对象文件的简单归档,而是多个对象文件的重新组合,可被多个程序共享。

ELF的第三种格式 -- 动态链接库

gcc -shared -fPIC -o libdynamicprocess.so process.o

当一个动态链接库被链接到一个程序文件中的时候,最后的程序文件并不包括动态链接库中的代码,而仅仅包括对动态链接库的引用,并且不保存动态链接库的全路径,仅仅保存动态链接库的名称。

gcc -o dynamiccreateprocess createprocess.o -L. -ldynamicprocess

当运行这个程序的时候,首先寻找动态链接库,然后加载它。默认情况下,系统在 /lib 和 /usr/lib 文件夹下寻找动态链接库。如果找不到就会报错,我们可以设定 LD_LIBRARY_PATH 环境变量,程序运行时会在此环境变量指定的文件夹下寻找动态链接库。

# export LD_LIBRARY_PATH=.
# ./dynamiccreateprocess
# total 40
-rw-r--r--. 1 root root 1572 Oct 24 18:38 CentOS-Base.repo
......

动态链接库,就是 ELF 的第三种类型,共享对象文件(Shared Object)。
基于动态链接库创建出来的二进制文件格式还是 ELF,但是稍有不同。
首先,多了一个.interp 的 Segment,这里面是 ld-linux.so,这是动态链接器,也就是说,运行时的链接动作都是它做的。

另外,ELF 文件中还多了两个 section,

  • 一个是.plt,过程链接表(Procedure Linkage Table,PLT)
  • 一个是.got.plt,全局偏移量表(Global Offset Table,GOT)。

它们是怎么工作的,使得程序运行的时候,可以将 so 文件动态链接到进程空间的呢?
dynamiccreateprocess 这个程序要调用 libdynamicprocess.so 里的 create_process 函数。由于是运行时才去找,编译的时候,压根不知道这个函数在哪里,所以就在 PLT 里面建立一项 PLT[x]。这一项也是一些代码,有点像一个本地的代理,在二进制程序里面,不直接调用 create_process 函数,而是调用 PLT[x]里面的代理代码,这个代理代码会在运行的时候找真正的 create_process 函数。

去哪里找代理代码呢?这就用到了 GOT,这里面也会为 create_process 函数创建一项 GOT[y]。这一项是运行时 create_process 函数在内存中真正的地址。
如果这个地址在 dynamiccreateprocess 调用 PLT[x]里面的代理代码,代理代码调用 GOT 表中对应项 GOT[y],调用的就是加载到内存中的 libdynamicprocess.so 里面的 create_process 函数了。
但是 GOT 怎么知道的呢?对于 create_process 函数,GOT 一开始就会创建一项 GOT[y],但是这里面没有真正的地址,因为它也不知道,但是它有办法,它又回调 PLT,告诉它,你里面的代理代码来找我要 create_process 函数的真实地址,我不知道,你想想办法吧。

PLT 这个时候会转而调用 PLT[0],也即第一项,PLT[0]转而调用 GOT[2],这里面是 ld-linux.so 的入口函数,这个函数会找到加载到内存中的 libdynamicprocess.so 里面的 create_process 函数的地址,然后把这个地址放在 GOT[y]里面。下次,PLT[x]的代理函数就能够直接调用了。

参考:进程

posted @ 2022-11-03 00:13  牛犁heart  阅读(875)  评论(0编辑  收藏  举报