编写LLVM Pass模块知识点梳理(二)

在上一篇博客中,以foo函数为例演示了如何编写Pass模块代码向一个Module植入所需的自定义函数“foo”,同时还留一些问题有待进一步探讨,本文将作为对上一篇博客的补充来介绍作者对这些情景的一些处理方法。

一、怎样调用植入的函数?

1.1 问题背景

在上一篇博客中,我们在Pass模块的void CreateFooFunc(Module &M) {…}函数中写入了完整的foo函数代码,但是有个比较棘手的问题,在CreateFooFunc()函数中创建的Function*对象作用域也只能是在CreateFooFunc()函数内部,那么在Pass模块的runOnFunction(){…}接口(或虚函数)内部该如何引用创建过的Function*对象呢?此外,还有为了辅助函数调用过程在Function首个BB块内考虑植入局部变量,这部分变量该如何植入并在后续位置调用?这是我们需要考虑的地方。

1.2 一种解决方案

由于自定义Pass模块是通过C++的类继承方式来实现,开发者当然可以通过在Pass类中定义Function*和Value*类成员来存储各种类方法中创建的Function*和Value*对象,再由其他类方法来访问这些成员变量,但需要注意的是,一旦创建的变量过多,这会令程序的编写非常复杂。

这里提供一种简单的Name-Value映射机制,如果读者之前仔细阅读过LLVM官方在线文档的自定义Kaleidoscope(万花筒)语言前端解析示例代码,那么对这个NameValues的映射机制可能还有印象,这里我同样模仿示例中的实现方法,在Pass模块类中定义若干map<>NameValues成员来辅助完成变量的跨函数引用。

代码示例如下

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
    // a new member: NameFuncs
	std::map<std::string, Function*> NameFuncs;
    void CreateFooFunc(Module &M){
        // write something here
        ...
    }

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

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

其中,NameFuncs负责存储函数命名字符串到Function*对象的映射,针对局部变量也可以按照类似方式处理,在下文中继续介绍。

有了这个map成员,那么我们在创建foo函数时就需要及时将生成的Function*对象存入map中,对上一篇博客中的create代码进行适当调整如下:

void CreateFooFunc(Module &M) {
	...
	//===--- 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 should use the NameFuncs map to store the foo func ptr as a member of map<>.
	NameFuncs.insert(make_pair(funcNameStr, fooFuncPtr));
	...
}

增加了一条NameFuncs.insert(…)语句即可完成Function*指针的存储,这样一来,在runOnFunction()接口中需要调用自定义的foo函数时,只需从NameFuncs中读取即可:

bool runOnFunction(Function &F) {
        ...
        std::string funcNameStr = "foo";
        Function* fooFuncPtr = NameFuncs.find(funcNameStr)->second;
        ...
}

1.3 注意!命名混淆机制

上述实现方式在处理单个Module时表现尚可,但需要注意,这是向一个Module中植入了一个foo函数,实际中许多开发项目都是由许多拆分开的源代码文件构成,那么Pass模块也就会在不同Module之间被启用,导致不同Module内都被植入了一个命名为“foo”的函数,最终各个Module在链接阶段就会发现有若干同名函数被反复定义产生编译报错。这个现象也是使用DLL时不会遇到的。

C++类方法的重载能力在底层通过命名混淆(name mangling)来实现,但C++语法规范并未规定编译器应该具体以怎样的混淆规则实现,clang对同名方法的处理则是为各自的命名字符串加上不同的随机前缀与后缀字符串,以此形成唯一的一个命名字符串序列,C++的命名混淆机制使得每个类方法在整个程序中仍然获得了唯一的命名。

回到我们的foo函数上,这样一来就需要开发者考虑设计并实现一种命名混淆机制,主要方法是对原始“foo”命名字符串做微调,令其可在不同Module之间加以区分。这里提供一种作者在项目中使用的简单方式,就是统计一个Module中的BasicBlock数量,将这个数字作为函数命名字符串后缀拼接到原始的“foo”之后(放到开头也可以),相当于是用这个数量值作为不同Module的区分标识加到“foo”上,以确保编译链接时函数命名的唯一性。

示例代码

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
	std::map<std::string, Function*> NameFuncs;
	std::map<std::string, std::string> RenamingList;
	int renameFlag;
    void CreateFooFunc(Module &M){
        // write something here
        ...
    }
    virtual bool doInitialization(Module &M){
		renameFlag = 0;
    	Function* func_tmp = &(*M.getFunctionList().begin());
    	for (Module::iterator m_itr = M.begin(), m_itr_end = M.end(); m_itr != m_itr_end; m_itr++)
    		for (Function::iterator f_itr = m_itr->begin(), f_itr_end = m_itr->end(); f_itr!=f_itr_end; f_itr++)
        		++renameFlag;
        CreateFooFunc();
        return true;
    }

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

注意到,这里在自定义Pass中新增了一个renameFlag成员与RenamingList,并在doInitialization()接口中编写了双层for循环通过Module与Function内置迭代器统计遍历到的BasicBlock数量,结果都记录在renameFlag中。RenamingList负责存储从原始。之后,我们对CreateFooFunc()内部植入部分代码进行调整如下:

void CreateFooFunc(Module &M) {
	...
	//===--- create Function* object for foo func ---===
	// create name string "foo"
	std::ostringstream oss;
	// attention, the suffix renameFlag was appended to the oss object
	oss << "foo" << renameFlag;
	std::string funcNameStr = oss.str();
	// the RenameList map<> is used here
	RenamingList.insert(make_pair("foo", funcNameStr));
	// create func ptr
	Function* fooFuncPtr = Function::Create(funcDef, Function::ExternalLinkage, funcNameStr, &M);
	NameFuncs.insert(make_pair(funcNameStr, fooFuncPtr));
	...
}

可以看到通过std::ostringstream oss对象完成了命名字符串后缀的拼接,并用于创建Function*对象,同时通过RenameList将开发者预设的“foo”与实际使用的“fooxxx”建立起映射,这样一来编程时只需要通过map映射就能得到实际使用的命名字符串,再通过实际命名字符串在NameFuncs中找到对应的Function*对象,这种二次索引的方式无需开发者自行记忆大量重命名后的函数名。

与之对应的,调取foo函数时也就多了一次索引,示例代码如下:

bool runOnFunction(Function &F) {
        ...
        std::string funcNameStr = "foo";
        Function* fooFuncPtr = NameFuncs.find(RenamingList.find(funcNameStr)->second)->second;
        ...
}

1.4 令runOnFunction()跳过自定义Function

那么问题又来了,我们在doInitialization()接口对原始Module植入了foo函数,但是在Pass模块的runOnFunction()接口执行时,会遍历Module下的每个Function并执行其中的代码。比如在编写代码混淆工具时,我们不会希望植入的自定义函数在runOnFunction()环节又被其中的代码调用自定义函数对自身加以“处理”,这样会陷入“套娃”的情况。所以,有必要令runOnFunction()函数跳过处理自定义函数,只作用于原始的函数部分。

针对此问题,作者的处理方法比较简单,就是在Pass中添加一个std::vector<Function*> FuncsListForCheck成员,在创建自定义函数时,将创建后的函数对象存入该成员中,在runOnFunction()中进入混淆处理环节之前,先检查当前处理的Function是否位于该vector中,只受理不在vector中的情况。另外,为了简化代码编写,将这个检查过程也封装成一个Pass模块的方法供runOnFunction()调用。此时FooPass的主体结构示例如下:

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
	std::map<std::string, Function*> NameFuncs;
	std::map<std::string, std::string> RenamingList;
	std::vector<Function*> FuncsListForCheck;
	int renameFlag;
	// a new function,  check whether the current function is in the custom function
	bool IsInCustomFuncList(Function& F){
		...
	}
    void CreateFooFunc(Module &M){
        ...
    }
    virtual bool doInitialization(Module &M){
		renameFlag = 0;
    	for (Module::iterator m_itr = M.begin(), m_itr_end = M.end(); m_itr != m_itr_end; m_itr++)
    		for (Function::iterator f_itr = m_itr->begin(), f_itr_end = m_itr->end(); f_itr!=f_itr_end; f_itr++)
        		++renameFlag;
        CreateFooFunc();
        return true;
    }

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

可以看到新增了成员变量std::vector<Function*> FuncsListForCheck与类方法bool IsInCustomFuncList(Function& F){…},这里先演示使用FuncsListForCheck记录创建的自定义函数对象:

void CreateFooFunc(Module &M) {
	...
	//===--- create Function* object for foo func ---===
	// create name string "foo"
	std::ostringstream oss;
	oss << "foo" << renameFlag;
	std::string funcNameStr = oss.str();
	RenamingList.insert(make_pair("foo", funcNameStr));
	// create func ptr
	Function* fooFuncPtr = Function::Create(funcDef, Function::ExternalLinkage, funcNameStr, &M);
	NameFuncs.insert(make_pair(funcNameStr, fooFuncPtr));
	
	// the FuncsListForCheck vector<> is used here
	FuncsListForCheck.push_back(DefOfCurFunc);
	...
}

一份IsInCustomFuncList()函数示例代码如下:

bool IsInCustomFuncList(Function& F){
      std::vector<Function*>::iterator func_itr = FuncsListForCheck.begin();
      for(; func_itr != FuncsListForCheck.end(); func_itr++){
        if (*func_itr == &F)
          return true;
      }
      return false;
}

完成上述代码后,再在runOnFunction()中对当前处理的Function*对象先添加一个简单的if判断即可:

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
	std::map<std::string, Function*> NameFuncs;
	std::map<std::string, std::string> RenamingList;
	std::vector<Function*> FuncsListForCheck;
	int renameFlag;
	// a new function,  check whether the current function is in the custom function
	bool IsInCustomFuncList(Function& F){
		...
	}
    void CreateFooFunc(Module &M){
        ...
    }
    virtual bool doInitialization(Module &M){
		...
        return true;
    }

    bool runOnFunction(Function &F) {
        if (!IsInCustomFuncList(F)){
        	// means that the current Function object is not one of the custom funcs
        	...
        }
        return false;
    }
}
}; 

至此,解决上述若干问题后,才算是建立起了自定义Function的创建、使用机制。

二、怎样在函数中植入自定义局部变量并使用?

正如标题所示,这里将探讨如何植入并使用自定义局部变量。首先要明确的是,这里仍然是针对原始程序中的Function来植入自定义局部变量,需要将前面自定义Function排除掉,不过好在上面已经介绍了一种跳过自定义Function的处理方法,这里直接关注局部变量的创建即可。

之前谈到过,创建局部变量的alloca指令应该放在Function的首个BB块内,但用于赋值的store指令无此要求,这里植入自定义局部变量的大致步骤如下:

  1. 定位到当前Function的首个BB块;
  2. 创建所需变量的alloca语句;
  3. 建立针对局部变量的内部索引机制;

2.1 定位到首个BB块

首先,这是针对当前待处理的原始Function,那么应该在Pass模块的runOnFunction()接口方法中来进行此操作,实现对每个原始Function逐个遍历并植入所需局部变量。

由于Function有内置迭代器成员可以对当前Function下所有BB块进行遍历,那么定位到首个BB块也就比较简单。需要注意的是,通过IRBuilder向BB块植入指令时,默认将新指令添加到BB块的末尾而非开头,这很可能会令BB块末端原始的终止符指令变成新植入的alloca指令,为了避免这个现象考虑将alloca指令的植入点定位到首个BB块的开头,示例如下:

bool runOnFunction(Function &F) {
	if (!IsInCustomFuncList(F)){
    	// means that the current Function object is not one of the custom funcs
       	...
       	// place the insert point at the first inst of the first BB in a func
       	Function::iterator fi = F.begin();
       	BasicBlock* firstBB = &(*fi);
       	Instruction* firstInst = &(*(firstBB->begin()));
       	llvm::IRBuilder<> Builder(firstBB->getContext());
        Builder.SetInsertPoint(firstIns);
        ...
    }
	return false;
}

根据LLVM的在线手册可知,上述操作已将IRBuilder对象的指令插入点定位到了首个BB块的首个Instruction之前,即后续通过IRBuilder创建的alloca指令都会依次位于原始首条Instruction之前。

为了简化描述,这里我们假设需要向原始Function中植入一个局部变量int a,且初始值为0。clang一般会在IR层将int类型表示成i32,那么还需要为这个局部变量准备对应的类型表示与初始值表示:

bool runOnFunction(Function &F) {
	if (!IsInCustomFuncList(F)){
       	// means that the current Function object is not one of the custom funcs
       	...
       	
       	// prepare the essential type and constant value objects for var int a
		IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);
		Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
		
       	// place the insert point at the first inst of the first BB in a func
      	Function::iterator fi = F.begin();
      	BasicBlock* firstBB = &(*fi);
      	Instruction* firstInst = &(*(firstBB->begin()));
     	llvm::IRBuilder<> Builder(firstBB->getContext());
    	Builder.SetInsertPoint(firstIns);
    	...
	}
	return false;
}

2.2 创建alloca语句

完成准备之后,就可以着手向BB块中植入alloca与store指令,直接调用IRBuilder的相关create方法即可,过程较为简单:

bool runOnFunction(Function &F) {
	if (!IsInCustomFuncList(F)){
       	// means that the current Function object is not one of the custom funcs
       	...
       	
       	// prepare the essential type and constant value objects for var int a
		IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);
		Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
		
       	// place the insert point at the first inst of the first BB in a func
      	Function::iterator fi = F.begin();
      	BasicBlock* firstBB = &(*fi);
      	Instruction* firstInst = &(*(firstBB->begin()));
     	llvm::IRBuilder<> Builder(firstBB->getContext());
    	Builder.SetInsertPoint(firstIns);
    	
    	// start to create the alloca and store inst
    	AllocaInst* VarA = Builder.CreateAlloca(i32, nullptr, "a");
        VarA->setAlignment(4);
        Builder.CreateAlignedStore(zero, VarA, 4);
        ...
        return true;
	}
	return false;
}

2.3 建立局部变量的内部索引机制

方式仍然是参照Name-Value机制来建立映射,这里在Pass模块中新建FuncNameValues成员与NameValues成员,注意NameValues是一个指针变量,这两个成员协作构建起变量的内部索引机制:

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
    std::map<Function*, std::map<std::string, Value*>*> FuncNameValues;
	std::map<std::string, Value*>* NameValues;
	std::map<std::string, Function*> NameFuncs;

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

其中NameValues负责存储局部变量命名字符串到Value*对象的映射,FuncNameValues则在NameValues的基础上又新增一层Function索引。

类似地,在完成局部Value*对象创建后,及时存入上述变量,到需要使用的环节在从中取出即可:

namespace {
struct FooPass : public FunctionPass {
    static char ID;
    FooPass() : ModulePass(ID) {}
    std::map<Function*, std::map<std::string, Value*>*> FuncNameValues;
	std::map<std::string, Value*>* NameValues;
	std::map<std::string, Function*> NameFuncs;

    bool runOnFunction(Function &F) {
    	if (!IsInCustomFuncList(F)){
	       	// means that the current Function object is not one of the custom funcs
	       	...
	       	
	       	// prepare the essential type and constant value objects for var int a
			IntegerType* i32 = llvm::IntegerType::get(M.getContext(), 32);
			Constant* zero = llvm::ConstantInt::get(M.getContext(), llvm::APInt(32, 0, true));
			
	       	// place the insert point at the first inst of the first BB in a func
	      	Function::iterator fi = F.begin();
	      	BasicBlock* firstBB = &(*fi);
	      	Instruction* firstInst = &(*(firstBB->begin()));
	     	llvm::IRBuilder<> Builder(firstBB->getContext());
	    	Builder.SetInsertPoint(firstIns);
	    	
	    	// start to create the alloca and store inst
	    	AllocaInst* VarA = Builder.CreateAlloca(i32, nullptr, "a");
	        VarA->setAlignment(4);
	        Builder.CreateAlignedStore(zero, VarA, 4);
	        
			// establish the map<> index
	        NameValues->insert(make_pair("a", VarA));
	        FuncNameValues.insert(make_pair(&F, NameValues));
	        ...
	        return true;
		} else {
			...
			// later, we need to use the var int a here
			NameValues = FuncNameValues.find(&F)->second;
			Value* VarA = (NameValues->find("a"))->second;
			...
		}
        return false;
    }
}
}; 

至此,自定义局部变量映射机制的建立也演示完成。


版权声明:本文为u010940020原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。