编程语言的编译器首先是将代码翻译成汇编语言还是直接转换成机器码?

71
我主要关注流行和广泛使用的编译器,例如gcc。但如果不同的编译器有不同的做法,我也想知道。 以gcc为例,它是直接将用C语言编写的短程序编译成机器代码,还是首先将其翻译成人类可读的汇编语言,然后再使用(内置的?)汇编器将汇编程序转换为二进制机器代码 - 一系列CPU指令? 使用汇编代码创建二进制可执行文件是一个显著的昂贵操作吗?还是一个相对简单和快速的操作? (让我们假设我们只处理x86系列处理器,并且所有程序都是为Linux编写的。)

相关:编译器是否总是生成汇编代码? - 不,大型主流 C 编译器通常直接生成机器码,特别是那些(不像 GCC)只针对少数 ISA / 目标文件格式的编译器。但是,小型开发团队的编译器通常将目标文件处理留给现有的汇编器。另外相关:C 和 Assembler 实际上编译成什么? - Peter Cordes
14个回答

66

gcc实际上生成汇编代码并使用as汇编器进行汇编。并非所有编译器都这样做 - MS编译器直接生成目标代码,尽管您可以让它们生成汇编输出。将汇编代码转换为目标代码是一个相当简单的过程,至少与C→Assembly或C→Machine-code翻译相比如此。

一些编译器会产生其他高级语言代码作为其输出 - 例如,第一个C++编译器cfront生成C作为其输出,然后由C编译器将其编译为机器码。

请注意,直接编译或汇编实际上并不会生成可执行文件。这是由链接器完成的,它获取编译/汇编产生的各种目标代码文件,解析它们包含的所有名称,并生成最终的可执行二进制文件。


4
一些历史编译器曾直接生成可执行文件。有些甚至可以在单次编译过程中直接写入可执行的.COM文件 [跟随每个过程的代码,编译器可以输出该过程内的补丁点列表以及前一个过程补丁点列表的地址;启动代码可以在加载代码时进行所有必要的补丁]。这使得即使使用软盘,在非常小的内存占用下也能实现快速编译。 - supercat
2
如果MS编译器直接生成目标代码,那么这是否意味着它们具有自己的转换过程,还是仅在RAM中将其转换为汇编语言,然后再转换为目标代码,而不将汇编代码保存为文件并将该文件用作下一个输入? - S. John Fagone

17

几乎所有编译器(包括gcc)都会生成汇编代码,因为这样做更容易——编写和调试编译器。主要的例外通常是即时编译器或交互式编译器,它们的作者不想承担性能开销或分叉整个进程以运行汇编程序的麻烦。一些有趣的例子包括

  • 新泽西标准ML,它在交互模式下运行,并即时编译每个表达式。

  • tinycc编译器,它被设计成足够快,可以在100毫秒内编译、加载和运行C脚本,因此不需要调用汇编器和链接器。

这些情况的共同点是对“即时”响应的需求。汇编器和链接器足够快,但对于交互响应来说还不够好。但是,技术正在不断进步。

还有一大类语言,例如Smalltalk、Java和Lua,它们编译为字节码而不是汇编代码,但其实现可能随后直接将该字节码转换为机器代码,而无需使用汇编器。

(注:在1990年代初,玛丽·费尔南德斯和我编写了新泽西机器码工具包,其代码在线上,可生成C库,编译器编写人员可以使用它来绕过标准汇编程序和链接程序。 玛丽用它将她的优化链接程序生成 a.out 的速度大约提高了一倍。 如果不写入磁盘,加速甚至更快……)

clang/LLVM、MSVC 和 ICC 都可以直接生成机器码。在主流的 C/C++ 编译器中,至少对于 x86 架构而言,GCC 是个例外而不是规则。如今,许多编译器都是基于 LLVM 的前端实现的。 - Peter Cordes
1
@PeterCordes 请注意我的回答中的日期。世界已经改变了! - Norman Ramsey
Clang在2009年还不存在,但我认为我的观点对于当时的大型主流C++实现仍然基本正确。许多其他语言的编译器确实将目标文件格式处理留给单独的汇编器,因此这个答案并不是错误的,只是忽略了一些被使用比许多其他较小的编译器组合更多的C++编译器。换句话说,这个答案需要进行一些维护。(另请参见编译器是否总是生成汇编代码? ,这是我试图回答基本上是重复的问题。) - Peter Cordes

7
根据 Mike Perry 和 Nasko Oskov 的《逆向工程软件入门》第 2 章节 链接,gcc 和 MSVC++ 的后端编译器 cl.exe 都有 -S 开关,可以用来输出每个编译器生成的汇编代码。
你也可以以详细模式运行 gcc(gcc -v),以获取它执行的命令列表,查看它在幕后做了什么。

gcc 在内部确实编译成一个临时的 .s 汇编文件,并在其上运行 as-S 选项只是停在那里。 另一方面,MSVC 通常只输出一个 .obj 文件,它的汇编输出选项会生成一个巨大的臃肿的 .asm 文件(其中包含你从未调用过的模板或库函数的定义),有时需要将其修剪以正确地进行汇编和链接,以避免重复符号错误。 GCC 在正常操作期间确实以非常真实的方式编译为汇编语言,而 MSVC 则不会。(ICC 或 clang/LLVM 也不会,但它们可以输出与其 .o 文件匹配的汇编语言)。 - Peter Cordes

7

GCC编译成汇编语言。其他一些编译器则不会。例如,LLVM-GCC编译为LLVM汇编或LLVM字节码,然后再编译成机器代码。几乎所有编译器都有某种形式的内部表示,LLVM-GCC使用LLVM,而GCC使用称为GIMPLE的东西。


真的,但只有GCC(在主流C/C++编译器中)实际上将汇编代码作为文本写入文件。只有在使用调试选项时,GIMPLE才会以文本形式转储到文件中;否则,它仅通过GCC的cc1内部的非文本数据结构表示。同样地,对于LLVM-IR来说,它可能从未序列化为字节码,更不用说文本了,只是在clang前端和LLVM后端及其优化器传递的数据结构之间传递。我听说过LLVM-GCC,但不知道它是如何工作的。我猜你是说它输出一个.ll的LLVM-IR,并在其上运行llvm-as进行优化,生成一个.o - Peter Cordes

6

编译器通常会将源代码解析成抽象语法树(AST),然后再转换成一些中间语言。通常在进行一些优化后,它们才会生成目标语言。

关于gcc,它可以编译到各种不同的目标平台。我不知道它是否首先将x86编译成汇编语言,但是我已经给你提供了一些有关编译器的见解 - 你也要求这样做。


4

没有一个答案阐明了汇编语言是在二进制代码和机器相关符号代码之间的第一层抽象。编译器是在机器相关符号代码和机器无关符号代码之间的第二层抽象。

如果编译器直接将代码转换为二进制代码,按定义,它将被称为汇编器而不是编译器。

更合适的说法是编译器使用中间代码,可能是汇编语言,也可能不是汇编语言,例如Java使用字节码作为中间代码,而字节码是Java虚拟机(JVM)的汇编语言。

编辑:您可能会想知道为什么汇编器总是产生机器相关代码,而编译器能够产生机器无关代码。答案很简单。汇编器是机器代码的直接映射,因此它生成的汇编语言始终与机器有关。相反,我们可以为不同的机器编写多个版本的编译器。因此,要使我们的代码独立于机器运行,我们必须在为该机器编写的编译器版本上编译相同的代码。


如果编译器直接将代码转换为二进制代码,根据定义,它将被称为汇编器而不是编译器。告诉这个事实给tcc,Tiny C编译器,它可以直接将C源代码编译成x86机器码,甚至没有像GIMPLE或LLVM字节码这样的内部表示形式。它绝对不是汇编器,因为它的输入是可移植的C代码。 - Peter Cordes
即使是clang/LLVM也从未创建包含汇编文本或LLVM字节码的文件,但它确实具有内部数据结构,用于表示优化期间的目标中立LLVM“指令”。也许还有一些在优化和代码生成的最终阶段表示机器指令的数据结构。 - Peter Cordes

3
一些之前的回答让我困惑,因为有些答案提到GCC(GNU编译器集合)是单个工具,但实际上它是一组工具,例如GNU汇编器(也称为GAS),链接器,编译器和调试器,这些工具一起使用以生成可执行文件。是的,GCC并不直接将C源文件转换为机器码。

它通过以下4个步骤实现:

  1. 预处理 - 删除注释和扩展C宏等
  2. 编译 - 源代码转换成汇编代码(由编译器完成)
  3. 汇编 - 将汇编代码转换成机器码(由汇编器完成)
  4. 链接 - 默认情况下将标准函数动态链接到共享库中(由链接器完成)

GCC的C和C++编译器将C预处理和实际编译成汇编语言结合为一个步骤,例如由/usr/lib/gcc/x86_64-pc-linux-gnu/10.1.0/cc1cc1plus完成。这种情况已经持续了很多年。几十年前,CPP是一个单独的步骤,它生成一个临时文件,但现在不再是这样了。然后,通常使用GNU Binutils中的as将asm->object文件完成(这是一个与GCC分开维护的软件包),然后使用ld进行链接(也来自Binutils)。 - Peter Cordes
1
GDB是另一个独立的程序,与gcc前端将源代码转换为链接可执行文件没有任何关系。 - Peter Cordes
也许你的意思是GDB的源代码与GNU Binutils在同一个代码库中。虽然它们通常被分开打包,但这是正确的。这与构建可执行文件无关。 - Peter Cordes

1

编译有许多阶段。抽象地说,有前端读取源代码,将其分解为标记,最终形成语法树。

后端负责首先生成类似三地址代码的顺序代码:

代码:

x = y + z + w

转换为:

reg1 = y + z
x = reg1 + w

然后对其进行优化,将其翻译成汇编语言,最终转换为机器语言。所有步骤都被仔细分层,以便在需要时可以替换其中之一。


1

1
在大多数多遍编译器中,汇编语言是在代码生成步骤中生成的。这使您可以编写词法分析器、语法和语义分析阶段一次,然后使用单个汇编器后端生成可执行代码。这在交叉编译器中经常用于生成适用于各种不同CPU的C编译器等。
几乎每个编译器都有某种形式的隐式或显式步骤。

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