DSA:上下文敏感的指针分析/别名分析

Data Structure Analysis (DSA)

Full Paper: [Data Structure Analysis: A Fast and Scalable Context-Sensitive Heap Analysis (2003)]

DSA算法(DataStructure Analysis的首字母缩写)是LLVM的发起人Chris Latter在其硕士、博士系列论文中提出的一个上下文感知(context sensitivity)的、过程间(inter-procedure)的数据结构分析算法。这个算法的强大之处在于可以分析像C这样拥有指针类型的复杂语言,并拥有可观的效率(在http://llvm.org/pubs/可以找到这篇论文“DataStructure Analysis: An Efficient Context-Sensitive Heap Analysis”)。

DSA算法的实现目前在llvm的poolalloc项目下(poolalloc的源代码可通过SVN从http://llvm.org/svn/llvm-project/poolalloc/trunk获取),poolalloc是应用DSA的一个强大的分配池框架。ChrisLatter的论文对此有详尽的描述。据注释显示,DSA的实现尚未稳定,还在剧烈改动中。

DSA算法在llvm的中间表达形式(llvm-IR)的基础上实现,这个中间表达形式的特点是保存了尽可能多的类型信息。而这是DSA能够实现的重要条件(llvm-IR的详尽说明可以参考http://llvm.org/docs/LangRef.html)。

Summary

This paper describes a scalable heap analysis algorithm, Data Structure Analysis, designed to enable analyses and transformations of programs at the level of entire logical data structures. Data Structure Analysis attempts to identify disjoint instances of logical program data structures and their internal and external connectivity properties (without trying to categorize their “shape”). To achieve this, Data Structure Analysis is fully context-sensitive (in the sense that it names memory objects by entire acyclic call paths), is field sensitive, builds an explicit model of the heap, and is robust enough to handle the full generality of C. Despite these aggressive features, the algorithm is both extremely fast (requiring 2-7 seconds for C programs in the range of 100K lines of code) and is scalable in practice. It has three features we believe are novel: (a) it incrementally builds a precise program call graph during the analysis; (b) it distinguishes complete and incomplete information in a manner that simplifies analysis of libraries or other portions of programs; and © it uses speculative field-senstivity in type unsafe programs in order to preserve efficiency and scalability. Finally, it shows that the key to achieving scalability in a fully context-sensitive algorithm is the use of a unification based approach, a combination that has been used before but whose importance has not been clearly articulated.

1. 概要

别名分析在指导传统的低级内存优化方面取得了很大的成功。别名分析提供了一种消除内存引用对的歧义的转换过程,并能识别语句的局部和过程间副作用。相比之下,应用于复杂数据结构(如列表、堆或图)的整个实例的转换的成功率要低得多。将指针分析/别名分析应用于复杂数据结构还需要一些强大的分析功能的支持:

  • Full Context-Sensitivity: 需要使用分析算法来区分通过程序中不同调用路径创建的堆对象(即,通过整个非循环调用路径命名对象),识别数据结构的不相交实例。
  • Field-Sensitivit: 需要区分不同结构字段的属性点,以识别数据结构的内部连接模式。
  • Explicit Heap Model: 分析堆数据结构需要构造一个显式堆模型,包括标识别名不直接需要的对象。

由于潜在的成本,实际的别名和指针分析算法尚未尝试提供上述属性的组合。相比之下,“Shape Analysis”算法强大到足以提供这些信息和更多信息(例如,足以将特定结构标识为“链表”或“二叉树”)。然而,到目前为止,形状分析在商业优化编译器中还没有被证明是实用的。

本文提出的DSA算法是一个上下文敏感(context-sensitivity)的、过程间(inter-procedure)的数据结构分析算法,适用于不相交的逻辑数据结构实例的转换,它提供了上面列出的三个必需功能。这个算法的强大之处在于可以分析C/C++这类拥有指针类型的复杂语言,并拥有可观的效率。DSA算法在LLVM的中间表达形式(LLVM-IR)的基础上实现,LLVM IR的特点是保存了尽可能多的类型信息。而这也是DSA能够实现的重要条件。

DSA有三个Novel的特征:

  • 它在分析过程中逐步建立一个精确的程序调用图。该算法是完全非迭代的,在分析过程中只访问每条指令和每个调用边一次。
  • 该算法明确区分了完整信息和不完整信息,使其即使在分析的中间阶段也是保守的,并允许它安全地分析程序的某些部分。
  • 该算法通过假设程序中的内存对象在显示之前是类型安全的,从而提供了推测字段敏感性。这使得该算法对以类型安全方式访问的对象(常见情况)具有完全的字段敏感性。

2. 数据结构图 (Data Structure Graph)

程序中的每个函数都会计算出一个数据结构图(DS图),图中总结函数中可访问的内存对象及其连接模式。每个DS图节点表示一组(可能是无限的)内存对象,不同的节点表示不相交的对象集,即图是内存对象的有限静态分区。所有可以由单个指针变量或字段指向的动态对象都表示为图中的单个节点。

一个数据结构图可以形式化表示为一个有限有向图 DSG(F)
DSG(F) = (N, E, E_v, C), 其中

N代表有向图中顶点的集合,图中每个顶点都表示一组内存对象;
E代表有向图中的边的集合,边的source和target都是DS node的fields(E的类型是[n_s,f_s]->[n_d,f_d]);
E_v代表vars(f)->[n,f]的函数,其中vars(f)是函数f中虚拟寄存器的集合;E_v(v)是从寄存器v到field[n,f]的边,它们之间是points-to关系
C是图中的一组“调用节点”,表示当前函数上下文中未解析的调用位置。每一个调用节点c∈C都是一个k+2元组:(r,f,a_1,...,a_k),f是被调用的函数,r是f的返回值,a_1...a_k是函数f的参数中的pointer-compatible变量

为了演示DS图和DSA分析算法,文章使用图1中的代码作为运行示例。本例使用迭代、递归、函数指针、指向子对象的指针和全局变量引用创建并遍历两个不相交的链表。尽管示例很复杂(这个例子太复杂了,笔者看了半天才看懂),但DSA分析能够证明两个列表X和Y是不相交的。
在这里插入图片描述

本文使用图2中所示的图形符号呈现DS图。
在这里插入图片描述

DS图中的DS节点负责表示与该节点对应的一组内存对象的信息。每个节点n有三条与之关联的信息:

T(n) 代表由n表示的存储器对象的Type
G(n) 表示一组(可能是空的)全局对象,即节点n表示的所有对象。
flags(n) 是与节点n相关联的一组标志。下面定义了八个可能的标志(h、s、g、u、m、r、c和o)。标志(n)中的“H”、“S”、“G”和“U”标志用于区分四类内存对象:堆分配、堆栈分配、全局(包括函数)和未知对象。特定的内存对象是否在当前的分析范围内被修改或读取,这通过“M”和“R”标志表示。信息完整用“C”标志表示。

图3显示了在应用任何过程间信息之前为do all和addG函数计算的示例图。该图包括一个调用节点的示例,该节点(在本例中)调用FP指向的函数,将L指向的内存对象作为参数传递,并忽略调用的返回值。
在这里插入图片描述
这个步骤还没有执行过程间分析,只是一个过程内分析,因此很多信息都是不全的。例如,在这个函数中,它可以确定L被视为一个列表对象(构造算法关注指针是如何使用的,而不是它们声明的类型是什么),但是它无法知道它为这个内存对象在更大的范围内是否正确。为了处理这种情况,DSA分析计算图中哪些节点是“完整的”,并用C标志标记节点。另一种情况是节点上某个对象可能存在类型安全冲突,则假定T(n)=void*,将节点标记为O,此时节点的field传出边被合并为一个。下面的伪代码描述了DSA是怎么处理collapse这种情况的:
在这里插入图片描述

3. DS图的构造算法

DS图的创建和优化过程一共可以分为三个步骤:

  • 第一阶段,为程序中的每个函数构造一个DS图,只使用过程内信息(“局部”图)。
  • 第二阶段,使用“自底向上”的分析消除DS图中由于函数中的被调用函数而导致的不完整信息,通过将被调用图中的信息合并到函数调用方的DS图中(此时第二阶段创建的图称为“BU”图)。
  • 第三阶段,使用“自顶向下”的分析将调用者的BU图合并到被调用者(创建“TD”图)来消除由于传入参数导致的不完整信息。BU和TD阶段对调用图中的“已知”强连接组件(scc)进行操作。
3.1 图的基本操作

该算法对DS图使用了几个基本操作,如图4所示。
在这里插入图片描述

这几个基本操作在算法中的意义简单解释一下:

  • mergeCells: 合并两个<node, field>对,这需要合并类型信息,flags,全局变量,两个节点的输出边缘,并将输入边缘移动到结果节点。;
  • cloneGraphInto: 合并两个节点,并以指定方式对齐字段;
  • resolveCallee: 将被caller的图内联到calleee的图中;
  • resolveCaller: 将被callee的图内联到caller的图中;
  • resolveArguments: 合并参数和返回值以完善函数调用信息(针对全局图)。
3.2 构建DS图(局部)

构建局部DS图阶段在无需任何有关Caller和Callee的信息为每个函数计算一个局部的DS图。函数F的局部DS图的构建算法如下图所示。LocalAnalysis函数首先为每个指针兼容的虚拟寄存器(在map E_v中输入它们)创建一个空节点作为目标,并为每个全局变量创建一个单独的节点。然后,分析对函数的指令进行线性扫描在malloc和alloca操作中创建新节点,在赋值和return指令中合并变量的边,并在特定定的操作中更新类型信息。
在这里插入图片描述

一个cell的信息E_v(Y)既<node, field>,仅当实际对Y的进行解引用操作(store或load)时以及在索引到结构或 Y指向的数组时,才被更新,即如下情况:

case X = &Y -> Z:
    updateType(E_v(Y), typeof(*Y))
case X = &Y[idx]:
    updateType(E_v(Y), typeof(*Y))

malloc,alloca和cast操作仅创建一个void类型的节点。结构体字段的访问调整传入边以指向地址字段。忽略对数组对象的索引,即数组被视为具有单个元素。return指令的处理是通过创建一个特殊的虚拟寄存器用于捕获返回值

函数调用会将一个新的调用节点添加到DS图中,并返回函数指针(用于直接调用和间接调用)。例如,图7(a)显示了addGTList函数的局部DS图,其中为调用的函数do_all创建了调用节点(为正确合并类型信息,将为每个条目创建一个空节点,然后使用mergeCells进行合并,因为参数类型可能与为正式参数或返回值声明的类型不匹配)。
在这里插入图片描述

局部DS图构造的最后一步是计算哪些DS节点是Complete的。对于可以从形式参数、全局参数访问的节点可能不会标记为C;对于可以传递一个参数到函数调用的节点可能不会被标记为C;对于返回一个值到调用函数的节点可能不会被标记为C。这反映了局部分析阶段没有任何过程间信息。

3.3 自底向上的分析阶段
自底向上(BU)分析阶段通过合并来自每个函数被调用者的过程间信息来优化每个函数的局部DS图。BU分析的结果是每一个函数有一个BU图,它总结了当没有上下文信息时调用该函数调用该函数(强制别名和mod/ref信息)的总体效果。它通过将所有已求解的callee的BU图克隆到caller的局部图中,合并由相应的形式参数和实际参数指向的节点来计算此图。

考虑函数F的形式参数f_1…f_n,其中传递的实际参数是a_1…a_n。图4中的函数resolveCallee显示了在BU阶段如何处理形式参数和实际参数的合并。首先为F复制BU图,清除所有的堆栈节点标记,因为被调用方的堆栈对象在调用方中不可合法访问。然后,我们将指针兼容类型的每个实际参数ai指向的节点与fi指向的节点的副本合并。我们还将合并调用节点中的返回值和来callee的返回值节点的副本。此外,F的BU图中的所有未解决的节点都将复制到caller的图中,并且callee的图中表示未解决的函数调用的参数的所有对象现在也都在caller中表示。

图8显示了用于遍历调用的完整的“自底向上”算法。注意,该阶段都不会重新访问以前访问的函数,但是该调用节点最终将在自顶向下的阶段得到解决。本文针对四种不同情况进行了解释。
在这里插入图片描述

  1. 在最简单的情况下,仅对非外部函数进行直接调用,没有递归并且没有函数指针的情况下,每个DS图中的调用节点都隐式定义了整个调用图。 BU阶段只需按后序遍历函数调用图(在调用者之前访问被调用者),如上所述将callee的BU图克隆和内联到caller的图中即可。
  2. 为了支持具有函数指针和外部函数(但不递归)的程序,仅将后序遍历限制为仅在其函数指针指向Complete节点(即,其目标已完全解析)的情况下处理调用站点。如果已知传递给函数指针参数的函数的情况下,此类caller才可能会解析。 例如例子中对FP的调用无法在函数do_all中求解,而是可以在BU图中针对函数addGToList进行解析,在此我们得出结论,将addG的BU图内联到addGToList的图中,得出的最终图如图7(c)所示。其中L指向的节点中的Modified标志是从addG的节点EV(X)(图3)获得的,该标志与从do all内联的第二个参数变量节点合并。
  3. 不带函数指针的递归情况(感兴趣的读者可以阅读原文)
  4. 有函数指针的递归情况(带有间接调用的递归函数,感兴趣的读者可以阅读原文)

在这里插入图片描述

图10的图显示了示例的main函数的BU图。 该图具有X和Y指向的list的不相交的子图。 由于我们克隆了该对象,然后为每个对addGToList()的调用内联了BU图,因此证明它们是不相交的。 这显示了上下文敏感性与克隆的组合如何识别不相交的数据结构,即使涉及复杂的指针操作也是如此。
在这里插入图片描述

3.4 自顶向下的分析阶段

与自底向上类似,不同的是将caller内联到calee中

3.5 算法复杂度分析

本文阅读难度较大,整体晦涩难懂,笔者也没有对算法复杂度进行更多的思考,就简单列一下吧。

  • 最坏时间复杂度:O(na(n)+ka(k)e)
  • 最坏空间复杂度:O(fk)

其中n是指令的数量,k是单个过程的数据结构图的最大大小,e是函数调用图中边的数量,f是函数的总数量。实际上k应该非常小,通常是100个或更少,即使在大型程序中也是如此。

4. 实验评价

作者在35个C程序上对DSA算法进行了评估,结果表明该算法在实际应用中(在性能和内存消耗方面)是非常有效的。这包括包含复杂堆结构、递归和函数指针的程序。例如,分析povray31(一个由130000多行代码组成的程序)只需要不到8秒的分析时间和大约16MB的内存。
在这里插入图片描述

5. 总结

本文提出了一种堆分析算法,用于对递归数据结构的不相交实例进行分析和转换。该算法使用了多种技术的组合,以平衡堆分析精度(上下文敏感、克隆、字段敏感和显式堆模型)和效率(流不敏感、统一和完全非迭代分析)。通过对整个递归数据结构进行操作,该算法能够实现指针密集型代码的分析和转换的新方法(用较弱但更有效的方法实现形状分析的一些目标)。实验结果表明,该算法在实际应用中速度非常快,占用的内存非常少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值