编译和链接过程是如何工作的?
(注:此文旨在成为Stack Overflow C++ FAQ的一部分。如果您想批评提供这种形式的FAQ的想法,那么meta上的这篇发帖就是该去的地方。那个问题的答案将在C++ chatroom中进行监控,FAQ的想法最初就是从那里开始的,所以您的回答很可能会被那些提出这个想法的人阅读。)
编译和链接过程是如何工作的?
(注:此文旨在成为Stack Overflow C++ FAQ的一部分。如果您想批评提供这种形式的FAQ的想法,那么meta上的这篇发帖就是该去的地方。那个问题的答案将在C++ chatroom中进行监控,FAQ的想法最初就是从那里开始的,所以您的回答很可能会被那些提出这个想法的人阅读。)
预处理:预处理器处理C++源代码文件,处理#include
、#define
和其他预处理指令。此步骤的输出是一个"纯"的C++文件,不含预处理指令。
编译:编译器使用预处理器的输出生成目标文件。
链接:链接器使用编译器生成的目标文件生成库文件或可执行文件。
预处理器处理预处理指令,如#include
和#define
。它对C++语法不加区分,因此必须小心使用。
它逐个处理C++源文件,在其中用相应文件(通常只包含声明)替换#include
指令,替换宏(#define
),并根据#if
、#ifdef
和#ifndef
指令选择不同的文本部分。
预处理器操作的是一系列预处理令牌。宏替换定义为将一个令牌替换为另一个令牌(运算符##
使两个令牌合并成一个有意义的令牌)。
在上述转换后,预处理器会生成一个单一的输出,其中包含一系列令牌。此外,它还添加了一些特殊标记,以告诉编译器每行代码的来源,以便编译器使用这些信息生成有意义的错误消息。
使用#if
和#error
指令可以在此阶段产生某些错误。
编译步骤对预处理器的每个输出执行。编译器解析纯C++源代码(现在不包含任何预处理指令),并将其转换为汇编代码。然后调用底层后端(在工具链中是汇编器)来将该代码汇编成机器代码,生成某种格式(ELF、COFF、a.out等)的实际二进制文件。此对象文件包含输入中定义符号的已编译代码(以二进制形式)。对象文件中的符号通过名称引用。
对象文件可以引用未定义的符号。当您使用声明但没有提供定义时,就会出现这种情况。编译器不会介意这一点,并愉快地生成对象文件,只要源代码格式良好即可。
编译器通常允许您在此时停止编译。这非常有用,因为您可以单独编译每个源代码文件。它提供的优势是,如果您只更改一个文件,则无需重新编译所有内容。
生成的对象文件可以放入称为静态库的特殊存档中,以便稍后更轻松地重复使用。
在这个阶段,“常规”编译器错误,例如语法错误或失败的重载分辨率错误,将被报告。
链接器是从编译器生成的对象文件中产生最终编译输出的工具。此输出可以是共享(或动态)库(虽然名称类似,但与前面提到的静态库没有太多共同之处),也可以是可执行文件。
它通过用正确的地址替换未定义符号的引用来链接所有对象文件。这些符号可以在其他对象文件或库中定义。如果它们在标准库以外的库中定义,您需要告诉链接器有关它们的信息。
在此阶段,最常见的错误是缺少定义或重复定义。前者意味着不存在定义(即它们没有写入),或者未将包含它们的对象文件或库提供给链接器。后者很明显:相同的符号在两个不同的对象文件或库中被定义。
gcc -o hello hello.c
的执行过程如下:
通过 GNU C 预处理器(cpp.exe
)进行预处理,包括引入头文件(#include
)和展开宏定义(#define
)。
cpp hello.c > hello.i
中间结果文件"hello.i"包含了展开后的源代码。
编译器将预处理后的源代码编译成特定处理器的汇编代码。
gcc -S hello.i
-S选项指定生成汇编代码,而不是目标代码。生成的汇编文件为"hello.s"。
汇编器(as.exe
)将汇编代码转换成目标文件"hello.o"中的机器码。
as -o hello.o hello.s
最后,链接器 (ld.exe
)将目标代码与库代码链接以生成可执行文件 "hello"。
ld -o hello hello.o ...libraries...
这个主题在CProgramming.com上有讨论:
https://www.cprogramming.com/compilingandlinking.html
以下是作者的原话:
首先,这种实现方式可能更容易。编译器进行编译操作,链接器进行链接操作——通过保持函数的独立性,减少了程序的复杂度。另一个(更显然的)优点是,这允许创建大型程序而无需每次更改文件时重新编译。相反,使用所谓的“条件编译”,只需要编译那些已更改的源文件;对于其余文件,目标文件就足够作为链接器的输入。最后,这使得实现预编译代码库非常简单:只需创建对象文件并像任何其他对象文件一样链接它们。(顺便说一下,每个文件都是从其他文件包含的信息中单独编译的,这被称为“分离式编译模型”)。编译并不完全等同于创建可执行文件!实际上,创建可执行文件是一个分成两个组件的多阶段过程:编译和链接。事实上,即使程序“编译良好”,由于链接阶段的错误,它也可能无法正常工作。从源代码文件到可执行文件的整个过程最好称为构建。
编译
编译指的是处理源代码文件(.c、.cc或.cpp)并创建“对象”文件的过程。此步骤不会创建用户实际可以运行的任何东西。相反,编译器仅会生成与已编译的源代码文件相对应的机器语言指令。例如,如果您编译(但不链接)三个不同的文件,则将产生三个对象文件作为输出,每个文件的名称都为.o或.obj(扩展名取决于您的编译器)。这些文件中的每一个都包含源代码文件的翻译成机器语言文件的内容——但您现在还无法运行它们!您需要将它们转换为操作系统可以使用的可执行文件。这就是链接器的作用。
链接
链接是指从多个对象文件创建单个可执行文件的过程。在此步骤中,链接器通常会抱怨未定义的函数(通常是main本身)。在编译期间,如果编译器找不到特定函数的定义,它将假设该函数在另一个文件中定义。如果不是这种情况,则编译器不会知道——它一次只查看一个文件的内容。另一方面,链接器可能会查看多个文件并尝试找到未提及的函数的引用。
您可能会问为什么有单独的编译和链接步骤。实际上,这种分离允许您更改源代码后进行部分重新编译,而不必重新编译整个程序。如果没有分开,每次更改都需要重新编译整个程序,这会花费大量时间。
在标准前沿:
翻译单元是源文件、已包含的头文件以及通过条件预处理指令跳过的源代码行之间的组合。
标准规定了9个翻译阶段。前4个对应预处理,接下来3个是编译,下一个是模板实例化(产生实例化单元),最后一个是链接。
实际上,第8个阶段(模板实例化)通常在编译过程中完成,但有些编译器会将其延迟到链接阶段,并且有些编译器会将其分布在这两个阶段中。