实现动态链接和加载

实现动态链接和加载





All problems in computer science can be solved by another level of indirection. (Butler Lampson)

2025 南京大学《操作系统原理》
实现动态链接和加载

实现应用程序的拆解

方案 1: libc.o

  • 在加载时完成重定位
    • 加载 = 静态链接
    • 省了磁盘空间,但没省内存
    • 致命缺点:时间 (链接需要解析很多不会用到的符号)

方案 2: libc.so (shared object)

  • 编译器生成位置无关代码
    • 加载 = mmap
    • 但函数调用时需要额外一次查表
  • 这才对:映射同一个 libc.so,内存中只需要一个副本
2025 南京大学《操作系统原理》
实现动态链接和加载

动态加载:A Layer of Indirection

编译时,动态链接库调用 = 查表

call  *TABLE[printf@symtab]

链接时,收集所有符号,“生成” 符号信息和相关代码

#define foo@symtab     1
#define printf@symtab  2
...

void *TABLE[N_SYMBOLS];

void load(struct loader *ld) {
    TABLE[foo@symtab] = ld->resolve("foo");
    TABLE[foo@printf] = ld->resolve("printf");
    ...
}
2025 南京大学《操作系统原理》
实现动态链接和加载

dlbox: 再次 “实现” binutils

编译和链接

  • 偷 GNU 工具链就行
    • ld = objcopy (偷来的)
    • as = GNU as (偷来的)

解析和加载

  • 剩下的就需要自己动手了
    • readdl (readelf)
    • objdump
    • 同样可以山寨 addr2line, nm, objcopy, ...
  • 加载器就是 ELF 中的 “INTERP”
2025 南京大学《操作系统原理》
实现动态链接和加载

我们实现了什么?

我们 “发明” 了 GOT (Global Offset Table)!

  • 对于每个需要动态解析的符号,GOT 中都有一个位置
  • ELF: Relocation section “.rela.dyn”
  Offset          Info           Type        Sym. Name + Addend
...
000000003fe0  000300000006 R_X86_64_GLOB_DAT printf@GLIBC_2.2.5 + 0
...
  • objdump 查看 3fe0 这个 offset ,位于 “GOT”:
    • printf("%p\n", printf); 看到的不是真正的 printf
    • *(void **)(base + 0x3fe0) 才是
    • 我们可以设置一个 “read watch point”,看看谁读了它
2025 南京大学《操作系统原理》
实现动态链接和加载

动态链接的主要功能

实现代码的动态链接和加载

  • main (.o) 调用 printf (.so)
  • main (.o) 调用 foo (.o)

难题:怎么决定到底要不要查表?

int printf(const char *, ...);
void foo();
  • 是在同一个二进制文件 (链接时确定)?还是在库中 (运行时加载)?
2025 南京大学《操作系统原理》
实现动态链接和加载

历史遗留问题:先编译、后链接

编译器的选择 1: 全部查表跳转

ff 25 00 00 00 00   call *FOO_OFFSET(%rip)
  • 调用个 foo 都多查一次表,性能我不能忍 ❌

编译器的选择 2: 全部直接跳转

e8 00 00 00 00      call <reloc>
  • %rip: 00005559892b7000
  • libc.so.6: 00007fdcdf800000
    • 相差了 2a8356549000
    • 4-byte 立即数放不下,无论如何也跳不过去 ❌
2025 南京大学《操作系统原理》
实现动态链接和加载

还能那怎么办?

为了性能,“全部直接跳转” 是唯一选择

e8 00 00 00 00      call <reloc>
  • 如果这个符号在链接时发现是 printf (来自动态加载),就在 a.out 里 “合成” 一段小代码
printf@plt:
    jmp *PRINTF_OFFSET(%rip)
  • 我们发明了 PLT (Procedure Linkage Table)!
2025 南京大学《操作系统原理》
实现动态链接和加载

PLT: 没能解决数据的问题

对于 extern int x,我们不能 “间接跳转”!

  • x = 1, 同一个 .so (或 executable)
mov $1, offset_of_x(%rip)
  • x = 1, 另一个 .so
mov GOT[x], %rdi
mov $1, (%rdi)

不优雅的解决方法

  • -fPIC 默认会为所有 extern 数据增加一层间接访问
    • __attribute__((visibility("hidden")))
2025 南京大学《操作系统原理》
实现动态链接和加载

LD_PRELOAD

一个神奇的 “hook” 机制

  • 允许 “preload” 一个自己的库
    • 当然,没有魔法
    • LD_PRELOAD 会传递给 ld-linux.so
  • 我们可以在运行时,用一个自己的库替换掉某个库
LD_PRELOAD=./mylib.so ./a.out
2025 南京大学《操作系统原理》