软件分析实验Tai-e代码理解与踩坑记录
静态分析实验太阿地址
同学优质的课程专属博客
太阿实验保姆级提示
A1
- 每个SetFact<Var>包括了一个Var集合的类成员和若干操作集合的方法,每个程序点的IN/OUT都拥有一个SetFact<Var>. 初始化时因为LiveVariableAnalysis都要初始化为空集,返回new的值即可。
@Override
public SetFact<Var> newBoundaryFact(CFG<Stmt> cfg) {//IN[exit]=∅
//空集->集合中没有任何var->新建一个SetFact即可
return new SetFact<>();
}
@Override
public SetFact<Var> newInitialFact() {//IN[B]=∅ 为了完成meet策略,OUT赋一样的初值∅
//空集->集合中没有任何var->新建一个SetFact即可
return new SetFact<>();
}
- 这里逻辑简单(因为analysis写好了),我因为在每轮循环开始忘记恢复change的值耽误了一会儿。
@Override
protected void doSolveBackward(CFG<Node> cfg, DataflowResult<Node, Fact> result) {
boolean change = false;//记录这一轮是否有至少一个变化的IN
do{
change = false;//这里我忘记恢复初值了,一度导致死循环
for (Node node : cfg) {
if(!cfg.isExit(node)){
for (Node succ : cfg.getSuccsOf(node)) {
//把每个succ的IN与target的OUT取并集
analysis.meetInto(result.getInFact(succ), result.getOutFact(node));
}
//cfg中的node在LiveVariableAnalysis中的类型是Stmt,所以调用了自己实现的代码
//利用返回值判断是否有IN值改变
if(analysis.transferNode(node, result.getInFact(node), result.getOutFact(node)) && !change){
change = true;
}
}
}
}while (change);
}
- 因为LValue和RValue不是永远是Var,所以强制类型转换前一定要用
instance of
判断,否则会报错exp.invokespecial cannot be cast to Var这样的类型转换错误。
- Optional<T>可以有效提醒程序员里面的值可能为空,为空isPresent()则返回false,不为空调用get()则获得T类型实例。
if(defB.isPresent()){//java.util.Optional<T>中的isPresent可判断是否为空
if(defB.get() instanceof Var) {//注意一定要判断,LValue不是永远为Var->不判断时会报cannot be cast to Var
res.remove((Var) defB.get());//OUT[B]-DEF[B]
}
}
for(RValue rValue: stmt.getUses()){//注意一定要判断,LValue不是永远为Var->不判断时会报cannot be cast to Var
if(rValue instanceof Var){
res.add((Var) rValue);
}
}
A2
-
关注细节 issue
-
evaluate函数判断常量不是看Var的isTempConst,这是理论(能有和真的存int有区别)要看实际:Value.isConstant判断
-
利用好copyFrom函数,然后要用中间值。如果不使用虽然比较了赋值语句对IN的修改,但没有比较旧的IN和旧的OUT。
-
newBoundaryFact要用cfg.getIR().getParams()获得方法的参数(不是getVars,这个是变量+%this),记得设置为NAC。(因为不分析过程外的方法,这里可能返回任何职,需要做最保守的假设。显然这是sound但是不精确的。第七课介绍的过程间分析会精确很多。)
-
IN依然记得赋初值。为了meet策略。
-
worklist中用有序队列存储,而不是set,因为有forward顺序需求
-
worklist添加时记得去重(用contains判断)
A3
- 用BFS/DFS遍历CFG图,我使用的是BFS,用queue进队出队实现。不能无脑遍历node,这样无法检测控制流不可达代码。
- 分支中的可达代码不用立即处理,加入队列之后处理即可。毕竟分支内的语句一般不止一条。
- 遍历BFS可处理分支不可达代码和无用赋值,但不能处理控制流不可达代码,所以BFS结束后还要另外进行处理。
for(Stmt stmt: cfg.getIR().getStmts()){//这里处理控制流不可达
if(!liveCode.contains(stmt) && !deadCode.contains(stmt)){
deadCode.add(stmt);
}
}
- AssignStmt 的 LValue 不一定是 Var,A1也有这个问题,见继承关系即知。issue
- transferNode函数在处理backward时自己会处理in/out的反转,传参时不用手动改,我在第一次写worklist.java时出错了。
if(analysis.transferNode(node, result.getInFact(node), result.getOutFact(node))){
for(Node pred : cfg.getPredsOf(node)){
if(!nodeQueue.contains(pred)){
nodeQueue.add(pred);
}
}
}
- 实现时的思路
A4
实现类层次结构分析(CHA)
- Resolve(b.foo())=ACD:这里为什么不是special?b.foo()并不代表直接调用super.foo(),所以是virtual
- getDeclaringClass返回class type;getSubsignature:返回被调用方法的子签名(两方法结合就能获得完整子签名了)
一个方法的子签名只包含它的方法名和方法签名的描述符,如 foo 的子签名是:“
T foo(P,Q,R)
” ,而它的完整签名是:“<C: T foo(P,Q,R)>
”。
- 子类包括直接和间接,所以要用bfs/dfs。我使用了bfs。
- 注意virtual call包含interface的情况。
if(jClass.isInterface()){
q.addAll(hierarchy.getDirectImplementorsOf(jClass));
q.addAll(hierarchy.getDirectSubinterfacesOf(jClass));
}else {
q.addAll(hierarchy.getDirectSubclassesOf(jClass));
}
实现过程间常量传播
- edge transfer逻辑:定义了
transferEdge(edge, fact)
函数来实现 edge transfer
- normal edge黑实线:transferEdge(edge, fact) = fact。
- call-to-return edge黑虚线:左值LHS kill掉,其他往下传。
- 若左侧没有变量的调用,比如
m(…)
:不修改 fact,edge transfer 是一个恒等函数。
- 若左侧没有变量的调用,比如
- call-edge:将实参(argument)在调用点中的值传递给被调用函数的形参(parameter)。返回值为被调用函数的形参,如x。
- 首先从调用点的 OUT fact 中获取实参的值
- 返回一个新的 fact
- 这个 fact 把形参映射到它对应的实参的值
- 易错点:
for(int i = 0; i < params.size(); i++){//callee中的param与Invoke中的arg值一一对应
res.update(params.get(i), callSiteOut.get(args.get(i)));
}
- return edge:edge transfer 函数将被调用方法的返回值传递给调用点等号左侧的变量。
-
从被调用方法的 exit 节点的 OUT fact 中获取返回值(可能有多个,你需要思考一下该怎么处理)
-
返回一个将调用点等号左侧的变量映射到返回值的 fact。(对应等号左边的变量)
-
如果该调用点等号左侧没有变量,那么 edge transfer 函数仅会返回一个空 fact。
-
易错点:多个返回值的处理利用cp.meetValue
-
for (Var var : edge.getReturnVars()) {
val = cp.meetValue(val, returnOut.get(var));
}
- 理解callNode和NonCallNode的区别。都是利用返回值判断OUT值是否改变,但CallNode调用TransferEdge实现对IN的修改,再与没变的OUT(存储OLD_OUT)进行对比,就实现判断OUT值是否改变。这也是为什么“你在实现 transfer*Edge() 方法的时候,不应该修改第二个参数,也就是该边的源节点的 OUT fact。”
/**
* Dispatches {@code Node} to specific node transfer functions for
* call nodes and non-call nodes.判断OUT是否有变化
*/
@Override
public boolean transferNode(Node node, Fact in, Fact out) {
if (icfg.isCallSite(node)) {
return transferCallNode(node, in, out);
} else {
return transferNonCallNode(node, in, out);
}
}
实现过程间 Worklist 求解器
过程间与过程内求解器仅有两处不同:
- 在计算一个节点的 IN fact 时,过程间求解器需要对传入的 edge 和前驱们的 OUT facts 应用 edge transfer 函数(transferEdge)。过程内直接是OUT[B]。
- 个人不太懂“仅需要”,感觉逻辑和过程间几乎一样。因为其他方法的 entry 节点和非 entry 节点还是要设置initial fact,否则会报错。可能这是实现meetInto策略的原因,算法本身没有特殊要求?不懂。
在初始化的过程中,过程间求解器需要初始化程序中所有的 IN/OUT fact,也就是 ICFG 的全部节点。但你仅需要对 ICFG 的 entry 方法(比如 main 方法)的 entry 节点设置 boundary fact。这意味着其他方法的 entry 节点和非 entry 节点的初始 fact 是一样的。
A5
- addReachable:处理 New Copy Invoke (static) StoreField (static) LoadField (static)
- 访问者模式:stmt.accept(stmtProcessor);
- 如何对new创建对象:Obj getObj(New allocSite);
- 遍历S_m:
method.getIR().getStmts().forEach
- static storefield中怎么获得T.f?pointerFlowGraph.getStaticField(field)
- 静态和动态invoke都可以用
Solver.resolveCallee(Obj,Invoke)
获得callee,静态时令obj=null即可
- addPFGEdge:AddEdge
- 如何获得pointer的指针集pt(s)?用pointer类自带的getPointsToSet()方法
- analyze:while循环,处理 Invoke - processCall StoreField LoadField StoreArray LoadArray
- 如何判断pointer能否代表变量x?pointer instanceof VarPtr ptr
- f怎么获得?JField field= storeField.getFieldAccess().getFieldRef().resolve();
- oi_f怎么拼凑?pointerFlowGraph.getInstanceField 会创建
- oi[*]怎么表示?用pointerFlowGraph.getArrayIndex()
- PointsToSet propagate(Pointer,PointsToSet):用filter函数
- void processCall(Var,Obj)
- m_ret怎么获取?用 callee.getIR().getReturnVars()
- 函数传参和A4中一样
- 判断CG里有没有边不能写callGraph.hasEdge(callsite.getContainer(), callee),而要写callGraph.getCalleesOf(callsite).contains(callee)。不是很理解,可能跟数据结构有关。
A6
- 关于A5->A6的变化这里写的十分清楚
- processSingleCall不用传Context,因为callee和callsite可以得到
- 注意new语句
// New {o_i}用heapModel创建
public Void visit(New stmt) {
Obj obj = heapModel.getObj(stmt);//获得o_i
Context c = contextSelector.selectHeapContext(csMethod, obj);//获得c
PointsToSet pts = PointsToSetFactory.make(csManager.getCSObj(c, obj));//获得c:o_i
workList.addEntry(csManager.getCSVar(context, stmt.getLValue()), pts);
return null;
}
- 不需要make多个参数或将CSCallSite变成Context时,不用使用ListContext
- selectContext中
- CallSelector :静态和非静态的处理一致,只需要关注 CSCallSite
- ObjSelector&TypeSelector:静态直接返回CallSite的上下文,而非静态关注 CSObj
A7
从A7开始是自行设计算法,不保证正确,仅通过平台的少量用例。
参考了这个知乎思考
个人思路是
- 对于load,把对应的store的rhs用meet计算出来,赋给load的lhs
- 对于store,若y.f的值有变化,把对应的load加入worklist
对应形容的规则为:
- load/store:在别名中找field一致的
- array:在别名中找index满足表格的
下面是汇总的太阿官方手册和SPA-Freestyle-Guidance,侵删
实例字段
load
找到所有对这一实例字段(以及其别名)进行修改的 store 语句,并将这些语句要 store 的值(即rhs) meet 之后赋给 L 等号左侧的变量。相同为精确值,不同为NAC。
y = 5;
p.f = y; // p.f is an alias of a.f(这里的y就是要找的值)
...
L: x = a.f; // meet val(y) to val(x)
利用指针分析计算程序的别名信息,若 x
和 y
的指针集有交集,则x.f
和 y.f
互为别名
store
在发现y.f的值改变了(也就是rhs,x的值改变了)之后,也要将某些节点加入worklist. 这些节点,也就是这次load可能产生影响的语句,是所有对y.f的读取,x = y.f
,以及所有对y的潜在别名的读取,x = z.f.
// x.f = y storeField遍历x的指针集,看指向对象o.f在store后有无变化,有变化时将所有潜在节点加入worklist
for obj in pts(x):
obj.f = meet(obj.f, y)
if obj.f changes:
for z in obj.alias:
foreach l: w = z.f:
kill (w, _), gen (w, obj.f)
start cp from l
这个算法基本上和上面的图片有差不多的意思了。现在需要解决的问题就是:
- 如何存储
obj.f
的值?这个问题就留给你来想了。“field 随便你加”。 - 怎么从
l: w = z.f
开始常量传播?讲义里面写了,InterCP 里面是有一个 solver 的,只需要稍加改造 solver 就可以把l
加入传播流程。
静态字段
load
当处理一个静态字段的 load 语句时(假设为 x = T.f;
),你只需要找到对同一个字段(T.f
)的 store 语句,并将这些保存进字段的值进行 meet 后赋给 load 语句等号左侧的变量(x
)。这一处理可以在没有指针分析和别名信息的情况下完成。
因为 JClass
不像 Var
那样自带所有的 RelevantStmt
,所以这似乎就需要花费一次 icfg 遍历来自行收集了。可能也有更好的办法,比如一些隐藏的 API,但是,我不清楚。
数组
load
当分析一个数组的 load 语句如 x = a[i];
时,你需要找到所有修改 a[i]
别名的值的 store 语句,并将这些值 meet 后赋给 x
。此处对数组的处理要更复杂一些:当你判断两个对数组的访问 a[i]
和 b[j]
是否互为别名时,你不仅需要考虑它们的 base 变量(a
和 b
)的指针集是否有交集,还需要考虑索引值 i
和 j
的关系。有趣的是,由于数组索引也是 int
类型,因此你可以用常量分析来实时计算 i
和 j
的关系。
下图判断 a[i]
和 b[j]
是否互为别名
a[i] and b[j] | j = UNDEF | j =c 2 | j = NAC |
---|---|---|---|
i = UNDEF | 不互为别名 | 不互为别名 | 不互为别名 |
i =c 1 | 不互为别名 | 当且仅当 c1=c2时互为别名 | 互为别名 |
i = NAC | 不互为别名 | 互为别名 | 互为别名 |
A8
source:类似指针分析里的new rule,会增加taint obj,也就是在遇到函数调用时,我们额外检查callee是否是source,是的话额外增加taints到worklist中。
sink:写在collectTaintFlows()中,在PTA分析结束后检查指针集情况来收集污点流,遍历所有的CSCallSite即可
taintTransfer:接口写在analysis中,而PTA 对 Invoke 的处理地点这么多,在哪里进行 transfer 的检测?当一个 Var 的 taint object 子集发生变化,检查其相关的语句,并看是否有符合条件的 transfer,然后将变化通过 worklist 算法传播出去。所以个人在函数调用的后面都调用了transfer,因为source会引起 taint object的变化(注意静态的base为null)