原文:http://www.ibm.com/developerworks/cn/opensource/os-createcompilerllvm1/
LLVM(之前称为低级虚拟机)是一种非常强大的编译器基础架构框架,专门为使用您喜爱的编程语言编写的程序的编译时、链接时和运行时优化而设计。LLVM 可运行于若干个不同的平台之上,它以能够生成快速运行的代码而著称。
LLVM 框架是围绕着代码编写良好的中间表示 (IR) 而构建的。本文(由两部分组成的系列文章的第一部分)将深入讲解 LLVM IR 的基础知识以及它的一些微妙之处。在这里,您将构建一个可以自动为您生成 LLVM IR 的代码生成器。拥有一个 LLVM IR 生成器意味着您所需要的是一个前端以供插入您所喜爱的编程语言,而且这还意味着您拥有一个完整的流程(前端解析器 + IR 生成器 + LLVM 后端)。创建一个自定义编译器会变得更加简单。
在开始之前,在您的开发计算器上必须已经拥有已编译好的 LLVM(参阅 参考资料 获取相关链接)。本文中的示例均基于 LLVM V3.0。对于 LLVM 代码的后期生成和安装,最重要的两个工具是 llc 和 lli。
因为 LLVM 是一个虚拟机,所以它可能应该拥有自己的中间字节代码表示,不是吗?最后,您需要将 LLVM 字节代码编译到特定于平台的汇编语言中。然后您才能通过本机汇编程序和链接器来运行汇编代码,从而生成可执行的共享库等。您可以使用 llc 将 LLVM 字节代码转换成特定于平台的汇编代码(请参阅 参考资料,获取关于此工具的更多信息的链接)。对于 LLVM 字节代码的直接执行部分,不要等到在本机执行代码崩溃后才发现您的程序中有一个或两个 bug。这正是 lli 的用武之地,因为它可以直接执行字节代码。lli 可以通过解释器或使用高级选项中的即时 (JIT) 编译器执行此工作。请参阅 参考资料,获取关于 lli 的更多信息的链接。
llvm-gcc 是 GNU Compiler Collection (gcc) 的修改版本,可以在使用 -S -emit-llvm 选项运行时会生成 LLVM 字节代码。然后您可以使用 lli 来执行这个已生成的字节代码(也称为 LLVM 汇编语言)。有关 llvm-gcc 的更多信息,请参阅 参考资料。如果您没有在自己的系统中预先安装 llvm-gcc,那么您应该能够从源代码构建它,请参阅 参考资料,获取分步指南的链接。
要更好地理解 LLVM,您必须了解 LLVM IR 及其微妙之处。这个过程类似于学习另一种编程语言。但是,如果您熟悉 C 语言和 C++语言以及它们的一些语法怪现象,那么在了解 LLVM IR 方面您应该没有太大的障碍。清单 1 给出了您的第一个程序,该程序将在控制台输出中打印 "Hello World"。要编译此代码,您可以使用 llvm-gcc。
#include <stdio.h>
int main( )
{
printf("Hello World!\n");
}
|
要编译此代码,请输入此命令:
Tintin.local# llvm-gcc helloworld.cpp -S -emit-llvm |
完成编译后,llvm-gcc 会生成 helloworld.s 文件,您可以使用 lli 来执行该文件,将消息输出到控制台。lli 的用法如下:
Tintin.local# lli helloworld.s Hello, World |
现在,先看一下 LLVM 汇编语言。清单 2 给出了该代码。
清单 2. Hello World 程序的 LLVM 字节代码
@.str = private constant [13 x i8] c"Hello World!\00", align 1 ;
define i32 @main() ssp {
entry:
%retval = alloca i32
%0 = alloca i32
%"alloca point" = bitcast i32 0 to i32
%1 = call i32 @puts(i8* getelementptr inbounds ([13 x i8]* @.str, i64 0, i64 0))
store i32 0, i32* %0, align 4
%2 = load i32* %0, align 4
store i32 %2, i32* %retval, align 4
br label %return
return:
%retval1 = load i32* %retval
ret i32 %retval1
}
declare i32 @puts(i8*)
|
LLVM 提供了一个详细的汇编语言表示(参阅 参考资料 获取相关的链接)。在开始编写我们之前讨论的自己的 Hello World 程序版本之前,有几个需知事项:
- LLVM 汇编语言中的注解以分号 (
;) 开始,并持续到行末。 - 全局标识符要以
@字符开始。所有的函数名和全局变量都必须以@开始。 - LLVM 中的局部标识符以百分号 (
%) 开始。标识符典型的正则表达式是[%@][a-zA-Z$._][a-zA-Z$._0-9]*。 - LLVM 拥有一个强大的类型系统,这也是它的一大特性。LLVM 将整数类型定义为
iN,其中 N 是整数占用的字节数。您可以指定 1 到 223- 1 之间的任意位宽度。 - 您可以将矢量或阵列类型声明为
[no. of elements X size of each element]。对于字符串 "Hello World!",可以使用类型[13 x i8],假设每个字符占用 1 个字节,再加上为 NULL 字符提供的 1 个额外字节。 - 您可以对 hello-world 字符串的全局字符串常量进行如下声明:
@hello = constant [13 x i8] c"Hello World!\00"。使用关键字constant来声明后面紧跟类型和值的常量。我们已经讨论过类型,所以现在让我们来看一下值:您以c开始,后面紧跟放在双引号中的整个字符串(其中包括\0并以0结尾)。不幸的是,关于字符串的声明为什么需要使用c前缀,并在结尾处包含 NULL 字符和 0,LLVM 文档未提供任何解释。如果您有兴趣研究更多有关 LLVM 的语法怪现象,请参阅 参考资料,获取语法文件的链接。 - LLVM 允许您声明和定义函数。而不是仔细查看 LLVM 函数的整个特性列表,我只需将精力集中在基本要点上即可。以关键字
define开始,后面紧跟返回类型,然后是函数名。返回 32 字节整数的main的简单定义类似于:define i32 @main() { ; some LLVM assembly code that returns i32 }。 - 函数声明,顾名思义,有着重大的意义。这里提供了
puts方法的最简单声明,它是printf:declare i32 puts(i8*)的 LLVM 等同物。该声明以关键字declare开始,后面紧跟着返回类型、函数名,以及该函数的可选参数列表。该声明必须是全局范围的。 - 每个函数均以返回语句结尾。有两种形式的返回语句:
ret <type> <value>或ret void。对于您简单的主例程,使用ret i32 0就足够了。 - 使用
call <function return type> <function name> <optional function arguments>来调用函数。注意,每个函数参数都必须放在其类型的前面。返回一个 6 位的整数并接受一个 36 位的整数的函数测试的语法如下:call i6 @test( i36 %arg1 )。
这只是一个开始。您还需要定义一个主例程、一个存储字符串的常量,以及处理实际打印的 puts 方法的声明。清单 3 显示第一次尝试创建的程序。
清单 3. 第一次尝试创建手动编写的 Hello World 程序
declare i32 @puts(i8*)
@global_str = constant [13 x i8] c"Hello World!\00"
define i32 @main {
call i32 @puts( [13 x i8] @global_str )
ret i32 0
}
|
这里提供了来自 lli 的日志:
lli: test.s:5:29: error: global variable reference must have pointer type
call i32 @puts( [13 x i8] @global_str )
^
|
程序并未按预期的运行。发生了什么?如之前所提及的,LLVM 拥有一个强大的类型系统。因为 puts 期望提供一个指向 i8 的指针,并且您能传递一个 i8 矢量,这样 lli 才能快速指出错误。该问题的常用解决方法(来自 C 编程背景)是使用类型转换。这将您引向了 LLVM 指令 getelementptr。请注意,您必须将 清单 3 中的 puts 调用修改为与 call i32 @puts(i8* %t) 类似,其中%t 是类型 i8*,并且是 [13 x i8] to i8* 的类型转换结果。(请参阅 参考资料,获取 getelementptr 的详细描述的链接。)在进一步探讨之前,清单 4 提供了可行的代码。
清单 4. 使用 getelementptr 正确地将类型转换为指针
declare i32 @puts (i8*)
@global_str = constant [13 x i8] c"Hello World!\00"
define i32 @main() {
%temp = getelementptr [13 x i8]* @global_str, i64 0, i64 0
call i32 @puts(i8* %temp)
ret i32 0
}
|
getelementptr 的第一个参数是全局字符串变量的指针。要单步执行全局变量的指针,则需要使用第一个索引,即 i64 0。因为getelementptr 指令的第一个参数必须始终是 pointer 类型的值,所以第一个索引会单步调试该指针。0 值表示从该指针起偏移 0 元素偏移量。我的开发计算机运行的是 64 位 Linux®,所以该指针是 8 字节。第二个索引 (i64 0) 用于选择字符串的第 0 个元素,该元素是作为 puts 的参数来提供的。
了解 LLVM IR 是件好事,但是您需要一个自动化的代码生成系统,用它来转储 LLVM 汇编语言。谢天谢地,LLVM 提供了强大的应用程序编程接口 (API) 支持,让您可以查看整个过程(请参阅 参考资料,获取程序员手册的链接)。在您的开发计算机上查找 LLVMContext.h 文件;如果该文件缺失,那么可能是您安装 LLVM 的方式出错。
现在,让我们创建一个程序,为之前讨论的 Hello World 程序生成 LLVM IR。该程序不会处理这里的整个 LLVM API,但是接下来的代码样例会证明,适量位数的 LLVM API 很直观而且易于使用。
LLVM 提供了一款出色的工具,叫做 llvm-config(参阅 参考资料)。运行 llvm-config –cxxflags,获取需要传递至 g++ 的编译标志、链接器选项的 llvm-config –ldflags 以及 llvm-config –ldflags,以便针对正确的 LLVM 库进行链接。在 清单 5的样例中,所有的选项均需要传递至 g++。
清单 5. 通过 LLVM API 使用 llvm-config 构建代码
tintin# llvm-config --cxxflags --ldflags --libs \ -I/usr/include -DNDEBUG -D_GNU_SOURCE \ -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS \ -D__STDC_LIMIT_MACROS -O3 -fno-exceptions -fno-rtti -fno-common \ -Woverloaded-virtual -Wcast-qual \ -L/usr/lib -lpthread -lm \ -lLLVMXCoreCodeGen -lLLVMTableGen -lLLVMSystemZCodeGen \ -lLLVMSparcCodeGen -lLLVMPTXCodeGen \ -lLLVMPowerPCCodeGen -lLLVMMSP430CodeGen -lLLVMMipsCodeGen \ -lLLVMMCJIT -lLLVMRuntimeDyld \ -lLLVMObject -lLLVMMCDisassembler -lLLVMXCoreDesc -lLLVMXCoreInfo \ -lLLVMSystemZDesc -lLLVMSystemZInfo \ -lLLVMSparcDesc -lLLVMSparcInfo -lLLVMPowerPCDesc -lLLVMPowerPCInfo \ -lLLVMPowerPCAsmPrinter \ -lLLVMPTXDesc -lLLVMPTXInfo -lLLVMPTXAsmPrinter -lLLVMMipsDesc \ -lLLVMMipsInfo -lLLVMMipsAsmPrinter \ -lLLVMMSP430Desc -lLLVMMSP430Info -lLLVMMSP430AsmPrinter \ -lLLVMMBlazeDisassembler -lLLVMMBlazeAsmParser \ -lLLVMMBlazeCodeGen -lLLVMMBlazeDesc -lLLVMMBlazeAsmPrinter \ -lLLVMMBlazeInfo -lLLVMLinker -lLLVMipo \ -lLLVMInterpreter -lLLVMInstrumentation -lLLVMJIT -lLLVMExecutionEngine \ -lLLVMDebugInfo -lLLVMCppBackend \ -lLLVMCppBackendInfo -lLLVMCellSPUCodeGen -lLLVMCellSPUDesc \ -lLLVMCellSPUInfo -lLLVMCBackend \ -lLLVMCBackendInfo -lLLVMBlackfinCodeGen -lLLVMBlackfinDesc \ -lLLVMBlackfinInfo -lLLVMBitWriter \ -lLLVMX86Disassembler -lLLVMX86AsmParser -lLLVMX86CodeGen \ -lLLVMX86Desc -lLLVMX86AsmPrinter -lLLVMX86Utils \ -lLLVMX86Info -lLLVMAsmParser -lLLVMARMDisassembler -lLLVMARMAsmParser \ -lLLVMARMCodeGen -lLLVMARMDesc \ -lLLVMARMAsmPrinter -lLLVMARMInfo -lLLVMArchive -lLLVMBitReader \ -lLLVMAlphaCodeGen -lLLVMSelectionDAG \ -lLLVMAsmPrinter -lLLVMMCParser -lLLVMCodeGen -lLLVMScalarOpts \ -lLLVMInstCombine -lLLVMTransformUtils \ -lLLVMipa -lLLVMAnalysis -lLLVMTarget -lLLVMCore -lLLVMAlphaDesc \ -lLLVMAlphaInfo -lLLVMMC -lLLVMSupport |
LLVM 模块类是其他所有 LLVM IR 对象的顶级容器。LLVM 模块类能够包含全局变量、函数、该模块所依赖的其他模块和符号表等对象的列表。这里将提供了 LLVM 模块的构造函数:
explicit Module(StringRef ModuleID, LLVMContext& C); |
要构建您的程序,必须从创建 LLVM 模块开始。第一个参数是该模块的名称,可以是任何虚拟的字符串。第二个参数称为LLVMContext。LLVMContext 类有些晦涩,但用户足以了解它提供了一个用来创建变量等对象的上下文环境。该类在多线程的上下文环境中变得非常重要,您可能想为每个线程创建一个本地上下文环境,并且想让每个线程完全独立于其他上下文环境运行。目前,使用这个默认的全局上下文来处理 LLVM 所提供的代码。这里给出了创建模块的代码:
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
|
您要了解的下一个重要类是能实际提供 API 来创建 LLVM 指令并将这些指令插入基础块的类:IRBuilder 类。IRBuilder 提供了许多华而不实的方法,但是我选择了最简单的可行方法来构建一个 LLVM 指令,即使用以下代码来传递全局上下文:
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
|
准备好 LLVM 对象模型后,就可以调用模块的 dump 方法来转储其内容。清单 6 给出了该代码。
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h"
int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
module->dump( );
}
|
运行 清单 6 中的代码之后,控制台的输出如下:
; ModuleID = 'top' |
然后,您需要创建 main 方法。LLVM 提供了 llvm::Function 类来创建一个函数,并提供了 llvm::FunctionType 将该函数与某个返回类型相关联。此外,请记住,main 方法必须是该模块的一部分。清单 7 给出了该代码。
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h"
int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getInt32Ty(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module);
module->dump( );
}
|
请注意,您需要让 main 返回 void,这就是您调用 builder.getVoidTy() 的原因;如果 main 返回 i32,那么该调用会是builder.getInt32Ty()。在编译并运行 清单 7 中的代码后,出现的结果如下:
; ModuleID = 'top' declare void @main() |
您还尚未定义 main 要执行的指令集。为此,您必须定义一个基础块并将其与 main 方法关联。基础块 是 LLVM IR 中的一个指令集合,拥有将标签(类似于 C 标签)定义为其构造函数的一部分的选项。builder.setInsertPoint 会告知 LLVM 引擎接下来将指令插入何处。清单 8 给出了该代码。
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h"
int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getInt32Ty(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module);
llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry);
module->dump( );
}
|
这里提供了 清单 8 的输出。请注意,由于现在已经定义了 main 的基础块,所以 LLVM 转储将 main 看作为是一个方法定义,而不是一个声明。非常酷!
; ModuleID = 'top'
define void @main() {
entrypoint:
}
|
现在,向代码添加全局 hello-world 字符串。清单 9 给出了该代码。
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h"
int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getVoidTy(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module);
llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry);
llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n");
module->dump( );
}
|
在 清单 9 的输出中,注意 LLVM 引擎是如何转储字符串的:
; ModuleID = 'top'
@0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00"
define void @main() {
entrypoint:
}
|
现在您需要做的就是声明 puts 方法,并且调用它。要声明 puts 方法,则必须创建合适的 FunctionType*。从您的 Hello World 源始代码中,您知道 puts 返回了 i32 并接受 i8* 作为输入参数。清单 10 给出了创建 puts 的正确类型的代码。
std::vector<llvm::Type *> putsArgs;
putsArgs.push_back(builder.getInt8Ty()->getPointerTo());
llvm::ArrayRef<llvm::Type*> argsRef(putsArgs);
llvm::FunctionType *putsType =
llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false);
llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType);
|
FunctionType::get 的第一个参数是返回类型;第二个参数是一个 LLVM::ArrayRef 结构,并且最后的 false 指明了后面未跟可变数量的参数。ArrayRef 结构与矢量相似,只是它不包含任何基础数据,并且主要用于包装诸如阵列和矢量等数据块。由于这个改变,输出显示将如 清单 11 所示。
; ModuleID = 'top'
@0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00"
define void @main() {
entrypoint:
}
declare i32 @puts(i8*)
|
剩下要做的是调用 main 中的 puts 方法,并从 main 中返回。LLVM API 非常关注转换等操作:您需要做的是调用 puts 来调用builder.CreateCall。最后,要创建返回语句,请调用 builder.CreateRetVoid。清单 12 提供了完整的运行代码。
#include "llvm/ADT/ArrayRef.h"
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Function.h"
#include "llvm/BasicBlock.h"
#include "llvm/Support/IRBuilder.h"
#include <vector>
#include <string>
int main()
{
llvm::LLVMContext & context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("asdf", context);
llvm::IRBuilder<> builder(context);
llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getVoidTy(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module);
llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry);
llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n");
std::vector<llvm::Type *> putsArgs;
putsArgs.push_back(builder.getInt8Ty()->getPointerTo());
llvm::ArrayRef<llvm::Type*> argsRef(putsArgs);
llvm::FunctionType *putsType =
llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false);
llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType);
builder.CreateCall(putsFunc, helloWorld);
builder.CreateRetVoid();
module->dump();
}
|
在这篇初步了解 LLVM 的文章中,了解了诸如 lli 和 llvm-config 等 LLVM 工具,还深入研究了 LLVM 中间代码,并使用 LLVM API 来为您自己生成中间代码。本系列的第二部分(也是最后一部分)将探讨可以使用 LLVM 完成的另一项任务,即毫不费力地添加额外的编译传递。
使用 LLVM 框架创建一个工作编译器,第 1 部分 探讨了 LLVM 中间表示 (IR)。您手动创建了一个 “Hello World” 测试程序;了解了 LLVM 的一些细微差别(如类型转换);并使用 LLVM 应用程序编程接口 (API) 创建了相同的程序。在这一过程中,您还了解到一些 LLVM 工具,如llc 和 lli,并了解了如何使用 llvm-gcc 为您发出 LLVM IR。本文是系列文章的第二篇也是最后一篇,探讨了可以与 LLVM 结合使用的其他一些炫酷功能。具体而言,本文将介绍代码测试,即向生成的最终可执行的代码添加信息。本文还简单介绍了 clang,这是 LLVM 的前端,用于支持 C、C++ 和 Objective-C。您可以使用 clang API 对C/C++ 代码进行预处理并生成一个抽象语法树 (AST)。
LLVM 以其提供的优化特性而著名。优化被实现为阶段 (pass)(有关 LLVM 阶段的高级功能的细节,请参见 参考资料)。这里需要注意的是 LLVM 为您提供了使用最少量的代码创建实用阶段 (utility pass) 的功能。例如,如果不希望使用 “hello” 作为函数名称的开头,那么可以使用一个实用阶段来实现这个目的。
从 opt 的手册页中可以看到,“opt 命令是模块化的 LLVM 优化器和分析器”。一旦您的代码支持定制阶段,您将使用 opt 把代码编译为一个共享库并对其进行加载。如果您的 LLVM 安装进展顺利,那么 opt 应该已经位于您的系统中。opt 命令接受 LLVM IR(扩展名为 .ll)和 LLVM 位码格式(扩展名为 .bc),可以生成 LLVM IR 或位码格式的输出。下面展示了如何使用 opt 加载您的定制共享库:
tintin# opt –load=mycustom_pass.so –help –S |
还需注意,从命令行运行 opt –help 会生成一个 LLVM 将要执行的阶段的细目清单。对 help 使用 load 选项将生成一条帮助消息,其中包括有关定制阶段的信息。
您需要在 Pass.h 文件中声明 LLVM 阶段,该文件在我的系统中被安装到 /usr/include/llvm 下。该文件将各个阶段的接口定义为 Pass类的一部分。各个阶段的类型都从 Pass 中派生,也在该文件中进行了声明。阶段类型包括:
BasicBlockPass类。用于实现本地优化,优化通常每次针对一个基本块或指令运行FunctionPass类。用于全局优化,每次执行一个功能ModulePass类。用于执行任何非结构化的过程间优化
由于您打算创建一个阶段,该阶段拒绝任何以 “Hello ” 开头的函数名,因此需要通过从 FunctionPass 派生来创建自己的阶段。从 Pass.h 中复制 清单 1 中的代码。
清单 1. 覆盖 FunctionPass 中的 runOnFunction 类
Class FunctionPass : public Pass {
/// explicit FunctionPass(char &pid) : Pass(PT_Function, pid) {}
/// runOnFunction - Virtual method overridden by subclasses to do the
/// per-function processing of the pass.
///
virtual bool runOnFunction(Function &F) = 0;
/// …
};
|
同样,BasicBlockPass 类声明了一个 runOnBasicBlock,而 ModulePass 类声明了 runOnModule 纯虚拟方法。子类需要为虚拟方法提供一个定义。
返回到 清单 1 中的 runOnFunction 方法,您将看到输出为类型 Function 的对象。深入钻研 /usr/include/llvm/Function.h 文件,就会很容易发现 LLVM 使用 Function 类封装了一个 C/C++ 函数的功能。而 Function 派生自 Value.h 中定义的 Value 类,并支持getName 方法。清单 2 显示了代码。
#include "llvm/Pass.h"
#include "llvm/Function.h"
class TestClass : public llvm::FunctionPass {
public:
virtual bool runOnFunction(llvm::Function &F)
{
if (F.getName().startswith("hello"))
{
std::cout << "Function name starts with hello\n";
}
return false;
}
};
|
清单 2 中的代码遗漏了两个重要的细节:
FunctionPass构造函数需要一个char,用于在 LLVM 内部使用。LLVM 使用char的地址,因此您可以使用任何内容对它进行初始化。- 您需要通过某种方式让 LLVM 系统理解您所创建的类是一个新阶段。这正是
RegisterPassLLVM 模板发挥作用的地方。您在 PassSupport.h 头文件中声明了RegisterPass模板;该文件包含在 Pass.h 中,因此无需额外的标头。
清单 3 展示了完整的代码。
class TestClass : public llvm::FunctionPass
{
public:
TestClass() : llvm::FunctionPass(TestClass::ID) { }
virtual bool runOnFunction(llvm::Function &F) {
if (F.getName().startswith("hello")) {
std::cout << "Function name starts with hello\n";
}
return false;
}
static char ID; // could be a global too
};
char TestClass::ID = 'a';
static llvm::RegisterPass<TestClass> global_("test_llvm", "test llvm", false, false);
|
RegisterPass 模板中的参数 template 是将要在命令行中与 opt 一起使用的阶段的名称。也就是说,您现在所需做的就是在 清单 3 中的代码之外创建一个共享库,然后运行 opt 来加载该库,之后是使用 RegisterPass 注册的命令的名称(在本例中为test_llvm),最后是一个位码文件,您的定制阶段将在该文件中与其他阶段一起运行。清单 4 中概述了这些步骤。
bash$ g++ -c pass.cpp -I/usr/local/include `llvm-config --cxxflags` bash$ g++ -shared -o pass.so pass.o -L/usr/local/lib `llvm-config --ldflags -libs` bash$ opt -load=./pass.so –test_llvm < test.bc |
现在让我们了解另一个工具(LLVM 后端的前端):clang。
LLVM 拥有自己的前端:名为 clang 的一种工具(恰如其分)。Clang 是一种功能强大的 C/C++/Objective-C 编译器,其编译速度可以媲美甚至超过 GNU Compiler Collection (GCC) 工具(参见 参考资料 中的链接,获取更多信息)。更重要的是,clang 拥有一个可修改的代码基,可以轻松实现定制扩展。与在 使用 LLVM 框架创建一个工作编译器,第 1 部分 中对定制插件使用 LLVM 后端 API 的方式非常类似,本文将对 LLVM 前端使用该 API 并开发一些小的应用程序来实现预处理和解析功能。
您需要熟悉一些最常见的 clang 类:
CompilerInstancePreprocessorFileManagerSourceManagerDiagnosticsEngineLangOptionsTargetInfoASTConsumerSemaParseAST也许是最重要的 clang 方法。
稍后将详细介绍 ParseAST 方法。
要实现所有实用的用途,考虑使用适当的 CompilerInstance 编译器。它提供了接口,管理对 AST 的访问,对输入源进行预处理,而且维护目标信息。典型的应用程序需要创建 CompilerInstance 对象来完成有用的功能。清单 5 展示了 CompilerInstance.h 头文件的大致内容。
class CompilerInstance : public ModuleLoader {
/// The options used in this compiler instance.
llvm::IntrusiveRefCntPtr<CompilerInvocation> Invocation;
/// The diagnostics engine instance.
llvm::IntrusiveRefCntPtr<DiagnosticsEngine> Diagnostics;
/// The target being compiled for.
llvm::IntrusiveRefCntPtr<TargetInfo> Target;
/// The file manager.
llvm::IntrusiveRefCntPtr<FileManager> FileMgr;
/// The source manager.
llvm::IntrusiveRefCntPtr<SourceManager> SourceMgr;
/// The preprocessor.
llvm::IntrusiveRefCntPtr<Preprocessor> PP;
/// The AST context.
llvm::IntrusiveRefCntPtr<ASTContext> Context;
/// The AST consumer.
OwningPtr<ASTConsumer> Consumer;
/// \brief The semantic analysis object.
OwningPtr<Sema> TheSema;
//… the list continues
};
|
在 clang 中,至少可以使用两种方法创建一个预处理器对象:
- 直接实例化一个
Preprocessor对象 - 使用
CompilerInstance类创建一个Preprocessor对象
让我们首先使用后一种方法。
单独使用 Preprocessor 不会有太大的帮助:您需要 FileManager 和 SourceManager 类来读取文件并跟踪源位置,实现故障诊断。FileManager 类支持文件系统查找、文件系统缓存和目录搜索。查看 FileEntry 类,它为一个源文件定义了 clang 抽象。清单 6 提供了 FileManager.h 头文件的一个摘要。
class FileManager : public llvm::RefCountedBase<FileManager> {
FileSystemOptions FileSystemOpts;
/// \brief The virtual directories that we have allocated. For each
/// virtual file (e.g. foo/bar/baz.cpp), we add all of its parent
/// directories (foo/ and foo/bar/) here.
SmallVector<DirectoryEntry*, 4> VirtualDirectoryEntries;
/// \brief The virtual files that we have allocated.
SmallVector<FileEntry*, 4> VirtualFileEntries;
/// NextFileUID - Each FileEntry we create is assigned a unique ID #.
unsigned NextFileUID;
// Statistics.
unsigned NumDirLookups, NumFileLookups;
unsigned NumDirCacheMisses, NumFileCacheMisses;
// …
// Caching.
OwningPtr<FileSystemStatCache> StatCache;
|
SourceManager 类通常用来查询 SourceLocation 对象。在 SourceManager.h 头文件中,清单 7 提供了有关 SourceLocation对象的信息。
/// There are three different types of locations in a file: a spelling /// location, an expansion location, and a presumed location. /// /// Given an example of: /// #define min(x, y) x < y ? x : y /// /// and then later on a use of min: /// #line 17 /// return min(a, b); /// /// The expansion location is the line in the source code where the macro /// was expanded (the return statement), the spelling location is the /// location in the source where the macro was originally defined, /// and the presumed location is where the line directive states that /// the line is 17, or any other line. |
很明显,SourceManager 取决于底层的 FileManager;事实上,SourceManager 类构造函数接受一个 FileManager 类作为输入参数。最后,您需要跟踪处理源代码时可能出现的错误并进行报告。您可以使用 DiagnosticsEngine 类完成这项工作。和Preprocessor 一样,您有两个选择:
- 独立创建所有必需的对象
- 使用
CompilerInstance完成所有工作
让我们使用后一种方法。清单 8 显示了 Preprocessor 的代码;其他任何事情之前已经解释过了。
using namespace clang;
int main()
{
CompilerInstance ci;
ci.createDiagnostics(0,NULL); // create DiagnosticsEngine
ci.createFileManager(); // create FileManager
ci.createSourceManager(ci.getFileManager()); // create SourceManager
ci.createPreprocessor(); // create Preprocessor
const FileEntry *pFile = ci.getFileManager().getFile("hello.c");
ci.getSourceManager().createMainFileID(pFile);
ci.getPreprocessor().EnterMainSourceFile();
ci.getDiagnosticClient().BeginSourceFile(ci.getLangOpts(), &ci.getPreprocessor());
Token tok;
do {
ci.getPreprocessor().Lex(tok);
if( ci.getDiagnostics().hasErrorOccurred())
break;
ci.getPreprocessor().DumpToken(tok);
std::cerr << std::endl;
} while ( tok.isNot(clang::tok::eof));
ci.getDiagnosticClient().EndSourceFile();
}
|
清单 8 使用 CompilerInstance 类依次创建 DiagnosticsEngine(ci.createDiagnostics 方法调用)和FileManager(ci.createFileManager 和 ci.CreateSourceManager)。使用 FileEntry 完成文件关联后,继续处理源文件中的每个令牌,直到达到文件的末尾 (EOF)。预处理器的 DumpToken 方法将把令牌转储到屏幕中。
要编译并运行 清单 8 中的代码,使用 清单 9 中的 makefile(针对您的 clang 和 LLVM 安装文件夹进行了相应调整)。主要想法是使用 llvm-config 工具提供任何必需的 LLVM(包含路径和库):您永远不应尝试将这些链接传递到 g++ 命令行。
CXX := g++
RTTIFLAG := -fno-rtti
CXXFLAGS := $(shell llvm-config --cxxflags) $(RTTIFLAG)
LLVMLDFLAGS := $(shell llvm-config --ldflags --libs)
DDD := $(shell echo $(LLVMLDFLAGS))
SOURCES = main.cpp
OBJECTS = $(SOURCES:.cpp=.o)
EXES = $(OBJECTS:.o=)
CLANGLIBS = \
-L /usr/local/lib \
-lclangFrontend \
-lclangParse \
-lclangSema \
-lclangAnalysis \
-lclangAST \
-lclangLex \
-lclangBasic \
-lclangDriver \
-lclangSerialization \
-lLLVMMC \
-lLLVMSupport \
all: $(OBJECTS) $(EXES)
%: %.o
$(CXX) -o $@ $< $(CLANGLIBS) $(LLVMLDFLAGS)
|
编译并运行以上代码后,您应当获得 清单 10 中的输出。
Assertion failed: (Target && "Compiler instance has no target!"), function getTarget, file /Users/Arpan/llvm/tools/clang/lib/Frontend/../.. /include/clang/Frontend/CompilerInstance.h, line 294. Abort trap: 6 |
在这里,您遗漏了 CompilerInstance 设置的最后一部分:即编译代码所针对的目标平台。这里是 TargetInfo 和TargetOptions 类发挥作用的地方。根据 clang 标头 TargetInfo.h,TargetInfo 类存储有关代码生成的目标系统的所需信息,并且必须在编译或预处理之前创建。和预期的一样,TargetInfo 包含有关整数和浮动宽度、对齐等信息。清单 11 提供了 TargetInfo.h 头文件的摘要。
class TargetInfo : public llvm::RefCountedBase<TargetInfo> {
llvm::Triple Triple;
protected:
bool BigEndian;
unsigned char PointerWidth, PointerAlign;
unsigned char IntWidth, IntAlign;
unsigned char HalfWidth, HalfAlign;
unsigned char FloatWidth, FloatAlign;
unsigned char DoubleWidth, DoubleAlign;
unsigned char LongDoubleWidth, LongDoubleAlign;
// …
|
TargetInfo 类使用两个参数实现初始化:DiagnosticsEngine 和 TargetOptions。在这两个参数中,对于当前平台,后者必须将 Triple 字符串设置为相应的值。LLVM 此时将发挥作用。清单 12 显示了对 清单 9 所附加的可以使预处理器工作的内容。
int main()
{
CompilerInstance ci;
ci.createDiagnostics(0,NULL);
// create TargetOptions
TargetOptions to;
to.Triple = llvm::sys::getDefaultTargetTriple();
// create TargetInfo
TargetInfo *pti = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to);
ci.setTarget(pti);
// rest of the code same as in Listing 9…
ci.createFileManager();
// …
|
就这么简单。运行代码并观察简单的 hello.c 测试的输出:
#include <stdio.h>
int main() { printf("hello world!\n"); }
|
清单 13 展示了部分预处理器输出。
typedef 'typedef'
struct 'struct'
identifier '__va_list_tag'
l_brace '{'
unsigned 'unsigned'
identifier 'gp_offset'
semi ';'
unsigned 'unsigned'
identifier 'fp_offset'
semi ';'
void 'void'
star '*'
identifier 'overflow_arg_area'
semi ';'
void 'void'
star '*'
identifier 'reg_save_area'
semi ';'
r_brace '}'
identifier '__va_list_tag'
semi ';'
identifier '__va_list_tag'
identifier '__builtin_va_list'
l_square '['
numeric_constant '1'
r_square ']'
semi ';'
|
clang 库的其中一个优点,就是您可以通过多种方法实现相同的效果。在本节中,您将创建一个 Preprocessor 对象,但是不需要直接向 CompilerInstance 发出请求。从 Preprocessor.h 头文件中,清单 14 显示了 Preprocessor 的构造函数。
Preprocessor(DiagnosticsEngine &diags, LangOptions &opts,
const TargetInfo *target,
SourceManager &SM, HeaderSearch &Headers,
ModuleLoader &TheModuleLoader,
IdentifierInfoLookup *IILookup = 0,
bool OwnsHeaderSearch = false,
bool DelayInitialization = false);
|
查看该构造函数,显然,要想让这个预处理器工作,您还需要创建 6 个不同的对象。您已经了解了DiagnosticsEngine、TargetInfo 和 SourceManager。CompilerInstance 派生自 ModuleLoader。因此您必须创建两个新的对象,一个用于 LangOptions,另一个用于 HeaderSearch。LangOptions 类使您编译一组 C/C++ 方言,包括 C99、C11 和C++0x。参考 LangOptions.h 和 LangOptions.def 标头,获取更多信息。最后,HeaderSearch 类存储目录的 std::vector,用于在其他对象中搜索功能。清单 15 显示了 Preprocessor 的代码。
using namespace clang;
int main() {
DiagnosticOptions diagnosticOptions;
TextDiagnosticPrinter *printer =
new TextDiagnosticPrinter(llvm::outs(), diagnosticOptions);
llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagIDs;
DiagnosticsEngine diagnostics(diagIDs, printer);
LangOptions langOpts;
clang::TargetOptions to;
to.Triple = llvm::sys::getDefaultTargetTriple();
TargetInfo *pti = TargetInfo::CreateTargetInfo(diagnostics, to);
FileSystemOptions fsopts;
FileManager fileManager(fsopts);
SourceManager sourceManager(diagnostics, fileManager);
HeaderSearch headerSearch(fileManager, diagnostics, langOpts, pti);
CompilerInstance ci;
Preprocessor preprocessor(diagnostics, langOpts, pti,
sourceManager, headerSearch, ci);
const FileEntry *pFile = fileManager.getFile("test.c");
sourceManager.createMainFileID(pFile);
preprocessor.EnterMainSourceFile();
printer->BeginSourceFile(langOpts, &preprocessor);
// … similar to Listing 8 here on
}
|
对于 清单 15 中的代码,需要注意以下几点:
- 您没有初始化
HeaderSearch并使它指向任何特定的目录。但是您应当这样做。 - clang API 要求在堆 (heap) 上分配
TextDiagnosticPrinter。在栈 (stack) 上分配会引起崩溃。 - 您还不能处理掉
CompilerInstance。总之是因为您正在使用CompilerInstance,那么为什么还要费心去手动创建它而不是更舒适地使用 clang API 呢?
您目前为止一直使用的是 C 测试代码:那么使用一些 C++ 代码如何?向 清单 15 中的代码添加 langOpts.CPlusPlus = 1;,然后尝试使用 清单 16 中的测试代码。
template <typename T, int n>
struct s {
T array[n];
};
int main() {
s<int, 20> var;
}
|
清单 17 展示了程序的部分输出。
identifier 'template'
less '<'
identifier 'typename'
identifier 'T'
comma ','
int 'int'
identifier 'n'
greater '>'
struct 'struct'
identifier 's'
l_brace '{'
identifier 'T'
identifier 'array'
l_square '['
identifier 'n'
r_square ']'
semi ';'
r_brace '}'
semi ';'
int 'int'
identifier 'main'
l_paren '('
r_paren ')'
|
clang/Parse/ParseAST.h 中定义的 ParseAST 方法是 clang 提供的重要方法之一。以下是从 ParseAST.h 复制的一个例程声明:
void ParseAST(Preprocessor &pp, ASTConsumer *C,
ASTContext &Ctx, bool PrintStats = false,
TranslationUnitKind TUKind = TU_Complete,
CodeCompleteConsumer *CompletionConsumer = 0);
|
ASTConsumer 为您提供了一个抽象接口,可以从该接口进行派生。这样做非常合适,因为不同的客户端很可能通过不同的方式转储或处理 AST。您的客户端代码将派生自 ASTConsumer。ASTContext 类存储有关类型声明的信息和其他信息。最简单的尝试就是使用 clang ASTConsumer API 在您的代码中输出一个全局变量列表。许多技术公司就全局变量在 C++ 代码中的使用有非常严格的要求,这应当作为创建定制 lint 工具的出发点。清单 18 中提供了定制 consumer 的代码。
class CustomASTConsumer : public ASTConsumer {
public:
CustomASTConsumer () : ASTConsumer() { }
virtual ~ CustomASTConsumer () { }
virtual bool HandleTopLevelDecl(DeclGroupRef decls)
{
clang::DeclGroupRef::iterator it;
for( it = decls.begin(); it != decls.end(); it++)
{
clang::VarDecl *vd = llvm::dyn_cast<clang::VarDecl>(*it);
if(vd)
std::cout << vd->getDeclName().getAsString() << std::endl;;
}
return true;
}
};
|
您将使用自己的版本覆盖 HandleTopLevelDecl 方法(最初在 ASTConsumer 中提供)。Clang 将全局变量列表传递给您;您对该列表进行迭代并输出变量名称。清单 19 摘录自 ASTConsumer.h,显示了客户端 consumer 代码可以覆盖的一些其他方法。
/// HandleInterestingDecl - Handle the specified interesting declaration. This
/// is called by the AST reader when deserializing things that might interest
/// the consumer. The default implementation forwards to HandleTopLevelDecl.
virtual void HandleInterestingDecl(DeclGroupRef D);
/// HandleTranslationUnit - This method is called when the ASTs for entire
/// translation unit have been parsed.
virtual void HandleTranslationUnit(ASTContext &Ctx) {}
/// HandleTagDeclDefinition - This callback is invoked each time a TagDecl
/// (e.g. struct, union, enum, class) is completed. This allows the client to
/// hack on the type, which can occur at any point in the file (because these
/// can be defined in declspecs).
virtual void HandleTagDeclDefinition(TagDecl *D) {}
/// Note that at this point it does not have a body, its body is
/// instantiated at the end of the translation unit and passed to
/// HandleTopLevelDecl.
virtual void HandleCXXImplicitFunctionInstantiation(FunctionDecl *D) {}
|
最后,清单 20 显示了您开发的定制 AST consumer 类的实际客户端代码。
清单 20. 使用定制 AST consumer 的客户端代码
int main() {
CompilerInstance ci;
ci.createDiagnostics(0,NULL);
TargetOptions to;
to.Triple = llvm::sys::getDefaultTargetTriple();
TargetInfo *tin = TargetInfo::CreateTargetInfo(ci.getDiagnostics(), to);
ci.setTarget(tin);
ci.createFileManager();
ci.createSourceManager(ci.getFileManager());
ci.createPreprocessor();
ci.createASTContext();
CustomASTConsumer *astConsumer = new CustomASTConsumer ();
ci.setASTConsumer(astConsumer);
const FileEntry *file = ci.getFileManager().getFile("hello.c");
ci.getSourceManager().createMainFileID(file);
ci.getDiagnosticClient().BeginSourceFile(
ci.getLangOpts(), &ci.getPreprocessor());
clang::ParseAST(ci.getPreprocessor(), astConsumer, ci.getASTContext());
ci.getDiagnosticClient().EndSourceFile();
return 0;
}
|
这篇两部分的系列文章涵盖了大量内容:它探讨了 LLVM IR,提供了通过手动创建和 LLVM API 生成 IR 的方法,展示了如何为 LLVM 后端创建一个定制插件,以及解释了 LLVM 前端及其丰富的标头集。您还了解了如何使用该前端进行预处理和使用 AST。在计算史上,创建一个编译器并进行扩展,特别是针对 C++ 等复杂的语言,看上去是个非常复杂的过程,但是有了 LLVM,一切都变得非常简单。文档工作是 LLVM 和 clang 需要继续加强的部分,但是在此之前,我建议尝试 VIM/doxygen 来浏览这些标头。祝您使用愉快!
学习
- 在 使用 LLVM 框架创建一个工作编译器,第 1 部分(Arpan Sen,developerWorks,2012 年 6 月)中了解 LLVM 的基础知识。使用功能强大的 LLVM 编译器基础架构优化用任何语言编写的应用程序。构建一个定制编译器现在变得非常简单!
- 了解有关 LLVM 阶段 的更多信息。
- 获取 clang 开发人员邮件列表。
- 阅读 Getting Started: Building and Running Clang,获取有关构建和安装 clang 的详细信息。
- 参加 官方 LLVM 教程,获取有关 LLVM 的出色介绍。
- 深入钻研 LLVM Programmer's Manual,这是了解 LLVM API 的必不可少的资源。
- 在 developerWorks 中国网站 Linux 技术专区,查找数百篇 how-to 文章和教程,以及下载、讨论论坛和面向 Linux 开发人员和管理员的其他丰富资源。
- developerWorks 中国网站 Web 开发专区 提供了涵盖各种基于 Web 的解决方案的文章。
- 查看 developerWorks 演示中心,包括面向初学者的产品安装和设置演示,以及为经验丰富的开发人员提供的高级功能。
- 在 Twitter 上关注 developerWorks。
- 随时关注 developerWorks 技术活动和网络广播。
- 访问 developerWorks Open source 专区获得丰富的 how-to 信息、工具和项目更新以及最受欢迎的文章和教程,帮助您用开放源码技术进行开发,并将它们与 IBM 产品结合使用。
获得产品和技术
- 访问 LLVM 项目站点 并下载 最新版本。
- 从 LLVM 站点查找有关 clang 的详细信息。
- IBM 产品评估试用版软件:下载产品试用版,在线试用产品,在云环境中使用产品,或在 IBM SOA 人员沙箱 中花几个小时了解如何高效实现面向服务的架构。
讨论
- 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。
- 加入 IBM 软件下载与技术交流群组,参与在线交流。
