# [llvm] 函数插桩的两种方式

函数插桩指利用编译器，在程序中特定的位置插入函数调用。比如当我们希望统计某个函数运行所花的时间时，可以使用如下的Routine

```
void some_function(int arg1, int arg2)
{
  unsigned long long tik,tok;
  tik = gettimeofday();
  /*** do_something  ***/ 
  tok = gettimeofday();
  printf("elipsed %lld seconds\n", tok-tik);
}
``` 
其中增加了tik, tok两个变量用于记录函数开始与结束的时刻，而两次gettimeofday()函数的调用被插入到了原来的程序段前后，完成测量函数运行时间的目的。
这种做法适用于插入少量函数的情况，当大量位置需要插桩时就比较麻烦。当然，也可以将需要插桩的程序段当作函数封装起来，这样只需要修改库函数就可以。视情况而定，在库函数中插桩也不失为一种简便的方法。

然而，有时需要插桩的位置不是函数，或者这些位置之间没有什么共性不能抽象为函数：举例来说，只在函数名中带“test”的函数之前插入函数（C语言中如何检测函数名？）；或者在某个特定的指令之前插入函数。这些情况下，要如何继续插桩呢？

显然，我们需要一些比编程语言本身提供的范式更底层的结构才能做到，这就需要在编译器层面完成函数插桩。
## LLVM-IR的结构
Cmake
```
cmake_minimum_required(VERSION 3.10)
project(proj DESCRIPTION "manual build of LLVM IR")
set(CMAKE_BUILD_TYPE Debug)

set(LLVMLibPath "/home/zhao/Disk/build_llvm1405/lib")

function(find_and_add_library target lib path)
  find_library(var-${lib} ${lib} ${path})
  #message(STATUS "lib ${var-${lib}}")
  if(NOT var-${lib})
  message(FATAL_ERROR "cannot find ${lib} in path ${path}!")
  endif() 
  target_link_libraries(${target} PRIVATE ${var-${lib}})
endfunction()


add_executable(my_ir manual_ir.cpp)
target_include_directories(my_ir PRIVATE /home/zhao/Disk/llvm1405/llvm/include)
target_include_directories(my_ir PRIVATE /home/zhao/Disk/build_llvm1405/include)

list(APPEND compile_libs LLVMSupport LLVMIRReader LLVMMIRParser LLVMCore)
foreach(lib ${compile_libs})
find_and_add_library(my_ir ${lib} ${LLVMLibPath})
endforeach()
```
源码

```
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"

//#include "llvm/Analysis/Verifier.h"

#include "llvm/IR/CallingConv.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm-c/Core.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Bitcode/BitcodeReader.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include "llvm/Support/ToolOutputFile.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

LLVMModuleRef createModule()
{
  LLVMModuleRef mod = LLVMModuleCreateWithName("sum.ll");
  //LLVMSetDataLayout(mod, "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128");
  //LLVMSetTarget(mod, "x86_64-unknown-linux-gnu");

  LLVMSetDataLayout(mod,"e-m:e-p:64:64-i64:64-i128:128-n64-S128");
  LLVMSetTarget(mod,"riscv64");
  return mod;
}

void createFunction(Module *mod){
  LLVMContext &context = mod->getContext();
  FunctionType *FcType = FunctionType::get(Type::getInt32Ty(context),
  {Type::getInt32Ty(context),Type::getInt32Ty(context)},false);

  Function *funcSum = Function::Create(FcType, GlobalValue::ExternalLinkage,"sum",mod);
  funcSum->setCallingConv(CallingConv::C);

  //FunctionCallee FcCallee = mod->getOrInsertFunction("sum", FcType);
  Function::arg_iterator args = funcSum->arg_begin();
  Value *int32_a = args++;
  //int32_a->setName("a");
  Value *int32_b = args++;
  //int32_b->setName("b");

  BasicBlock *labelEntry = BasicBlock::Create(context, "entry", funcSum, 0);
  //IRBuilder<> builder(labelEntry);
  //builder.CreateAlloca();
  AllocaInst *ptrA = new AllocaInst(IntegerType::get(context,32),0, "addr.a",labelEntry);
  //ptrA->setAlignment(Align(8));
  AllocaInst *ptrB = new AllocaInst(IntegerType::get(context,32),0, "addr.b",labelEntry);
  //ptrB->setAlignment(Align(8));
  StoreInst *st0 = new StoreInst(int32_a, ptrA, false, labelEntry);
  //st0->setAlignment(Align(8));
  StoreInst *st1 = new StoreInst(int32_b, ptrB,false, Align(8), labelEntry);
  
  LoadInst *ld0 = new LoadInst(IntegerType::get(context,32), ptrA, "val_a", false, Align(8), labelEntry);
  LoadInst *ld1 = new LoadInst(IntegerType::get(context,32), ptrB, "val_b", false, Align(8), labelEntry);

  BinaryOperator *addRes = BinaryOperator::Create(Instruction::Add, ld0, ld1,"add", labelEntry);
  ReturnInst *ret = ReturnInst::Create(context, addRes,labelEntry);

}

int main()
{
  Module *mod = unwrap(createModule());
  createFunction(mod);
  mod->print(outs(),nullptr);
  //std::error_code EC;

  //WriteBitcodeToFile(*mod, raw_fd_ostream("sum.bc",EC));
  
  
  return 0;
}

```

## LLVM RTTI
runtime type identification
### 什么是RTTI——从C++的原生RTTI谈起
virtual function与RTTI，什么时候用哪个

## IR层插桩
LLVM-IR虽然是比源语言更底层的一种程序表示，但是它在设计上大量参考C语言，仍保留了编程语言中常见的程序流控制语法。在LLVM-IR的文本形式中，函数调用写作如下形式
```
; 类比为C语言中 int32_t ret = func_call(int32_t arg1, float arg2);
%ret = call i32 @func_call(i32 %arg1, float %arg2) 
```
 在LLVM-IR文本形式中，被调用的函数必须在同一文件中已经声明或定义，即必须存在如下两段代码之一，否则编译会出错。
 ```
 declare i32 @func_call(i32, float)
 ;或者
 define i32 @func_call(i32, float) 
 {
   %result = ...
   ret i32 result
 }
 ```

IR层的函数插桩，只不过是在特定位置增加这样一条函数调用，（以及它必要的声明或定义）而已。
> 复习一下，LLVM中程序嵌套结构依次为Module→Function→BasicBlock(→InstrBundle)→Instr

### 调用API
之前用LLVM-IR的文本形式示例只是为了方便理解。实际插桩时，是在LLVM-IR的内存形式中进行的。LLVM框架为我们提供了多种Pass接口，虽然这些Pass一般是用作实现优化的，但由于函数插桩的确改变了程序流以及数据流，也可以当作一个优化Pass实现。  
函数插桩的大致流程如下：
1. 遍历每一条Instr，检查是否为插桩的触发条件。至于什么是触发条件就由需求决定了，比如想在每一个函数之前插桩，只需要检查目前所在Instr是否为CallInstr；如果只想在函数名含有“test”的函数之前插桩，就还需要检查该CallInstr
2. 当条件成立时，创建一条新的CallInstr，赋予其合适的FunctionType、按上下文补齐参数。这就是一条合法的Instr了
3. 将新建的Instr插入当前所在Instr之前

TODO:LLVM-IR的内存表示
从上图中可以理解很重要的一点：LLVM-IR的内存表示有Type与Value两大类。前者指抽象的数据类型，后者指在内存中占据位置的“值”。像BasicBlock,Instrunction等由于都含有被处理的程序的数据因此它们都是值，而FunctionType只是一个函数的声明因此是类型。

核心的程序段示例如下
```
/* 实现一个FunctionPass 
bool runOnFunction(Function &F) override{
*/
LLVMContext &context = F.getParent()->getContext();
// 创建一个类型为 void (*func)(int32) 的函数声明
FunctionType *type = FunctionType::get(
  Type::getVoidTy(context),
  {Type::getInt32Ty(context)},
  false);
//给将要插入的函数命名，并声明为刚才定义的类型
FunctionCallee fc = F.getParent()->getOrInsertFunction("__inserted_func", type);
if(Function *fun = dyn_cast<Function>(fc.getCallee())){
  // 参数输入为32位整型常数42
  CallInst *inst = CallInst::Create(fun, {ConstanInt::get(Type::getInt32Ty(context),42,false)});
}
```
这个示例中最重要的是FunctionType::get()与CallInst::Create()两个API，它们都大量使用了Type::get...Ty(context)获取当前SubTarget中数据类型。
完整代码示例如下：
TODO

以上给出的代码片段实现了函数插桩Pass，但要让LLVM框架调用它，还需要增加如下代码
TODOC

## MI层插桩
在IR层插桩是非常方便的，因为LLVM-IR几乎就相当于不包含类型推导，流程控制只能靠goto的C语言，高级语言所有的函数封装、类型系统等在LLVM-IR中均存在等价语法。  
经过指令选择后，IR语言就变成了三地址形式的MachineInstruction，此时虽然还没有进行寄存器分配，程序的结构已经大致是汇编的形式，即线性的指令序列。而在MachineFunction中也不存在函数的概念了——和汇编一样，只是存放在某个特定位置的一段指令序列，函数调用也就变成了跳转到这段指令序列的入口。  

想要在MI层新增一次函数调用因此变得非常麻烦：如果函数有返回值，需要显式地写寄存器（即便在这个阶段物理寄存器还没开始分配，只需要使用虚拟寄存器）接受函数返回值的指令；如果函数有参数，同样需要写将参数传入寄存器的指令。而到底是从哪个寄存器取数据、往哪个寄存器存数据，这是由目标架构决定的，需要查阅手册；另外，用于传参的寄存器必须要提前lowering，只使用特定的几个寄存器（如在RISCV上，X1寄存器存放返回地址，X10、X11等寄存器存放参数），这又给代码引入了非常底层的信息，手敲底层汇编代码非常容易出错。

可能在MI层插桩比较好的场景只有`void (*f)(void)`类型的函数吧。这样只需要插入一句调用指令+标号就能完成。  
示例代码如下（RISCV后端）
```
// bool runOnMachineFunction(MachineFunction& MF)
MachineModuleInfo &MMI = MF.getMMI();
const Module *M = MMI.getModule();
auto *to_insert = M->getFunction("my_function");
if(nullptr = to_insert){
  errs()<<"there is no function has the name my_function\n";  
  return;
}
MachineInstrBuilder call = BuildMI(MBB, MI, MI.getDebugLoc(), ST.getInstrInfo()->get(RISCV::PseudoCALL));
call.addGlobalAddress(M->getNamedValue(to_insert->getName()),0,RISCVII::MO_CALL);
errs()<<"insert a call to function my_function\n";
```
这里要注意的是，为跳转指令增加标号应该使用addGlobalAddress()。
> 题外话，关于我曾经花了两天寻找PseudoCALL这条MachineInstr 后面接的MachineOperand的类型是哪个，就靠着读文档、望文生义，试过许多数据类型。比如ExternalSymbol，还有MCSymbol，然而没一条对的。生成的PseudoCALL 指令不能被后续的Assember（即llvm-mc）成功汇编。
> 最后是通过在整个LLVM源码目录中搜索创建PseudoCALL指令，找到了参考: 
> 源码在 llvm-project/llvm/lib/Target/RISCV/RISCVInstrInfo.cpp, line 1133, in RISCVInstrInfo::insertOutlinedCall
>```text
MBB.insert(It, BuildMI(MF,DebugLoc(),get(RISCV::PsuedoCALLReg),RISCV::X5)
.addGlobalAddress(M.getNamedValue(MF.getName(),0,RISCVII::MO_CALL));
```
> 看了这个例子，才知道CALL指令后面跟的标号的类型是GlobalAddress。回头一想又觉得很自然，是啊，跳转地址不是GlobalAddress还能是什么呢？然而，因为对LLVM-IR中各级结构的表示不熟悉，走了很多弯路。
> 最后我个人的体会就是: 百闻不如一见。在开发少量功能时，阅读llvm doxgen文档是一个快速的方法，但是当开发涉及多种LLVM结构体时，还是应该要先查阅一下别人是怎么写的，才能事半功倍。



