近年来静态程序分析已成为保障软件可靠性、安全性和高效性的关键技术之一. 指针分析作为基 础程序分析技术为静态程序分析提供关于程序的一系列基础信息,例如程序任意变量的指向关系、变量 间的别名关系、程序调用图、堆对象的可达性等. 介绍了 Java 指针分析的重要内容:指针分析算法、上下文 敏感、堆对象抽象、复杂语言特性处理、非全程序指针分析,特别是对近年来指针分析的研究热点选择性 上下文敏感技术进行了梳理和讨论.
目录
1 指针分析算法
1.1 Java 中的指针
1.2 影响指针的 Java 语句
1.3 算 法
指针分析(pointer analysis),又称指向分析(pointsto analysis)或别名分析(alias analysis),是计算程序中 的指针(或变量、引用)在运行时所能指向的内存位 置(或对象)的一种静态程序分析技术. 指针分析的 结果通常可表示为指针与内存位置之间的指向关系 (points-to relation)或每个指针的指针集(points-to set). 指针分析提供了程序中基础的数据流信息,对于一 系列技术如编译优化[1-3]、故障检测[4-6]、安全分析[7-11]、 程序理解[12-13]、程序验证[14-15] 等具有重要作用. 作为 公认的最基础的静态分析技术之一[1,16] ,指针分析这一研究领域已有超过 40 年的历史[17] ,至今仍是静态 程序分析学术研究的重点.
指针分析通过对程序中语句语义的分析来计算 指针的指向信息,因此指针分析与被分析语言的语 义紧密相关,导致针对不同程序设计语言的指针分 析技术呈现出较大的差异性. 目前,主流指针分析的 研究主要有两大流派,针对 Java 语言[1,3,18-20] 与 C/C++ 语言[21-25] 的指针分析研究. Java 作为一门典型的面向 对象语言,其程序中绝大部分数据分配在堆上;而 C/C++程序中许多数据分配在栈上,通常栈上的数据 仅限于其声明的函数内部使用. 而相比之下,堆上的 数据可以跨函数存在,一般具有更大的活动范围和 生存周期,因此更难以分析. 此外,Java 具有反射、本 地代码调用等 C/C++所没有的语言特性,会给指针分 析造成很大挑战. Java 语言具有广泛的应用生态,近 十年针对 Java 指针分析的研究工作也多于针 对 C/C++的. 因此,本文关注 Java 指针分析技术.
衡量指针分析有效性有 3 个关键指标 :效率 (efficiency)、精度(precision)和可靠性(soundness). 快 速的指针分析运行时间较短,可以更容易部署到实 际生产当中,为相关的应用提供支撑;精准的指针分 析,则可以提升相关应用的准确度(例如精度更高的 指针分析,能使得程序代码在编译时有更多机会被 优化,或使得以它为基础的错误检测工具具备更低 的误报率);可靠性更好的指针分析能覆盖更多程序 行为(例如可使以它为基础的安全漏洞检测工具查 出更多的安全问题). 因此,有效提升效率、精度和可 靠性一直以来是指针分析领域的主要研究问题. 本 文将介绍指针分析提升这 3 个关键指标的经典技术 以及近年来的主要研究进展.
具体而言,相比于已有 Java 指针分析及其综述 工作[1,3] (最近的一篇综述工作发表于 2015 年),本文 的主要贡献包括 3 个方面:
1) 描述了更完整且简洁易懂的 Java 指针分析算法;
2) 讨论了更多关于 Java 指针分析重要内容的研 究工作(例如关于堆抽象、新语言特性处理、增量分 析等方面的最新工作);
3)系统性地梳理并讨论了近年来 Java 指针分析 的研究热点——选择性上下文敏感技术.
本节接下来简要介绍本文后续章节讨论的指针 分析技术所针对的问题,方便读者理解这些研究的 关联性,并了解本文结构.
1 指针分析算法
为了便于介绍 Java 指针分析技术和详细理解指 针分析 ,本文设计了一套较为完整且易于理解的 Java 指针分析算法. 本节先介绍该算法分析的 Java 中的指针和语句,然后详细介绍算法本身.
1.1 Java 中的指针
在指针分析中,我们主要关注引用类型(reference type)指针. 由于原子类型(primitive type)变量与字段 不能指向堆上的对象,因此它们通常不在指针分析 所考虑的范围内. Java 指针可分为 4 类:
1) 局部变量,如 x,即声明在方法内部的变量. 这 也是程序中数量最多的一种指针.
2) 静态字段,如 C.f,静态字段的处理方式与局部 变 量 类 似(文 献 [3] 将 其 称 为 全 局 变 量 (global variable)). 为了简化算法,本文接下来忽略静态字段 及其相关语句的处理.
3) 实例字段,如 x.f,Java 程序中可以写出复杂的 实例字段访问表达式,如 x.f.g.h,但这种复杂的表达 式不易于分析,因此通常在分析前先引入临时变量 将程序转换为三地址码(如语句 v = x.f.g 会被转换为 t = x.f 和 v = t.g;)再进行分析.
4) 数组元素,如 a[i]. 由于许多情况下静态分析 无法获取准确的数组长度以及索引值,因此指针分 析通常会忽略数组长度,且不区分具体的索引值,并 将数组对象建模为只有一个特殊实例字段 arr 的对 象,且该字段指向所有被存入数组的元素,如表 1 的 例子所示.
指针分析对数组进行特殊的建模后,对数组元 素的处理与实例对象一致,因此本文在算法中不再 讨论对数组元素及其相关语句的处理. 综上所述,为了简化算法,本文只考虑局部变量 与实例字段.
1.2 影响指针的 Java 语句
Java 有许多种语句,但在指针分析中,只需要关 注直接影响指针的语句. 当只考虑局部变量与实例 字段时,影响指针分析的语句有 5 种:
1) 对象创建,如 x = new T();
2) 复制,如 x = y;
3) 字段存储,如 x.f = y;
4) 字段读取,如 y = x.f;
5) 方法调用,如 r = x.m(a, …).
这 5 种语句最复杂的是方法调用语句. Java 有 3 种方法调用:静态调用(static invocation)、特殊调用 (special invocation)和虚调用(virtual invocation). 其中 虚调用的处理最为复杂,而静态调用与特殊调用的 处理逻辑均可视为虚调用处理逻辑的简化. 因此本 文关注虚调用的处理.
1.3 算 法
指针分析算法的输入是一个 Java 程序,输出是程序中每个指针可能指向的对象集合,算法运行过 程中在线地解析方法调用,因此运行结束后也输出 程序的调用图. 为了便于理解算法,本文在表 2 中列 出了算法中所用到的符号以及相关域.
Pointer 表示程序中指针的集合. 本文关注变量 与实例字段,因此 Pointer 由变量集合(V)与实例字段 集合(O × F,即对象集合 O 与字段集合 F 的笛卡尔乘 积)组成. 本文用指向关系 pt 表示指针分析的结果, pt(p)表示指 针 p 指向的对象集合 , 即 p 的指针集 (points-to set). 此处 (O)表示对象集合 O 的幂集.
本节介绍的指针分析是一种 Andersen 风格的指 针分析[21] ,即它是流不敏感(flow-insensitive)且基于 子集约束(subset constraint)的指针分析. 流不敏感意 味着指针分析不考虑程序语句的执行顺序[26] ,并将 程序语句视为一个无序集合[1] . 子集约束指程序中的 指针发生赋值时,指针分析将建立相应指针间的子 集约束关系,例如对于语句 x = y,分析将建立约束 pt(y) pt(x)并保证分析结果满足约束[3,21] . 此外,本 节介绍的是上下文非敏感(context-insensitive)指针分 析 ,即不区分每个方法在不同调用上下文(calling context)中指向信息的差别. 本文在第 3 节介绍如何 将本节的算法扩展成上下文敏感(context- sensitive) 指针分析从而提升精度.