我们已经看到了如何实现Kaleidoscope编程语言,从源代码到LLVM IR到优化到实现JIT编译器和实现进一步的扩展。在这篇文章中,我们将把LLVM IR编译成目标代码。
目录:
- 简介
- 目标机
- 目标代码
- 捆绑在一起
- 总结
- 参考资料
先决条件
介绍
在这篇文章中,我们将我们的代码编译成目标代码。对象代码是我们从编译器得到的输出。对象文件还没有准备好被执行,链接和加载是代码运行前仍需进行的步骤。链接器将所有东西连接在一起,这包括头文件等,而加载器将程序加载到内存中执行。
目标机
对象代码是依赖于机器的,这意味着可以在一个特定的处理器架构上执行的对象代码不一定能够在不同的平台上执行。
首先,我们要为一个特定的目标机器生成对象代码。为了得到你当前的机器架构,我们执行以下命令。
$ clang --version | grep Target
LLVM提供了sys::getDefaultTargetTriple,它返回当前机器的目标三元组,因此我们不需要为目标机器硬编码目标三元组
auto TargetTriple = sys::getDefaultTargetTriple();
另外,LLVM不允许我们链接所有的目标功能,也就是说,如果我们只想使用JIT,我们不需要汇编打印机,另外,如果我们的目标是特定的机器,我们只链接与该任务相关的功能。
下面,我们初始化所有的目标,用于发射目标代码
InitializeAllTargetInfos();
InitializeAllTargets();
InitializeAllTargetMCs();
InitializeAllAsmParsers();
InitializeAllAsmPrinters();
现在要使用我们的目标三要素来获得一个目标
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;
}
TargetMachine类提供了我们所针对的机器的完整机器描述。这是我们选择特定特征或处理器的部分。
为了查看LLVM识别的特征和处理器,我们执行以下命令:
$ llvm-as < /dev/null | llc -march=x86 -mattr=help
在这个例子中,我们使用一个没有任何特征、选项或重定位模型的通用处理器:
auto CPU = "generic";
auto Features = "";
TargetOptions opt;
auto RM = Optional<Reloc::Model>();
auto TargetMachine = 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 (TargetMachine->addPassesToEmitFile(pass, dest, nullptr, FileType)) {
errs() << "TargetMachine can't emit a file of this type";
return 1;
}
pass.run(*TheModule);
dest.flush();
把它绑在一起
我们使用以下命令编译代码:
$ clang++ -g -O3 toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs all` -o toy
然后要执行它并指定一个平均函数:
$ ./toy
ready> def average(x y) (x + y) * 0.5;
^D
Wrote output.o
一旦完成,我们按CTRL + D:
我们通过编写以下代码来测试对象代码,然后将其与输出连接起来:
#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;
}
最后,我们将我们的代码与output.o对象文件链接:
$ clang++ main.cpp output.o -o main
$ ./main
average of 3.0 and 4.0: 3.5
总结
对象代码是指可以被目标机器理解和执行的指令。这段代码离执行还很远,它需要被链接并加载到内存中执行。
在这篇文章中,我们学习了如何将LLVM IR编译成其等价的目标代码。