自己的程序如何引入第三方库

第三方提供了:

  • 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(),无额外开销!

和正常动态链接的第二次调用完全一致。