Ver código fonte

feat: 添加链接流程

nicetry12138 1 ano atrás
pai
commit
cc21c38bc3
1 arquivos alterados com 110 adições e 0 exclusões
  1. 110 0
      Build-Project/程序员的自我修养/README.md

+ 110 - 0
Build-Project/程序员的自我修养/README.md

@@ -266,3 +266,113 @@ movl %eax, array(, %edx, 4)
 
 > 乘法由一条相对复杂的 **基址比例变址寻址**(`Base Index Scale Addressing`) 的 lea 指令完成
 
+首先C++ 语言的定义极为复杂;然后现代 CPU 也相当复杂,CPU 采用了流水线、多发射、超标量等诸多复杂的特性。为了支持这些特性,编译器的机器指令优化过程也变得十分复杂
+
+使编译过程更为复杂的是有些编译器支持多种硬件平台,即允许编译器编译出多种目标 CPU 的代码
+
+> 比如 GCC 编译器就几乎支持所有 CPU 平台
+
+```cpp
+array[index] = (index + 4) * (2 + 6);
+```
+
+经过前面的扫描、词法分析、语法分析、语义分析、源代码优化、代码生成和目标代码优化后,源代码被编译成了目标代码
+
+那么问题来了,`array` 和 `index` 的地址如何得到?
+
+进而引入新的问题:代码中有变量定义在其他模块,该怎么办?
+
+## 链接器
+
+```
+0001 0100
+...
+...
+...
+1000 0111
+```
+
+在很久之前纯二进制指令(打孔纸条)控制机器时,假设 `0001` 是跳转指令,那么上述代码第一条 `0001 0100` 就是跳转到 `0100` 也就是第四条指令中
+
+但是代码并不是一成不变的,如果在第四条之前插入新的指令,那么原本第四条指令就变成第五条指令了,对应的第一条指令也要修改 `0001 0101`。这一步就是 **重定位**(`Relocation`)
+
+再后面开始使用汇编语言,使用类似 `jmp` 来替代 `0001` 表示跳转命令。可以使用符号来标记位置,比如使用 `divide` 来表示一个除法子程序的起始地址
+
+如果将之前二进制代码的第四条指令的起始地址使用符号 `foo` 来标记,那么命令就从 `0001 0100` 变成了 `jmp foo`
+
+当使用符号命名子程序或跳转目标之后,不管 `foo` 之前插入或减少了多少条指令导致 `foo` 目标地址发生了什么变化,汇编器在每次汇编程序的时候会重新计算 `foo` 这个符号的地址
+
+上述整个过程不需要人工参与,终于不用在人工进行低级繁琐的地址调整工作
+
+现在软件开发过程中,往往会拆分成多个模块,这些模块之间互相依赖有,又相对独立。这种模块化开发的好处就是代码容易阅读、理解、重用,每个模块单独开发、编译、测试,改变部分代码不用编译整个程序等
+
+C++ 模块之间通信有两种方式:模块间的**函数调用**和模块间的**变量访问**
+
+函数访问必须知道目标函数的地址,变量访问也必须知道目标变量的地址,所以总的来的两种方式都可以归结为**模块间符号的引用**
+
+这个过程也被称为**链接**
+
+链接的主要内容就是把各个模块之间互相引用的部分都处理好,使得各个模块之间能够正确地衔接
+
+链接器要做的工作跟前面说的人工调整地址本质没什么两样。**链接**过程主要包括了**地址和空间分配**(Address And Storage Allocation)、**符号决议**(Symbol Resolution)和**重定位**(Relocation)等步骤
+
+| 步骤 | 作用 | 过程 |
+| --- | --- | --- |
+| 步骤和空间分配 | 为每个模块中的代码和数据分配内存地址和存储空间 | 链接器首先扫描所有输入的可重定位目标文件,确定每个模块的大小和对齐要求。根据这些信息,链接器为每个模块分配内存地址和存储空间,确保他们在内存中的布局符合要求 |
+| 符号决议 | 将每个符号引用与其定义关联起来 | 每个模块中定义和引用的符号都会被记录在符号表中。链接器扫描所有输入的目标文件,解析每个符号的定义和引用。链接器将每个符号引用与其定义关联起来,确保所有引用都能正确解析 |
+| 重定位 | 调整代码和数据中的地址引用,使他们指向正确的内存位置 | 编译器和汇编器生成的代码和数据通常从地址 0 开始,在实际运行时会发生变化。链接器根据地址和空间分配的结果,调整代码和数据中的地址引用,使他们指向正确的内存位置。也包括修改指令中的地址引用、调整数据段中的指针等 |
+| 合并和优化 | 合并相同的代码和数据段,并进行优化以提高执行效率 | 链接器会合并相同的代码和数据段,减少冗余。链接器还会进行一些优化操作,如消除未使用的代码和数据、优化指令顺序等 |
+
+> 代码在运行的时候才会被加载到内存中,所以上述所有的内存地址都是虚拟地址,而不是物理内存地址,这些虚拟内存地址在程序运行时会被映射到实际的物理内存地址
+
+一般来说,每个模块的源代码文件经过编译器编译成**目标文件**(Object File,一般后缀为 .0),**目标文件**和**库**(`Library`) 一起链接形成最终的可执行文件
+
+最常见的库就是**运行时库**(Runtime Library),它是支持程序运行的最基本函数的集合
+
+**库**其实时一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放
+
+```cpp
+// main.c
+#include <stdio.h>
+#include <stdbool.h>
+
+extern bool foo();
+
+int main() {
+    if (foo()) {
+        printf("foo returned true\n");
+    } else {
+        printf("foo returned false\n");
+    }
+    return 0;
+}
+
+
+// foo.c
+#include <stdbool.h>
+
+bool foo() {
+    return true;
+}
+```
+
+> `c89` 中没有定义 `bool`,`c99` 中在 `stdbool.h` 中定义了 `bool`
+
+通过 `gcc -c main.c -o main.o` 和 `gcc -c foo.c -o foo.o` 生成目标文件
+
+1. 地址和空间分配:为每个模块中的代码和数据分配虚拟地址和存储空间
+   - 扫描器扫描 `main.o` 和 `foo.o` 确定模块大小和对齐要求
+   - 为 `main` 函数和 `foo` 函数分配虚拟地址
+   - 为犬奴变量和静态数据分配存储空间
+
+2. 符号决议:链接器解析每个符号的定义和引用,将符号引用与其定义关联起来
+   - 在 `main.o` 中,链接器发现 `foo` 函数是一个外部符号(`extern`),需要解析其定义
+   - 在 `foo.o` 中,链接器找到 `foo` 函数的定义,并将 `main.o` 中对 `foo` 的引用于 `foo.o` 中的定义关联起来
+
+3. 重定位:链接器调整代码和数据中的地址引用,使他们指向正确的虚拟地址
+   - 链接器修改 `main.o` 中对 `foo` 函数的调用地址,使其指向 `foo.o` 中 `foo` 函数的实际地址
+
+4. 合并和优化:链接器合并相同的代码段和数据段,并进行优化以提高执行效率
+
+## 目标文件
+