【问题标题】:Do programming language compilers first translate to assembly or directly to machine code?编程语言编译器首先转换为汇编还是直接转换为机器代码?
【发布时间】:2010-10-25 03:01:58
【问题描述】:

我主要对流行且广泛使用的编译器感兴趣,例如 gcc。但如果不同的编译器做的事情不同,我也想知道。

以gcc为例,它是直接将C编写的短程序编译成机器代码,还是先将其翻译成人类可读的汇编,然后才使用(in-内置?)汇编程序将汇编程序翻译成二进制,机器代码——对CPU的一系列指令?

使用汇编代码创建二进制可执行文件是一项非常昂贵的操作吗?还是相对简单快捷的事情?

(假设我们只处理 x86 系列处理器,并且所有程序都是为 Linux 编写的。)

【问题讨论】:

标签: assembly gcc compilation compiler-construction


【解决方案1】:

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

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

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

【讨论】:

  • 一些历史编译器用于直接生成可执行文件。有些人甚至可以在编译期间一次性编写一个可执行的 .COM 文件[按照每个过程的代码,编译器可以输出该过程中的补丁点列表以及前一个过程的补丁点列表的地址;启动代码可以在加载代码时制作所有必要的补丁]。这使得即使在使用软盘时,也可以在非常小的内存占用中进行快速编译。
  • 如果 MS 编译器直接生成目标代码。这是否意味着他们有自己的转换过程,或者他们只是在 ram 中转换为汇编然后转换为目标代码,而不将汇编代码保存为文件并将该文件用作下一个输入?
【解决方案2】:

包括 gcc 在内的几乎所有编译器都生成汇编代码,因为它更容易生成和调试编译器。主要的例外通常是即时编译器或交互式编译器,它们的作者不希望性能开销或分叉整个进程来运行汇编器的麻烦。一些有趣的例子包括

  • Standard ML of New Jersey,它以交互方式运行并即时编译每个表达式。

  • tinycc compiler,其设计速度足以在 100 毫秒内编译、加载和运行 C 脚本,因此不需要调用汇编器和链接器的开销。

这些案例的共同点是渴望“即时”响应。汇编器和链接器非常快,但对于交互式响应来说还不够好。然而。

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

(脚注:在 1990 年代初期,Mary Fernandez 和我编写了 New Jersey Machine Code Toolkitcode 在线,它生成 C 库,编译器编写者可以使用这些库绕过标准汇编器和链接器。Mary 使用它在生成a.out 时,她的优化链接器的速度大约会提高一倍。如果你不写入磁盘,加速会更大......)

【讨论】:

  • clang/LLVM、MSVC、ICC都直接产生机器码。 GCC 是主流 C/C++ 编译器中的例外,而不是规则,至少对于 x86 而言。如今,许多编译器被实现为 LLVM 的前端。
  • @PeterCordes 请注意我的回答日期。世界变了!
  • Clang 在 2009 年还不存在,但我认为我的观点对于当时的大型主流 C++ 实现仍然基本正确。许多其他语言的编译器确实将目标文件格式处理留给了单独的汇编程序,所以这个答案没有错,只是忽略了一些 C++ 编译器,这些编译器比许多其他较小的编译器组合使用得更多。或者换句话说,这个答案可能需要一些维护。 (另请参阅Does a compiler always produce an assembly code?,了解我尝试回答基本上是重复的。)
【解决方案3】:

GCC 编译为汇编器。其他一些编译器没有。例如,LLVM-GCC 编译为 LLVM-assembly 或 LLVM-bytecode,然后编译为机器码。几乎所有编译器都有某种内部表示,LLVM-GCC 使用 LLVM,而 IIRC,GCC 使用称为 GIMPLE 的东西。

【讨论】:

  • 没错,但只有 GCC(在主流 C/C++ 编译器之外)实际上将 asm 作为文本写入文件。如果您使用调试选项,GIMPLE 仅作为文本转储到文件中,否则仅通过 GCC 的 cc1 内的非文本数据结构表示。 LLVM-IR 也是如此;它可能永远不会序列化为字节码,更不用说文本了,只是作为数据结构在 clang 前端和 LLVM 后端及其优化器之间传递。我听说过 LLVM-GCC 但 IDK 它是如何工作的。我猜你是说它输出 LLVM-IR 的 .ll,并在其上运行 llvm-as 以优化为 .o
【解决方案4】:

根据Introduction to Reverse Engineering Software 中的chapter 2(由 Mike Perry 和 Nasko Oskov 撰写),gcc 和 cl.exe(MSVC++ 的后端编译器)都有您可以使用的 -S 开关输出每个编译器生成的程序集。

您还可以在详细模式 (gcc -v) 下运行 gcc,以获取它执行的命令列表,以查看它在幕后所做的事情。

【讨论】:

  • gcc 在内部确实编译为一个临时的.s asm 文件,并在其上运行as-S 选项就停在那里。另一方面,MSVC 通常只输出一个.obj,它的汇编输出选项会产生一个巨大的膨胀.asm 文件(包含你从未调用过的模板或库函数的定义),有时需要修剪甚至汇编 +正确链接,没有重复符号错误。 GCC 在正常运行期间确实会编译为真正意义上的 asm,而 MSVC 则不会。 (ICC 或 clang/LLVM 都没有,但它们可以输出与其 .o 匹配的 asm)
【解决方案5】:

编译器通常将源代码解析为抽象语法树(AST),然后解析为某种中间语言。只有这样,通常经过一些优化后,它们才会发出目标语言。

关于 gcc,它可以编译成各种各样的目标。我不知道对于 x86,它是否首先编译为程序集,但我确实让您对编译器有所了解 - 您也要求这样做。

【讨论】:

    【解决方案6】:

    没有一个答案能阐明汇编器是二进制代码和机器相关符号代码之间的第一层抽象这一事实。编译器是 MACHINE DEPENDENT SYMBOLIC CODE 和 MACHINE INDEPENDENT SYMBOLIC CODE 之间的第二层抽象。

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

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

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

    【讨论】:

    • 如果编译器直接将代码转换为二进制代码,根据定义,它将被称为汇编程序而不是编译器。 - 告诉tcc, the Tiny C Compiler,它可以直接编译C 源代码转换为 x86 机器代码,甚至没有内部使用的 GIMPLE 或 LLVM 字节码等内部表示。它绝对不是一个汇编器,因为它的输入是可移植的 C。
    • 即使 clang/LLVM 也从未真正创建包含 asm 文本或 LLVM 字节码的文件,但它确实具有在优化期间表示目标中立 LLVM“指令”的内部数据结构。也可能是在优化和代码生成的最后阶段代表机器指令的那些。
    【解决方案7】:

    在大多数multi-pass compilers 中,汇编语言是在代码生成步骤中生成的。这允许您编写一次词法分析器、语法和语义阶段,然后使用单个汇编器后端生成可执行代码。这在交叉编译器中被大量使用,例如为一系列不同 cpu 生成的 C 编译器。

    几乎每个编译器都有这种形式,无论是隐式还是显式步骤。

    【讨论】:

      【解决方案8】:

      编译有很多阶段。抽象来说就是前端读取源代码,分解成token,最后分解成解析树。

      后端负责首先生成一个顺序码,比如三地址码 eg:

      代码:

      x = y + z + w
      

      进入:

      reg1 = y + z
      x = reg1 + w
      

      然后对其进行优化,将其翻译成汇编,最后翻译成机器语言。所有步骤都经过精心分层,以便在需要时可以替换其中一个

      【讨论】:

        【解决方案9】:

        您可能有兴趣收听此播客:Internals of GCC

        【讨论】:

          【解决方案10】:

          Visual C++ 有一个switch 来输出汇编代码,所以我认为它会在输出机器码之前生成汇编代码。

          【讨论】:

          • 不,如果您不要求,MSVC 的 asm 输出并不是它实际生成的。与 LLVM 不同的是,如果您只是编译,它甚至不能真正反映它在目标文件中的确切内容。 (例如,如果你用 MASM 组装它的输出,你会得到一个不同的 .obj。编译器 asm 输出为你没有使用的函数添加了额外的定义。我想我已经读过,如果你尝试,有时甚至会出现链接错误单独编译 + 汇编 + 链接,而不是仅仅编译 + 与 MSVC 链接。)
          【解决方案11】:

          Java 编译器编译成 java 字节码(二进制格式),然后使用虚拟机 (jvm) 运行它。

          虽然这可能看起来很慢,但它可以更快,因为 JVM 可以利用以后的 CPU 指令和新的优化。 C++ 编译器不会这样做 - 您必须在编译时定位指令集。

          【讨论】:

            【解决方案12】:

            虽然不是所有编译器都将源代码转换为中间级代码,但在几个编译器中存在将源代码转换为机器级代码的桥梁

            【讨论】:

              【解决方案13】:

              上面的一些答案让我感到困惑,因为在一些答案中 GCC(GNU Compiler Collection)被称为单个工具,但它是一套工具,如 GNU Assembler(也称为 GAS)、链接器、编译器和调试器,它们被使用一起生成可执行文件。是的,GCC 不会直接将 C 源文件转换为机器码。

              它分 4 步完成:

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

              【讨论】:

              • GCC 的 C 和 C++ 编译器将 C 预处理和实际编译结合到一个步骤中,例如由 /usr/lib/gcc/x86_64-pc-linux-gnu/10.1.0/cc1cc1plus 完成。多年来一直如此。几十年前,CPP 是一个生成临时文件的单独步骤,但现在情况已不再如此。那么是的,asm->object files 是使用(通常)来自 GNU Binutils(一个单独维护的包而不是 GCC)的 as 完成的,然后与 ld(也来自 Binutils)链接。
              • GDB 是另一个独立的程序,完全不涉及gcc 前端如何将源代码转换为链接的可执行文件。
              【解决方案14】:

              列表文件是编译器生成的包含汇编语言的文本文件 编译器生成的代码。大多数编译器都支持在编译过程中生成列表文件。对于某些编译器,例如 GCC,这是编译过程的标准部分,因为编译器不会直接生成对象 文件,而是生成一个汇编语言文件,然后对其进行处理 由汇编程序。在这样的编译器中,请求一个列表文件仅仅意味着 汇编器完成后编译器不得删除它。其他 编译器(例如 Microsoft 或 Intel 编译器),列表文件是可选的 必须通过命令行启用的功能。

              【讨论】:

                猜你喜欢
                • 2017-06-17
                • 1970-01-01
                • 2014-09-16
                • 1970-01-01
                • 2011-01-11
                • 1970-01-01
                • 2021-04-23
                • 1970-01-01
                • 2019-08-03
                相关资源
                最近更新 更多