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

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

< p > < em > <子> (注意:这意味着Stack Overflow的c++ FAQ的一个条目。如果你想批评以这种形式提供FAQ的想法,那么在meta上的帖子引发了这一切将是这样做的地方。这个问题的答案在c++聊天室中被监控,FAQ的想法最初就是在这里开始的,所以你的答案很可能会被想出这个想法的人读到。) < / sub > < / em > < / p >
268514 次浏览

c++程序的编译包括三个步骤:

  1. 预处理:预处理程序接受一个c++源代码文件,处理__abc0、__abc1和其他预处理程序指令。这一步的输出是一个“纯”c++文件,没有预处理器指令。

  2. 编译:编译器接受预处理器的输出并从中生成一个目标文件。

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

预处理

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

它每次工作在一个c++源文件上,方法是用各自文件的内容替换#include指令(通常只是声明),替换宏(#define),并根据#if#ifdef#ifndef指令选择文本的不同部分。

预处理程序在预处理令牌流上工作。宏替换被定义为用其他标记替换标记(操作符##允许在有意义时合并两个标记)。

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

聪明地使用#if#error指令可以在这个阶段产生一些错误。

编译

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

目标文件可以引用未定义的符号。当您使用声明而不为其提供定义时,就是这种情况。编译器并不介意这一点,只要源代码格式良好,它就会愉快地生成目标文件。

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

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

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

链接

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

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

在这个阶段,最常见的错误是缺少定义或重复定义。前者意味着要么定义不存在(即它们没有被编写),要么它们所在的目标文件或库没有给链接器。后者是显而易见的:在两个不同的目标文件或库中定义了相同的符号。

在标准的正面:

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

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

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

这个话题是在CProgramming.com上讨论的:
https://www.cprogramming.com/compilingandlinking.html < / p >

作者是这样写的:

编译和创建可执行文件不太一样! 相反,创建可执行文件是一个分为多个阶段的过程 两个组件:编译和链接。在现实中,即使是一个程序 “编译fine"它可能无法实际工作,因为在 连接阶段。从源代码文件开始的整个过程 到可执行文件可能更好地被称为构建。

编译

编译指的是对源代码文件(.c, .cc,或 .cpp)和创建一个“对象”文件。此步骤不创建 任何用户可以实际运行的东西。相反,编译器只是 类对应的机器语言指令 已编译的源代码文件。例如,如果编译(but 不要链接)三个独立的文件,你将有三个目标文件 作为输出创建,每个都有.o或.obj的名称 (扩展将取决于你的编译器)。每个文件 包含源代码文件到机器的翻译 语言文件——但您还不能运行它们!你需要改变他们 转换成操作系统可以使用的可执行文件。这就是

链接

链接是指创建一个单独的可执行文件 多个目标文件。在这一步中,链接器通常会 抱怨未定义的函数(通常是main本身)。在 的定义,则进行编译 特定的函数,它会假设这个函数是 在另一个文件中定义。如果不是这样,就没有办法 编译器会知道——它不会查看超过 一次一个文件。另一方面,链接器可以查看 多个文件,并尝试查找函数的引用 没有提到。< / p > 你可能会问为什么编译和链接步骤是分开的。 首先,以这种方式实现事情可能更容易。编译器 做它的事情,链接器做它的事情——通过保持 功能分离,降低了程序的复杂性。另一个 (更明显的)优势是,这允许创建大型 程序而不必每次重新编译一个文件的步骤 是改变。相反,使用所谓的“条件编译”,它是 必须只编译那些已更改的源文件;为 其余的对象文件是链接器的足够输入。 最后,这使得实现预编译的库变得简单 代码:只是创建目标文件和链接他们就像任何其他 对象文件。(事实上,每个文件都是分开编译的 顺便提一下,包含在其他文件中的信息称为 “单独编译模型”)

要获得条件编译的全部好处,它可能是 让一个程序来帮助你比试着记住哪个更容易 自上次编译后更改的文件。(当然,你可以, 只需要重新编译时间戳大于 对应的目标文件的时间戳。)如果你和一个 集成开发环境(IDE) 这是给你的。如果您正在使用命令行工具,有一个漂亮的 大多数*nix发行版都附带一个名为make的实用程序。沿着 通过条件编译,它还有其他几个不错的特性 编程,例如允许对程序进行不同的编译 ——例如,如果你有一个用于调试的详细输出版本 知道编译阶段和链接之间的区别 Phase可以使查找bug变得更容易。编译器错误通常是 语法本质上——少了一个分号,多了一个圆括号。 链接错误通常与缺失或多重有关 定义。如果你得到一个函数或变量的错误 从链接器定义多次,这说明 错误是两个源代码文件具有相同的功能 或变量。< / p >

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

首先,在我们知道每个单元的具体情况之前,我们尽可能地分配内存。我们计算出字节,或单词,或任何组成指令,文字和任何数据的东西。我们只是开始分配内存,并建立将创建程序的值,并记下任何我们需要返回并修复地址的地方。在那个地方,我们放了一个虚拟来填充这个位置,这样我们就可以继续计算内存大小。例如,我们的第一个机器代码可能需要一个单元格。下一个机器代码可能需要3个单元格,包括一个机器代码单元格和两个地址单元格。现在我们的地址指针是4。我们知道机器单元格里有什么,也就是操作码,但我们必须等待计算地址单元格里有什么,直到我们知道数据的位置,即该数据的机器地址是什么。

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

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

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

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

很明显,我把它简化了,所以你可以试着去理解它,我故意没有使用目标文件、符号表等术语,这对我来说是混乱的一部分。

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.组装

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

as -o hello.o hello.s

4. 链接器

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

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