动态链接原理

阶段

  • 静态编辑
  • 动态加载
  • 延迟绑定

一、静态编辑

链接器(ld)在生成可执行文件时,会记录可执行文件所依赖的共享库的名称以及需要的符号,并将这些信息存储在特定的段中(例如 .dynamic 段)。同时,它会创建 PLT(Procedure Linkage Table,过程链接表)和 GOT(Global Offset Table,全局偏移表)作为占位符,用于后续的动态链接过程。GOT 表初始时包含的是用于延迟绑定的地址,而非实际的函数地址。

二、动态加载

加载可执行文件时,从其 .interp 字段找到解释器(也就是 loader,一般是 /lib64/ld-linux-x86-64.so.2,可以用 ldd <prog> 或者 readelf -l <prog> | grep "interp" 来检查)。让解释器来加载它。

动态链接器会从 搜索路径(例如 LD_LIBRARY_PATH 环境变量、/etc/ld.so.conf 配置的路径、默认系统路径 /lib, /usr/lib 等)搜索可执行文件所依赖的共享库,并将这些库映射到进程的虚拟地址空间中。在开启了 -fPIC 编译的情况下,动态链接器会解析全局变量和函数的地址,并将解析后的地址填充到 GOT 表中。如果程序编译时使用了 -fno-plt 选项,则所有函数调用都将直接通过 GOT 表进行,而不再依赖 PLT。在这种情况下,所有函数的 GOT 表项在加载时都会被解析并填充实际地址。

如果没有开启 -fPIC 编译,则动态链接器在加载进程时会直接修改 .text 段中的地址,导致 .text 变得私有而不在系统范围内共享。

三、延迟绑定

如果程序使用了 PLT (默认行为),当第一次调用某个外部函数时,会跳转到该函数在 PLT 中的条目。PLT 中的代码会调用动态链接器,动态链接器会查找该函数在共享库中的实际地址,并将该地址写入到 GOT 表中对应的条目。此后,对该函数的调用将直接通过 GOT 表跳转到实际地址,避免了重复的动态链接开销。

细节

共享库函数的三种绑定方式

绑定方式编译选项GOTPLT绑定时机备注
延迟绑定-fPIC第一次调用时
加载时立即绑定-fPIC -fno-plt加载时
加载时修改 .text 段立即绑定-fno-pic加载时无法实现系统层面的代码共享,每个进程需要在地址空间中维护一份私有的代码

https://godbolt.org/z/orrzPhPcK

Note

Clang 和 gcc 在无 PIC 相关编译选项时,“是否对未定义的函数使用 PLT”存在差异。Gcc 默认没有 -fPIC,而 Clang 默认有。所以最好显式注明。

动态链接器如何知道一个符号是否应该立即解析?

.rela.dyn 和 .rela.plt 重定位表:链接器会生成重定位表(例如 .rela.dyn 和 .rela.plt),这些表描述了哪些 GOT 条目需要被动态链接器修改,以及如何修改。.rela.dyn 通常用于处理全局变量和那些需要立即绑定的函数(例如使用 -fno-plt 编译时),如果是通过修改 GOT 完成绑定,则记录了符号和要修改的 GOT 表项的位置信息;如果是通过直接修改 .text 字段绑定,则 .rela.dyn 记录了符号和要修改的 .text 中的位置信息。.rela.plt 用于处理需要延迟绑定的函数。动态链接器会读取这些重定位表,并根据表中的信息来更新 GOT 表。

绑定方式信息记录位置
延迟绑定.rela.plt
加载时立即绑定.rela.dyn
加载时修改 .text 段立即绑定.rela.dyn

搜索符号的过程

  1. 查找符号: 当动态链接器需要解析一个函数地址时,它会首先在可执行文件自身的符号表中查找该函数。
  2. 在共享库中查找: 如果在可执行文件中找不到该函数,动态链接器会在可执行文件依赖的共享库的符号表中查找该函数。动态链接器会按照一定的顺序搜索共享库,例如 LD_LIBRARY_PATH 环境变量、/etc/ld.so.conf 配置的路径、默认系统路径 /lib, /usr/lib 等。
  3. 地址填充: 找到函数地址后,动态链接器会将该地址写入到 GOT 表中对应的条目。

动态链接实现 IFUNC

无论是否使用了延迟绑定,动态链接时的符号解析都能实现 IFUNC 的功能。IFUNC 全称 Indirect Function,是 GLIBC 引入的一种机制,使得用户可以通过条件来干预延迟绑定过程中动态链接器对符号的选择。GLIBC 中很多函数(如 memcpy)都会在特定硬件下选择可用的高效实现。具体见 能不能解释一下 Linux 中的 IFUNC 机制?