Week 14: 编译与链接
深入理解编译过程、静态库、动态库、ELF 格式与链接脚本.
1. 编译过程回顾
1.1 完整流程
源代码 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件
.c .i .s .o a.outgcc -E main.c -o main.i # 预处理
gcc -S main.i -o main.s # 编译
gcc -c main.s -o main.o # 汇编
gcc main.o -o main # 链接1.2 目标文件类型
| 类型 | 描述 |
|---|---|
| 可重定位文件 (.o) | 待链接的目标文件 |
| 可执行文件 | 可直接运行 |
| 共享库 (.so / .dll) | 动态链接库 |
2. 静态库
2.1 创建静态库
# 编译目标文件
gcc -c math_ops.c -o math_ops.o
gcc -c string_ops.c -o string_ops.o
# 创建静态库
ar rcs libutils.a math_ops.o string_ops.o
# ar 选项:
# r: 插入/替换
# c: 创建
# s: 索引2.2 使用静态库
# 编译并链接
gcc main.c -L. -lutils -o main
# -L.: 在当前目录查找库
# -lutils: 链接 libutils.a
# 或者直接指定
gcc main.c libutils.a -o main2.3 查看静态库内容
ar -t libutils.a # 列出成员
nm libutils.a # 查看符号2.4 静态库特点
- 链接时复制代码到可执行文件
- 可执行文件较大
- 不依赖外部库文件
- 更新库需重新编译
3. 动态库
3.1 创建动态库
# 编译位置无关代码
gcc -fPIC -c math_ops.c -o math_ops.o
gcc -fPIC -c string_ops.c -o string_ops.o
# 创建共享库
gcc -shared -o libutils.so math_ops.o string_ops.o3.2 使用动态库
# 编译时链接
gcc main.c -L. -lutils -o main
# 运行时需要找到库
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
# 或者安装到标准路径
sudo cp libutils.so /usr/local/lib/
sudo ldconfig3.3 动态库版本
# 创建带版本号的库
gcc -shared -Wl,-soname,libutils.so.1 -o libutils.so.1.0.0 *.o
# 创建符号链接
ln -s libutils.so.1.0.0 libutils.so.1
ln -s libutils.so.1 libutils.so3.4 动态库特点
- 运行时加载
- 可执行文件较小
- 多程序共享
- 可独立更新 (保持 ABI 兼容)
3.5 rpath 与 runpath (动态库搜索路径)
动态链接器 (ld.so) 搜索共享库的顺序:
1. DT_RPATH (可执行文件内嵌, 已弃用)
2. LD_LIBRARY_PATH 环境变量
3. DT_RUNPATH (可执行文件内嵌, 推荐)
4. /etc/ld.so.cache 缓存
5. /lib, /usr/lib 默认路径设置 rpath/runpath:
# 编译时设置 RPATH (传统, 优先级高于 LD_LIBRARY_PATH)
gcc main.c -L./lib -lutils -Wl,-rpath,./lib -o main
# 编译时设置 RUNPATH (推荐, 可被 LD_LIBRARY_PATH 覆盖)
gcc main.c -L./lib -lutils -Wl,-rpath,./lib,--enable-new-dtags -o main
# 使用 $ORIGIN (相对于可执行文件的路径)
gcc main.c -L./lib -lutils -Wl,-rpath,'$ORIGIN/lib' -o main查看和修改:
# 查看 RPATH/RUNPATH
readelf -d main | grep -E 'RPATH|RUNPATH'
# 使用 chrpath 修改 (需要安装)
chrpath -r '$ORIGIN/lib' main
# 使用 patchelf 修改
patchelf --set-rpath '$ORIGIN/lib' mainrpath vs runpath:
| 特性 | RPATH | RUNPATH |
|---|---|---|
| 优先级 | 高于 LD_LIBRARY_PATH | 低于 LD_LIBRARY_PATH |
| 传递性 | 影响依赖库的依赖 | 不影响 |
| 安全性 | 较低 | 较高 |
| 推荐 | 否 | 是 |
安全注意: 避免在 setuid 程序中使用 rpath, 可能导致权限提升漏洞.
4. 动态加载
4.1 dlopen API
#include <dlfcn.h>
int main(void) {
// 加载库
void *handle = dlopen("./libplugin.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error: %s\n", dlerror());
return 1;
}
// 获取函数指针
typedef int (*plugin_func)(int);
plugin_func func = (plugin_func)dlsym(handle, "process");
char *error = dlerror();
if (error) {
fprintf(stderr, "Error: %s\n", error);
dlclose(handle);
return 1;
}
// 调用函数
int result = func(42);
printf("Result: %d\n", result);
// 卸载库
dlclose(handle);
return 0;
}gcc main.c -ldl -o main5. ELF 格式
5.1 ELF 结构
+-------------------+
| ELF Header |
+-------------------+
| Program Headers |
+-------------------+
| .text (代码) |
+-------------------+
| .rodata (只读数据)|
+-------------------+
| .data (初始化数据)|
+-------------------+
| .bss (未初始化) |
+-------------------+
| Section Headers |
+-------------------+5.2 查看 ELF 信息
# ELF 头
readelf -h main
# 段信息
readelf -S main
# 程序头
readelf -l main
# 符号表
readelf -s main
nm main
# 动态依赖
ldd main5.3 常见段
| 段 | 描述 |
|---|---|
| .text | 代码段 |
| .rodata | 只读数据 |
| .data | 已初始化全局变量 |
| .bss | 未初始化全局变量 |
| .symtab | 符号表 |
| .strtab | 字符串表 |
| .rel.text | 重定位信息 |
| .dynamic | 动态链接信息 |
6. 符号与链接
6.1 符号类型
$ nm main.o
0000000000000000 T main # T: 代码段
0000000000000000 D global # D: 已初始化数据
0000000000000004 C common # C: 未初始化公共
U printf # U: 未定义 (需链接)6.2 符号可见性
// 默认: 全局可见
int public_var = 10;
void public_func(void) { }
// static: 局部可见
static int private_var = 20;
static void private_func(void) { }
// GCC: 控制可见性
__attribute__((visibility("hidden")))
void hidden_func(void) { }6.3 弱符号
// 弱符号: 如果有强符号则被覆盖
__attribute__((weak))
void weak_func(void) {
printf("Default implementation\n");
}7. 链接脚本
7.1 基本结构
/* linker.ld */
ENTRY(_start)
SECTIONS {
. = 0x10000;
.text : {
*(.text)
}
.rodata : {
*(.rodata)
}
.data : {
*(.data)
}
.bss : {
*(.bss)
}
}7.2 使用链接脚本
gcc -T linker.ld main.c -o main7.3 应用场景
- 嵌入式系统开发
- 操作系统内核
- 内存布局控制
- 符号地址控制
8. 练习
8.1 创建库
创建一个包含数学函数的静态库和动态库.
8.2 插件系统
使用 dlopen 实现简单的插件加载系统.
8.3 分析 ELF
分析一个可执行文件的 ELF 结构.
9. 思考题
- 静态库和动态库的优缺点?
- 位置无关代码 (PIC) 有什么作用?
- 符号解析顺序是什么?
- 什么是 GOT 和 PLT?
- 链接脚本的作用是什么?
10. 本周小结
- 静态库: ar 创建, 链接时嵌入.
- 动态库: -shared 创建, 运行时加载.
- 动态加载: dlopen/dlsym.
- ELF 格式: 段, 符号表.
- 链接脚本: 控制内存布局.
理解编译和链接过程, 是解决复杂构建问题的关键.