ELF 重定位的应用顺序在哪里指定?

Tar*_*ama 14 c++ elf abi relocation dynamic-linking

考虑 Linux 系统上的以下两个文件:

使用消息.cpp

#include <iostream>

extern const char* message;
void print_message();

int main() {
    std::cout << message << '\n';
    print_message();
}
Run Code Online (Sandbox Code Playgroud)

libmessage.cpp

#include <iostream>
const char* message = "Meow!";   // 1. absolute address of string literal
                                 //    needs runtime relocation in a .so
void print_message() {
    std::cout << message << '\n';
}
Run Code Online (Sandbox Code Playgroud)

我们可以将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
Run Code Online (Sandbox Code Playgroud)

的定义message最初位于libmessage.so中。执行时use_message,动态链接器执行重定位:

  1. 使用字符串数据的加载地址更新libmessage.somessage内部的定义
  2. 将libmessage.somessage的定义复制到use_message的部分.bss
  3. 更新libmessage.so中的全局偏移表,使其指向use_messagemessage内部的新版本

由 转储的相关搬迁readelf是:

使用消息

  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000004150  000c00000005 R_X86_64_COPY     0000000000004150 message + 0
Run Code Online (Sandbox Code Playgroud)

这是我之前写的列表中的第 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
Run Code Online (Sandbox Code Playgroud)

这些分别是重定位编号 1 和 3。

重定位编号 1 和 2 之间存在依赖性:对libmessage.so定义的更新message必须在该值复制到use_message之前进行,否则use_message将不会指向正确的位置。

我的问题是:申请搬迁的顺序是如何规定的?ELF 文件中是否有编码指定这一点?或者在 ABI 中?或者动态链接器只是期望计算出重定位本身之间的依赖关系,并确保写入给定内存地址的任何重定位都在从同一位置读取的任何重定位之前运行?静态链接器是否仅输出重定位,以便可执行文件中的重定位始终可以在共享库重定位之后处理?

Mas*_*Ray 17

我的问题是:申请搬迁的顺序是如何规定的?ELF 文件中是否有编码指定这一点?或者在 ABI 中?或者动态链接器只是期望计算出重定位本身之间的依赖关系,并确保写入给定内存地址的任何重定位都在从同一位置读取的任何重定位之前运行?静态链接器是否仅输出重定位,以便可执行文件中的重定位始终可以在共享库重定位之后处理?

我认为重定位解析顺序没有由标准指定。动态加载器定义顺序。为了支持复制重定位,主可执行文件最后被重定位。链接器仅为可执行链接(-no-pie/-pie)生成复制重定位,并且了解动态加载器语义。


引用 https://maskray.me/blog/2021-01-18-gnu-indirect-function#relocation-resolving-order

有两部分:模块内的顺序和两个模块之间的顺序。

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
Run Code Online (Sandbox Code Playgroud)

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]);
Run Code Online (Sandbox Code Playgroud)

穆斯尔ldso/dynlink.c有:

/* The main program must be relocated LAST since it may contain
 * copy relocations which depend on libraries' relocations. */
reloc_all(app.next);
reloc_all(&app);
Run Code Online (Sandbox Code Playgroud)

FreeBSD rtld 使用更复杂的顺序,这使得某些 ifunc 代码更加健壮。

$ 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
Run Code Online (Sandbox Code Playgroud)

顺便说一句,use_message(使用 -fPIE 可重定位文件)由于 GCC 需要复制重定位HAVE_LD_PIE_COPYRELOC。对于 Clang 和 GCC 的其他架构,PIE 模式不会导致复制重定位。