LLVM-插桩

Clnag插件编写教程

Posted by Ted on June 28, 2020

0、Clang插桩原理

Clang在优化过程中,可以自己定义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的路径,让xode使用clang的替代版本

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

img

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

img

4、插桩的其他思路:SanitizerCoverage

LLVM本身提供了一种覆盖函数的方案:SanitizerCoverage

img

在官方文档中就有说明,如果在编译配置里有-fsantize-cover等参数,就会在每个函数的边缘插入一个sanitizer-cov—trace-guard函数,也就是说,每个方法函数执行的时候,都会调用一次这个插入的函数,所以我们可以通过这个插入函数,来获取方法函数名,从而获取启动过程中的符号顺序。

5、插桩的其他思路:SanitizerCoverage

OC 的方法调用在底层都是objc_msgSend函数。所以,如果能够Hook它,获取每个调用objc_msgSend的方法名,也能够达到插桩效果。objc_msgSend是C函数而且是系统函数,C 函数在编译链接时就确定了函数指针的地址偏移量(Offset),虽然这个偏移量在编译好的可执行文件中是固定的,但是可执行文件每次被重新装载到内存中时被系统分配的起始地址(在 lldb 中用命令image List获取)是不断变化的。所以,我们就可以借助facebook公司的一个开源库fishhook来达到系统函数与自己定义的函数进行了交换;

苹果采用了PIC(Position-independent code)技术成功让 C 的底层也能有动态的表现:

  • 编译时在 Mach-O 文件 _DATA 段的符号表中为每一个被引用的系统 C 函数建立一个指针(8字节的数据,放的全是0),这个指针用于动态绑定时重定位到共享库中的函数实现。
  • 在运行时当系统 C 函数被第一次调用时会动态绑定一次,然后将 Mach-O 中的 _DATA 段符号表中对应的指针,指向外部函数(其在共享库中的实际内存地址)。

fishhook 正是利用了 PIC 技术做了这么两个操作:

  • 将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。
  • 将内部函数的指针在动态链接时指向系统方法的地址。

这样就把系统方法与自己定义的方法进行了交换,达到 HOOK 系统 C 函数(共享库中的)的目的。

下面是是我们的hook大概过程

static void hook_objc_msgSend() {
    /// begin之前保存objc_msgSend的参数
    save()
    /// 将objc_msgSend执行的下一个函数地址传递给beginHook的第二个参数x0 self, x1 _cmd, x2: lr address
    __asm volatile ("mov x2, lr\n");
    /// 执行beginHook
    call(blr, & beginHook)
    /// 恢复objc_msgSend参数
    load()
    // 执行
    call(blr, orig_objc_msgSend)
    /// after之前保存objc_msgSend执行完成的参数----------
    save()
    /// 调用 endHook----------
    call(blr, & endHook)
    /// 将endHook返回的参数放入lr,恢复调用beginHook前的lr地址
    __asm volatile ("mov lr, x0\n");
    /// 恢复objc_msgSend执行完成的参数
    load()
    /// 方法结束,继续执行lr
    ret()
}

我们需要使用汇编指令对寄存器进行存取和执行,stp存储原参数,blr调用执行,ldp加载指令。这个过程与OC方法的Method-Swilling类似。最终可以达到hook_objc_msgSend函数与原来的objc_msgsend函数进行调换的目的。