CG
Last updated
Last updated
进行过程间分析(Interprocedural Analysis,跨函数分析)前需要有函数调用图。
调用图是程序中调用关系的一种表示方式
JVM中的几种调用指令👇
Static Call | Special Call | Virtual Call | |
---|---|---|---|
instruction | invokestatic | invokespecial | invokeinterface、invokevirtual |
receiver objects | × | √ | √ |
description | static methods | constructors、private instance methods、superclass instance methods | other instance methods |
target methods | 1 | 1 | ≥1(polymorphism) |
determinacy | Compile-time | Compile-time | Run-time |
由于Java语言的多态特性,虚方法的调用是在动态执行时分派的,子类可能重写了父类的方法,我们无法通过静态分析确定实际调用的方法,因此只能做May Analysis
。
SootUp
目前提供了两种构建调用图的方法,分别是CHA和RTA
Class Hierarchy Analysis(CHA)是根据receiver variable
的声明类型来解析virtual call
的,需要有类继承树的信息。
这里变量a
的声明类型是A
,但实际上a
可能new
的是A
类本身或者A
的子类。
因此这里找的调用方法是A
类的foo
方法,以及所有能够继承到A
类foo
方法的子类重写的foo
方法。
在SootUp
中也提供了CHA的相关接口。
以下面程序为例
继承关系如下:
IDEA -> navigate -> call hierarchy得到的结果如下:
下面用SootUp
进行分析
ClassHierarchyAnalysisAlgorithm
这个类实现了CHA算法
In this algorithm, every virtual call is resolved to the all implemented overwritten methods of subclasses in the entire class path
调用图的构建需要有一个入口方法,由入口方法逐步扩大 “reachable world”
ClassHierarchyAnalysisAlgorithm#initialize
可以传入一个入口方法签名的列表,不传默认会寻找main
方法
得到的调用图
显然比IDEA的准确一点。。。
下面看一下SootUp
中的实现
创建一个ClassHierarchyAnalysisAlgorithm
需要传入JavaView
,其含有所有类和方法的数据。initialize
不传参则会寻找main
方法,具体就是遍历当前view
中所有的类(除掉library class
),找到方法签名符合main
方法签名的方法。
找到main
方法后会将其作为entry point
initialize
开始构造CG
worklist
是Queue
的一个具体实现Deque
(Double Ended Queue
,双端队列)
注意这里worklist
中的元素是方法签名而非方法语句。
将entryPoints
添加到worklist
中。
这里就有一点比较tricky的,构造调用图的时候还是考虑到了Java语言的特性
addImplicitEdgesOfEntryPoints
首先会找entry point
方法所在类是否有静态初始化方法,即<clinit>
方法,接着把entry point
方法和<clinit>
方法都加入CG,再加个entry point
到<clinit>
的调用边,并把<clinit>
方法加入到worklist
接下来就是worklist
算法启动,processed
集合用于记录已经reach
到的方法
从worklist
里pop出一个方法签名,如果已经在processed
集合里,就不进行处理。
找出当前方法签名所在类,对方法进行预处理(preProcessingMethod
,这是一个抽象方法,由AbstractCallGraphAlgorithm
子类实现),将当前方法签名加入CG。
找出当前方法中所有调用语句指向的callee
(resolveAllCallsFromSourceMethod
)
解析这些方法调用(resolveCall
由子类实现)
下面便是CHA的核心算法👇
CHA中,通过receiver object
的声明类的类结构来获取所有可能的调用目标,声明类的每个子类,只要有调用方法的实现(不管是继承得到的还是重写的),都会被考虑为调用目标。
dispatch
findConcreteMethod
会对方法进行dispatch,即从自身往父类上找,直到找到方法被实现的地方。
superClassesOf
并不能得到接口的父接口。
这里还考虑了一个Java语言的另一个feature
Java中接口是可以多继承的(类就不可以)
而且接口声明的方法不一定要被实现(默认方法default和静态方法static可以不被实现)
注意,default方法虽然被default修饰,但访问级别是public的
implementedInterfacesOf
会找到当前类实现的所有接口,包括父类所实现的接口,以及接口所继承的父接口。(如果传入的classType
是接口,也能找到其继承的接口)
在这些接口中寻找子签名对应的方法,并获取最小的那个接口中的方法(最小即继承结构最底端,因为对于default方法,子接口是可以重写的)
B b = new B(); b.hack()
这里得到的是<org.demo.inter3: void hack()>
能走到这一步要么是receiver object的声明类是接口,调用的是接口的普通方法
要么调用的是接口的默认方法或静态方法
inter3 b = new B(); b.hack();
改成inter1
中声明普通方法,A类实现hack接口
这里得到的便是<org.demo.inter1: void hack()>
specialinvoke&staticinvoke
dispatch之后便对调用类型进行判断
感觉这里的逻辑有问题,invoke static
/invoke special
的一些特殊情况需要dispatch
super和静态方法的调用可能是目标继承得到的方法,通过原方法签名不能直接获取到,不能直接返回targetMethodSignature
,而应该返回findConcreteMethod
的得到的targetMethod
的签名。
virtualinvoke
我们主要关注虚方法的调用
subtypesOf
遍历子类(适用于接口)找非抽象的实现方法
如果子类中没找到这个方法,即子类没有实现它并且子类不是接口,就将子类加入noImplementedMethod
接着看子类实现的接口中,如果有子签名对应的默认方法,也加入targets
这里getInterfaces
得到的是类声明时明确写的implements
后面跟的接口,是直接实现的接口,而非继承得到的。
这么操作的意图不是很懂。。。感觉会引入假的调用边。个人觉得这里的顺序应该这样,先在当前子类拿到method,拿不到再尝试接口的default方法,再拿不到才加入noImplementedMethod
interfaceinvoke
接着判断若是invoke interface
,则将上面noImplementedMethod
再进行dispatch
resolveConcreteDispatch
还是调的findConcreteMethod
接着还得对当前方法中的一些隐式调用进行处理。
resolveAllStaticInitializerCallsFromSourceMethod
对于当前方法中存在的:
静态字段使用
构造器调用
静态方法调用
都会造成对目标类静态初始化方法clinit
方法的隐式调用
ClassType#getStaticInitializer
获取这些类的<clinit>
方法
接着在CG中添加当前方法到这些<clinit>
方法的调用边
最后对方法进行后处理(postProcessingMethod
)
Rapid Type Analysis(RTA)在CHA的基础上,对不可能调用到的方法进行剪枝。
RTA只关注分析中已经被用于初始化了的类型。
把上面的ClassHierarchyAnalysisAlgorithm
改成RapidTypeAnalysisAlgorithm
得到调用图如下:
因为我们上面并没有对C
类和D
类进行实例化,所以这里的结果就只有A#foo
如果我们增加一个语句C c = new C();
那么结果就会多出一条C#foo