编写 LLVM Pass

作者: 佩玖吟 | 来源:发表于2021-08-29 14:49 被阅读0次

介绍 —— 什么是 pass

官方文档

LLVM Pass框架是LLVM系统的重要组成部分,pass执行构成编译器的转换和优化,构建这些转换所使用的分析结果,最重要的是,它们是编译器代码的结构化技术。

所有的pass是 Pass 的子类,通过重写 Pass 中的方法来实现功能。根据你的 pass 是如何工作的,可以从 ModulePass , CallGraphSCCPass, FunctionPass , LoopPass, RegionPass 继承,这样系统就能更多地了解你的 pass 的作用,以及如何与其他 pass 相结合。LLVM Pass框架的主要特性之一是,它根据 pass 满足的约束(由它们派生的类来指示),以一种有效的方式调度 pass 运行。

快速开始 —— hello world

这里我们介绍如何书写 pass 的 "hello world",这个 pass 的作用是打印出正在编译的程序中存在的非外部函数的名称。它不会修改程序,它只是检查它,该 pass 的源代码和文件可在 LLVM 源代码树中找到 lib/Transforms/HelloHello.cpp

可以参考 llvm 进行阅读实现。

设置 build 环境

先 configure 以及 build LLVM,然后在 LLVM 源代码中的某个位置创建一个新的目录,在该例子中,假设建立了 lib/Transforms/Hello,然后要设置脚本文件编译 pass。可以将下面代码复制到 Hello 目录下的 CMakeLists.txt

add_llvm_library( LLVMHello MODULE
  Hello.cpp

  PLUGIN_TOOL
  opt
  )

add_llvm_library() 是 LLVM 的内部 cmake 宏函数,它接受不同的参数来指示将构建指定源文件的库类型,包括 SHARED, BUILDTREE_ONLY, MODULE, 和 INSTALL_WITH_TOOLCHAIN

lib/Transforms/CMakeLists.txt 里面加入:

add_subdirectory(Hello)

(请注意,已经有一个名为 Hello 的目录,其中包含一个示例 "Hello" pass)

这个 build 脚本指定工作目录中的 Hello.cpp 文件将被编译并链接到一个共享对象 $(LEVEL)/lib/LLVMHello.so,这个文件可以通过 opt 工具的 -load 选项被动态加载。如果您的操作系统使用了其他后缀,不是 .so (如 Windows 或 macOS) ,将使用适当的扩展。

现在我们已经建立了 build 脚本,我们只需要为 pass 本身编写代码。

所需的基本代码

因为我们在写 Pass,我们需要操作 Functions,并且我们需要输出一些东西,所以首先添加头文件:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

然后使用命名空间:

using namespace llvm;

然后使用:

namespace {

开始是一个匿名命名空间。匿名命名空间对于 C++ 就像 static 关键字对于 C 一样(在全局范围内)。它使匿名命名空间内声明的内容只对当前文件可见。如果你不熟悉它们,可以查阅一本像样的 C++ 书籍以获得更多信息。

然后声明我们要书写的 pass:

struct Hello : public FunctionPass {

Hello 是 FunctionPass 的子类,可以在 classes 中查看不同 pass 子类的区别。现在我们只需要知道 FunctionPass 一次操作一个函数。

static char ID;
Hello() : FunctionPass(ID) {}

这声明了 LLVM 用于标识 pass 的 pass 标识符。这使得 LLVM 避免使用昂贵的 C++ 运行时信息。

    bool runOnFunction(Function &F) override {
        errs() << "Hello: ";
        errs().write_escaped(F.getName()) << '\n';
        return false;
    }
}; // end of struct Hello
}  // end of anonymous namespace

我们声明了 runOnFunction 方法,它重写了从 FunctionPass 继承的方法,这就是我们要做的事情,所以我们只需要打印出带有每个函数名的消息。

char Hello::ID = 0;

我们在这里初始化 pass ID,LLVM 使用 ID 地址来识别 pass,所以初始值并不重要。

static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

最后,我们注册 Hello 类,给它一个命令行参数 "hello",以及名字 "Hello World Pass",最后两个参数描述了它的行为:如果一个 pass 在没有修改的情况下遍历 CFG,那么第三个参数设置为 true; 如果 pass 是 analysis pass,例如 dominator tree pass,那么第四个参数设置为 true。

如果我们希望将 pass 注册为现有管道的一个步骤,则会提供一些扩展点,如:PassManagerBuilder::EP_EarlyAsPossible 在任何优化之前应用我们的 pass,或者 PassManagerBuilder::EP_FullLinkTimeOptimizationLast 在链接时间优化之后应用它。

static llvm::RegisterStandardPasses Y(
    llvm::PassManagerBuilder::EP_EarlyAsPossible,
    [](const llvm::PassManagerBuilder &Builder,
       llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });

最后, .cpp 文件为:

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"

using namespace llvm;

namespace {
struct Hello : public FunctionPass {
  static char ID;
  Hello() : FunctionPass(ID) {}

  bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << '\n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

static RegisterStandardPasses Y(
    PassManagerBuilder::EP_EarlyAsPossible,
    [](const PassManagerBuilder &Builder,
       legacy::PassManagerBase &PM) { PM.add(new Hello()); });

现在一切都完成了,使用简单的 gmake 命令从构建目录的顶层编译该文件,您应该得到一个新文件 lib/LLVMHello.so。请注意,这个文件中的所有内容都包含在一个匿名命名空间中ーー这反映了这样一个事实,即 pass 是自包含的单元,不需要外部接口(尽管它们可以有外部接口)才能发挥作用。

使用 opt 运行 pass

按照 LLVM 系统入门 末尾的示例将 "Hello World" 编译为 LLVM。 我们现在可以通过像这样的转换来运行程序的位码文件(hello.bc)(当然,任何位码文件都可以工作):

$ opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main

-load 选项指定 opt 应该将您的 pass 作为共享对象加载,这使得 -hello 成为有效的命令行参数(这是您需要注册 pass 的原因之一)。因为 Hello pass 不会以任何方式修改程序,所以我们只是丢弃 opt 的结果(将其发送到/dev/null)。

要查看您注册的其他字符串发生了什么,请尝试使用 -help 选项运行 opt:

$ opt -load lib/LLVMHello.so -help
OVERVIEW: llvm .bc -> .bc modular optimizer and analysis printer

USAGE: opt [subcommand] [options] <input bitcode file>

OPTIONS:
  Optimizations available:
...
    -guard-widening           - Widen guards
    -gvn                      - Global Value Numbering
    -gvn-hoist                - Early GVN Hoisting of Expressions
    -hello                    - Hello World Pass
    -indvars                  - Induction Variable Simplification
    -inferattrs               - Infer set function attributes
...

pass 名称被添加为 pass 的信息字符串,为 opt 的用户提供了一些文档。一旦你让它全部工作和测试,它可以用于找出你的 pass 有多快。PassManager 提供了一个很好的命令行选项(-time-passes),它允许你获取关于你的 pass 以及你排队等待的其他 pass 的执行时间的信息。例如:

$ opt -load lib/LLVMHello.so -hello -time-passes < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main
===-------------------------------------------------------------------------===
                      ... Pass execution timing report ...
===-------------------------------------------------------------------------===
  Total Execution Time: 0.0007 seconds (0.0005 wall clock)

   ---User Time---   --User+System--   ---Wall Time---  --- Name ---
   0.0004 ( 55.3%)   0.0004 ( 55.3%)   0.0004 ( 75.7%)  Bitcode Writer
   0.0003 ( 44.7%)   0.0003 ( 44.7%)   0.0001 ( 13.6%)  Hello World Pass
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0001 ( 10.7%)  Module Verifier
   0.0007 (100.0%)   0.0007 (100.0%)   0.0005 (100.0%)  Total

如您所见,我们上面的实现非常快。列出的其他 pass 会自动被 opt 工具插入,以验证你的 pass 发出的 LLVM 是否仍然有效且格式良好,且未以某种方式被破坏。

相关文章

网友评论

    本文标题:编写 LLVM Pass

    本文链接:https://www.haomeiwen.com/subject/kqowiltx.html