【南大软件分析-学习笔记】Lecture 2-IR

0. 前言

紧接着就来到了第二节课啊,这节课主要讲的是 Intermediate Representation (中间表示),废话不多说直接开始吧。

1. Compliers & Static Analyzers

首先简单介绍一下编译器,编译器的目的是将程序员输入的高级语言(如C、Java等)转化为机器识别的二进制语言(010100.etc)如下图所示,一个编译器通常要完成一下几个部分:

  1. Lexical Analysis:词法分析,通过 Scanner(扫描器)对输入进行扫描,根据 Regular Expression(正则表达式)进行词法分析,判断输入中是否存在非法字母或单词,检测完毕后输出 Tokens 供下一步检查;
  2. Styntax Analysis:语法分析,通过 Parser(解析器),使用 Context-Free Grammar(上下文无关文法)对上一步输入的 Tokens 进行语法分析,分析输入是否符合语法规则,随后将输出 AST 供下一步检查;
  3. Semantic Analysis:语义分析,通过 Type Checker(类型检查),根据 Attribute Grammar(属性问法)判断是否存在语法符合规则但是语义不合理的情况(如实际代码中 int、string、double不同类型间的直接计算这样的错误),随后生成 Decorated AST 供下一步检查;
  4. 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 的方法:

  1. 寻找 leaders,即第一条指令,leaders 可以由以下几个性质得到:
    • 序列中的第一条指令一定是 leader;
    • 所有 jump 指令的 target 指令;
    • 所有 jump 指令的后一条指令;
  2. 每个 leaders 和下一个 leaders 前面的那一条指令合并为一个 BB。

3.2 Control Flow Graphs (CFG)

划分好 BB 后就可以进一步构建 CFG 了,构建方法如下:

  1. 如果块 A 可以 jump 到块 B,或块 B 紧挨着 A ,且 A 的最后一条指令不是无条件跳转,那么从块A到块B之间就会有一个有向边;
  2. 将原来 3AC 序列中的所有指令序号改为基本块序号。

4. 后记

这一节课也结束啦,鼠鼠有幸在大二学过编译原理,因此这节课整体上理解起来会比较简单,下节课开始就正式的进入静态分析了,冲。

posted @ 2026-10-06 16:07  乙烯酷  阅读(10)  评论(0)    收藏  举报