首页
论坛
课程
招聘
[原创]ollvm源码分析 - Pass之SplitBaiscBlocks
2019-10-17 21:21 4176

[原创]ollvm源码分析 - Pass之SplitBaiscBlocks

2019-10-17 21:21
4176

ollvm源码分析 - Pass之SplitBaiscBlocks

概述

  • SplitBasicBlocks是一个llvm体系下的标准Pass风格的代码处理组件,继承自FunctionPass。 根据llvm的官方文档描述,这种Pass只会作用于Function这种粒度,也就是llvm在编译流程里面,只有对单个Function应用Pass逻辑的时候, 才会调用继承自FunctionPass的各种Pass(包括llvm自带的Pass和普通开发人员自定义的Pass), 来看下官方解释:

    All LLVM passes are subclasses of the Pass class, which implement functionality by overriding virtual methods inherited from Pass. Depending on how your pass works, you should inherit from the ModulePass , CallGraphSCCPass, FunctionPass , or LoopPass, or RegionPass, or BasicBlockPass classes, which gives the system more information about what your pass does, and how it can be combined with other passes. One of the main features of the LLVM Pass Framework is that it schedules passes to run in an efficient way based on the constraints that your pass meets (which are indicated by which class they derive from).

    我粗略的翻译一下:所有的LLVM Pass都是Pass这个类的子类,这些Pass继承自Pass类,然后通过实现Pass类的虚函数来实现自身的功能。如果你要自己实现一个Pass, 那么你要从哪个类继承呢?目前LLVM系统提供了 ModulePass、CallGraphSCCPass、FunctionPass、LoopPass和BasicBlocks这个基类可供使用,根据你想要实现的功能来选择从哪个类来继承。一旦你写好了自己的Pass, LLVM Pass Framework会以一种高效的方式在LLVM编译过程中在合适的时机调用你的Pass(具体的调用实际就完全取决于你继承的基类)

  • 上面说了SplitBasicBlocks是一个什么东西,那它的实际作用是什么呢?为什么要有SplitBasicBlocks这么一个Pass呢?

    • 要回答清楚这个问题,就必须要说到目前ollvm这个开源项目中的一个重要功能,代码流程平坦化
    • 这里我大致简述一下代码流程平坦化:

      1. 平坦化一句话概括就是重组原始代码执行流程,把原本易于阅读的代码流程重组成一个switch case形式的代码执行流程,大大降低代码的直观可读性
      2. 平坦化这么做的目的就是要大幅度降低代码的可读性,从而达到防破解的目的
      3. 下面我画一个粗略的图来形象的展示一下:
    • 了解了什么是流程平坦化之后,那我们来思考,按照图中左半部分标示出来的A B C三个代码块来看,如果我们把ABC三个代码快每个都再进行细粒度的切割,变成A1,A2...Ax, B1,B2,Bx, C1,C2,Cx这样,那再按照流程平坦化的逻辑去进行一次代码重组,重组之后的画面,大家可以脑补一下,重组之后的代码会充斥着switch case,而且case的顺序还是随机的,结果是大大的降低了整个代码逻辑的可读性。

    • 哈哈,希望到这里我已经很明白的解释了为什么需要SplitBasicBlocks这个东西了

源码分析

  • 上面废话了那么久,其实我只有一个目的,就是告诉你这个东西是有用的,值得你花一点时间来继续看下面的源码分析 ^<>^
  • 先来看一下SplitBasicBlocks的定义,来一个宏观印象
  •    29 namespace {
       30 struct SplitBasicBlock : public FunctionPass {
       31   static char ID; // Pass identification, replacement for typeid
       32   bool flag;
       33
       34   SplitBasicBlock() : FunctionPass(ID) {}
       35   SplitBasicBlock(bool flag) : FunctionPass(ID) {
       36
       37     this->flag = flag;
       38   }
       39
       40   bool runOnFunction(Function &F);
       41   void split(Function *f);
       42
       43   bool containsPHI(BasicBlock *b);
       44   void shuffle(std::vector<int> &vec);
       45 };
       46 }
    
    1. 按照前面的分析,这个SplitBasicBlock是对函数的代码块进行分割的,所以按照LLVM Pass Framework的要求,继承自 FunctionPass, 然后实现 bool runOnFunction(Function &F)这个虚函数,这样以来,当LLVM编译代码的过程中遇到需要处理一个函数的情况,就会调用SplitBasicBlock.runOnFunction函数,并把当前要处理的函数对象作为参数传入进来
  • 直接来看runOnFunction函数的实现吧:
  •    55 bool SplitBasicBlock::runOnFunction(Function &F) {
       56   // Check if the number of applications is correct
       57   if (!((SplitNum > 1) && (SplitNum <= 10))) {
       58     errs()<<"Split application basic block percentage\
       59             -split_num=x must be 1 < x <= 10";
       60     return false;
       61   }
       62
       63   Function *tmp = &F;
       64
       65   // Do we obfuscate
       66   if (toObfuscate(flag, tmp, "split")) {
       67     split(tmp);
       68     ++Split;
       69   }
       70
       71   return false;
       72 }
    
    1. 函数逻辑看起来很干净简单,判断从编译选项中传入进来的split_num参数的大小,这个参数是用来指定要把一个BasicBlock分割成几块,经过llvm框架解析之后会保存到第57行代码中所示的SplitNum这个变量里面,从目前的代码来看,这个值的有效区间是(1, 10], 左开右闭的一个区间,不满足的话就直接退出SplitBasicBlock这个Pass的逻辑
    2. 接着调用toObfuscate函数来判断当前函数是否满足其他的内置条件(内置条件的判断,待会儿再分析),如果条件都满足,调用split函数来进行实际的分割操作
  • 先分析主干逻辑,继续进入split函数看看具体干了啥
  •    74 void SplitBasicBlock::split(Function *f) {
       75   std::vector<BasicBlock *> origBB;
       76   int splitN = SplitNum;
       77
       78   // Save all basic blocks
       79   for (Function::iterator I = f->begin(), IE = f->end(); I != IE; ++I) {
       80     origBB.push_back(&*I);
       81   }
       82
       83   for (std::vector<BasicBlock *>::iterator I = origBB.begin(),
       84                                            IE = origBB.end();
       85        I != IE; ++I) {
       86     BasicBlock *curr = *I;
       87
       88     // 这里省略了一些细枝末节...
       98     // 这里开始,生成切割点
       99     // Generate splits point
      100     std::vector<int> test;
      101     for (unsigned i = 1; i < curr->size(); ++i) {
      102       test.push_back(i);
      103     }
      104
      105     // Shuffle
      106     if (test.size() != 1) {
      107       shuffle(test);
      108       std::sort(test.begin(), test.begin() + splitN);
      109     }
      110     //切割点生成完毕,下面开始切割
      111     // Split
      112     BasicBlock::iterator it = curr->begin();
      113     BasicBlock *toSplit = curr;
      114     int last = 0;
      115     for (int i = 0; i < splitN; ++i) {
      116       for (int j = 0; j < test[i] - last; ++j) {
      117         ++it;
      118       }
      119       last = test[i];
      120       if(toSplit->size() < 2)
      121         continue;
      122       toSplit = toSplit->splitBasicBlock(it, toSplit->getName() + ".split");
      123     }
      124
      125     ++Split;
      126   }
      127 }
    
    1. 为了捋清楚主干,我贴的代码里面省略了一些数据有效性检查的细枝末节,不影响核心逻辑分析
    2. 代码先是便利Function对象里包含的所有基本快,并且保存到一个vector里面,供后续的逻辑使用,这里有必要提一下llvm框架里面,模块、函数、指令之间的关系,这样就比较直观的理解这种类似的iterator的便利手法获取到的是什么内容了,还是用一个简单的图形来表示一下:
    3. Function对象里面包含了很多的BasicBlock,BasicBlock里面包含了很多的Instruction,所以反过来看,BasicBlock->getParent()就是Function, Instruction->getParent()就是BasicBlock了,后面的逻辑中就会看到这种用法
    4. 接着看从83行开始的这个大循环,先是生成一个切割点数组,具体作用就是作为后续进行实际切割是时候,告诉后面的切割逻辑,从第几个位置开始切割,然后这个小片段包含多少条指令
    5. 切割点生成算法也比较简单,假设需要生成x个切割点,那就先生成一个1,2,3,... n这样的数组,其中n是当前BasicBlock中包含的指令的条数,接着调用shuffle函数来对整个数组进行随机乱序,接着对前x个数值进行排序,后面也就使用前x个值作为有效切割点,看115行的for循环的结束条件就知道了
    6. 接下来就开始切割了,我用一个图表示下,现在假设生成的切割点数组如下:pt[2, 4, 5],根据这个切割点数组,下面代码块会这么切分:
    7. 知道了在什么地方进行切割,那就看具体是怎么切割, 关键代码就在这122行了toSplit = toSplit->splitBasicBlock(it, toSplit->getName() + ".split");
    8. 其实仔细观察这块代码会觉得比较奇怪,每次调用toSplit->splitBasicBlock的第一个参数it是来自第一个代码块的itrator, 为什么后续的新代码块的切割还可以使用这个迭代器作为切割点标记呢,这个迭代器不会失效吗?你看117行还在根据切割点进行迭代器操作呢?这个代码确实比较恶心,个人认为是比较不符合常规编程习惯的。真正的原因是splitBasicBlock内部会更改it这个迭代器的指向,每当一个块被分割完成(按照it指向的指令切割成2块),这个迭代器就被更新成了新代码块(第二块)的指令列表的begin(), 因此117行的操作才是正确的
  • 直接进入分割函数看看吧,BasicBlock::splitBasicBlock
  •   363 /// This splits a basic block into two at the specified
      364 /// instruction.  Note that all instructions BEFORE the specified iterator stay
      365 /// as part of the original basic block, an unconditional branch is added to
      366 /// the new BB, and the rest of the instructions in the BB are moved to the new
      367 /// BB, including the old terminator.  This invalidates the iterator.
      368 ///
      369 /// Note that this only works on well formed basic blocks (must have a
      370 /// terminator), and 'I' must not be the end of instruction list (which would
      371 /// cause a degenerate basic block to be formed, having a terminator inside of
      372 /// the basic block).
      373 ///
      374 BasicBlock *BasicBlock::splitBasicBlock(iterator I, const Twine &BBName) {
      375   assert(getTerminator() && "Can't use splitBasicBlock on degenerate BB!");
      376   assert(I != InstList.end() &&
      377          "Trying to get me to create degenerate basic block!");
      378
      379   BasicBlock *New = BasicBlock::Create(getContext(), BBName, getParent(),
      380                                        this->getNextNode());
      381
      382   // Save DebugLoc of split point before invalidating iterator.
      383   DebugLoc Loc = I->getDebugLoc();
      384   // Move all of the specified instructions from the original basic block into
      385   // the new basic block.
      386   New->getInstList().splice(New->end(), this->getInstList(), I, end());
      387
      388   // Add a branch instruction to the newly formed basic block.
      389   BranchInst *BI = BranchInst::Create(New, this);
      390   BI->setDebugLoc(Loc);
      391
      392   // Now we must loop through all of the successors of the New block (which
      393   // _were_ the successors of the 'this' block), and update any PHI nodes in
      394   // successors.  If there were PHI nodes in the successors, then they need to
      395   // know that incoming branches will be from New, not from Old.
      396   //
      397   for (succ_iterator I = succ_begin(New), E = succ_end(New); I != E; ++I) {
      398     // Loop over any phi nodes in the basic block, updating the BB field of
      399     // incoming values...
      400     BasicBlock *Successor = *I;
      401     PHINode *PN;
      402     for (BasicBlock::iterator II = Successor->begin();
      403          (PN = dyn_cast<PHINode>(II)); ++II) {
      404       int IDX = PN->getBasicBlockIndex(this);
      405       while (IDX != -1) {
      406         PN->setIncomingBlock((unsigned)IDX, New);
      407         IDX = PN->getBasicBlockIndex(this);
      408       }
      409     }
      410   }
      411   return New;
      412 }
    
    1. 我们先来看下官方的函数注释吧,先了解下这个函数大致是什么,按照官方的说法,这个函数根据传入的第一个参数iterator I作为切割点,所有在在切割点之前的指令保留在原来的块中,所有剩余的指令包含切割点指令都放到新生成的代码块里面,并且会在原来代码块的最后添加一条无条件跳转指令来跳转到新代码块,这样既做到了代码块切割,又做到了保持原有逻辑不变的效果
    2. 函数在379行 先是直接先创建一个新的BasicBlock, 这个BasicBlock的后续块就是原BasicBlock的nextNode, 这样就把新代码块和正确的后续块连接了起来
    3. 接着386行,根据切割点的位置,把原来代码块中的包含的指令列表中后半部分(以切割点为界限)移动到新代码块,同时更新切割点迭代器为新代码块指令表的开端(还记得前面提到的那个奇怪的代码写法吗?)
    4. 然后389行,创建一个无条件分支指令,也就是无条件跳转指令放到被切割之后形成的第一块代码块的最后,这个无条件跳转指令把新老的两块BasicBlock连接了起来
    5. 从392行开始,代码处理了一个叫PHI的东西,所以好好说下,什么是PHI?这里直接引用一篇博文来说明吧,为了防止博文地址失效,我把主要内容贴出来:

      博客地址:https://www.cnblogs.com/ilocker/p/4897325.html
      什么是 PHI node?
      所有 LLVM 指令都使用 SSA (Static Single Assignment,静态一次性赋值) 方式表示。意思是所有变量都只能被赋值一次,这样做主要是便于后期的代码优化。

           1 a = 1;
           2 if (v < 10)
           3     a = 2;
           4 b = a;
      

      假设 v 的值小于 10,变量 a 就要被赋值为 2,但 a 已经被赋值了一次,由于 SSA 性质的约束,只能赋值另外一个“a”。最后在给 b 赋值时,通过添加一个 PHI node,由其来决定选择哪个版本的 a 来给 b 赋值。

           1 a1 = 1;
           2 if (v < 10)
           3     a2 = 2;
           4 b = PHI(a1, a2);
      

      PHI node 根据控制流是从哪一个 block (“a1 = 1” or “a2 = 2”) 到达,来决定使用 a1 还是 a2 来给 b 赋值。
      这些 PHI node 必须在 IR 中显示创建,llvm 指令集中有对应的 phi 指令。

    6. 所以为了保证PHI逻辑正确性,需要遍历PHI,并修正来源块

总结

  • 按照 -mllvm -split_num=x 指定的参数值,把原BaiscBlock切割成x+1块,新老块之间用无条件跳转指令连起来
  • 来个直观的效果图对比下
  • 分割前:
  • 分割后:
  • 如有错误,欢迎批评指正哈~~

2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

最后于 2019-10-17 21:23 被freakish编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (8)
雪    币: 1506
活跃值: 活跃值 (2765)
能力值: ( LV13,RANK:420 )
在线值:
发帖
回帖
粉丝
xiaofu 活跃值 8 2019-10-17 22:16
2
0
6666
雪    币: 3434
活跃值: 活跃值 (3499)
能力值: ( LV9,RANK:181 )
在线值:
发帖
回帖
粉丝
nevinhappy 活跃值 2 2019-10-18 09:45
3
0
学习,666
雪    币: 402
活跃值: 活跃值 (148)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
lykseek 活跃值 2019-10-18 15:54
4
0
好厉害
雪    币: 14875
活跃值: 活跃值 (12974)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
有毒 活跃值 10 2019-10-18 16:25
5
0
厉害厉害,正需要。老哥要不要考虑出个系列
雪    币: 1192
活跃值: 活跃值 (468)
能力值: ( LV8,RANK:150 )
在线值:
发帖
回帖
粉丝
freakish 活跃值 1 2019-10-18 18:28
6
0
有毒 厉害厉害,正需要。老哥要不要考虑出个系列
怎么个系列法? flatten 正在写,Sub 请参看 https://blog.csdn.net/freakishfox/article/details/102627536
雪    币: 115
活跃值: 活跃值 (35)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
dalerkd 活跃值 1 2019-10-21 15:58
7
0
支持大佬的知识分享
雪    币: 1638
活跃值: 活跃值 (964)
能力值: ( LV6,RANK:87 )
在线值:
发帖
回帖
粉丝
洋洋不得意 活跃值 1 2019-10-21 19:00
8
0
有没有人遇到 编译加上 -bcf clang就像假死一样,cpu占用满, 像是死循环了
雪    币: 1192
活跃值: 活跃值 (468)
能力值: ( LV8,RANK:150 )
在线值:
发帖
回帖
粉丝
freakish 活跃值 1 2019-10-21 22:16
9
0
ollvm有bug很正常
游客
登录 | 注册 方可回帖
返回