指针分析能够用于空指针引用、内存泄漏、内存释放后重用等缺陷分析,是重要的静态代码分析方法之一。流敏感(Flow-Sensitive)、流不敏感(Flow-Insensitive)、上下文敏感(Context-Sensitive)、上下文不敏感(Context-Insensitive)以及流敏感+上下文敏感组合、流敏感+上下文不敏感组合分析算法是常用的指针分析方法。这里仅讨论基于流敏感的指针分析方法,其他分析方法读者可参考相关文献。
流敏感是指基于程序语句执行顺序,如数据流分析的指针别名(Pointer Alias)分析过程中,一个流不敏感指针别名分析可能得到“变量 x 和 y 可能指向同一位置”,流敏感指针别名分析得到的结论则类似于“在执行第20条指令后,变量 x 和 y 可能指向同一位置”。一个流不敏感指针别名分析不考虑控制流,且认为所发现的别名在程序所有位置均成立。
程序控制流图上的每个节点
k
维护两个指向图:
表示进入节点
k
的指针信息,
表示从节点
k
流出的指针信息。每个节点都有一个传递函数将
传递到
。传递函数的一个特点就是采用两个集合
和
分别表示节点
k
产生的指针信息和删除的指针信息,此两个集合的内容依赖于节点
k
关联程序语句的语义,随着新指针信息的积累,在分析过程中,其内容发生变化。对于所有节点
k
,指针分析迭代计算如下函数,直至收敛
(5-5)
(5-6)
对于一条语句
k
,如果关联语句是
x
=
y
,则
表示
x
指向的所有信息被杀(kill)掉,即被新的关系替代。对于一条语句
k
,如果关联的语句是*
x
=
y
,则情况将变得比较复杂。如果
x
被定义为仅仅指向一个单一的对象
z
,那么
。因此,虽然添加了新的信息,但保守地保留了所有现有指向关系。在如下情况下,一个指针可能指向多个具体的内存地址:
(1)指针的指针集包含多个具体的内存地址。
(2)指针是一个heap变量。
(3)递归函数中的局部变量,在栈上运行时将产生多个对象实例。
堆模型是指针分析的基础,它将概念上无限大小的堆抽象为一组有限的内存位置。基本做法是将每个静态内存分配位置视为一个不同的抽象内存位置,并在程序执行过程中映射到多个具体内存位置。
对于静态单赋值(Static Single Assignment,SSA),每个变量在程序中只定义一次,原始程序中具有多个定义的变量被拆分为单独的实例,每个实例对应于一个定义。在控制流图中的汇聚点处,使用
函数组合同一变量的不同实例,生成对该变量新实例的赋值。SSA形式显式地表示定义−使用(def-use)信息,并允许数据流信息直接从变量定义流到相应使用的地方,是执行稀疏分析的理想形式。
向SSA形式的转换是通过指针使用实现的,且指针只能通过指针分析发现,因为间接def的存在,使得这种转换异常复杂。由于得到的指针信息是保守的,实际上,每个间接的def和use都是一个可能的def或use。我们使用
X
和
μ
函数表示这些可能的def和use,在原始程序表示的每一个间接STORE,如*
x
=
y
中,对于每一个可能被STORE定义的变量
v
,用一个函数
v
=
X
(
v
) 进行注释。类似地,对于每一个可能被LOAD如
访问的变量
v
,则用一个函数
注释。当转换为SSA形式时,每个
X
函数被视为给定变量的定义和使用,每个
函数则被视为对给定变量的使用。
为了有效避免处理间接STORE和LOAD的复杂性,GCC、LLVM等编译器使用SSA的一个变体,称为不完全SSA形式。变量可分为两类:一类是包含从不被指针引用的顶级变量(Top-Level Variables,TLV),其定义和使用可以通过检查予以简单确定,无须指针信息,这些变量可以使用任何构造SSA形式的算法转换为SSA;另一类是包含那些可以被指针引用的变量(Address-Taken Variables,ATV),为避免上述复杂情况,这些变量不以SSA形式放置。
在LLVM中,TLV保存在概念上无限的虚拟寄存器中,这些寄存器以SSA形式维护。ATV以非SSA形式保存在内存中而非寄存器中,使用ALLOC和COPY指令修改TLV。ATV通过LOAD和STORE指令访问,这两条指令将TLV作为参数。ATV在内部表示IR从未被语法引用,而是使用LOAD和STORE指令来间接引用。LLVM指令使用三地址格式,因此每条指令最多有一个指针解引用级别,通过引入临时变量,具有多个间接级别的源状态被简化为一个指针解引用级别。图5-32给出了一个C代码片段及其对应的不完全SSA形式示例。
图5-32中,变量
w
、
x
、
y
、
是TLV,已转换为SSA形式;变量
a
、
a
、
c
、
d
是ATV,因为它们存储在内存中,仅通过LODA和STORE指令访问。因为ATV为非SSA形式,它们可以分别被定义多次,就像变量
c
、
d
一样。由于无法直接命名ATV,LLVM将对它保持不变,即每个ATV至少有一个仅引用该变量的虚拟寄存器。
图5-32 一个C代码片段及其对应的不完全SSA形式示例
稀疏流敏感指针分析(Sparse Flow-Sensitive Pointer Analysis,SFS)的主要数据结构是def-use图,它包含程序语句的一个节点以及表示def-use链的边,如果在节点
中定义变量并在节点
中使用,则存在从
到
的有向边。TLV的def-use边,可以通过程序检查确定。ATV的边则需要通过辅助分析进行计算。第一步是使用辅助分析结果,将ATV转换为SSA形式,既然STORE和LOAD已经使用了
和
函数进行注释,则采用任何标准的SSA算法,都可以将程序转化为SSA形式。图5-33给出了一个示例程序片段及采用辅助分析计算的指针信息。
图5-33 一个示例程序片段及采用辅助分析计算的指针信息
图5-34给出了用
和
函数注释并翻译成SSA的相同程序片段,注释来自于辅助分析计算的指针信息。例如,对于控制流图节点
,为p的指针集合中每个变量(a)添加一个
函数,
。对于控制流图节点
,为v的指针集合中每个变量(e和f)添加一个
函数
。其他节点都可以通过类似方式进行注释。一旦对所有节点都进行了注释,就可以使用任何标准SSA算法导出其SSA形式。
图5-34 用
和
函数注释并翻译成SSA的相同程序片段
相对于分析计算更加精确的流敏感信息,辅助分析计算def-use信息则偏于保守。因此,对于一个增加了注释
的STORE指令
,必须解决如下3个方面的问题:
(1)在流敏感结果中,
可能并不指向
。在这种情况下,
是
的副本,且不会并入
的任何信息。
(2)在流敏感结果中,
可能仅指向
。在这种情况下,对
的指针集进行强更新,
是
的副本,且不包含
的任何信息。
(3)
可能指向
及流敏感结果中的其他变量。在这种情况下,对
的指针集进行弱更新,
包含来自
和
y
的指向信息。
通过计算得到的SSA信息,能够构建def-use图。但要创建def-use图,必须从每个间接定义,即STORE到可能使用这个间接定义变量的每个语句之间,增加一条边。为每个用函数
注释的STORE,创建一个从该STORE到每个使用
作为参数的
X
、
、
函数的语句之间,定义一条def-use边,用ATV变量的名称标记与ATV相对应的每条def-use边,当传播指针信息时,分析只沿着标有变量名称的边传播变量的信息即可。图5-35展示了由图 5-34转换而成的def-use图。
图5-35 由图5-34转换而成的def-use图
在实际应用中,可能需要大量def-use边,而且每个LOAD和STORE都可以访问数以千计的变量,基于解引用变量的指针集来设置大小,每个变量都可以在程序中的数十或数百个位置进行访问。在大型基准测试中,可能会创建数以亿计的def-use边,以至于无法实现可扩展分析。为此,引入访问等效(Access Equivalence,AE)的概念,以便以紧凑的方式表示相同的信息。也就是无论何时,用LOAD或STORE指令访问一个变量时,两个ATV变量
和
访问等效。即对于所有变量
,使得
在LOAD或STORE中被解引用,
,这种对等概念与Hardekopf和Lin所描述的位置对等概念相似,其不同点为:通过位置对等,检查程序中的所有指针,以确定两个变量是否相等,而访问等效性只关注在LOAD或STORE中解所使用的指针。两个变量的访问等效是一种非位置等效。访问等效的优点是SSA算法将为所有访问等效变量计算相同的def-use链。根据定义,任何定义一个变量的STORE也必须定义所有访问等效变量。类似地,当使用一个变量时,必须使用所有访问等效变量的LOAD。
使用辅助分析确定访问等效性,必须识别由同一组LOAD和STORE访问的变量。设AE是从ATV到指令集的映射,对于每个LOAD或STORE指令I,以及对于I访问的每个变量
,
包括I。一旦处理完所有指令,如果
,那么任意两个变量
和
是访问等价的。一旦ATV被划分为访问等价类,def-use图的边就会使用分区而不是变量名重新标记。对于图5-33,访问等效项为:
、
、
、
。图5-36展示了与图5-35相同的def-use图,其不同之处在于同一分区中访问等效变量的边折叠成了一条边。
图5-36 基于访问等效性分析的def-use图
将所有内容放在一起,稀疏流敏感指针分析算法从计算def-use图的一系列预处理开始。
(1)通过辅助分析计算程序保守的def-use信息,通过辅助分析结果计算程序的过程间控制流程图(Interprocedural Control-Flow Graph,ICFG),包括对其潜在目标间接调用的解决方案。然后,所有函数调用被转换为一组COPY指令,用以表示参数分配。类似地,函数返回亦被转换为COPY指令。
(2)计算所有TLV确切的SSA信息。这个步骤所计算的
函数被转换成COPY语句。例如,将
转换为
,将地址获取变量划分为访问等价类。
(3)对于每个分区
P
,使用辅助分析结果标记每个STORE,该STORE可以使用函数
修改
P
中的变量,用函数
标记每个可能访问
P
中变量的LOAD。
(4)使用多种可用方法中的任一方法计算分区的SSA形式创建
函数。
(5)通过为每个指针相关的指令和步骤(4)创建的每个
函数,创建一个节点,构造def-use图。然后,对于每个ALLOC、COPY和LOAD节点
,将来自
的未标记的边添加到使用由
定义TLV的每个其他节点;对于具有定义分区变量
的
函数的每个STORE节点
,将来自
的边添加到使用
的每个节点,并由分区
P
标记;对于定义分区变量
的每个
节点
,为每个使用
的节点创建一个未标记的边。
一旦完成预处理,即可使用如下数据结构进行稀疏分析:
(1)节点工作列表初始化为包含所有ALLOC节点的Worklist。
(2)全局指针图FG包含所有TLV的指针集,设
是TLV变量
的指针集。
(3)每个LOAD和
函数
包含一个指向图
,以保存该节点可能访问的所有ATV的指针信息。设
是包含在
中的ATV变量
的指针集。
(4)每个STORE节点
包含两个指向图,保存该节点可以定义的所有ATV的指针信息:
用于输入指针信息,
用于输出指针信息。
是
中的地址获取ATV变量
的指针集。提升
对地址获取变量集的运算能力,使其结果是该集中每个变量指针集的并集。
(5)对于每个ATV变量
,
返回
所属变量分区。
指向图的点被初始化为空,循环迭代地从工作列表中选择和处理一个节点。在此期间,可以将新节点添加到工作列表中,循环一直持续到工作列表为空,分析计算结束。上述处理ATV变量的phi函数计算方法实现如下: