【软件分析学习笔记】5:可达定义分析(Reaching Definitions Analysis)
从这节开始两节课先学习数据流分析的应用,再往后两节学习数据流分析的基本原理。这节学习可达定义分析,下节学习活跃变量分析和可用表达式分析,它们都是数据流分析的常见应用。
1 数据流分析回顾
数据流分析研究的是抽象出的application-specific data如何在控制流图(CFG)上流动,大多数静态分析都是在CFG上进行分析的。
1.1 safe-approximation
对大多数静态分析而言,都是牺牲complete来保证sound,要进行over-approximation,即may分析,这种时候分析出的错误中可能有误报,但是涵盖了要分析的所有错误。
还有一些静态分析是must分析,要进行under-approximation,要求报出的错误必须是真实存在的。例如,对于编译优化就需要进行must分析,如果使用may分析,那么对于那些误报的地方就会导致错误的优化。
无论是over的还是under的近似都是要保证分析的safety,都是为了迎合具体的使用场景下分析是安全的。所以可以统称safe-approximation,对于may分析safe=over,对于must分析safe=under。
1.2 不同的数据流分析
因为数据流分析是在CFG上,前面学习了,在CFG上结点(Node) 是基本块(BB)或者单条语句(statement),用转换函数(transfer function) 得到新的抽象数据表示(data abstraction)。而对于边(Edge) 就是控制流(control flow),也使用专门的处理方法(control flow handling)。
对于不同的数据流分析有不同的data abstraction,也有不同的safe-approximation策略,也有不同的transfer function和control flow handling。
2 预备知识
2.1 IR语句的输入输出状态
在得到中间表示(IR)的语句之后,用IN[s1]
表示s1执行前的状态,用OUT[s1]
表示s1执行后的状态,每个IN
和OUT
都关联一个语句执行前/后的程序点。
上图前两个很好理解,即流入方的IN
就是流出方的OUT
,第三个是控制流汇聚的情况,这个时候要对不同的场景设定不同的meet operator,用符号表示,来汇聚多个流入的数据。
2.2 数据流分析要达到的效果
而数据流分析要达到的效果,就是给每一个程序点(program point) 关联一个数据流值(data-flow value) ,这个值代表在这一点能观测到的所有可能的程序状态(program state)的抽象。
注意,关联的data-flow value取值自值域(domain),例如对于一段程序的符号分析:
从另一个角度理解,数据流分析是对那些IN
和OUT
所描述的一系列的safe-approximation约束规则进行求解,在CFG上约束规则就是这两类:
- 对statement的语义的约束规则,即基于transfer function
- 基于控制流信息的约束规则
下面分别简要说明。
2.3 基于transfer function的约束规则
有两种,前向分析(forward analysis) 和逆向分析(backward analysis)。
对于单条语句s
,前向分析的transfer functionf_s
就是将语句前程序点的状态IN[s]
转换为语句执行后的OUT[s]
;而逆向分析则是反过来,从OUT[s]
得到IN[s]
。
这里老师提到,有些时候说到backward analysis,实际做的时候是把整个CFG倒过来,然后再进行forward analysis完成的。正常时候就是理解成沿着CFG反着做就行了。
2.4 基于控制流信息的约束规则
多个statement组成BB,所以控制流信息就可以分为BB内部的和BB之间的。
对于BB内的statement,下一条的输入状态IN
就是上一条的输出状态OUT
。而整个BB的IN
就是第一条语句的IN
,整个BB的OUT
就是最后一条语句的OUT
。对于整个BB而言,其transfer function就是里面每一条statement的transfer function结合在一起,从而可以从IN[B]
计算OUT[B]
。而要计算BB的IN[B]
,就还是它的所有前驱BB进行meet运算()得到的。
3 可达定义分析(Reaching Definitions Analysis)
3.1 简述
先说明什么是"可达的定义":在程序点的一个定义,到程序点是可达的,当且仅当这个定义在这条路径上不会被kill掉。
注意这里说的一个变量的定义(definition)即是指为这个变量设置值的statement,在程序语言中初始化和赋值都算是definition。
也就是说变量在处有definition,然后在不能被新的definition盖掉,这样它(指处的definition,而不是本身)在这条路径上就是可达定义(reaching definition)。
可达定义分析除了用于编译优化,也可以用作简单的错误检测。在上节学习的CFG中最开始有个额外的entry块,把程序中所有变量在这个entry块中添加一个dummy definition,用来指示这个变量还是未定义的状态。接下来在一个可达的程序点如果发现变量被使用了,并且的dummy definition在这里还是个可达定义,那么这里就可能发生了"使用未定义的变量"的错误(当然也可能没有,因为可以在其它path上有新的definition,程序运行时可能走了那条path),显然可达定义分析属于may分析。
3.2 definition的表示
可达定义分析用Bit Vector表示definition,如程序中有100个definition就用100个比特位来表示,从左往右第位就表示第个definition:
3.3 transfer function的设计
对于程序中一个对变量的definition:
它kill掉了前面所有的对的definition,因此基本块的transfer function的设计:
这里补一下郭老师的PPT:
这即是说:
这里特别注意的含义,实际上只要是中的定义的那些赋值,在之外的其它基本块中对这个变量的赋值全部都会被KILL掉。
例如,考虑下面这个CFG:
对于基本块而言,因为里面有对变量、、的定义,所以就是所有其它地方对这三个变量的定义。注意后面的和虽然不可能出现在基本块前面,但是还是要算进去,毕竟只是求一个差集,这样能保证那些循环回来的路径上的定义不会混入中而不被消除掉。
3.4 control flow的设计
回顾这节的2.4
可以知道,实际上不同数据流分析的control flow的设计,区别就在于控制流合并时的meet操作()的不同。在可达定义分析中,这个操作是集合求并(),即是说:
因为是may分析,一条path也不能放过,所以是所有前驱的OUT
求并。
3.5 可达定义分析的算法
算法的输入是控制流图,输出就是对每个基本块都要得到和:
第一步定义最上面的虚拟Entry块的OUT
为空集,因为这个虚拟块中实际上没有definition,所以也就没有能流到OUT[Entry]
这一程序点的definition,因此是空集。
第二步对所有除了Entry之外的基本块,将其OUT
设置为空集。这里老师解释了一下,因为这个算法是一个数据流分析的比较经典的模板算法,对于有些数据流分析而言,Entry和其它基本块的boundary condition(这里指初始的边界条件)是不一样的,所以这里分开写了一下。实际上在这个算法里就是所有的基本块OUT
先设置为空集。
数据流分析的boundary condition都可以靠思考语义去设计,比如在这里就要询问自己
OUT[B]
在这个分析中是什么含义,回答是从基本块流出来的程序状态,具体指有哪些definition可以到达这个程序点。
may分析的boundary condition一般都是空,must分析的boundary condition一般都是top。
接下来是一个while循环,一轮循环下来只要有一个BB的OUT
改变了,这个循环就不会终止。在这个while循环内部有一个for循环遍历所有的除了Entry之外的BB,然后里面的两行就是为每一个BB不断执行3.3
和3.4
的约束规则。直到所有的OUT
都不变了,结果就收敛了,程序就终止了。
3.6 可达定义分析的例子
一共有8个定义,用8位的Bit Vector表示:
每个程序点都需要有这样一个Bit Vector,其中0表示在这点对应的definition不可达,1表示可达,所以空集其实就是这样8个0组成的Bit Vector。
先执行算法的第一第二步,将所有BB的OUT
初始化为空:
接下来要不停应用循环体内的control flow策略求IN
和transfer function求OUT
:
在上执行data flow规则计算,由于前驱只有Entry块,所以。
在上执行transfer function计算,由于是,是空集,是,所以得到。
在上执行data flow规则计算,由于前驱有和,其中,,求并集(位运算的或)得。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱只有,所以。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱只有,所以。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱有和,其中,,求并集(位运算的或)得。
在上执行transfer function计算,由于是,是,是,所以得到。
第一轮遍历结束,这轮遍历中存在OUT
发生变化的BB,因此还要继续下一轮遍历。
在上执行data flow规则计算,由于前驱只有Entry块,所以。
在上执行transfer function计算,由于是,是空集,是,所以得到。
在上执行data flow规则计算,由于前驱有和,其中,,求并集(位运算的或)得。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱只有,所以。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱只有,所以。
在上执行transfer function计算,由于是,是,是,所以得到。
在上执行data flow规则计算,由于前驱有和,其中,,求并集(位运算的或)得。
在上执行transfer function计算,由于是,是,是,所以得到。
第二轮遍历结束,这轮遍历中存在OUT
发生变化的BB(分别是和),因此还要继续下一轮遍历。
接下来的一轮遍历OUT
全都不变,不再写出了,最终结果:
从整个流程跑下来可以发现,和在每次循环中都是不变的,只和CFG本身有关,可以一开始就算好,每次拿来直接用就行了。
由此,只要输入和上一轮相比不变,那么输出一定也不变。另外遍历的次序也很重要,上面用的是拓扑排序的遍历次序,这样只要花费较少的while循环次数。
3.7 为什么算法一定能终止
算法最关键的transfer function中GEN和KILL都是不变的,实际上只由IN
的变化决定OUT
的变化。而IN
的变化即每次加入新的fact,只可能比之前多而不会比之前少,而新加入的部分可能在计算OUT
的公式中被KILL掉,或者成为幸存者保留到OUT
中,这导致OUT
也只可能比之前多而不会比之前少。
总的来说,就是整个过程是不断加入fact,不断达到最终的sound的结果过程。反映到Bit Vector中即不可能存在某位=1变成该位=0的情况。
3.8 为什么while中的循环条件能保证safe
这即是在问为什么所有OUT
都不变就能保证结果已经符合2.3
和2.4
的约束规则了。因为OUT
全都不变,所有的IN
也就不会变,所以再进行多少次循环结果也不会变化,达到了这样一个不动点(fixed point)。