编译/链接过程如何工作?

How does the compilation/linking process work?

提问人: 提问时间:6/7/2011 最后编辑:10 revs, 5 users 25%Tony The Lion 更新时间:3/26/2021 访问量:289065

问:

编译和链接过程如何工作?

(注意:这是Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供常见问题解答的想法,那么在开始这一切的 meta 上的帖子将是这样做的地方。该问题的答案在 C++ 聊天室中受到监控,FAQ 的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。

C 编译器构造 链接器 C++-FAQ

评论


答:

692赞 8 revs, 7 users 81%R. Martinho Fernandes #1

C++程序的编译涉及三个步骤:

  1. 预处理:预处理器采用 C++ 源代码文件并处理 s、s 和其他预处理器指令。此步骤的输出是没有预处理器指令的“纯”C++ 文件。#include#define

  2. 编译:编译器获取预处理器的输出并从中生成目标文件。

  3. 链接:链接器获取编译器生成的对象文件,并生成库或可执行文件。

预处理

预处理器处理预处理器指令,如 和 。它与 C++ 的语法无关,这就是为什么必须谨慎使用它的原因。#include#define

它一次处理一个 C++ 源文件,方法是将指令替换为相应文件的内容(通常只是声明),替换宏 (),并根据 和 指令选择文本的不同部分。#include#define#if#ifdef#ifndef

预处理器处理预处理令牌流。宏替换被定义为用其他标记替换标记(运算符允许在有意义时合并两个标记)。##

完成所有这些操作后,预处理器将生成单个输出,该输出是由上述转换产生的令牌流。它还添加了一些特殊的标记,告诉编译器每行来自哪里,以便它可以使用这些标记来生成合理的错误消息。

在这个阶段,巧妙地使用 and 指令可能会产生一些错误。#if#error

汇编

编译步骤在预处理器的每个输出上执行。编译器解析纯 C++ 源代码(现在没有任何预处理器指令)并将其转换为汇编代码。然后调用底层后端(工具链中的汇编器),将该代码组装成机器代码,生成某种格式(ELF、COFF、a.out 等)的实际二进制文件。此目标文件包含输入中定义的符号的编译代码(二进制形式)。对象文件中的符号按名称引用。

对象文件可以引用未定义的符号。当您使用声明,并且不为其提供定义时,就是这种情况。编译器并不介意这一点,只要源代码格式正确,就会很乐意生成目标文件。

编译器通常允许您在此时停止编译。这非常有用,因为有了它,您可以单独编译每个源代码文件。这样做的好处是,如果只更改单个文件,则无需重新编译所有内容。

生成的目标文件可以放在称为静态库的特殊存档中,以便以后重用。

在此阶段,会报告“常规”编译器错误,例如语法错误或失败的重载解析错误。

连接

链接器是从编译器生成的对象文件生成最终编译输出的。此输出可以是共享(或动态)库(虽然名称相似,但它们与前面提到的静态库没有太多共同之处)或可执行文件。

它通过将对未定义符号的引用替换为正确的地址来链接所有对象文件。这些符号中的每一个都可以在其他对象文件或库中定义。如果它们是在标准库以外的库中定义的,则需要将它们告知链接器。

在此阶段,最常见的错误是缺少定义或重复定义。前者意味着定义不存在(即它们未写入),或者它们所在的对象文件或库未提供给链接器。后者是显而易见的:在两个不同的对象文件或库中定义了相同的符号。

评论

50赞 manav m-n 1/24/2013
编译阶段还会在转换为目标文件之前调用汇编程序。
4赞 Bart van Heukelom 5/21/2014
优化应用在哪里?乍一看,它似乎会在编译步骤中完成,但另一方面,我可以想象只有在链接后才能进行适当的优化。
8赞 R. Martinho Fernandes 5/21/2014
@BartvanHeukelom传统上它是在编译过程中完成的,但现代编译器支持所谓的“链接时间优化”,其优点是能够跨翻译单元进行优化。
3赞 Kevin Zhu 7/3/2014
C 有相同的步骤吗?
7赞 Dan Carter 9/11/2014
如果链接器将引用库中的类/方法的符号转换为地址,这是否意味着库二进制文件存储在操作系统保持不变的内存地址中?我只是对链接器如何知道所有目标系统的 stdio 二进制文件的确切地址感到困惑。文件路径将始终相同,但确切的地址可能会更改,对吧?
27赞 AProgrammer #2

在标准方面:

  • 翻译单元是源文件、包含的标头和源文件的组合,减去条件包含预处理器指令跳过的任何源行。

  • 该标准定义了翻译的 9 个阶段。前四个对应预处理,接下来三个是编译,下一个是模板的实例化(生成实例化单元),最后一个是链接。

在实践中,第八阶段(模板的实例化)通常在编译过程中完成,但有些编译器将其延迟到链接阶段,有些则将其分散到两个阶段。

评论

15赞 jalf 6/7/2011
你能列出所有 9 个阶段吗?我认为,这将是对答案的一个很好的补充。:)
0赞 sbi 6/7/2011
@jalf: 相关新闻: stackoverflow.com/questions/1476892/....
0赞 AProgrammer 6/7/2011
@jalf,只需在@sbi指出的答案的最后一个阶段之前添加模板实例化即可。IIRC 在处理宽字符时,精确措辞存在细微差异,但我认为它们不会出现在图表标签中。
2赞 jalf 6/7/2011
@sbi是的,但这应该是常见问题解答,不是吗?那么,这些信息不应该在这里提供吗?;)
3赞 jalf 6/7/2011
@AProgrammmer:简单地按名称列出它们会有所帮助。然后,如果人们想要更多细节,他们就知道要搜索什么。无论如何,+1'ed你的答案无论如何:)
59赞 2 revs, 2 users 68%neuronet #3

本专题将在 CProgramming.com 讨论:
https://www.cprogramming.com/compilingandlinking.html

这是作者在那里写的:

编译与创建可执行文件并不完全相同! 相反,创建可执行文件是一个多阶段过程,分为 两个组件:编译和链接。实际上,即使一个程序 “编译良好”它实际上可能不起作用,因为在 链接阶段。从源代码文件开始的整个过程 对于可执行文件,最好将其称为构建。

汇编

编译是指对源代码文件(.c、.cc 或 .cpp)并创建“对象”文件。此步骤不会创建 用户可以实际运行的任何内容。相反,编译器只是 生成与 源代码文件。例如,如果编译(但 不要链接)三个单独的文件,您将拥有三个目标文件 创建为输出,每个名称为 .o 或 .obj (扩展将取决于您的编译器)。这些文件中的每一个 包含将源代码文件转换为计算机的翻译 语言文件 -- 但你还不能运行它们!你需要转动它们 转换为操作系统可以使用的可执行文件。这就是 链接器进来了。

连接

链接是指从 多个对象文件。在此步骤中,链接器通常会 抱怨未定义的函数(通常是 main 本身)。在 编译,如果编译器找不到 特定函数,它只是假设该函数是 在另一个文件中定义。如果不是这种情况,则不可能 编译器会知道 -- 它不会查看超过 一次一个文件。另一方面,链接器可能会查看 多个文件,并尝试查找以下函数的引用 没有提到。

你可能会问为什么有单独的编译和链接步骤。 首先,以这种方式实现事情可能更容易。编译器 做它的事情,链接器做它的事情——通过保持 功能分离,降低了程序的复杂度。另一个 (更明显的)优点是这允许创建大 程序,而不必在每次文件时重做编译步骤 已更改。取而代之的是,使用所谓的“条件编译”,它是 仅编译已更改的源文件是必要的;为 其余的,目标文件是链接器的足够输入。 最后,这使得实现预编译库变得简单 代码:只需创建对象文件并像其他文件一样链接它们 对象文件。(事实上,每个文件都是从 顺便说一句,包含在其他文件中包含的信息称为 “单独的编译模型”。

为了获得条件编译的全部好处,可能是 获得一个程序来帮助你比试图记住哪个更容易 自上次编译以来更改的文件。(当然,你可以 只需重新编译每个时间戳大于 相应对象文件的时间戳。如果您正在使用 集成开发环境 (IDE),它可能已经处理好了 这是给你的。如果您使用的是命令行工具,那么有一个漂亮的 大多数 *nix 发行版都附带的称为 make 的实用程序。沿 通过条件编译,它还有其他几个不错的功能 编程,例如允许对程序进行不同的编译 -- 例如,如果您有一个版本生成用于调试的详细输出。

了解编译阶段和链接之间的区别 阶段可以更轻松地搜寻错误。编译器错误通常是 本质上是句法 -- 缺少分号,多一个括号。 链接错误通常与缺失或多个有关 定义。如果收到函数或变量 从链接器中多次定义,这是一个很好的指示 错误是您的两个源代码文件具有相同的功能 或可变。

评论

2赞 binarysmacker 12/22/2013
我不明白的是,如果预处理器管理诸如 #includes 以创建一个超级文件之类的事情,那么在那之后就没有什么可链接的了吗?
0赞 Elliptical view 1/10/2014
@binarysmacer 看看我在下面写的内容对你有意义吗?我试图从内到外描述这个问题。
8赞 Karan Joisher 6/17/2016
@binarysmacker 现在对此发表评论为时已晚,但其他人可能会觉得这很有用。youtu.be/D0TazQIkc8Q基本上,您包含头文件,这些头文件通常只包含变量/函数的声明,而没有定义,定义可能存在于单独的源文件中。因此,预处理器仅包含声明,而不包括定义,这就是链接器的用武之地。将使用变量/函数的源文件与定义它们的源文件链接。
0赞 Display Name 5/8/2021
很抱歉打断:“从源代码文件到可执行文件的整个过程可能最好称为构建”,如果最终输出是静态库或动态库而不是可执行文件,情况如何?“构建”一词是否仍然合适?
26赞 2 revsmy username was hijacked here #4

瘦身是 CPU 从内存地址加载数据,将数据存储到内存地址,并在内存地址外按顺序执行指令,并在处理的指令序列中进行一些条件跳转。这三类指令中的每一类都涉及计算要在机器指令中使用的存储单元的地址。由于机器指令的长度是可变的,具体取决于所涉及的特定指令,并且由于我们在构建机器代码时将它们的可变长度串在一起,因此计算和构建任何地址都涉及两个步骤。

首先,我们尽可能地分配内存,然后才能知道每个单元格中到底发生了什么。我们找出字节、单词或构成指令、文字和任何数据的东西。我们只需开始分配内存并构建将创建程序的值,并记下我们需要返回并修复地址的任何地方。在那个地方,我们放了一个假人来填充位置,这样我们就可以继续计算内存大小。例如,我们的第一个机器代码可能采用一个单元格。下一个机器代码可能需要 3 个单元,包括一个机器代码单元和两个地址单元。现在我们的地址指针是 4。我们知道机器单元中的内容,即操作代码,但我们必须等待计算地址单元中的内容,直到我们知道该数据的位置,即该数据的机器地址是什么。

如果只有一个源文件,理论上编译器可以在没有链接器的情况下生成完全可执行的机器代码。在两次传递过程中,它可以计算任何机器加载或存储指令引用的所有数据单元的所有实际地址。它可以计算任何绝对跳转指令引用的所有绝对地址。这就是更简单的编译器(如 Forth 中的编译器)的工作方式,无需链接器。

链接器是允许单独编译代码块的东西。这可以加快构建代码的整个过程,并允许以后如何使用块,换句话说,它们可以在内存中重新定位,例如,在每个地址上添加 1000 个地址,以增加 1000 个地址单元。

因此,编译器输出的是尚未完全构建的粗略机器代码,但布局已为我们知道所有内容的大小而已布局,换句话说,我们可以开始计算所有绝对地址的位置。编译器还输出一个符号列表,这些符号是名称/地址对。这些符号将模块中机器代码中的内存偏移量与名称相关联。偏移量是到模块中符号的内存位置的绝对距离。

这就是我们到达链接器的地方。链接器首先将所有这些机器代码块首尾相连,并记下每个代码块的起始位置。然后,它通过将模块内的相对偏移量和模块在更大布局中的绝对位置相加来计算要固定的地址。

显然,我过于简化了这一点,因此您可以尝试掌握它,并且我故意不使用对象文件、符号表等行话,这对我来说是混淆的一部分。

60赞 3 revs, 2 users 69%kaps #5

GCC 通过 4 个步骤将 C/C++ 程序编译为可执行文件。

例如,按如下方式执行:gcc -o hello hello.c

1. 预处理

通过 GNU C 预处理器 () 进行预处理,其中包括 标头 () 并展开宏 ()。cpp.exe#include#define

cpp hello.c > hello.i

生成的中间文件“hello.i”包含扩展的源代码。

2. 编译

编译器将预处理的源代码编译为特定处理器的汇编代码。

gcc -S hello.i

-S 选项指定生成汇编代码,而不是目标代码。生成的程序集文件为“hello.s”。

3. 组装

汇编程序 () 将汇编代码转换为目标文件 “hello.o” 中的机器代码。as.exe

as -o hello.o hello.s

4. 链接器

最后,链接器()将目标代码与库代码链接起来,以生成可执行文件“hello”。ld.exe

    ld -o hello hello.o ...libraries...