【南大软件分析-学习笔记】Lecture 2-IR
0. 前言
紧接着就来到了第二节课啊,这节课主要讲的是 Intermediate Representation (中间表示),废话不多说直接开始吧。
1. Compliers & Static Analyzers
首先简单介绍一下编译器,编译器的目的是将程序员输入的高级语言(如C、Java等)转化为机器识别的二进制语言(010100.etc)如下图所示,一个编译器通常要完成一下几个部分:
- Lexical Analysis:词法分析,通过 Scanner(扫描器)对输入进行扫描,根据 Regular Expression(正则表达式)进行词法分析,判断输入中是否存在非法字母或单词,检测完毕后输出 Tokens 供下一步检查;
- Styntax Analysis:语法分析,通过 Parser(解析器),使用 Context-Free Grammar(上下文无关文法)对上一步输入的 Tokens 进行语法分析,分析输入是否符合语法规则,随后将输出 AST 供下一步检查;
- Semantic Analysis:语义分析,通过 Type Checker(类型检查),根据 Attribute Grammar(属性问法)判断是否存在语法符合规则但是语义不合理的情况(如实际代码中 int、string、double不同类型间的直接计算这样的错误),随后生成 Decorated AST 供下一步检查;
- Static Analysis:静态分析,将抽象语法树转换为 IR 进行静态分析,一般是转换为 3AC(三地址码),最后将优化后的代码转换为二进制机器码供机器使用。
2. IR
2.1 AST vs. IR
由上面可以看到 AST 和 IR 是最常用的两个中间结果,那么二者又各自有什么区别呢?为什么使用 IR 进行静态分析,而不是直接使用 AST呢,下面使用一段简单的代码来介绍一下二者的区别:
以上代码的 AST 和 IR(三地址形式)分别如上方左右子图所展示,二者主要区别如下:
- AST:层次更高,主要接近于语法结构,通常和语言紧密相关,但是缺乏控制流信息;
- IR:层次更低,更加接近于机器码,通常是语言无关的,更加的简洁(compact)且统一(uniform),并且包含控制流信息。
在实际工程项目中,正是受益于 IR 的这些特效,大型企业的静态分析器往往可以对多种语言进行静态分析。
2.2 Three-Address Code(3AC)
3AC (三地址码)就是静态分析常用的一种中间表达,它的每条指令最多包含两个操作数,一个结果变量以及一个操作符,每条指令最多涉及三个变量,并且每个 3AC 最多包含三个地址,分别是:变量名,常量,编译器生成的临时变量。
在不同的编译器中往往采用不同的 3AC 表示形式,下图就是一种较为常见的形式:
2.3 3AC in Real Static Analyzer: Tai-e
那么在真实应用场景中的 IR 又会是什么样子的呢?下面就以李樾老师和谭添老师设计的 Tai-e 框架为例,简要介绍一下三个实例。
注: 此段在教学视频(20年录制版本)是以 Soot 框架为例进行介绍, 而在课程官网 PPT (应该是24年教学版本) 是以 Tai-e 框架为例进行介绍的, 由于课程配套作业也是以 Tai-e 框架教学版本为例, 因此这里以 PPT 上为准
Tai-e 是一个分析 Java 程序的静态程序分析框架,相比于已有的知名静态程序分析框架(如 Soot、Wala 等),Tai-e 要易学易用很多。Tai-e 提供精炼的 IR,明晰且易拓展的接口,丰富的程序分析算法,直观易懂的框架结构,以及指导性强的文档等。需要注意的是,Tai-e 分为教学版和科研版两个版本,两个版本虽都易学易用,但侧重有所不同。该套实验作业的设计是基于教学版,相比于科研版,教学版更容易方便学生理解作业题目及实验相关代码,但是由于教学版在很多分析的具体设计上与科研版有所不同,因此它们在分析能力和性能上有较大差距。尽管如此,由于 IR、框架结构、程序表示等诸多方面的一致性,熟悉教学版后会非常容易上手科研版。([以上摘自课程实验平台官网介绍](概述 | Tai-e))
2.3.1 Do-While Loop
Java src:
package nju.sa.examples;
public class DoWhileLoop3AC{
public static void main(String[] args) {
int[] arr = new int[10];
int i = 0;
do {
i = i + 1;
} while(arr[i] < 10);
}
}
3AC (Tai-e):
public static void main(java.lang.String[] args) {
int %intconst0, i, %intconst1, temp$4;
int[] arr;
[0@L4] %intconst0 = 10;
[1@L4] arr = newarray int[%intconst0];
[2@L5] i = 0;
[3@L6] nop;
[4@L7] %intconst1 = 1;
[5@L7] i = i + %intconst1;
[6@L7] nop;
[7@L8] temp$4 = arr[i];
[8@L8] if (temp$4 < %intconst0) goto 3;
[9@L8] goto 10;
[10@L8] nop;
[11@L8] return;
}
2.3.2 Method Call
Java src:
package nju.sa.examples;
public class Methodcall3Ac {
String foo(String paral, String para2) {
return paral + " " + para2;
}
public static void main(String[] args) {
MethodCall3AC mc = new MethodCall3AC();
String result = mc.foo("hello", "world");
}
}
3AC (Tai-e):
java.lang.String foo(java.lang.String para1, java.lang.String para2) {
java.lang.StringBuffer temp$0;
java.lang.String %stringconst0, temp$1;
[0@L4] temp$0 = new java.lang.StringBuffer;
[1@L5] invokespecial temp$0.<java.lang.StringBuffer: void <init>()>();
[2@L5] invokevirtual temp$0.<java.lang.StringBuffer:java.lang.StringBuffe append(java.lang.Object)>(para1);
[3@L5] %stringconst0 = " ";
[4@L5]invokevirtual temp$0.<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.Object)>(%stringconst0);
[5@L5]invokevirtual temp$0.<java.lang.StringBuffer: java.lang.StringBuffer append(java.lang.Object)>(para2);
[6@L5] temp$1 = invokevirtual temp$0.<java.lang.StringBuffer: java.lang.String toString()>();
[7@L5] return temp$1;
}
public static void main(java.lang.String[] args) {
nju.sa.examples.Methodcall3Ac temp$0, mc;
java.lang.String temp$1, %stringconst0, %stringconst1, result;
[0@L9] temp$0 = new nju.sa.examples.Methodcall3AC;
[1@L9] invokespecial temp$0.<nju.sa.examples.Methodcall3AC: void <init>()>();
[2@L9] mc = temp$0;
[3@L10] %stringconst0 = "hello";
[4@L10] %stringconst1 = "world";
[5@L10] temp$1 = invokevirtual mc.<nju.sa.examples.MethodCall3AC:
java.lang.String foo(java.lang.String, java.lang.String)>(%stringconst0, %stringconst1);
[6@L10] result = temp$1;
[7@L10] return;
}
2.3.3 Class
Java src:
package nju.sa.examples;
public class Class3AC {
public static double pi = 3.14;
public static void main(String[] args) {
}
}
3AC (Tai-e):
public class nju.sa.examples.Class3Ac extends java.lang.Object {
public static double pi;
public void <init>() {
[0@L2] invokespecial %this.<java.lang.Object: void <init>()>();
[1@L2] return;
}
public static void main(java.lang.String[] r0) {
[0@L6] return;
}
static void <clinit>() {
double %doubleconst0;
[0@L3] %doubleconste = 3.14;
[1@L3] <nju.sa.examples.Class3AC: double pi> = %doubleconst0;
[2@L3] return;
}
}
这里简单的介绍一下指令前面的形如[0@L3]的表示什么意思,@前面的数字表示该条 3AC 指令在当前方法指令序列中的全局索引,从零开始,而@后面的“L+数字”表示该条 3AC 指令在 JAVA 源码中的行号。
其余不过多赘述,看的话应该大概能读懂啥意思,老师在课程上也说了全部背下来也不现实,借助工具多学多看即可。
2.4 Static Single Assignment(SSA)
静态单赋值 SSA 是指对每个变量都单独赋值一次,这样一来就不会出现变量的重复赋值,可以帮助简化分析,但是也有可能引入过多变量和 φ 函数,可能会降低机器码的生成效率,就像下图所展示的一样。
3. Control Flow Analysis
控制流分析通常使用 CFG(控制流图)进行分析,其基本单位可以是单个的三地址码,也可以是 Basic Block(BB),后者更为常见,下图就是一个典型的 CFG。(此节将是接下来四节课的重点)
3.1 Basic Blocks (BB)
BB 是指一个连续的、最长的满足以下特性的 3AC 序列:
- 控制流只能从该序列的第一条指令进入;
- 控制流只能从该序列的最后最后一条指令退出。
只要再增加一条就不满足以上特性,根据以上特性,我们不难得出划分 BB 的方法:
- 寻找 leaders,即第一条指令,leaders 可以由以下几个性质得到:
- 序列中的第一条指令一定是 leader;
- 所有 jump 指令的 target 指令;
- 所有 jump 指令的后一条指令;
- 每个 leaders 和下一个 leaders 前面的那一条指令合并为一个 BB。
3.2 Control Flow Graphs (CFG)
划分好 BB 后就可以进一步构建 CFG 了,构建方法如下:
- 如果块 A 可以 jump 到块 B,或块 B 紧挨着 A ,且 A 的最后一条指令不是无条件跳转,那么从块A到块B之间就会有一个有向边;
- 将原来 3AC 序列中的所有指令序号改为基本块序号。
4. 后记
这一节课也结束啦,鼠鼠有幸在大二学过编译原理,因此这节课整体上理解起来会比较简单,下节课开始就正式的进入静态分析了,冲。

浙公网安备 33010602011771号