想学数据流分析的人还是找一个国外大学的讲义学吧,以下内容都是自己多年前按照自己的理解写的,很多内容可能会误人子弟,sorry
#引子
编译器后端会对前端生成的中间代码做很多优化,也就是在保证程序语义不变的前提下,提高程序执行的效率或减少代码size等优化目标。优化需要依靠代码分析给出的“指导信息”来相应地改进代码,而代码分析中最重要的就是数据流分析。另外数据流分析是程序静态分析的基础。所以掌握数据流分析对编译后端极为重要。
##何为数据流分析
数据流分析指的是一组用来获取有关数据如何沿着程序执行路径流动的相关信息的技术. 《编译原理》
数据流分析的目的是提供一个过程(或一大段程序)如何操纵其数据的全局信息. 《高级编译器设计与实现》
从上面的表述中,我们可以看出数据流分析通过静态代码来**“推断”**程序动态执行的相关信息,数据流分析并不真正执行程序。虽然数据流分析和符号执行在某些方面比较相似,但还是两种完全不同的概念,更确切的说数据流分析是符号执行的基础。
数据流分析和符号执行从某些方面都很相似,例如符号执行有程序点(ProgramPoint)的概念,并且在当前程序点存储着程序运行到此刻的所有状态和值信息(一般情况下不会维护历史程序点的信息,开销太大)。数据流分析中也有程序点的概念,程序点存储着数据流信息。两者都是在CFG(Control Flow Graph)图的基础上,进行的分析。Clang的静态分析示意图如下所示,Clang会时刻维护符号执行当前的状态和内存信息。从这一点上看,符号执行和虚拟机更为相似。
但数据流分析和符号执行还是不同的,虽然都有程序点,但程序点存储的信息却是两个不同的概念。数据流分析中程序点存储的是数据流值,这些数据流值是和具体的数据流问题相关的,有可能是当前程序点的定值信息,也有可能是可用表达式信息,这些信息标识着该程序内含的一些属性。符号执行中程序点存储的是程序符号执行到此处的所有状态和值信息,这些信息和程序运行更为相关。
而且两者的分析方法也不同,符号执行是单次执行,而数据流分析大多采用迭代分析的框架,然后在迭代分析的过程中不断更新程序点的数据流信息,最终得到比精确解更小(更保守)的解。但为了进行更为激进的优化,要求数据流分析在保证保守的同时又尽可能是激进的。
##数据流抽象
在前面的文章中我们也提到过,程序的执行可以看作是程序状态的一系列转换。程序状态是由程序中所有变量的值以及运行时栈帧上的相关值组成。程序语句对应着转换函数,将前一个程序点的输入程序状态转换到下一个程序点的新的输出状态。
上图所示中的红点表示的就是程序点,数据流转换函数就是作用在程序点上的状态,并沿着程序路径一步步进行的。其实这个过程就是一个自动机,抽象出的自动机如下图所示。程序点代表自动机中的一个节点,程序语句或者说是转换函数代表自动机中的一条边。一般来说,一个程序有无穷多条可能的执行路径,执行路径的长度并没有上界(例如死循环)。程序分析可以推断出各个程序点的程序状态(有穷的特性集合),当然很少有哪种数据流分析会用到所有的数据流信息,一般只是提取出感兴趣的特性集合进行分析。
我们考虑的多数数据流分析问题关注的是各种程序对象(常数,变量,定值,表达式等)的集合,以及在过程内任意一点这些对象的什么集合是合法的有关判断。另外在数据流分析中,一般是会忽略掉路径条件判断的,也就是说默认所有路径都可达(这种近似是正确且有效的,现在我还没有找到忽略控制条件也能够保证数据流分析正确性的证明!),在程序分析中忽略掉程序控制条件,所以核心的部分就是状态数据如何变化了,也就是数据流分析。
我们虽然可以对过程的控制流图进行数据流分析,但通常更有效的做法是将它分解为局部数据流分析和全局数据流分析,局部数据流分析针对每一个基本块进行,全局数据流分析针对控制流图进行分析,其实就是一个粒度问题。我们可以将同一个基本块内的各个语句的作用综合起来合成整一个基本块的作用。例如我们可以将上面的自动机改造为基于基本块的形式,如下图所示。
##数据流分析模式
在数据流分析中,程序点一般和数据流值(data-flow value)关联起来,注意这个数据流值不是程序中变量的值。“这个值是在该点可能观察到的所有程序状态的集合的抽象表示”,这句话说起来有点绕口,每个数据流分析问题都有其对应的值域,每个程序点的数据流值都是该值域的子集。比如,到达定值的数据流值的域是程序的定值集合的所有子集的集合。某个数据流值是一个定值的集合,数据流分析的目的就是推导出所有程序点与其对应的到达定值的集合。
一个定值是对某个变量的赋值。可能沿着某条路径到达某个程序点的定值称为到达定值(reaching definition)。
我们把每个语句s之前和之后的数据流值分别记为***IN***[s]和***OUT***[s]。数据流问题就是对一组约束求解,得到所有IN[s]和OUT[s]的结果。
每个语句都约束了该语句之前程序点状态和之后程序点状态的关系,也就是说语句s限定了IN[s]和OUT[s]之间的关系。整个程序就是由无穷个这样的约束构成的。数据流问题(data-flow problem)就是对这一组约束求解,另外约束不仅有语义(传递函数)上的约束,更有基于控制流的约束。
###传递函数
在一个语句之前和之后的数据流值受该语句语义的约束,也就是程序语句前后程序点的数据流值受该语句语义的约束,这种约束关系成为传递函数(transfer function)。
传递函数有两种风格:数据流信息可能沿着执行路径向前传播,或者沿着程序路径逆向流动,相应的就有前向(forward)数据流问题和后向(backward)数据流问题。
Forward data-flow analysis, Information at a node is based on what happens earlier in the flow graph.
Backward data-flow analysis, Information at a node is based on what happens later in the flow graph.
大部分人刚接触到后向数据流问题时会比较困惑,数据流值怎么会依赖于后面的数据流值信息呢。其实这是由于有些人还是对于数据流值的概念不是很理解,将数据流值简单的归结于变量的值,如果这么对比的话,就会出现矛盾
对于前向数据流问题,一个程序语句s的传递函数以语句前程序点的数据流值作为输入,并产生出语句之后程序点对应的新数据流值。例如到达定值就是前向数据流问题。
对于后向数据流问题,一个程序语句s的传递函数以语句后的程序点的数据流值作为输入,转变成语句之前程序点的新数据流值。例如活变量分析就是后向数据流问题。
###控制流约束
第二组关于数据流值的约束是从控制流中得到的,基本块内都是顺序执行,没有控制流的约束。但是基本块之间有相应的控制流约束,例如一个基本块的最后一个语句和后继基本块的第一个语句之间的约束,这些约束比较复杂。
##基本块上的数据流模式
前面我们已经提到过程序语句的约束分为两种,基于程序语句语义的约束和基于控制流的约束。基本块之间的约束都是基于控制流的约束,由于基本块内没有分支,所以我们可以基于整个基本块来描述基本块对于数据流值的约束,而不是基于程序语句(前面也提到过,我们可以使用局部数据流和全局数据流分析结合更加高效)。我们以基本块为最小单位来研究基本块上的数据流模式。
基本块的传递函数和基本块内程序语句所表示的传递函数之间的关系如上图所示。那么基本块之间的约束是如何的呢?如下图所示。
图中展示出来的是基本块之间的前向数据流问题的约束方程。后向数据流问题的方程如下图所示。
数据流分析就是根据这一组约束,得到一个满足这些约束的解。和线性算术方程不同,数据流方程通常没有唯一解。数据流分析的目标是寻找一个最“精确的”满足这两组约束(即控制流和传递函数)的解,当然这个解必须是保守的,能够保证我们根据这个解进行代码优化不会导致不安全的转换。
当然数据流分析,不是直接联立方程求解的,一般是通过一种迭代分析的方法来求解的。
在下一篇文章中我们继续分析到达定值、活变量和可用表达式的数据流分析实例。