背景

ELF 可执行文件

本次课内容与目标

自己动手实现动态加载

  • 理解动态加载的动机
  • “lazy” 的延迟符号绑定

动态链接一直都没能讲得很简单

  • 今天再试一次!

动态链接与加载

为什么需要动态链接/加载?

例子:libc.so

  • 300K 条指令,2 MiB 大小
    • 每个程序如果都静态链接,浪费的空间很大
    • 最好是整个系统里只有一个 libc 的副本
      • 文件系统里只有一个副本 (libc.so)
      • 内存里只有一个副本

问题:真的整个操作系统里只有一个 libc 的副本吗?

  • 方法 1: 看 Linux Kernel 的 trace
  • 方法 2: 调试 Linux Kernel, 查看内存映射 (QEMU monitor)
  • 方法 3: 简单做个实验

让我们加载很多个很大的程序吧!

huge.c

for i in $(seq 1 1000); do; ./a.out &; done
  • 通过 inline assembly 创建一个 “巨大” 的可执行文件
    • 128M 个 nop
  • 然后再创建 1000 份
    • 如果每个进程都有独立的代码副本,总共需要 128G 内存

操作系统内核:不难实现

  • 所有以只读方式映射同一个文件的部分时,都指向同一个副本

实现动态链接与加载

需求分析

printf("Hello, World\n");

链接时

  • 不知道 printf 符号的地址
    • 无法用 rip 相对寻址直接调用

加载时

  • 可以正确跳转到 printf 代码执行

第一步:库函数和位置无关代码

库内的代码和数据:只需生成位置无关代码即可

liba_foo:
  movl $1, %eax
  ret

liba_bar:
  incl x(%rip)
  movl x(%rip), %eax
  ret

x:
  .int 0

访问库函数:动态链接时定位

如果另一个库需要调用 foo/bar?

libb_baz:
  call liba_bar // 编译时: 00 00 00 00
  ret

两个问题

  • 浪费了内存
    • 代码不再能共享 (操作系统的精妙设计)
  • 浪费了时间
    • 没有调用的代码也会被重定位

为每一个程序创建一张 “表”

把需要地址解析的部分集中在表里。

  call liba_bar
.section .data
liba_bar_dyn:
  .quad 0 // 加载时重填

.section .text
  call *liba_bar_dyn(%rip)

如果要给这张表一个名字,就叫它 GOT (Global Offset Table) 吧

自己动手实现动态库和 Loader

mini-loader.tar.gz


struct symbol {
  int64_t offset;
  char name[REC_SZ - sizeof(int64_t)];
};

动态库是由 “symbol” 列表、代码 + 数据组成

  • symbols 位于动态库的头部,以空名字结束
  • 分为两种符号
    • IMPORT (需要从其他库导入)
    • EXPORT (导出其他库可见)

PLT: 无论何种符号,都仅使用普通的跳转

hello();

编译时无法决定是以下哪种情况

call hello
call *hello_dyn(%rip)

解决办法

liba_bar@plt:
  jmp *liba_bar_dyn(%rip) // 这里可以实现 “延迟绑定”
puts_bar@plt:
  jmp *puts_dyn(%rip)

  call liba_bar@plt
  call puts@plt

再来看看动态链接的 ELF

Program header 中有 PT_INTERP

  • elf(5) PT_INTERP: The array element specifies the location and size of a null-terminated pathname to invoke as an interpreter. This segment type is meaningful only for executable files (though it may occur for shared objects). However it may not occur more than once in a file. If it is present, it must precede any loadable segment entry.
    • system (UNIX) 的世界各种不完美

ELF 的加载

ldd

  • ldd 比 readelf 做的事情稍稍多一些
    • 它会试着 “加载”
    • RTFM
  • ld.so 会比我们的 “简易” 加载器完成更多的操作
    • LD_PRELOAD (允许我们预先加载 “覆盖” 一些符号)
    • 递归解析依赖关系
    • ……

总结

总结

本次课内容与目标

  • 自己实现动态加载

Take-away messages

  • RTFM; RTFSC
  • 正确理解需求,你也能给出和工业界/开源社区一样的方案