本文是一篇译文,翻译自:
https://llvm.org/docs/CodeGenerator.html​llvm.org/docs/CodeGenerator.html
如有问题,敬请指出。
转载需注明出处,若需相关专业翻译服务,可联系我。


LLVM 目标无关代码生成器是一个软件框架,它提供了一些可重用的组件,能将 LLVM 中间表示语言翻译成特殊目标下的机器代码。这里所指的机器代码,既可以是汇编码(静态编译器组成部分),也可作为二进制机器码(通过 JIT 编译器运行)。

译注:本文中会把原文 Machine Code 翻译为机器码,但需注意,此处机器码并不表示二进制可执行文件,如 ELF 文件,Machine Code 是 LLVM 后端中 MC 层的中间表示,虽然其与二进制可执行文件有较大相似之处,但依然是两个不同的东西。若文中需要翻译为二进制可执行文件,会特殊指出。

LLVM 目标无关代码生成器由 6 个主要的组件组成:

  • 抽象的目标描述接口:用来维护那些重要的机器特性,而不需要考虑他们的使用。这些接口存放在 include/llvm/Target/ 下。
  • 用来做代码生成的类:这些类是通用的,抽象地囊括了所有的后端机器,他们用来做代码生成工作,定义在 include/llvm/CodeGen/ 下。在这一层下,如 “常量池” 和 “跳转表” 的概念都被显式的暴露出来。
  • 用来描述目标文件的类和算法:在 MC 层下,这些类描述了汇编级别的概念,如 labels、 sections、 instructions。在这一层下,如 “常量池” 和 “跳转表” 的概念已经不存在。
  • 目标无关的算法:提供了一些基础的后端算法,如寄存器分配、调度、栈帧表示等工作的算法。这些代码放在 lib/CodeGen/ 下。
  • 抽象目标描述接口的实现:针对特定后端目标,这部分代码实现了 LLVM 提供的抽象描述接口,并且可以根据需要自定义一些目标相关的 pass,从而来实现针对特定目标的代码生成器。这些代码放在 lib/Target/ 下。
  • JIT 组件:完全与目标无关的工具,实现 JIT 的功能。代码存放在 lib/ExecutionEngine/JIT/ 下。

你可以根据你说感兴趣的部分,选择在部分组件上工作。任何情况下,你都应该熟悉目标描述和机器代码生成的部分类(Class)。如果你需要新增一个自己的后端,你还需要对抽象的目标描述接口进行实现,还需要对 LLVM 语言(LLVM IR)有一定了解。如果你对后端代码生成的算法比较感兴趣,它仅仅依赖于目标描述和机器代码生成的一些类,需要确保它们工作正常。

译注:本文所谓的目标无关target independent),是指 LLVM 后端本身实现了一整套的算法和接口,这些庞大的代码库与某个具体的后端架构无关,也就是说这些代码库实现了编译器后端所有的通用功能。按照 LLVM 的设计理念,所有和机器相关的信息,都使用 TableGen 描述语言来描述,并通过 tblgen 工具(本身也是一个小编译器)来生成可以对接到目标无关算法和接口的 c++ 代码。然而,目前还有很多比较复杂的场景 TableGen 无法有效描述,所以还需要编写一些 c++ 代码。
本文频繁强调目标无关,其愿望是想表达 LLVM 高度模块化、低耦合的特性,其实不必过于困惑其含义。下文翻译中,结合上下文,可能将 target independent 译为目标无关、机器无关、平台无关等,实则表示同一个意思。

  1. LLVM 目标无关代码生成器 1:介绍
  2. LLVM 目标无关代码生成器 2:目标描述类
  3. LLVM 目标无关代码生成器 3:机器代码描述类
  4. LLVM 目标无关代码生成器 4:MC 层
  5. LLVM 目标无关代码生成器 5:目标无关代码生成算法
  6. LLVM 目标无关代码生成器 6:实现原生汇编器

1.1 代码生成

LLVM 代码生成器的两个最重要的组成部分分别是:代码生成器的高层接口一系列可重用与构建目标相关后端的组件。有两个很重要的接口:TargetMachine 和 DataLayout,是必须要在一个确定的后端中实现的,但其他接口都是可选的,在你需要它们的时候再来定义(译注:如果是修改现有后端,则不必太关心这块)。

这个设计具有两个非常重要的意义。

首先,LLVM 可以完整的为非传统的后端生成代码。比如,一个 C 后端(指输出的代码是 C 语言)并不需要做寄存器分配、指令选择或其他实际后端的标准工作,它仅需要实现上边提到的这两个接口。需要说明的是,C 后端在 LLVM 3.1 版中被移除了。另一个例子是将 LLVM IR 翻译成 GCC RTL 格式的代码,提供给 GCC 来生成具体的目标机器代码。

其次,这也意味着能够在 LLVM 框架下实现完全不依赖内置组件的后端代码生成器。虽然并不推荐这么做,但针对有些与 LLVM 描述的机器模型完全不同的目标,是可以实现的,比如 FPGA。

1.2 代码生成器的高层设计

LLVM 目标无关的代码生成器,被用来支持生成高效的、高质量的寄存器模型机器(译注:与其对应的机器模型如栈机,用 LLVM 支持度不佳)的执行代码。这一模型可分为以下几个组成部分:

  • 指令选择:这个阶段实现了一套高效的方式来将输入的 LLVM 代码展开成目标指令集,使用了 SSA 格式的虚拟寄存器以及一部分寄存器分配(由于目标约束或调用约定限制)要求的物理寄存器。这一步骤,会将 LLVM 的代码转换为目标相关的 DAG (有向无环图)。
  • 指令调度和规范化:这个阶段对目标相关的 DAG 进行处理,对指令顺序进行调整,然后依据这种顺序发射一种叫做 MachineInstrs 的代码表示。这一部分内容会在指令选择阶段描述,因为它是在 SelectionDAG 数据结构中的操作。
  • 基于 SSA 的机器代码优化:这是一个可选的阶段,由一系列针对 SSA 结构的机器代码优化操作组成,它在指令选择阶段工作,比如像 modulo-scheduling 和 peephole 优化工作在这里完成。
  • 寄存器分配:将 SSA 结构下带有无限虚拟寄存器的机器代码转换为有限机器寄存器的结构。这一阶段会实现 spill 功能代码来消除所有的虚拟寄存器。
  • Prolog/Epilog 代码插入:当需要生成带有函数的机器代码时,prolog 和 epilog 代码将会被插入到逻辑代码之间,并且抽象的栈引用会被消除。这个阶段还负责实现一些优化,如 frame pointer elimination 和 stack packing。
  • 机器代码优化:对“最终”的机器代码进行一次后优化,包括如 spill code scheduling 和 peephole optimization。
  • 代码发射:真正的最后阶段,将输出适配对应机器汇编格式的汇编码或可执行的二进制机器码。

代码生成器基于以下假设:指令选择器将使用最佳的模式匹配方式来常见针对本机指令的高质量序列。备选的基于模式扩展(pattern expansion)和激进迭代式窥孔优化(aggressive iterative peephole optimization)的代码生成器会更慢一些。这种设计将复杂程度不同的组件组合起来的方式,可以同时满足进行高效编译(有利于 JIT 环境)和激进优化(有利于离线代码生成)的需求。

除了以外的这些阶段,目标代码生成器的实现还可以插入一些额外的操作,通过特殊的 pass 来完成。比如在 X86 平台上,通过一个特殊的 pass 来处理 80x87 浮点栈架构。其他一些后端平台也可以通过自定义的 pass 来完成一些不常见的需求。

1.3 使用 TableGen 来描述目标平台

目标描述的类需要一些有关于目标平台的细节,这些目标描述同时还会有很大一部分通用的信息(比如,一个 add 指令大多数时候与一个 sub 指令等价)。为了能让这些通用的信息独立出来,LLVM 使用了叫 TableGen 的工具来描述目标机器有差异性的信息,通过使用领域特定语言来描述目标特殊的细节,从而避免大量的通用信息的重复实现。

随着 LLVM 持续性的开发,我们计划将越来越多的目标描述性信息放到 .td 文件(TableGen 处理文件)中完成,这将会是一个非常大的优势。这对于简化 LLVM 的 porting 有很重要的意义,因为它减少了大量需要手工完成的 C++ 代码,并且代码生成器的接口结构也更容易被理解。另外,这也有利于快速改变一些东西,如果所有的表结构和描述信息都通过 tblgen (TableGen执行程序)来发射,我们便仅仅需要修改 tblgen 工具便可以针对所有后端适用新的接口。


除了最后一大章和硬件有关的内容以外,全文已经翻译结束。一共写了 1 万 7 千字,本来打算一次性发出来,但知乎有每篇文章的字数限制,想了想,还是按章节发吧。

下边我会根据新发布之后的其他文章给出跳转链接。