静态分析学习笔记02:程序中间表示(IR)
1.程序中间表示(IR)?
IR的本质是编译器前端工作的最后产物,编译器会先生成IR,然后进一步地对程序进行优化,最后生成目标算法,常见的IR可以通过抽象层次可以分为三类
- HIR:这类IR主要基于语言进行一些分析与变换,能够准确的表达源语言的语义,IDE中的语法符号依赖关系、类型检查、语法错误纠正都属于这类。HIR的代表性例子是AST和符号表,
- MIR:这类IR独立于原语言和系统CPU架构进行分析与优化,其中主要包括算术优化、死代码删除等。MIR的代表例子是三地址码。
- LIR:LIR是依赖于cpu架构进行优化与代码生成的,这类IR通常与机器指令一一对应。部分JIT编译器的IR就是属于LIR。
2.编译器与静态分析
编译器的前端基本步骤主要有以下4个
- 词法分析(Lexical Analysis):生成Token,其常用的生成的规则为正则表达式
- 语法分析(Syntax Analysis):生成AST, 其常用的规则为上下文无关文法,这里之所以使用上下文无关文法来对语法进行分析是因为上下文相关文法的分析复杂度远远高于上下文有关文法,上下有关文法主要是用于 自然语言。
- 语义分析(Semantic Analysis):生成进一步的 AST,对类型进行检查,这里的检查规则是属性语法
- 转换器(Translator):将上一级的AST转换为三地址码
这里编译器前端(到IR之前的步骤)可以被视为静态分析的基础,大部分静态分析的基底是IR中的三地址码,静态分析主要研究的是non-trivial的问题,而编译器前端则会对程序trivial部分进行分析,其中trivial的正确性是对于non-trivial进行分析的前提,在trivial的部分已经出现错误的情况下,进行non-trival问题的分析意义不大
3.AST与三地址码
我们来看以下这个例子,下面是一段简单的程序
ini
do
i=i+1;
while(a[i]<v);
下图是其对应的AST结构
其对应的三地址码则是
ini
1: i=i+1
2: t1=a[i]
3: if t1<v goto 1
我们这里可以总结一下IR与AST的区别
特性 | 抽象语法树(AST) | 三地址码(3AC) |
---|---|---|
与机器级表示接近程度 | 远离汇编 | 接近汇编 |
与语言相关程度(重点) | 依赖于语言 | 不依赖于语言 |
控制流信息 | 不包含 | 包含 |
常见优势 | 适合进行快速类型检查 | 更加精简 |
4.三地址码下的真实静态分析
此处我们会简单介绍基础的三地址码,同时更加集中地介绍soot当中使用的三地址码
4.1 基本的三地址码
首先先了解三地址码的基本格式,三地址要求最多只能有一个操作符在语句的右侧,下面是一个例子
以下是一个基本三地址码的基本语句规则:
soot是目前java所使用的主流静态分析器,其使用的三地址码被称为jimple(typed 3 adress code)
4.2 循环的三地址号码
参考以下这个简单的for循环
ini
public class Forloop3AC{
public static void main(String[] args){
int x=0;
for(int i=0; i<10;i++){
x=x+1;
}
}
}
其所对应的jimple格式的三地址码如下所示,注意此处的x已经被优化掉了,此处x没有用到
ini
public static void main(java.lang.string[]){
java.lang.String[] ro;int i1;
r0 := @parameter0: java.lang.String[];
i1 = 0;
label1:
if i1 >= 10 goto label2;
i1 = i1 + 1;
goto label1;
label2:
return;
}
4.3 函数的情况
这里我们再看函数调用的情况,下图是java的原代码
soot生成的jimple如下图所示(foo函数段):
留意上面这段程序
-
此处的java.lang.StringBuilder是java用来处理字符串的一个特殊类,上述语句的字符串拼接可以理解为java先对运算符"+"进行了重载。
-
此处的specialinvoke和virtualinvoke是jvm所支持的四种指令中的一种,jvm中一共有四种指令
-
invokespecial:用于调用构一些需要特殊处理的的方法比如构造函数、父类中的方法、私有的方法
-
invokevirutal:调用实例方法,根据对象的实际类型进行分派(虚方法分派)
-
invokeinterface:不作优化,用于调用
-
invokestatic:用于调用类方法(static方法)
-
invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,为动态语言在jvm上运行提供支持。
- 静态语言:静态语言是在编译时变量的数据类型即可确定的语言(C/C++,Java),常见特征是需要先生命函数类型
- 动态语言:动态语言的数据类型需要在运行时才能确定(python,ruby)
-
留意此处的代码段,这个是本段代码的方法签名,一个简单的的方法签名分别包括
,类名、返回类型、方法名、参数类型
-
csharp
specialinvoke $r3.<java.lang.StringBuilder: void <init>()>();
上面这段jimple的主要过程是分别三次调用StringBuilder类,并分别依次向代码中传入r1," ","r2"最后再调用toString类来返回一个String类型的变量
下面这段jimple则是main段的jimple
此处需要主义的是,在新建数组后,java会调用其构造函数,这就是第一行specialinvoke的来源,之后就是通过virtualinvoke来调用之前的foo方法,并且传入"hello","world"两个实参,此处的result因为没有用到所以被优化了
4.4 类的情况
以下是class的java代码与其生成的三地址码
这里的第一行是java根据语义自动继承了Object这个父类,在没有给出构造函数的情况下,java会自动生成一个构造函数(init函数),构造函数会用specialinvoke来调用父类的构造函数
此处的第二个函数就是main函数,此处我们main函数是空的,所以只有获取实参数值的部分
此处的第三个函数clinit是用于初始化类中的静态数值,此处我们只有一个pi,所以在此处进行初始化
5.静态单赋值(SSA)
SSA可以被认为是一种特殊的三地址码,其主要的规则如下
- 给定每一个定义一个新的名字
- 用下标来区分不同的定义
- 保证每一个变量都只有一个定义
在出现分支情况(如if语句),会定义一个Φ函数,其中函数的两个参数是两个分支中的一个取值
SSA的好处有以下几点
- 把流信息间接地包含在单一的变量名称中:这样有助于在flow-insensitive的情况下获得flow-sensitive的精度。
- 定义与使用更加明显:有助于优化算法。
SSA也有一定的问题
- SSA可能会定义出太多变量
- SSA最后还是需要转换为字节码,在这个转换过程中可能会出现性能问题
7.控制流分析(CFG):
7.1 基本块(Basic Block)
为了进一步分析,我们需要将三地址码转换为CFG
这里涉及到如何将程序进行划分,在进行CFG的时候,我们往往会将3AC以基本块(Basic Block)作为图的结点
Basic Block的 划分遵循的原则是:基本格第一条指令围为控制流入口,基本格的最后一条指令是控制流出口。
basic block的划分算法遵从以下4个准则:
- 每一个跟在goto指令后面的指令都是basic block的起点,
- 第一条指令也是basic block的起点
- 所有goto指令的目标语句也 basic block的起点
- basic block的终点是下一个basicblock起点的上一条语句
以下是一个例子
7.2 构建CFG
在确定了Basic block就可以通过添加边来构建CFG了,添边的规则
- 无条件跳转只需要在跳转目标所示的Basic Block间添加一条边
- 有条件跳转需要在满足表达式的的Basic Block与相邻的下一个Basic Block间都添加一条边
- 如果没有出现上述两种情况,且相邻的basic block则需要按照先后顺序添加一条边
这里还有一个变换,需要将goto的具体指令转换为Basic Block的标志,这样做的目的是通过将信息转换为粗粒度避免Basic Block中的指令对控制流的影响
以下是一个例子
最后需要在包含第一个指令和最后一个指令的Basic Block加上出口与入口节点