自己的程序如何引入第三方库
第三方提供了:
- foo.a 文件
- foo.so 文件(共享库)
- foo.h 文件
静态链接
用在编译阶段
-
直接将foo.a文件复制到自己的可执行文件。
-
程序在 编译链接时通过
-lfoo将foo.a文件连接进ELF文件 -
链接时,如果foo.a中的符号(也就是函数)和foo.h头文件中的对应不上就直接报错。
编译时动态链接
用在编译阶段 + 启动时需要so文件
- 生成 NEEDED(依赖声明,记录需要的共享库) + PLT 条目(过程链接表,本质是一段桩代码(Stub)负责处理外部函数的具体调用地址。)
- ld.so 在 main() 之前自动加载 .so
- 部署需要带 .so
- 库升级换 .so 即可(ABI 兼容前提下)
- 缺 .so 启动即崩
运行时动态加载
运行时引入库
- dlopen + 三种子方式
- 方式1,dlsym 函数指针,编译时不需要
-lfoo链接,要自己声明函数指针,然后再使用。 - 方式2,PLT 惰性绑定 + patchelf,需要
-lfoo链接,生成 PLT,然后去掉 NEEDED。foo() 直接调。 - 方式3,PLT 惰性绑定 + --unresolved-symbols,不需要
-lfoo(但链接器不生成 PLT,不可行),淘汰方案。
方式1: dlsym 函数指针
void *h = dlopen("libfoo.so", RTLD_LAZY); //懒加载 ,将需要的库导入到运行中的程序中的.dynamic段
int (*foo)(void) = dlsym(h, "foo"); //声明使用库中的foo函数
foo(); // 通过函数指针调用,完全绕过 PLT
//调用foo()函数指针时,其实就是跑到.dynamic段的地址去执行一下库函数
方式2: PLT 惰性绑定 + patchelf
先让链接器按正常动态链接生成完整的 PLT/GOT 基础设施,再用 patchelf 抹掉 NEEDED 骗过 ld.so 的启动加载,最后用 dlopen + RTLD_GLOBAL 把库"注册"进全局符号表,让 PLT 的懒解析机制自动完成原本需要 dlsym 手动做的事。
第一阶段:
$(CC) -o re001 main.o camera.o -lfoo -Wl,-z,lazy
链接器 ld 的工作:
1. 看到 -lfoo → 打开 libfoo.so
2. 看到源码调用 foo() → 在 libfoo.so 的 .dynsym 找到 foo
3. 生成 PLT 条目 foo@PLT:
foo@PLT:
jmp *foo@GOT # 间接跳转,通过 GOT
push $foo_idx # 压入符号表索引(用于标识是哪个符号)
jmp PLT0 # 跳到公共解析桩
4. 生成 GOT 条目 foo@GOT:
- 初始值指向 PLT 的第二条指令(push $foo_idx)
5. 写入重定位项 .rela.plt:
R_X86_64_JUMP_SLOT foo # 告诉运行时:这个 GOT 项需要动态填充
6. 写入 .dynamic 段:
NEEDED libfoo.so # 启动时要加载这个库
此时 ELF 结构:
.rela.plt:
[0] R_X86_64_JUMP_SLOT foo → foo@GOT
.plt:
[foo@PLT] jmp *foo@GOT
push $0 ← 符号索引
jmp PLT0
.got.plt:
[foo@GOT] → 指向 PLT 的 push 指令(初始陷阱值)
.dynamic:
NEEDED libfoo.so
第二阶段:
patchelf 抹掉 NEEDED
patchelf --remove-needed libfoo.so re001
ELF 变化:
.dynamic 段:
之前: NEEDED libfoo.so ✅
之后: (无) ❌ ← 被移除
其他段(PLT/GOT/.rela.plt):
完全不变! ✅ ← 保留
关键:patchelf 只改 .dynamic 的一个字符串条目,不动代码段、不动 PLT/GOT。
第三阶段:
启动程序->ld.so读.dynamic 段->NEEDED: (无 libfoo.so)不加载!->这时候调用会找不到foo函数地址
第四阶段:
代码主动 dlopen
void *handle = dlopen("libfoo.so", RTLD_LAZY | RTLD_GLOBAL);
dlopen 内部流程:
1. 打开 libfoo.so 文件
2. mmap 到进程地址空间(匿名映射,无固定段名)
┌─────────────────────────────────────┐
│ libfoo.so 被映射到的内存区域 │
│ (不是 ELF 的"段",是内核的 VMA) │
│ │
│ .text → 代码(可执行) │
│ .data → 已初始化数据 │
│ .rodata→ 只读数据 │
│ .dynsym→ 动态符号表 │
└─────────────────────────────────────┘
3. 处理 libfoo.so 的依赖(递归加载其他 .so)
4. 符号表合并:
RTLD_GLOBAL → 把 libfoo.so 的 .dynsym 注入**全局符号表**
全局符号表现在包含:
foo → 0x7f...(libfoo.so 中的真实地址)
bar → 0x7f...
...
此时内存状态:
进程地址空间:
[text] re001 代码段
[data] 数据段
[libc.so] ✓
[libdl.so] ✓
[libfoo.so] ✓ ← 刚 mmap 进来!
阶段五:第一次调用 foo()(PLT 解析触发)
call foo@PLT
↓
foo@PLT:
jmp *foo@GOT # 取 GOT[foo] 的值
↓
GOT[foo] 当前值 = 0x401036(指向 PLT 的 push 指令)
↓
"跳转"到 0x401036,即:
push $0 # 符号索引 0(foo 的索引)
jmp PLT0
↓
PLT0:
push GOT[1] # 模块 ID(re001 的 link_map)
jmp *GOT[2] # 跳转到 _dl_runtime_resolve
↓
_dl_runtime_resolve(link_map, symbol_index)
│
├─ 根据 link_map 找到 re001 的重定位表
├─ 根据 symbol_index 找到 .rela.plt[0] = R_X86_64_JUMP_SLOT foo
├─ 根据重定位项的符号名 = "foo"
├─ 去**全局符号表**搜索 "foo"
│ ↓
│ 找到了!dlopen + RTLD_GLOBAL 注入的
│ foo → 0x7f...(libfoo.so 中的地址)
│ ↓
├─ 回填 GOT[foo] = 0x7f...(真实地址)
└─ 跳转 0x7f...(调用真正的 foo)
foo() 执行完毕,ret
关键洞察:_dl_runtime_resolve 根本不查 NEEDED!它只查全局符号表。NEEDED 只是 ld.so 启动时自动加载库的依据,不是符号解析的依据。
阶段六:第二次调用 foo()(直接跳转)
call foo@PLT
↓
foo@PLT:
jmp *foo@GOT
↓
GOT[foo] = 0x7f...(已缓存的真实地址)
↓
直接跳到 foo(),无额外开销!
和正常动态链接的第二次调用完全一致。