5

基于 LLVM 自制编译器(8)——目标文件编译

 2 years ago
source link: http://chuquan.me/2022/09/11/compiler-for-kaleidoscope-08/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

基于 LLVM 自制编译器(8)——目标文件编译

发表于 2022-09-11

|

更新于 2022-09-12

| 分类于 Compiler

本章,我们将使用自制的编译器将 Kaleidoscope 代码编译成目标文件,并结合 C++ 代码进行混编。

LLVM 支持交叉编译,因此可以将源代码编译成任意目标架构的可执行文件。本章,我们将本机架构作为目标架构,编译可执行文件。

那么,如何获取本机架构的信息呢?我们使用一个字符串来表示,也称为 Target Triple,其采用 <arch><sub>-<vendor>-<sys>-<abi> 格式来表示目标架构的基本信息。详细信息可见 Cross-Compilation using Clang

如下所示,我们可以通过 clang 的相关命令获取本机的 target triple。对于不同的目标架构和操作系统,target triple 的值也不同。

$ clang --version | grep Target
Target: x86_64-apple-darwin21.6.0

在实际开发中,我们不需要为本机架构硬编码 target triple。LLVM 提供了 sys::getDefaultTargetTriple 以支持动态获取本机的 target triple。

在我们的编译器实现中,首先注册所有平台的目标信息,从而支持用户指定任意目标进行编译,具体如下所示。

InitializeAllTargetInfos();
InitializeAllTargets();
InitializeAllTargetMCs();
InitializeAllAsmParsers();
InitializeAllAsmPrinters();

当所有平台的目标信息注册完成后,我们获取本机的目标进行,并设置模块的目标为本机目标。

auto TargetTriple = sys::getDefaultTargetTriple();
TheModule->setTargetTriple(TargetTriple);

然后,我们基于本机目标的 target triple 来获取一个 Target,如下所示。

std::string Error;
auto Target = TargetRegistry::lookupTarget(TargetTriple, Error);

// Print an error and exit if we couldn't find the requested target.
// This generally occurs if we've forgotten to initialise the
// TargetRegistry or we have a bogus target triple.
if (!Target) {
errs() << Error;
return 1;
}

除了 target triple,我们还需要更加完整的目标机器的信息。对此,LLVM 提供了一个 TargetMachine 的类,用于描述目标机器的完整信息。如果我们希望指定一个特定特性(如:SSE)或特定 CPU(如:Intel 的 Sandylake),我们就可以基于 TargetMachine 进行配置。

为了查看 LLVM 支持的所有的特性和 CPU,我们可以通过 llc 命令进行查看。比如,我们可以查看 x86 相关的信息。

$ llc -march=x86 -mattr=help
Available CPUs for this target:

alderlake - Select the alderlake processor.
amdfam10 - Select the amdfam10 processor.
athlon - Select the athlon processor.
...

Available features for this target:

16bit-mode - 16-bit mode (i8086).
32bit-mode - 32-bit mode (80386).
3dnow - Enable 3DNow! instructions.
3dnowa - Enable 3DNow! Athlon instructions.
...

在我们的编译器实现中,我们使用通用的 CPU,并且不包含任何额外的特性、选项或重定位模型,具体的设置如下所示。

auto CPU = "generic";
auto Features = "";

TargetOptions opt;
auto RM = Optional<Reloc::Model>();
auto TheTargetMachine = Target->createTargetMachine(TargetTriple, CPU, Features, opt, RM);

下面,我们来对模块进行配置,指定目标和数据布局。虽然模块配置不是必须的,但是官方教程推荐进行配置。配置模块,指定目标和数据布局,有利于后续进行优化。

TheModule->setDataLayout(TargetMachine->createDataLayout());
TheModule->setTargetTriple(TargetTriple);

目标代码生成

至此,我们已经完成了代码生成的前期准备和设置。下面,我们来定义输出文件。

auto Filename = "output.o";
std::error_code EC;
raw_fd_ostream dest(Filename, EC, sys::fs::OF_None);

if (EC) {
errs() << "Could not open file: " << EC.message();
return 1;
}

最后,我们定义了一个通道用于进行代码生成,并最终调用执行。

legacy::PassManager pass;
auto FileType = CGFT_ObjectFile;

if (TheTargetMachine->addPassesToEmitFile(pass, dest, nullptr, FileType)) {
errs() << "TheTargetMachine can't emit a file of this type";
return 1;
}

pass.run(*TheModule);
dest.flush();

下面,我们来编译代码,生成编译器,并进行测试。我们输入基于 Kaleidoscope 编写的 average 函数,并输入 Ctr-D 退出执行。此时编译器编译生成一个 output.o 文件。

$ ./Kaleidoscope-Ch8
ready> def average(x y) (x + y) * 0.5;
^D
Wrote output.o

至此,我们生成了一个基于 Kaleidoscope 编写的 average 函数的 output.o 目标文件。接下来,我们使用 C++ 编写一个简单的程序 main.cpp,并调用 Kaleidoscope 编写的 average 函数。C++ 程序如下所示。

#include <iostream>

extern "C" {
double average(double, double);
}

int main() {
std::cout << "average of 3.0 and 4.0: " << average(3.0, 4.0) << std::endl;
}

最后,我们编译 main.cpp 文件,链接 output.o 文件,并运行最终的可执行文件。如下所示,最终的可执行文件 main 的执行结果与我们的预期是一致的。

$ clang++ main.cpp output.o -o main
$ ./main
average of 3.0 and 4.0: 3.5

本章,我们为本机架构配置目标信息,并基于此构建 Kaleidoscope 编译器。我们通过该编译器,对一段 Kaleidoscope 代码进行编译,生成了一个目标文件。最终,对一段 C++ 代码进行编译,链接 Kaleidoscope 目标文件,实现混合编译。

欣赏此文?求鼓励,求支持!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK