Clang前端使用LLVM Pass示例

发布时间 2023-05-24 04:06:13作者: 吴建明wujianming

Clang前端使用LLVM Pass示例

https://mp.weixin.qq.com/s/e3e4a7ei61O99-JUWjDbnA

Objective-C在函数hook的方案比较多,但通常只实现了函数切片,也就是对函数的调用前或调用后进行hook,这里介绍一种利用llvm pass进行静态插桩的另外一种思路,希望起到抛砖引玉的作用,拿来实现更多有意思的功能。

1. Objective-C中的常见的函数Hook实现思路

Objective-C是一门动态语言,具有运行时的特性,所以能选择的方案比较多,常用的有:method swizzle,message forward(aspectku),libffi,fishhook。但列举的这些方案只能实现函数切片,也就是在函数的调用前或者调用后进行Hook,但比如想在这函数的逻辑中插入桩函数(如下),常见的hook思路就没办法实现了。

- (NSInteger)foo:(NSInteger)num {
    NSInteger result = 0;
    if (num > 0) {
        // 往这里插入一个桩函数:__hook_func_call(int,...)
        result = num + 1;
    }else {
        // 往这里插入一个桩函数:__hook_func_call(int,...)
        result = num + 2;
    }
    return result;
}

为了解决上述问题,接下来介绍如何利用在编译的过程中修改对应的文件,实现把桩函数插入到指定的函数实现中。例如以上的函数,插入桩函数之后的效果(在函数打个断点,然后查看汇编代码,就能看到对应的自定义桩函数)。

那么如何自定义Clang命令,利用llvm Pass实现对函数的静态插桩,下面分为两部分,一部分是llvm Pass,另外一部分是自定义Clang的编译参数。两者合起来实现这个功能。

2. 什么是LLVM Pass

LLVM Pass是一个框架设计,是LLVM系统里重要的组成部分,一系列的Pass组合,构建了编译器的转换和优化部分,抽象成结构化的编译器代码。LLVM Pass分为两种Analysis pass(分析流程)和Transformation pass(变换流程)。前者进行属性和优化空间相关的分析,同时产生后者需要的数据结构。两都都是LLVM编译流程,并且相互依赖。

由于 LLVM 良好的模块化,因此直接写一个优化遍来实现优化算法的方法是可行

的,也是相对容易的。编写 pass 的流程是:

1)挑选测试用例 foo.c 作为 LLVM 编译器的输入。

2)利用 clang 前端生成 LLVM 中间表示 foo.ll,通过 LLVM 后端的 CodeGen 生

成 Target 代码(Target 是目标平台)。命令是 clang -emit-llvm foo.c –S –o foo.ll,需要参考的文档可能包括 LLVM Command Guide。

3)生成目标平台的汇编代码,命令是 llc foo.ll –march=Target –o foo.s,参考文档

Writing an LLVM Backend[46],The LLVM Target-Independent Code Generator[47]。

4)使用汇编器和链接器,将 foo.s 编译成平台可执行 exe 文件。执行测试程序的

执行时间。

5)用 oprofile 等性能分析工具对程序做 profiling,找出程序的热点,也就是程序

的性能瓶颈,看汇编中哪段代码耗时比较多,有可提升的空间。

6)在分析 foo.s 后,找出程序的缺陷,分析一般形式,提出改进后的目标代码

foo_opt.s。

7)找出与热点代码相对应的 IR,在对 IR 实现理解的基础上,结合改进的目标代

码,写出改进后的 IR。这是最关键的一步,因为 IR 到目标代码之间还要进行很多的优

化、转化,必须对程序以及 IR 进行足够的分析,才能知道什么样的 IR 可以生成期望的

汇编代码。这需要参考一些 LLVM 的文档,包括 LLVM Language Reference Manual,

LLVM’s Analysis and Transform Passes。

8)编写 LLVM 转化 Pass,参考文档 LLVM Programmer’s Manual,LLVM Coding

Standards,Doxygen generated documentation,Writing an LLVM Pass。

该过程在图 4.7 中给出。通过上面的步骤就可以实现一个优化遍。该优化算法最重

要解决的问题就是如何使数组地址能够实现自增,以及在何处插入 PHI 结点。

 图4.7 编写pass流程

常见的应用场景有代码混淆 、单测代码覆盖率、代码静态分析等。

3. 编译过程

 这里“插桩”的思路就是利用OC编译过程中,使用自定义的Pass(这里使用的是transformation pass),来篡改IR文件。比如上述的代码,如果不加入自定义的Pass(左图)加入自定义的Pass(右图)编译出来的IR文件,可以看到两者在对应的基础块不同的地方。

4. LLVM IR文件的描述

LLVM IR(Intermediate Representation)直译过来是“中间表示”,它是连接编译器中前端和后端的桥梁,它使得LLVM可以解析多种源语言,并为多个目标机器生成代码。前端产生IR,而后端消费它。更多的介绍看这个视频LLVM IR Tutorial。

5. 下载LLVM

苹果fork分支https://github.com/apple/llvm-project选择一个新apple/main那个分支即可。

clone下来之后,在编译之前,要实现想要的效果,需要处理两个问题:

6. 写自定义的Pass

1)编写插桩的代码

也就是llvm pass,这里主要是要插入代码,所以用的是transformation pass

在llvm/include/llvm/Transforms/新增一个文件夹(InjectFuncCall),然后上面放着LLVM Pass的头文件声明

新建头文件:

namespace llvm {
class InjectFuncCallPass : public PassInfoMixin {
public:
    /// 构造函数
    /// AllowlistFiles 白名单
    /// BlocklistFiles 黑名单
    explicit InjectFuncCallPass(const std::vector &AllowlistFiles,const std::vector &BlocklistFiles) {
        if (AllowlistFiles.size() > 0)
          Allowlist = SpecialCaseList::createOrDie(AllowlistFiles,                                                  *vfs::getRealFileSystem());
        if (BlocklistFiles.size() > 0)
          Blocklist = SpecialCaseList::createOrDie(BlocklistFiles,                                                 *vfs::getRealFileSystem());
    }
  PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM);
  bool runOnModule(llvm::Module &M);
private:
    std::unique_ptr Allowlist;
    std::unique_ptr Blocklist;
};
} // namespace llvm

在llvm/lib/Transforms新增一个文件夹(InjectFuncCall),然后上面放着对应的LLVM Pass的cpp文件新建cpp文件:llvm/lib/Transforms/InjectFuncCall/InjectFuncCall.cpp

using namespace llvm;
bool InjectArgsFuncCallPass::runOnModule(Module &M) {
    bool Inserted = false;
    auto &CTX = M.getContext();
    for (Function &F : M) {
        if (F.empty())
            continue;;
        if (F.isDeclaration()) {
            continue;
        }
        if (F.getLinkage() == GlobalValue::AvailableExternallyLinkage)
           continue;
        if (isa(F.getEntryBlock().getTerminator()))
           continue;;
        if (Allowlist && !Allowlist->inSection("Inject-Args-Stub", "fun", F.getName())) {
            continue;
        }
        if (Blocklist && Blocklist->inSection("Inject-Args-Stub", "fun", F.getName())) {
            continue;
        }
        IntegerType *IntTy = Type::getInt32Ty(CTX);
        PointerType* PointerTy = PointerType::get(IntegerType::get(CTX, 8), 0);
        FunctionType *FuncTy = FunctionType::get(Type::getVoidTy(CTX), IntTy, /*IsVarArgs=*/true);
        FunctionCallee FuncCallee = M.getOrInsertFunction("__afp_capture_arguments", FuncTy); // 取到一个callee
        for (auto &BB : F) {
            SmallVector<value*, 16=""> CallArgs;
            for (Argument &A : F.args()) {
                CallArgs.push_back(&A);
            }
            Builder.CreateCall(FuncCallee, CallArgs);
        }
      Inserted = true;
    }
    return Inserted;
}
 
PreservedAnalyses InjectArgsFuncCallPass::run(Module &M,
                                              ModuleAnalysisManager &MAM) {
   bool Changed =  runOnModule(M);
   return (Changed ? llvm::PreservedAnalyses::none()
                   : llvm::PreservedAnalyses::all());

2)CMake相关声明和配置

llvm/utils/gn/secondary/llvm/lib/Transforms/InjectArgsFuncCall/BUILD.gn中需要添加以下声明,才会创建一个对应的静态库。
static_library("InjectFuncCall") {
  output_name = "LLVMInjectFuncCall"
  deps = [
    "//llvm/lib/Analysis",
    "//llvm/lib/IR",
    "//llvm/lib/Support",
  ]
  sources = [ "InjectFuncCall.cpp" ]
}
 
llvm/utils/gn/secondary/llvm/lib/Passes/BUILD.gn添加一行:“//llvm/lib/Transforms/InjectFuncCall”
 
"//llvm/lib/Transforms/Scalar",
    "//llvm/lib/Transforms/Utils",
    "//llvm/lib/Transforms/Vectorize",
    "//llvm/lib/Transforms/InjectFuncCall",
  ]
  sources = [
    "PassBuilder.cpp",
 
llvm/lib/Transforms/CMakeLists.txt添加一行代码。cmake声明工程新增一个子目录。
add_subdirectory(ObjCARC)
add_subdirectory(Coroutines)
add_subdirectory(CFGuard)
add_subdirectory(InjectArgsFuncCall)
llvm/lib/Passes/CMakeLists.txt添加一行代码。声明Pass Build会链接 “InjectFuncCall” COMPONENTS
add_llvm_component_library(LLVMPasses
  PassBuilder.cpp
  PassBuilderBindings.cpp
  PassPlugin.cpp
  StandardInstrumentations.cpp
 
  ADDITIONAL_HEADER_DIRS
  ${LLVM_MAIN_INCLUDE_DIR}/llvm
  ${LLVM_MAIN_INCLUDE_DIR}/llvm/Passes
 
  DEPENDS
  intrinsics_gen
 
  LINK_COMPONENTS
  AggressiveInstCombine
  Analysis
  Core
  Coroutines
  InjectArgsFuncCall
  IPO
  InstCombine
  ObjCARC
  Scalar
  Support
  Target
  TransformUtils
  Vectorize
  Instrumentation
  )

7. 自定义Clang命令

如何让Clang识别到自定义的命令和根据需要要加载对应的代码呢,需要修改以下几处地方。

在llvm-project/clang/include/clang/Driver/Options.td文件里面:

1)添加命令到Driver

文件很长,一般加在sanitize相关的配置后面。搜索end-fno-sanitize* flags,往下一行插入。

// 开始自定义的命令到Driver
def inject_func_call_stub_EQ : Joined<["-","--"],"add-inject-func-call=">, Flags<[NoXarchOption]>,HelpText<"Add Inject Func Call">;
def inject_func_call_allowlist_EQ : Joined<["-","--"],"add-inject-allowlist=">, Flags<[NoXarchOption]>,HelpText<"Enable Inject Func Call From AllowList">;
def inject_func_call_blocklist_EQ : Joined<["-","--"],"add-inject-blocklist=">, Flags<[NoXarchOption]>,HelpText<"Disable Inject Func Call From BlockList">;
def inject_func_call : Flag<["-","--"],"add-inject-func-call">, Flags<[NoXarchOption]>, Alias, AliasArgs<["none"]>, HelpText<"[None] Add Inject Func Call.">;
// 结束自定义的命令到Driver

2)添加命令到Fronted cc1

//===----------------------------------------------------------------------===//
// 自定义插桩 Options
//===----------------------------------------------------------------------===//
def inject_func_call_type : Joined<["-"],"inject_func_call_type=">, HelpText<"CC1 add args stub [bb,func]">;
def inject_func_call_allowlist : Joined<["-"],"inject_func_call_allowlist=">, HelpText<"CC1 add args from allow list">;
def inject_func_call_blocklist : Joined<["-"],"inject_func_call_blocklist=">,

llvm-project/clang/lib/Driver/ToolChains/Clang.cpp

3)添加Driver到Fronted之间的命令链接

在ConstructJob这个函数里面添加Driver到Fronted之间的命令链接

void Clang::ConstructJob(Compilation &C, const JobAction &JA,const InputInfo &Output, 
const InputInfoList &Inputs,const ArgList &Args, 
const char *LinkingOutput) const {
...
...
const SanitizerArgs &Sanitize = TC.getSanitizerArgs();
Sanitize.addArgs(TC, Args, CmdArgs, InputType);
 
/// 添加Driver 到Fronted之间的命令的链接
  if(const Arg *arg = Args.getLastArg(options::OPT_inject_func_call_stub_EQ)){
        StringRef val = arg->getValue();
        if (val != "none") {
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_type=" + Twine(val)));
            
            StringRef allowedFile = Args.getLastArgValue(options::OPT_inject_func_call_allowlist_EQ);
            llvm::errs().write_escaped("Clang:allowedFile:") << allowedFile << '\\n';
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_allowlist=" + Twine(allowedFile)));
 
            StringRef blockFile = Args.getLastArgValue(options::OPT_inject_func_call_blocklist_EQ);
            llvm::errs().write_escaped("Clang:blockFile:") << blockFile << '\\n';
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_blocklist=" + Twine(blockFile)));
        }
  }
...
...
}

这文件/llvm-project/clang/lib/Frontend/CompilerInvocation.cpp中处理第四步。

4)参数赋值给Option

把解析逻辑中,真正拿到clang传进来的参数赋值给Option,需要给Option新增几个变量。

在对应的文件/clang/include/clang/Basic/CodeGenOptions.h

/// type of inject func call  std::string InjectFuncCallOption;      /// inject func allow list  std::vector InjectFuncCallAllowListFiles;      /// inject func block list  std::vector InjectFuncCallBlockListFiles;bool CompilerInvocation::ParseCodeGenArgs(CodeGenOptions &Opts, ArgList &Args,                                          InputKind IK,                                          DiagnosticsEngine &Diags,                                          const llvm::Triple &T,                                          const std::string &OutputFile,                                          const LangOptions &LangOptsRef) {...for (const auto &Arg : Args.getAllArgValues(OPT_inject_args_stub_type)) {      StringRef Val(Arg);      Opts.InjectArgsOption = Args.MakeArgString(Val);  }  Opts.InjectArgsAllowListFiles = Args.getAllArgValues(OPT_inject_args_stub_allowlist);  Opts.InjectArgsBlockListFiles = Args.getAllArgValues(OPT_inject_args_stub_blocklist);...}

5)将自定义的Pass添加到Backend

在emit assembly的时机,判断Option,然后执行Model Pass Manager 的add Pass操作。

对应的文件/clang/lib/CodeGen/BackendUtil.cpp

#include "llvm/Transforms/InjectArgsFuncCall/InjectArgsFuncCall.h"
// 最后添加 Inject Args Function Pass。
if (CodeGenOpts.InjectArgsOption.size() > 0) {
      MPM.addPass(InjectArgsFuncCallPass(CodeGenOpts.InjectArgsAllowListFiles, CodeGenOpts.InjectArgsBlockListFiles));
}

8. 编译llvm

上述的配置和代码都搞完之后,接下来编译,编译的过程直接看github的readme,安装必要的工具cmake,najia等。

cd llvm-project
// 新建一个build文件夹来生成工程
mkdir build 
cd build
// -G Xcode会cmake出来一个xcode工程,也可以选择ninja
cmake -DLLVM_ENABLE_PROJECTS=clang -G Xcode ../llvm
// 执行结束后,会在build文件夹生成完整的工程目录

目前LLVM,只能用Legacy Build System。所以需要在文件→项目设置→构建系统里面切换一下。

9. 执行结果验证

1)生成IR文件调试效果

打开llvm的工程,选择clang的target,设置Clang的运行参数。

2)把上述的的路径替换成自己的路径

// 指定使用new pass manager,llvm里面有两套写自定pass的接口,现在是使用新的接口。
-fexperimental-new-pass-manager
// 启动功能,以基础块级别地插入函数
-add-inject-func-call=bb
// 设置白名单,只有在白名单里面的文件/函数才会插桩
-add-inject-allowlist=$(SRCROOT)/config/allowlist.txt
// 设置黑名单,黑名单里指定的文件/函数会忽略掉
-add-inject-blocklist=$(SRCROOT)/config/blocklist.txt

3)白名单&黑名单

简单的格式:

#指定对应的section
[InjectFuncCallSection]
# 指定对应的文件
src:/OC-Hook-Demo/OC-Hook-Demo/Foo.m
# 指定对应的函数名,*号可支持模糊匹配
func:*foo*

白名单和黑名单是参考Clang Sanitizer配置文件的格式。

 

参考文献链接

https://mp.weixin.qq.com/s/e3e4a7ei61O99-JUWjDbnA