【南大软件分析-学习笔记】Lecture 7-Interprocedural Analysis

0. 前言

我们在第三节课开始讲数据流分析之前,曾说明为了简化学习便于理解,在当时的课程中并未涉及到过程间分析,比如函数的调用以及别名的使用,前者将在这节课进行介绍,而后者将在下节课的指针分析中进行介绍。

1. Motivation

截至目前为止,我们所学过的分析都是过程内的( intraprocedural),以下面这个包含函数调用(method call)的简单例子为例,对其进行常量分析,分别进行过程内(intraprocedural)和过程间(interprocedural)分析,二者的结果有什么不同?

void foo(){
    int n = bar(42);
}

int bar(int x) {
    int y = x + 1;
    return 10;
}
  1. 采用过程内分析时,为了 safe-approximation,我们会采用更加保守的估计方法,最终分析出来的结果应该是:\(x=NAC,y=NAC,n=NAC\);
  2. 而采用过程间分析时,我们往往可以分析得到正确的结果,\(x=42,y=43,n=10\)。

不难看出,过程间分析相比过程内分析具有更高的精度,那么过程间分析具体要怎么做呢?这就需要我们来构造调用图(call graph)。

2. Call Graph Construction (CHA)

2.1 Definition

定义: 调用图表现了程序中的调用关系,其在本质上就是从调用点(call-sites)到目标方法(callees)的一系列调用边(a set of call edges)。

对于下面展示的一段简单程序,它的调用图就是右图所展示的。

其中每个节点分为上下两层,上层是此处所处的方法,下层是本方法内使用的别的方法,并从下层方法延伸一条调用边到被调函数中。

调用图的主要作用有:

  1. 过程间分析的基础;
  2. 程序的优化;
  3. 程序的理解;
  4. 程序的 debug;
  5. 程序的测试 ...

调用图构造主要用于以 Java 语言代表的 OOP 语言,主要会用到以下四种算法,自上而下精度越来越高,但效率越来越低,此节课将介绍 CHA,下面几节课会介绍 k-CFA。

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

2.2 Method Calls (Invocations) in Java

在 java 中有三种函数的调用方法,分别是 static call、special call 和 virtual call,其中 virtual call 支持多态,同一处调用代码可能执行不同的方法实现,因此构建调用图时最需要分析它,前面介绍的四种调用图构造方法的主要区别也是对 virtual call 的处理方式的区别。

2.3 Method Dispatch of Virtual Calls

前面也说了,静态调用和特殊调用,可以确定单一目标;而虚调用则可能因为多态,对应不同的方法实现。因此我们需要想办法确定 virtual call 究竟调用了谁,在程序运行时,一个 virtual call 被确定具体调用哪个由以下两点确定:

  • 接受者对象(object receiver)的实际类型 c;
  • 调用点处的方法签名(method signature) m 。

其中方法签名指的是一个能够标志方法的标识符形式。

下面我们以这一个方法 foo 为例,展示一下它的签名的组成:

class C{
  T foo(P p, Q q, R r) {...}
}

方法 foo 的签名应该为: <C: T foo(P, Q, R)>,简写为 C.foo(P, Q, R)。

具体的又可以将其分为:

  • Signature = class type + method name + descriptor
  • Descriptor = return type + parameter types

这样一来,我们就可以定义函数 \(Dispatch(c,m)\) 来模拟运行时 method dispatch 的步骤了,\(Dispatch(c,m)\) 是这样的:

其中,m’ 是 c 中的一个非抽象方法,拥有与 m 相同的 name 和 descriptor;c’ 是 c 的父类。函数会先在调用了这个方法的对象/类中找有没有这个方法,找得到就返回这个方法,否则就在这个类的父类中找,结合下面的这个简单例子可以更好的理解这个寻找过程。

\[Dispatch(B, A.foo()) = A.foo() \\ Dispatch(C, A.foo()) = C.foo() \]

2.4 Class Hierarchy Analysis (CHA)

下面介绍 类层次分析法(CHA), 从 Dispatch 函数我们已经可以看出一个程序中可能有多处定义 foo 方法的情况了,CHA 算法的目的就是为了寻找此处使用的 foo 方法是在何处定义的,进而提高数据流分析的准确性,它的精度较低但是速度极快,一个最典型的应用就是 IDEA 中按住 ctrl后将鼠标放在一个方法调用上,能看到一堆这个方法可能所处的位置。

CHA 的 Resolve(cs) 会根据调用类型,计算这处调用的可能目标方法集合 T,设 m 是调用位置 cs 上的方法签名。

针对前面提到的三种调用方法,CHA 算法有不同的解决方法:

  1. static call:直接将指定的静态方法放入 T;
  2. special call:从签名指定的类开始,查找唯一实现;
  3. virtual call:遍历声明类型及其所有子类,收集所有可能实现。

同样的,我们用一个例子过一下整个算法的过程:

2.5 Call Graph Construction

Resolve(cs) 只解决一处调用,要构建整个程序的调用图,还需要从入口出发不断探索。

构造 call graph 的整体思路是这样的:

  1. 从入口进入,一般是 main 方法;
  2. 对于每个可到达的方法 m ,通过 CHA 算法找出点调用的方法 m' 可能是哪里的;
  3. 对于每个 m 以及新加入的 m‘ 不断重复步骤二,直至不再有新的方法被发现。

3. Interprocedural Control-Flow Graph

调用图告诉我们“谁可能调用谁”,但进行数据流分析,还需要知道具体语句之间的连接关系,因此,进一步引入 ICFG:过程间控制流图。

具体的,\(ICFG=CFGs+\text{call \& return edges}\)。

其中,

  • call edge(调用边):从调用点 call site 到被调方法 callee 的入口;
  • return edge(返回边):从 callee 的返回语句到 call site 的下一句。

结合图上的例子可以很简单的理解构建的过程,唯一需要注意的是为什么 call-to-return 边不可以省略,这是为了传递同一方法内部的上下文信息,如本地变量等,call/return edges 不会传递这些局部信息。

4. Interprocedural Data-Flow Analysis

最后来看过程间的数据流分析,分析带有函数调用的程序依赖于过程间的控制流图(ICFG)。

首先比较一下过程内和过程间的区别,主要就是过程间分析多了一个调用返回边,以及对应的传递函数:

其中,

  • Node Transfer:
    • Call Nodes(调用节点):遵循恒等传递,节点本身不修改分析信息,把参数和返回值的处理交给边;
    • Others Nodes:与过程内常量传播相同,计算并更行变量的值;
  • Edge Transfer:
    • Normal edges:遵循恒等传递,不修改传递信息;
    • Call edges:实参 \(\rightarrow\) 形参,把参数值传入被调用方法;
    • Return edges:返回值 \(\rightarrow\) 接受变量,把结果传回调用者;
    • call-to-return edges:删除赋值左侧变量的旧值,并且在下一步中有返回边传递函数重新赋值(这是为了防止返回值与原值不同造成的数据冲突),保留其他局部变量的值。

下面用一个常量传播来简单对比一下过程内分析和过程间分析两种情况下,这个例子没有继承,也没有 meet/join 分支操作,所以比较简单。

posted @ 2026-10-18 18:32  乙烯酷  阅读(4)  评论(0)    收藏  举报