软件分析笔记5 PA
PA=Pointer Analysis 指针分析
Motivation
CHA方法实际上没有利用足够的信息(即实际上变量可能指向的对象所从属的类的范围可以进一步缩小),会引入假的调用边
Pointer Analysis
PA是一种基础的静态分析,它要回答的问题是"某个指针p的值域可能是啥"
对于OO语言来说,其实就是回答某个指针可能指向哪些对象(对于非OO语言的指针则可以直接指向内存)
很显然这是一个may analysis, 用over-approximation
Alias Analysis
这个叫别名分析,关注的是某一个对象是否会被两个指针指向
实现
程序的实际运行情况复杂,因此一种简单的对对象建立的建模就是“我们认为每个new语句只会建立一个唯一的对象”,也叫allocation site abstract
通常分析分为流敏感(flow sensitive)和不敏感分析,即考量是否考虑语句间的执行顺序(即控制流)来分类。这里采用的是流不敏感的技术,即我们认为代码整体是由若干语句组成的集合
定义pt(x)表示变量x所可能指向的对象的集合,并根据不同的赋值语句对对象的流动情况进行建模
大概可以分为几类:
- x = new Obj(); //令pt(x).insert(Obj);
- x = y; //pt(x).insert(pt(y));
- x.f = y; //for (Obj in pt(x)) pt(Obj.f).insert(pt(y))
- x = y.f; //for (Obj in pt(y)) pt(x).insert(pt(Obj.f))
容易发现,四条规则对应了四种集合关系的限制,因此指针分析的一种看法就是解一类限制方程(Anderson Style Analysis)
PFG
Pointer Flow Graph
一个有向图,用来描述指针所指向对象的流向信息
点
变量、对象及其field的表示
变量就标号。对象的field用二元组(o,f)表示,表示对象o的域f
边
有向边x->y表示pt(x)中元素的更新可能影响pt(y)的元素
如果我们知道了PFG,再把所有new都分配到对应的点上,那么指针分析就变成简单的传递闭包问题了!
但是建图的过程也依赖于求pt(x)的问题,因此这是一个边算边建图的过程.....
大概伪代码是这样的
void addEdge(Q,G,x,y) {
G.addEdge(x,y);
Q.push(pts(x),y);
}
void solve(Code C) {
Queue Q; Graph G;
for (i in C) if (i.type==NEW) { // x = new Obj(); at line i
Q.push(x,{oi});
}
for (i in C) if (i.type==ASSIGN) { // x = y;
addEdge(Q,G,x,y);
}
while (!Q.empty()) {
(x,P)=Q.top(); Q.pop();
P=P-pts(x);
if (!P.empty()) {
pts(x).insert(P);
for (y in adjacent(G,x)) {
Q.push(y,P);
}
}
for (i in C) if (i.type==load) { // y = x.f;
// for (o in pts(x)) { // 已经加过了的边不必再加
for (o in P) {
addEdge(Q,G,o.f,y);
}
}
for (i in C) if (i.type==store) { // x.f = y;
for (o in pts(x)) { // 同上
addEdge(Q,G,y,o.f);
}
}
for (i in C) if (i.type==store) { // y.f = x; // 这种写法和上面是等价的
for (o in pts(y)) { // 同上
addEdge(Q,G,x,o.f);
}
}
}
}
一开始没有看明白为什么pts(x)发生变动时只考虑了y=x.f
和x.f=y
考虑所有的情况只有四种:
y=x.f
x.f=y
y.f=x
x=y.f
可以发现情况4在pts(x)发生变化时不需要更新后继点
而情况3和情况1是对称的...因此写法是任意的
Inter-Procedural Analysis
考虑方法调用的指针分析,就是要考虑形如
a=b.f(r1,r2...)
这样的语句如何建模。同样也是建Call Graph
在前面用到的CHA方法并不准确,因为我们并没有考虑指针实际可能指向的对象域的情况。注意到这恰好是PA在做的事情,因此可以结合着指针分析进行CG的建立
实际上就是枚举pts(b)中的对象o,通过dispatch(o,f)来找到实际调用的方法m,然后枚举所有实参的指针域流向方法m中的对应形参,返回值的指针域流向a。同时类的this指针也不能忽略,因此也要把o流到m的this去(这里我们认为每个方法都有它自己的this指针)
在建PFG的时候,我们会对参数、返回值进行连边,而this则不会连边(why?)
考虑如下代码片段:
class A {
T foo() { this; }
}
class B extends A {
T foo() { this; }
}
class C extends A {
T foo() { this; }
}
那么给定下列指针域和调用
pts(x)={A, B, C};
x.foo();
当x分别指向A,B,C时,对应应该流向A.foo.this,B.foo.this,C.foo.this
也就是说,对某一段方法调用代码片而言,LHS变量的指针域中的每个对象只会流向唯一的方法的this指针(这是由dispatch的唯一性保证的)。假若我们为LHS变量到所有可能方法都连上了边,那么就会出现不必要的精度损失。
具体的实现可以看PPT
本文来自博客园,作者:jjppp。本博客所有文章除特别声明外,均采用CC BY-SA 4.0 协议