考虑 Linux 系统上的以下两个文件:
use_message.cpp
#include <iostream>
extern const char* message;
void print_message();
int main() {
std::cout << message << '\n';
print_message();
}
libmessage.cpp
#include <iostream>
const char* message = "Meow!";
void print_message() {
std::cout << message << '\n';
}
我们可以将 use_message.cpp 编译为目标文件,将 libmessage.cpp 编译为共享库,并将它们链接在一起,如下所示:
$ g++ use_message.cpp -c -pie -o use_message.o
$ g++ libmessage.cpp -fPIC -shared -o libmessage.so
$ g++ use_message.o libmessage.so -o use_message
message
的定义最初位于libmessage.so中。当执行 use_message
时,动态链接器执行重定位:
libmessage.so中的
message
定义message
的定义从 libmessage.so 复制到 use_message 的 .bss
部分use_message中新版本的
message
由
readelf
转储的相关搬迁是:
使用消息
Offset Info Type Sym. Value Sym. Name + Addend
000000004150 000c00000005 R_X86_64_COPY 0000000000004150 message + 0
这是我之前写的列表中的第 2 号搬迁。
libmessage.so
Offset Info Type Sym. Value Sym. Name + Addend
000000004040 000000000008 R_X86_64_RELATIVE 2000
000000003fd8 000b00000006 R_X86_64_GLOB_DAT 0000000000004040 message + 0
这些分别是搬迁编号 1 和 3。
重定位编号 1 和 2 之间存在依赖关系:对 libmessage.so 的
message
定义的更新必须在该值复制到 use_message 之前进行,否则 use_message 将不会指向正确的位置。
我的问题是:申请搬迁的顺序是如何规定的? ELF 文件中是否有编码指定这一点?或者在 ABI 中?或者动态链接器只是期望计算出重定位本身之间的依赖关系,并确保写入给定内存地址的任何重定位都在从同一位置读取的任何重定位之前运行?静态链接器是否只输出重定位,以便可执行文件中的重定位始终可以在共享库重定位之后处理?
我的问题是:申请搬迁的顺序是如何规定的? ELF 文件中是否有编码指定这一点?或者在 ABI 中?或者动态链接器只是期望计算出重定位本身之间的依赖关系,并确保写入给定内存地址的任何重定位都在从同一位置读取的任何重定位之前运行?静态链接器是否只输出重定位,以便可执行文件中的重定位始终可以在共享库重定位之后处理?
我认为重定位解决顺序没有由标准指定。 动态加载器定义顺序。为了支持复制重定位,主可执行文件最后被重定位。 链接器仅生成可执行链接的复制重定位(-no-pie/-pie),并且了解动态加载器语义。
有两部分:模块内的顺序和两个模块之间的顺序。
glibc rtld 以反向搜索顺序(反向 l_initfini)处理重定位,其中 rtld 本身有一个特殊情况。主可执行文件需要最后处理才能处理 R_*_COPY。如果A有一个ifunc引用B,一般B需要在A之前重定位。没有ifunc,共享对象的解析顺序可以是任意的。
假设我们有以下依赖树。
main
dep1.so
dep2.so
dep3.so
libc.so.6
dep4.so
dep3.so
libc.so.6
libc.so.6
libc.so.6
l_initfini 包含 main、dep1.so、dep2.so、dep4.so、dep3.so、libc.so.6、ld.so。重定位解析顺序为 ld.so (bootstrap)、libc.so.6、dep3.so、dep4.so、dep2.so、dep1.so、main、ld.so。
在模块内,glibc rtld 按顺序解析重定位。假设DT_RELA(.rela.dyn)和DT_PLTREL(.rela.plt)都存在,glibc逻辑如下:
// Simplified from elf/dynamic-link.h
ranges[0] = {DT_RELA, DT_RELASZ, 0};
ranges[1] = {DT_JMPREL, DT_PLTRELSZ, do_lazy};
if (!do_lazy && ranges[0].start + ranges[0].size == ranges[1].start) { // the equality operator is always satisfied in practice
ranges[0].size += size;
ranges[1] = {};
}
for (int ranges_index = 0; ranges_index < 2; ++ranges_index)
elf_dynamic_do_Rela (... ranges[ranges_index]);
FreeBSD rtld 使用更复杂的顺序,这使得某些 ifunc 代码更加健壮。
顺便说一句,
use_message
(带有-fPIE可重定位文件)由于HAVE_LD_PIE_COPYRELOC
而需要复制重定位。
对于 Clang 和 GCC 的其他架构,PIE 模式不会导致复制重定位。
$ g++ use_message.cpp -c -pie -o use_message.o
$ g++ libmessage.cpp -fPIC -shared -o libmessage.so
$ g++ use_message.o libmessage.so -o use_message