自己动手写LLVM Pass 一

PassManager系统是LLVM中最重要的基础架构之一。

经过大约10多年的努力,开发人员决定给她一个新的面目。

新的PassManager Pass背后的基本概念仍然与旧版本相同:通过LLVM IR单元运行Pass,例如,功能来检查内部的IR或甚至修改它,然后将结果IR传递给下一个Pass管道。最大的区别是我们写Pass的方式。

新Pass和新PassManager背后有几种设计理念。我强烈建议读者查看过去LLVM开发者大会的相关讲座。但我不打算详细介绍。本文仅通过简单的HelloNewPM Pass 作为新PassManager系统的预告。让我们亲自动手吧!

通常,编写(遗留)LLVM Pass的教程将首先告诉您编写一个继承其中一个llvm::Pass族的类, 例如llvm::FunctionPass , 然后实现一些必要的方法,比如bool FunctionPass::runOnFunction(Function &F)

在这里你第一步将要做的也是类似的事情:

struct HelloNewPMPass : public PassInfoMixin<HelloNewPMPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) {
return PreservedAnalyses::all();
}
};

我们任然需要继承父类,但是这次PassInfoMixin类中不包含任何供我们覆盖的虚函数。提供默认实现HelloNewPMPass::name()是它在这里的唯一原因。我们只使用参数类型的run方法,即FunctionFunctionAnalysisManager,告诉我们应用的是哪个IR单元。

Run方法,正如它的名字表现的,担任在过去的的FunctionPass::runOnFunctionModulePass::runOnModule等方法相同的功能,但它不再是虚拟方法,所以这里没有关键字 override。此外,返回类型不同,还有一个额外的FunctionAnalysisManager参数。事实证明,它们都与分析框架相关,前一个用于分析数据失效,后一个用于检索分析结果,这与getAnalysis<…>()传统通过中的方法类似。我们将分析主题留给第二部分。目前,由于我们不打算修改任何IR,我们只需返回PreservedAnalyses::all()告诉框架,运行此Pass后所有分析结果都是一致的。

然后我们添加一些丢失的片段并将一些代码填充到run函数体中….

#include "llvm/IR/PassManager.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

namespace {
struct HelloNewPMPass : public PassInfoMixin<HelloNewPMPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) {
if(F.hasName())
errs() << "Hello " << F.getName() << "\n";
return PreservedAnalyses::all();
}
};
} // end anonymous namespace

你猜怎么着?这就是构建新Pass所需要做的一切!我们只需要注册它。这是骨架代码:

extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "HelloNewPMPass", "v0.1",
[](PassBuilder &PB) {...}
};
}

这与传统Passes中的Pass注册有很大不同的是,我们只是从 llvmGetPassPluginInfo函数返回Pass实体,而不是使用 RegisterPass<...>实例和一些静态Pass注册魔术,这里是return语句之后的花括号将构造一个llvm::PassPluginInfo对象,携带一些传递信息。像HelloNewPMPass是Pass名称,v0.1是Pass版本。最后一个字段是lambda函数,它提供了一个PassBuilder实例。PassBuilder正如其名称所示,用于构建PassManager管道。因此我们将使用它将我们的Pass“插入”管道内的适当位置。

在我们继续之前,让我们看看如何使用该opt工具运行新的PassManager Passes。新的PassManager使用普通字符串来描述Pass管道的外观,而不是使用命令行选项来判断您将要运行哪些传递。例如:

opt -passes =“sroa,instcombine”foo.ll

它将首先运行SROA,然后运行指令组合器。当然还有一堆其他复杂的语法,但在这里我们只需要知道Pass管道可以通过削减这种文本描述来构造。

所以现在我们的想法是,如果我们可以“拦截”上面的解析过程,那么我们可以在出现某个Pass名称时插入我们的Pass。我们是这样做的:

extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "HelloNewPMPass", "v0.1",
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef PassName, FunctionPassManager &FPM, ...) {
if(PassName == "hello-new-pm-pass"){
FPM.addPass(HelloNewPMPass());
return true;
}
return false;
}
);
}
};
}

(上面省略了一些不相关的代码)

registerPipelineParsingCallback那里,您可以注册从文本描述中解析通行证名称时调用的回调。在这里,当管道字符串中遇到的传递名称是hello-new-pm-pass时,我们将传递添加到管道。因此,我们可以使用opt类似于下面的命令运行我们的传递:

opt -passes ="hello-new-pm-pass"......

最后,这是我们的完整代码:

#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

namespace {
struct HelloNewPMPass : public PassInfoMixin<HelloNewPMPass> {
PreservedAnalyses run(Function &F,
FunctionAnalysisManager &FAM) {
if(F.hasName())
errs() << "Hello " << F.getName() << "\n";
return PreservedAnalyses::all();
}
};
} // end anonymous namespace

extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "HelloNewPMPass", "v0.1",
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if(Name == "hello-new-pm-pass"){
FPM.addPass(HelloNewPMPass());
return true;
}
return false;
}
);
}
};
}

要构建此Pass,请使用与构建可加载旧Pass的完全相同的方式。如果您不确定,请参阅官方教程。然后使用以下命令运行pass:

opt -disable-output \
-load-pass-plugin=/path/to/libHelloNewPMPass.so
-passes="hello-new-pm-pass" foo.ll

在我看来,PassBuilder在新的PassManager系统扮演者核心角色,还有很多我没有涉及的主题。例如,分析管道,管道扩展点和文本管道表示的有趣语法。与往常一样,在进一步发布文档之前,深入了解源代码树始终是了解有关新PassManager的更多信息的最佳方式。