🔗 课程链接:李樾老师和谭天老师的:南京大学《软件分析》课程02(Intermediate Representation)_哔哩哔哩_bilibili
目录
第二章 Intermediate Representation
2.1 编译器与静态分析器的关系(Compilers & Static Analzers)
2.2 AST vs. IR
2.3 IR: 三地址码(Three-Address Code, 3AC)
2.4 真实静态分析器中的3AC:Soot
2.5 静态单赋值(Static Single Assignment, SSA)
2.6 控制流分析(Control Flow Analysis)⭐
2.6.1(Basic Blocks, BB)——建立节点
2.6.2 控制流图 (Control Flow Graph, CFG)——添加边
2.7 总结
第二章 Intermediate Representation
2.1 编译器与静态分析器的关系(Compilers & Static Analzers)
编译器的作用:
将程序员写的Source Code转换成机器可以识别的Machine Code,并在转换中及时报错。
编译器的主体框架:
- 通过扫描器(Scanner)做词法分析(Lexical Analysis):根据正则表达式(Regular Expression)检查输入字符是否合法(例如如果是翻译一个英语句子,就是检查字符是否是英文的,单词是否正确/合法),如果通过了词法分析,每个单词就会生成Tokens,给下一步进行分析。
- 通过解析器(Parser)做语法分析(Syntax Analysis):通过语法规则(上下文无关文法,Context-Free Grammar)检查语法,通过就转换成抽象语法树AST
- 通过类型检查(Type Checker)做语义分析(Semantic Analysis):根据属性语法(Atrribute Grammar)检查语义(期望实现的是识别在英语中的例如Apple eats you,这样语法正确但是语义不对的句子,但是实际上实现的是,例如String的值除以int 这种类型不匹配的错误)如果通过,就生成Decorated AST
- 为了进行静态分析优化,要将Decorated AST转换为中间表示(IR),这里的IR一般为三地址码,再进行静态分析,最后通过代码生成器将优化后的代码生成机器码传给机器。
实际上,静态分析就是做code优化的,静态分析器在IR的基础上进行分析。
2.2 AST vs. IR
如图所示
AST | IR |
语法树形式的,hight-level,更接近程序源码 | 通常是三地址码的形式,low-level,更接近机器编码 |
通常与语言有关 | 通常与语言无关 |
\ | 统一、简洁 |
缺少控制流信息 | 包含控制流信息 |
适合做快速的类型检测 | 通常被认为是静态分析的基础 |
所以,以三地址码为主要形式的IR是更有利于做静态分析的。
2.3 IR: 三地址码(Three-Address Code, 3AC)
什么是3AC?
三地址码就是最多有三个地址(address)的表达形式,并且由于这个性质,每个三地址码的右侧最多只能有一个运算符。这里的地址(Adress)可以是以下三种形式之一:
- 变量名称: a, b
- 常量Constant: 3
- 编译器生成的临时变量: t1, t2
例如:
t2 = a + b + 3
转换为三地址码的形式就是:
t1 = a + b
t2 = t1 + 3
一些常见的3AC形式:
如上图所示,3AC的操作符也包含很多种形式可以是很多种形式:
- bop 代表的普通二进制运算或逻辑运算(+ - * / ……)
- uop 代表的单运算符(取负数,非……)
- 也可以没有运算符
- goto L 无条件的跳转
- if… goto L 有条件的跳转
- rop 代表条件操作(>,<,==,……)
当然这些知识简单的3AC形式,还有更复杂的,我们通过soot这个例子来学习,见2.4
2.4 真实静态分析器中的3AC:Soot
Soot是一个Java优化框架。它提供了四种中间表示法,用于分析和转换Java字节码。其中 Jimple是一种适合优化的类型化3地址中间表示法(typed 3-address code)。这一部分通过Jimple对3AC有更好的了解。
https://github.com/Sable/soot
Tutorials · soot-oss/soot Wiki · GitHub
1. For循环
如下图所示将一个for循环的java代码转成3ac,这个例子比较简单,可以直接看懂,额外表注的还有关于传递参数时候的3AC表示。
2. Do-while 循环
以下是do-while循环的3AC,理解起来也比较简单
3. 方法调用
方法调用相对复杂一点,首先了解一些JVM的知识。
① JVM中主要的方法调用:
- invokespecial:用于调用实例构造器<init>()方法、私有方法和父类中的方法
- invokevirtual:调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种,在调用的过程中会进行派生(vitual dispatch)
- invokeinterface:调用接口方法,会检查实现这个接口的对象,但是调用的时候不能做一些优化
- invokestatic:调用静态方法,
- Java7: invokedynamic -> Java 是静态语言,动态解析出需要调用的方法,然后执行。
参考:5、JVM中的方法调用 - CarBlack - 博客园 (cnblogs.com)
② 方法签名(Method Signature)包含类名、 返回值类型、方法名 (参数1 类型,参数2 类型)
例如:specialinvoke $r3.<java.lang.StringBuilder: void <init>()>(); 这个构造函数的尖括号里的就是一个方法签名,具体含义就是
- 方法类型:java.lang.StringBuilder 构造函数
- 方法名字:默认构造函数没有名字,统一叫<init>
- 参数:没有参数 () 为空
- 返回值:没有返回值,void
再如下图中的3AC意思就是:调用$r3的append 方法,将 r1 加进字符串中
再看3AC的代码可能就会理解一些了,以下的示例代码是在main函数中调用了foo方法,其中foo方法中有两个参数,返回拼接的字符串,先看foo函数的3AC代码
main函数的3AC代码
4. Class 类的3AC
这里不太明白也没关系,反正再看地址码的时候别怕,仔细分析一定是可以看懂的。
2.5 静态单赋值(Static Single Assignment, SSA)
1. SSA的两个特点:
① 如下图,SSA里,会给每个变量起不同的名字:
② 但是如果是同一个变量在不同的分支呢?在这种情况下,SSA会在合并的时候,引入一个 Φ函数,如下图所示,在引入x2 等于这个 Φ 函数,后续使用x2。这个Φ(x0,x1)会根据流的路径来选择是x0 还是x1然后赋值给x2。
2. SSA的优点:
- 程序流信息可以间接体现在不同的变量名上,通过不同的变量名,流的信息被间接地合并到唯一的变量名中
- 可能有助于提供一些更简单的分析,例如,流不敏感分析通过SSA定义和使用对显式获得流敏感分析的部分精度。(因为流敏感度分析精度虽然高,但是太耗时了,而流不敏感分析就相当于上下文无关的分析,通过单一赋值制就可以获得很久之前就定义过的信息,知道是具体用了那边信息,就相当于获取到了上下文信息。)
- Define-and-Use pairs(定义-使用对)明确
- 在一些按需任务中启用更有效的数据事实存储和传播。
- 一些优化任务在SSA上表现更好(例如,条件常数传播,全局值编号)。
3. SSA的缺点:
- 如果程序有太多分叉,则会引入太多变量和Φ函数,
- 在最后还要转为机器码执行,在转换回去的时候由于过多的赋值操作产生性能方面的影响。
2.6 控制流分析(Control Flow Analysis)⭐
- 在进行控制流分析的时候,通常采用建立控制流图(CFG)
- CFG是静态分析的基础结构
- CFG中的节点可以是单个的3AC,不过通常用Basic Blocks(BB),如下图所示
2.6.1(Basic Blocks, BB)——建立节点
1. BB的基本概念:
Basic Blocks(BB)是连续的、满足以下性质的、最大长度的 三地址指令的组成单元:
- 这个Block的入口只有一个,就是第一个指令,不能从其他地方进来。
- 这个Block的出口只有一个,就是最后一条指令,不能从其他地方出去。
如下图所示,仅能从第一条指令出进去该Block,并且仅能从最后一条指令出去,中间的指令不允许进出,能满足这些性质的、连续的、最大的指令集合就是一个Basic Blocks。
2. 举个例子🌰
如下图所示,怎么样来分代码块呢?
- 首先,程序从(1)入口, (2) 没有其他入口和出口,但是(3) 有额外的入口,即从(11)跳转进入,所以(3) 不能和(1)(2)组合在一起,所以 (1)(2)可以组成一个Basic Block
——>得出结论:如果一个指令是某个跳转的目标,则该指令只能作为一个BB的入口 - 在看,(3)作为一个Block的入口,(4)没有入口,有个出口,可以加入,但是(5)不能加入,因为(4)已经有出口了,不能作为一个BB的中间指令,所以(3)(4)可以组成一个Basic Block
——>得出结论:如果一个指令紧跟着一个跳转,则该指令只能作为一个BB的入口
3. 怎么建立BB
通过上述例子的思考,就可以来尝试设计建立Basic Blocks的算法了,伪代码如下:
INPUT: 一个三地址指令序列 P
OUTPUT: P 的Basic Block列表
Method:
(1) 确定 P 的入口 (leaders)
· P 的第一句是一个入口
· 任何一个跳转的目标指令,是一个入口
· 任何一个跳转的下一条指令,是一个入口
(2) 建立BBs for P
· 每个入口到下一个入口之间就是一个BB
练习:根据算法的设计思想,再看一下2中的例子,完成程序BBs的划分:
4. 将跳转到指令标签替换为跳转到基本块。
经过上述的划分,我们就把一个程序P 分割成了多个BBs,并且由于BB的定义导致跳转目标必定是某个BB的入口指令,所以可以将跳转目标的指令标签的形式,换成BB的编号。如下图所示,将原本的跳转到指令标签(7)替换为跳转到基本块 B4
2.6.2 控制流图 (Control Flow Graph, CFG)——添加边
1. 建立边
在建立完CFG的节点之后,需要添加边(edge),基本规则如下:
- A--->B 是跳转过去的,则A到B 建立一个边,如第一组红色的AB
- A--->B 是有条件跳转,如第二组的蓝色的A跳转到第一组红B,且蓝A后还有指令蓝B
- A--->B 建立一条边(if goto)
- A--->B(A紧接着的下一条指令 B) 也需要建立一条边(条件jump block 天然有两出口)
- A->B 是第三组的绿A无条件跳转到第一组的红B,其中第三组的绿B按原指令紧挨着第三年组的绿A
- A--->B 建立一条边 (goto)
- A-×-> B 不建立绿A 向其代码紧挨着的下一条指令的边
完成节点的划分、编号,最后一步就是再按照顺序以及根据上述示例的规则添加上边,将BB连接起来,最后通常,我们在加上Entry 和 Exit,这样就得到了一个控制流图。
我们还用2.6.1中的例子,为其添加上边,如下图所示。
2.7 总结
这一章先讲了编译器与静态分析器的关系,侧重于了解静态分析处在什么位置,主要运用IR进行静态分析,介绍了AST与IR的不同。
什么是三地址码,并通过soot更清楚的认识真实分析中的3AC。
介绍CFG的建立步骤,以及Basic Blocks的划分方法,这些静态分析的基础。
🌻写在最后:
笔者也是软件分析方向的学生, 这个是跟着课程总结和记录的学习笔记, 如果有错误的地方, 欢迎讨论\批评\指正.
如果有更多关于软件分析领域的资料\论文\课程\建议, 欢迎留言, 一起学习进步~