为什么要先编译成目标文件?

36

去年我开始在一所研究型大学学习Fortran编程。我的大部分经验都是在像PHP或旧版ASP这样的Web语言中,因此我对编译语句是个新手。

我正在修改两个不同的代码。

一个使用显式语句从模块创建.o文件(例如gfortran -c filea.f90)然后创建可执行文件。

另一个直接创建可执行文件(有���会创建.mod文件,但没有.o文件,例如gfortran -o executable filea.f90 fileb.f90 mainfile.f90)。

  • 除了Makefiles之外,有没有其他原因使一种方法优于另一种方法?

3
通常我们使用obj文件来“缓存”编译结果,以便轻松重用旧的已编译代码,从而最小化编译时间。 - stefan
5
在这个语境中,“Code”是一个不可数名词,因此,“two codes”是不正确的。 - Lightness Races in Orbit
3
在物理学界中,将“code”作为可数名词用于程序和库的说法是不幸常见的用法。在该社区中很难进行更正。 - wnoise
4
但是我们会尝试,该死的。我们会尝试。 - AndyPerfect
1
哈,谢谢您的纠正。在我来这里工作之前,我从未将这些程序称为“代码”。 - tomshafer
5个回答

33

首先将程序编译成目标文件,这被称为分离式编译。这样做有许多优点和一些缺点。

优点:

  • 容易将目标文件(.o)转换为库文件,稍后可以链接到它们上面
  • 许多人可以同时处理不同的源文件
  • 编译速度更快(当源代码没有改变时,不需要反复编译相同的文件)
  • 可以从不同语言源文件制作目标文件,并在以后的某个时间链接在一起。为此,这些目标文件只需使用相同的格式和兼容的调用约定即可。
  • 分离式编译使系统范围的库(操作系统库、语言标准库或第三方库)可以静态或动态地分发。

缺点:

  • 有一些优化(例如优化函数)是编译器无法执行并且链接器不关心的;但是,现在许多编译器都包括执行“链接时优化”的选项,这在很大程度上消除了这个缺点。但对于系统库和第三方库仍然是一个问题,特别是对于共享库(无法优化掉每次运行可能会更改的组件部分,尽管其他技术如JIT编译可以缓解这个问题)。
  • 在某些语言中,程序员必须提供一些头文件以供其他人使用,并链接到该对象。例如,在C语言中,您必须提供与目标文件配套的.h文件。但这也是一个好习惯。
  • 在像C或C++这样具有文本包含的语言中,如果您更改了一个函数原型,则必须在两个位置进行更改。一次在头文件中,一次在实现文件中。

2
有些链接器实际上可以在汇编级别执行内联或其他优化。 - Phil Miller
2
有编译器可以优化目标文件。新的VC版本可以做到这一点。尽管如此,这是一个好答案,我给你点赞。 - sbi
2
@ybungalobill,模板?在Fortran中?!? - SK-logic
@Tomalak:坦白说,我不知道(真正的)编译是延迟到链接阶段还是链接器那么聪明。 - sbi
@ybungalobill,无论如何,实例化的函数模板都被标记为弱符号,并由链接器折叠。 - SK-logic
显示剩余5条评论

15

当你有一个由数百个源文件组成的项目时,你不希望每次更改其中一个文件时都要重新编译所有文件。通过将每个源文件编译为单独的目标文件,并仅重新编译受到更改影响的源文件,你能够在源代码更改后花费最少的时间生成新的可执行文件。

make是常用的工具,用于跟踪这种依赖关系,并在某些内容发生更改时重新生成二进制文件。通常,你设置每个源文件依赖的内容(这些依赖关系通常可以由编译器生成,并以适合make的格式呈现),然后让 make 处理创建最新的二进制文件的细节。


6
.o文件是目标文件,它是最终程序的中间表示形式。通常情况下,.o文件包含编译后的代码,但它没有所有不同例程或数据的最终地址。在程序运行之前,需要的一个东西类似于内存映像。例如,如果您有一个主程序并调用一个例程A(这是虚假的Fortran语言,在几十年前就已经过时了,请跟我一起工作)。
PROGRAM MAIN
INTEGER X,Y
X = 10
Y = SQUARE(X)
WRITE(*,*) Y
END

然后你有平方函数。
FUNCTION SQUARE(N)
SQUARE = N * N
END

它们是单独编译的单元。当编译MAIN时,它不知道“SQUARE”在哪里,也不知道它的地址。所以当它调用微处理器的JUMP SUBROUTINE(JSR)指令时,指令需要有一个地方可以跳转。
.o文件已经包含了JSR指令,但它没有实际值。这个值在链接或加载阶段(取决于应用程序)后才会出现。
因此,MAINS .o文件具有主函数的所有代码和一系列需要解决的引用(特别是SQUARE)。SQUARE基本上是独立的,没有任何引用,但同时,它也没有存在于内存中的地址。
连接器将把所有的.o文件合并成一个可执行文件。在旧日子里,编译后的代码实际上是一个内存映像。程序将从某个地址开始,直接全部加载到RAM中,然后执行。因此,在这种情况下,你可以看到连接器将两个.o文件连接在一起(以获取SQUARE的实际地址),然后返回到MAIN中查找SQUARE引用,并填写地址。
现代连接器不会走得那么远,而是将大部分最终处理推迟到实际加载程序时进行。但概念是相似的。
通过编译为.o文件,您最终获得了可重用的逻辑单元,这些单元稍后会在链接和加载过程中组合在一起以进行执行。
另一个好处是.o文件可以来自不同的语言。只要调用机制兼容(即如何传递参数到函数和过程),那么一旦编译成.o,源语言就变得不那么重要了。您可以将C代码与FORTRAN代码链接和组合在一起。
在PHP等语言中,过程略有不同,因为所有代码都在运行时加载到单个映像中。您可以将FORTRAN的.o文件视为如何使用PHP的include机制将文件组合成一个大而完整的整体。

非常好。这为我澄清了一些事情。希望我早点读到它。 - Noel Widmer
我想指出的是,您可以在一个gcc编译命令中结合Fortran、C、C++,甚至是Ada或Go。不确定10年前是否已经有这个功能,但这个特性并不是很新。 - Vladimir F Героям слава

2
另一个原因,除了编译时间之外,是编译过程是一个多步骤的过程。

The multi-part process.

目标文件只是这个过程中的一个中间输出。它们最终将被链接器使用,以产生可执行文件。

编译确实是一个多步骤的过程,但将汇编器、链接器和加载器包括在其中(即使明确指出)会导致关于编译器的许多误解。您图表中的唯一第一行可以归因于编译过程(即使对于某些人/编译器来说,这可能也太多了)。 - Neowizard
你似乎误解了问题。 :-) - Peter K.
这个问题与我的评论无关。我在谈论图表以及它描述编译步骤的建议。我不反对你对原始问题的回答(在我看来非常公平)。 - Neowizard
因为我认为图表带来的困惑比答案提供的信息更大,而且我在教学生时经常遇到这种错误。 - Neowizard
好的。我不同意;但你是巫师。 :-) - Peter K.

1

我们编译成目标文件,以便将它们链接在一起形成更大的可执行文件。这不是唯一的方法。

还有一些编译器不是这样做的,而是直接编译到内存中并立即执行结果。早期,当学生们必须使用大型机时,这是标准的。Turbo Pascal也是这样做的。


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