指针分析
一.前言
1.1.指针分析
指针分析是数据流分析的一种,主要目的是计算运行时指针可能指向的内存区域。比如
p = &a;
q = p;
则 p t s ( p ) = p t s ( q ) = { a } pts(p) = pts(q) = \{a\} pts(p)=pts(q)={a} 。
指针分析可以应用于:
-
建立变量之间的数据依赖关系。
-
变量别名分析:在下面代码中
p = &a; q = p; *p = x; y = *q;
。因为p
、q
都指向同一块内存,所以y
的值和x
一样。 -
编译优化和bug检测:
- 常量传播:
*p = 1; x = *q;
中,如果p
和q
在任意情况下都是别名(must-aliases,在每个执行路径p
和q
都指向同一块内存)那么x
就是常量1
。 - 污点分析:
*p = taintInput; x = *q;
如果p
、q
是别名那么x
可能受污点影响。
- 常量传播:
和其它程序分析技术一样,指针分析也可分为:
-
flow-insensitive 和 flow-sensitive
-
context-insensitive 和 context-sensitive
-
path-insensitive 和 path-sensitive
1.2.SVF
SVF是UTS Sui老师开发的一个基于LLVM的针对LLVM IR的程序静态分析工具。可以对
-
基于LLVM IR的语言进行过程间依赖分析。
-
执行指针别名分析。
-
value-flow追踪等。
详细介绍可参考SVF-概述。
SVF中已经实现了一些AnderSen算法的变体,这里我关注最简单、原始版本的AnderSen算法。这里的内容参考了Sui老师的课件[2]。
二.AnderSen算法
2.1.AnderSen算法约束
这里我主要探究AnderSen算法,一个flow-insensitive、context-insensitive、path-insensitive的算法。主要分析的对象是LLVM IR(首先将C代码用clang编译成LLVM IR),基于SVF的API实现简单的AnderSen算法。
AnderSen算法的约束如下:
Constraint Type | Assignment | Constraint | Meaning |
---|---|---|---|
Base | a = &b | a ⊇ { b } a \supseteq \{b\} a⊇{b} | { b } ∈ p t s ( a ) \{b\} \in pts(a) {b}∈pts(a) |
Simple | a = b | a ⊇ b a \supseteq b a⊇b | p t s ( a ) ⊇ p t s ( b ) pts(a) \supseteq pts(b) pts(a)⊇pts(b) |
Complex | a = *b | a ⊇ ∗ b a \supseteq *b a⊇∗b | ∀ v ∈ p t s ( b ) , p t s ( a ) ⊇ p t s ( v ) \forall v \in pts(b), pts(a) \supseteq pts(v) ∀v∈pts(b),pts(a)⊇pts(v) |
Complex | *a = b | ∗ a ⊇ b *a \supseteq b ∗a⊇b | ∀ v ∈ p t s ( a ) , p t s ( v ) ⊇ p t s ( b ) \forall v \in pts(a), pts(v) \supseteq pts(b) ∀v∈pts(a),pts(v)⊇pts(b) |
而SVF中实现了多种AnderSen-style算法,而我目前实现的简易AnderSen算法只关注4种基本情况,对应LLVM IR种存在的4种约束。
Constraint Type | Assignment | LLVM IR | Constraint Rule |
---|---|---|---|
Address | a = &b | %a = alloca //b | p t s ( a ) = p t s ( a ) ∪ { b } pts(a) = pts(a) \cup \{b\} pts(a)=pts(a)∪{b} |
Copy | a = b | %a = bitcast %b | p t s ( a ) = p t s ( a ) ∪ p t s ( b ) pts(a) = pts(a) \cup pts(b) pts(a)=pts(a)∪pts(b) |
Load | a = *b | %a = load %b | ∀ v ∈ p t s ( b ) , p t s ( a ) = p t s ( a ) ∪ p t s ( v ) \forall v \in pts(b), pts(a) = pts(a) \cup pts(v) ∀v∈pts(b),pts(a)=pts(a)∪pts(v) |
Store | *a = b | store %b, %a | ∀ v ∈ p t s ( a ) , p t s ( v ) = p t s ( v ) ∪ p t s ( b ) \forall v \in pts(a), pts(v) = pts(v) \cup pts(b) ∀v∈pts(a),pts(v)=pts(v)∪pts(b) |
-
在执行AnderSen算法之前,需要将C语言代码编译成LLVM IR并通过SVF的API构造相应的Constraint Graph。
-
之后在Constraint Graph上执行AnderSen算法,在执行AnderSen算法的时候Constraint Graph会发生变动。
这里先从[1]中摘取AnderSen算法的流程
2.2.示例
2.2.1.将C语言源代码编译为SSA形式的LLVM IR
这里引用1个简单的例子,C语言代码如下(example.c
):
void swap(char **p, char **q){
char* t = *p;
*p = *q;
*q = t;
}
int main(){
char a1, b1;
char *a = &a1;
char *b = &b1;
swap(&a,&b);
}
然后分别用
-
clang -c -S -fno-discard-value-names -Xclang -disable-O0-optnone -emit-llvm example.c -o example.ll
保留变量名将C代码编译为LLVM IR。 -
opt -S -mem2reg example.ll -o example.ll
将LLVM IR转化为SSA形式的IR。
完成编译后SSA形式的LLVM IR如下:
define dso_local void @swap(i8** %p, i8** %q) #0 {
entry:
%0 = load i8*, i8** %p, align 8
%1 = load i8*, i8** %q, align 8
store i8* %1, i8** %p, align 8
store i8* %0, i8** %q, align 8
ret void
}
; Function Attrs: noinline nounwind uwtable
define dso_local i32 @main() #0 {
entry:
%a1 = alloca i8, align 1
%b1 = alloca i8, align 1
%a = alloca i8*, align 8
%b = alloca i8*, align 8
store i8* %a1, i8** %a, align 8
store i8* %b1, i8** %b, align 8
call void @swap(i8** %a, i8** %b)
ret i32 0
}
未转化成SSA的IR如下:
define dso_local void @swap(i8** %p, i8** %q) #0 {
entry:
%p.addr = alloca i8**, align 8
%q.addr = alloca i8**, align 8
%t = alloca i8*, align 8
store i8** %p, i8*** %p.addr, align 8
store i8** %q, i8*** %q.addr, align 8
%0 = load i8**, i8*** %p.addr, align 8
%1 = load i8*, i8** %0, align 8
store i8* %1, i8** %t, align 8
%2 = load i8**, i8*** %q.addr, align 8
%3 = load i8*, i8** %2, align 8
%4 = load i8**, i8*** %p.addr, align 8
store i8* %3, i8** %4, align 8
%5 = load i8*, i8** %t, align 8
%6 = load i8**, i8*** %q.addr, align 8
store i8* %5, i8** %6, align 8
ret void
}
; Function Attrs: noinline nounwind uwtable
define dso_local i32 @main() #0 {
entry:
%a1 = alloca i8, align 1
%b1 = alloca i8, align 1
%a = alloca i8*, align 8
%b = alloca i8*, align 8
store i8* %a1, i8** %a, align 8
store i8* %b1, i8** %b, align 8
call void @swap(i8** %a, i8** %b)
ret i32 0
}
明显SSA形式的更简单,之后的分析也建立在SSA形式IR之上。
2.2.2.构造程序赋值图(Program Assignment Graph,PAG)
程序赋值图和约束图(Constraint Graph)广义上来说是同一个图,只不过在执行指针分析过程中,PAG保持不变,CG可能会改变。
上面SSA IR对应的PAG如下(简化版):
SVF版AnderSen算法伪代码如下:
2.2.3.执行AnderSen算法
首先是初始化部分,初始化部分主要关注LLVM IR中的 alloca
语句,在PAG中就是address边(绿色的边),从中初始化部分变量的 pts
集和 WorkList
。初始化之后部分变量的 pts
集如下:
-
pts(a1) = { o1 }
-
pts(a2) = { o2 }
-
pts(a) = { o3 }
-
pts(b) = { o4 }
WorkList
从头到尾依次为 %a1, %b1, %a, %b
。
AnderSen算法主循环过程中,会发生2个变化:
-
约束图中会不断新添加新的Copy边(黑色)。
-
约束图中变量的
pts
集会变大(不会变小)。
在 WorkList
开头4个元素(%a1 -> %b
)遍历完之后,约束图、pts
集和 WorkList
变动如下:
可以看出:
-
约束图添加了
%a1 -> o3
和%b1 -> o4
2条Copy边。 -
%p
和%q
的pts
集被更新。 -
WorkList
新添加了%a1, %p, %b1, %q
。
在现在的 WorkList
开头4个元素(%a1 -> %q
) 遍历完之后,约束图的变化如下:
可以看到:
-
约束图多了
%1 -> o3
,%0 -> o4
,o3 -> %0
,o4 -> %1
2条Copy边。 -
o3
和o4
的pts
集被更新。 -
WorkList
从头至尾依次为o3, %1, o3, o4, %0, o4
。
再次遍历完 WorkList
中的6个元素,约束图变化如下:
可以看到约束图中已经没有新的Copy边被添加进来,发生的变化有:
-
o3
、%0
、%1
的pts
集发生了变化。 -
WorkList
有新元素添加。
最后经过几轮迭代,算法也达到收敛状态,收敛状态如下图所示:
至此AnderSen算法也算运行完毕,那么下面就看下在SVF中这些元素是以什么样的形式存在的。
可以注意到AnderSen算法运行过程中,需要做的操作有:
-
修改变量(结点)的
pts
集。 -
往约束图上添加Copy边。
2.3.SVF版AnderSen算法
我的运行环境如下:
-
Ubuntu 18.04
-
LLVM-12.0.0
-
SVF-2.2
2.3.1.SVF API
AnderSen算法实现可参考SVF wiki。如果需要自定义AnderSen算法只需写类继承AndersenBase类即可。需要自定义的函数是 processAllAddr
、solveWorklist
和 addCopyEdge
,分别对应初始化、主体循环和添加Copy边。
AnderSen算法是在约束图上运行的,约束图由程序赋值图 build
而成。因此需要了解的SVF中的数据类型主要有:
-
约束图的定义:参考ConstraintGraph类,在AnderSen算法实现种用到了这个类的
addAddrCGEdge
、getConstraintNode
函数。 -
边的定义:参考ConstraintEdge类、连接的结点类型为
ConstraintNode
类。成员变量ConstraintEdgeK
包括Addr, Copy, Store, Load, NormalGep, VariantGep
6种,不过我这里只关注前4种,后2种在field-sensitive的分析中会用到,成员变量还有edgeId
用来标识边。算法中还用到了父类的getSrcID
和getDstID
方法。 -
结点的定义:参考ConstraintNode类,其继承于GenericNode类。成员变量包括12个EdgeSet,分别保存6种
ConstraintEdge
的所有入边和出边,以及nodeID
来标识结点。
这里还有程序赋值图的定义,约束图可以看作是程序赋值图的简化,方便指针分析算法执行,不考虑结点中的信息。分别包含 PAG 类、PAGEdge类和PAGNode类。这里
-
PAGEdge
类相对ConstraintEdge
类,种类包括Addr, Copy, Store, Load, Call, Ret, NormalGep, VariantGep, ThreadFork, ThreadJoin, Cmp, BinaryOp, UnaryOp
。 -
PAGNode
相对ConstraintNode
多了种类信息,类别包括ValNode, ObjNode, RetNode, VarargNode, GepValNode, GepObjNode, FIObjNode, DummyValNode, DummyObjNode, CloneGepObjNode, CloneFIObjNode, CloneDummyObjNode.
以及多了一些成员变量。
可以说 ConstraintGraph
只包含了 PAG
的拓扑信息,而不包含 PAG
的语义信息
上面示例的 PAG
如下图所示:
运行完AnderSen算法后约束图如下:
可以看到PAG的每个结点的内容为一个语句,约束图中每个结点仅包含ID,部分结点还包含了变量信息。
在上面两个图中:
-
18对应
O1
、20对应O2
、22对应O3
、24对应O4
-
17对应
%a1
、19对应%b1
、21对应%a
、23对应%b
-
7对应
%p
、8对应%q
-
9对应
%0
、10对应%1
2.3.2.AnderSen算法
完整的AnderSen算法代码如下(AnderSen.h
):
#include "SVF-FE/LLVMUtil.h"
#include "SVF-FE/PAGBuilder.h"
#include "WPA/Andersen.h"
using namespace SVF;
using namespace llvm;
using namespace std;
class AndersenPTA: public SVF::AndersenBase{
public:
// Constructor
AndersenPTA(SVF::PAG* _pag) : AndersenBase(_pag){};
//dump constraint graph
void dump_consCG(string name){
consCG->dump(name);
};
private:
// To be implemented
void processAllAddr();
// To be implemented
virtual void solveWorklist();
/// Add copy edge on constraint graph
virtual bool addCopyEdge(SVF::NodeID src, SVF::NodeID dst){
if (consCG->addCopyCGEdge(src, dst))
return true;
else
return false;
}
};
// TODO: Implement your Andersen's Algorithm here
void AndersenPTA::solveWorklist(){
processAllAddr();
// Andersen's worklist-based transitive closure solving starts from here
// Keep solving until workList is empty.
while (!isWorklistEmpty()){
NodeID nodeId = popFromWorklist();
ConstraintNode *node = consCG->getConstraintNode(nodeId);
/// foreach o \in pts(p)
for (NodeID o : getPts(nodeId)) {
/// *p = q pts(q) \subseteq pts(o)
for (ConstraintEdge *edge: node->getStoreInEdges())
if (addCopyEdge(edge->getSrcID(), o))
pushIntoWorklist(edge->getSrcID());
// r = *p pts(o) \subseteq pts(r)
for (ConstraintEdge *edge: node->getLoadOutEdges())
if (addCopyEdge(o, edge->getDstID()))
pushIntoWorklist(o);
}
/// q = p or q = &p->f pts(p) \subseteq pts(q)
for (ConstraintEdge *edge : node->getDirectOutEdges())
if(unionPts(edge->getDstID(),edge->getSrcID()))
pushIntoWorklist(edge->getDstID());
}
}
// TODO: Initialize each pointer at the address constraint
void AndersenPTA::processAllAddr(){
for (ConstraintGraph::const_iterator nodeIt = consCG->begin(), nodeEit = consCG->end(); nodeIt != nodeEit; nodeIt++){
ConstraintNode *cgNode = nodeIt->second;
for (ConstraintNode::const_iterator it = cgNode->incomingAddrsBegin(), eit = cgNode->incomingAddrsEnd();
it != eit; ++it){
/// Implement your code here:
numOfProcessedAddr++;
const AddrCGEdge *addr = SVFUtil::cast<AddrCGEdge>(*it);
NodeID dst = addr->getDstID();
NodeID src = addr->getSrcID();
if (addPts(dst, src))
pushIntoWorklist(dst);
}
}
}
main
函数代码如下:
#include <iostream>
#include "SVF-FE/LLVMUtil.h"
#include "SVF-FE/PAGBuilder.h"
#include "AnderSen.h"
using namespace llvm;
using namespace std;
using namespace SVF;
int main(int argc, char **argv) {
int arg_num = 0;
char **arg_value = new char*[argc];
std::vector<std::string> moduleNameVec;
SVFUtil::processArguments(argc, argv, arg_num, arg_value, moduleNameVec);
cl::ParseCommandLineOptions(arg_num, arg_value, "Whole Program Points-to Analysis\n");
SVFModule* svfModule = LLVMModuleSet::getLLVMModuleSet()->buildSVFModule(moduleNameVec);
/// Build Program Assignment Graph (PAG)
SVF::PAGBuilder builder;
SVF::PAG *pag = builder.build(svfModule);
pag->dump ("pag");
AndersenPTA *andersenPTA = new AndersenPTA(pag);
andersenPTA->analyze();
andersenPTA->dump_consCG("consG");
SVF::LLVMModuleSet::releaseLLVMModuleSet();
SVF::PAG::releasePAG();
delete andersenPTA;
return 0;
}
约束图中每个结点的 pts
集为其它结点。获取 pts
集用到了 getPts
方法,这个方法最初在BVDataPTAImpl类中定义,而 AndersenBase
继承了 BVDataPTAImpl
类,并覆写了该方法。它底层调用了PTDataTy类的 getPts
方法。
typedef PTData<NodeID, NodeBS, NodeID, PointsTo> PTDataTy
...
virtual inline const PointsTo& getPts(NodeID id){
return ptD->getPts(id);
}
可以看到 getPts
方法返回值是PointsTo
类型,其中 PointsTo
本质是llvm::SparseBitVector<>。
typedef llvm::SparseBitVector<> NodeBS;
typedef NodeBS PointsTo;
2.3.3.程序执行流程
从代码中可以看出整个程序的执行流程如下:
我这里只探究了AnderSen算法的实现,并没有探究build PAG和构建约束图的过程。
2.3.4.应用
指针分析的一个应用是变量别名分析,别名分析的结果用llvm中的AliasResult表示:
enum AliasResult : uint8_t {
NoAlias = 0,
MayAlias,
PartialAlias,
MustAlias,
};
包含4种结果,基类 PointerAnalysis
(继承链 AndersenBase -> BVDataPTAImpl -> PointerAnalysis
)提供alias方法查询2个元素(PAG结点、Value
或者 MemoryLocation
)是否存在别名,具体实现在PointerAnalysisImpl.cpp,这里直接调用。
在上述例子中调用 andersenPTA->alias(9, 22);
(分析9号和22号PAG结点,分别对应 %0
、o3
)得到的结果是 MayAlias
。参考BVDataPTAImpl::alias
AliasResult BVDataPTAImpl::alias(const PointsTo& p1, const PointsTo& p2){
PointsTo pts1;
expandFIObjs(p1,pts1);
PointsTo pts2;
expandFIObjs(p2,pts2);
if (containBlackHoleNode(pts1) || containBlackHoleNode(pts2) || pts1.intersects(pts2))
return llvm::MayAlias;
else
return llvm::NoAlias;
}
传入pts
集合计算别名返回值只有 MayAlias
和 NoAlias
两种。至于 MustAlias
和 PartialAlias
以后再探索吧。
三.总结
这里我参考SVF的教程实现了一个简单的flow-insensitive和field-insensitive的AnderSen算法,算法不难不过SVF内部构造PAG和Constraint Graph的过程我这里没探究,以及PAG结点、别名分析过程都没探究。以后会慢慢研究这些过程。
SVF中也实现了一些AnderSen算法的变体,以及其它flow-sensitive的算法,以后我也会深入研究这块内容。
四.参考文献
[1] Pointer Analysis. CS252r Spring 2011
[2] Teaching-Software-Analysis