---
title: 链接与加载
url: https://doc.liz6.com/compilers/07-runtime/02-linking-and-loading
locale: zh
area: compilers
tags:
- compilers
- 运行时
date: 2026-06-30
modified: 2026-07-16
description: 编译器输出 .o 后,到能运行还差链接和加载——重定位把符号引用变成地址,PLT/GOT 惰性绑定让动态库只在第一次调用时解析,LTO 把跨模块优化推迟到链接时做全程序分析。
---

# 链接与加载

> 编译器输出 .o 后,到能运行还差链接和加载——重定位把符号引用变成地址,PLT/GOT 惰性绑定让动态库只在第一次调用时解析,LTO 把跨模块优化推迟到链接时做全程序分析。

## 概述

编译器输出 `.o`(目标文件)后,到"能运行的可执行文件"还差两件事:**链接**(把多个 `.o` 和库的符号引用解析成地址)和**加载**(把可执行文件映射到进程地址空间,初始化后跳转到入口)。链接器不只是"拼接"——它在这一步做重定位(relocation)、做 GC sections 去未引用代码、做 LTO (跨编译单元的全局优化)。这篇沿着"编译器输出 → 静态链接 → 动态链接 → 动态加载"的链路,把链接和加载的每一层机制讲清楚。

## 编译器的输出:目标文件(ELF)

一个 `.o` 文件(ELF 格式)包含:

```
- .text:   机器码 (代码段)
- .data:   已初始化的全局变量
- .bss:    未初始化 (或零初始化) 的全局变量 —— 不占文件空间, OS 在加载时映射零页
- .rodata: 只读数据 (字符串字面量, 浮点常量)
- .symtab: 符号表 (本文件导出/导入的符号)
- .rela.text: 文本节的重定位表 (哪些指令引用了外部符号, 需要链接器修补)
- .rela.data: 数据节的重定位表
- .debug_info, .debug_line, ...: DWARF 调试信息
```

编译器的最后一步(汇编器)输出 `.o`。链接器读取所有 `.o` 和库的符号表,做重定位,产出可执行文件。

## 重定位:从"符号引用"到"地址"

`.o` 文件的机器码里,引用外部符号的地方填的是 0 或占位符。`.rela.text` 记录这些坑位:

```
典型的重定位项 (R_X86_64_PC32):
  偏移: 0x42    ← .text 中需要修补的位置
  符号: printf  ← 引用哪个符号
  类型: R_X86_64_PC32  ← 相对跳转 (PC + offset)
  加数: -4      ← 补偿 (指令中 PC 指向下一条, 差 4 字节)
```

链接器在分配好所有节的加载地址后,遍历所有重定位项,计算目标符号的最终地址,按类型公式改写目标位置的字节:

```
R_X86_64_PC32 修补公式:   *(target) = sym_addr - (target_addr + 4) + addend
R_X86_64_64   修补公式:   *(target) = sym_addr + addend        (绝对地址)
```

## 静态链接:`.a` (archive)

静态库是一组 `.o` 的打包(`ar` 格式),链接时按需取出:

```
gcc main.o -L. -lmylib → 链接器做:
  1. 收集 main.o 中 unresolved 的符号
  2. 在 libmylib.a 中查找每个 unresolved 符号
  3. 找到 → 从 .a 中提取对应的 .o, 加到链接集合
  4. 提取的 .o 可能又引入新的 unresolved → 递归 (或分遍 scan)
  5. 所有符号 resolved → 做重定位 → 输出可执行文件
```

链接器的符号解析是**顺序敏感**的——传统 Unix 链接器从左到右处理 `.o` 和 `.a`,如果 `libfoo.a` 放在引用它的 `.o` 之前,可能不被扫描(因为没有 unresolved 符号需要它里面的符号)。现代链接器(LLD)有 `--start-group` / `--end-group` 做迭代扫描,消除顺序问题。

### 链接器 GC(dead section elimination)

编译器以函数为单位生成 `.text` 节片段(section fragments,gold 和 LLD 有 `--gc-sections`)。链接器从入口点(`_start`)出发,追踪 call/jump 引用,把未引用的函数节标记为 dead 并丢弃。这减少最终二进制大小——类似 DCE 但跨 `.o` 单元。

## 动态链接:`.so` (shared object)

动态库不是在链接时拷贝到可执行文件里,而是在链接时**记录依赖**,在运行时**加载并动态解析**。

### GOT 和 PLT:惰性绑定的两层

[目标代码与 ABI](/compilers/06-code-generation/02-target-code-and-abi.md) 提了 PIC 需要 GOT。动态链接的完整图是 GOT + PLT:

<svg viewBox="0 0 720 330" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="GOT加PLT惰性绑定:首次调用走动态链接器解析,之后直接跳转">
  <defs><marker id="gotplt-ah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="330" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">GOT + PLT 惰性绑定:首次调用慢,之后直接跳(lazy binding)</text>

  <text x="40" y="58" font-size="13" font-weight="700" fill="#3730a3">首次调用 printf(缓慢路径:走动态链接器解析)</text>
  <rect x="40" y="70" width="203" height="64" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="141" y="88" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">call printf@plt</text>
  <text x="141" y="104" text-anchor="middle" font-size="10" fill="#4f46e5">(PLT 桩)</text>
  <text x="141" y="120" text-anchor="middle" font-size="9.5" fill="#4f46e5">→查 GOT[printf](未解析)</text>
  <rect x="258" y="70" width="203" height="64" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="359" y="96" text-anchor="middle" font-size="10.5" font-weight="700" fill="#3730a3">jmp _dl_runtime_resolve</text>
  <text x="359" y="116" text-anchor="middle" font-size="9.5" fill="#4f46e5">动态链接器查符号表→找到地址</text>
  <rect x="476" y="70" width="203" height="64" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="577" y="96" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">更新 GOT[printf]</text>
  <text x="577" y="116" text-anchor="middle" font-size="9.5" fill="#4f46e5">=真实地址→jmp printf</text>
  <line x1="243" y1="102" x2="258" y2="102" stroke="#475569" stroke-width="1.6" marker-end="url(#gotplt-ah)"/>
  <line x1="461" y1="102" x2="476" y2="102" stroke="#475569" stroke-width="1.6" marker-end="url(#gotplt-ah)"/>

  <text x="40" y="154" font-size="13" font-weight="700" fill="#0f766e">第 N 次调用(命中路径:GOT 已有真实地址)</text>
  <rect x="40" y="166" width="300" height="54" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="190" y="188" text-anchor="middle" font-size="11" font-weight="700" fill="#115e59">call printf@plt</text>
  <text x="190" y="206" text-anchor="middle" font-size="10" fill="#0f766e">同一 PLT 桩</text>
  <rect x="380" y="166" width="300" height="54" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="530" y="188" text-anchor="middle" font-size="11" font-weight="700" fill="#115e59">GOT[printf] 已有真实地址</text>
  <text x="530" y="206" text-anchor="middle" font-size="10" fill="#0f766e">→ jmp printf,无额外开销</text>
  <line x1="340" y1="193" x2="380" y2="193" stroke="#475569" stroke-width="1.6" marker-end="url(#gotplt-ah)"/>

  <rect x="40" y="240" width="640" height="64" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="56" y="264" font-size="12.5" fill="#3730a3">GOT 相当于一层缓存:首次调用必须经过动态链接器完整解析并写回 GOT;</text>
  <text x="56" y="286" font-size="12.5" fill="#3730a3">之后每次调用直接读 GOT 命中真实地址跳转——这就是惰性绑定(lazy binding)的省时之处。</text>
</svg>

这就是 **lazy binding**:动态库的符号只在第一次被调用时才解析,GOT 充当缓存。`LD_BIND_NOW=1` 环境变量强制在加载时立即解析所有符号(不惰性)——启动慢,但运行时无 PLT 开销。

### 符号插入(symbol interposition)

动态链接支持**符号插入**:如果多个 `.so` 都定义了同名的 `malloc`,最先加载的定义生效。可执行文件定义的符号覆盖 `.so` 的(默认行为),`LD_PRELOAD` 显式插入。这是 `jemalloc`/`tcmalloc` 替换系统 `malloc` 的机制——但也是性能坑:插入阻止编译器内联 `malloc`,每次调用都走 PLT。

## dlopen:运行时加载

程序运行时可以动态加载任意库:

```
void *handle = dlopen("libplugin.so", RTLD_NOW);
void (*fn)(int) = dlsym(handle, "plugin_init");
fn(42);
dlclose(handle);
```

JIT 编译器常用 `dlopen` 加载编译好的 `.so`——编译的代码写出 `.o`→ 链接成 `.so`→ `dlopen`→ `dlsym` 调具体函数。V8 和 Julia 都用这条路——把 JIT 编译的机器码包装为 `.so` 并动态加载,比直接写可执行内存安全(OS 保证 `.text` 段的 NX/写保护,不需要手动 `mprotect`)。

## LTO(Link-Time Optimization):链接时全程序优化

传统的每一个 `.c`(编译单元)单独编译成 `.o`,优化只在单元内做——跨单元的函数内联、死代码消除、常量传播都看不见其他单元。LTO 把编译单元间的优化推迟到链接时:

<svg viewBox="0 0 720 310" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="LTO对比普通编译:链接时把各单元的IR攒到一起做全程序优化">
  <defs><marker id="lto-ah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="310" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">LTO:链接时把 IR 攒到一起做全程序优化,而非编译时各单元独立优化</text>

  <text x="40" y="58" font-size="13" font-weight="700" fill="#475569">普通编译:各编译单元独立优化,链接器只管拼接</text>
  <rect x="40" y="70" width="145" height="48" rx="7" fill="#e2e8f0"/>
  <text x="112" y="99" text-anchor="middle" font-size="13" font-weight="700" fill="#334155">.c</text>
  <rect x="205" y="70" width="145" height="48" rx="7" fill="#e2e8f0"/>
  <text x="277" y="90" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">opt</text>
  <text x="277" y="108" text-anchor="middle" font-size="10" fill="#475569">单元内优化</text>
  <rect x="370" y="70" width="145" height="48" rx="7" fill="#e2e8f0"/>
  <text x="442" y="90" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">.o</text>
  <text x="442" y="108" text-anchor="middle" font-size="10" fill="#475569">机器码</text>
  <rect x="535" y="70" width="145" height="48" rx="7" fill="#e2e8f0"/>
  <text x="607" y="90" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">链接器</text>
  <text x="607" y="108" text-anchor="middle" font-size="10" fill="#475569">拼接</text>
  <line x1="185" y1="94" x2="205" y2="94" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>
  <line x1="350" y1="94" x2="370" y2="94" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>
  <line x1="515" y1="94" x2="535" y2="94" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>

  <text x="40" y="146" font-size="13" font-weight="700" fill="#0f766e">LTO:.c 产出 IR 而非机器码,链接时合并做全程序优化</text>
  <rect x="40" y="158" width="145" height="64" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="112" y="193" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">.c</text>
  <rect x="205" y="158" width="145" height="64" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="277" y="184" text-anchor="middle" font-size="10.5" font-weight="700" fill="#115e59">IR(非机器码)</text>
  <text x="277" y="202" text-anchor="middle" font-size="9.5" fill="#0f766e">存入 .o(bitcode)</text>
  <rect x="370" y="158" width="145" height="64" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="442" y="184" text-anchor="middle" font-size="10.5" font-weight="700" fill="#115e59">链接器收集</text>
  <text x="442" y="202" text-anchor="middle" font-size="9.5" fill="#0f766e">所有 .o 的 IR</text>
  <rect x="535" y="158" width="145" height="64" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="607" y="184" text-anchor="middle" font-size="10" font-weight="700" fill="#115e59">合并为一个模块</text>
  <text x="607" y="202" text-anchor="middle" font-size="9" fill="#0f766e">opt→codegen→链接</text>
  <line x1="185" y1="190" x2="205" y2="190" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>
  <line x1="350" y1="190" x2="370" y2="190" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>
  <line x1="515" y1="190" x2="535" y2="190" stroke="#475569" stroke-width="1.6" marker-end="url(#lto-ah)"/>

  <rect x="40" y="240" width="640" height="52" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="56" y="262" font-size="12.5" fill="#115e59">LTO 把优化推迟到链接时:链接器能看到全部 IR,做普通编译看不到的跨模块内联/DCE;</text>
  <text x="56" y="282" font-size="12.5" fill="#115e59">代价是链接时间变长(大型项目可能翻倍),换来 5–15% 的性能提升。</text>
</svg>

- **ThinLTO**:全程序优化的可扩展方案——不合并所有 IR 到单一模块(内存太大),而是按"调用关系"做跨模块摘要,在每个模块内各自优化,只对热点跨模块调用做内联。ThinLTO 是 gold/LLD 的默认 LTO 模式。

LTO 对编译时间有显著影响——整个程序的优化在链接时重新跑,大型项目链接时间可能翻倍。收益也非常大:跨模块内联消除了调用边界,常带 5–15% 的性能提升。

## 参考

- **Levine**: "Linkers and Loaders" — 链接与加载的经典教材
- **Ian Lance Taylor**: "Linkers" 系列 (https://lwn.net/Articles/276782/) — gold 链接器的作者写链接器原理
- **System V AMD64 ABI**: 动态链接部分 (GOT/PLT 的正式规范)

*Keywords: static linking, dynamic linking, archive, .a, shared object, .so, ELF, .text, .data, .bss, .symtab, relocation, R_X86_64_PC32, GOT, PLT, lazy binding, _dl_runtime_resolve, symbol interposition, LD_PRELOAD, dlopen, dlsym, linker script, gc-sections, LTO, ThinLTO, link-time optimization, gold, LLD*
