实现第一个语言前端LLVM教程(八)编译为目标代码

65 阅读2分钟

8.1. 简介

欢迎来到"用LLVM实现一门语言"教程的第8章。本章描述了如何将我们的语言编译成目标文件。

8.2. 选择一个目标机器

LLVM本机支持交叉编译。您可以编译到当前机器的体系结构,也可以很容易地编译到其他体系结构。在本教程中,我们将以当前机器为目标。

为了指定您想要瞄准的体系结构,我们使用一个称为"目标三元组"的字符串。其形式为---(请参阅交叉编译文档)。

作为一个例子,我们可以看到clang认为当前的目标三元组是什么:

$ clang --version | grep Target
Target: x86_64-unknown-linux-gnu

运行这个命令可能会在您的机器上显示一些不同的东西,因为您可能使用不同的体系结构或操作系统。

幸运的是,我们不需要硬编码一个目标三元组来针对当前的机器。LLVM提供sys::getDefaultTargetTriple,它返回当前机器的目标三元组。

auto TargetTriple = sys::getDefaultTargetTriple();

LLVM不需要我们链接所有的目标功能。例如,如果我们只使用JIT,我们不需要反编译打印。类似地,如果我们只针对特定的体系结构,我们只能链接这些体系结构的功能。

对于本例,我们将初始化用于所有目标代码

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

现在我们可以使用target三元组来获取target:

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


if (!Target) {
  errs() << Error;
  return 1;
}

8.3. 目标机器

我们还需要一个TargetMachine。这个类提供了我们所瞄准的机器的完整机器描述。如果我们想要针对特定的功能(如SSE)或特定的CPU(如英特尔的Sandylake),我们现在就可以这样做。

要查看LLVM知道哪些特性和cpu,我们可以使用llc。例如,让我们看看x86

$ llvm-as < /dev/null | llc -march=x86 -mattr=help
Available CPUs for this target:

  amdfam10      - Select the amdfam10 processor.
  athlon        - Select the athlon processor.
  athlon-4      - Select the athlon-4 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 TargetMachine = Target->createTargetMachine(TargetTriple, CPU, Features, opt, Reloc::PIC_);

8.4. 配置模块

现在我们已经准备好配置模块,以指定目标和数据布局。这并不是绝对必要的,但是前端性能指南建议这样做。优化得益于了解目标和数据布局。

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

8.5. 生成目标代码 让我们定义要将文件写入的位置:

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;
}

最后,定义一个发出目标代码的pass,然后运行该pass

legacy::PassManager pass;
auto FileType = CodeGenFileType::ObjectFile;

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

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

8.6. 把上面放在一起运行

我们需要编译代码,但请注意,llvm-config的参数与前几章不同。

$ clang++ -g -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all` -o toy

让我们运行它,并定义一个简单的average函数。完成后按Ctrl-D。

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

我们有一个目标文件!为了测试它,让我们编写一个简单的程序并将其与我们的输出链接起来。下面是源代码:

#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;
}

我们将程序链接到输出。并检查结果是否符合我们的预期:

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

完整代码见链接最后