本页目录

动态链接与 ld.so

覆盖: PLT/GOT → lazy binding → LD_PRELOAD → LD_LIBRARY_PATH → rpath/runpath → symbol versioning → dlopen/dlsym → IFUNC 适用: glibc ld.so, musl ldso

概述

动态链接让多个程序共享同一个 .so 的代码页(物理内存中只有一份),同时允许库独立更新(不重新链接程序)。代价是首次调用每个外部函数的微小延迟(lazy binding),以及 ABI 兼容性的维护负担。理解 PLT/GOT 机制是排查"undefined symbol"、"LD_PRELOAD 不生效"、"libfoo.so.1 vs libfoo.so.2" 等问题的前提。

PLT/GOT: 延迟绑定的引擎

静态链接的问题

// main.c:
extern int foo(int);
int main() { return foo(42); }

// 静态链接: 链接器把 foo 的地址硬编码在 call 指令中
//   call <address_of_foo>
//   问题: libfoo.so 每次更新 → foo 地址变化 → 必须重新链接所有调用者

动态链接: PLT + GOT

flowchart TD
    MAIN["程序 (main)<br/>call foo@PLT<br/>不直接调 foo,调 PLT 桩 (stub)"]

    MAIN --> PLT{"PLT[foo]<br/>jmp *GOT[foo]"}

    PLT -->|"首次调用<br/>GOT → PLT 下一条"| LAZY["push index<br/>压入 foo 在 .rela.plt 的索引"]

    PLT -->|"后续调用<br/>GOT → foo 真实地址"| DIRECT["jmp foo<br/>直接跳转 ✅<br/><br/>每次调用只多一次间接跳转"]

    LAZY --> RESOLVER["jmp PLT[0]<br/>→ _dl_runtime_resolve()"]

    RESOLVER --> FIND["🔍 在 libfoo.so 中<br/>查找 foo 函数地址"]

    FIND --> UPDATE["📝 更新 GOT[foo]<br/>= foo 真实地址"]

    UPDATE --> EXEC["▶️ jmp foo<br/>执行真正的 foo"]

    classDef entry fill:#e3f2fd,stroke:#1565c0
    classDef decision fill:#fff3e0,stroke:#ef6c00
    classDef resolver fill:#f3e5f5,stroke:#7b1fa2
    classDef done fill:#e8f5e9,stroke:#2e7d32
    class MAIN entry
    class PLT decision
    class LAZY,RESOLVER,FIND,UPDATE resolver
    class DIRECT,EXEC done

三段内存布局

.got.plt (Global Offset Table — PLT 专用):
   GOT[0]:  .dynamic section 地址
   GOT[1]:  struct link_map * (当前共享库的链接映射)
   GOT[2]:  dl_runtime_resolve 地址 (由 ld.so 填充)
   GOT[3+]: 各外部函数的地址 (初始指向 PLT[func] 的第二条指令)

.plt (Procedure Linkage Table):
   PLT[0]: 公共入口 — push link_map + jmp dl_runtime_resolve
   PLT[1+]: 各函数的 stub — jmp *GOT[n]; push index; jmp PLT[0]

.got (非 PLT 的全局数据):
   存放全局变量的地址 (如 extern int errno;)

实测

# 编译并查看 PLT/GOT:
gcc -o test main.c -lfoo
objdump -d test | grep -A3 'foo@plt'
objdump -R test | grep foo      # 查看 foo 的 .rela.plt 条目

# 运行时确认 lazy binding:
LD_DEBUG=bindings ./test 2>&1 | grep foo
# 输出: binding file ./test [0] to ./test [0]: normal symbol `foo'

ld.so: 动态链接器

加载与搜索

1. 内核加载 ELF → 看到 PT_INTERP segment → /lib64/ld-linux-x86-64.so.2
2. 内核把 ld.so 加载到内存 → 跳转到 ld.so 的 _start
3. ld.so 自举 (重定位自己,因为 ld.so 自己也是动态链接的!)
4. ld.so 读主程序的 .dynamic → DT_NEEDED → 加载依赖的 .so
5. 对每个 .so: 读 .dynamic → 加载依赖的依赖
6. 符号解析: 遍历所有 loaded .so → 解析所有 undefined 符号
7. 重定位: 填充 GOT, 处理 R_X86_64_GLOB_DAT / R_X86_64_JUMP_SLOT
8. 调主程序的 _start

搜索路径顺序

1. DT_RPATH (ELF .dynamic 中的 RPATH, 已废弃,被 DT_RUNPATH 替代)
2. LD_LIBRARY_PATH (用户覆盖)
3. DT_RUNPATH (ELF .dynamic 中的 RUNPATH)
4. /etc/ld.so.cache (由 ldconfig 生成)
5. /lib64, /usr/lib64 (默认路径)

检查:
  readelf -d /bin/ls | grep -E 'RPATH|RUNPATH|NEEDED'
  ldconfig -p | grep libfoo
  LD_DEBUG=libs ./test 2>&1  # 追踪全部搜索

LD_PRELOAD

# 强制在符号解析的最前面插入共享库 → 覆盖任意符号
LD_PRELOAD=./override.so ./program

# 典型用途:
#   1. 替换 malloc → 追踪内存分配
#   2. 替换 connect → 网络重定向
#   3. 替换 open → 文件系统沙箱

# 条件: 不能用来覆盖静态链接的函数
#       (静态链接没有 PLT/GOT, call 直接到函数地址)

dlopen / dlsym

#include <dlfcn.h>
void *handle = dlopen("libfoo.so", RTLD_LAZY | RTLD_LOCAL);
void (*func)(int) = dlsym(handle, "foo");
func(42);
dlclose(handle);

// RTLD_LAZY:   延迟绑定 (默认)
// RTLD_NOW:    立即解析所有符号 (dlopen 失败更早暴露)
// RTLD_GLOBAL: 此库的符号对后续 dlopen 可见
// RTLD_LOCAL:  符号仅对本 handle 可见 (默认)
// RTLD_NODELETE: dlclose 不卸载 (防止 dangling pointer)
// RTLD_NOLOAD:  不加载,只查是否已加载

符号版本与 ABI

// 符号版本 (GNU extension):
//   libfoo.so: foo@@VERS_2.0 (default), foo@VERS_1.0 (旧)
//   链接时: 取 default version
//   运行时: 检查已加载的 .so 是否有匹配版本

// GCC 的 -fno-semantic-interposition (5.x+):
//   告诉编译器库内的函数调用可以假设为"本库的版本"
//   → 跳过 PLT → 直接调用 → 更快
//   但禁用 LD_PRELOAD 对此函数的效果

调试

# 查看所有链接的共享库
ldd /bin/ls

# 查看符号解析 (极详细)
LD_DEBUG=all ./test 2>&1 | head -50

# 查看当前进程加载的 .so
cat /proc/self/maps | grep '\.so'

# 未定义的符号 (链接时)
nm -u /bin/ls

参考

  • 源码⁠: glibc elf/dl-runtime.c (_dl_runtime_resolve), elf/rtld.c (ld.so main)
  • man: ld.so(8), dlopen(3), dlsym(3), elf(5)
  • LWN: "How to write shared libraries", "The cost of lazy binding"

关键词: PLT, GOT, lazy binding, dl_runtime_resolve, LD_PRELOAD, dlopen, dlsym, DT_NEEDED, RPATH, RUNPATH, symbol versioning