前言
上一节中,李樾老师讲了IR相关的内容。从本节开始的连续四节课程都是关于数据流分析(Data Flow Analysis,简称DFA)的,其中第三、四节是DFA的应用部分,第五、六节则是理论基础部分。本节课首先介绍了数据流分析,然后讲解了定义可达性分析(Reaching Definitions Analysis)这一部分。
数据流分析简介
李樾老师对于数据流分析的介绍十分易懂。Data flow analysis研究的问题就是:How data flows on CFG?
进一步扩展后是:How application-specific data flows the nodes (BBs/statements) and edges (control flows) of CFG (a program)?
第一节课曾提到,静态分析可以从两个方面把握:首先是对data做abstraction(如第一节课的“$+ - \Omicron \top \bot$”),然后做over-approximation。事实上,对于绝大多数静态分析场景来说,我们要做的都是over-approximation,也可以称为may analysis,即输出结果包含的信息可能为真。然而,还有一些场景是需要under-approximation,也可以称为must analysis,即输出结果必须包含的信息必须为真。无论如何,over-approximation和under-approximation的目的都是在各自的场景中实现safety of analysis,使结果符合该场景的需求。
对于CFG中的nodes,我们需要用transfer function来进行处理;对于edges,我们需要处理控制流,例如需要在merge处对上游分支做union。
不同的数据流分析应用(application)有着不同的数据抽象(data abstraction)和不同的流安全近似策略(flow safe-approximation strategies),即不同的transfer functions和control-flow handlings。
数据流分析基础
我们首先需要了解一下数据流分析中的输入状态和输出状态:
- 每个IR语句的执行将一个输入状态(input state)转换成了一个新的输出状态(output state)。
- 某个IR语句的输入(输出)状态与该语句之前(之后)的程序点(program point)关联。
下图展示了三种不同的控制流情况:
IN[s1] │ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │ │
┌────┴─────┐ │ s1 │ │ s1 │ │ s3 │
│ │ │ │ │ │ │ │
│ s1 │ └─────┬────┘ └────┬─────┘ └─────┬────┘
│ │ │ │ │
└────┬─────┘ ┌───┴──┐ └────────┬─────────┘
OUT[s1] │ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │ │ │
│ s2 │ │ s2 │ │ s3 │ │ s2 │
│ │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
对于语句s1、s2和s3来说:
- 左侧控制流,IN[s2] = OUT[s1]
- 中间控制流,IN[s2] = IN[s3] = OUT[s1]
- 右侧控制流,IN[s2] = OUT[s1] ^ OUT[s3]
^
是meet operator,根据实际控制流走向取值。
在数据流分析应用(application)中,我们通常将每个program point与一个数据流值(data-flow values)关联,此data-flow value代表了在该点上可以观察到的所有可能的程序状态的抽象集合。
对于一个application来说,所有可能的data-flow values构成的集合称为该application的域(domain)。
在以上这些内容的基础上,我们可以说,数据流分析的目的就是要为一个“以safe-approximation为导向”的约束(constraints)集合,为所有语句(statements)s,在IN[s]和OUT[s]上,找到一个解(solution)。
约束体现在哪里呢?体现在两方面:
- 基于语句语义的约束(表示为transfer functions)。
- 基于控制流的约束。
对于transfer functions的约束来说,有两类分析:
- 前向分析(forward analysis),根据输入求输出:$OUT[s] = f_{s}(IN[s])$
- 后向分析(backward analysis),根据输出求输入:$IN[s] = f_{s}(OUT[s])$
在我看来,这两种分析非常像数学上的“互为反函数”的定义。
对于控制流的约束来说,也有两类情况:
- BB内控制流:$IN[s_{i+1}] = OUT[s_{i}],\ for\ all\ i = 1, 2, …, n-1$
- BB间控制流:$IN[B] = IN[s_{1}],\ OUT[B] = OUT[s_{n}]$
另外,从前向分析的角度看,BB间控制流有以下特性:
- $OUT[B] = f_{B}(IN[B]), f_{B} = f_{s_{n}} \circ\ …\ \circ f_{s_{2}} \circ f_{s_{1}}$
- $IN[B] = \bigwedge_{P\ a\ predecessor\ of\ B} OUT[P]$
$\bigwedge\ $符号用于表示B的所有前驱对B的贡献的综合结果。
从后向分析的角度看,BB间控制流有以下特性:
- $IN[B] = f_{B}(OUT[B]), f_{B} = f_{s_{1}} \circ\ …\ \circ f_{s_{n-1}} \circ f_{s_{n}}$
- $OUT[B] = \bigwedge_{S\ a\ successor\ of\ B} IN[S]$
Reaching Definitions Analysis
接下来就是可达性分析了。在具体讲解之前,我们需要排除一下本节不会考虑到的一些问题:
- 函数调用,涉及到跨过程的CFG。
- 别名(Aliases),目前我们处理的都是没有别名的变量。
OK,首先看一下“定义可达性”这个概念:
A definition d at program point p reaches a point q if there is a path from p to q such that d is not “killed” along that path.
对变量v的定义,指的是一个对v赋值的语句。因此,我们可以借助变量v来进一步阐释上述定义:program point p处对变量v的定义在q可达,指的是从p到q有一条路径,而且这条路径上没有其他对v的定义语句。
定义可达性分析可以用来检测源代码中的未定义变量。思路是,在CFG入口处为源代码中的所有变量引入一个dummy definition,如果最终某个变量v的dummy definition能够达到该变量被使用的program point,那么变量v就是一个未定义的变量。
前面我们提到,不同的数据流分析应用有着不同的data abstraction和不同的flow safe-approximation strategies,即不同的transfer functions和control-flow handlings。这句话很重要。现在,我们来考虑定义可达性分析的data abstraction和safe-approximation。
对于定义可达性分析的来说,数据流中的值是一个程序中的所有变量的定义,它们可以用bit vector来表示。例如,如果$D_{1}$到$D_{100}$表示程序中所有的100个变量定义,那么我们可以用一个长度为100的bit vector来表示它。在program point p处,$D_{i}$被置1,当且仅当$D_{i}$处的变量定义能够到达p。
接下来考虑transfer functions和control-flow。我们用下面这个抽象语句来明确一下定义$D$: $$ D:\ v\ =\ x\ op\ y $$ 上述语句将为变量v“生成”一个定义$D$,并kill掉程序中所有其他地方的变量v的定义。注意,是所有其他地方,不仅仅是之前的control flow,因为程序control flow可能存在环,我们不能确定运行时真正的执行先后顺序。即使kill掉了无环执行流的下游的定义也没有关系,后面再重新定义即可。
基于此,我们可以得出BB级的transfer function: $$ OUT[B] = gen_{B} \cup (IN[B] - kill_{B}) $$ 下图展示了一个CFG中BBs的$gen$和$kill$:
我们同样可以得出control flow handling: $$ IN[B] = \bigcup_{P\ a\ predecessor\ of\ B}OUT[P] $$ 接下来就是定义可达性分析的算法了。算法初看起来比较复杂,但是后面李樾老师用一个例子把它讲得非常清楚,十分精彩。首先来看一下算法:
上述算法中有两个值得讨论的点:
- 为什么 $OUT[entry] = \empty$ 被单独拎出来了,没有和下面的for循环合并?
- 为什么我们能确定while循环一定会终止?
第一个问题比较简单,因为这么写在修改时更方便。在一些其他的分析中,可能entry之外的其他BB的初始化值并非空集,此时只需要修改for循环内部的赋值语句即可。
第二个问题稍微复杂一些,借助后面的例子能够更好地理解。这里我们先回答一下:一个程序中的definitions是有限的,即bit vector的长度是有限的,每个BB的$OUT[B]$实际上只与$IN[B]$有关(因为$gen_{B}$和$kill_{B}$总是保持不变),且按照算法来看,$OUT[B]$置1数一定不会比$IN[B]$小。而$IN[B]$又是基于前驱$OUT[P]$产生的,因此每个循环后的$IN[B]$一定不会减小。该算法是单调递增的,并且有一个上界(bit vector全部置1),因此最终一定会停止。
OK,接下来看一个例子:
除了初始化过程外,这个分析进行了三轮。第三轮中,bit vector没有发生变化,循环终止。最终得到的不同program point处的bit vector就是定义可达性的分析结果。
关于这个算法在该例子上执行的具体过程,推荐去看李樾老师的授课视频或课件,非常详细清楚。
事实上,最后求出的解是数学上的“不动点”。
总结与思考
本节课,李樾老师介绍了数据流分析的基础知识,然后介绍了第一个数据流分析的应用——定义可达性分析。
有趣有料,继续前行吧。