链接
链接过程就是从编译过程拿到的可重定位目标文件中生成出可执行目标文件。
链接使得分离编译成为可能。如果平时写的是小项目,那你可认为是没有必要,杀鸡焉用牛刀;但是大型项目中,分离编译可以大量提高编译效率,充分利用并行性能。另外,模块化在现实世界中是客观存在的。
什么时候发生链接
如果是按照整个文件生成到执行的过程来看的话,一般情况是这样的:
- 编译阶段
- 预处理 cpp
- 编译 cc1
- 汇编 as
- 链接阶段←here ld
- 装载阶段
而实际上,链接本身是可以位于不同的时机:
- 编译时
- 装载时
- 运行时
第一种是静态库所用到的时机,也是上面提到的“一般情况”,而后两种是动态库用到的
编译和链接是程序构建的两个核心阶段,它们共同完成了从源代码到可执行文件的转换过程。
1. 编译(Compilation)
编译是将高级语言(如 C、C++)的源代码转换为机器码的中间步骤,包含三个子步骤:预处理、编译和汇编。其目的是生成目标文件(.o
文件),这些目标文件是编译器将源代码转换为机器代码的一部分,但它们尚不能独立运行。
编译的三个子步骤:
-
预处理(Preprocessing)
- 作用:处理所有的预处理指令(如
#include
、#define
、#ifdef
等),生成预处理后的文件。 - 输出:预处理后的文件,通常是
.i
文件。 - 功能:将头文件展开、替换宏、处理条件编译,删除注释。
gcc -E main.c -o main.i
- 作用:处理所有的预处理指令(如
-
编译(Compilation)
- 作用:将预处理后的文件转化为汇编代码。
- 输出:汇编代码文件,通常是
.s
文件。 - 功能:将高级语言代码翻译成汇编语言,这是一种更接近机器语言的表示形式,但还不是机器码。
gcc -S main.i -o main.s
-
汇编(Assembling)
- 作用:将汇编代码转化为机器代码,生成目标文件。
- 输出:目标文件,通常是
.o
文件。 - 功能:将汇编代码翻译为机器指令,但这些机器指令还不能直接运行,因为它们可能依赖其他模块或函数。
gcc -c main.s -o main.o
经过编译步骤后,生成的 .o
文件(目标文件)包含了程序的一部分机器码,但它们独立时并不完整。要生成可执行文件,还需要通过链接步骤将这些目标文件与其他库或模块结合起来。
2. 链接(Linking)
链接是将编译生成的目标文件和库文件合并在一起,生成最终的可执行文件的过程。在编译阶段生成的每个目标文件通常只包含程序的一部分代码,可能还有对其他模块或库的引用,比如对标准库函数 printf
或外部定义的函数的调用。在链接阶段,所有这些外部引用都会被解析,并最终生成完整的可执行文件。
链接的两个主要步骤:
2.1 静态链接(Static Linking)
在静态链接中,所有的符号引用(函数、变量等)都在链接阶段解析,所有需要的外部代码和数据都会被拷贝到最终的可执行文件中。因此,静态链接生成的可执行文件是完整的,独立的,不依赖外部库。
步骤:
- 链接器将多个目标文件(.o 或 .obj 文件)合并,并将符号表中的外部引用(如函数调用)与正确的符号解析相对应。
- 如果程序引用了库(如
libm.a
),链接器会将库中的符号(如数学函数)也合并到最终的可执行文件中。
优点:可执行文件独立运行,发布时不需要依赖库文件。
缺点:
- 可执行文件较大。
- 如果有多个程序使用同样的库,每个程序都需要包含一份库代码,浪费存储空间。
gcc main.o utils.o -o myprogram
2.2 动态链接(Dynamic Linking)
在动态链接中,符号引用在编译时并没有被完全解析,链接器会将目标文件中的外部符号以“占位符”方式放入最终的可执行文件中。真正的符号解析工作会在程序运行时通过动态链接库(DLL 或 shared object,.so 文件)来完成。
步骤:
- 编译器生成目标文件时,依然会保持符号引用,但不做解析,目标文件中的符号在链接阶段将不会被完全确定。
- 程序运行时,操作系统的动态链接器会加载所需的动态库(如
libm.so
)并将符号解析到实际地址。
优点:
- 节省存储空间,因为多个程序可以共享同一个动态链接库。
- 程序更新更为简便,只需要更新库文件,而不必重新编译整个程序。
缺点:运行时需要依赖动态库,库版本不兼容时可能出现问题。
gcc main.o utils.o -o myprogram -L/path/to/library -lmylib
链接的核心功能:
-
符号解析:在目标文件中,可能存在对外部函数或变量的引用,这些符号在其他目标文件或库文件中定义。链接器的任务是找到这些定义并将引用和定义匹配起来。例如,如果一个目标文件中调用了
printf
,链接器需要在标准库中找到printf
的定义并将其链接到最终的可执行文件中。 -
地址重定位:每个目标文件中的代码和数据都有自己的地址空间,但当这些文件被链接到一个可执行文件中时,链接器必须调整这些地址,以确保它们在内存中的位置是正确的。
总结
- 编译(Compilation):
- 包括预处理、编译和汇编三个阶段,生成
.o
目标文件。 - 目标文件是机器代码的片段,但不能单独运行。
- 包括预处理、编译和汇编三个阶段,生成
- 链接(Linking):
- 将多个目标文件和库文件链接起来,生成可执行文件。
- 包括静态链接和动态链接两种方式。
- 链接器负责符号解析和地址重定位,使得程序可以完整运行。
通常情况下,执行 gcc
命令时,会自动执行编译和链接两个阶段。比如,运行下面的命令:
gcc main.c utils.c -o myprogram
等同于:
- 预处理并编译
main.c
和utils.c
,生成main.o
和utils.o
目标文件。 - 将生成的目标文件
main.o
和utils.o
进行链接,生成最终的可执行文件myprogram
。
这样,通过编译和链接这两个阶段,源代码最终被转换为可以在操作系统上运行的程序。