Wiki LogoWiki - The Power of Many

Week 14: 编译与链接

深入理解编译过程、静态库、动态库、ELF 格式与链接脚本.

1. 编译过程回顾

1.1 完整流程

源代码 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件
 .c       .i       .s      .o      a.out
gcc -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 main

2.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.o

3.2 使用动态库

# 编译时链接
gcc main.c -L. -lutils -o main

# 运行时需要找到库
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main

# 或者安装到标准路径
sudo cp libutils.so /usr/local/lib/
sudo ldconfig

3.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.so

3.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' main

rpath vs runpath:

特性RPATHRUNPATH
优先级高于 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 main

5. 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 main

5.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 main

7.3 应用场景

  • 嵌入式系统开发
  • 操作系统内核
  • 内存布局控制
  • 符号地址控制

8. 练习

8.1 创建库

创建一个包含数学函数的静态库和动态库.

8.2 插件系统

使用 dlopen 实现简单的插件加载系统.

8.3 分析 ELF

分析一个可执行文件的 ELF 结构.


9. 思考题

  1. 静态库和动态库的优缺点?
  2. 位置无关代码 (PIC) 有什么作用?
  3. 符号解析顺序是什么?
  4. 什么是 GOT 和 PLT?
  5. 链接脚本的作用是什么?

10. 本周小结

  • 静态库: ar 创建, 链接时嵌入.
  • 动态库: -shared 创建, 运行时加载.
  • 动态加载: dlopen/dlsym.
  • ELF 格式: 段, 符号表.
  • 链接脚本: 控制内存布局.

理解编译和链接过程, 是解决复杂构建问题的关键.

On this page