(五)体验静态分析工具Soot

2022/10/14

简介

项目地址:https://github.com/Sable/soot

官方教程:https://github.com/soot-oss/soot/wiki/Tutorials

soot是java优化框架,提供4种中间代码来分析和转换字节码。

soot提供的分析功能

目前来说,要使用soot有三种途径,分别是命令行、程序内(Maven)以及Eclipse插件(不推荐)

命令行

soot jar包下载地址

可以在上面的地址下载最新的soot jar包,我下载的是4.1.0版本中的sootclasses-trunk-jar-with-dependencies.jar 包,这个包应该自带了soot所需要的所有依赖。下载完成后使用powershell进入jar文件所在的文件夹(我的是D:\Tools\Soot),输入以下命令:

java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main

image-20221222175407448

关于命令行的使用是有很多问题的,我们需要详细了解命令行相关参数的设置

处理单个文件

Soot处理类时,我们必须以下面三种格式传递给它:

我们可以使用Soot将.java或.class文件转换为.jimple文件,然后修改.jimple来优化程序,最终再转换为 .class

官方文档说明:如果我们使用的是jdk8,则需要先编译Java文件,然后才能将它们作为命令行参数传递。

小例子:这里我们先在刚才的jar包所在文件夹下新建一个Test.java编译为Test.class

image-20221222180819992

下面我们尝试将上面得到的class文件作为输入传给soot.

java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main Test

image-20221222184007723

可以看到,这里是报错了,这是因为soot不会默认去当前文件夹下寻找符合条件的文件,而是会去它自身的classpath寻找,而soot的classpath默认情况下是空的,这也就导致soot找不到对应的文件,解决办法是在命令里添加指定位置的代码-cp,-cp .(有一个点符号哦)表示在当前目录寻找。添加classpath相关语句之后再次尝试:

java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -cp . Test

image-20221222184609550

网上查找相关原因后发现是缺少java.lang类,我按照网上的说法在语句里添加了-pp参数,

java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -pp -cp .  Test

image-20221222184839722

得到的结果没有报错,但是也无事发生,这是因为soot需要通过-f属性指定输出的类型,这里我们将输出类型指定为Jimple,查询文档之后得知要添加-f J以确定输出格式,最终的语句如下

java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -f J -pp -cp .  Test

该命令在jar文件所在目录下生成了一个sootOutput文件夹,里面有一个Test.jimple文件,这就是一个最基本的Test.java文件所形成的jimple

image-20221222215148066

CFG

如果是将 Soot 当作简单工具来分析的人,可以直接使用 Soot 自带的工具 soot.tools.CFGViewer 分析类中的每个方法的控制流并生成 DOT 语言描述的控制流图,然后用 graphviz 中的 dot 命令来转换成可视化图形格式如.PNG

这里为了使得CFG更饱满,使用Leetcode第4题(困难)的解析代码:

public class LeetCode {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int[] nums;
        int m = nums1.length;
        int n = nums2.length;
        nums = new int[m + n];
        if (m == 0) {
            if (n % 2 == 0) {
                return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
            } else {

                return nums2[n / 2];
            }
        }
        if (n == 0) {
            if (m % 2 == 0) {
                return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
            } else {
                return nums1[m / 2];
            }
        }

        int count = 0;
        int i = 0, j = 0;
        while (count != (m + n)) {
            if (i == m) {
                while (j != n) {
                    nums[count++] = nums2[j++];
                }
                break;
            }
            if (j == n) {
                while (i != m) {
                    nums[count++] = nums1[i++];
                }
                break;
            }

            if (nums1[i] < nums2[j]) {
                nums[count++] = nums1[i++];
            } else {
                nums[count++] = nums2[j++];
            }
        }

        if (count % 2 == 0) {
            return (nums[count / 2 - 1] + nums[count / 2]) / 2.0;
        } else {
            return nums[count / 2];
        }
    }

}

同样,先编译生成Class文件:

javac LeetCode.java

使用Soot自带的工具类soot.tools.CFGViewer生成控制流图

 java -cp sootclasses-trunk-jar-with-dependencies.jar soot.tools.CFGViewer -pp -cp . LeetCode

image-20221223200012920

运行上述命令则会在SootOutPut目录下,生成dot文件。(注意:需要修改一下dot文件名,自动生成的dot文件名中带有空格,会导致命令行操作失败)

接下来将dot文件转换为图片,需要借助以下graphviz工具 下载地址:http://www.graphviz.org/download/#windows

image-20221223201340450

image-20221223201517703

image-20221223201804915

在Windows命令行下,打开dot文件所在文件夹,输入命令语句

 dot -Tpng  LeetCode.dot -o LeetCode.png

LeetCode

测试

对一系列Java经典的代码片段通过soot框架编译成jimple代码,以观察不同Java程序转化成jimple码之后的变化

For Loop

public class ForLoop3AC {
    public static void main(String[] args) {
        int x =0;
        for (int i = 0; i<10; i++)
            x = x + i;
    }
}
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -f J -pp -cp .  ForLoop3AC

image-20221223205637779

在jimple代码中,开头是一个叫Loop的类继承了java.lang.Object类(默认的所有类的父类),然后是一个初始化的过程,生成默认的构造函数,默认会调用父类的构造函数(即java.lang.Object),接下来就是main函数,在源代码里main函数有一个String[] args的参数,这在jimple代码中就对应了一个声明的参数r0(即r0 := ……这一段),源代码中for循环里面的i在jimple代码中用i1指代,jimpl;e代码中用label来表示程序语句的位置,label1里面的内容就是for循环的条件内容,只要不满足循环条件,用一个goto语句跳转到label2。这里出现了一个bug,那就是源代码中x值的变化在jimple中被“优化”掉了,这大概是soot自身的问题

Do-While Loop

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);
    }
}
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -f J -pp -cp .  DoWhileLoop3AC

image-20221223205913082

这里是给数据对象arr用r0来代替,并对它进行了初始化,接下来还是用label表示程序的位置,将每一次循环的条件都能表示出来。可以注意到,Do-While循环是先进入循环执行对应的语句,再通过if语句进行循环的跳转。

Method call

public class MethodCall3AC {
    public String foo(String para1, String para2){
        return para1 +" "+ para2;
}

    public static void main(String[] args) {
        MethodCall3AC mc = new MethodCall3AC();
        String result = mc.foo("hello","gk0d");
    }
}
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -f J -pp -cp .  MethodCall3AC
public class MethodCall3AC extends java.lang.Object
{

    public void <init>()
    {
        MethodCall3AC r0;

        r0 := @this: MethodCall3AC;

        specialinvoke r0.<java.lang.Object: void <init>()>();

        return;
    }

    public java.lang.String foo(java.lang.String, java.lang.String)
    {
        java.lang.StringBuilder $r0, $r2, $r3, $r5;
        java.lang.String r1, r4, $r6;
        MethodCall3AC r7;

        r7 := @this: MethodCall3AC;

        r1 := @parameter0: java.lang.String;

        r4 := @parameter1: java.lang.String;

        $r0 = new java.lang.StringBuilder;

        specialinvoke $r0.<java.lang.StringBuilder: void <init>()>();

        $r2 = virtualinvoke $r0.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r1);

        $r3 = virtualinvoke $r2.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(" ");

        $r5 = virtualinvoke $r3.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r4);

        $r6 = virtualinvoke $r5.<java.lang.StringBuilder: java.lang.String toString()>();

        return $r6;
    }

    public static void main(java.lang.String[])
    {
        MethodCall3AC $r0;
        java.lang.String[] r3;

        r3 := @parameter0: java.lang.String[];

        $r0 = new MethodCall3AC;

        specialinvoke $r0.<MethodCall3AC: void <init>()>();

        virtualinvoke $r0.<MethodCall3AC: java.lang.String foo(java.lang.String,java.lang.String)>("hello", "gk0d");

        return;
    }
}

首先注意到foo方法中(33行)生成了java.lang.StringBuilder,然而事实上源代码中我们并没有使用StringBuilder,这就是soot根据java语言的语义生成的(用老师的话来说就是一个“语法糖”),相当于是将源代码里面字符串拼接的代码重载了,通过StringBuilder这个对象不断地调用append方法以将字符串进行累加操作(47到52行),最后(53行)将StringBuilder转化为String。接下来再看main方法们可以看到soot将实例化出来的对象mcs用$r0来表示,我们在实例化一个对象时,要自动的去调用对应类的构造函数,如果我们没有显式地定义这个构造函数,会初始化默认构造函数,这就是24行中specialinvoke的作用,接下来调用foo方法就会调用virtualinvoke相关的方法,并将真实值传入进去。

JVM里四种主要方法调用