Skip to content

方法Method

调用图构造

Call graph construction · Static call graph

不知道运行时接收者或函数指针时,怎样可靠地列出可能的被调函数。

形式陈述 ​

静态调用图从程序的调用、分配与方法声明语法中提取哪些调用位置可能执行哪些函数或方法。设 M 是程序方法集,C 是调用点集,可把结果写成

Targets:C⟶P(M).

若调用点 c 位于方法 m,且 m′∈Targets(c),就画边 m→cm′。保留调用点标签很重要:同一方法中的两个调用即使目标相同,返回位置仍不同。方法级调用图比过程内控制流图粗;它不代替参数传递、异常边或调用返回匹配。

可靠性要求:任意允许执行在 c 真正调用了 m′,静态结果中都含 m′。多画边造成保守近似,少画一条真实边则可能让所有后续分析漏掉行为。

本页固定封闭世界、静态类型的单继承面向对象核心:全部类定义已知;无反射、动态装载、native回调;对象来自显式分配;静态初始化和构造器调用已表示成普通可达代码。dispatch(K,f) 表示对象动态类为 K 时,沿继承链找到的实际方法实现。继承方法可能仍属于父类,不要求每个类都有一份新的实现。

两种可执行构造 ​

类层次分析(CHA):对接收者静态类型为 T、方法名为 f 的调用 c,把所有已知具体子类 K<:T 的 dispatch(K,f) 都列入目标;直接静态调用只加其确定目标。子类型关系提供候选动态类的范围。

快速类型分析(RTA):除可达方法集 R 外,再维护可达代码中可能被分配的类集 A。从程序入口开始,反复执行:

  1. 扫描新可达方法,将其中分配的类加入 A,将直接调用的方法加入 R
  2. 对所有可达的虚调用点,只考虑 A 中且满足接收者静态类型约束的类,加入相应目标方法
  3. 新目标可能暴露新分配,继续直到两个集合和调用边都不变

这是一个最小不动点,不能只扫描入口中的 new 一次。RTA的类集是全程序共享的近似,还没有知道某个具体变量究竟指向哪一个分配。

直觉

CHA问:“按类定义,这个对象有可能是哪类?”RTA再加一条过滤:“从入口可到达的代码,真的有机会造出这个类吗?”后者通常少些边,但类是否会被分配与方法是否可达互相依赖,必须一边发现、一边更新。

这两种方法都不按运行时间排序。即使一个分配语句在调用之后,流不敏感的RTA仍可能用它扩大该调用的候选集。调用图说明潜在调用关系,不是一段运行日志。

例子与边界

新发现的分配会反过来改变旧调用点 ​

类A和子类B都实现方法 f。入口代码为:

text
main:
    x = new B
    x.f()                 // c

B.f:
    makeA()

makeA:
    y = new A
    return y

A.f:
    return

假设 x 的静态类型声明为A。CHA从一开始就在调用点 c 放入 A.f 和 B.f。RTA则逐步得到:

发现阶段 可达方法中的新信息 已分配类 c的候选目标
扫描main 分配B,遇到虚调用 {B} {B.f}
扫描B.f 直接调用makeA {B} 不变
扫描makeA 分配A {A,B} 加入A.f
扫描A.f 没有新调用或分配 {A,B} 稳定

实际运行中,x始终指向B,makeA的返回值也没有赋给x。因此A.f仍是一条伪边。RTA可靠地保留了真实B.f,却因为全局类集合而失去变量级区别;指针分析可以继续问“哪个对象能流到x”,从而进一步过滤目标。

若删除B.f中的makeA调用,而makeA又没有其他入口,RTA不会因源码中出现 new A 就加入A。扫描所有方法中的分配而不管可达性,会退化为另一种更粗算法,不能仍用上面的迭代过程解释结果。

继承不等于产生一份新的方法 ​

再增加C继承B,但不重写f。new C使C进入已分配类集后,dispatch(C,f)仍是B.f;图中应加入B.f这条边,而不是虚构一个C.f方法节点。多继承、接口默认方法及语言的派发规则需要相应调整lookup,不能只比较类名字。

开放世界的边界 ​

如果外部插件可以稍后装载D并重写f,封闭世界结果不能直接用于删除该派发分支。可以显式把未知实现建成保守外部目标,或在受控的全程序构建中证明类集已经封闭。反射、JNI与回调也需模型;“分析器没看到”不是不可发生的证据。

推论与应用

CHA可靠性的理由是:真实接收者动态类必须满足其静态类型限制,实际派发方法必在枚举结果里。RTA还需对真实执行归纳:入口可达;每次真实分配所在的方法已可达,因此相应类进入 A;真实虚调用的接收者来自某个允许分配,于是目标最终加入 R。环境可以提供预先创建对象时,它们的类也必须列为初始输入,归纳才能成立。

设方法数为 m、候选分配类数为 k、可达调用点数为 c。显式表最多有 ck 次“调用点、动态类”组合,方法和类各只首次入队一次。预先处理继承lookup并对每个组合去重的工作队列,可把主体工作计为程序扫描加 O(ck) 次派发查询;若每次沿深度 h 的继承链查询,需再乘相应的 h。这个界不包括把反射或动态代码转换成模型的成本。

调用图常用于删除不可达代码、估算递归分量、构造过程间控制流图和安全扫描。但方法级图中的一个环只说明存在潜在递归依赖,不证明每次执行都会递归,也不证明递归无法终止。进一步区分调用和返回,应进入上下文敏感的过程间分析。

参考资料
关系图谱7 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系