在 Linux 下面,二进制的程序也要有严格的格式,这个格式我们称为ELF(Executeable and Linkable Format,可执行与可链接格式)。这个格式可以根据编译的结果不同,分为不同的格式。
一般代码的编译过程,是这么一个流程,如右所示
首先源代码会编辑成 .o
文件,这就是 ELF 的第一种类型,文件格式如右所示
<aside> 💡 局部变量由运行时分配在栈上,所以这上面没有
</aside>
这些节的元数据信息也需要有一个地方保存,就是最后的节头部表(Section Header Table)。在这个表里面,每一个 section 都有一项,在代码里面也有定义 struct elf32_shdr 和 struct elf64_shdr。在 ELF 的头里面,有描述这个文件的节头部表的位置,有多少个表项等等信息。
有的 section,例如.rel.text, .rel.data 和重定位有关。例如这里的 createprocess.o,里面调用了 create_process 函数,但是这个函数在另外一个.o 里面,因而 createprocess.o里面根本不可能知道被调用函数的位置,所以只好在 rel.text 里面标注,这个函数是需要重定位的。所以称之为可重定位的。
编译后的 .o 经过链接,可以形成二进制执行文件,文件格式如右
如果是含有动态链接库的可执行文件,里边还会多一个 .interp 的 Segment,这里面是 ld-linux.so,这是动态链接器,也就是说,运行时的链接动作都是它做的。
另外,ELF 文件中还多了两个 section,一个是.plt,过程链接表(Procedure Linkage Table,PLT),一个是.got.plt,全局偏移量表(Global Offset Table,GOT)。
含有动态链接库的可执行文件,在运行函数时,由于是运行时才去找,编译的时候,压根不知道这个函数在哪里,所以就在 PLT 里面建立一项 PLT[x]。这一项也是一些代码,有点像一个本地的代理,在二进制程序里面,不 直接调用 create_process 函数,而是调用 PLT[x] 里面的代理代码,这个代理代码会在运行的时候找真正的 create_process 函数。
而 GOT,这里面也会为 create_process 函数创建一项 GOT[y]。这一项是运行时 create_process 函数在内存中真正的地址。
也就是说,在运行时,先调用 PLT[x] 里的代理代码,代理代码调用GOT 表中对应项 GOT[y],调用的就是加载到内存中的 so 中的函数了。
但是 GOT 怎么知道的呢?对于 create_process 函数,GOT 一开始就会创建一项 GOT[y],但是这里面没有真正的地址,因为它也不知道,但是它有办法,它又回调 PLT,告诉它,你里面的代理代码来找我要 create_process 函数的真实地址,我不知道,你想想办法吧。
PLT 这个时候会转而调用 PLT[0],也即第一项,PLT[0] 转而调用 GOT[2],这里面是 ldlinux.so 的入口函数,这个函数会找到加载到内存中的 libdynamicprocess.so 里面的 create_process 函数的地址,然后把这个地址放在 GOT[y] 里面。下次,PLT[x] 的代理函数就能够直接调用了。
ELF Header是ELF文件的第一部分,64 bit的ELF文件头定义可以在 /usr/include/elf.h
中看到。
通过执行 readelf -h simple.o
可以看到