目录
前言
一、Soot 的下载和安装
1.1 在命令行中使用 Soot
1.2 在项目中使用 Soot
二、使用 Soot 生成中间代码 (IR)
三、使用 Soot 进行 Java 类插桩
四、使用 Soot 生成控制流图 (CFG)
4.1 按语句划分的控制流程图
4.2 按基本块划分的控制流程图
五、Graphviz 工具的安装和使用
5.1 Graphviz 工具的安装
5.2 Graphviz 工具的使用
六、其他 CFG 图绘制方法
6.1 使用工具
6.2 使用在线网站
前言
Soot 是 McGill 大学的 Sable 研究小组自 1996 年开始开发的 Java 字节码分析工具,它提供了多种字节码分析和变换功能。通过它可以进行过程内和过程间的分析优化,以及程序流图的生成;还能通过图形化的方式输出,让用户对程序有个直观的了解。尤其是做单元测试的时候,可以很方便的通过这个生成控制流图然后进行测试用例的覆盖,显著提高效率。Soot 项目已经不再继续维护,其最高支持到 Java 9 版本。如果要在更新的项目中使用,请配置项目组最新开发和维护的 SootUp 项目(https://soot-oss.github.io/SootUp/)。
一、Soot 的下载和安装
1.1 在命令行中使用 Soot
Soot 项目在 Github 上的地址为:https://github.com/Sable/soot。目前来说,要使用 Soot 有三种途径,分别是命令行、添加到项目以及 Eclipse 插件(不推荐)。
可以选择使用 Github 上的 Release 或者 Git 克隆项目到本地,然后使用 Maven 或者 IDEA -maven 构建项目。
可以在这里(https://repo1.maven.org/maven2/org/soot-oss/soot/)下载最新的 soot jar 包,我下载的是 4.4.1 版本中的 sootclasses-trunk-jar-with-dependencies.jar 包。
这个包应该自带了 soot 所需要的所有依赖。下载完成后使用命令提示符进入 jar 文件所在的文件夹(我的是 D:\programing\sootTest),输入以下命令:
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main
输出如下图:
在输入 -h 命令可以回显帮助信息:
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -h
1.2 在项目中使用 Soot
从 Github 上 Soot 项目的简介可知,Soot 一般配合 Maven 来进行部署,相关的 POM 文件依赖添加语句如下(在 <dependencies> 下追加 <dependency>):
<dependencies><dependency><groupId>ca.mcgill.sable</groupId><artifactId>soot</artifactId><version>4.4.1</version></dependency>
</dependencies>
二、使用 Soot 生成中间代码 (IR)
Soot 是 Java 优化框架,提供 4 种中间代码来分析和转换字节码。
Baf:精简的字节码表示,操作简单
Jimple:适用于优化的 3-Address 中间表示
Shimple:Jimple 的 SSA 变体
Grimple:适用于反编译和代码检查的 Jimple 汇总版本。
由于在命令行中调用 Soot 是最为简单的模式,所以后文均以在命令行中使用 Soot 为基准。
我的目标是将 Java 源文件转化为 Jimple 以发现程序编译中的问题和规律。因此本文的重点就在这里,我先在 soot.jar 所在的文件夹下新建了一个 Java 源文件 HelloWorld.java 如下图所示:
// HelloWorld.java
public class HelloWorld {public static void main(String[] args) {System.out.println("hello");}
}
因为我使用的 Java 版本是 JDK1.8(Java 8),根据 Soot 提示,默认输入是 class 文件,所以我先用 javac 命令将 HelloWorld.java 编译为 HelloWorld.class。
javac HelloWorld.java
下面我们尝试将上面得到的 class 文件作为输入传给 soot 。
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -pp -cp . HelloWorld
得到的结果没有报错,但是也无事发生,这是因为 soot 需要通过 -f 属性指定输出的类型,这里我们将输出类型指定为 Jimple,查询文档之后得知要添加 -f J 以确定输出格式,最终的语句如下:
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.Main -f J -pp -cp . HelloWorld
该命令在 soot 工具所在目录下生成了一个 sootOutput 文件夹,里面有一个 HelloWorld.jimple 文件,使用 Idea 编辑器打开这个文件,得到的内容如下,这就是一个最基本的 HelloWorld.java文件所形成的 Jimple 码。
public class HelloWorld extends java.lang.Object
{public void <init>(){HelloWorld r0;r0 := @this;specialinvoke r0.<init>();return;}public static void main(java.lang.String[]){java.io.PrintStream $r0;java.lang.String[] r1;r1 := @parameter0;$r0 = java.lang.System.out;$r0.println("hello");return;}
}
上文使用 sootclasses-trunk-jar-with-dependencies.jar 也可以改用 sootclasses-trunk.jar (只不过用这个 sootclasses-trunk.jar 必须配置好 classpath 环境变量),但是我不建议这么做。
三、使用 Soot 进行 Java 类插桩
(该小节摘录自官方教程的机器翻译内容)
我将首先展示一个例子。从这个示例中,您可以了解到如何使用 Soot 修改类文件。然后,我将解释在 Soot 中类、方法和语句的内部表示。
在本教程开始前,建议你先掌握 JVM 指令的相关知识。你还应该学习如何使用 Soot 添加局部变量和字段等。这里的类插桩只修改字节码文件,通过注入性能分析代码,来记录程序运行时的重要信息。
任务:计算在运行一个微小的基准测试 TestInvoke.java 时执行了多少条 InvokeStatic 指令。
class TestInvoke {private static int calls=0;public static void main(String[] args) {for (int i=0; i<10; i++) {foo();}System.out.println("我使用了 " + calls + " 个静态调用");}private static void foo(){calls++;bar();}private static void bar(){calls++;}
}
为了实现计数器,我编写了一个名为 MyCounter 的辅助类:
/* 计数器类 */
public class MyCounter {/* 计数器变量存储,初始化为0 */private static int c = 0;/*** 将计数器增加多少(howmany)* @param howmany :计数器的增量。*/public static synchronized void increase(int howmany) {c += howmany;}/*** 报告计数器内容。*/public static synchronized void report() {System.err.println("计数 : " + c);}
}
现在,我要创建一个包装器类,在 Soot 中添加一个阶段,用于插入分析指令,然后调用 Soot.Main.main()。该驱动程序类的 main 方法将名为" jtp.instrumenter "的转换阶段添加到 Soot 的" jtp "包中。
PackManager 是对 Soot 注册的不同阶段的类的包装。当 MainDriver 调用 soot.Main.main 时, Soot 将从 PackManager 得知注册了一个新阶段,并且标志着一个新阶段的 internalTransform 方法会被 Soot 调用。
MainDriver.java :
/* 用法: java MainDriver [soot-options] appClass*//* 导入必要的 soot 包 */import soot.*;public class MainDriver {public static void main(String[] args) {/* 检查参数 */if (args.length == 0) {System.err.println("用法: java MainDriver [options] classname");System.exit(0);}/* 通过调用 Pack.add 方法添加一个阶段(phase)到 transformer 包 */Pack jtp = PackManager.v().getPack("jtp");jtp.add(new Transform("jtp.instrumenter",new InvokeStaticInstrumenter()));/* 把控制权交给 Soot 来处理所有选项,* InvokeStaticInstrumenter.internalTransform 将被调用。*/soot.Main.main(args);}}
instrumenter(插桩)的实际实现扩展了一个抽象类 BodyTransformer。它实现了 internalTransform 方法,该方法采用方法体(指令)和一些选项。主要操作发生在该方法中。根据您的命令行选项,Soot 构建一个类列表(这也意味着方法列表),并通过传入每个方法的主体来调用InvokeStaticInstrumenter.internalTransform。
InvokeStaticInstrumenter.java :
/** InvokeStaticInstrumenter 在程序中的 INVOKESTATIC * 字节码之前插入计数指令。插桩后的程序将* 报告在一次运行中发生了多少静态调用。* * 目标:* 在静态调用指令之前插入计数器指令。* 在程序正常退出点之前报告计数器数值。* * 方法:* 1. 创建一个计数器类,它有一个计数器字段,和* 一个报告方法。* 2. 获取每个方法体,遍历每个指令,并且* 在 INVOKESTATIC 之前插入计数指令。* 3. 调用计数器类的报告生成方法。* * 从这个例子中可以学到的东西:* 1. 如何使用 Soot来测试 Java 类。* 2. 如何在类中插入分析指令。*//* InvokeStaticInstrumenter 扩展了抽象类 BodyTransformer,* 并实现 internalTransform 方法。*/import soot.*;import soot.jimple.*;import soot.util.*;import java.util.*;public class InvokeStaticInstrumenter extends BodyTransformer{/* 一些内部字段 */static SootClass counterClass;static SootMethod increaseCounter, reportCounter;static {counterClass = Scene.v().loadClassAndSupport("MyCounter");increaseCounter = counterClass.getMethod("void increase(int)");reportCounter = counterClass.getMethod("void report()");}/* InternalTransform 遍历方法体并将计数器指令插入* 在 INVOKESTATIC 指令之前。*/protected void internalTransform(Body body, String phase, Map options) {// 主体的方法SootMethod method = body.getMethod();// 调试System.out.println("instrumenting method : " + method.getSignature());// 将主体的单元作为一个链(单元链)Chain units = body.getUnits();// 获取单元的快照迭代器,因为我们将在// 迭代链时对其进行变异。//Iterator stmtIt = units.snapshotIterator();// 用于迭代每个语句的典型 while 循环while (stmtIt.hasNext()) {// 回溯一个声明Stmt stmt = (Stmt)stmtIt.next();// 语句有很多种类型,这里只是// 对包含 InvokeStatic 的语句感兴趣// 注意:有两种语句可能包含// invoke 表达式:InvokeStmt 和 AssignStmtif (!stmt.containsInvokeExpr()) {continue;}// 取出 invoke (调用)表达式InvokeExpr expr = (InvokeExpr)stmt.getInvokeExpr();// 现在跳过非静态调用if (! (expr instanceof StaticInvokeExpr)) {continue;}// 现在我们到达真正的指令// 调用 Chain.insertBefore() 在其之前插入指令//// 1. 首先,新建一个 invoke 表达式InvokeExpr incExpr= Jimple.v().newStaticInvokeExpr(increaseCounter.makeRef(),IntConstant.v(1));// 2. 然后,构造一个 invoke 语句Stmt incStmt = Jimple.v().newInvokeStmt(incExpr);// 3. 向链中插入新语句// (我们正在对单元链实施变异操作)。units.insertBefore(incStmt, stmt);}// 不要忘记插入报告计数器的指令// 这只发生在 main 方法的退出点之前。// 1. 通过检查签名来检查这是否是 main 方法String signature = method.getSubSignature();boolean isMain = signature.equals("void main(java.lang.String[])");// 2. 重新迭代主体以查找 return 语句if (isMain) {stmtIt = units.snapshotIterator();while (stmtIt.hasNext()) {Stmt stmt = (Stmt)stmtIt.next();// 检查指令是否是带值/不带值的 returnif ((stmt instanceof ReturnStmt)|| (stmt instanceof ReturnVoidStmt)) {// 1. 构造 MyCounter.report() 的 invoke 表达式InvokeExpr reportExpr= Jimple.v().newStaticInvokeExpr(reportCounter.makeRef());// 2. 然后,构造一个 invoke 语句Stmt reportStmt = Jimple.v().newInvokeStmt(reportExpr);// 3. 向链中插入新语句// (我们正在对单元链实施变异操作)。units.insertBefore(reportStmt, stmt);}}}}}
现在,在正式插桩(instrumentation)之前,需要测试一下 instrumenter :
[cochin] [621tutorial] java TestInvoke
我使用了 20 个静态调用
运行该 instrumenter :
[cochin] [621tutorial] java MainDriver TestInvoke
Soot started on Tue Feb 12 21:22:59 EST 2002
Transforming TestInvoke... instrumenting method : <TestInvoke: void <init>()>
instrumenting method : <TestInvoke: void main(java.lang.String[])>
instrumenting method : <TestInvoke: void foo()>
instrumenting method : <TestInvoke: void bar()>
instrumenting method : <TestInvoke: void <clinit>()>
Soot finished on Tue Feb 12 21:23:02 EST 2002
Soot has run for 0 min. 3 sec.
这会将转换后的 TestInvoke.class 放入 ./sootOutput 中。运行这个新转换的基准测试(注意你现在需要在你的类路径上放置 MyCounter.class 文件):
[cochin] [621tutorial] cd sootOutput
[cochin] [621tutorial] java TestInvoke
Exception in thread "main" java.lang.NoClassDefFoundError: MyCounter
at TestInvoke.main(TestInvoke.java)
[cochin] [621tutorial] cp ../MyCounter.class .
[cochin] [621tutorial] java TestInvoke
我使用了 20 个静态调用
计数 : 20
比较插桩前后的JIMPLE代码:
插桩前:
class TestInvoke extends java.lang.Object{......public static void main(java.lang.String[] ){......label0:staticinvoke <TestInvoke: void foo()>();i0 = i0 + 1;......return;}private static void foo(){......staticinvoke <TestInvoke: void bar()>();return;}private static void bar(){......return;}......}
插桩后:
class TestInvoke extends java.lang.Object{......// 这是主方法public static void main(java.lang.String[] ){......label0:// 这里插入了计数器静态方法staticinvoke <MyCounter: void increase(int)>(1);staticinvoke <TestInvoke: void foo()>();i0 = i0 + 1;......// 这里插入了报告生成器静态方法staticinvoke <MyCounter: void report()>();return;}private static void foo(){......// 这里插入了计数器静态方法staticinvoke <MyCounter: void increase(int)>(1);staticinvoke <TestInvoke: void bar()>();return;}private static void bar(){......return;}......}
我们看到,在每个 staticinvoke 指令之前添加了对 MyCounter.increase(1) 的方法调用,并且在 main 方法的返回指令之前插入了对 MyCounter.report() 的调用。
关于这一部分更多的讲解可以看以下几篇文章:
1. Soot 知识点整理 | fynch3r 的小窝;
2. Soot 使用记录 | Jckling's Blog;
3. 利用 Soot 对 APK 插桩实践 - 博客园;
4. Soot 生成控制流图 - 博客园;
四、使用 Soot 生成控制流图 (CFG)
Soot 利用 AST (抽象语法树)生成程序的控制流程关系。soot.tools.CFGViewer 分析类中的每个方法的控制流并生成 DOT 语言描述的控制流图。我们使用 Graphviz 工具中的 dot 命令将其转换成可视化图形。
任务:使用 soot.tools.CFGViewer 生成 Triangle.class 的控制流图
首先使用 javac 命令编译此源代码文件:
// Triangle.class
package Soot;public class Test {private double num = 5.0;public double cal(int num, String type){double temp=0;if(type == "sum"){for(int i = 0; i <= num; i++){temp =temp + i;}}else if(type == "average"){for(int i = 0; i <= num; i++){temp = temp + i;}temp = temp / (num -1);}else{System.out.println("Please enter the right type(sum or average)");}return temp;}
}
运行 “sootclasses-trunk-jar-with-dependencies.jar” 时,输入文件 Triangle.class 文件的位置与sootclasses-trunk-jar-with-dependencies.jar 在同一目录下。
soot 生成控制流程关系时,有两种增量方式:语句划分和按基本块划分。
4.1 按语句划分的控制流程图
使用下面的命令执行生成按照语句划分的控制流程关系。
# 命令1:按语句划分
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.tools.CFGViewer -cp . -pp Triangle
其中:
(1)“soot.tools.CFGViewer” 表示使用 soot 的控制流图绘制功能
(2)“-cp .” 表示 soot 指明类路径,“.” 表示类路径为当前路径。
(3)Soot 还必须指明 java.lang.Object,可以用 “-pp”,也可以添加 “rj.jar”。
(4)Triangle 指代 Triangle.class,Soot 默认输入 class 文件;当然,你可以用 "–src-prec" 指定输入文件类型。
4.2 按基本块划分的控制流程图
# 命令2:按基本块划分
java -cp sootclasses-trunk-jar-with-dependencies.jar soot.tools.CFGViewer -cp . -pp --graph=BriefBlockGraph Triangle
其中:“–graph=BriefBlockGraph” 表示按基本块划分。
使用基本块划分将使得最终生成的控制流程图分支更少,调理更清晰。但可能忽略一些块内细节的展示。我们在大多数情况下使用基本块划分模式。
五、Graphviz 工具的安装和使用
Graphviz 是开源图形可视化软件。图形可视化是一种将结构信息表示为抽象图形和网络图的方法。它在网络、生物信息学、软件工程、数据库和网页设计、机器学习以及其他技术领域的可视化界面中具有重要应用。
5.1 Graphviz 工具的安装
它的官网服务器是在国外,所以国内浏览会比较慢。
官网链接:Graphviz。
关于安装教程方面,我暂时也没时间截图重新整理一份。索性在 CSDN 找到两篇写的很好的教程,可以结合着看。
1. https://blog.csdn.net/qq_42294351/article/details/119754109
2. https://blog.csdn.net/qq_42257666/article/details/121688656
5.2 Graphviz 工具的使用
Graphviz 画图只需两步:
- 创建 .dot 文本文件, 在其中使用 DOT 语言描述图形;这里我们已经有了由 soot 生成的 DOT 文件,可以直接使用 Graphviz 生成可视化图像了。
- 使用命令将 dot 文本内容转换为图片:
dot Triangle.dot -T png -o Triangle.png
# -T指定输出类型, 可以指定jpg, gif, svg等
# -o 指定输出文件名, 不指定则输出到标准输出上# 或者:
# dot -T png -o Triangle.png Triangle.dot
# dot -Tpng -o Triangle.png Triangle.dot
按语句划分的(太长了只能截图上传):
按基本块划分的:
经过人工优化代码以及修改 DOT 后生成的简化版:
【注】:因为一些原因,我不能展示完整的图像给大家。
关于 Graphviz 工具和它的 DOT 语法的使用细节可以看这篇文章:
- Graphviz 绘图 — Graphviz 笔记 (graphviz-note.readthedocs.io);
- DOT 用法 — Graphviz 笔记 (graphviz-note.readthedocs.io);
六、其他 CFG 图绘制方法
6.1 使用工具
Visustin 工具是一个老牌的多语言流程图生成工具。这款软件是商业化的,虽然他支持多种自然语言,但演示版仅允许免费使用 30 天,并且功能受到限制。
Visustin 演示版工具的图标:
支持的自然语言列表:
样例程序:
Visustin 工具生成的控制流图虽然相对准确,但是很不美观。我们可以截图保存并使用 Viso 工具重新画一个 CFG 图。
6.2 使用在线网站
有很多网站提供简单源代码的 CFG 图生成和编辑功能。比如 Code2Flow 网站(code2flow - online interactive code to flowchart converter)。虽然这些网站生成控制流图较为精美且操作简单,但是大多数都不完全免费。
本文发布于:2024.03.25,更新于:2024.03.25.