phasar LLVM静态分析框架使用

一、数据流分析

1.1 介绍

原理:数据流分析的原理可参考这些文章 Useful Literature

说明:phasar提供了很多复杂机制,以便于用户自定义数据流分析。用户针对自己的需求,选择最合适的接口,用户只需要提供一个新类,即可实现该接口缺少的功能,该功能可作为分析的问题描述。将具体问题描述交给求解器求解,以下将列出多种数据流求解器。

(1)选择控制流图

首先需选择一种控制流图,以基于此图进行分析,如CFG (intra-procedural control-flow graph) 或 ICFG (inter-procedural control-flow graph) 。例如,使用单调框架写过程内数据流分析时,用户需使用一种CFG实现或自己实现,也可以使用LLVM自带的LLVMBasedCFG.h

过程间call-string方法和IFDS/IDE/WPDS框架解决了基于过程间控制流图的具体分析。根据分析需要,可使用正向(LLVMBasedICFG)、反向(LLVMBasedBackwardICFG)或双向过程间控制流图ICFG。如果想自定义实现ICFG,则至少应实现ICFG.h定义的接口。

1.2 有用的代码风格

(1)std::algorithm

单调框架中,经常要覆写类来描述问题,这就要用到set操作(如set unionset intersectset difference)。自己实现这些函数很麻烦,可用到std::algorithm中已有的set操作函数:

    ...
    std::includes /* subset */
    std::set_difference
    std::set_intersection
    std::set_union
    ...

这些函数对iterators进行操作,例子:

std::set a = {1, 2, 3};
std::set b = {6, 4, 3, 2, 8, 1};
std::set c;
std::set_union(a.begin(), a.end(),
               b.begin(), b.end(),
               inserter(c, c.begin()));
bool isSubSet = std::includes(b.begin(), b.end(), a.begin(), a.end());
(2)预定义的流函数类

在IFDS/IDE/WPDS中定义流函数时,有时某个流函数会出现多次,例如,需多次调用Kill流函数来除去特定的data-flow fact。phasar有很多已有的流函数可以使用,如Identity流函数,有的函数可以完成call、returen点的参数映射,有的边函数可以用于IDE分析中。小例子:

// in this example let domain D be const llvm::Value*

shared_ptr> getNormalFlowFunction(....) {
    // check the type of the instruction
    if( ...) {
        // do some work
    } else if (...) {
        // kill some data-flow fact
        return make_shared>(/* some fact to be killed */);
    } else {
        // just treat everything else as Identity
        return Identity::getInstance();
    }
}
(3)重要的参数模板

data-flow solvers代码写的非常通用化,所以需要用到模板化参数。

  • Ddata-flow damain D中的data-flow facts的类型,通常为const llvm::Value *类型。
  • N:过程内/间控制流图中节点的类型(也即statements/instructions),通常是const llvm::Instruction *类型。
  • M:框架中函数的类型,通常是const llvm::Function *类型。
  • I:过曾经控制流图ICFG用到的类型,通常引用ICFG.h接口中的类型,如LLVMBasedICFG&
  • V/L:IDE问题的第2个值域的类型,该类型取决于具体是哪种分析,使用IFDS时该类型是默认的。
enum class BinaryDomain {
    BOTTOM = 0,
    TOP = 1
};

1.3 过程内的单调分析

参见Writing an intra procedural monotone framework analysis

过程内单调分析:过程内的数据流分析是最简单的分析,用户需实现IntraMonotoneProblem.h的接口,再交给相应的IntraMonotonSolver.h来求解分析问题。

过程间单调分析:使用call-strings来写过程间单调分析(待实现)

该solver使用传统的单调框架和call-string来实现k-context sensitivity(对复杂程序来说,k不宜过大)。用户需实现InterMonotoneProblem接口(包含比IntraMonotoneProblem.h多几个函数),用IFDS/IDE来形式化数据流问题。当然,使用IFDS/IDE的前提是该数据流问题满足分配性的(distributive),通常的Gen/Kill问题都满足可分配性,也有不满足的,如full-constant propagation。总之,如果data-flow fact的正确性同时取决于两个或多个data-flow fact,则流函数不满足可分配性。

1.4 写IFDS分析

参见Writing an IFDS analysis

说明:写IFDS分析只需实现一个类,该类需从DefaultIFDSTabulationProblemLLVMDefaultIFDSTabulationProblem继承。通过重写接口中的抽象函数来实现具体的分析,用户需重写的成员函数是:

  • getNormalFlowFunction():返回流函数(该流函数描述了过程内的流)。

  • getCallFlowFunction():描述了当求解器遇到call指令时该做什么,该流函数需将实参和形参映射起来。

  • getRetFlowFunction():将被调用函数的信息传递给调用者。

  • getCallToRetFlowFunction():处理函数调用中未涉及的信息。除了可能在被调用函数中被修改的指针参数,其他信息都作为标识传递。

  • 可选—getSummaryFlowFunction():返回空指针nullptr,可用于建模llvm.intrinsic函数或者你不想跟踪的libc函数。

  • initialSeeds():返回分析的起点和该点的data-flow fact集合,目标程序可以有多个分析起点,不过通常以main函数第1条指令为起点,且零初始化data-flow fact

  • createZeroValue():定义哪个值作为分析时使用的特殊”零“值。

  • Constructor:接收一个ICFG作为输入,接着,DefaultIFDSTabulationProblemConstructor会被调用,并初始化特殊的”零“值。例如:

      IFDSSolverTest::IFDSSolverTest(LLVMBasedICFG &I, vector EntryPoints)
        : DefaultIFDSTabulationProblem(I), EntryPoints(EntryPoints) {
        DefaultIFDSTabulationProblem::zerovalue = createZeroValue();
      }
    

内存管理:IFDS接口函数使用自定义的FlowFunctionPtrType作为返回类型。它包含一种内置的方法,可让框架对所有流函数进行内存管理。使用此内存管理前,需调用getFFMM().make_flow_function(FLOWFUNCTIONPARAMS)来构造新的流函数,这样该流函数就会被内存管理器所管理。对于特殊的单流函数(如Identity),就不需要了,使用静态成员函数getInstance()即可。

1.5 写IDE分析

参见Writing an IDE analysis

说明:写IDE分析也只需实现一个类,该类需继承DefaultIDETabulationProblemLLVMDefaultIDETabulationProblem。除了本文,还需要参考参考phasar内部关于IDE 数据流分析的代码,如IDELinearConstantAnalysis。用户需重写的函数如下:

  • getNormalFlowFunction()
  • getCallFlowFuntion()
  • getRetFlowFunction()
  • getCallToRetFlowFunction()
  • 可选—getSummaryFlowFunction()
  • initialSeeds()
  • topElement():A function that returns the top element of the underlying lattice that the analysis uses -> meaning ‘no information at all’
  • bottomElement():A function that returns the bottom element of the underlying lattice that the analysis is using -> meaning ‘all information’ (the most imprecise element in the lattice)
  • join():定义信息是如何join的(merge操作)。
  • allTopFunction():Function that returns the a special edge function allTop that is the EdgeFunction representation of the top value.
  • getNormalEdgeFunction():函数内的边函数。
  • getCallEdgeFunction():函数间调用边的传递函数,实参传递给形参。
  • getReturnEdgeFunction():被调用函数返回到调用点的边函数。
  • getCallToReturnEdgeFunction():从调用点到返回点(调用点的下一条指令)的边函数。
  • Constructor

内存管理:参考上一节IFDS分析。IDE接口函数使用EdgeFunctionPtrType作为返回类型。也是要使用getEFMM().make_edge_function(FLOWFUNCTIONPARAMS)来构造新的边函数,这样该函数就能被自带的内存管理器所管理。对于特殊的单流函数(如EdgeIdentity),就不需要了,使用静态成员函数getInstance()即可。

composeWithjoinwith:边函数的成员函数composeWithjoinwith都需要一个引用EdgeFunctionManager的参数,此参数用于给边函数提供内存管理。可以像getEFMM()结果一样使用MemoryManager引用,make_edge_function(FLOWFUNCTIONPARAMS)也可能直接在自定义的边函数中使用。

1.6 写WPDS分析

参见Writing a WPDS analysis

说明:WPDS是针对distributive data-flow问题,和IDE类似,WPDSSolverIDESolver的一种特殊实现。但WPDS没有使用用户的流和边函数实现来构造exploded super-graph,而是构建加权下推系统,其规则是从用户的分析描述中得出。每条规则都对应exploded super-graph中的一条边。使用通过post*或pre*算法获得的堆栈自动机解决下推式系统中的分析问题。对于该任务,我们使用WALi库的修改版本。WPDS有几种变体,如EWPDS、FWPDS、SWPDS。

为了编写WPDS分析,基本上需要编写一个IDE分析。 WPDS分析有自己的问题接口WPDSProblem,可用于编码WPDS问题。另外,现有的IDEProblem可以很容易地转换为WPDSProblem,因为两者都是distributive问题。可参考WPDSLinearConstantAnalysis,是如何将IDEProblem转换为WPDSProblem的。


二、全程序分析(使用WLLVM)

说明:对于复杂的C/C++项目,可使用WLLVM来生成全程序的LLVM bitcode文件。

编译步骤

$ sudo pip install wllvm
$ export LLVM_COMPILER=clang
# 修改编译器
$ CXX = wllvm++
$ set(CMAKE_C_COMPILER clang)
$ set(CMAKE_CXX_COMPILER clang++)

$ extract-bc main

你可能感兴趣的:(静态分析,软件分析,程序分析)