本页目录
动态链接与 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 ;
int
// 静态链接: 链接器把 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:
|
|
# 运行时确认 lazy binding:
LD_DEBUG=bindings |
# 输出: 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
# 典型用途:
# 1. 替换 malloc → 追踪内存分配
# 2. 替换 connect → 网络重定向
# 3. 替换 open → 文件系统沙箱
# 条件: 不能用来覆盖静态链接的函数
# (静态链接没有 PLT/GOT, call 直接到函数地址)
dlopen / dlsym
void *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 对此函数的效果
调试
# 查看所有链接的共享库
# 查看符号解析 (极详细)
LD_DEBUG=all |
# 查看当前进程加载的 .so
|
# 未定义的符号 (链接时)
参考
- 源码: 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