【南大软件分析-学习笔记】Lecture 3-Data Flow Analysis(上)
0. 前言
从本节课开始的四节课都是在讲述数据流分析,本节课与下一节课主要介绍数据流分析的实际应用,重点介绍 Reaching Definitions Analysis、Live Variables Analysis 以及 Avaiable Expressions Analysis 三种分析方法,再之后的两节课则是介绍数据流分析的理论框架,废话不多说,直接开始吧。
1. Overview of Data Flow Analysis
首先简要说明一下叫做 数据流分析(Data Flow Analysis),在上一节课中我们已经知道了在静态分析中,我们主要是对 控制流图(CFG)进行分析,这样做的目的是为了使用图上的一系列便利的算法,那么数据流分析所研究的问题也就很明了了, “How Data Flows on CFG?”,即数据是如何在控制流图上传递的,如果进一步的拓展开来,也可以说成:
“How application-specific Data Flows through the Nodes(BBs/statements) and Edges(control flows) of CFG(a program)?”
对于 CFG 中的节点,我们需要使用 Transfer Function 进行处理,而对于边来说我们则需要进行控制流处理(Control-flow handles),比如说对上游分支进行union操作。
前面也说了静态分析中主要是研究对象是抽象的符号,那么用一句话概述数据流分析的主要思想就是:特定的抽象符号所代表的数据是如何在环境中安全流动的,这个环境主要研究 CFG 中的传递函数和控制流处理。
不同的数据流分析(data-flow analysis application)往往具有不同的数据抽象方法(data abstraction)、不同的安全近似策略(flow safe-approximation)、不同的传递函数(transfer functions)以及不同的控制流处理方案(control-flow handlings),我们会在第三节分别介绍三个常见的控制流处理方案。
2. Preliminareis of DFA
为了更好的理解后面所介绍的数据流分析方法,在此节会先介绍一下两个基本概念。
2.1 Input and Output States
首先介绍一下输入(input)和输出(output)状态,对于一个IR 语句 \(s_n\) 来说:
- 每一个 IR 语句的执行都会将一个 input 转化为新的 output;
- 某个 IR 语句的输入 / 输出状态都与该语句之前 / 之后的程序点(Program point)关联。
上图展示了三种典型关系中各个相邻的 IR 语句之间 input state 和 output state 的关系,在每个数据流分析中,我们会为每个程序点关联一个数据流值(data-flow value),这个值表示该点可以观察到的所有可能程序状态(program state)的集合的抽象,因此,对于上图的三种关系,s1、s2、s3的关系分别是:
- IN[s2] = OUT[s1];
- IN[s2] = IN[s3] = OUT[s1];
- IN[s2] = OUT[s1] ^ OUT[s3] (这里的 ^ 不是∩的意思,也可能是union等其它关系)
数据流分析的目的就是为所有语句的输入输出找到一组,基于控制流和传递函数的,安全的近似约束(safe-approximation)的解决方案。
2.2 Notations for Transfer Function’s Constraints
基于传递函数的约束分为两类:
- 前向分析(forward analysis): \(OUT[s] = f_s(IN[s])\)
- 反向分析(back analysis):\(IN[s] = f_s(OUT[s])\)
2.3 Notations for Control Flow’s Constraints
基于控制流的约束也分为两类,分别是 BB 内和 BB 间,其中 BB 间还可以细分为前向和反向两种情况:
- Control flow winthin a BB:\(IN[s_{i+1}] = OUT[s_i]\), for all \(i = 1,2,3...n-1\)
- Control flow among BBs:\(IN[B]=IN[s_1]\), \(OUT[B] = OUT[s_n]\)
- forward:\(OUT[B]=f_B(IN[B]),f_B=f_{s_n}∘ … ∘f_{s_2}∘f_{s_1}\),\(\text{IN}[B]=\bigwedge_{P\ \text{a predecessor of }B}\text{OUT}[P]\)
- backward:\(IN[B]=f_B(OUT[B]),f_B=f_{s_n}∘ … ∘f_{s_2}∘f_{s_1}\),\(\text{OUT}[B]=\bigwedge_{P\ \text{a predecessor of }B}\text{INT}[P]\)
这四个公式的意思分别是:
- BB 间一个 IR语句的输出等于紧邻它的前一句 IR 语句的输入
- 一个 BB 的输入等于这个BB的第一句 IR 的输入,输出等于这个BB的最后一句 IR 的输出
- 前向传播的输出由输入经过运算得到,输出由指向自己的输出的union决定
- 反向传播的输入由输出经过运算得到,输出由自己指向的输入的union决定
3. Reaching Definitions Analysis
在进入正式的 DFA 之前,为了简化初学门槛,在这一节的所学习的方法中并不会涉及函数调用和别名,前者将放在 Inter-procedural Analysis 中进行学习,而后者将放到 Pointer Analysis 中进行学习。
3.1 Reaching Definition
定义:在程序中的一个程序点 p 和程序点 q 构成一个路径 ,在 p 点的定义 d 在这条路径中不会被杀死(killed),那么就定义 d 可以从点 p reach 到点 q。也就是说,v 在整个路径中不会被新的定义覆盖。
用途: Reaching Definitions 可以用来检测可能存在的、使用了未被定义的变量(detect possible undefined varibales)的情况,例如:在 CFG 入口为每个变量引入一个虚拟定义,如果这个虚拟定义能从入口 reach 一个使用了该变量的点 p,那么这个变量就有可能未定义便被使用,因为 undefined reache 了使用变量的 p。
3.2 Understanding Reaching Definitions
在第一节课和前面都提到了 DFA 主要关注两个方向,一个是如何进行 data abstraction,另一个是如何构建 flow safe-approximation strategies,后者包括 Transfer functions 和 control-flow handlings,接下来从这三个方面详细介绍。
3.2.1 data abstraction
对于 DFA 来说,数据流中的值是一个程序中所有变量的定义,对于一个节点而言,我们所关注的点是其余点能否到达该点,联想计组中的相关知识,我们可以用一个bit vector 来表示。如下图示例所展示,如果程序有 D1 到 D100 共计100个变量定义,那么就可以用一个长度为100的 bit vector 来表示它,其中对于 D_i 来说,如果第 n 位为1,则表示 n reaches i,反之就是 n 不可以 reach i。
3.2.2 Safe-approximation
D: v = x op y
对于一个 Definition D来说,可以将其视为一个三地址码,对于它而言,它所做的的事有:
- 生成了一个定义D;
- 在保持其他传入程序不受影响的同时,杀死了程序中其他对变量 v 的定义。
所以对于一个 BB 而言,它的 Transfer Function 应该是:
下图展示了一个 CFG 中各 BB 的 gen 和 kill 情况:
并且也可以得到它的 control-flow handling:
3.2.3 Algorithm
首先来看一下伪代码:
算法的整体思路是这样的:
首先初始化每个 BB (包括入口)的 OUT 为空,紧接着只要有任何一个块的 OUT 改变了,那么就不断的进行遍历输出所有块新的 IN 和 OUT,直至循环停止。
需要关注的点有两个:
- 为什么要单独给 entry 赋值空,而不是和 BB 一起赋值,反正都是空?
- 如何保证 while 循环一定会停止?
问题一很容易理解,因为伪代码是通用的模板,在实际中当然可以进行合并操作,特意把它提出来是为了方便修改,比如说 entry 以外的 BB 的初始化集有可能不是空集。
问题二是因为一个程序中的 definitions 是有限的,那么 bit vector 的长度也是有限的,由于 gen 和 kill 在 CFG 确认时就跟着确定了,是不变的,因此 BB 的 OUT 只与 IN 有关,并且 OUT 置 1 数一定不会比 IN 小。而 IN 又是基于它的前驱的 OUT 产生的,因此每个循环后的 IN 一定不会减小,所以该算法是单调递增的,并且有一个上界,即 bit vector 各位都是1,全部可达的情况,因此最终循环一定可以停止。
下面就结合一个具体的例子来过一遍整个算法。
初始化所有 BB 的 IN 和 OUT 都是 0000 0000,即初始化所有 BB 之间都是不可达的,随后依次进行三次迭代,第三次迭代各 BB 的 OUT 输出不变,循环停止,最终得到的不同 program point 处的 bit vector 就是定义可达性的分析结果,例如 B5 的最终 OUT 为 0011 1011 就代表 D3、D4、D5、D7、D8可以到达 B5,每一轮迭代的核心就是上面介绍的公式1和公式2。
BB 序号的不同可能会导致达到稳定状态的迭代次数的不同,但是不会影响稳定状态的结果。

浙公网安备 33010602011771号