为了回答您的问题,让我们首先在实体和定义方面打下坚实的基础。
ELF代表“可执行和可链接格式”。
也就是说,它定义了两种类型文件的结构和形状:
- 可执行文件(共享对象 *.so 和独立可执行文件)
- 可链接(目标文件 *.o)
让我们专注于可执行文件。
可执行文件的依赖解析
其中,ELF定义了一种描述和解决可执行文件依赖项的方法。
依赖关系
简单来说,依赖是需要外部符号的。符号是命名(标识)的内存块。一些块是数据块(全局变量),而另一些是代码数据块(全局函数)。由于符号是模块(又名共享对象)的一部分,因此任何需要的符号都与模块耦合。
总之,依赖是需要符号和模块的。
请注意,作为 OS API 一部分的函数可以并且通常是外部符号。然而,情况并非总是如此。
依赖描述
ELF 定义了一个称为 Dynamic Segment 的结构,用于在可执行文件的加载过程中存储加载器(也称为动态链接器)所需的信息。可执行文件的依赖项描述存储在其动态段中。
需要的符号被组织在一个称为动态符号表的表中,该表由动态段引用:
引用加载程序指令下的符号表 - https://elfy.io/KYze4
动态符号表是一个连续的符号描述符数组:
Symbols 下的 .dynsym - https://elfy.io/KYze4
另一方面,需要的模块直接用 DT_NEEDED 条目描述:
Loader 指令下需要的模块 - https://elfy.io/KYze4
动态链接
现在我们准备讨论一旦加载器解析可执行文件就可以访问其依赖项的连接机制。我们将按照外部函数调用的步骤来完成。
我们以调用__android_log_print为例(ARM 32 位)。
...
1d21a: f7fa e8e8 blx 173ec ; __android_log_print@plt
...
上面是一个调用 __android_log_print 的程序集,它将文本打印到 Android Logcat。但实际上,该blx指令分支到称为过程链接表(PLT)的特殊区域中的特定代码存根。PLT 中有一个代码存根,用于每个需要的外部功能。
这是 __android_log_print 的存根:
...
000173ec __android_log_print@plt:
173ec: e28fc600 add ip, pc, #0, 12
173f0: e28cca11 add ip, ip, #69632
173f4: e5bcf9f4 ldr pc, [ip, #2548]!
000173f8 sleep@plt:
173f8: e28fc600 add ip, pc, #0, 12
173fc: e28cca11 add ip, ip, #69632
17400: e5bcf9ec ldr pc, [ip, #2540]!
...
存根中的三个指令执行以下操作:(伪代码)
JUMP *(GOT_ADDRESS + GOT_OFFSET_OF(__android_log_print))
所述全局偏移表(GOT)是指针的表。对于每个外部功能,GOT 中都有一个单元格。也就是说,每个外部函数在 GOT 中都有自己的单元格。加载过程完成后,函数 X 的单元格包含函数 X 的内存地址。
- 由于编码限制,GOT 中正确单元格的地址计算被拆分为 3。例如:不能在单个指令中编码大偏移量。
根据前面讨论的信息,使用正确的内存地址初始化 GOT 是 OS 加载程序的责任。
PLT 和 GOT 是 ELF 规范的一部分。