实现动态加载

实现动态加载





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

2024 南京大学《操作系统:设计与实现》
实现动态加载

动态加载: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");
    ...
}
2024 南京大学《操作系统:设计与实现》
实现动态加载

dlbox: 再次 “实现” binutils

编译和链接

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

解析和加载

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

我们实现了什么?

我们 “发明” 了 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”,看看谁读了它
2024 南京大学《操作系统:设计与实现》
实现动态加载

动态链接的主要功能

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

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

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

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

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

编译器的选择 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 立即数放不下,无论如何也跳不过去 ❌
2024 南京大学《操作系统:设计与实现》
实现动态加载

还能那怎么办?

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

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

反思 PLT

我们真的需要 PLT 吗?

  • 编译和链接如果一起做,我们就知道每一个 call 指令的目标
puts@PLT:
    endbr64
    bnd jmpq *GOT[n]  // *offset(%rip)
  • 为什么 PLT 使用了 endbr64 和 bind jmpq 指令实现跳转?
    • 实际上完全可以有很多 “其他” 方案
2024 南京大学《操作系统:设计与实现》