参考资料
康奈尔大学博客: 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);
上述代码核心逻辑如下:
- 迭代函数中的每一条指令
- 判断当前指令是否为 LLVM IR 的
call
指令, 这样的指令为函数调用指令 - 判断被调用的函数名是否为
posix_memalign
- 如果是, 定位当前
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 说明
- 函数原型定义
//// 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);
- 获取
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);
}
}
- 使用
builder
在指定位置构建指令
//// 创建 IRBuilder, 用于 IR 指令构建
IRBuilder<> builder(call);
//// 设置插桩点, 在当前位置插桩, 后续内容会向后移动
builder.SetInsertPoint(&B, builder.GetInsertPoint());
// 如果需要在下一行插桩
// builder.SetInsertPoint(&B, ++builder2.GetInsertPoint());
//// 插入一条 call 指令, 传入函数原型与参数列表
builder.CreateCall(rt_posix_memalign, args);
文档信息
- 本文作者:Birkhoff
- 本文链接:https://blogs.maikebuke.com/2023/02/18/03-LLVM-Pass-%E9%9D%99%E6%80%81%E6%8F%92%E6%A1%A9%E6%96%B9%E5%BC%8F/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)