【LLVM】如何写一个pass

1.简介

LLVM pass是编译器中很重要的部分,能够对代码进行转化和优化。所有pass都是Pass类的子类,通过覆盖Pass类的虚函数来实现功能,可继承的类有ModulePass , CallGraphSCCPass, FunctionPass , LoopPass, RegionPass, BasicBlockPass。详见https://llvm.org/doxygen/classllvm_1_1Pass.html

环境搭建参考:https://blog.csdn.net/l2563898960/article/details/82871826

本文参考:https://llvm.org/docs/WritingAnLLVMPass.html

2.写hello world pass

Hello pass用于打印出内部函数的函数名,不会修改程序,只是监视作用,Hello pass的源码和文件在LLVM源码的lib/Transforms/Hello目录下。

(1)设置

首先,配置和安装LLVM;然后在LLVM源码目录下创建一个新目录,这里假设你创建了lib/Transforms/Hello目录;最后,设置build脚本,用于编译新的pass。将以下代码拷贝到lib/Transforms/Hello/CMakeLists.txt

add_llvm_library( LLVMHello MODULE
  Hello.cp

  PLUGIN_TOOL
  opt
  )

将以下行加入到lib/Transforms/CMakeLists.txt

add_subdirectory(Hello)

这个build脚本表示当前目录的Hello.cpp文件将被编译和链接成共享对象$(LEVEL)/lib/LLVMHello.so,能被opt工具通过-load选项动态加载。

(2)写Helllo.cpp

首先需要添加头文件,因为在写 Pass,在函数 Function上操作,且需要打印数据。

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

接下来,include文件中的函数落在llvm命名空间。

using namespace llvm;

接下来,开始一段匿名空间,匿名空间在c++中用到,c中采用static实现,定义在匿名空间中的变量只能在当前文件可见。

namespace{

接下来定义pass,定义Hello类,从FunctionPass类继承过来,每次处理一个函数。

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

声明 runOnFunction 方法,重写从 FunctionPass继承来的同名虚函数。runOnFunction()就是以函数为单位进行处理,LLVM会以一次一个function为单位,喂进来给你处理,接下来就是将喂进来的function的名字打印出来。

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

初始化pass的ID,LLVM使用ID地址来识别pass,所以所用初始值不重要。

char Hello::ID = 0;

最后,注册Hello类,给一个命令行参数"hello"和一个名字"Hello World Pass",最后两个参数描述它的行为:如果pass需要修改CFG则第3个参数设为true,若pass是一个analysis pass,例如dominator tree pass,则第4个参数设为true。

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

如果要将本pass注册为现有流水线中的一步,则还需要一些扩展。eg,加到优化步骤之前则用PassManagerBuilder::EP_EarlyAsPossible;加到优化之后则用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 llvm::RegisterStandardPasses Y(
    llvm::PassManagerBuilder::EP_EarlyAsPossible,
    [](const llvm::PassManagerBuilder &Builder,
        llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });

在顶层LLVM根目录下创建build目录,在build目录下先"cmake ../",再"make"即可生成lib/LLVMHello.so文件。

(3)使用opt运行pass

生成共享目标文件后,由于已经通过RegisterPass注册过了,所以可以使用opt命令通过你的pass运行LLVM程序。首先参考Getting Started with the LLVM System编译"Hello World"为bitcode文件(hello.bc),通过以下命令运行hello.bc("-load"表示加载pass库):

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

3.Pass类和需求

设计一个pass首先要考虑需要继承哪一个类,Hello World例子使用了FunctionPass类来实现,接下来看看其它可用的类。

(1)ImmutablePass

这个类比较无聊,这个类用在不用运行、不会改变状态、不需要更新的pass,在转化和分析中不常用到,但能提供当前编译器配置信息。尽管这个类不常用到,但是能提供当前被编译的目标机的信息,以及影响转化的静态信息。

(2)ModulePass

最常用的一个类,使用该类表示将整个程序当做一个单元,可以随意引用函数主体,添加和移除函数。由于不知道ModulePass子类的行为,不能作优化。

ModulePass可以使用函数级passes(如dominators,函数级pass使用getAnalysis接口,也即getAnalysis<DominatorTree>(llvm::Function *)来提供获取分析结果的一个函数)。写ModulePass时需要重写runOnModule函数,该函数完成pass的主要工作,如果原IR发生修改则返回True,如果只是分析,则返回False。

(3)CallGraphSCCPass

在调用图上从后往前遍历程序。CallGraphSCCPass可以帮助构建和遍历调用图。CallGraphSCCPass子类需满足的要求:只能监控和修改当前SCC、SCC的直接调用者和直接被调用者,不能监控和修改其他Function;需要保存当前CallGraph对象,显示对代码做了哪些修改;不能从当前Module添加/去除SCC,但可以增减全局变量;调用runOnSCC之间能保持状态(包括全局变量)。

需重写以下函数:

  • doInitialization(CallGraph &):能做CallGraphSCCPass不允许的事情,如增减函数、获取指向函数的指针。
  • runOnSCC:完成pass的主要工作。
  • doFinalization(CallGraph &):很少使用,在pass调用完runOnSCC之后才调用它。

(4) FunctionPass

FunctionPass处理程序中每个函数,并不依赖其他函数的结果,FunctionPass不需要它们按特定顺序执行,不会修改外部函数。

FunctionPass子类的要求:只能监控和修改当前被处理的Function;不能从当前Module增减Function、全局变量;调用runOnFunction之间不能保持状态(包括全局变量)。

需重写以下函数:

  • doInitialization(Module &):能做FunctionPass不允许的事情,如增减函数、获取指向函数的指针。使用示例见LowerAllocations pass,该pass将mallocfree指令转化为malloc()free()函数调用,它使用doInitialization函数对mallocfree函数的索引,如果有需要,则往module添加prototypes。
  • runOnFunction函数:完成pass的主要工作,转化或分析。
  • doFinalization(Module &)函数:很少使用,在pass调用完runOnFunction之后才调用它。

(5) LoopPass

LoopPass遍历处理函数中的loop,并不依赖函数中其他loop。遍历时遇到嵌套循环,最外面的循环最后处理。LoopPass的子类可以使用LPPassManager接口来更新loop。

如果把LoopPass的子类嵌入到main loop pass流水线中去运行,则需要保存其他loop pass需要用到的function分析。LoopUtils.h提供了getLoopAnalysisUsage函数,可以在子类的getAnalysisUsage重载中调用。

需重写以下函数:

  • doInitialization(Loop *, LPPassManager &):可以使用LPPassManager接口来访问FunctionModule级分析信息。
  • runOnLoop:完成pass的主要工作,即代码转换或分析。
  • doFinalization()

(6)RegionPass

LoopPass类似,但只处理函数中单入口单退出的区域。

需重写的函数:

  • doInitialization(Region *, RGPassManager &):可以使用LPPassManager接口来访问FunctionModule级分析信息。
  • runOnRegion
  • doFinalization()

(7)MachineFunctionPass类

是LLVM code generator的一部分,依赖机器类型。Code generator passes采用TargetMachine::addPassesToEmitFile来注册和初始化,所以不能通过optbugpoint命令来运行。MachineFunctionPass也属于FunctionPass,所以和FunctionPass的限制一样,此外MachineFunctionPass还不能修改和创建 LLVM IR InstructionBasicBlockArgumentFunctionGlobalVariableGlobalAliasModule,只能修改当前正被处理的MachineFunction,调用runOnMachineFunction不能保持状态。

需重写的函数:

  • runOnMachineFunction(MachineFunction &MF)MachineFunctionPass的主入口点,完成pass主要工作。每遇到Module中的一个MachineFunction都调用runOnMachineFunction函数。

(8)pass注册

前面Hello World pass已经展示了如何注册,采用RegisterPass调用。

待实现的函数:

  • print函数:virtual void print(llvm::raw_ostream &O, const Module *M) const;

    用于打印分析结果,对于调试和展示该分析如何工作很有用,使用opt的-analyze参数来调用此函数。llvm::raw_ostream参数确定了打印结果采用什么流,Module参数是指向程序的顶层模块的指针,当从调试器调用Pass::dump()时该指针可为NULL

  • pass之间的联系:PassManager负责pass之间的联系和依赖(决定执行顺序),每个pass可以声明必须在它之前执行的pass。如果某pass未实现getAnalysisUsage函数,则默认没有要预先执行的pass。

  • getAnalysisUsagevirtual void getAnalysisUsage(AnalysisUsage &Info) const;

    实现时需用requiredinvalidated pass填充AnalysisUsage对象,通过以下函数来实现:

    • AnalysisUsage::addRequired<> & AnalysisUsage::addRequiredTransitive<>:定义必须在本pass之前执行的pass,如DominatorSetBreakCriticalEdges。有些分析需要和其他分析一起才能正常工作,如AliasAnalysis <AliasAnalysis>实现需要链接到其他分析pass,这时候要用到addRequiredTransitive函数,而非addRequired函数(表示只要the requiring pass在运行,过渡的required pass也要保持运行)。
    • AnalysisUsage::addPreserved<>setPreservesAll表示pass不会修改LLVM程序;setPreservesCFG表示会修改指令但不会修改CFG或终止指令;addPreserved适用于代码转换类的pass,如BreakCriticalEdges

    getAnalysisUsage实现示例:

      // This example modifies the program, but does not modify the CFG
      void LICM::getAnalysisUsage(AnalysisUsage &AU) const {
        AU.setPreservesCFG();
        AU.addRequired<LoopInfoWrapperPass>();
      }
    
  • getAnalysis<>getAnalysisIfAvailable<>Pass::getAnalysis<>自动从你实现的类中继承,它使你能访问之前用getAnalysisUsage声明的pass,只需要1个参数来指定需要哪个pass类,然后返回对该pass的引用。

    例如:如果你试图获取一个没在getAnalysisUsage中声明的分析,则会报错。可在你的run*函数中调用它。

      bool LICM::runOnFunction(Function &F) {
        LoopInfo &LI = getAnalysis<LoopInfoWrapperPass>().getLoopInfo();
        //...
      }
    

    还有个优点,该接口使你能在module级pass中使用函数级分析,例如:pass manager会在返回索引之前调用DominatorTree

      bool ModuleLevelPass::runOnModule(Module &M) {
        //...
        DominatorTree &DT = getAnalysis<DominatorTree>(Func);
        //...
      }
    

    如果你的pass可以更新分析,可以使用getAnalysisIfAvailable方法,如果分析是active的则返回一个指针。

      if (DominatorSet *DS = getAnalysisIfAvailable<DominatorSet>()) {
        // A DominatorSet is active.  This code will update it.
      }
    

(9)实现Analysis Groups

说明:有些复杂的分析,如流敏感、上下文敏感的过程间分析需要很多分析配合一起实现。如果其他pass想使用Analysis groups,也需要调用AnalysisUsage::addRequired()Pass::getAnalysis()来获取,但使用PassManager更简单。Analysis group也需要使用RegisterAnalysisGroup来注册,并调用INITIALIZE_AG_PASS宏来整合。示例可以看看AliasAnalysis

  • RegisterAnalysisGroupRegisterAnalysisGroup用于注册Analysis group,INITIALIZE_AG_PASS宏用于将pass实现添加到analysis group。

    static RegisterAnalysisGroup<AliasAnalysis> A("Alias Analysis");

    注册完分析之后,pass可以利用以下代码来声明有效的接口实现:表示FancyAA类使用INITIALIZE_AG_PASS宏来注册和整合到AliasAnalysis analysis group中。

      namespace {
        // Declare that we implement the AliasAnalysis interface
        INITIALIZE_AG_PASS(FancyAA, AliasAnalysis , "somefancyaa",
            "A more complex alias analysis implementation",
            false,  // Is CFG Only?
            true,   // Is Analysis?
            false); // Is default Analysis Group implementation?
      }
    

    以下代码展示如何指定默认的实现(由INITIALIZE_AG_PASS的最后一个参数确定),一个Analysis Group必须有一个默认实现(该默认实现可以从ImmutablePass继承),例如BasicAliasAnalysis就是该接口的默认实现。

(10)Pass Statistic

介绍:命令行加上-stats选项后,就会在运行的结尾打印数据,详情可参考Programmer's Manual中的Statistics section <Statistic>

PassManager功能PassManagerclass可以保证很多pass以正确的顺序、高效的运行。

  • 1.共享分析结果:避免重复分析。PassManager可以追踪所有分析结果的生存周期,展示哪些分析已经已经完成并且可用。
  • 2.pass流水化执行:以高效的使用cache和内存。具体来说,给定连续的FunctionPass,它将先对第1个函数执行所有的FunctionPass,再对第2个函数执行所有的FunctionPass,以此类推。一次只需计算1个DominatorSet,使cache更高效,也有利于进一步优化。

PassManager的有效性取决于开发者提供的信息,也即pass的行为。例如,如果开发者不实现getAnalysisUsage方法,则该pass无法被其他pass使用。

PassManager提供--debug-pass命令行选项,以对pass执行过程进行调试。如果想知道--debug-pass选项的变体,可使用opt -help-hidden命令查看。可以使用-debug-pass=Structure选项来查看指定pass如何与其他pass进行交互的,例如:

# Hello World pass之后运行gvn pass、licm pass
$ opt -load lib/LLVMHello.so -gvn -licm --debug-pass=Structure < hello.bc > /dev/null
  ModulePass Manager
    FunctionPass Manager
      Dominator Tree Construction
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Memory Dependence Analysis
      Global Value Numbering
      Natural Loop Information
      Canonicalize natural loops
      Loop-Closed SSA Form Pass
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Scalar Evolution Analysis
      Loop Pass Manager
        Loop Invariant Code Motion
      Module Verifier
    Bitcode Writer

以上输出结果显示了pass创建时间。GVN pass使用了dominator tree信息,LICM pass使用了loop信息(loop信息也使用了dominator tree)。LICM pass运行完后,自动运行module verifier,它会使用dominator tree来检查生成的LLVM code是否正确。注意:dominator tree只计算了一次,被3个pass共享。

# Hello World pass在gvn pass、licm pass中间运行
$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
  ModulePass Manager
    FunctionPass Manager
      Dominator Tree Construction
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Memory Dependence Analysis
      Global Value Numbering
      Hello World Pass
      Dominator Tree Construction
      Natural Loop Information
      Canonicalize natural loops
      Loop-Closed SSA Form Pass
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Scalar Evolution Analysis
      Loop Pass Manager
        Loop Invariant Code Motion
      Module Verifier
    Bitcode Writer
  Hello: __main
  Hello: puts
  Hello: main

以上输出结果显示Hello World pass 终止了Dominator Tree pass,接下来的pass会再次调用Dominator Tree pass获取dominator信息,为了修复这个问题,需实现getAnalysisUsage方法:

  // We don't modify the program, so we preserve all analyses
  void getAnalysisUsage(AnalysisUsage &AU) const override {
    AU.setPreservesAll();
  }
# 修复成功,dominator信息不会被计算两次
$ opt -load lib/LLVMHello.so -gvn -hello -licm --debug-pass=Structure < hello.bc > /dev/null
  Pass Arguments:  -gvn -hello -licm
  ModulePass Manager
    FunctionPass Manager
      Dominator Tree Construction
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Memory Dependence Analysis
      Global Value Numbering
      Hello World Pass
      Natural Loop Information
      Canonicalize natural loops
      Loop-Closed SSA Form Pass
      Basic Alias Analysis (stateless AA impl)
      Function Alias Analysis Results
      Scalar Evolution Analysis
      Loop Pass Manager
        Loop Invariant Code Motion
      Module Verifier
    Bitcode Writer
  Hello: __main
  Hello: puts
  Hello: main

releaseMemory方法virtual void releaseMemory();——PassManager自动确定何时开始运行分析、分析结果保存多久,如果不再用到了,需要用releaseMemory释放内存。

(11)构建pass 插件

LLVM提供了一种机制,可以在clang、opt、bugpoint内自动注册pass插件。首先要创建一个独立的project,添加到tools/,或者使用MonoRepo布局,跟其他project并列。该project的CMakeLists.txt如下所示:

add_llvm_pass_plugin(Name source0.cpp)

该pass需为新的pass manager提供两个入口点,一个用来静态注册,一个用来动态加载插件:

  • llvm::PassPluginLibraryInfo get##Name##PluginInfo();
  • extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() LLVM_ATTRIBUTE_WEAK;

pass插件默认是动态编译和链接的,可以修改LLVM_${NAME}_LINK_INTO_TOOLS变量来修改,ON表示将该project静态链接。可使用以下代码来加载静态链接的pass插件:

// fetch the declaration
#define HANDLE_EXTENSION(Ext) llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"

[...]

// use them, PB is an llvm::PassBuilder instance
#define HANDLE_EXTENSION(Ext) get##Ext##PluginInfo().RegisterPassBuilderCallbacks(PB);
#include "llvm/Support/Extension.def"

(12)注册可动态加载的pass

pass注册的主要机制是MachinePassRegistry类和MachinePassRegistryNode的子类。MachinePassRegistry实例用于保存MachinePassRegistryNode对象组成的list,并且通过和命令行接口进行通信,对MachinePassRegistryNode对象进行增减。

MachinePassRegistryNode子类用于保存特定pass提供的信息,信息包含command line name, the command help string和用于创建pass实例的函数地址。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268