编写 LLVM Pass

导言 — 什么是 Pass?

警告

本文档介绍新的 Pass 管理器。LLVM 在代码生成流水线中使用旧的 Pass 管理器。有关更多详细信息,请参阅 编写 LLVM Pass(旧版 PM 版本)使用新的 Pass 管理器

LLVM Pass 框架是 LLVM 系统的重要组成部分,因为 LLVM Pass 是编译器中最有趣的部分存在的地方。Pass 执行构成编译器的转换和优化,它们构建这些转换所使用的分析结果,并且最重要的是,它们是编译器代码的结构化技术。

与旧 Pass 管理器下的 Pass(其中 Pass 接口通过继承定义)不同,新 Pass 管理器下的 Pass 依赖于基于概念的多态性,这意味着没有显式的接口(有关更多详细信息,请参阅 PassManager.h 中的注释)。所有 LLVM Pass 都继承自 CRTP mix-in PassInfoMixin<PassT>。Pass 应具有一个 run() 方法,该方法返回 PreservedAnalyses 并接受一些 IR 单元以及一个分析管理器。例如,函数 Pass 将具有一个 PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM); 方法。

我们首先向您展示如何构建一个 Pass,从设置构建、创建 Pass,到执行和测试它。查看现有的 Pass 始终是学习细节的好方法。

快速开始 — 编写 hello world

在这里,我们描述如何编写 Pass 的 “hello world”。“HelloWorld” Pass 旨在简单地打印出正在编译的程序中存在的非外部函数的名称。它根本不修改程序,只是检查它。

以下代码已存在;您可以随意创建一个具有不同名称的 Pass,与 HelloWorld 源文件放在一起。

设置构建

首先,按照 LLVM 系统入门 中的描述配置和构建 LLVM。

接下来,我们将重用一个现有的目录(创建一个新目录涉及处理比我们想要的更多的 CMake 文件)。在此示例中,我们将使用 llvm/lib/Transforms/Utils/HelloWorld.cpp,它已经创建。如果您想创建自己的 Pass,请在 llvm/lib/Transforms/Utils/CMakeLists.txt 中添加一个新的源文件(假设您希望您的 Pass 在 Transforms/Utils 目录中)。

现在我们已经为新的 Pass 设置了构建,我们需要编写 Pass 本身的代码。

所需的基本代码

现在已经为新的 Pass 设置了构建,我们只需要编写它。

首先,我们需要在头文件中定义 Pass。我们将创建 llvm/include/llvm/Transforms/Utils/HelloWorld.h。该文件应包含以下样板代码

#ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
#define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

#include "llvm/IR/PassManager.h"

namespace llvm {

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};

} // namespace llvm

#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

这将为 Pass 创建类,并声明 run() 方法,该方法实际运行 Pass。从 PassInfoMixin<PassT> 继承设置了更多的样板代码,这样我们就不必自己编写它。

我们的类在 llvm 命名空间中,这样我们就不会污染全局命名空间。

接下来,我们将创建 llvm/lib/Transforms/Utils/HelloWorld.cpp,从

#include "llvm/Transforms/Utils/HelloWorld.h"

… 包含我们刚刚创建的头文件开始。

using namespace llvm;

… 是必需的,因为 include 文件中的函数位于 llvm 命名空间中。这应该只在非头文件中完成。

接下来,我们有 Pass 的 run() 定义

PreservedAnalyses HelloWorldPass::run(Function &F,
                                      FunctionAnalysisManager &AM) {
  errs() << F.getName() << "\n";
  return PreservedAnalyses::all();
}

… 它只是将函数的名称打印到 stderr。Pass 管理器将确保 Pass 将在模块中的每个函数上运行。PreservedAnalyses 返回值表示在此 Pass 之后,所有分析(例如支配树)仍然有效,因为我们没有修改任何函数。

Pass 本身就是这样。现在为了“注册” Pass,我们需要将其添加到几个地方。将以下内容添加到 llvm/lib/Passes/PassRegistry.defFUNCTION_PASS 部分

FUNCTION_PASS("helloworld", HelloWorldPass())

… 它以名称 “helloworld” 添加 Pass。

llvm/lib/Passes/PassRegistry.def 被 #include 多次到 llvm/lib/Passes/PassBuilder.cpp 中,原因各不相同。由于它构建了我们的 Pass,我们还需要在 llvm/lib/Passes/PassBuilder.cpp 中添加正确的 #include

#include "llvm/Transforms/Utils/HelloWorld.h"

这应该是我们的 Pass 所需的所有代码,现在是编译和运行它的时间了。

使用 opt 运行 Pass

现在您有了一个全新的闪亮的 Pass,我们可以构建 opt 并使用它来通过 Pass 运行一些 LLVM IR。

$ ninja -C build/ opt
# or whatever build system/build directory you are using

$ cat /tmp/a.ll
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

define void @bar() {
  ret void
}

$ build/bin/opt -disable-output /tmp/a.ll -passes=helloworld
foo
bar

我们的 Pass 运行并按预期打印了函数的名称!

测试 Pass

测试我们的 Pass 对于防止未来回归非常重要。我们将在 llvm/test/Transforms/Utils/helloworld.ll 中添加一个 lit 测试。有关测试的更多信息,请参阅 LLVM 测试基础设施指南

$ cat llvm/test/Transforms/Utils/helloworld.ll
; RUN: opt -disable-output -passes=helloworld %s 2>&1 | FileCheck %s

; CHECK: {{^}}foo{{$}}
define i32 @foo() {
  %a = add i32 2, 3
  ret i32 %a
}

; CHECK-NEXT: {{^}}bar{{$}}
define void @bar() {
  ret void
}

$ ninja -C build check-llvm
# runs our new test alongside all other llvm lit tests

常见问题解答

必需的 Pass

定义静态 isRequired() 方法并返回 true 的 Pass 是必需的 Pass。例如

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
  PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);

  static bool isRequired() { return true; }
};

必需的 Pass 是可能不会被跳过的 Pass。必需的 Pass 的一个示例是 AlwaysInlinerPass,它必须始终运行以保留 alwaysinline 语义。Pass 管理器是必需的,因为它们可能包含其他必需的 Pass。

Pass 如何被跳过的一个示例是 optnone 函数属性,它指定不应在函数上运行优化。必需的 Pass 仍将在 optnone 函数上运行。

有关更多实现细节,请参阅 PassInstrumentation::runBeforePass()

将 Pass 注册为插件

LLVM 提供了一种机制,用于在各种工具(如 clangopt)中注册 Pass 插件。Pass 插件可以将 Pass 添加到默认优化流水线,或者通过 opt 等工具手动运行。有关更多信息,请参阅 使用新的 Pass 管理器

在仓库的根目录中与其他项目一起创建一个 CMake 项目。此项目必须包含以下最少的 CMakeLists.txt

add_llvm_pass_plugin(MyPassName source.cpp)

有关更多 CMake 详细信息,请参阅 add_llvm_pass_plugin 的定义。

Pass 必须为新的 Pass 管理器提供至少两个入口点之一,一个用于静态注册,另一个用于动态加载的插件

  • llvm::PassPluginLibraryInfo get##Name##PluginInfo();

  • extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() LLVM_ATTRIBUTE_WEAK;

默认情况下,Pass 插件是动态编译和链接的。将 LLVM_${NAME}_LINK_INTO_TOOLS 设置为 ON 会将项目转换为静态链接的扩展。

有关树内示例,请参阅 llvm/examples/Bye/

要使 PassBuilder 了解静态链接的 Pass 插件

// Declare plugin extension function declarations.
#define HANDLE_EXTENSION(Ext) llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"

...

// Register plugin extensions in PassBuilder.
#define HANDLE_EXTENSION(Ext) get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB);
#include "llvm/Support/Extension.def"

要使 PassBuilder 了解动态链接的 Pass 插件

// Load plugin dynamically.
auto Plugin = PassPlugin::Load(PathToPlugin);
if (!Plugin)
  report_error();
// Register plugin extensions in PassBuilder.
Plugin.registerPassBuilderCallbacks(PB);