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

Ton*_*ion 395 c++ compiler-construction linker c++-faq

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

(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)

R. *_*des 523

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

  1. 预处理:预处理器采用C++源代码文件并处理#includes,#defines和其他预处理器指令.此步骤的输出是没有预处理器指令的"纯"C++文件.

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

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

预处理

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

它通过更换工作于一个C++源文件在一个时间#include与相应的文件的内容(这是通常只是声明)指令,做替换宏(的#define),以及选择取决于的文本的不同部分#if,#ifdef#ifndef指令.

预处理器适用于预处理令牌流.宏替换被定义为用其他令牌替换令牌(操作员##在有意义时可以合并两个令牌).

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

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

汇编

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

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

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

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

正是在这个阶段,报告了"常规"编译器错误,如语法错误或失败的重载解析错误.

链接

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

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

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

  • 编译阶段在转换为目标文件之前也会调用汇编程序. (35认同)
  • @BartvanHeukelom传统上它是在编译期间完成的,但是现代编译器支持所谓的"链接时优化",其优点是能够跨翻译单元进行优化. (6认同)
  • 如果链接器将引用库中的类/方法的符号转换为地址,这是否意味着库二进制文件存储在操作系统保持不变的内存地址中?我只是对链接器如何知道所有目标系统的stdio二进制文件的确切地址感到困惑.文件路径总是相同的,但确切的地址可以改变,对吗? (6认同)
  • 优化应用在哪里?乍一看似乎它会在编译步骤中完成,但另一方面,我可以想象只有在链接后才能进行适当的优化. (3认同)
  • C有相同的步骤吗? (3认同)
  • 需要明确的是,链接时优化不会阻止在编译过程中进行优化.它的作用是利用链接时的附加信息来执行更强大的优化. (2认同)
  • @DanCarter这取决于平台和链接器,但一般来说,链接器仅生成相对地址。这意味着它可能把`main()`放在0,`myFunction()`放在100。然后当操作系统真正加载可执行文件运行时,它会加载某个地址的代码,然后所有地址偏移可执行文件的代码加载到的任何地址。(它只是添加一个数字) (2认同)

小智 37

本主题在CProgramming.com上进行了讨论:https://www.cprogramming.com/compilingandlinking.html

这是作者写的:

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

汇编

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

链接

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

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

为了获得条件编译的全部好处,可能更容易让程序帮助您,而不是尝试记住自上次编译以来您更改了哪些文件.(当然,您可以重新编译时间戳大于相应目标文件的时间戳的每个文件.)如果您正在使用集成开发环境(IDE),它可能已经为您解决了这个问题.如果您正在使用命令行工具,那么大多数*nix发行版都会附带一个名为make的漂亮实用程序.除了条件编译之外,它还有其他一些很好的编程功能,例如允许对程序进行不同的编译 - 例如,如果你有一个版本产生用于调试的详细输出.

了解编译阶段和链接阶段之间的差异可以更容易地捕获错误.编译器错误本质上通常是语法错误 - 缺少分号,附加括号.链接错误通常与缺失或多个定义有关.如果从连接器中多次定义函数或变量的错误,则表明错误是两个源代码文件具有相同的函数或变量.

  • @binarysmacker对此发表评论为时已晚,但其他人可能会觉得这很有用.https://youtu.be/D0TazQIkc8Q基本上你包含头文件,这些头文件通常只包含变量/函数的声明而不是定义,定义可能存在于一个单独的源文件中.所以预处理器只包含声明而不包含声明定义这是链接器帮助的地方.您将使用变量/函数的源文件与定义它们的源文件链接起来. (3认同)
  • 我不明白的是,如果预处理器管理诸如 #includes 之类的内容来创建一个超级文件,那么在那之后就没有任何链接了吗? (2认同)

APr*_*mer 23

在标准方面:

  • 一个翻译单元是源文件的组合,包括标头和源文件少任何源极线通过跳过条件包含预处理指令.

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

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

  • 你能列出所有9个阶段吗?我认为这对答案来说是一个很好的补充.:) (12认同)
  • @sbi是的,但这应该是FAQ问题,不是吗?所以不应该提供这些信息*这里*?;) (2认同)
  • @AProgrammmer:只需按名称列出它们就会有所帮助.然后人们知道如果他们想要更多细节需要搜索什么.无论如何,+ 1'你的答案无论如何:) (2认同)

Ell*_*iew 14

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

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

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

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

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

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

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


Cha*_*ang 9

查看URL:http
://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html此URL中明确介绍了C++的完整编译过程.

  • 感谢您分享,理解它非常简单明了. (2认同)

kap*_*aps 8

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 ...库...