静态程序分析chapter3 - 数据流分析详述(Reaching Definitions、Live Variables、Available Expressions Analysis)


二、

数据流分析

introduction1

      数据流分析:怎样的数据?如何流向?在什么上分析?

      怎样的数据?我们所关心的感兴趣的属性,比如像之前提到过的变量的符号(抽象和过近似见博客:https://blog.csdn.net/Little_ant_/article/details/118701090),需要对属性进行抽象

      如何流向?一般情况下需要采用过近似,包括转换函数控制流处理。通常情况下视问题的不同,需要采用 over-approximate 或 under-approximate,对应的是倾向于 Sound 和 Complete。通常将采用的 over-approximate 的数据流分析称为 may analysis,将采用的 under-approximate 的数据流分析称为 must analysis。因为 may analysis 是偏向于 Sound 的,所以它分析的结果可能是真的;而 must analysis 是偏向于 Complete 的,所以它分析的结果一定是真的。

      在什么上分析?在 CFG 上来分析(CFG是在 IR 上建立的,IR 是由待分析程序得到的),CFG 包括节点和边。在节点上分析遵循的方法是转换函数,在边上的分析遵循的是控制流处理。

introduction2

输入和输出状态

      针对一条 IR 语句和我们感兴趣的属性,在该语句执行前属性的状态称为输入状态;在该语句执行后属性的状态称为输出状态。在顺序执行时,上一条语句的输出状态是下一条语句的输入状态。在有分支的情况下,汇合点之前的输入状态和多条分支的输出状态均有关,需要对这些输出状态进行某种操作,具体为哪种操作取决于具体的问题。见下图:其中 s1、s2、s3 为 IR 语句。
little_ant_
      在一个数据流分析应用中,在每一个程序点都会关联一个数据流状态,这个数据流状态是我们能够在该点处能够观测到的所关心属性的抽象之后的全部状态。比如在对所有变量的符号判断中,抽象之后的符号域中有 5 个元素:+、-、O、T、⊥。对下图中程序,一些程序点处的数据流状态被列出在右侧绿色区域中。
little_ant_
      数据流分析就是在所有程序点的输入状态和输出状态上,采用某种近似方法(包括转换函数和控制流处理)对程序进行分析,从而得到针对某种问题的解决方案。

转换函数

       前向分析:按着程序执行流的方向进行分析,可认为是由输入得到输出。后向分析:按着程序执行流的反方向进行分析,可认为是由输出得到输入。

      转换函数:描述某一条语句 s 的输入状态 IN[s] 和输出状态 OUT[s] 之间的转换关系。在前向分析中,描述如何通过 IN[s] 得到 OUT[s];在后向分析中,描述如何通过 OUT[s] 得到 IN[s]。如下图:
little_ant_

      存在一个基本块 B ,它包含有语句 s1、s2、s3 … sn 。

      那么在基本块之内有:IN[s2] = OUT[s1]、IN[s3] = OUT[s2]、IN[s4] = OUT[s3] …即之前提到过的 “在顺序执行时,上一条语句的输出状态是下一条语句的输入状态”

      如果 B 的前驱为 P1 和 P2,后继为 S1 和 S2 ,在基本块之间有:IN[B] = IN[s1]、OUT[B] = OUT[sn] 。从而得到 OUT[B] 和 IN[B] 的关系与 B 中的所有语句的转换函数有关,并且在前向分析中: B 的输入状态 IN[B] 和它的所有前驱的输出状态 OUT[P] 有关系;后向分析中:B 的输出状态 OUT[B] 和它的所有后继的输入状态 IN[S] 有关系。在下图中,淡黄色部分表示前向分析,淡红色部分表示后向分析,Λ表示 meet operator。
little_ant_

数据流分析应用

      下面介绍三个基本的数据流分析应用示例。

1,Reaching Definitions Analysis

概述

      注意在这里 definition 的意思不是定义,而是赋值/初始化。

      Reaching Definition Analysis :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 . 中文翻译为:在程序点 p 处,存在一个赋值语句 d 表示对某个变量进行赋值,如果在通往之后的程序点 q 之间的任意一条路径上,d 是存活的(即该变量并没有被重新赋值,仍然是之前的那个值),那么认为在 p 处的赋值语句 d 能够到达 q 点(即 d 在 q 处依然是有效的)。

      Reaching Definitions Analysis是,针对程序中的每个赋值语句(d, v = C),分析出它可能到达的所有程序点(通常是一条指令的前后处)。等价说法是,RDA是针对程序中的每个程序点(通常是一条指令的前后处),分析出可能到达此处的所有赋值语句(d, v = E)。

      可以看出,它属于 may analysis。因为只要存在一条路径上没有对变量 v 重新赋值,就认为原来的赋值语句 v = C 是有效地,但是,可能会在别的路径上对 v 重新赋值(令 v = C1)。所以并不能确定该赋值语句 100% 能够到达之后的程序点,这取决于程序执行时走的哪条路径。may analysis 采用的是过近似,即哪怕有99条路径上变量 v 都被赋值/初始化了,但是有一条路径上没有,那这一条路径也是需要考虑的,因为程序是有可能走这条没有被赋值/初始化的路径的,过近似的特点就是不放过任何一个程序执行时可能的行为(不放过任何一条路径),所以它得到的结果有可能是真的。

用途

      可用于死代码去除。对于某条赋值语句(d, v = C),它可以到达的程序点假设为 p1、p2、p3 … 那么如果在这些程序点处,发现变量 v 并没有被使用过,从而得出该赋值语句是一条无用代码,删除该赋值语句 d 做代码优化。

      简单的内存泄露检查。原理是类似的,对于动态内存分配指令(d,v = new …),判断在该语句所有可达的程序点处,是否存在有 delete 语句,如果这些可达程序点处均没有 delete 语句,则认为发生了内存泄露。

      简单的错误检测。我们首先在 entery 块中对程序中的变量引入一条条假的赋值/初始化语句(即这些变量还没有被赋值/初始化),如果在之后某条假赋值语句的的可达程序点处,发现这个变量被使用了,那么有可能发生了错误(变量在赋值/初始化前被使用)。“可能” 意味着不确定,因为有可能别的路径上存在这个变量的赋值/初始化语句。

分析流程

      可以发现:对于 RDA,在程序的第一条语句之前,还没有任何语句被执行,那么在此处所有赋值语句都是不可达的。而在程序的最后一条语句之后,所有的赋值语句是否可达并不确定,所以我们采用前向分析,从第一条语句至最后一条语句。

1,抽象

      抽象:因为我们关心的是程序中所有变量的赋值语句都能到达哪些程序点,那么就需要对这些赋值语句进行抽象。这里采用位向量来表示。假设程序中有赋值语句100个,那么需要有100个比特位来表示,第一位表示第一条赋值语句是否可达,若可达置为1,不可达置为0。那么在第一个程序点(程序的第一条语句之前)处,置这100个比特位均为0,表示均不可达。

2,转换函数

      转换函数:在前向分析中的转换函数是计算如何从输入状态得到输出状态,我们以一条语句来进行分析。

      假设存在一条语句 D:v = x op y ,这一条语句使用变量 x 和 y 对变量 v 赋值。重申:我们分析的是程序中所有赋值语句,判断它们在各个程序点处的可达情况。所以得出:在语句 D 执行之后的程序点处,赋值语句 D 中变量 v 是可达的,而在程序中其余的对 v 的赋值语句在此是不可达的。而对于变量 x、y,仍然保持在语句 D 执行之前的可达性。 新的赋值语句 D 的比特位在此设置为 1,其余的对 v 的赋值语句全设置为 0.

      转换函数用公式表示为:OUT[B] = genB U (IN[B] - killB)       U表示或操作。

      B表示一个基本块,这个公式做两件事:令其他地方的赋值语句不可达,设置当前的新赋值语句可达。当然对于每一条语句也是一样适用的。下图计算程序中的基本块的 genB 和 killB:
little_ant_

3,控制流处理

      控制流处理:在有分支的情况下,假设基本块 B 的两个前驱为 P1 和 P2。现在讨论IN[B],如果OUT[P1]中的某些赋值语句的变量是可达的,而在OUT[P2]中的这些赋值语句的变量是不可达的,根据 RDA 的定义,只要有一条路径上某个变量是可达的,就认为它是可达的。所以应该采用或操作,1 或 0 = 1。

      控制流处理用公式表示为:IN[B] = U OUT[P](p is a predecessor of B )

4,算法实现

      在抽象、转换函数和控制流处理之后,关于 RDA 的算法如下图:
little_ant_      这个算法是一个迭代算法,可以当做模板应用于别的数据流分析中。算法的输入是 CFG ,输出是判断每一个程序点(这里以基本块为单位)处所有赋值语句是否可达。

1,初始化

      初始化语句第一步,令 entry 的输出状态为空,这个之前讲过,需要从语义上来理解:因为此处之前没有任何语句,所以在这里所有赋值语句均是不可达的。

      初始化语句第二步,令除了 entry 之外的其他基本块的输出状态也为空,为空的原因后面会讲到,总之 may analysis 通常都会初始化为空集,用符号⊥来表示,称为 bottom。而 must analysis 刚好相反,常被初始化为满集,用符号 T 来表示,称为 top。

      while 循环的判定条件为任何一个基本块的 OUT 集合发生改变时,循环主体是对除了 entry 块的其余基本块做:控制流处理和转换函数的实现。至于这个循环一定会停止的原因后面再讲,我们先通过一个例子熟悉这个迭代算法,见下图:一共有8个赋值语句,5个基本块,紧接着会对算法执行流程做说明。
little_ant_

      初始化:初始化的结果如上图的黑色字体来表示,按照算法描述,所有的 OUT 均初始化为 0000 0000。

2,第一轮迭代

      第一轮迭代:执行循环体,用红色字体来表示执行结果,如上图显示的是 IN[B1] = OUT[entry] = 0000 0000
前向分析继续往下走,按照转换函数计算出:OUT[B1] = 1100 0000 U (0000 0000 - 0001 1010)= 1100 0000。这里的减法运算为:1-1=0、1-0=1、0-1=0、0-0=0。

IN[B2] = OUT[B4] U OUT[B1] = 0000 0000 U 1100 0000 = 1100 0000;
OUT[B2] = 0011 0000 U (1100 0000 - 0100 0000)= 1011 0000

IN[B3] = OUT[B2];
OUT[B3] = 0000 0010 U (1011 0000 - 1000 1000)= 0011 0010

IN[B4] = OUT[B2];
OUT[B4] = 0000 1100 U (1011 0000 - 1000 0011)= 0011 1100

IN[B5] = OUT[B4] U OUT[B3] = 0011 1100 U 0011 0010 = 0011 1110;
OUT[B5] = 0000 0001 U (0011 1110 - 0000 0100)= 0011 1011

      根据算法的循环判定条件,存在有基本块的 OUT 发生了变化(事实上所有基本块的 OUT 都发生了变化,不再是初始的 0000 0000了),那么将进入第二轮迭代。第一轮迭代执行完毕的结果如下图:
little_ant_

3,第二轮迭代

      第二轮迭代:在上图第一轮的执行结果上进行,IN[B1]保持不变,IN[B1] = OUT[entry] = 0000 0000
前向分析继续往下走,按照转换函数计算出:OUT[B1] = 1100 0000 U (0000 0000 - 0001 1010)= 1100 0000

IN[B2] = OUT[B4] U OUT[B1] = 0011 1100U 1100 0000 = 1111 1100;
OUT[B2] = 0011 0000 U (1111 1100 - 0100 0000)= 1011 1100

IN[B3] = OUT[B2];
OUT[B3] = 0000 0010 U (1011 1100 - 1000 1000)= 0011 0110

IN[B4] = OUT[B2];
OUT[B4] = 0000 1100 U (1011 1100 - 1000 0011)= 0011 1100

IN[B5] = OUT[B4] U OUT[B3] = 0011 1100 U 0011 0110 = 0011 1110;
OUT[B5] = 0000 0001 U (0011 1110 - 0000 0100)= 0011 1011

      根据算法的循环判定条件,存在有基本块的 OUT 发生了变化(B2,B3),那么将进入第三轮迭代。第二轮迭代执行完毕的结果如下图:
little_ant_

4,第三轮迭代

      第三轮迭代:在上图第二轮的执行结果上进行,IN[B1]保持不变,IN[B1] = OUT[entry] = 0000 0000
前向分析继续往下走,按照转换函数计算出:OUT[B1] = 1100 0000 U (0000 0000 - 0001 1010)= 1100 0000

IN[B2] = OUT[B4] U OUT[B1] = 0011 1100 U 1100 0000 = 1111 1100;
OUT[B2] = 0011 0000 U (1111 1100 - 0100 0000)= 1011 1100

IN[B3] = OUT[B2];
OUT[B3] = 0000 0010 U (1011 1100 - 1000 1000)= 0011 0110

IN[B4] = OUT[B2];
OUT[B4] = 0000 1100 U (1011 1100 - 1000 0011)= 0011 1100

IN[B5] = OUT[B4] U OUT[B3] = 0011 1100 U 0011 0110 = 0011 1110;
OUT[B5] = 0000 0001 U (0011 1110 - 0000 0100)= 0011 1011

      根据算法的循环判定条件,所有基本块的 OUT 都没有发生变化,迭代停止,算法执行完毕。第三轮迭代执行完毕的结果如下图:
little_ant_

      此时那些绿色字体表示的就是算法的最终结果,我们在每一个程序点处得到了所有赋值语句的最终状态(是否可达)。

小结

      数据流分析:我们首先在每一个程序点处关联一个数据流值,这个数据流值是对该程序点处可以观测到的所有可能的程序状态做抽象得到的 ;然后对所有程序点处应用某种近似(over-appro or under-appro)方法得到某种结论的分析手段。(可直白的认为:在抽象域上,采用转换函数和控制流处理对程序分析并得出结论)

      RDA可应用于死代码消除,在上面例子中,对于赋值语句 D1,它的可达程序点为 OUT[B1] 和 OUT[B2]。而在 B1 和 B2 两个基本块中可以发现 D1 中的变量 x 并没有被使用过,所以赋值语句 D1 在这个程序就是一条死代码,于程序而言,它的存在没什么意义,应予以删除。

      迭代为什么能停止?
      可以发现:根据转换函数的公式,一个基本块中的 genB 和 killB 在每一轮的迭代中是不会发生变化的,因为程序没有改变,只有 IN[B] 发生变化,OUT[B] 才会发生变化,而由初始化可知,所有程序点的 OUT 是初始化为空集的(bottom),所以 OUT[B] 只能增长或不变,而不会下降,即不会从 1 变为 0。这些可能增长的状态(从 0 变为 1)在之后的迭代中会流向 IN[B] 并生成新的 OUT[B] (这里忽略掉控制流处理,因为它也不能将 1 变为 0)。这个新的 OUT[B] 因为 genB 和 killB 始终是不变的,而它又可能保留有更多的状态,所以它比之前会包含更多的 1 。而程序之中的赋值语句数量是有限的,那么最多当 OUT[B] 增长为满集(全为1)时,程序的 OUT 集就不再发生变化了,那么算法就会停止。当算法的所有 OUT 集不发生变化时,称这个状态为该算法的不动点(fixed point)。

2,Live Variables Analysis

概述

      RDA是对赋值语句的可达性进行判定,应用于死代码消除中的判断某个变量在程序点是否被使用便是LVA的工作,这两者结合起来便构成了死代码消除。

      LVA :Live variables analysis tells whether the value of variable v at program point p could be used along some path in CFG starting at p. If so, v is live at p; otherwise, v is dead at p. 在程序点 p 处,一个变量 v 的值如果被 CFG 中在 p 之后直到程序结束的任何一条路径上被使用,则认为 v 在 p 处是存活的,否则为死亡的。

      注意:这里指的是在程序点 p 处变量 v 的值会不会在之后被使用,如果变量在程序点之后直到程序结束的某一条路径上,先是被重新赋值,然后再被使用,那么显而易见,在 p 处变量 v 是死亡的

      LVA 在每一个程序点处判断程序中所有变量的存活性,可以看出,它也属于 may analysis ,因为在某个程序点之后,只要存在一条路径上某个变量是存活的,就认为该变量存活,但是在程序中别的路径上该变量可能是死亡的,所以并不能确定该变量是100%存活的,这取决于程序执行时走的哪条路径。may analysis 采用的是过近似,即哪怕有99条路径上变量 v 都是存活的,但是有一条路径上没有,那这一条路径也是需要考虑到的,因为程序执行时是有可能走这条路径的,过近似的特点就是不放过任何一个程序执行时可能的行为(不放过任何一条路径),所以它得到的结果有可能是真的。

用途

      LVA 可用于寄存器分配上。当所有寄存器上都存在数据时,而我们又需要使用一个寄存器,就可以采用 LVA 淘汰掉一个寄存器,因为这个寄存器中的数据值一直到程序结束时都不会被使用。

分析流程

      对于 RDA ,它是判断在程序点 p 处的赋值语句在之后的程序点上的可达性,是依据于当前的情况去判断之后程序点上的状态,采用的是前向分析;而 LVA 是根据在 p 处之后的路径上判断在 p 处的变量是否被使用,从而判断在 p 处变量的存活性,就是说:它是从后往前分析的,通过 p 后面的情况得到 p 处的结果。

      并且可以可发现:所以对于 LVA ,在程序的最后一条语句之后,所有变量在此处都是死亡的,因为后面不会有语句执行了,也就不存在变量被使用了;而在程序的第一条语句之前,我们无法判断在此处变量的存活性,所以我们采用后向分析

前向分析与后向分析

      对于如下代码:

a = 1;
b = 2;
c = a + 8;
b = 9;
d = b + c;

      通过前向分析,在第二条语句执行之后:在 RDA 中,我们可以轻易得到,赋值语句 a =1 和 b = 2 在此处是可达的。而在 LVA 中,因为没有执行后面的三行代码,我们并不清楚在此处所有变量的存活性,程序必须走完最后一行才可以判断,所以需要采用后向分析。

1,抽象

      抽象:因为我们关心的是程序中所有变量在所有程序点的存活性,那么就需要对这些变量进行抽象。这里采用位向量来表示。类似的:假设程序中有变量100个,那么需要有100个比特位来表示,第一位表示第一个变量是否存活,若存活置为1,死亡置为0。那么在最后一个程序点(程序的最后一条语句之后)处,置这100个比特位均为0,表示均不存活。

2,转换函数

      转换函数:在后向分析中的转换函数是计算如何从输出状态得到输入状态。

      要判断一个变量在程序点 p 处的存活性,首先需要判断之后的基本块中该变量是否被使用,其次需要注意的是只有在重新赋值语句之前被使用才表示在 p 处存活,我们来分析一下可能存在的语句情况:

      假设存在有三个基本块,其中基本块 P 是 B 的前驱,基本块 S1 是 B 后继。变量 v 在 P 中被赋值/初始化,S1 是程序中最后一个基本块(除过 Exit),在 S1 中变量 v 被使用。下面需要针对 B 中可能存在的情况对 IN[B] 处变量 v 的存活性进行讨论:
若:
      B中语句为:k = n ;      按照定义,在 IN[B] 处变量 v 存活。
      B中语句为:k = v ;      按照定义,在 IN[B] 处变量 v 存活。
      B中语句为:v = 2 ;      因为之前的 v 还没有被使用,就对 v 重新赋值,所以在 IN[B] 处变量 v 死亡。
      B中语句为:v = v - 1 ;      因为先执行等式右边的表达式再对 v 重新赋值,即使用在赋值之前,故在 IN[B] 处变量 v 存活。
      B中语句为:v = 2 ; k = v ;      重新赋值在使用之前,故在 IN[B] 处变量 v 死亡。
      B中语句为:k = v ; v = 2 ;      和上面刚好相反,使用在重新赋值之前,故在 IN[B] 处变量 v 存活。

      得出转换函数为:IN[B] = useB U (OUT[B] - redefB)

      U 表示或操作。只要存在 redefinition 就要减去,之后再加上 “used before redefinition” 的情况(通过 U 操作)。OUT[B] 表示没有被重新赋值的其他变量的状态。

3,控制流处理

      控制流处理:在有分支的情况下,假设基本块 B 的两个后继为 S1 和 S2。现在讨论OUT[B],如果IN[S1]中的某些变量是存活的,而在IN[S2]中的这些变量是死亡的,根据 RDA 的定义,只要在 B 之后有一条路径上某个变量是存活的,就认为它是存活的。所以应该采用或操作,1 或 0 = 1。

      控制流处理用公式表示为:OUT[B] = U IN[S](S is a successor of B )

      关于转换函数和控制流处理也可以参考下图:其中 defB 和上面的 redefB 意思相同。
little_ant_

4,算法实现

      在抽象、转换函数和控制流处理之后,关于 LVA 的算法如下图:

little_ant_
      这个算法也是一个迭代算法,算法的输入是 CFG ,输出是判断每一个程序点(这里以基本块为单位)处所有变量是否存活。

1,初始化

      初始化语句第一步,令 exit 的输出状态为空,这个之前讲过,需要从语义上来理解:因为此处之后没有任何语句,所以在这里所有变量均是死亡的。

      初始化语句第二步,令除了 exit 之外的其他基本块的输出状态也为空,为空的原因后面会讲到,总之 may analysis 通常都会初始化为空集,用符号⊥来表示,称为 bottom。而 must analysis 刚好相反,常被初始化为满集,用符号 T 来表示,称为 top。

      while 循环的判定条件为任何一个基本块的 IN 集合发生改变时,循环主体是对除了 exit 块的其余基本块做:控制流处理和转换函数的实现。我们先通过一个例子熟悉这个算法,见下图:一共有7个变量,5个基本块,紧接着会对算法执行流程做说明。
little_ant_
      初始化:初始化的结果如上图的黑色字体来表示,按照算法描述,所有的 IN 均初始化为 000 0000。

2,第一轮迭代

      第一轮迭代:执行循环体,用红色字体来表示执行结果,如上图显示的是 OUT[B5] = IN[exit] = 000 0000
后向分析继续往下走,按照转换函数计算出:IN[B5] = 000 1000 U(000 0000 - 001 0000)= 000 1000。这里的减法运算为:1-1=0、1-0=1、0-1=0、0-0=0。

OUT[B3] = IN[B5] = 000 1000;
IN[B3] = 100 0000 U (000 1000 - 100 0000)= 100 1000

OUT[B4] = IN[B5] U IN[B2] = 000 1000 U 000 0000 = 000 1000;
IN[B4] = 010 0000 U (000 1000 - 100 0100)= 010 1000

OUT[B2] = IN[B3] U IN[B4] = 100 1000 U 010 1000 = 110 1000;
IN[B2] = 000 0001 U (110 1000 - 010 0010)= 100 1001

OUT[B1] = OUT[B4] = IN[B2] = 100 1001
IN[B1] = 001 1100 U (100 1001 - 110 0000)= 001 1101

      根据算法的循环判定条件,存在有基本块的 IN 发生了变化(事实上所有基本块的 IN 都发生了变化,不再是初始的 000 0000了),那么将进入第二轮迭代。第一轮迭代执行完毕的结果如下图:
little_ant_

3,第二轮迭代

      第二轮迭代:在上图第一轮的执行结果上进行,OUT[B5] 保持不变, OUT[B5] = IN[exit] = 000 0000。后向分析继续往下走,按照转换函数计算出:IN[B5] = 000 1000 U(000 0000 - 001 0000)= 000 1000

OUT[B3] = IN[B5] = 000 1000;
IN[B3] = 100 0000 U (000 1000 - 100 0000)= 100 1000

OUT[B4] = IN[B5] U IN[B2] = 000 1000 U 100 1001 = 100 1001;
IN[B4] = 010 0000 U (100 1001 - 100 0100)= 010 1001

OUT[B2] = IN[B3] U IN[B4] = 100 1000 U 010 1001 = 110 1001;
IN[B2] = 000 0001 U (110 1001 - 010 0010)= 100 1001

OUT[B1] = OUT[B4] = IN[B2] = 100 1001
IN[B1] = 001 1100 U (100 1001 - 110 0000)= 001 1101

      根据算法的循环判定条件,存在有基本块的 IN 发生了变化(B4),那么将进入第三轮迭代。第二轮迭代执行完毕的结果如下图:
little_ant_

4,第三轮迭代

      第三轮迭代:在上图第二轮的执行结果上进行,OUT[B5] 保持不变, OUT[B5] = IN[exit] = 000 0000。后向分析继续往下走,按照转换函数计算出:IN[B5] = 000 1000 U(000 0000 - 001 0000)= 000 1000

OUT[B3] = IN[B5] = 000 1000;
IN[B3] = 100 0000 U (000 1000 - 100 0000)= 100 1000

OUT[B4] = IN[B5] U IN[B2] = 000 1000 U 100 1001 = 100 1001;
IN[B4] = 010 0000 U (100 1001 - 100 0100)= 010 1001

OUT[B2] = IN[B3] U IN[B4] = 100 1000 U 010 1001= 110 1001;
IN[B2] = 000 0001 U (110 1001 - 010 0010)= 100 1001

OUT[B1] = OUT[B4] = IN[B2] = 100 1001
IN[B1] = 001 1100 U (100 1001 - 110 0000)= 001 1101

      根据算法的循环判定条件,所有基本块的 IN 都没有发生变化,迭代停止,算法执行完毕。第三轮迭代执行完毕的结果如下图:
在这里插入图片描述
      此时那些绿色字体表示的就是算法的最终结果,我们在每一个程序点处得到了所有变量的存活状态。

3,Available Expressions Analysis

概述

      AEA :An expression x op y is available at program point p if (1) all paths from the entry to p must pass through the evaluation of x op y, and (2) after the last evaluation of x op y, there is no redefinition of x or y 。在程序点 p 处,一个表达式 x op y (IR的值 )是否有效取决于:(1)所有从 entry 到 p 处的路径都必须执行 x op y 表达式。(2)在每一条路径最后一次执行 x op y 后,都不能重新对 x 或者 y 赋值。

      AEA 在每一个程序点处判断程序中所有表达式的有效性,可以看出,它属于 must analysis ,因为从 entry 到 p 处的路径都必须执行 x op y 表达式,并且都不能重新对 x 或者 y 赋值,must analysis 采用的是 under-appro,它要求所有路径上的行为都必须符合要求,哪怕有一条路径不符合要求也不行,相对的是 may analysis ,它要求只要有一条路径符合要求就可以了。从这里能够看出,may analysis 在控制流处理时一般取或操作(1 op 0 = 1),而 must analysis 应该取与操作(1 op 0 = 0)。所以 must analysis 得到的结果必然是真的,缺点是可能存在有漏报。

用途

      用作优化:在程序点 p 处,可以用 p 之前的某一条路径上最后一次执行 x op y 的结果来代替表达式 x op y ,这样就不需要在 p 处重新计算表达式 x op y 的值了 。(类似于 C 中的 #define   x op y   number) 下面这个示例形象地说明了 AEA 的用途,建议阅读完分析流程的前三小节之后食用。
little_ant_

      图片的左边表示在最后一条语句之前表达式 e16 * x 是有效的。这个是由 AEA 的定义得出的结论,虽然 a 的值与 b 的值不相等,但是不妨碍做右侧的编译优化:这个优化是在表达式 e16 * x 有效的程序点处,将该表达式统一用一个临时变量 t 做替换,不再保留原来的变量 a、b、c ,当程序执行时,如果走左边的路径,原来 c 的值等于 b 的值,如果走右边的路径,原来 c 的值等于 a 的值。也就不要求 a 的值等于 b 的值啦。

分析流程

      对于 AEA ,它是判断从 entry 开始到当前程序点之间所有表达式的在当前程序点的有效性,应该采用前向分析

      并且可以发现:对于 AEA ,在程序的第一条语句之前,没有任何表达式被执行,所以在此处所有表达式都不是有效的;而在程序的最后一条语句之后,我们无法判断在此处表达式的有效性,所以我们采用前向分析。

1,抽象

      抽象:因为我们关心的是程序中所有表达式在所有程序点的有效性,那么就需要对这些表达式进行抽象。这里采用位向量来表示。类似的:假设程序中有表达式100个,那么需要有100个比特位来表示,第一位表示第一个表达式是否有效,若有效置为1,否则置为0。那么在第一个程序点(程序的第一条语句之前)处,置这100个比特位均为0,表示均不有效。

2,转换函数

      转换函数:在前向分析中的转换函数是计算如何从输入状态得到输出状态,我们以一条语句来进行分析。

      假设存在一条语句 D:a = x op y ,这一条语句使将表达式 x op y 的结果赋给 a。所以得出:在语句 D 执行之后的程序点处,新的表达式 x op y 肯定是有效的,而所有含有 a 的表达式在此时均不是有效的,因为 a 在此处被重新赋值了。。 新的表达式 x op y 的比特位在此设置为 1,其余和 a 相关的表达式全设置为 0.

      转换函数用公式表示为:OUT[B] = genB U (IN[B] - killB)       U表示或操作。

      B表示一个基本块,genB 表示符合要求的有效表达式,而 killB 表示与被重新赋值的变量相关的表达式。公式对于每一条语句也是一样适用的

3,控制流处理

      控制流处理:在有分支的情况下,假设基本块 B 的两个前驱为 P1 和 P2。现在讨论 IN[B] ,如果 OUT[P1] 中的某些表达式是有效的,而在 OUT[P2] 中的这些表达式不是有效的,根据 AEA 的定义 “必须保证每一条路径都是有效的”,认为 IN [B] 不是有效的。所以应该采用与操作,1 与 0 = 0。

      控制流处理用公式表示为:IN[B] = OUT[P](P is a predecessor of B )

must analysis 可能会产生漏报的示例:

little_ant_
      上图左侧得到在最后一条语句之前该表达式不是有效的无可厚非,那如果恰好是右侧的那一种情况呢?虽然它实际上是有效的,但是数据流分析认为它不是有效的,这便是产生了漏报。

4,算法实现

      在抽象、转换函数和控制流处理之后,关于 AEA 的算法如下图:
little_ant_
      这个算法是一个迭代算法,算法的输入是 CFG ,输出是判断每一个程序点(这里以基本块为单位)处所有表达式是否有效。

1,初始化

      初始化语句第一步,令 entry 的输出状态为空,这个之前讲过,需要从语义上来理解:因为此处之前没有任何语句,所以在这里所有表达式均不是有效的。

      初始化语句第二步,令除了 entry 之外的其他基本块的输出状态也为满,为满的原因后面会讲到,总之 may analysis 通常都会初始化为空集,用符号⊥来表示,称为 bottom。而 must analysis 刚好相反,常被初始化为满集,用符号 T 来表示,称为 top。

       在这里多讲一句,前两个例子中 1 op 0 = 1,所以不难看出总体上是从 0 → 1进行的;而 AEA 是 must analysis,1 op 0 = 0,总体上是从 1 → 0 进行的,所以需要将所有的 OUT 初始化为满集。也可以理解为 may analysis 是单调递增的,而 must analysis 是单调递减的。而它的算法停止条件和 may analysis是类似的,因为must 是递减的,所以最多减小到全为 0 的时候这个算法就停止。

      while 循环的判定条件为任何一个基本块的 OUT 集合发生改变时,循环主体是对除了 entry 块的其余基本块做:控制流处理和转换函数的实现。我们先通过一个例子熟悉这个迭代算法,见下图:一共有5个表达式,5个基本块,紧接着会对算法执行流程做说明。
little_ant_
      初始化:初始化的结果如上图的黑色字体来表示,按照算法描述,entry 的 OUT 初始化为 00000,其余的 OUT 初始化为 11111。

2,第一轮迭代

      第一轮迭代:执行循环体,用红色字体来表示执行结果,如上图显示的是 IN[B1] = OUT[entry] = 00000
前向分析继续往下走,按照转换函数计算出:OUT[B1] = 10000 U (00000 - 00101)= 10000。这里的减法运算为:1-1=0、1-0=1、0-1=0、0-0=0。

IN[B2] = OUT[B4] ∩ OUT[B1] = 11111 ∩ 10000 = 10000;
OUT[B2] = 01010 U (10000 - 10000)= 01010

IN[B3] = OUT[B2];
OUT[B3] = 00001 U (01010 - 01000)= 00011

IN[B4] = OUT[B2];
OUT[B4] = 00110 U (01010 - 00010)= 01110

IN[B5] = OUT[B4] ∩ OUT[B3] = 01110 ∩ 00011 = 00010;
OUT[B5] = 01010 U (00010 - 00101)= 01010

      根据算法的循环判定条件,存在有基本块的 OUT 发生了变化(事实上所有基本块的 OUT 都发生了变化,不再是初始的 11111了),那么将进入第二轮迭代。第一轮迭代执行完毕的结果如下图:little_ant_

3,第二轮迭代

      第二轮迭代:在上图第一轮的执行结果上进行,IN[B1]保持不变,IN[B1] = OUT[entry] = 00000
前向分析继续往下走,按照转换函数计算出:OUT[B1] = 10000 U (00000 - 00101)= 10000

IN[B2] = OUT[B4] ∩ OUT[B1] = 01110∩ 10000 = 00000;
OUT[B2] = 01010 U (00000 - 10000)= 01010

IN[B3] = OUT[B2];
OUT[B3] = 00001 U (01010 - 01000)= 00011

IN[B4] = OUT[B2];
OUT[B4] = 00110 U (01010 - 00010)= 01110

IN[B5] = OUT[B4] ∩ OUT[B3] = 01110 ∩ 00011 = 00010;
OUT[B5] = 01010 U (00010 - 00101)= 01010

      根据算法的循环判定条件,此时没有基本块的 OUT 发生了变化,迭代停止,算法执行完毕。第二轮迭代执行完毕的结果如下图:
little_ant_
      此时那些蓝色字体表示的就是算法的最终结果,我们在每一个程序点处得到了所有表达式的有效性。

总结

      本文首先讲述了数据流分析的基本方法,并通过三个经典的示例来阐述了如何对一个具体地问题采用数据流分析。在这三个示例中,我详细介绍了实现数据流分析的具体细节,比如:抽象(根据问题来选择被抽象的对象),转换函数(一般通过一个语句/基本块得到其一般形式),控制流处理(通过问题来确定采用 与 or 或)。关于采用前向分析还是后向分析的原因,应该采用 may 还是 must 的原因,在边界处(entry 和 exit)的初始化值和别的初始化语句应该为空还是满的原因在文中也多次提到,不再赘述。

      简单在聊一下对一个问题做数据流分析的流程:

      首先对问题进行分析,分析之后能够得到 如何抽象采用前向还是后向采用 may 还是 must在边界处的初始化值,然后在确定了采用 may 或 must 之后,就可以得到 除边界外的初始化语句值控制流处理采用 与 or 或。而转换函数是需要另外对具体的语句/基本块来分析得到的。

      三个应用的差异性比较见下图:
little_ant_
      后面会继续介绍静态程序分析的理论基础-格理论。未完待续~

      行文仓促,文章较长,若存在笔误,抱歉!

猜你喜欢

转载自blog.csdn.net/Little_ant_/article/details/118732952