• 作者:老汪软件技巧
  • 发表时间:2024-08-23 11:02
  • 浏览量:

静态链接基本概念

链接 (linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行 。

链接可以执行于编译时 (compile time), 也就是在源代码被翻译成机器代码时;

也可以执行于加载时 (load time), 也就是在程序被加载器 (load­er)加载到内存并执行时;

甚至执行于运行时 (runtime), 也就是由应用程序来执行。

在早期的计算机系统中,链接是手动执行的。在现代系统中,链接是由叫做链接器 (linker) 的程序自动执行的

链接器在软件开发中扮演着一个关键的角色,因为它们使得分离编译 (separate com- pilation)成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以 把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

引用 >

静态链接器 (static linker) 以一组可重 定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出

目标文件(.o)

目标文件可分为三种形式

包含二进制代码和数据。其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。例如编译 main.c 生成的 main.o

包含二进制代码和数据。可以执行复制到内存并执行。例如链接后生成的可执行文件

一种特殊类型的可重定位目标文件,可以在加载或运行时被动态的加载进内存并链接。例如动态库 dylib

libxxx.a 文件

随着软件行业的发展,人们很快就开始共享这些对象文件。但是这个分享过程很繁琐,于是人们就想出把一系列 .o 文件打包成一个「归档」用于共享。在当时,将一系列 .o 文件打包在一起的标准方法是使用一个名为 「ar」 的归档工具,它一般被用于二进制的备份和分发上。

此时链接的工作流就变成了这样:用 ar 将多个 .o 文件放入归档文件 .a 中,然后修改链接器让它知道如何读取从 .a 文件中提取 .o 文件。这一机制对于代码复用是一个很大的改进,今天我们习惯将这些库或者归档文件称为「静态库」(.a 文件)。

再后来,我们发现最终链接而成的程序变得越来越大,因为那些库中数以千计的函数都会被链接器全部复制到最终的二进制中,即使只有其中一部分的函数会被最终使用。因此,链接器中加入了一个优化:它不会去链接静态库中所有的 .o 文件,而是在决议某个未定义符号时才会去静态库中按需拉取对应的 .o 文件。 这意味着人们可以创建一个包含所有 C 标准库函数的巨大 libc 静态库,然后让每个程序都链接到这个 libc 库,而每个程序只会得到实际需要的那一部分

符号

每个可重定位目标模块 m 都有一个符号表,它包含 m 定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

由模块 m 定义并能被其他模块引用的全局符号。对应上述例子中 static1_method和hello由其他模块定义并被模块 m 引用的全局符号,这些符号也称为外部符号。对应 printf符号只被模块 m 定义和引用的局部符号,它们对应于带 static 属性的 c 函数和全局变量。这些符号在模块 m 中任何位置都可见,但是不能被其他模块引用。对应 static int a = 10;

static int a = 10;
// static1_method 全局导出符号
void static1_method() {
    // printf 未定义、引用外部符号
    printf("static1_method \n");
}
// hello 全局导出符号
void hello() {
    printf("Hello, static-1 \n");
}

符号解析

在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的 .c 文件翻译为 .o 文件) 在这次扫描中,链接器会维护三个集合:

可重定位目标文件的集合 E(这个集合中的文件会被合并起来形成可执行文件)一个未解析的符号集合 U (即引用了但是尚未定义的符号)以及一个在前面输入文件中已定义的符号集合 D。

初始时, E、U 和 D 均为空

对于命令行上的每个输入文件 f,链接器会判断 f 是一个目标文件还是一个存档文件。如果 f 是一个目标文件,那么链接器把 f 添加到 E,修改 U 和 D 来反映 f 中符号的定义和引用,并继续下一个输入文件如果 f 是一个存档文件,那么链接器就尝试匹配 U 中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员 m,定义了一个符号来解析 U 中的一个引用,那么就将 m 添加到 E 中,并且链接器修改 U 和 D 来反映 m 中的符号定义和引用。对存档文件中所有的成员目标文件都依次进行这个过程,直到 U 和 D 都不再发生变化。此时,任何不包含在 E 中的成员目标文件都简单的被丢弃,而链接器将继续处理下一个输入文件如果当链接器完成对命令行上输入文件的扫描后,U 是非空的,那么链接器就会抛出一个错误并终止。否则,它会合并和重定位 E 中的目标文件,构建输出的可执行文件

可参考动画讲解CSAPP链接

符号冲突

上面了解了静态链接的基本原理后,下面通过几个例子来具体看一下重复符号的冲突以及链接器是如何决议的

以下例子使用平台为 mac m1 芯片, 编译器为 clang、链接器为 clang 自带链接器 ld64

静态库与静态库符号冲突

准备两个文件 static1.c、static2.c,内部都定义了 hello 函数,分别生成两个动态库。

准备 main.c 文件,调用 hello 函数,观察结果

static1.c

// static1.c
#include 
void static1_method() {
    printf("static1_method \n");
}
void hello() {
    printf("Hello, static-1 \n");
}

static2.c

// static2.c
#include 
void static2_method() {
    printf("static2_method \n");
}
void hello() {
    printf("Hello, static-2 \n");
}

编译 static1、static2 生成静态库

clang -c static1.c -o static1.o
clang -c static2.c -o static2.o
ar rcs libstatic1.a static1.o
ar rcs libstatic2.a static2.o

4. main 函数直接调用 hello 函数,观察结果

#include 
// 声明库中的函数
void hello();
void static1_method();
void static2_method();
int main() {
    hello();
    return 0;
}

clang -o main main.c -L. -lstatic1 -lstatic2
./main
// 输出结果
Hello, static-1 

说明这里的 hello 实际调用的是 static1 中的代码。

静态符号冲突链接是什么__静态符号冲突链接怎么设置

此时我们调换一下链接顺序,把-lstatic2 换到前面,-lstatic1 换到后面

clang -o main main.c -L. -lstatic2 -lstatic1
./main
// 输出结果
Hello, static-2

从输出结果可以看出调用的是 static2 中的代码。

我们继续来修改 main.c 内的代码

int main() {
    // libstatic1.a
    static1_method();
    // libstatic2.a
    static2_method();
    hello();
    return 0;
}

先调用 static1_method() 和 static2_method() 两个方法,再调用 hello 函数。

clang -o main main.c -L. -lstatic1 -lstatic2
./main
// 输出结果
duplicate symbol '_hello' in:
    ./libstatic1.a[2](static1.o)
    ./libstatic2.a[2](static2.o)
ld: 1 duplicate symbols
clang: error: linker command failed with exit code 1 (use -v to see invocation)

此时不论怎么调换链接顺序,都会报错

先来总结一下上述两种情况

直接调用 hello 函数,正常运行,运行结果跟链接参数的顺序有关系先调用各自库中其他函数,再调用 hello 函数,此时链接失败

针对第一种情况,按照符号解析原理来看,main 中未定义符号 _hello,在遇到-lstatic1 的时候找到了,所以在遇到-lstatic2 的时候发现没有未定义符号,所以并没有链接 static2 库中目标文件。此时最后的可执行文件中只包含了 static1 中的符号,所以并不会冲突。反之调换链接顺序是一样的道理

针对第二种情况,首先调用了static1_method、static2_method,相当于把 static1 和 static2 都链接进来了,静态库的链接原则是不允许有重复符号,所以在碰到相同符号 _hello 时就会报错

在第二种情况的前提下,再来看最后一种情况,代码不需要修改,只需在链接时增加 -dead_strip 选项

clang -o main main.c -L. -lstatic1 -lstatic2 -Xlinker -dead_strip
./main
// 输出结果
duplicate symbol '_hello' in:
    ./libstatic1.a[2](static1.o)
    ./libstatic2.a[2](static2.o)
static1_method 
static2_method 
Hello, static-1 

仍然在上面生成了符号冲突的报错,但是结果正常输出了。说明此时只是警告,链接器自己选择了一个符号

继续修改 -lstatic1 -lstatic2 选项,使-lstatic2 在前,-lstatic1 在后

clang -o main main.c -L. -lstatic2 -lstatic1 -Xlinker -dead_strip
./main
// 输出结果
duplicate symbol '_hello' in:
    ./libstatic2.a[2](static2.o)
    ./libstatic1.a[2](static1.o)
static1_method 
static2_method 
Hello, static-2 

说明链接器还是根据参数顺序来自动选择符号的。

-dead_strip死代码剥离。这个选项会让链接器删除那些无法访问的代码和数据。死代码剥离可以隐藏许多静态库中的问题,通常情况下,缺少符号或重复的符号会导致链接器出错,但是死代码剥离会导致链接器从 main 函数开始对所有代码和数据进行可达性检测,如果这时链接器发现缺失的符号是来自一段不可达的代码,链接器将抑制这个符号缺失的错误。类似的,如果 ld64 发现了来自不同静态库的重复符号,链接器将选择第一个遇到的符号而不是直接报错

静态库与动态库符号冲突

静态库与动态库符合冲突分为两种情况

若静态库未被强制加载,则依然根据链接顺序来决定,-lshared1 在前,就会使用动态库中符号,-lstatic1 在前,就会使用静态库内符号若静态库内符号被强制加载,则会无视链接顺序,默认静态库符号比动态库符号优先级高(如果动态库链接顺序在前,先找到了动态库符号,再碰到静态库中有相同符号时,会把符号的地址替换为静态库内的符号地址)

例子:

static1.c

#include 
void static1_method() {
    printf("static1_method \n");
}
void hello() {
    printf("Hello, static-1 \n");
}

shared1.c

#include 
void shared1_method() {
    printf("shared1_method \n");
}
void hello() {
    printf("Hello, shared-1\n");
}

main.c

int main() {
    static1_method();
    shared1_method();
    hello();
    return 0;
}

编译链接

// 静态库
clang -c static1.c -o static1.o
ar rcs libstatic1.a static1.o
// 动态库
clang -dynamiclib -o libshared1.dylib shared1.c
clang -o main main.c -L. -lshared1 -lstatic1
./main 
// 输出结果
static1_method 
shared1_method 
Hello, static-1 

有关符合的决议过程。感兴趣的可参考 ld64 的源码ld64可调试源码

动态库与动态库符合冲突

动态库符号冲突不会报错,系统会根据符号的强弱规则以及链接顺序选择符号

感兴趣的可按照上述方式写 demo 测试,也可直接通过 ld64 源码查看处理逻辑

参考资料

Library order in static linking - Eli Bendersky’s website

GCC 同名符号冲突解决办法_怎么解决符号抢占-CSDN博客

C/C++多个链接库存在同名函数,编译会报错吗?

C++库符号冲突杂谈

深入 iOS 静态链接器(一)— ld64

ld64可调试源码

CSAPP链接

动画讲解CSAPP链接

WWDC22 110362