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

概述

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

目标选择

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

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

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

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

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

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

1
2
3
4
5
InitializeAllTargetInfos();
InitializeAllTargets();
InitializeAllTargetMCs();
InitializeAllAsmParsers();
InitializeAllAsmPrinters();

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

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

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

1
2
3
4
5
6
7
8
9
10
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 相关的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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,并且不包含任何额外的特性、选项或重定位模型,具体的设置如下所示。

1
2
3
4
5
6
auto CPU = "generic";
auto Features = "";

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

模块配置

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

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

目标代码生成

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

1
2
3
4
5
6
7
8
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;
}
最后,我们定义了一个通道用于进行代码生成,并最终调用执行。
1
2
3
4
5
6
7
8
9
10
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 文件。

1
2
3
4
$ ./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++ 程序如下所示。

1
2
3
4
5
6
7
8
9
#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 的执行结果与我们的预期是一致的。

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

总结

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

参考

  1. Cross-Compilation using Clang
  2. Kaleidoscope: Compiling to Object Code