Clang插桩

Clnag插件编写教程

Posted by Ted on June 28, 2020

0、Clang插桩原理

Clang-LLVM编译过程如下:

  • 预处理(Pre-process):他的主要工作就是将宏替换,删除注释展开头文件,生成.i文件。

  • 词法分析(Lexical Analysis):将代码切成一个个 token,比如大小括号,等于号还有字符串等。是计算机科学中将字符序列转换为标记序列的过程。

  • 语法分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成 AST(Abstract Syntax Tree抽象语法树) 。

  • 静态分析(Static Analysis):使用它来表示用于分析源代码以便自动发现错误。

  • 中间代码生成(Code Generation):生成中间代码 IR(Intermediate Representation),Code Generation 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,后端的输入。

  • 优化(Optimize):LLVM 会去做些优化工作,在 Xcode 的编译设置里也可以设置优化级别-O1、-O3、-Os…还可以写些自己的 Pass

  • 生成目标文件(Assemble):生成Target相关Object(Mach-o)。

  • 链接(Link):生成Executable可执行文件。

    img

而在优化过程中,可以自己定义Pass来优化代码

img

1、编译插件的工具准备

1.1 新建文件夹llvm,下载LLVM(预计大小 648.2 M)

$ git clone https://git.llvm.org/git/llvm.git/

1.2 在llvm/tools文件夹下载clang(预计大小 240.6 M)

$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/

img

1.3 安装编译工具ninja和cmake

$ brew install cmake
$ brew install ninja

1.4 在llvm同级目录下新建llvm_build和llvm_release两个文件夹,llvm是编译起始文件夹,llvm_release则是编译结果文件夹

img

1.5 在llvm_build文件夹下设定编译结果路径

$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=’编译结果路径,也就是llvm_release文件夹’
(参考:cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=‘/Users/xxxx/LLVMProject/llvm_release’)

1.6 在llvm_build路径下依次执行编译和安装命令

$ ninja
$ ninja install

1.7 在llvm源码同级目录下新建文件夹llvm_xcode

1.8 在llvm_xcode路径下,编译xcode

$ cd llvm_xcode
$ cmake -G Xcode ../llvm

最终效果:

img

1.9 打开LLVM.xcodeproj并用Automaticallly Create Schemes

img

1.10 然后选择ALL_BUILD进行编译,编译过程需要几十分钟不等。

img

2、编写PASS插件

在$LLVM_SOURCE/lib/Transforms/ 目录下有一个Hello的自带Demo,可以仿照Hello编写我们自己的PASS

2.1 在Hello同级目录下创建文件夹MyPass,并在MyPass文件夹下创建CMakeLists.txt和MyPass.cpp两个文件

img

2.2 在$LLVM_SOURCE/lib/Transforms/MyPass/CMakeLists.txt内添加内容,需要注意的是add_llvm_library后面的MyPass是将要生成的Target的名称,自带的Hello文件夹内添加的是LLVMHello名称,所以Target是LLVMHello。

add_llvm_library( MyPass MODULE BUILDTREE_ONLY
  MyPass.cpp  
  DEPENDS
  intrinsics_gen
  PLUGIN_TOOL
  opt
)

2.3 在$LLVM_SOURCE/lib/Transforms/CMakeLists.txt内把我们的pass添加进去

add_subdirectory(MyPass)

img

2.4 回到llvm_xcode文件夹重新生成xcode

cmake -G Xcode ../llvm

2.5 再次打开LLVM.xcodeproj就能找到MyPass的Target。

img

2.6 在MyPass.cpp内编写PASS内容

#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/DebugLoc.h"
#include "llvm/IR/DebugInfo.h"
#include <string>

using namespace llvm;

namespace {
    struct MyFunctionPass : public FunctionPass {
        static char ID;
        MyFunctionPass() : FunctionPass(ID) {}
        bool runOnFunction(Function &F) override {
            if (F.getName().startswith("my_mark_executed_func")){ // 如果已经插入则不用再次插入
                  return false;
            }
            LLVMContext &context = F.getParent()->getContext();
            BasicBlock &bb = F.getEntryBlock();
            IRBuilder<> bbBuilder(&bb);
            IRBuilder<> contextBuilder(context);
            
            Instruction *beginInst = dyn_cast<Instruction>(bb.begin()); // 所有函数的起始位置
            FunctionType *type = FunctionType::get(Type::getVoidTy(context), {Type::getInt8PtrTy(context),}, false); // 函数的返回类型和参数类型
            FunctionCallee beginFun = F.getParent()->getOrInsertFunction("my_mark_executed_func", type); // 获取函数
            CallInst *inst = contextBuilder.CreateCall(beginFun,{bbBuilder.CreateGlobalStringPtr(F.getName())}); // 构造函数
            inst->insertBefore(beginInst); // 插入标记函数
            
            auto SP = F.getSubprogram();
            DebugLoc DL = DebugLoc::get(SP->getScopeLine(), 0, SP);
            inst->setDebugLoc(DL); //设置DebugLoc,给debugger使用
            return false;
        }
    };
}

char MyFunctionPass::ID = 0;
static RegisterPass<MyFunctionPass> X("func-coverage", "A pass that can check function coverage.", false, false);

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

2.7 build MyPass可以得到Mypass.dylib

img

3、使用PASS

3.1 新建一个普通xcode project,并添加一个hook.c的文件,里面是刚才在pass里添加的标记函数

void my_mark_executed_func(char *name ) {
    printf("方法 %s \n",name);
}

3.2 将bulid system改为旧版构建方法,File-Project Setting-Build System

img

3.3 在build setting里添加MyPass.dylib。

-Xclang -load -Xclang Pass路径

img

3.4 在User-Defined内添加CC和CXX,值分别是刚刚构建的clang的路径

3.5 将Enable Index-While-Building Functionality值改为NO,否则会报错

img

3.6 执行可以获取到log输出方法执行情况

img