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

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

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。

编写一个C ++程序包括三个步骤:

  1. 预处理:预处理器需要一个C ++源代码文件,并处理#include#include define和其他预处理器指令。 这一步的输出是一个没有预处理器指令的“纯”C ++文件。

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

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

预处理

预处理器处理预处理器指令 ,如#include#define 。 C ++的语法是不可知的,这就是为什么它必须谨慎使用。

它通过将#include指令replace为相应文件的内容(通常只是声明),replacemacros( #define ),并根据#ifselect不同部分的文本,从而在一个C ++源文件上工作, #ifdef#ifndef指令。

预处理器处理预处理令牌stream。 macros替代被定义为用其他标记代替标记(当有意义时,运算符##可以合并两个标记)。

毕竟,预处理器产生一个单一的输出,它是由上述变换产生的一个记号stream。 它还添加了一些特殊的标记,告诉编译器每一行来自哪里,这样就可以使用这些标记来产生明智的错误消息。

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

汇编

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

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

编译器通常会让你停止编译。 这是非常有用的,因为使用它你可以分别编译每个源代码文件。 这样做的好处是,如果只更改单个文件,则不需要重新编译所有内容

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

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

链接

链接器是从编译器生成的对象文件中产生最终编译输出的东西。 这个输出可以是一个共享(或dynamic)库(虽然名称相似,但与前面提到的静态库没有太多相同之处),也可能是一个可执行文件。

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

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

在标准方面:

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

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

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

编译与创build可执行文件不太一样! 相反,创build一个可执行文件是一个多阶段的过程,分为两个部分:编译和链接。 实际上,即使程序“编译好”,在链接阶段也可能因为错误而无法正常工作。 从源代码文件到可执行文件的总体过程可能更好地称为构build。

汇编

编译是指处理源代码文件(.c,.cc或.cpp)和创build“对象”文件。 这一步不会创build用户可以实际运行的任何内容。 相反,编译器只是生成与已编译的源代码文件相对应的机器语言指令。 例如,如果编译(但不链接)三个单独的文件,则将有三个作为输出创build的对象文件,每个都以名称.o或.obj(扩展名取决于您的编译器)。 这些文件中的每一个都包含将您的源代码文件翻译成机器语言文件 – 但是您不能运行它们! 你需要把它们变成你的操作系统可以使用的可执行文件。 这是链接器进来的地方。

链接

链接是指从多个目标文件创build单个可执行文件。 在这一步中,链接器会抱怨未定义的函数(通常是main本身)是很常见的。 在编译过程中,如果编译器找不到某个特定函数的定义,那么只会假定该函数是在另一个文件中定义的。 如果情况并非如此,那么编译器就不会知道 – 它不会一次查看多个文件的内容。 另一方面,链接器可能会查看多个文件并尝试查找未提及的函数的引用。

你可能会问为什么有单独的编译和链接步骤。 首先,这样做可能更容易。 编译器做它的事情,链接器做它的事情 – 通过保持function分开,程序的复杂性减less。 另一个(更明显的)优点是这允许创build大型程序,而不必在每次更改文件时重新编译步骤。 相反,使用所谓的“条件编译”,只需要编译那些已经改变的源文件; 其余的,对象文件是链接器的足够的input。 最后,这使得实现预编译代码库变得非常简单:只需创build目标文件,并将它们链接到任何其他目标文件。 (顺便说一句,每个文件与其他文件中包含的信息分开编译的事实被称为“单独编译模型”)。

为了获得条件编译的全部好处,让程序来帮助你比试图记住自上次编译以来更改过的文件可能更容易。 (当然,您可以重新编译每个文件的时间戳大于对应的目标文件的时间戳。)如果您正在使用集成开发环境(IDE),则可能已经为您处理这个问题。 如果你使用的是命令行工具,那么有一个叫做make的漂亮的实用程序可以与大多数* nix发行版一起提供。 除了条件编译外,还有其他一些编程function,比如允许程序的不同编译 – 例如,如果你有一个产生debugging输出的详细输出。

了解编译阶段和链接阶段之间的差异可以更容易地寻找错误。 编译器错误通常是语法上的 – 一个缺less的分号,一个额外的括号。 链接错误通常与丢失或多个定义有关。 如果你得到一个函数或variables被链接器多次定义的错误,那么这个错误就是你的两个源代码文件具有相同的函数或variables。

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

首先,我们要尽可能地分配内存,然后才能知道每个单元中究竟发生了什么。 我们计算字节,单词,或任何forms的指令和文字和任何数据。 我们刚开始分配内存,并build立将创build程序的值,并记下我们需要返回并修复地址的任何地方。 在那个地方,我们把一个虚拟的只填充位置,所以我们可以继续计算内存大小。 例如,我们的第一个机器码可能需要一个单元格。 下一个机器码可能需要3个单元,涉及一个机器码单元和两个地址单元。 现在我们的地址指针是4.我们知道在机器单元中发生了什么,这是操作码,但是我们必须等待计算地址单元中的内容,直到我们知道数据的位置,也就是说,该数据的机器地址。

如果只有一个源文件,编译器理论上可以生成完全可执行的机器代码而无需链接器。 在双程过程中,它可以计算所有实际地址给所有由任何机器加载或存储指令引用的数据单元。 它可以计算任何绝对跳转指令引用的所有绝对地址。 这是比较简单的编译器,比如Forth中的编译器,没有链接器。

链接器是允许分开编译代码块的东西。 这可以加速构build代码的整个过程,并允许稍后使用块的一些灵活性,换句话说,它们可以被重新定位在存储器中,例如将1000添加到每个地址以将该块向上移动1000个地址单元。

所以编译器输出的是粗糙的机器代码,它还没有完全构build,但是已经布置好了,所以我们知道所有东西的大小,换句话说,我们可以开始计算所有绝对地址的位置。 编译器还输出一个名称/地址对的符号列表。 这些符号与模块中的机器代码中的名称相关。 偏移量是模块中符号的存储位置的绝对距离。

这就是我们到达链接器的地方。 链接器首先将所有这些机器代码块从头到尾放在一起,并记下每个启动的地方。 然后,通过将模块中的相对偏移量与模块的绝对位置相加,计算要固定的地址。

显然我已经简化了这个,所以你可以试着去理解它,而且我故意不使用目标文件,符号表等术语,这对我来说是混乱的一部分。

看url: http : //faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
这个URL清楚地介绍了C ++的完整的编译过程。