适用于百万行代码的流敏感指针分析

图片

Flow-Sensitive Pointer Analysis For Millions of Lines of Code

适用于百万行代码的流敏感指针分析

1,背景

传统的流敏感的指针分析是在CFG(control-flow graph)上进行的,CFG是一个有向图

图片

图片

图片

。为了确保分析的可判定性,我们不解释分支条件,因此分支被视为不确定的,采用保守的分析。

CFG上的每个节点K维护两个指向图(指向图也就是指向关系的集合):INk表示进入节点K的指针信息,OUTk表示从节点K出去的指针信息。每个节点有一个传递函数将INk传递到OUTk,传递函数的一个特点就是采用了两个集合:GENk和KILLk,分表示节点k产生的指针信息和删除的指针信息,这两个集合的内容依赖于节点k关联的程序语句的语义,并且随着新指针信息的积累,内容可以在分析过程中变化。对于所有节点k,指针分析迭代计算以下两个函数,直到收敛:

图片

对于一条程序语句k,如果关联的程序语句是x = y,则

图片

→_,表示x指向的所有信息),即kill掉,被新的关系替代(即gen出来)。

对于一条程序语句k,如果关联的程序语句是*x = y,则情况将比较复杂。如果x被定义仅仅指向一个单一的对象z,那么

图片

分析任然执行一个强更新。然而,如果x可能指向多个具体的内存地址,那么

图片

,是一个空集,分析执行一个弱更新,分析不能确定哪个具体的内存地址被这个赋值语句更新。因此,添加了新的信息(即GEN产生的),但保守地保留了所有现有的指向关系。 

因此,添加了新的信息(即GEN产生的),但保守地保留了所有现有的指向关系。    

一个指针在下面情况下可能指向多个具体的内存地址:

(1)该指针的指针集包含多个具体的内存地址;

(2)该指针是一个heap变量,这将在下面解释说明;

(3)递归函数中的局部变量,在栈上运时将会产生多个对象实例。

任何指针分析的一个重要方面就是堆模型,它将概念上无限大小的堆抽象为一组有限的内存位置。我们采用的常见做法是将每个静态内存分配位置视为一个不同的抽象内存位置(在程序执行过程中可能映射到多个具体内存位置)。 

2,SSA

SSA,static single assignment,静态单赋值,每个变量在程序中只定义一次,原始程序中具有多个定义的变量被拆分为单独的实例,每个实例对应一个定义。在CFG中的汇聚点处,使用φ函数组合同一变量的不同实例,生成对该变量的新实例的赋值。SSA形式是执行稀疏分析的理想形式,因为它显式地表示定义-使用(def-use)信息,并允许数据流信息直接从变量定义流到相应使用的地方。

向SSA形式的转换由于间接defs的存在而变得复杂,并且是通过指针来使用的,而这些指针只能通过指针分析来发现。因为得到的指针信息是保守的,所以每个间接的def和use实际上都是一个可能的def或use。跟随Chow et al [8],我们使用χ和μ函数来表示这些可能的defs和uses。在原始程序表示的每一个间接store(例如,*x = y)中,对于每一个可能被store定义的变量v,我们用一个函数 v = χ(v)来注释。类似的,对于每一个可能被load(例如,x =* y)访问的变量v,我们用一个函数μ(v)来注释。当转换为SSA形式时,每个χ函数被视为给定变量的定义和使用,每个μ函数都被视为对给定变量的使用。    

为了避免处理间接stores和loads的复杂性,现代编译器如GCC、LLVM使用SSA的一个变体,我们叫做partial SSA形式。其思想是将变量分为两类:一类是包含从不被指针引用的top-level variables(顶级变量),因此它们的定义和使用可以通过检查来简单地确定,而不需要指针信息,这些变量可以使用任何构造SSA形式的算法转换为SSA。第二类包含那些可以被指针引用的变量(address-taken variables),为了避免上述复杂情况,这些变量不以SSA形式放置。       

3,LLVM IR

我们的实现使用LLVM编译器基础设施,因此为了使我们的讨论具体化,我们现在描述LLVM的内部表示(IR)及其partial SSA形式的特定实例化。虽然细节是LLVM特有的,但这些想法可以转换为partial SSA的其他形式。在LLVM中,top-level variables保存在概念上无限的虚拟寄存器集中,这些寄存器以SSA形式进行维护。address-taken variables保存在内存中而不是寄存器中,并且它们不是SSA形式。使用ALLOC(内存分配)和COPY指令修改top-level variables。address-taken variables通过LOAD和STORE指令访问,这两条指令将top-level variables作为参数。address-taken variables在IR中从未被语法引用,而是仅使用这些LOAD和STORE指令来间接引用它们。LLVM指令使用三地址格式,因此每条指令最多有一个指针解引用级别(通过引入临时变量,具有多个间接级别的源状态被简化为一个指针解引用级别)。    

图片

图1给出一个C代码片段和它对应的partial SSA形式,变量w、x、y和z是top-level variables,已转换为SSA形式;变量a、b、c和d是address-taken variables,因此它们存储在内存中,仅通过LOAD和STORE指令访问。因为address-taken variables不是SSA形式的,所以它们可以分别定义多次,就像示例中的变量c和d一样。由于无法直接命名address-taken variables,LLVM将对它保持不变,即每个address-taken variable至少有一个仅引用该变量的虚拟寄存器。本文的其余部分将假设使用LLVM IR,这意味着address-taken variable只能由LOAD和STORE指令定义或使用。       

4、稀疏流敏感指针分析SFS(Sparse Flow-Sensitive Pointer Analysis)

SFS的主要数据结构是def-use图(DUG),它包含每个程序语句的一个节点,并包含表示def-use链的边,如果在节点x中定义变量并在节点y中使用,则存在从x到y的有向边。top-level variables的def-use边对于从程序的检查中确定是非常简单直接的。address-taken variables的address-taken variables边需要AUX(auxiliary analysis)进行计算。第一步是使用AUX的结果将address-taken variables转换为SSA形式,既然STORES和LOADS已经使用了χ和μ函数进行注释,采用任何标准的SSA算法可以将程序转换为SSA形式。图2显示了一个示例程序片段以及采用AUX计算的一些指针信息。    

图片

图3显示了用χ和μ函数注释并翻译成SSA的相同程序片段,注释步骤直接来自AUX计算的指针信息,例如,对于CFG节点*p=w,我们为指针p的指针集合中的每个变量(即a)添加一个χ函数,

图片

。对于CFG节点u=*v,我们为指针v的指针集中的每个变量(即e和f)添加一个μ函数,

图片

。其他节点以类似的方式进行注释,一旦对所有节点进行了注释,就可以使用任何标准SSA算法来导出SSA形式。    

图片

相对于由主要分析计算的更精确的流敏感信息来说,AUX计算的def-use信息是保守的。因此,对于一个增加了注释

图片

的STORE指令*x = y ,必须解决下面三种可能性:

(1)在流敏感的结果中,x可能不指向v。在这种情况下,vm应该是vn的副本,并且不会并入y的任何信息。

(2)在流敏感的结果中,x可能仅指向v。在这种情况下,对v的指针集进行强更新,也就是说,vm应该是y的副本,并且不包含vn的任何信息。

(3)x可能指向v以及流敏感结果中的其他变量。在这种情况下,对v的指针集进行弱更新,也就是说,vm应该包含来自vn和y的指向信息。    

通过使用计算出的SSA信息来构建def-use图,我们可以适应所有这些可能性。要创建DUG,我们必须从每个间接定义(即STORE)到可能使用这个间接定义变量的每个语句之间增加一条边。因此,我们为每个用函数vm=χ(vn)注释的STORE创建一个从该STORE到每个使用vm作为参数的χ、μ或φ函数的语句之间定义一个def-use边,我们用address-taken variables变量的名称标记与address-taken variables相对应的每个def-use边,当传播指针信息时,分析只沿着标有变量名称的边传播变量的信息,图4显示了根据图3中的SSA信息转换为DUG的图2中的程序。

图片

          

原则上,实际的流敏感分析与第一节中描述的传统流敏感分析非常相似,但有以下情况除外:(1)分析沿着def-use边而不是控制流边传播信息;(2)变量的信息仅沿标有该变量名称的边传播。         

5、访问等效性AE(Access Equivalence)

使用上述技术时立即出现的一个困难是可能需要大量的def-use边,每个LOAD和STORE可以访问数千个变量,基于解引用变量的指针集来设置大小,每个变量可以在程序中的几十个或几百个位置访问。在大型基准测试中,可能会创建数亿个def-use边,太多以至于无法进行可扩展的分析。为了解决这个问题,我们引入了访问等效(access equivalence)的概念,以更紧凑的方式表示相同的信息。如果无论何时LOAD或STORE指令访问一个变量,另一个变量也是,则两个address-taken variable变量x和y是访问等效的,换句话说,对于所有变量v,使得v在LOAD或STORE中被解引用,

图片

图片

,这种对等概念与Hardekopf和Lin所描述的位置对等概念(location equivalence)相似,但并不完全相同,不同点是:位置对等检查程序中的所有指针,以确定两个变量是否相等,而访问等效性只关注在LOAD或STORE中解用的指针。两个变量可以是访问等效的,而不是位置等效的(但相反不成立)。    

访问等价的优点是SSA算法将为所有访问等价变量计算相同的def-use链,这将很容易看到:根据定义,任何定义一个变量的STORE也必须定义所有访问等效变量,类似地,任何使用一个变量也必须使用所有访问等价变量的LOAD。

要使用AUX确定访问等效性,我们必须识别由同一组LOAD和STORE访问的变量,设AE是从address-taken variable到指令集的映射。对于每个LOAD或STORE指令I,以及对于I访问的每个变量v,AE(v)包括I。一旦处理完所有指令,如果AE(x)=AE(y),那么任意两个变量x和y都是访问等价的。一旦address-taken variable被划分为访问等价类,DUG的边就会使用分区而不是变量名重新标记,对于图2,访问等效项为:{a}, {b, d}, {c}, 和 {e, f},图5显示了与图4相同的def-use图,不同之处在于同一分区中访问等效变量的边折叠成一条边。    

图片

6、过程间的分析

有两种可能的方法可以将上述分析扩展到过程间分析。

第一种选择是分别计算每个函数的稀疏性,将函数调用视为:将callee定义的所有变量的当作def,将callee使用的所有变量的当作use。这种方法的缺点是def-use链可以跨越许多函数,将定义和实际使用之间的每个函数调用视为集合点可能会对分析的稀疏性产生不利影响。

第二种选择,也是我们选择的一种,是将整个程序的稀疏性计算为一个单元,直接连接变量定义和使用,甚至跨函数边界的使用。这种方法的一个重要考虑因素是通过函数指针处理间接调用的方法,一些跨越多个函数的定义使用链可能取决于间接调用的解析,我们所给出的分析并不能弥补这个问题,假设def-use链仅依赖于指令所使用的指针的指针集,而不考虑对不相关函数指针集的任何额外依赖。换言之,如果AUX计算的调用图与流敏感指针分析计算的调用图形过于近似,则此技术可能会失去精度。

7、算法

将所有内容放在一起,稀疏流敏感指针分析的最终算法从计算DUG的一系列预处理步骤开始:

(1)运行AUX来计算正在分析的程序的保守的def-use信息。使用AUX的结果来计算程序的过程间控制流程图(ICFG),包括对其潜在目标的间接调用的解决方案。然后,所有函数调用都被转换为一组COPY指令,以表示参数分配。类似地,函数返回被转换为COPY指令。

(2)计算所有top-level variable的确切SSA信息。这个步骤计算的φ-函数被转换成COPY语句,例如

图片

转换为x1 = x2 x3,这将这些语句与下面步骤4中为address-taken variable计算的φ-函数区分开来。将地址获取变量划分为访问等价类。

(3)对于每个分区P,使用AUX的结果来标记每个STORE,该STORE可以用函数 P=χ(p)修改P中的变量,用函数μ(P)标记每个可能访问P中变量的LOAD。

(4)使用多种可用方法中的任何一种来计算分区的SSA形式。

(5)通过为每个指针相关的指令和步骤4创建的每个φ函数创建一个节点来构建def-use图,然后:

对于每个ALLOC、COPY和LOAD节点N,将来自N的未标记的边添加到使用由N定义的top-level variable的每个其他节点。

对于具有定义分区变量Pn的χ函数的每个STORE节点N,将来自N的边添加到使用Pn的每个节点(在φ、χ或μ函数),并由分区P标记。

对于定义分区变量Pn的每个φ节点N,为每个使用Pn的节点创建一个未标记的边。

          

一旦预处理完成,就可以开始稀疏分析了。分析使用以下数据结构:    

(1)有一个节点工作列表Worklist,初始化为包含所有ALLOC节点。

(2)有一个全局指针图FG,包含了所有top-level variable的指针集,设Ptop(v)是为top-level variable变量v的指针集。

(3)每个LOAD和φ-函数k都包含一个指向图INk,以保存该节点可能访问的所有address-taken variable的指针信息。设Pk(v)是包含在INk中的address-taken variable变量v的指针集。

(4)每个STORE节点k包含两个指向图,以保存该节点可以定义的所有address-taken variable的指针信息:INk用于输入指针信息,OUTk用于输出指针信息。设Pk(v)是INk中的地址取address-taken variable变量v的指针集合。我们提升Pk()对地址获取变量集进行的运算,使其结果是该集中每个变量指针集的并集。

(5)对于每个address-taken variable变量v,part(v)返回v所属的变量分区。

   指向图的点都被初始化为空。循环迭代地从工作列表中选择和处理一个节点,在此期间,可以将新节点添加到工作列表中,循环一直持续到工作列表为空,此时分析完成。这个

图片

运算符表示集合更新,→表示def-use图中的未标记边,

图片

x表示用x标记的边。

图片

    

图片

这里的

图片

表示x指向一个对象,

图片

表示def-use边,下同。

图片

 

 

图片

这里是处理address-taken variable变量的phi函数。

              

8、参考论文    

【1】B. Hardekopf and C. Lin. Flow-sensitive pointer analysis for millions of lines of code. In CGO '11.

【2】F. Chow, S. Chan, S. Liu, R. Lo, and M. Streich. Eective representation of aliases and indirect memory operations in SSA form. In CC '96.

【3】S. Cherem, L. Princehouse, and R. Rugina. Practical memory leak detection using guarded value-ow analysis.In PLDI '07.    

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值