原理:数据流分析的原理可参考这些文章 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
定义的接口。
std::algorithm
单调框架中,经常要覆写类来描述问题,这就要用到set操作(如set union
、set intersect
、set 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());
在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();
}
}
data-flow solvers
代码写的非常通用化,所以需要用到模板化参数。
data-flow damain
D中的data-flow facts
的类型,通常为const llvm::Value *
类型。const llvm::Instruction *
类型。const llvm::Function *
类型。ICFG.h
接口中的类型,如LLVMBasedICFG&
。enum class BinaryDomain {
BOTTOM = 0,
TOP = 1
};
参见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
,则流函数不满足可分配性。
参见Writing an IFDS analysis
说明:写IFDS分析只需实现一个类,该类需从DefaultIFDSTabulationProblem
或LLVMDefaultIFDSTabulationProblem
继承。通过重写接口中的抽象函数来实现具体的分析,用户需重写的成员函数是:
getNormalFlowFunction()
:返回流函数(该流函数描述了过程内的流)。
getCallFlowFunction()
:描述了当求解器遇到call指令时该做什么,该流函数需将实参和形参映射起来。
getRetFlowFunction()
:将被调用函数的信息传递给调用者。
getCallToRetFlowFunction()
:处理函数调用中未涉及的信息。除了可能在被调用函数中被修改的指针参数,其他信息都作为标识传递。
可选—getSummaryFlowFunction()
:返回空指针nullptr,可用于建模llvm.intrinsic
函数或者你不想跟踪的libc
函数。
initialSeeds()
:返回分析的起点和该点的data-flow fact
集合,目标程序可以有多个分析起点,不过通常以main函数第1条指令为起点,且零初始化data-flow fact
。
createZeroValue()
:定义哪个值作为分析时使用的特殊”零“值。
Constructor
:接收一个ICFG作为输入,接着,DefaultIFDSTabulationProblem
的Constructor
会被调用,并初始化特殊的”零“值。例如:
IFDSSolverTest::IFDSSolverTest(LLVMBasedICFG &I, vector EntryPoints)
: DefaultIFDSTabulationProblem(I), EntryPoints(EntryPoints) {
DefaultIFDSTabulationProblem::zerovalue = createZeroValue();
}
内存管理:IFDS接口函数使用自定义的FlowFunctionPtrType
作为返回类型。它包含一种内置的方法,可让框架对所有流函数进行内存管理。使用此内存管理前,需调用getFFMM().make_flow_function
来构造新的流函数,这样该流函数就会被内存管理器所管理。对于特殊的单流函数(如Identity
),就不需要了,使用静态成员函数getInstance()
即可。
参见Writing an IDE analysis
说明:写IDE分析也只需实现一个类,该类需继承DefaultIDETabulationProblem
或LLVMDefaultIDETabulationProblem
。除了本文,还需要参考参考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()
:从调用点到返回点(调用点的下一条指令)的边函数。内存管理:参考上一节IFDS分析。IDE接口函数使用EdgeFunctionPtrType
作为返回类型。也是要使用getEFMM().make_edge_function
来构造新的边函数,这样该函数就能被自带的内存管理器所管理。对于特殊的单流函数(如EdgeIdentity
),就不需要了,使用静态成员函数getInstance()
即可。
composeWith
与joinwith
:边函数的成员函数composeWith
和joinwith
都需要一个引用EdgeFunctionManager
的参数,此参数用于给边函数提供内存管理。可以像getEFMM()
结果一样使用MemoryManager
引用,make_edge_function
也可能直接在自定义的边函数中使用。
参见Writing a WPDS analysis
说明:WPDS是针对distributive data-flow
问题,和IDE类似,WPDSSolver
是IDESolver
的一种特殊实现。但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
的。
说明:对于复杂的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