%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 - [[#❓为什么需要链接?]] - [[#^r57k85|❓为什么需要重定位?]] - [[#重定位 (Relocation)|❓什么是重定位?]] - [[#符号解析(Symbol Resolution)|❓什么是符号解析?]] 3 - [[#强引用、弱引用|❓什么是强符号、弱符号、强引用、弱引用]] #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # ❓<font color="#c0504d">为什么需要链接?</font> 1. 完成**符号解析**——解决**全局符号的多重定义问题**,确保**每个符号在整个程序中最终只有一个有效定义**; 2. 完成**重定位**——将**每个符号的目标地址**修正为 "**运行时的虚拟内存地址**" ![[02-开发笔记/03-计算机基础/编译与链接/链接#^r57k85]] > [!quote] 链接的作用 > ![[_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-DEA4ADB6ED062537C0B6ABF4B4422BFE.png|606]] <br><br><br> # 链接 > 编译器为每个源代码文件**独立编译**,生成 "**可重定位目标代码文件**"(每份 "源代码文件" 各种对应一份 "可重定位目标代码文件") 「**链接**」是对**独立的编译单元**(包括**可重定位目标代码文件**、**静态/动态链接库**)进行整合,**分配地址空间,解析 ==符号引用==**,**==重定位地址==**,生成 **可执行目标文件** 的过程: 1. **将各个目标文件中的代码和数据段合并、按需排列,分配虚拟地址空间**; 2. **解析、合并各目标文件中的==符号定义及引用==** 3. 修正对**符号地址**的 **==重定位==**。 4. **==合并静态库代码==** 到**最终生成的可执行文件**中; 5. **消除重复定义的符号和不必要的代码**,优化最终的可执行文件大小。 ![image-20230912150514877|478](_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-BD7BD3363149D6EF595F731743E16F8B.png) > [!summary] 链接的目的&作用:**整合多个可重定位目标文件**(包括**动、静态链接库**),**生成可执行目标文件** <br> ## 链接的执行时机 链接可以执行于: - **编译时**(由**静态链接器**完成) - **加载时**(在程序被**加载器**加载到内存中运行时,由**动态链接器**完成对**动态库**的链接) - **运行时**(由**应用程序**来执行链接,通过 `dlopen` 等动态链接函数) <br> ## 链接的过程与步骤 在**编译时的链接过程**中,链接器需要完成两个主要任务: - **符号解析** - **重定位** - **重定位节**和**符号定义**(**==运行时地址分配==** 发生在此步) - **重定位节中的符号引用** > [!NOTE] 符号解析将目标文件中的每个全局符号都绑定到一个唯一的定义,而重定位确定每个符号的最终内存地址,并修改对那些目标的引用。 <br><br><br> # 链接时的库声明顺序 🚨 **在链接器的符号解析方式下(见下文)**,**==库文件==在命令行中的从左到右声明顺序必须满足**——"**==引用一个符号==的库**" 排在 "**==定义该符号==的库之前**"。 否则,引用就不能被解析,导致链接失败。 因此: 1. **"库文件" 通常需要放在命令行最==末尾==**: 保证 "**其他文件对库文件中符号的引用**" 出现在 "**库文件中对该符号的定义**" 之前。 2. **"库文件" 之间可能需考虑先后顺序**: - **如果各个库相互独立**(不存在一个库引用另一个库的符号),则**库之间的顺序可任意排列**; - **反之**,必须保证 **"==引用一个符号==的库" 排在 "==定义该符号==的库之前"**。 - 为满足依赖,**可在命令行上==重复声明==库名字**,确保**完成所有符号解析** > [!example] 链接时对库文件的声明顺序示例 > > 例如,对于三个静态库 `libx.a` ,`liby.a`,`libz.a` : > > - 若三个库之间没有依赖关系,则可使用命令 `gcc foo.c libx.a liby.a libz.a`; > - 若 `libx.a` 和 `libz.a` 中调用了 `liby.a` 中的函数,则需要命令为 `gcc foo.c libx.a libz.a liby.a`; > - 若 `libx.a` 调用了 `liby.a` 中,同时后者也调用了前者中的函数,则需要命令为 `gcc foo.c libx.a liby.a libx.a libz.a` (重复声明库) > [!example] > > ```shell > # gtest 库依赖于 pthread 库, 因此-lpthread 必须放在-lgtest 之后. > g++ main. cpp -o test -std=c++11 -lgtest -lpthread > ``` > ![[#链接器的符号解析方式]] <br><br><br> # 符号解析(Symbol Resolution) > ❓<font color="#c0504d">什么是符号解析?</font> 「**符号解析**」:**链接阶段,将目标文件中的每个 "==全局符号==" 都绑定到一个==唯一==的符号定义(对应于全局符号表中的一个确定符号条目)** - 对 "**局部符号**" 的解析:由 "**编译器**" 在编译时完成,只允许**每个模块中每个局部符号定义一次**; - 对 "**全局符号**" 的解析:由 "**链接器**" 在链接时完成,针对**强符号、弱符号、强引用、弱引用**采用不同规则进行处理。 > [!NOTE] **编译器在编译时**输出了各个全局符号(**==强==或==弱==**),汇编器根据**汇编文件中的符号**构建 "**符号表**" <br> ## 链接器的符号解析方式 > [!info] 链接器的符号解析方式 > > 链接器会按命令行中 "**从左到右的==声明顺序==**" 来扫描各个文件,同时**记录/维护三个集合**(初始时全空): > > - **已扫描的可重定位目标文件集合 $E$** > - **==待解析==的符号集合** $U$(出现了但尚未确定其定义的) > - **==已定义==的符号集合** $D$ > > 对于每个文件: > > - 若该文件是 "**可重定位目标文件**",则会**记录/处理其中的==所有符号定义和引用==**,并将其加入 $E$ 中: > - 若链接器在其中发现一个 "**未存在定义的符号**",就将其加入到 $U$ 中; > - 若链接器在其中发现一个 "**新的符号定义**",就将其加入 $D$ 中;**若该符号在 $U$ 中存在,就从中移除**。 > - 若该文件是 "**静态库文件**",链接器会尝试 **==匹配==** "**$U$ 中待解析的符号**" 和 "**静态库中定义的符号**", > 仅会将那些 "**==需要解析并且能够在该静态库中找到定义的符号==**" 加入 $D$ 中,而**静态库中其他==未出现于 $U$ 中的符号都将被简单丢弃==**。 > > 如果最后 $U$ 非空,则会报错 "**未定义符号**"。 > > 在该机制下,**静态库中只有那些 "==被程序使用了的符号==" 才被记录到了符号表中**。 > **当链接器解析到静态库中的一个新符号定义,但此时却==不存在对该符号的引用==时,链接器会将该符号丢弃**。 > > ^nqimjn <br> ## 强符号、弱符号定义 - 「**强符号定义**」:在目标文件中**明确定义的符号**(**已初始化的全局变量、函数**) - 「**弱符号定义**」:**"未初始化的全局变量"** & **通过 `__attribute__((weak))` 修饰的符号定义** > [!info] 链接器默认: > > - **已初始化的全局变量**、**函数** 是 "**==强符号==**"; > - **未初始化的全局变量** 是 "**==弱符号==**"。 > [!NOTE] "强符号" 与 "弱符号" 是针对 "==符号定义本身==" 而言的,与引用无关。 示例: ```cpp int my_variable = 10; // 强符号定义 int my_variable; // 弱符号定义 __attribute__((weak)) int my_variable = 10; // 弱符号定义 ``` #### 强、弱符号相关的多重定义规则 **链接器对强/弱符号定义的要求**(在整个程序中,针对 **==全局==符号** 的要求): > ![[_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-08963A30C7FA0CDDE300BE05C80C7496.png|533]] 1. **每个==强符号==定义必须==唯一==**,**不允许有多个同名的强符号定义**,否则链接器会报" **==符号重定义==**"; 2. **允许存在同名的==一个强符号==及==多个弱符号==定义**: - 若某一符号**存在一个强符号及多个弱符号**,则**链接器将取 "强符号" 定义**。 - 若某一符号**在所有目标文件中==均为弱符号==**,则**链接器将取其中 "数据类型大小" 最大的定义** > [!example] > > `gcc foo3.c bar3.c` 可成功编译,编译器将采用 "**强符号定义**" > > ![[_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-0CD9F3FDBE10D41BAC3E1EB7ADD2F443.png|558]] > > [!NOTE] 链接时启用 `-fno-common` 选项,可在遇见 "**多重定义的全局符号(无论强弱)**" 时报错 <br> ## 强引用、弱引用 - 「**强引用**」:目标文件中**对 "符号" 的普通引用**; - 「**弱引用**」:通过 `__attribute__((weak))` 或 `__attribute__((weakref))` 标识的**符号引用** 示例: ```cpp extern int my_variable; // 对外部全局变量的强引用 extern __attribute__((weak)) int my_variable; // 对外部全局变量的弱引用 void myfunc(); // 对函数的强引用 __attribute__(weak)) void myfunc(); // 对函数的弱引用(也即弱符号定义) ``` #### 强、弱引用相关规则 **链接器对强/弱引用的要求**(在整个程序中,针对 "**外部符号**" 的要求): 1. 对于**强引用**:要求**被引用的符号在链接时==必须存在==**,否则报错 "==**未定义符号**==**"; 2. 对于**弱引用**:**被引用的符号在链接时==可以不存在==**,当**符号不存在**时会**将符号值设置为 0**,但**链接器不会报错**。 > [!example] 弱引用特性示例 > > ```cpp title:weakref.cpp > // 弱引用: 如果在链接时未找到该符号的定义, 则其符号值(地址值)将会是0, 但不会报错. > > // 声明为"弱引用", 如果符号在链接时没有定义, 其符号值(地址值)将会是0(运行时若访问将触发段错误) > __attribute__((weak)) void foo(); > > // 声明为"弱引用" 并指定 "别名", 如果符号在链接时没有定义, 则调用别名函数. > __attribute__((weak, alias("foo_replace"))) void foo2(); > > void foo_replace() { > printf("foo_replace\n"); > } > > // 下述实现保证了当foo函数定义存在时, 调用foo函数. > // 若foo函数定义不存在, 则不调用foo函数, 同时不会导致编译报错或运行时错误. > int main() { > if (foo) { // 如果foo函数定义存在, 则调用. > foo(); > } > // 如果foo2函数定义存在则调用foo2, 否则调用foo_replace函数. > foo2(); > } > ``` > <br><br><br> # 重定位 (Relocation) > ❓<font color="#c0504d">什么是重定位?</font> 「**重定位**」:**==合并==各个链接输入模块(可重定位目标文件、静态库)** 并**为每个符号==分配运行时地址==** 的过程。 重定位包括两个步骤: - **重定位 "==节==" 和 "==符号定义=="**: **合并多个输入文件中的节** & **分配运行时内存地址** - **重定位 "节中的==符号引用==**":**修改代码节、数据节中对每个符号的引用,使其指向最终的运行时内存地址**。 > [!NOTE] ❓ <font color="#c0504d">为什么需要重定位?</font> > > 原因有两点: > > 1. 合并多个输入文件,为**合并后的节和符号定义**分配**运行时内存地址** > 2. 修正每个符号引用,使其**指向该符号最终的运行时内存地址** > > 关于第二点的说明: > > 编译器**对每个源文件的编译是==独立==进行的**, > 当**一个编译单元中==引用==了另一个编译单元中==定义的符号==(例如==全局变量、函数、类==等)** 时, > **编译器在编译时无法知道这些 "外部定义" 的具体地址**,因而只产生了"**临时地址**"(例如**全 0 地址**),同时 > > 重定位的目的之一即在于 **==修正/明确每个符号的目标地址==**——将 "**对临时地址的引用**" 修改为 "**对运行时实际的==绝对内存地址==引用**" [^6]。 > ^r57k85 > <br> ## (1)重定位节和符号定义——空间与地址分配(Address and Storage Allocation) 该步骤中包括两点: 1. **==合并多个输入文件中的节==**——采用 "**相似段合并**" 的方式,**将所有==同一类型的节==合并得到新的==聚合节==**。 2. **==虚拟地址分配==**——**为 "==合并后的节段 & 符号定义==" 分配运行时的虚拟内存地址** #### 相似段合并 例如**所有文件的 `.text` 节统一合并在一起,`.data` 节等同理。** ![[_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-5629A2FB92B71B4D4C58BC84C26C85EC.png|409]] #### 虚拟地址分配 > **虚拟内存地址**(Virtual Memory Address,VMA) 1. 对 ELF 文件中各个 "**段**" 进行 **==虚拟地址空间分配==**,这一地址也即**程序被加载到内存中后,各个段将被映射到的虚拟内存地址**。 2. **确定各个==符号的虚拟内存地址==**: "**符号**" 在各个节段内的相对位置已固定的,因此只需要**计算 "相较于段起始地址" 的偏移量**即可。 > [!example] 示例: > > - 对 "**可重定位目标文件**",其各个段的 "**地址**" 均为 0,因为尚未进行链接。 > - 对 "**可执行目标文件**",其各个段的 "**地址**" 已确定,即为 **==进程中各个段所映射到的虚拟内存地址==**。 > > ![[_attachment/02-开发笔记/03-计算机基础/编译与链接/链接.assets/IMG-链接-9E87973FF3FF65AD29BE93D3F81A49FA.png|921]] > <br> ## (2)重定位节中的符号引用 此步中,链接器将编译时在 "**可重定位目标文件**" 中为**各符号**使用的 "**==临时地址==**" 修改为 **运行时实际的==绝对内存地址==**。 这一过程依赖于 ELF 文件中的 [[02-开发笔记/03-计算机基础/编译与链接/目标文件#重定位表|重定位表]] 进行,表中的每一个条目(**重定位入口**)都是**对一个符号的引用**, 当链接器需要**对某个符号的引用进行重定位**时,则会查找 **==全局符号表==**,获取**符号对应实体的==地址**==(**==符号值==**),进行替换。 <br><br><br> # 「静态链接」与「动态链接」 「**静态链接**」与「**动态链接**」是程序代码**链接库文件**的两种方式。 - 「**静态链接**」:在链接过程中**将程序所依赖的库代码==直接嵌入==到最终生成的可执行文件中**。 - 优点: - 通过静态链接生成的可执行文件**在==运行时无需依赖外部库文件==**,可独立运行。 - 缺点: - 生成的可执行文件**体积会较大**; - 当程序要**使用新版本的动态库**时,需**重新编译生成可执行文件**。 - **空间浪费**:当多个程序**静态链接**到**同一个静态库**时,代码嵌入会导致**磁盘和内存上的空间浪费**。 - 「**动态链接**」:由程序**在==运行时==将库代码==动态加载到内存中==**(而不会在链接时嵌入到生成的可执行文件) - 优点: - **运行时加载**:库代码在程序运行时加载,可减少可执行文件的大小。 - **易于更新**:当程序需要**使用新版本的动态库**时,只需**替换库文件**即可。 - **代码复用**:当多个程序需要**动态链接**到**同一个动态库**时,内存中**只会存在==一份动态库的实例==**,为**所有引用该库的进程所==共享==**。 - 缺点: - **==运行时依赖==**:程序运行**依赖于外部动态库文件**,**若动态库文件不存在,则程序无法运行**。 - **==库版本兼容性问题==**:**若程序所需的动态库版本与运行时环境中提供的不一致可能导致错误** > [!important] "静态链接、动态链接" 与 "静态库、动态库" 的对应关系 > > > - 「**静态库**」被设计于应用在 "**==静态链接==**" 的场景(也可**通过 `dlopen` 动态加载函数在运行时动态加载**) > > > > > - 「**动态库**」被设计于应用在 "**==动态链接==**" 的场景,但也可通过 "==**静态链接**==" 地方式**将动态库的代码和数据内嵌到生成的可执行文件中**(例如 `gcc -static` 选项) ^w3s7wr <br><br> # 参考资料 # Footnotes [^1]: [C语言 | 什么是动态链接与静态链接?](https://blog.51cto.com/u_15244533/2845292) [^2]: [计算机那些事(5)——链接、静态链接、动态链接](http://chuquan.me/2018/06/03/linking-static-linking-dynamic-linking/) [^3]: [C++静态库与动态库的区别?](https://blog.csdn.net/dd_hello/article/details/81782934) [^4]: [C语言 Windows下使用gcc制作静态库与动态库](https://blog.csdn.net/TIME_LEAF/article/details/115333179) [^5]: [linux下 GCC编译链接静态库&动态库 ](https://www.cnblogs.com/thechosenone95/p/10605172.html) [^6]: 《程序员的自我修养:链接、装载与库》