为什么我们需要将编译和链接分开处理?

5

编译时是否可以链接并在此过程中省略独立的链接步骤?


这取决于编译器,理论上是可以的。 - Luchian Grigore
链接意味着将目标文件捆绑在一起,而目标文件只能在编译后生成。 - Dayal rai
1
这取决于你对“在那时”的定义。在将一个函数的调用链接到另一个函数之前,您需要先编译单个函数 - 因此必须是顺序的。但它可以是“同一工作流程的一部分”,因此您不会“看到”有两个不同的步骤... - Floris
如果是这样,当编译器编译第一行时,在完成主循环(入口函数)之前它将如何链接? - Round Robin
1
定义“编译时”。在单个命令行调用中编译和(部分)链接是完全可能的;实际上,这是所有Unix和Windows编译器的默认设置。另一方面,在几乎所有情况下,“链接”的重要部分发生在运行时,当加载单独的库时。 - James Kanze
显示剩余6条评论
6个回答

10
你一次可以编译一个或多个翻译单元,但就语言而言,在编译时每个翻译单元都被视为独立的。你需要将一个或多个翻译单元链接在一起。
因此,如果程序中的所有翻译单元都同时编译,则可以在该时刻链接它们(通常情况下,链接将紧随编译之后进行,但这是内部细节,并且没有任何阻止你编写一个编译器/链接器,以某种方式交错步骤,以便在所有编译结束之后但在任何链接开始之前没有单一点)。
然而,如果你只编译了许多稍后将被链接在一起组成程序的翻译单元中的一个,那么当然你无法同时进行链接。用什么链接?其他翻译单元可能还没有被编写,特别是如果你要编译的翻译单元是用于分发为静态链接库的情况。

当编译器编译第一行时,在完成主循环(入口函数)之前,它将如何链接? - Round Robin
1
许多现代编译器只在“编译”步骤中部分编译;实际的机器码生成被推迟到传统上称为“链接”步骤之后。(当然,对于DLL,许多传统上被认为是链接的内容都被推迟到运行时。) - James Kanze
关于您的第一段:标准定义了9个翻译阶段。它非常小心地避免使用“编译”和“链接”这些术语,但是在第8阶段中组合了翻译单元(在今天最常见的系统中由“编译器”完成)。当然,在大多数通用系统(Unix、Windows)中,第9阶段的大部分工作直到程序被调用才会发生。OP的问题实际上引出了一个问题:他所说的“编译”,“链接”和“在那时”的含义是什么。我能想到的唯一合理的解释是通过调用单个命令来完成。 - James Kanze

5
简短回答:是的,完全有可能。事实上,已经做到了。
一些旧的Pascal编译器(例如早期版本的Turbo Pascal)没有单独的链接器。要创建可执行文件,您需要将所有代码编译在一起。它们并没有跟踪使用了哪些标准库函数,并仅链接所需的函数,而是将整个标准库(大约8千字节)复制到可执行文件中。
为了使这种方法实用,您需要一个快速的编译器、小型项目或两者兼备。
当您使用64千字节的RAM系统,并且存储介质只有容量约为100到200千字节的软盘驱动器时,您没有太多选择。现在,我无法想象有人会接受相同(或类似)的限制。
尽管如此,这不是适用于C或C++的模型。它们从一开始就假定有单独的编译和链接。语言本身的许多部分(例如文件级静态变量)只有在至少模拟单独链接时才能正常工作。

即使在今天,标准的 Pascal 没有提供独立编译的功能。(这无疑是为什么没有人使用它的原因之一。) - James Kanze
@JamesKanze:有一个小细节需要注意。如果我没记错的话,ISO标准在某个时候必须重新批准,否则它将自动撤回。如果是这样的话,我非常确定现在已经没有所谓的标准Pascal了;标准无疑已经被撤销了。 - Jerry Coffin
@Floris:没错,即使在 C 中也是类似的情况。虽然这种情况比较少见,但是有几个实现方式已经存在(我只是记不清它们的名字了——好像 BSD C 就是其中之一,但我不确定)。 - Jerry Coffin
2
@Floris 我不确定这是否重要。关键是,这样的事情是可能的。(可以说,g++和VC++在C++中做到了这一点。至少当我调用g++ mysource.cccl mysource.cc时,我会得到一个可执行文件。它们都使用多个不同的子进程来实现这一点,但我不确定这是否相关——即使你使用-c调用g++,它至少也使用3个或更多的子进程。 - James Kanze
1
@JamesKanze:是的,快速检查一下,Pascal标准(ISO 7185)在2008年进行了审查和重新确认。 - Jerry Coffin
显示剩余2条评论

2
理论上是可能的,但你可能不会看到任何实现这样做的情况。
例如,假设我有以下代码:
#include <stdio.h>

int main( void )
{
  printf( "Hello, world\n" );
  return 0;
}

编译完成后,我得到了以下机器码:
        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "Hello, world"
        .text
.globl main
        .type   main, @function
main:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movl    $.LC0, %edi
        call    puts
        movl    $0, %eax
        leave
        ret

请注意,生成的机器码调用库函数 puts1,但是 puts 函数的机器码不是对象文件的一部分。
这就是为什么需要第二个链接步骤的原因;当您编译一个翻译单元时,如果它调用了另一个翻译单元或库中定义的函数,那么该机器码对编译器来说并不立即可用。链接步骤是必要的,以解决所有对外部函数的引用,并将这些函数的机器码包含在最终的可执行文件中。
1. 如果只传递一个参数,则此版本的 gcc 将使用 puts 替换 printf

为什么“链接”是“单独的步骤”?您需要至少两个“单独的步骤”来获取汇编器输出,然后再进行两个单独的步骤才能获得可执行文件。 - James Kanze

2
为了让你更好地理解编译和链接过程,以gcc为例进行简要解释。我希望这能让您理解为什么在编译时进行链接是困难的。
编译器将源代码从一种语言转换为另一种语言。gcc编译器将C代码转换为汇编代码。汇编程序将汇编代码转换为目标代码。尽管目标代码主要由机器代码组成,但操作系统无法执行它。目标代码没有必要引用外部函数和库以正确运行。
链接器将编译器的各种输出组合起来创建应用程序。
源文件由编译器分别进行编译。这些源文件可能引用其他地方存在的函数。编译器保留这些函数的空引用。
链接器使用所有文件的编译输出和系统上可用的库来填充这些引用。一旦所有空引用都已解决,链接器就将所有编译器输出组合起来创建可执行文件。

但这对于 g++ 是非常特定的,不是通常的过程。一般来说,编译器会将源代码(经过预处理后)转换为一些中间格式,传统上是目标代码(其中包含机器指令的直接二进制表示),但现在通常转换为某种字节码(而几乎从不使用汇编语言)。并且“链接器”几乎从不实际链接所有内容; DLL(包括与系统的接口)在运行时链接。 - James Kanze
此外,这并不是关于g++工作原理的准确描述。g++是一个驱动程序,它调用许多其他程序来完成实际工作:在经典意义上并没有真正的“编译器”。 - James Kanze

0

将编译和链接分开可以仅编译已更改的翻译单元。

这很好,因为它可以在大型项目上实现更快的构建,并减少关键项目的测试。

通常,编译阶段是最慢的。必须搜索文本并构建中间形式(对象文件)。

链接阶段更快,因为它在表格中查找符号并执行地址和符号解析。

通过不每次编译大型系统中的每个文件,可以节省时间。

此外,测试时间也会节省,因为一旦编译和测试了翻译单元,就可以将其保留下来。只有修改过的翻译单元需要重新测试。

一个例子是将数据文件编码为初始化数组。这些数据,例如字体位图,很少会更改。翻译单元只编译一次并保存为对象文件。这将我们的构建时间从5分钟缩短到1分钟。


0

简短回答:不可能。

即使您将所有代码放入单个翻译单元中,您的程序使用的库也需要链接。


如果他们没有使用任何库调用呢? - John Bode
@JohnBode 你仍然需要C/C++运行时。 - James Kanze
@JohnBode:詹姆斯是对的。虽然理论上可以开发一个不使用任何外部代码的应用程序,但很有可能您至少依赖于运行时和一些操作系统支持。 - David Rodríguez - dribeas
1
没有操作系统或运行时的独立实现怎么办? - John Bode
@JohnBode:从我的角度来看,这些属于“不常见”的……嵌入式开发人员会有不同的看法,但我认为大多数人确实会使用某种运行时。 - David Rodríguez - dribeas
简短的回答是,对于任何合理的定义,g++和VC++都可以做到。 - James Kanze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接