GCC 完全指南 / 09 - 链接器详解
09 - 链接器详解
深入理解链接器的工作原理——符号解析、重定位、静态库和动态库的创建与链接。
9.1 链接器概述
链接器(Linker)是编译流程的最后一步,负责将多个目标文件和库组合成一个可执行文件或共享库。
┌──────────┐ ┌──────────┐ ┌──────────┐
│ main.o │ │ greet.o │ │ libm.so │
│ │ │ │ │ (math) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼──────────────┘
│
┌────▼────┐
│ 链接器 │
│ (ld) │
└────┬────┘
│
┌────▼────┐
│ hello │ ← 可执行文件
└─────────┘
链接器的两大核心任务
| 任务 | 说明 |
|---|---|
| 符号解析(Symbol Resolution) | 将每个符号引用关联到唯一定义 |
| 重定位(Relocation) | 合并各段,修正地址引用 |
9.2 符号解析
符号的三种状态
# 查看目标文件中的符号
nm main.o
# 输出符号类型:
# T: 已定义,text 段(函数)
# D: 已定义,data 段(已初始化全局变量)
# B: 已定义,bss 段(未初始化全局变量)
# U: 未定义(外部引用)
# W: 弱符号
# t: 已定义,局部(static 函数)
# d: 已定义,局部(static 变量)
// symbols.c - 演示各种符号类型
int global_var = 42; // D: 已初始化全局变量
int uninitialized_var; // B: 未初始化全局变量
static int static_var = 10; // d: 静态全局变量
void external_func(void); // U: 外部函数声明
int my_function(int x) { // T: 函数定义
return x + global_var;
}
static void local_func(void) { // t: 静态函数
static_var++;
}
gcc -c symbols.c
nm symbols.o
# 0000000000000000 D global_var
# 0000000000000000 T my_function
# 0000000000000000 t local_func
# 0000000000000004 d static_var
# U external_func
# 0000000000000004 C uninitialized_var
强符号与弱符号
// file1.c
int count = 10; // 强符号
// file2.c
int count = 20; // 强符号 → 链接错误: multiple definition
// 修复方法: 使用 weak 属性
// file2.c
__attribute__((weak)) int count = 20; // 弱符号
// 链接时选择强符号,弱符号作为默认值
常见符号解析错误
# 未定义符号
gcc main.o -o hello
# main.o: In function `main':
# main.c:(.text+0x5): undefined reference to `greet'
# 原因: greet 函数未被链接
# 多重定义
gcc main.o greet.o extra.o -o hello
# multiple definition of `count'
# 原因: 多个 .o 中定义了相同符号
# 库链接顺序错误
gcc -lm main.o -o hello
# undefined reference to `pow'
# 正确: gcc main.o -lm -o hello
9.3 重定位
重定位过程
编译 main.c 时:
main() 中调用 greet() → 地址未知 → 记录在重定位表中
链接时:
1. 合并所有 .o 的 .text 段
2. 确定 greet() 的最终地址
3. 将 greet() 的调用地址修正为真实地址
查看重定位信息
# 查看重定位条目
readelf -r main.o
# Offset Type Sym.Name
# 00000000000b R_X86_64_PLT32 greet-0x4
# 查看所有段的大小和地址
readelf -S main.o
重定位类型
| 类型 | 说明 |
|---|---|
R_X86_64_64 | 64 位绝对地址 |
R_X86_64_PC32 | 32 位 PC 相对地址 |
R_X86_64_PLT32 | PLT(过程链接表)引用 |
R_X86_64_GOTPCREL | GOT(全局偏移表)引用 |
R_X86_64_RELATIVE | 相对重定位(PIE) |
9.4 静态库
创建静态库
# 编译目标文件
gcc -c math_utils.c -o math_utils.o
gcc -c string_utils.c -o string_utils.o
# 创建静态库(使用 ar)
ar rcs libmyutils.a math_utils.o string_utils.o
# 查看静态库内容
ar t libmyutils.a
# math_utils.o
# string_utils.o
# 详细信息
ar dv libmyutils.a # 显示详细信息
# 链接静态库
gcc main.o -L. -lmyutils -o hello
# 静态库本质就是 .o 文件的归档
# libmyutils.a = ar 打包的 math_utils.o + string_utils.o
使用 gcc-ar 替代 ar(LTO 兼容)
# 当使用 -flto 编译时,必须使用 gcc-ar
gcc -flto -c math_utils.c -o math_utils.o
gcc-ar rcs libmyutils.a math_utils.o
gcc-ranlib libmyutils.a
提取静态库中的目标文件
# 列出内容
ar t libmyutils.a
# 提取特定目标文件
ar x libmyutils.a math_utils.o
# 替换库中的目标文件
ar r libmyutils.a updated_math_utils.o
9.5 动态库(共享库)
创建动态库
# 编译为位置无关代码(PIC)
gcc -fPIC -c math_utils.c -o math_utils.o
gcc -fPIC -c string_utils.c -o string_utils.o
# 创建共享库
gcc -shared -o libmyutils.so math_utils.o string_utils.o
# 或一步完成
gcc -fPIC -shared -o libmyutils.so math_utils.c string_utils.c
# 链接动态库
gcc main.c -L. -lmyutils -o hello
# 运行时需要找到 .so 文件
# 方法 1: 设置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./hello
# 方法 2: 使用 RPATH(推荐)
gcc main.c -L. -lmyutils -Wl,-rpath,'$ORIGIN' -o hello
查看动态库信息
# 查看 .so 文件的依赖
ldd libmyutils.so
# 查看导出的符号
nm -D libmyutils.so
# 查看 SONAME
readelf -d libmyutils.so | grep SONAME
# 查看动态段信息
readelf -d libmyutils.so
# 查看动态库的版本信息
objdump -p libmyutils.so | grep SONAME
9.6 静态链接 vs 动态链接
| 维度 | 静态链接 | 动态链接 |
|---|---|---|
| 文件大小 | 较大(包含库代码) | 较小(运行时加载) |
| 内存使用 | 每个进程独立副本 | 共享内存中的同一份 |
| 启动速度 | 较快(无加载开销) | 较慢(需加载 .so) |
| 更新 | 需重新编译 | 更新 .so 文件即可 |
| 依赖 | 无运行时依赖 | 依赖 .so 文件存在 |
| 部署 | 单文件部署 | 需要配套 .so 文件 |
| 安全更新 | 需重新编译 | 可单独更新库 |
混合链接
# 静态链接特定库,动态链接其他库
gcc main.o -Wl,-Bstatic -lmyutils -Wl,-Bdynamic -lm -lc -o hello
# 完全静态链接
gcc -static main.o -lmyutils -lm -o hello_static
# 查看链接了哪些动态库
ldd hello
9.7 链接器脚本(Linker Script)
默认链接器脚本
# 查看默认链接器脚本
ld --verbose
# 将默认脚本保存到文件
ld --verbose > default.ld
# 使用自定义脚本
gcc -T custom.ld -o hello main.c
基本链接器脚本示例
/* custom.ld - 自定义链接器脚本 */
ENTRY(main)
SECTIONS
{
. = 0x400000; /* 起始地址 */
.text : {
*(.text) /* 合并所有 .text 段 */
}
.rodata : {
*(.rodata) /* 只读数据 */
}
.data : {
*(.data) /* 已初始化数据 */
}
.bss : {
*(.bss) /* 未初始化数据 */
}
}
嵌入式系统的链接器脚本
/* STM32 链接器脚本示例 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.isr_vector) /* 中断向量表 */
*(.text*)
*(.rodata*)
} > FLASH
.data : {
*(.data*)
} > RAM AT > FLASH /* 加载到 FLASH,运行在 RAM */
.bss : {
*(.bss*)
} > RAM
}
9.8 符号可见性控制
# 默认所有符号都导出(可被其他 .o 或 .so 引用)
# 使用 -fvisibility=hidden 默认隐藏所有符号
gcc -fvisibility=hidden -fPIC -shared -o libhello.so hello.c
# 在代码中显式导出需要的符号
// visibility.h
#ifndef VISIBILITY_H
#define VISIBILITY_H
#ifdef BUILDING_DLL
#define EXPORT __attribute__((visibility("default")))
#define HIDDEN __attribute__((visibility("hidden")))
#else
#define EXPORT
#define HIDDEN
#endif
EXPORT int public_function(int x);
HIDDEN int internal_function(int x);
#endif
# 编译时定义 BUILDING_DLL
gcc -DBUILDING_DLL -fvisibility=hidden -fPIC -shared -o libhello.so hello.c
# 查看导出的符号
nm -D libhello.so
9.9 链接器常用选项
| 选项 | 说明 |
|---|---|
-L<path> | 库搜索路径 |
-l<name> | 链接 lib |
-static | 强制静态链接 |
-shared | 创建共享库 |
-rpath <path> | 设置运行时库搜索路径 |
-soname <name> | 设置共享库的 SONAME |
--gc-sections | 删除未使用的段 |
--print-gc-sections | 显示被删除的段 |
-z noexecstack | 标记栈为不可执行 |
-z relro | 启用 RELRO(只读重定位) |
-z now | 立即绑定(Full RELRO) |
--as-needed | 仅链接实际引用的库 |
--no-as-needed | 链接所有指定的库 |
-Map=<file> | 生成链接映射文件 |
使用链接映射文件调试
# 生成链接映射
gcc -Wl,-Map=hello.map -o hello main.c
# 查看映射文件
cat hello.map
# 包含: 各段的地址分配、符号表、库加载顺序
9.10 常见链接错误及解决
| 错误 | 原因 | 解决 |
|---|---|---|
undefined reference to 'func' | 缺少定义或未链接库 | 检查链接顺序,添加 -l 选项 |
multiple definition of 'var' | 多个 .o 定义了同名全局变量 | 使用 extern 声明或 static |
cannot find -lxxx | 库文件不存在或路径不对 | 检查 -L 路径和库名 |
relocation truncated to fit | 代码段过大 | 使用 -mcmodel=large |
symbol 'xxx' has wrong size | 符号类型不匹配 | 检查声明和定义的一致性 |
要点回顾
| 要点 | 核心内容 |
|---|---|
| 链接器任务 | 符号解析 + 重定位 |
| 静态库 | .a = .o 的归档,ar rcs 创建 |
| 动态库 | .so,-fPIC -shared 创建 |
| 链接顺序 | 被依赖的库放在后面 |
| 符号可见性 | -fvisibility=hidden + 显式导出 |
| 安全选项 | -z relro -z now -z noexecstack |
注意事项
链接顺序很重要: 被依赖的库必须放在依赖它的目标文件/库之后。
gcc main.o -lmylib,而非gcc -lmylib main.o。
PIC 性能开销:
-fPIC在 x86-64 上几乎无性能开销,但在某些架构(如 32-bit ARM)上可能有 2-5% 的性能影响。
RPATH 安全: 使用
$ORIGIN而非绝对路径,以保证可移植性。避免使用LD_LIBRARY_PATH的 setuid 程序。
SONAME 版本控制: 生产环境的共享库应设置 SONAME,便于版本管理和升级兼容性。
扩展阅读
- ld 手册 — GNU 链接器文档
- Linkers and Loaders(John Levine)— 链接器经典教材
- ELF 规范 — ELF 文件格式
- How to Write Shared Libraries — Ulrich Drepper 的共享库最佳实践
下一步
→ 10 - 库的创建与使用:学习如何创建、安装和管理 C/C++ 库,使用 pkg-config 和 RPATH。