为LLVM添加简易RISCV后端(二):创建后端
LLVM
为一个新的指令集编写编译器是一件复杂的事情,尽管LLVM的出现使这个过程比之前简单多了! 一个匪夷所思的困难是缺乏一个简单的、循序渐进的教程12。 因此,本系列博客试图提供一个从头开始编写LLVM后端的简易教程来解决(部分)问题。
创建后端
开发LLVM后端并不是一件特别吸引人的事情。 您很快就会意识到,这项工作在很大程度上就是从其他现有后端复制代码。 在线论坛上,LLVM开发者建议从“复制一个现有的后端,重命名并修改它以适应您的需要”开。 但是即使是相对较小的后端,比如Lanai或XCore,也相当复杂,而且代码也不容易理解!
在本系列文章中,将采取略有不同的方法。 我们将使用现有的LLVM后端作为起点,但我已经删除了大部分代码,并将其减少到编译一个(很小的)程序所需的最低限度。 精简的后端,称为RISCW,非常简单,可以帮助理解LLVM目标独立代码生成器,而不必纠缠于细节。 在这篇文章的其余部分,我将使用RISCW后端来展示如何创建一个新的LLVM后端。 我们还将看到如何用一个实验性的后端构建LLVM,甚至编译一个(非常简单的) C程序到汇编。
Triple和ELF配置
我们首先为后端配置一个新的目标描述Triple。 由于历史原因,Triple编码了目标平台的重要信息(如体系结构、供应商和操作系统)。 以下是配置一个新的Triple的步骤:
- 在llvm/include/llvm/ADT/Triple.h用Triple声明一个新的体系结构 (见这里)。
- 提供字符串和Triple之间的类型转换(参见这里,这里及这里)。
- 指出后端支持的目标文件类型,例如ELF、COFF等,ricw只支持ELF(参见这里)。
- 指出目标平台的字平台,例如32位或64位,以及指针的大小(请参阅这里,这里及这里)。
注意:你可以在这里和这里找到更多关于Triple的信息。
注意:指令集并不一定意味着指针的大小。 例如,在为RV64编译时,指针并不总是64位的。 指针大小通常由ABI给出,在64位机器中,它可以是ilp32(即int、long和指针为32位)。
下面的参数用于配置ELF:
创建一个枚举值作为RISCW的体系结构的标识(见此处)。 这个值被编码在ELF文件头的e_machine字段中。 这个值不是随意设置的; 它必须取得授权,例如:0xF3 for RISCV。 但是我们现在将它设置为一个未使用的值。
声明ELF重定位类型(见这里和这里)。 同样,这些是依赖于架构的,这里列出了用于RISCV的类型。 在这个阶段,我们将简单地为RISCW放置了占位符。
文件格式名称(见此处)。
指示给定类的目标描述Triple(见此处)。目前,ELF头中的类是一个字节,用于对格式是32位还是64位进行编码。
注意:查看wikipedia获取更多关于ELF文件的信息。
配置驱动器
回想一下,我们使用clang将输入的C代码编译成LLVM IR。 但是clang不仅仅是我们的编译器前端,它也是一个驱动器,类似GCC,驱动编译流水线将输入的C程序转换为另一个表示,比如把C转换为汇编或目标代码。 因此,我们需要告诉clang
新后端的支持特性。例如,clang需要知道RISCW是32位还是64位。
新后端的编译流程。例如,它应该使用什么汇编程序? 什么连接器? 有哪些包括路径等等。
我们可以通过添加一个新的target类RISCWTargetInfo来告诉clang有关RISCW的信息,该类与LLVM已有的target类一起被实例化,如这里所示。 该类在这里和这里分别被声明和定义。 在这段代码中有一些重要的事情需要强调:
RISCWTargetInfo通过字符串描述数据布局。这个字符串编码许多重要信息,比如指针中每一位、堆栈对齐要求等。- 基本C数据类型的大小。
- 函数
RISCWTargetInfo::getTargetDefines(**指示编译时定义的C预处理器宏,例如,这些宏是在使用RISCV后端编译代码时定义的。 宏通常描述后端支持的体系结构、ABI、启用/禁用任何特性等
注意:一个后端可能支持多个指令集和ABI,因此驱动器的配置必须根据选定的目标Triple进行更改。 例如,RISCWTargetInfo根据Triple包含riscv32还是riscv64来更改数据布局字符串。
注意:这里可以查看RISCWTargetInfo的父类TargetInfo的声明。 它包含了更多的可以配置的选项。
配置工具链相对简单。 我们只需要实现一个从Toolchain继承的RISCWToolChain类,如下所示。 代码基本上是不言自明的,通过覆盖ToolChain类的成员,您可以修改更多的选项(见此处)。
创建新Target
每个后端在llvm/lib/Target下都有一个单独的目录,其中包含后端的大部分代码。 我们不会在这篇文章中深入讨论代码的细节(稍后我们会这样做) ,因为即使是一个很小的后端,比如RISCW,也有很多文件。 目前,我们可以将这些文件大致分为三类:
TableGen文件LLVM目标无关代码生成框架实现了一个精心设计的模式匹配算法,用于为输入的程序选择指令。 待匹配的模式使用TableGen语法描述。 此外,TableGen文件还描述了target在体系结构方面的重要特性,如寄存器的数量和调用约定等。
Build文件后端的每个目录都必须被声明,否则它将不会被构建。 此外,我们的后端的顶部目录(
llvm/lib/Target/RISCW) ,以及每个子目录必须包含两个构建文件:CMakeLists.txt和LLVMBuild.txt, 前者将源文件和任何子目录添加为生成目标,而后者为生成目标设置简单的生成参数,参数包括生成目标的名称、链接所需的库等。C++文件包含了大量的后端代码,实现了从简单的配置选项到更复杂的指令选择功能(TableGen没有实现或不能实现)的所有功能。
建立实验性后端
现在,一切都已经建立,我们可以构建带有RISCW后端的LLVM。 但是我们不能简单地根据上一章的内容修改CMake的-DLLVM_TARGETS_TO_BUILD选项,以包含RISCW,因为后端仍处于试验阶段。 相反,我们使用-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD选项,如下:
cmake -G "Ninja" -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_TARGETS_TO_BUILD="ARM;Lanai;RISCV" -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD="RISCW" -DCMAKE_BUILD_TYPE="Debug" -DLLVM_ENABLE_ASSERTIONS=On ../llvm
ninja当构建完成后,你可以检查RISCW现在是否是一个可用的后端,如下所示:
$ ./build/bin/llc --version
LLVM (http://llvm.org/):
LLVM version 10.0.1
DEBUG build with assertions.
Default target: x86_64-unknown-linux-gnu
Host CPU: znver2
Registered Targets:
arm - ARM
armeb - ARM (big endian)
lanai - Lanai
riscv32 - 32-bit RISC-V
riscv64 - 64-bit RISC-V
riscw - 32-bit RISC-V <== YAY!!
thumb - Thumb
thumbeb - Thumb (big endian)编译C程序
我们的RISCW后端只能发出两条add和ret指令,而且它不能正确处理函数调用、堆栈和几乎所有其他的东西! 因此,我们将约束自己,只编译这个小函数:
int test(int a, int b)
{
return a + b;
}就这样,我们得到了这样一个代码:
.text
.file "test.c"
.globl test ; -- Begin function test
.type test,@function
test: ; @test
; %bb.0: ; %entry
add x0, x1, x0
ret
.Lfunc_end0:
.size test, .Lfunc_end0-test
; -- End function
.ident "clang version 10.0.1 (https://github.com/llvm/llvm-project 89f2d2cc3bba7cb12cee346b3205cb0335e758cd)"
.section ".note.GNU-stack","",@progbits有很多东西缺失了,代码实际上是不正确的,在RISCV中的x0是一个硬编码为0的只读寄存器。 但是我认为我们已经达到了目标: 建立了一个最小的LLVM后端,可以很容易地用更多的特性进行扩展。
注意:如果您使用上一篇文章中的命令来编译上面的测试函数,请确保为clang设置了-target riscw和为llc设置了-march=riscw。
注意:试图编译更复杂的程序将导致cannot select...错误。如果你感兴趣,就试一试。
注意:您可以通过将-debug选项传递给llc来指示编译器打印调试信息。