crossain

导航

软件分析笔记:3.过程分析(Interprocedural Analysis)

本文是针对南京大学《软件分析》谭添老师的视频课的课堂笔记。

1.Motivation

此前我们讨论的问题都是过程内的,也就是不涉及到方法调用。然而实际程序中方法调用屡见不鲜,继续采用之前的分析方法会丢失精度,这也就是为什么我们需要过程(间)分析。二者的区别如下:

过程内分析Intra-procedural Analysis

  • 只考虑过程内部语句,不考虑过程调用
  • 目前的所有分析都是过程内的

过程间分析Inter-procedural Analysis

  • 考虑过程调用的分析
  • 有时又称为全程序分析Whole Program Analysis
  • Call edges和Return edges
  • 需要call graph

2.Call Graph(调用图)

2.1调用图的概念以及简单示例

本质上可以看做是一个调用边的集合,每个调用边从调用点连接到目标方法(target methods或者callees),简单例子如下:

void foo(){
	int n = ten();
	addOne(42)
}

int ten(){
	return 10;
}

int addOne(int x){
	int y = x + 1;
	return y;
}

上面代码所对应的调用图如下所示:

该程序有三个方法(foo()、ten()、addOne()),调用图就是由调用点引出的箭头指向被调用的方法。

2.2调用图的应用

  • 理论上所有过程间(跨函数)分析的基础
  • 程序的优化
  • 程序理解
  • 程序debugging
  • 程序测试

2.3针对面向对象语言的调用图构造(以Java为例)

2.3.1代表性算法

  • Class hierarchy analysis(CHA)
  • Rapid type analysis(RTA)
  • Variable type analysis(VTA)
  • Pointer analysis(k-CFA)

以上四个算法的排列是有规律的,从上到下越往下精度越高(more precise),越往上速度越快(more efficient)。我们将重点学习第一个和最后一个算法。

2.3.2预备知识

Java中的调用

Java调用主要分为三大类,如下图所示:

invokestatic调用的目标方法是static methods,就是静态方法。所以它是没有reciever object,目标个数只有一个,在编译期可以确定。

后两种调用的都是instance(实例)方法:

invokespecial调用的方法有构造函数、私有的实例方法以及父类的实例方法,它的目标个数也是只有一个,在编译期可以确定。

invokeinterface和invokevirtual调用其他的方法,因为有多态的存在,所以可能调用不同的方法,因此目标方法可能大于一个,具体调用的方法要在运行时才能确定。

因为前两类相对来说比较简单,所以我们过程间分析的关键是对于第三种Virtual call的分析。

Virtual call中有个关键步骤,叫Method Dispatch。因为Virtual call调用的具体方法是要在程序运行时才能得到,在这一过程中涉及到两个要素:

  • reciever object的具体类型:c
  • 方法的签名(method signature):m,一个signature可以充当一个方法的identifier.即通过一个signature可以唯一确定一个方法。
    • Signature = class type(方法具体所在的类) + method name(方法名) + descriptor
    • Descriptor = return type + parameter types
    • 可以参考soot工具中采取的格式,如下图所示,红色的是class type,蓝色的是method name,绿色的就是desciptor

缩写为

求这个方法的过程,我们叫做Method Dispatch。

2.3.3Method Dispatch of Virtual Calls

我们定义了一个函数Dispatch(c,m)以模拟动态Method Dispatch的过程。参数c和m是上面定义里的两个要素(已加粗)。具体过程如下图所示:

如果非抽象方法(因为dispatch要找的是一个具体的能被调用的方法,所以必须是非抽象)里包含一个和m有着相同名字和descriptor的m,那么就直接返回m,我们就认为dispatch找到了目标函数。如果c中没有满足条件的方法,那么我们就去c的父类里面找,重复这个过程直到找到为止。

下面是一个利用Method Dispatch的例子:

如图所示,第一个Dispatch是先在B里面找foo方法,然后发现找不到,所以就去B的父类A里面找,找到了,所以第一个Dispatch的结果就是A中的foo方法。第二个Dispatch是先在C里面去找foo方法,在这里因为C自己就有foo方法,所以第一步我们就返回了C中的foo方法。

2.3.4Class Hierarchy Analysis*(CHA)

该方法需要程序中类继承(也就是名字里的Class hierarchy)的信息,也就是需要知道每个类的父类和子类。核心思想就是根据每一个Virtual call的receiver variable的declared type(声明类型)来解目标方法。举例说就是

对于上图a这个变量的declared type就是A,那么CHA就会根据A的方式去算。具体思想就是假设a可以指向A以及它所有子类的对象,因此CHA的实现过程就是查询A的继承结构,从A和子类继承结构去找目标方法。

CHA的具体实现过程

我们定义了一个方法Resolve(cs)以利用CHA算法找到可能的对应call site的目标方法。算法伪代码如下:

首先初始化一个空集合T以装call site的目标方法,然后我们取出调用点cs的签名,接下来对cs的类型进行判定:

  • 如果cs是静态调用,那么T就等于对应类中的方法。

  • 如果cs是special call,在预备知识中我们知道special call有三种情况(构造函数、私有方法或者父类方法),以父类方法为例:

如图所示,因为C继承了B类,所以C中的父方法就是B中的方法,我们应该先出去方法名的class type,在例子中也就是B类,接下来对cm和m调用Dispatch(因为B中可能并没有foo方法,我们可能还要从B的父类中去找,所以在这里调用了Dispatch()),因为Dispatch返回的目标方法是唯一的,这也就解答了之前为什么说special call目标个数也是唯一的原因。

  • 如果cs是virtual call,如下图所示:

我们会先找出c的声明类型,也就是C,对C本身和C所有子类以及子类的子类等等(在这里我们定义它为c)都调用一个Dispatch(c,m)并将返回值添加到集合T中,最后返回集合T。

一个CHA应用实例

如图所示,c的声明类型是C,而C没有子类,所以Resolve(c.foo()) = {C.foo()}。同理可得Resolve(a.foo()) = {A.foo(),C.foo(),D.foo()},Resolve(b.foo()) = {A.foo(),C.foo(),D.foo()}(这里要注意因为B中没有foo方法,所以要到B的父类A中去找)。

这里也暴露了CHA算法的问题,那就是如果将“B b = ..."替换成”B b = new B()",Resolve(b.foo())还是会得出同样的结果,而事实上C.foo()和D.foo()都是错误的结果。那是因为CHA只考虑声明类型,也就是B,这样就会导致精度的下降。

CHA的特征

优点:快,只需要考虑声明类型,忽略所有数据流和控制流。

缺点:精度差,因为忽略的太多了。

CHA最常用的场景就是IDE中,如下图所示:

2.3.5利用CHA构造调用图

简单步骤:

  • 从程序的入口方法开始(如Java里的main方法)构造调用图
  • 在构造过程中可以通过调用边达到一些新的方法,每遇到一个可达方法,对他们用CHA的Resolve方法找到目标方法,以此往复,知道找到所有的可达方法,最终得到调用图。

具体算法实现

如上图所示,第一行对算法进行了初始化,先是将入口方法添加到Work List里,然后将CG和RM两个集合清空。整个算法是一个大的while循环,我们不断从WL中取出方法m并将它添加到RM中(代表此方法已经被分析)并对m中的调用点进行分析,利用CHA找到对应的目标方法和调用边,将目标方法添加到WL中,并将它和调用边组成调用图。

使用实例


3.过程间的控制流图(ICFG)

3.1与CFG区别

  • CFG表示的是单个方法的结构
  • ICFG表示的是整个程序的结构
    • 我们可以用ICFG进行过程间分析
    • 一个ICFG是由程序中各个方法自己的CFG再加上两种额外的边(Call edges和Return edges)组成的
      • Call edges连结调用点和目标方法的入口
      • Return edges从一个return语句连回到紧跟着调用点下面的语句
      • Return site一般紧跟着Call site。

一个理解ICFG的例子:

如图所示,三个方法对应了三个CFG,将这三个CFG用Call edges和Return edges连结到一起。


4.过程间的数据流分析

4.1原理

实际上就是对有method call的程序,基于该程序的ICFG对数据流进行分析。

如图所示,相较于过程内的数据流分析,过程间 的数据流分析的转换函数多了一个edge transfer的部分(包含Call edge transfer和Return edge transfer),这也跟前面说的ICFG相较于CFG多的那两种边相对应。

4.2过程间的常量传播数据流分析

对于常量传播来说:

  • Call edge transfer:就是用来传参的
  • Return edge transfer:就是用来传返回值的
  • Node transfer:和过程内大致一样,对每一个方法调用节点,将等号左边部分去掉。

实例分析

这部分很好理解,唯一需要注意的是图上两条黄色背景的边,这两条边并不是可有可无的,上面说到的node transfer提过对于每一个方法调用节点将等号左边部分去掉,也就是说从“b=addOne(a)"语句到"c=b-3"语句我们是将a的值传递了过去,而b的值由addOne()传递,如果去掉这条边的话就意味着a的值只能通过addOne()传播,而addOne()中对a并没有更改,这样会额外消耗系统资源,另外在第二个黄色边中,如果不去除掉b的值,那么最后一个节点得到的b就为NAC,出现错误,所以我们才会要求去掉等号左边的元素。

5.总结

过程间分析相较于过程内分析的精度更高,因此在实际项目中,我们应该更倾向于使用过程间分析。

posted on 2020-04-17 16:08  crossain  阅读(3668)  评论(1编辑  收藏  举报