LLVM Pass 静态插桩

2023/02/18 LLVM 共 4786 字,约 14 分钟

参考资料

康奈尔大学博客: https://www.cs.cornell.edu/~asampson/blog/llvm.html

终于开始正题了! 中文互联网上关于 LLVM 静态插桩的资料实在是太少了. 经过了一个月的磨难, 终于摸清了如何使用 LLVM 进行静态插桩.

从简单的示例开始: 获取 posix_memalign 参数值

项目结构如下:

root/
	|---- Hello.cpp		# LLVM Pass 函数插桩代码
	|---- main.c		# 样本程序
	|---- runtime.c		# 运行时库

其中, 样本程序的功能非常简单, 为使用 posix_memalign 函数分配一块内存, main.c代码如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char** argv) {
    char *s=NULL;
    printf("LLVM Test posix_memalign\n");
    posix_memalign((void **)&s, 16, 1024);
    printf("====== Finish ======\n");
    return 0;
}

为了对上述代码进行插桩, Hello.cpp 代码如下:

#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstrTypes.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/TypeFinder.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"
#include "llvm/Transforms/Utils/BasicBlockUtils.h"

using namespace llvm;

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

  virtual bool runOnFunction(Function& F) {
    // 获取函数上下文
    LLVMContext& Ctx = F.getContext();
    // 定义插桩函数原型, 参数类型+返回类型+插桩函数
    Type* retType = Type::getVoidTy(Ctx);
    std::vector<Type*> rt_posix_memalign_param_types = {
        Type::getHalfPtrTy(Ctx), Type::getInt32Ty(Ctx), Type::getInt32Ty(Ctx)};
    FunctionType* rt_posix_memalign_type =
        FunctionType::get(retType, rt_posix_memalign_param_types, false);
    FunctionCallee rt_posix_memalign = F.getParent()->getOrInsertFunction(
        "rt_posix_memalign", rt_posix_memalign_type);

    // 迭代基本块
    for (auto& B : F) {
      // 迭代指令
      for (auto& I : B) {
        // 判断指令是否是函数调用指令 CallInst
        if (auto* call = dyn_cast<CallInst>(&I)) {
          // 获取被调用的函数
          Function* fun = call->getCalledFunction();
          if (!fun) {
            continue;
          }
          // 判断函数是否为指定名称函数
          if (0 == fun->getName().compare(StringRef("posix_memalign"))) {
            // 创建 IRBuilder, 用于 IR 指令构建
            IRBuilder<> builder(call);
            // 设置插桩点, 在当前指令前插桩
            builder.SetInsertPoint(&B, builder.GetInsertPoint());

            // 设置新的插桩点
            auto* call2 = dyn_cast<CallInst>(&I);
            IRBuilder<> builder2(call2);
            builder2.SetInsertPoint(&B, ++builder2.GetInsertPoint());

            std::vector<Value*> args;
            for (auto arg = call->arg_begin(); arg != call->arg_end(); ++arg) {
              // 将当前调用函数 call 的参数列表推到 args 中
              args.push_back(*arg);
            }
            // 将插桩函数插入上下文中, 分别在函数调用前后插桩
            builder.CreateCall(rt_posix_memalign, args);
            builder2.CreateCall(rt_posix_memalign, args);
          }
        }
      }
    }

    return false;
  }
};
}  // namespace

char SkeletonPass::ID = 0;

static RegisterPass<SkeletonPass> X("rhpass", "Hello World Pass");

static void registerSkeletonPass(const PassManagerBuilder&,
                                 legacy::PassManagerBase& PM) {
  PM.add(new SkeletonPass());
}
static RegisterStandardPasses RegisterMyPass(
    PassManagerBuilder::EP_EarlyAsPossible,
    registerSkeletonPass);

上述代码核心逻辑如下:

  1. 迭代函数中的每一条指令
  2. 判断当前指令是否为 LLVM IR 的 call 指令, 这样的指令为函数调用指令
  3. 判断被调用的函数名是否为 posix_memalign
  4. 如果是, 定位当前 call 指令的前后位置, 并将构造好的函数 rt_posix_memalign 插入当前指令前后

显然, 由于 rt_posix_memalign 函数是自己定义的函数, 未在样本中实现, 因此该二进制代码无法直接编译为可执行文件. 需要后续与 rt_posix_memalign 函数所在模块链接才行. 该函数所在的文件 runtime.c 内容如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void rt_posix_memalign(void **p, size_t align, size_t size) {
    printf("%p -- %p -- %zu -- %zu\n", p, *p, align, size);
    return;
}

其功能为打印 posix_memalign 函数参数到标准输出流中. 使用如下命令编译与验证插桩:

# 删除生成的中间文件
rm -f *.so *.elf *.ll

# 构建 LLVM Pass
clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared Hello.cpp -o LLVMHello.so `llvm-config --ldflags`
# 使用 Pass 编译样本
clang -flegacy-pass-manager -g -Xclang -load -Xclang ./LLVMHello.so -c main.c -o main.o
# 构建运行时函数
clang -c runtime.c -o runtime.o
# 链接生成可执行文件
clang main.o runtime.o -o main.elf

# 运行样本
./main.elf

得到的输出如下:

LLVM Test posix_memalign
0x7ffec9cef698 -- (nil) -- 16 -- 1024
0x7ffec9cef698 -- 0x17516b0 -- 16 -- 1024
====== Finish ======

关键 API 说明

  1. 函数原型定义
//// F <- LLVM::Function
//// 获取上下文环境
LLVMContext& Ctx = F.getContext();
//// 定义插桩函数原型, 参数类型+返回类型+插桩函数
// 定义返回值类型
Type* retType = Type::getVoidTy(Ctx);
// 定义形参类型
std::vector<Type*> rt_posix_memalign_param_types = {
    Type::getHalfPtrTy(Ctx), Type::getInt32Ty(Ctx), Type::getInt32Ty(Ctx)};
// 定义函数类型(函数声明, 函数指针原型)
FunctionType* rt_posix_memalign_type =
    FunctionType::get(retType, rt_posix_memalign_param_types, false);
// 构建函数原型
FunctionCallee rt_posix_memalign = F.getParent()->getOrInsertFunction(
    "rt_posix_memalign", rt_posix_memalign_type);
  1. 获取 call 指令的相关信息
//// I <- LLVM::Instruction
//// 尝试将指令转换为 call 指令原型
auto* call = dyn_cast<CallInst>(&I);
//// 获取 call 调用的函数 f
Function* f = call->getCalledFunction();
//// 判断函数名是否与特定字符串匹配
if (0 == fun->getName().compare(StringRef("posix_memalign"))) {
    // 创建一个向量, 用于存储函数调用时的参数
    std::vector<Value*> args;
    // 迭代参数, 从call->arg_begin()到call->arg_end(), 迭代参数本身
    for (auto arg = call->arg_begin(); arg != call->arg_end(); ++arg) {
        // 将当前调用函数 call 的参数列表推到 args 中
        args.push_back(*arg);
    }
}
  1. 使用 builder 在指定位置构建指令
//// 创建 IRBuilder, 用于 IR 指令构建
IRBuilder<> builder(call);
//// 设置插桩点, 在当前位置插桩, 后续内容会向后移动
builder.SetInsertPoint(&B, builder.GetInsertPoint());
// 如果需要在下一行插桩
// builder.SetInsertPoint(&B, ++builder2.GetInsertPoint());
//// 插入一条 call 指令, 传入函数原型与参数列表
builder.CreateCall(rt_posix_memalign, args);

文档信息

搜索

    Table of Contents