为啥要先编译成目标文件?
Posted
技术标签:
【中文标题】为啥要先编译成目标文件?【英文标题】:Why Compile to an Object File First?为什么要先编译成目标文件? 【发布时间】:2011-07-14 02:53:17 【问题描述】:去年,我在一所研究型大学工作,开始使用 Fortran 进行编程。我之前的大部分经验都是使用 php 或旧 ASP 等网络语言,所以我是编译语句的新手。
我正在修改两个不同的代码。
一个在创建可执行文件之前有一个从模块创建 .o 文件的显式语句(例如 gfortran -c filea.f90)。
另一个是直接创建可执行文件(有时会创建 .mod 文件,但不创建 .o 文件,例如 gfortran -o executable filea.f90 fileb.f90 mainfile。 f90)。
是否有原因(可能是 Makefile 除外)一种方法优于另一种方法?【问题讨论】:
一般我们使用obj文件来“缓存”编译。使重用旧编译代码变得容易,以最大限度地减少编译时间。 “代码”在此上下文中是不可数名词;因此,“两个代码”是不正确的。 使用“代码”作为程序和库的计数名词,不幸的是,物理学家普遍使用这种用法。该社区不太可能接受更正。 @wnoise 但我们会尝试 gosh darnit。我们会努力的。 哈,谢谢指正。在我开始在这里工作之前,我从未将这些程序称为“代码”。 【参考方案1】:首先编译成目标文件称为单独编译。有很多优点,也有一些缺点。
优点:
易于将目标文件 (.o) 转换为库并稍后链接到它们 很多人可以同时处理不同的源文件 编译速度更快(在源未更改的情况下,您不会一次又一次地编译相同的文件) 目标文件可以由不同的语言源制作,并在以后链接在一起。为此,目标文件只需使用相同的格式和兼容的调用约定。 单独编译支持静态或共享系统范围的库(操作系统库、语言标准库或第三方库)分发。缺点:
编译器无法执行一些优化(如优化函数),链接器不关心;然而,许多编译器现在包括执行“链接时间优化”的选项,这在很大程度上消除了这个缺点。但这对于系统库和第三方库来说仍然是一个问题,尤其是对于共享库(不可能优化掉每次运行时可能发生变化的组件部分,但是 JIT 编译等其他技术可能会缓解这种情况)。 在某些语言中,程序员必须提供某种类型的标头以供其他将与该对象链接的人使用。例如,在 C 中,您必须提供.h
文件才能与您的目标文件一起使用。但无论如何,这是一种很好的做法。
在包含基于文本的语言(如 C 或 C++)中,如果更改函数原型,则必须在两个地方进行更改。一次在头文件中,一次在实现文件中。
【讨论】:
一些链接器实际上可以在汇编级别执行内联或其他优化。 有些编译器可以跨目标文件进行优化。较新的 VC 版本可以做到这一点。不过,我的回答很好,+1
。
+0 表示没有在缺点中提及模板。
@ybungalobill,模板?在 fortran 中?!?
@Tomalak:坦率地说,我不知道(真正的)编译是推迟到链接阶段还是链接器那么聪明。【参考方案2】:
我们编译为目标文件,以便能够将它们链接在一起以形成更大的可执行文件。这不是唯一的方法。
也有编译器不这样做,而是编译到内存并立即执行结果。早些时候,当学生不得不使用大型机时,这是标准的。 Turbo Pascal 也是这样做的。
【讨论】:
【参考方案3】:.o 文件是目标文件。它是最终程序的中间表示。
具体来说,.o 文件通常具有已编译的代码,但它没有所有不同例程或数据的最终地址。
程序在运行之前需要的东西之一类似于内存映像。
例如。
如果你有你的主程序并且它调用了一个例程 A。(这是假的 fortran,我已经几十年没有接触过,所以在这里和我一起工作。)
PROGRAM MAIN
INTEGER X,Y
X = 10
Y = SQUARE(X)
WRITE(*,*) Y
END
那么你就有了 SQUARE 函数。
FUNCTION SQUARE(N)
SQUARE = N * N
END
它们是单独编译的单元。您可以看到,编译 MAIN 时,它不知道“SQUARE”在哪里,它在什么地址。它需要知道,所以当它调用微处理器的 JUMP SUBROUTINE (JSR) 指令时,该指令有地方可去。
.o 文件已有 JSR 指令,但没有实际值。这在链接或加载阶段的后期出现(取决于您的应用程序)。
因此,MAINS .o 文件包含 main 的所有代码,以及它想要解析的引用列表(特别是 SQUARE)。 SQUARE 基本上是独立的,它没有任何引用,但同时它还没有关于它在内存中的位置的地址。
链接器将删除所有 .o 文件并将它们组合成一个 exe。在过去,编译后的代码实际上就是一个内存映像。该程序将从某个地址开始并简单地批量加载到 RAM 中,然后执行。因此,在场景中,您可以看到链接器获取两个 .o 文件,将它们连接在一起(以获得 SQUARE 的实际地址),然后它会返回并在 MAIN 中找到 SQUARE 引用,并填写地址。
现代链接器并没有走那么远,并将大部分最终处理推迟到程序实际加载时。但概念是相似的。
通过编译为 .o 文件,您最终会得到可重用的逻辑单元,然后在执行之前通过链接和加载过程组合这些逻辑单元。
另一个不错的方面是 .o 文件可以来自不同的语言。只要调用机制是兼容的(即如何将参数传递给函数和过程),那么一旦编译为 .o,源语言就变得不那么相关了。例如,您可以将 C 代码与 FORTRAN 代码链接、组合。
在 PHP et all 中,该过程是不同的,因为所有代码在运行时都加载到单个图像中。您可以考虑 FORTRAN .o 文件,类似于使用 PHP 包含机制将文件组合成一个大的、有凝聚力的整体。
【讨论】:
非常好。这为我澄清了一些事情。希望我早点读到。 我会注意到,您也可以将 Fortran C C++ 甚至 Ada 或 Go 组合到一个 gcc 编译命令中。不知道 10 年前是不是这样,但这个功能并不是很新。【参考方案4】:另一个原因,除了编译时间之外,编译过程是multi-step process。
目标文件只是该过程的一个中间输出。它们最终会被链接器用来生成可执行文件。
【讨论】:
编译确实是一个多步骤的过程,但是将汇编器、链接器和加载器包括在其中(甚至是明确的)是导致对编译器产生如此多误解的根源。只有图表中的第一行可以归因于编译过程(甚至对于某些人/编译器来说可能太多了)。 您似乎误解了这个问题。 :-) 这个问题与我的 cmets 无关。我在谈论图表和它描述编译步骤的建议。我不会反对您对原始问题的回答(这比 IMO 更公平)。 因为我认为图表造成的混乱比答案给出的信息更大,而且我经常遇到学生这个错误。 好的。我不同意;但你是巫师。 :-)【参考方案5】:当您的项目包含 100 个源文件时,您不希望每次更改时都重新编译所有。通过将每个源文件编译成一个单独的目标文件并仅重新编译那些受更改影响的源文件,您从源代码更改到新的可执行文件所花费的时间最少。
make
是用于跟踪此类依赖关系并在发生变化时重新创建二进制文件的常用工具。通常,您设置每个源文件所依赖的内容(这些依赖项通常可以由您的编译器生成 - 采用适合 make
的格式),并让 make 处理创建最新二进制文件的细节。
【讨论】:
以上是关于为啥要先编译成目标文件?的主要内容,如果未能解决你的问题,请参考以下文章