来自C++ 中的思考 - 第 1 卷:
在第二遍中,代码生成器遍历分析树并为树的节点生成汇编语言代码或机器代码。
至少在 GCC 中,如果我们提供生成汇编代码的选项,编译器会通过创建包含汇编代码的文件来服从。但是,当我们简单地运行gcc
没有任何选项的命令时,它不会在内部生成汇编代码吗?
如果是,那为什么需要先生成汇编代码,然后再翻译成机器语言?
来自C++ 中的思考 - 第 1 卷:
在第二遍中,代码生成器遍历分析树并为树的节点生成汇编语言代码或机器代码。
至少在 GCC 中,如果我们提供生成汇编代码的选项,编译器会通过创建包含汇编代码的文件来服从。但是,当我们简单地运行gcc
没有任何选项的命令时,它不会在内部生成汇编代码吗?
如果是,那为什么需要先生成汇编代码,然后再翻译成机器语言?
TL:DR 不同的目标文件格式/更容易移植到新的 Unix 平台(历史上)是 gcc 将汇编器与编译器分开的主要原因之一,我认为。 在 gcc 之外,主流的 x86 C 和 C++ 编译器(clang/LLVM、MSVC、ICC)直接进入机器代码,如果您要求它们,可以选择打印 asm 文本。
LLVM 和 MSVC 带有完整的工具链,而不仅仅是编译器。(还带有汇编器和链接器)。LLVM 已经将目标文件处理作为库函数,因此它可以使用它而不是写出 asm 文本来提供给单独的程序。
较小的项目通常选择将目标文件格式的详细信息留给汇编器。例如,FreePascal 可以直接转到它的一些目标平台上的目标文件,否则只能转到 asm。有许多说法(1、2、3、4)几乎所有的编译器都通过 asm 文本,但对于许多拥有大量开发人员的最大最广泛使用的编译器(GCC 除外)来说,情况并非如此。
C 编译器倾向于仅针对单个平台(例如供应商的微控制器编译器)并被编写为“该平台的 C 实现”,或者是非常大的项目,例如 LLVM,其中不包括机器代码生成编译器自己的代码大小的很大一部分。不太广泛使用的语言的编译器通常更可移植,但不想编写自己的机器代码/目标文件处理。(现在很多编译器都是 LLVM 的前端,所以可以.o
免费获得输出,比如rustc
,但是旧的编译器没有这个选项。)
在所有编译器中,大多数都使用 asm。但是,如果您按每天使用每个文件的频率来衡量,那么直接访问可重定位目标文件 ( .o
/ .obj
) 占全球任何一天完成的总构建量的很大一部分。即,如果您正在阅读本文,您关心的编译器可能会以这种方式工作。
此外,像javac
这样针对可移植字节码格式的编译器使用 asm 的理由较少;相同的输出文件和字节码格式适用于它们必须运行的每个平台。
有关的:
as
分离优势的其他答案。.o
或.obj
)。tcc
Tiny C 编译器除外,它旨在动态用于单文件 C 程序。是的,as
是一个单独的程序,gcc
前端实际上与cc1
(生成文本 asm 的 C 预处理器 + 编译器)分开运行。
这使得 gcc 更加模块化,使编译器本身成为一个文本 -> 文本程序。
GCC 在内部使用一些二进制数据结构来表示 GIMPLE 和 RTL 内部表示,但它不会将这些 IR 格式(的文本表示)写入文件,除非您使用特殊选项进行调试。
那么为什么要在组装时停下来呢?这意味着 GCC 不需要知道同一目标的不同目标文件格式。 例如,不同的 x86-64 操作系统使用 ELF、PE/COFF、MachO64 目标文件和历史上的 a.out。 as
将相同的文本汇编组装成相同的机器代码,由不同目标上的不同目标文件元数据包围。(gcc 必须知道一些细微的区别,比如是否在_
符号名称前加上一个,是否可以使用 32 位绝对地址,以及代码是否必须是 PIC。)
任何特定于平台的怪癖都可以留给 GNU binutils as
(又名 GAS),或者 gcc 可以使用系统随附的供应商提供的汇编程序。
从历史上看,有许多不同的 Unix 系统具有不同的 CPU,或者特别是相同的 CPU,但它们的目标文件格式有不同的怪癖。更重要的是,一组相当兼容的汇编指令,如.globl main
,.asciiz "Hello World!\n"
和类似指令。GAS 语法来自 Unix 汇编程序。
过去确实可以将 GCC 移植到新的 Unix 平台而无需移植as
,只需使用操作系统附带的汇编程序即可。
从来没有人将汇编器作为库集成到 GCC 的cc1
编译器中。这是为 C 预处理器完成的(历史上也是在一个单独的进程中完成的),但不是汇编器。
大多数其他编译器确实直接从编译器生成目标文件,没有文本 asm 临时文件/管道。通常是因为编译器仅针对一个或几个目标设计,例如 MSVC 或 ICC 或最初仅作为 x86 的各种编译器,或许多供应商提供的嵌入式芯片编译器。
clang/LLVM 的设计比 GCC 晚得多。它被设计为优化 JIT 后端,因此它需要一个内置的汇编程序来快速生成机器代码。作为一个提前编译器,添加对不同目标文件格式的支持可能是一件小事,因为内部软件架构可以直接生成二进制机器代码。
LLVM 当然在寻找特定于后端的优化之前在内部使用 LLVM-IR 进行与目标无关的优化,但同样,如果您要求它,它只会将此格式写为文本。
这是编译器实现的问题。汇编代码是高级语言(正在编译的语言)和生成的二进制输出之间的中间步骤。一般来说,首先转换为汇编,然后转换为二进制代码比直接创建二进制代码更容易。
Gcc 确实将汇编代码创建为临时文件,调用汇编器,可能还有链接器,具体取决于您在命令行上执行或不添加的操作。这会创建一个对象,然后如果启用二进制文件,则所有临时文件都会被清理。使用 -save-temps 查看实际情况(有许多临时文件)。
不带任何选项运行 gcc 绝对会创建一个 asm 文件。
这没有“需要”,这只是他们碰巧设计它的方式。我假设出于多种原因,您在开始编译器之前已经想要/需要一个汇编器和链接器(先于马车,在处理器上先于其他语言使用 asm)。“unix 方式”是不重新发明工具或库,而只是在顶部添加一点,这意味着要使用 asm,然后让汇编器和链接器完成其余的工作。您不必以这种方式重新发明这么多的组装工作(多次通过、解析标签等)。开发人员调试 ascii asm 比调试位更容易。几代编译器都在这样做。及时编译器是这种习惯的主要例外,根据定义,它们必须能够转到机器代码,所以它们可以或可以。