编写LLVM Pass模块知识点梳理

2 篇文章 0 订阅
2 篇文章 0 订阅

一、本文目标

  • LLVM 平台提供了 Pass 模块编写功能,类似于一种插件,可以对高级语言源代码被前端处理后生成的中间 IR 语句进行处理
  • 基于此特性,可以将自己设计的代码混淆的一些机制、语句编写成Pass模块来作用于目标代码上,并且具有跨平台的效果
  • 以不透明谓词为例,目前大量文献甚至包括 OLLVM 这套工具在内都是以实现一些简单算式为目标,更多复杂数据结构如何植入并未系统介绍
  • 本文旨在以C语言为原型,整理部分常见的语句结构如何转化成 Pass 对应的代码,这里只提供本人的一种实现方法供参考,如有更好的实现方案欢迎网友在评论区补充,或者后续我在博客里更新
  • 注:Pass模块中涉及到的相关API、LLVM自定义的数据类型、以及Pass模块在LLVM源代码中的注册方式,LLVM 官方提供了在线手册供查阅,本文不再赘述,相关问题欢迎在评论区探讨

二、自定义混淆代码转化为Pass的一种工作流程

根据自己的工作经验,推荐按如下流程来开展混淆代码转化的工作:

  1. 先用高级语言(比如C语言)将代码混淆的设计写出原型,并编程生成程序文件先验证设计能否满足预期要求;
  2. 使用clang/clang++ 对原型设计代码编译输出中间IR,此过程需使用编译选项 -emit-llvm-S,编译出中间 IR 语句主要是便于观察程序内部的转化细节;
  3. 观察自己的原型代码在IR层的映射,特别是每个Function内部BasicBlock块的生成以及BR指令形成的BasicBlock块的跳转关系。如果生成的BB块(BasicBlock)较多,建议使用 Visio 或者其他绘图工具先辅助绘制出每个 Function下的BB块跳转流程图
  4. 编写Pass模块的代码,将上面原型设计中的映射的每条 IR 指令转化成 Pass 模块中的 API 调用,步骤3中绘制的跳转流程图可以辅助厘清BB块的跳转关系,减少出错
  5. 写好 Pass 模块代码后,可以集成到 LLVM 源代码(建议将clang的代码也集成在里面)中一起编译生成一套新的clang/LLVM工具,然后使用 clang/clang++ 工具使用选项-emit-llvm-S编译待混淆的目标代码,在 IR 层观察是否植入了设计的混淆代码,并生成可执行文件运行观察混淆设计是否会影响程序执行效果。

三、一些常见语句结构在Pass中的实现

我们用C语言设计一个返回值为int型的函数foo作为一种不透明谓词的实现,并以此来演示上述流程,代码所在文件命名为foo.c,内容如下,基本功能是对数组元素求和。这里将负责实现植入此函数的Pass模块命名为FooPass,对应的代码文件名为FooPass.cpp

int foo(int* a, int len)
{
	int sum = 0;
	for (int i = 0; i<len; i++)
		sum += a[i];
	return sum;
}

3.1 生成、植入Function

关键API整理如下

FunctionType::get()
Function::Create()

参考前面的工作流程,首先对 foo.c 文件编译生成中间 IR 进行观察,命令如下,使用的clang版本为9.0:

$ clang foo.c -emit-llvm -S -o foo.ll

得到foo.ll后,其中关键内容节选如下,包含了foo函数的全部功能(clang 4.0 编译输出中没有dso_local关键字):

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @foo(i32*, i32) #0 {
  %3 = alloca i32*, align 8
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  %6 = alloca i32, align 4
  store i32* %0, i32** %3, align 8
  store i32 %1, i32* %4, align 4
  store i32 0, i32* %5, align 4
  store i32 0, i32* %6, align 4
  br label %7

7:                                                ; preds = %19, %2
  %8 = load i32, i32* %6, align 4
  %9 = load i32, i32* %4, align 4
  %10 = icmp slt i32 %8, %9
  br i1 %10, label %11, label %22

11:                                               ; preds = %7
  %12 = load i32*, i32** %3, align 8
  %13 = load i32, i32* %6, align 4
  %14 = sext i32 %13 to i64
  %15 = getelementptr inbounds i32, i32* %12, i64 %14
  %16 = load i32, i32* %15, align 4
  %17 = load i32, i32* %5, align 4
  %18 = add nsw i32 %17, %16
  store i32 %18, i32* %5, align 4
  br label %19

19:                                               ; preds = %11
  %20 = load i32, i32* %6, align 4
  %21 = add nsw i32 %20, 1
  store i32 %21, i32* %6, align 4
  br label %7

22:                                               ; preds = %7
  %23 = load i32, i32* %5, align 4
  ret i32 %23
}

注意到这里是用C语言编译得到的输出,函数名不会受到C++命名混淆机制(name mangling)的影响,保留为原始的foo

上述示例代码中包含了foo.c的主体IR指令,我们依据Function、BasicBlock、Instruction这三个层次来剖析这个函数的结构,并对应介绍如何手动自定义植入这些内容:

  • 首先, 关键字define…{…}覆盖的区域构成一个 Function 对象的内容;
  • 其次,诸如7:11:等标签开启的一小段指令构成一个BasicBlock(常见以 brret 指令结束)
    • 需要说明的是,每个Function对象中首个入口BB块(entryBB)在IR中不会显式给出诸如entry:之类的标签开启一段代码,而是直接紧跟define从下面一行直接开始,如果涉及到遍历BB块处理的时候要注意第一个BB块所对应的位置。
  • 最后,每个BB块中每一行内容构成一个Instruction对象。

这里的Module对应foo.ll文件,我们计划在目标Module中植入上述foo函数,首先是构造Function对象的声明语句,接着是填充这个Function对象的BB块与Instruction。这个过程会对原始程序进行改动,根据LLVM手册说明,如果要对原始程序的函数进行增删,那么应该在Pass模块的doInitialization(){}虚函数中加以实现,这里假定将自定义Function的植入操作封装在一个void CreateFooFunc(Module &M)的成员函数中。根据LLVM手册说明,由于调用该成员函数会涉及到对被植入对象的改动,doInitialization(){}的返回值应该设为true,初步构建Pass模块代码框架如下:

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

    void CreateFooFunc(Module &M){
        // write something here
        ...
    }

    virtual bool doInitialization(Module &M){
        CreateFooFunc();
        return true;
    }

    bool runOnFunction(Function &F) {
        ...
    }
    return false;
}
}; 

接下来就是填充void CreateFooFunc(Module &M)的函数体部分,该成员函数主要完成以下两个任务:

  1. 向当前Module中植入foo函数的声明
  2. 植入foo函数的函数体

先介绍如何向Module中植入foo函数的声明,对应到前面介绍的工作流程步骤3,可观察到在IR层foo函数的声明如下(dso_local关键字可忽略):

define dso_local i32 @foo(i32*, i32) 

foo函数在IR层被表示为返回值为i32型,参数类型依次为i32*,i32,那么在void CreateFooFunc(Module &M)中写入语句如下:

void CreateFooFunc(Module &M) {
	// create the definition of foo func in the Module M
	//===---- prepare the type var and constant nums to simplify the codes ---===
	IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);			// i32 type
	PointerType* i32_ptr = llvm::IntegerType::getInt32PtrTy(M.getContext());		// i32* type
	Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
	//===--- create the argu list (i32*, i32)---===
	std::vector<Type*> argsList;
	argsList.push_back(i32_ptr);			// i32* type
    argsList.push_back(i32);
	//===--- declare the func prototype in the Module M ---===
	FunctionType* funcDef = FunctionType::get(i32, argsList, false);
	//===--- create Function* object for foo func ---===
	// create name string "foo"
	std::ostringstream oss;
	oss << "foo" ;
	std::string funcNameStr = oss.str();
	// create func ptr
	Function* fooFuncPtr = Function::Create(funcDef, Function::ExternalLinkage, funcNameStr, &M);
	// Now, we have finished inserting the declaration of a custom func in the Mdoule M.
	......
}

以上语句向Module M中植入了一条函数声明语句,第一个关键APIFunctionType::get()负责生成Module中的函数声明,传入的第一个参数指定了自定义函数的返回值类型,第二个参数传入一个容器类,里面按从左到右的顺序依次存放了自定义函数的各个参数类型,这个过程没有涉及到函数命名的问题。

第二个关键APIFunction::Create()接收上面生成的函数声明,并指定函数命名,在Module中植入了一个函数体为空的“foo”函数。
至此,向Module中植入Function的初步工作完成,下面介绍如何填充foo函数的函数体。

3.2 生成、植入 BasicBlock

关键API整理如下

llvm::BasicBlock::Create()

接上,观察foo函数生成的IR语句,我们可以梳理出BasicBlock块如下:

define dso_local i32 @foo(i32*, i32) #0 {
  [entryBB:]
  %3 = alloca i32*, align 8
  ...
  
  7:                                                ; preds = %19, %2
  %8 = load i32, i32* %6, align 4
  ...
  
  11:                                               ; preds = %7
  %12 = load i32*, i32** %3, align 8
  ...
  
  19:                                               ; preds = %11
  %20 = load i32, i32* %6, align 4
  ...
  
  22:                                               ; preds = %7
  %23 = load i32, i32* %5, align 4
  ...

由此可见,foo函数一共由5个BasicBlock块组成,而且仔细观察BB块7:如下,可以发现此BB块末尾使用了一个条件分支指令br结束,其中判断条件部分则使用一条icmp比较指令生成,这些特征可以很容易联想到for循环的条件判断部分:

7:                                                ; preds = %19, %2
  %8 = load i32, i32* %6, align 4
  %9 = load i32, i32* %4, align 4
  %10 = icmp slt i32 %8, %9
  br i1 %10, label %11, label %22

再看BB块19: 如下,里面包含了一条加法指令add,对变量%20执行加1操作,可看出这个BB块有变量自增特征,并且末尾采用了一条无条件分支指令br,跳转目标指向前面的BB块7:,可以判断出这个BB块负责了for循环的循环变量自增部分:

19:                                               ; preds = %11
  %20 = load i32, i32* %6, align 4
  %21 = add nsw i32 %20, 1
  store i32 %21, i32* %6, align 4
  br label %7

那么可知在条件和自增BB块中间则包含了for循环的循环体部分,在本示例中for循环体只有一个BB块,如果for循环内部比较复杂,则中间循环体部分也可能由多个BB块组成。

按照本文前面介绍的工作流程,这里先绘制出这个函数BB块之间的跳转关系示意图如下,绘制这个图示可帮助厘清复杂函数植入时的BB块跳转关系:
在这里插入图片描述
按照上面这个顺序,继续往void CreateFooFunc(Module &M)函数中添加语句如下,这个过程要用到前面创建的Function* fooFuncPtr

void CreateFooFunc(Module &M) {
	...
	// Now, we have finished inserting the declaration of a custom func in the Mdoule M.
	//===--- prepare the BasicBlocks for the func body ---===
	// create BBs' name str
	Twine* entryBBName = new Twine("entryBB");
	Twine* forLoopCondBBName = new Twine("forLoopCondBB");
	Twine* forLoopBodyBBName = new Twine("forLoopBodyBB");
    Twine* forLoopIncBBName = new Twine("forLoopIncBB");
    Twine* endBBName = new Twine("endBB");
	// create the BasicBlock objects
	BasicBlock* entryBB = llvm::BasicBlock::Create(M.getContext(), *entryBBName, fooFuncPtr);
	BasicBlock* forLoopCondBB = llvm::BasicBlock::Create(M.getContext(), *forLoopCondBBName,fooFuncPtr);
	BasicBlock* forLoopBodyBB = llvm::BasicBlock::Create(M.getContext(), *forLoopBodyBBName, fooFuncPtr);
	BasicBlock* forLoopIncBB = llvm::BasicBlock::Create(M.getContext(), *forLoopIncBBName, fooFuncPtr);
    BasicBlock* endBB = llvm::BasicBlock::Create(M.getContext(), *endBBName, fooFuncPtr);
    // Now, we have finished inserting the essential BasicBlock objects into the custom Function object.
    ...
}

至此,已向Function* fooFuncPtr中植入了构成这个函数的BB块,注意上述BB块创建语句的顺序不能随意变动,要根据IR语句中的排列顺序保持一致,这个调用顺序也就是各个BB块在Function* fooFuncPtr中的排列顺序。

3.3 创建、使用局部变量

关键API调用语句整理如下

IRBuilder<> Builder(entryBB->getContext());
Builder.SetInsertPoint(entryBB);
Function::arg_iterator Arg = fooFuncPtr->arg_begin();
Builder.CreateAlloca();
Builder.CreateAlignedStore();
Builder.CreateAlignedLoad();
Builder.CreateICmpSLT();
Builder.CreateBr();
Builder.CreateCondBr();
Builder.CreateNSWAdd();
Builder.CreateSExtOrTrunc();
Builder.CreateInBoundsGEP();
Builder.CreateRet();

现在自定义foo函数的基本框架已经从宏观上搭建起来,接着就是要对每个BB块填充对应IR指令,此环节需要通过一个IRBuilder<> Builder对象来辅助完成,调用此对象的各种CreateXXX()方法即可创建出对应的IR指令对象,这也是现在构建IR指令的推荐方法,而过去借助各种XXXInst类对象及其成员函数来创建相应IR指令。

创建局部变量是由alloca指令完成,单独提出来是因为在实际编程中发现,由用户自定义创建一些局部变量供后续使用时存在一个原因不明的bug,即创建自定义局部变量的alloca指令如果不位于Function下的第一个BB块内,则植入此alloca指令的语句会引起Pass模块的执行报错。

这个现象需要引起重视的场景是假设我们希望对目标程序段的中间部分植入复杂谓词运算提高程序的复杂度,而其中需要用到一些局部变量来辅助运算,那么这些局部变量的创建alloca语句应植入到当前Function的首个BB块内(在OLLVM中进行了验证,LLVM 9.0有待测试)。

这里clang生成的IR语句中所有alloca都位于第一个BB块内,只需模仿即可,IRBuilder<> Builder对象创建一次后可反复使用,一个BB块以一条终止指令结束,常见的终止指令有brret

void CreateFooFunc(Module &M) {
	...
	// Now, we have finished inserting the essential BasicBlock objects into the custom Function object.
    IRBuilder<> Builder(entryBB->getContext());
    //===--- the first BasicBlock: entryBB ---===
    Builder.SetInsertPoint(entryBB);
	// allocate stack space for func arguments: a, len
	// [ %a.addr = alloca i32*, align 8 ]
	AllocaInst* argVarA = Builder.CreateAlloca(i32_ptr, nullptr, "a");
    argVarA->setAlignment(8);
    // [ %len.addr = alloca i32, align 4 ]
    AllocaInst* argVarLen = Builder.CreateAlloca(i32, nullptr, "len");
    argVarLen->setAlignment(4);
    // allocate stack space for local vars: sum, i
    // [ %sum = alloca i32, align 4 ]
    AllocaInst* VarSum = Builder.CreateAlloca(i32, nullptr, "sum");
    VarSum->setAlignment(4);
    // [ %i = alloca i32, align 4 ]
    AllocaInst* VarI = Builder.CreateAlloca(i32, nullptr, "i");
    VarI->setAlignment(4);
	
	// store the argu data into the above space
	Function::arg_iterator Arg = fooFuncPtr->arg_begin();
	// the first argu value: a
	Value* argAVal = &(*Arg);
	// [ store i32* %a, i32** %a.addr, align 8 ]
	Builder.CreateAlignedStore(argAVal, argVarA, 8);
    ++Arg;
	// the second argu value: len
	Value* argLenVal = &(*Arg);
	// [ store i32 %len, i32* %len.addr, align 4 ]
	Builder.CreateAlignedStore(argLenVal, argVarLen, 4);
	
	// initialize the local var
	// [ store i32 0, i32* %sum, align 4 ]
	Builder.CreateAlignedStore(zero, VarSum, 4);
	// [ store i32 0, i32* %i, align 4 ]
    Builder.CreateAlignedStore(zero, VarI, 4);

	// add the terminator inst: br
	// [ br label %7]
    Builder.CreateBr(forLoopCmpBB);
	......
}

至此,在首个BB块中已完成相关局部变量的创建工作,并且上述代码已经示范了如何逐条构建自定义函数的指令,其他BB块的构建仿照此过程即可,关键在于调用IRBuilder相应的CreateXX方法。

一份植入foo函数的完整代码示例如下,而实际开发中还可能需要针对复杂的数据结构、程序结构对应采取不同的实现策略,本文整理了一些常见的实现方式参见后面几个小点:

void CreateFooFunc(Module &M) {
// create the definition of foo func in the Module M
	//===---- prepare the type var and constant nums to simplify the codes ---===
	IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);			// i32 type
	PointerType* i32_ptr = llvm::IntegerType::getInt32PtrTy(M.getContext());		// i32* type
	Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
	//===--- create the argu list (i32*, i32)---===
	std::vector<Type*> argsList;
	argsList.push_back(i32_ptr);			// i32* type
    argsList.push_back(i32);
	//===--- declare the func prototype in the Module M ---===
	FunctionType* funcDef = FunctionType::get(i32, argsList, false);
	//===--- create Function* object for foo func ---===
	// create name string "foo"
	std::ostringstream oss;
	oss << "foo" ;
	std::string funcNameStr = oss.str();
	// create func ptr
	Function* fooFuncPtr = Function::Create(funcDef, Function::ExternalLinkage, funcNameStr, &M);

	//===--- prepare the BasicBlocks for the func body ---===
	// create BBs' name str
	Twine* entryBBName = new Twine("entryBB");
	Twine* forLoopCondBBName = new Twine("forLoopCondBB");
	Twine* forLoopBodyBBName = new Twine("forLoopBodyBB");
    Twine* forLoopIncBBName = new Twine("forLoopIncBB");
    Twine* endBBName = new Twine("endBB");
	// create the BasicBlock objects
	BasicBlock* entryBB = llvm::BasicBlock::Create(M.getContext(), *entryBBName, fooFuncPtr);
	BasicBlock* forLoopCondBB = llvm::BasicBlock::Create(M.getContext(), *forLoopCondBBName,fooFuncPtr);
	BasicBlock* forLoopBodyBB = llvm::BasicBlock::Create(M.getContext(), *forLoopBodyBBName, fooFuncPtr);
	BasicBlock* forLoopIncBB = llvm::BasicBlock::Create(M.getContext(), *forLoopIncBBName, fooFuncPtr);
    BasicBlock* endBB = llvm::BasicBlock::Create(M.getContext(), *endBBName, fooFuncPtr);

    IRBuilder<> Builder(entryBB->getContext());
    //===--- the first BasicBlock: entryBB ---===
    Builder.SetInsertPoint(entryBB);
	// allocate stack space for func arguments: a, len
	AllocaInst* argVarA = Builder.CreateAlloca(i32_ptr, nullptr, "a");
    argVarA->setAlignment(8);
    AllocaInst* argVarLen = Builder.CreateAlloca(i32, nullptr, "len");
    argVarLen->setAlignment(4);
    // allocate stack space for local vars: sum, i
    AllocaInst* VarSum = Builder.CreateAlloca(i32, nullptr, "sum");
    VarSum->setAlignment(4);
    AllocaInst* VarI = Builder.CreateAlloca(i32, nullptr, "i");
    VarI->setAlignment(4);
	
	// store the argu data into the above space
	Function::arg_iterator Arg = fooFuncPtr->arg_begin();
	// the first argu value: a
	Value* argAVal = &(*Arg);
	Builder.CreateAlignedStore(argAVal, argVarA, 8);
    ++Arg;
	// the second argu value: len
	Value* argLenVal = &(*Arg);
	Builder.CreateAlignedStore(argLenVal, argVarLen, 4);
	
	// initialize the local var
	Builder.CreateAlignedStore(zero, VarSum, 4);
    Builder.CreateAlignedStore(zero, VarI, 4);

	// add the terminator inst: br
    Builder.CreateBr(forLoopCmpBB);

	//===--- the second BasicBlock: forLoopCondBB ---===
	Builder.SetInsertPoint(forLoopCondBB);
	Value* IVal_1 = Builder.CreateAlignedLoad(VarI, 4);
    Value* LenVal_1 = Builder.CreateAlignedLoad(argVarLen, 4);
    Value* BrCond_1 = Builder.CreateICmpSLT(IVal_1, LenVal_1);
    Builder.CreateCondBr(BrCond_1, forLoopBodyBB, endBB);

	//===--- the thrid BasicBlock: forLoopBodyBB ---===
	Builder.SetInsertPoint(forLoopBodyBB);
	Value* AVal_1 = Builder.CreateAlignedLoad(argVarA, 8);
    Value* IVal_2 = Builder.CreateAlignedLoad(VarI, 4);
    Value* tmp_1 = Builder.CreateSExtOrTrunc(IVal_2, i64);
    Value* elePtr_1 = Builder.CreateInBoundsGEP(i32, AVal_1, tmp_1);
    Value* eleVal_1 = Builder.CreateAlignedLoad(elePtr_1, 4);
    Value* SumVal_1 = Builder.CreateAlignedLoad(VarSum, 4);
    Value* tmp_2 = Builder.CreateNSWAdd(SumVal_1, eleVal_1);
    Builder.CreateAlignedStore(tmp_2, VarSum, 4);
    Builder.CreateBr(forLoopIncBB);

	//===--- the fourth BasicBlock: forLoopIncBB ---===
	Builder.SetInsertPoint(forLoopIncBB);
	Value* IVal_3 = Builder.CreateAlignedLoad(VarI, 4);
	Value* tmp_3 = Builder.CreateNSWAdd(IVal_3, one);
	Builder.CreateAlignedStore(tmp_3, VarI, 4);
	Builder.CreateBr(forLoopCondBB);
	
	//===--- the fifth BasicBlock: forLoopIncBB ---===	
	Builder.SetInsertPoint(endBB);
	Value* SumVal_2 = Builder.CreateAlignedLoad(VarSum, 4);
	Builder.CreateRet(SumVal_2);
}

此外,还有一个值得考虑的问题,就是植入Module后的foo函数如何在其他代码中被调用,与之类似地,前面谈到为了辅助运算而在第一个BB块内植入了部分局部变量,这些变量仍然可以仿照上述代码调用CreateXXX()方法实现alloca指令开辟空间,但开辟空间后生成的变量指针则应如何保存并应用到Function后续环境中,这些问题我计划单写一篇博客来介绍一种处理方法。

3.4 创建、使用数组

关键API整理如下

Builder.CreateInBoundsGEP();
Builder.CreateGEP();

这是访问(多维)数组元素、结构体成员的两个重要指令,其直接作用就是提取被访问对象的内存地址,而提取被访问对象的内容则还需要搭配一条Load指令加载内存地址对应的内容,详细说明可参考LLVM官方手册介绍(http://llvm.org/docs/GetElementPtr.html#introduction),这里直接给出示例代码与说明。

一维数组

假设有数组 int a[10] = {0},现需要访问下标为3的元素值(即a[3]),使用IRBuilder.CreateInBoundsGEP()方法,则代码如下:

// argVarArr contains the array a[10]'s pointer
Value* arrVal = Builder.CreateAlignedLoad(argVarArr, 8);
// index value
Constant* three = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 3, true));
// set the element's type: i32
Type* eleTy = llvm::Type::getInt32Ty(M.getContext());
// get the element a[3]'s pointer
// [ %arrayidx = getelementptr inbounds i32, i32* %a, i32 3 ]
Value* elePtr = Builder.CreateInBoundsGEP(eleTy, arrVal, three);
// get the element a[3]'s value
// [ %4 = load i32, i32* %arrayidx, align 4 ]
Value* eleVal = Builder.CreateAlignedLoad(elePtr, 4);

注意到,这里索引值采用了 i32 型数值3,如果观察clang输出的 .ll 内容,则会发现clang在生成GEP指令时一般将此数值处理成 i64型,但这个索引值使用 i32 或 i64 型数值都可以。

使用IRBuilder.CreateGEP()方法则稍微麻烦,需要准备两个索引值,内存地址偏移量的起始仍然是数组指针变量(arrVal)所指向位置,第一个索引值为0,这个偏移量令下一个索引值的起始位置以 arrVal + 0 开始,LLVM手册中引入此索引的原因做出了详细说明,参见链接 http://llvm.org/docs/GetElementPtr.html#introduction,此处不再赘述;第二个索引值就是真正的要访问的数组元素下标值。

二维数组

处理多维数组方法同一维数组基本一致,以二维数组为例,假设有数组 int a[3][3],需要获取元素 a[1][2] 的数值,需要分成两步来完成,第一步先获取 a[1]* 指针,第二步基于该指针传入下标值 2 来获取 a[1][2] 的内容,此过程较为麻烦的地方在于要准备好数组每下一层的元素指针类型,供下层GEP指令使用,示例代码如下

Value* arrVal = Builder.CreateAlignedLoad(argVarArr, 8);
// index value
Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(64, 0, true));
Constant* one = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 1, true));
Constant* two = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 2, true));
Constant* three = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 3, true));
// types
IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);
ArrayType* int_3 = llvm::ArrayType::get(i32, 3);
ArrayType* int_3_3 = llvm::ArrayType::get(int_3, 3);
Type* eleTy = llvm::Type::getInt32Ty(M.getContext());
// get the ptr of a[1]*
Value* indexList[2] = {zero, one};
Value* a_1_ptr = Builder.CreateInBoundsGEP(int_3_3, arrVal, indexList);
// get the ptr of a[1][2]
Value* indexList[1] = two;
Value* ele_ptr = Builder.CreateInBoundsGEP(int_3_3, a_1_ptr, indexList);
// get the value of a[1][2]
Value* ele_val = Builder.CreateAlignedLoad(ele_ptr, 4);

如果是更高维的数组结构,则仿照上述过程依次提取每一层的指针再传入下一层索引值,重复这个过程直到最后一层取得被访问元素的内容。

结构体

结构体的处理方法也基本一致,稍微复杂的情况是结构体内部嵌套其他复杂数据结构,假设有结构体如下:

struct S{
	int a[3];
	float b;
};

struct S ss = {{1,2,3}, 2.0};

假设要访问结构体的成员ss.a[1]数值,则取地址访问数值的顺序为:

  1. index[2] = {0, 0},a_3_arrPtr = CreateInBoundsGEP(StructType, ssPtr, index)
  2. index[2] = {0, 1},a_3_elePtr = CreateInBoundsGEP(Int_3, a_3_arrPtr, index)
  3. 取值,a_3_eleVal = CreateAlignedLoad(a_3_elePtr, 4)

3.5 创建、使用全局变量

3.5.1 创建

假设要创建全局变量如下:

int a;
float b;
int c[10] = {0};
char d[] = "abc";

则在代码中可使用如下语句进行创建:

// types
IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);
Type* floatTy = llvm::Type::getFloatTy(M.getContext());
ArrayType* int_10 = llvm::ArrayType::get(i32, 10);
// var names
Twine * varAName = new Twine("a");
Twine * varBName = new Twine("b");
Twine * varCName = new Twine("c");
Twine * varDName = new Twine("d");
// initial values
Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
Constant* ten = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 10, true));
// create global vars
llvm::IRBuilder<> Builder(module.getContext());
GlobalVariable * a = new GlobalVariable(M, i32, false,
          GlobalValue::CommonLinkage, nullptr,
          *varAName);
GlobalVariable * b = new GlobalVariable(M, floatTy, false,
          GlobalValue::CommonLinkage, nullptr,
          *varBName);
GlobalVariable * c = new GlobalVariable(M, int_10, false,
          GlobalValue::CommonLinkage, nullptr,
          *varCName);
ConstantAggregateZero* const_zero = ConstantAggregateZero::get(int_10);
c->setInitializer(const_zero);
Value* d = Builder.CreateGlobalStringPtr(StringRef("abc"), varDName);

3.5.2 使用

对上述存放全局变量对象的Pass模块变量使用Load操作加载其对应内容即可取得全局变量的值对象,全局数组元素值的取用仍然采用GEP指令即可实现,此处不再赘述:

llvm::IRBuilder<> Builder(module.getContext());
Value* aVal = Builder.CreateAlignedLoad((Value *)a, 4);
Value* bVal = Builder.CreateAlignedLoad((Value *)b, 4);

3.6 for 循环结构的实现

由前面foo函数移植示例可知,for循环结构会被clang在IR层处理成三段式BB块结构,基本跳转关系如下图所示,其中For.Body BB位置可能不止一个BB块,内部结构由循环体内部代码控制:
在这里插入图片描述
因此移植for循环体时至少要先在Function内依次开辟3个BB块,然后再在BB块内植入具体的每条Instruction,此过程的流程仍然遵循开头提到的移植流程,这里给出一份主要语句编写的示例代码:

Twine* forLoopCondBBName = new Twine("forLoopCondBB");
Twine* forLoopBodyBBName = new Twine("forLoopBodyBB");
Twine* forLoopIncBBName = new Twine("forLoopIncBB");
Twine* xxxBBName = new Twine("xxxBB");
// 依次开辟 for 循环体所需要的BB块,xxxBB只作为示范,无实际意义
BasicBlock* forLoopCondBB = llvm::BasicBlock::Create(M.getContext(), *forLoopCondBBName,fooFuncPtr);
BasicBlock* forLoopBodyBB = llvm::BasicBlock::Create(M.getContext(), *forLoopBodyBBName, fooFuncPtr);
BasicBlock* forLoopIncBB = llvm::BasicBlock::Create(M.getContext(), *forLoopIncBBName, fooFuncPtr);
BasicBlock* xxxBB = llvm::BasicBlock::Create(M.getContext(), *xxxBBName, fooFuncPtr);
// 开始给各个BB块植入指令
// forLoopCondBB
Builder.SetInsertPoint(forLoopCondBB);
...
BrCond = ...
Builder.CreateCondBr(BrCond, forLoopBodyBB, xxxBB);

// forLoopBodyBB
Builder.SetInsertPoint(forLoopBodyBB);
....
Builder.CreateBr(forLoopIncBB);

// forLoopIncBB
Builder.SetInsertPoint(forLoopIncBB);
...
Builder.CreateBr(forLoopCondBB);

3.7 while 循环结构的实现

while循环又分为while(…){…}do{…}while(…);两类,这两类经clang处理后得到的BB块结构有所差异。假设有数组循环加法运算代码如下:

// while{...} loop
void foo1(int* a, int len) {
    int i = 0;
    int sum = 0;
    while (i<len) {
        sum += a[i];
        ++i;
    } 
}

// do{...}while loop
void foo2(int* a, int len) {
    int i = 0;
    int sum = 0;
    do {
        sum += a[i];
        ++i;
    } while (i<len);
}

对于while(…){…},其BB块结构如下:
在这里插入图片描述
编写while(…){…}的主体代码如下:

Twine* whileLoopCondBBName = new Twine("whileLoopCondBB");
Twine* whileLoopBodyBBName = new Twine("whileLoopBodyBB");
Twine* xxxBBName = new Twine("xxxBB");

BasicBlock* whileLoopCondBB = llvm::BasicBlock::Create(M.getContext(), *whileLoopCondBBName,fooFuncPtr);
BasicBlock* whileLoopBodyBB = llvm::BasicBlock::Create(M.getContext(), *whileLoopBodyBBName, fooFuncPtr);
BasicBlock* xxxBB = llvm::BasicBlock::Create(M.getContext(), *xxxBBName, fooFuncPtr);
// whileLoopCondBB
Builder.SetInsertPoint(whileLoopCondBB);
...
BrCond = ...
Builder.CreateCondBr(BrCond, whileLoopBodyBB, xxxBB);

// whileLoopBodyBB
Builder.SetInsertPoint(whileLoopBodyBB);
....
Builder.CreateBr(whileLoopCondBB);

对于do{…}while(…);,其BB块结构如下,与前面的不同就是循环体与判定部分的前后顺序进行了互换:
在这里插入图片描述
编写do{…}while(…);的主体代码如下:

Twine* xxxBBName = new Twine("xxxBB");
Twine* whileLoopBodyBBName = new Twine("whileLoopBodyBB");
Twine* whileLoopCondBBName = new Twine("whileLoopCondBB");
Twine* yyyBBName = new Twine("yyyBB");

BasicBlock* xxxBB = llvm::BasicBlock::Create(M.getContext(), *xxxBBName, fooFuncPtr);
BasicBlock* whileLoopBodyBB = llvm::BasicBlock::Create(M.getContext(), *whileLoopBodyBBName, fooFuncPtr);
BasicBlock* whileLoopCondBB = llvm::BasicBlock::Create(M.getContext(), *whileLoopCondBBName,fooFuncPtr);
BasicBlock* yyyBB = llvm::BasicBlock::Create(M.getContext(), *yyyBBName, fooFuncPtr);
// xxxBB
Builder.SetInsertPoint(xxxBB);
...
Builder.CreateBr(whileLoopBodyBB);
// whileLoopBodyBB
Builder.SetInsertPoint(whileLoopBodyBB);
....
Builder.CreateBr(whileLoopIncBB);
// whileLoopCondBB
Builder.SetInsertPoint(whileLoopCondBB);
...
BrCond = ...
Builder.CreateCondBr(BrCond, whileLoopBodyBB, yyyBB);

3.8 if else 结构的实现

假设有代码如下:

int cmp(int a, int b) {
	if (a > b)
		return 1;
	else if (a == b)
		return 0;
	else
		return -1;
}

其中BB块的结构大致如下,else if 的分支则相当于多个 if … else … 的组合嵌套:
在这里插入图片描述
编写if(…){…}else{…}主体代码如下:

Twine* ifCondBBName = new Twine("ifCondBB");
Twine* ifBodyBBName = new Twine("ifBodyBB");
Twine* elseBodyBBName = new Twine("elseBodyBB");
Twine* xxxBBName = new Twine("xxxBB");

BasicBlock* ifCondBB = llvm::BasicBlock::Create(M.getContext(), *ifCondBBName,fooFuncPtr);
BasicBlock* ifBodyBB = llvm::BasicBlock::Create(M.getContext(), *ifBBName, fooFuncPtr);
BasicBlock* elseBodyBB = llvm::BasicBlock::Create(M.getContext(), *elseBodyBBName, fooFuncPtr);
BasicBlock* xxxBB = llvm::BasicBlock::Create(M.getContext(), *xxxBBName, fooFuncPtr);

// ifCondBB
Builder.SetInsertPoint(ifCondBB);
...
BrCond = ...
Builder.CreateCondBr(BrCond, ifBodyBB, elseBodyBB);
// ifBodyBB
Builder.SetInsertPoint(ifBodyBB);
....
Builder.CreateBr(xxxBB);
// elseBodyBB
Builder.SetInsertPoint(elseBodyBB);
....
Builder.CreateBr(xxxBB);

3.9 调用函数

主要分为两步:

  1. 函数参数准备
  2. 创建调用指令

假设有调用语句如下

// int foo(int* a, int len)
sum = foo(a, 10);

3.9.1 参数准备

需要借助一个std::vector<Value*>容器收纳要传入函数的参数值,之后再依次将待传入的参数值添加进去,完成参数准备工作,示例代码如下:

std::vector<Value*> func_args;
Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
Constant* ten = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 10, true));
ArrayType* int_10 = llvm::ArrayType::get(i32, 10);
Value* indexList[2] = {zero, zero};
Value* seqAPtr = Builder.CreateInBoundsGEP(int_10, VarSeqA, indexList);
func_args.push_back(seqAPtr);
func_args.push_back(ten);

3.9.2 创建调用指令

调用IRBuilder.CreateCall(…)接口如下:

Function* fooFuncPtr = module.getOrInsertFunction("foo",...);
Value* sumVal = Builder.CreateCall(fooFuncPtr, func_args);

还有一种简便写法,直接使用{…}将待传入的参数值直接嵌套起来传入接口函数中:

Value* sumVal = Builder.CreateCall(fooFuncPtr, {seqAPtr, ten});

至此,函数调用的call指令创建完成。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
LLVM编写Backend Pass的详细教程: 1. 确定目标体系结构:首先要确定你要为哪个体系结构编写Backend PassLLVM支持多种体系结构,如x86、ARM等。对于每个目标体系结构,都需要编写对应的Backend Pass。 2. 理解传统的后端工作流程:了解传统的后端工作流程对于编写Backend Pass非常重要。这一流程涉及从LLVM IR生成目标体系结构的机器代码的各个阶段,包括指令选择、寄存器分配、指令调度、代码生成等。 3. 创建新的Backend Pass:在LLVM中,Backend Pass是通过继承MachineFunctionPass类来创建的。你需要为你的Backend Pass选择一个合适的名字,并在其中实现必要的功能。 4. 实现指令选择:指令选择是Backend Pass的第一个关键部分。在这个阶段,需要根据目标体系结构的特点,将LLVM IR中的指令转换为目标体系结构的机器指令。你可以使用TableGen来生成指令选择的描述文件,并使用这些描述文件来实现指令选择。 5. 实现寄存器分配:寄存器分配是生成机器代码的关键步骤之一。在这个阶段,需要为每个变量分配一个合适的寄存器。LLVM提供了许多寄存器分配算法和数据结构,你可以根据需要选择合适的算法来实现寄存器分配。 6. 实现指令调度:指令调度是优化生成的机器代码的一个重要步骤。在这个阶段,需要对生成的指令进行重新排序,以提高代码的性能。LLVM提供了一些指令调度的接口和算法,你可以使用它们来实现指令调度。 7. 实现代码生成:代码生成是Backend Pass的最后一步。在这个阶段,需要将LLVM IR转换为目标体系结构的机器代码,并生成可执行文件。你需要根据目标体系结构的特点来实现代码生成的功能。 以上是在LLVM编写Backend Pass的基本步骤和关键要点。在实现每个步骤时,需要对目标体系结构有一定的了解,并根据特定的需求选择合适的算法和数据结构。希望这些信息对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值