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

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

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


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

  • 预处理:预处理需要花费C ++源代码文件,并处理所述#include S, #define S和其它预处理指令。 这一步的输出是一个没有预处理器指令的“纯”C ++文件。

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

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

  • 预处理

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

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

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

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

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

    汇编

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

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

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

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

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

    链接

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

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

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


    CProgramming.com对此主题进行了讨论:
    https://www.cprogramming.com/compilingandlinking.html

    这里是作者在那里写道:

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

    汇编

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

    链接

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

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

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

    了解编译阶段和链接阶段之间的差异可以更容易地寻找错误。 编译器错误通常是语法上的 - 缺少分号,额外的括号。 链接错误通常与丢失或多个定义有关。 如果出现连接器多次定义一个函数或变量的错误,那么这很好地表明错误是您的两个源代码文件具有相同的函数或变量。


    在标准方面:

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

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

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

    链接地址: http://www.djcxy.com/p/2011.html

    上一篇: How does the compilation/linking process work?

    下一篇: Undefined behavior and sequence points