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

时间:2011-06-07 11:04:33

标签: c++ compiler-construction linker c++-faq

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

<子> (注意:这是Stack Overflow's C++ FAQ的一个条目。如果您想批评在此表单中提供常见问题解答的想法,那么the posting on meta that started all this就是这样做的地方。在C++ chatroom中监控了这个问题,首先是FAQ的想法,所以你的答案很可能会被那些提出想法的人阅读。)

6 个答案:

答案 0 :(得分:493)

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

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

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

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

  4. 预处理

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

    它一次在一个C ++源文件上工作,将#include指令替换为相应文件的内容(通常只是声明),替换宏(#define),然后选择文本的不同部分取决于#if#ifdef#ifndef指令。

    预处理器适用于预处理令牌流。宏替换被定义为用其他标记替换标记(操作符##允许合并两个标记)。

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

    通过巧妙地使用#if#error指令,可以在此阶段产生一些错误。

    汇编

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

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

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

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

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

    链接

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

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

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

答案 1 :(得分:32)

CProgramming.com讨论了这个话题:
https://www.cprogramming.com/compilingandlinking.html

这是作者写的:

  

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

     

汇编

     

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

     

链接

     

链接是指从中创建单个可执行文件   多个目标文件。在此步骤中,链接器通常是   抱怨未定义的函数(通常是main本身)。中   编译,如果编译器找不到a的定义   特殊功能,它只是假设功能是   在另一个文件中定义。如果不是这样的话,就没办法了   编译器会知道 - 它不会查看超过的内容   一次一个文件。另一方面,链接器可能会查看   多个文件并尝试查找函数的引用   没有提到。

     

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

     

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

     

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

答案 2 :(得分:23)

在标准方面:

  • 翻译单元是源文件,包含的标题和源文件的组合,而不是条件包含预处理程序指令跳过的任何源行。

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

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

答案 3 :(得分:14)

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

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

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

链接器允许单独编译代码块。这可以加快构建代码的整个过程,并允许稍后如何使用块的灵活性,换句话说,它们可以重新定位在内存中,例如向每个地址添加1000以便将块扫描1000个地址单元。

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

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

显然我已经过度简化了这一点,所以你可以试着去掌握它,我故意不使用对象文件,符号表等行话,这对我来说是混乱的一部分。

答案 4 :(得分:9)

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

答案 5 :(得分:2)

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

例如,执行“ gcc -o hello.exe 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”中的机器代码。

  

为-o hello.o hello.s

4。链接器

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

  

ld -o hello.exe hello.o ...库...