1. 简介
在本文中,我们将介绍 SootUp 库。SootUp 是一个用于对 JVM 代码进行静态分析的库,可以分析原始源代码或已编译的 JVM 字节码。它是对 Soot 库的彻底重构,目标是更加模块化、可测试、可维护和易用。
2. 依赖
在使用 SootUp 之前,我们需要在构建中引入最新版本(截至撰写时为 1.3.0)。
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.sourcecode</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.bytecode</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.jimple.parser</artifactId>
<version>1.3.0</version>
</dependency>
我们这里有几个不同的依赖,它们的作用如下:
- •
org.soot-uss:sootup.core
是核心库。 - •
org.soot-uss:sootup.java.core
是用于 Java 的核心模块。 - •
org.soot-uss:sootup.java.sourcecode
是用于分析 Java 源代码的模块。 - •
org.soot-uss:sootup.java.bytecode
是用于分析已编译 Java 字节码的模块。 - •
org.soot-uss:sootup.jimple.parser
是用于解析 Jimple(SootUp 用于表示 Java 的中间表示)的模块。
很遗憾,目前没有 BOM 依赖可用,因此我们需要单独管理这些依赖的版本。
3. 什么是 Jimple
SootUp 能够分析多种格式的代码,包括 Java 源代码、已编译的字节码,甚至 JVM 内部的类。
为此,它会将各种输入转换为一种名为 Jimple 的中间表示。
Jimple 的存在是为了以更易分析的方式,表达 Java 源代码或字节码能实现的所有功能。这意味着它在某些方面有意与原始输入不同。
JVM 字节码在访问某些值时采用基于栈的方式,这种方式对运行时非常高效,但对分析来说却很困难。Jimple 则将其转换为完全基于变量的方式,这样既能实现相同的功能,又更易于理解。
相反,Java 源代码虽然也是基于变量的,但其嵌套结构也让分析变得复杂。Jimple 会将其转换为扁平结构,便于工具分析。
Jimple 也可以作为一种我们可以直接读写的语言。例如,下面的 Java 源代码:
public void demoMethod() {
System.out.println("Inside method.");
}
可以被写成如下 Jimple 代码:
public void demoMethod() {
java.io.PrintStream $stack1;
target.exercise1.DemoClass this;
this := @this: target.exercise1.DemoClass;
$stack1 = <java.lang.System: java.io.PrintStream out>;
virtualinvoke $stack1.<java.io.PrintStream: void println(java.lang.String)>("Inside method.");
return;
}
虽然更为冗长,但功能完全一致。SootUp 提供了解析和生成 Jimple 代码的能力,便于我们在需要时以这种格式存储和转换代码。
无论原始代码来源如何,分析时都会被转换为这种结构。此时我们将操作如 SootClass
、SootField
、SootMethod
等类型,它们都直接对应这种中间表示。
4. 代码分析
在使用 SootUp 做任何事情之前,首先需要分析一些代码。这需要创建合适的 AnalysisInputLocation
实例,并基于它构建一个 JavaView
。
具体创建哪种 AnalysisInputLocation
,取决于我们要分析的代码来源。
最简单但实际用处较少的方式,是分析 JVM 自身的类。可以通过 JrtFileSystemAnalysisInputLocation
类实现:
AnalysisInputLocation inputLocation = new JrtFileSystemAnalysisInputLocation();
更常用的是分析源码文件,可以用 OTFCompileAnalysisInputLocation
:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(
Path.of("src/test/java/com/baeldung/sootup/AnalyzeUnitTest.java"));
它还支持一次分析多个源码文件:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(List.of(.....));
也可以分析内存中的源码字符串:
Path javaFile = Path.of("src/test/java/com/baeldung/sootup/AnalyzeUnitTest.java");
String javaContents = Files.readString(javaFile);
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation("AnalyzeUnitTest.java", javaContents);
最后,还可以分析已编译的字节码。通过 JavaClassPathAnalysisInputLocation
,可指向任何 classpath(包括 JAR 包或 class 文件目录):
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("target/classes");
此外还有其他标准方式,如直接解析 Jimple 表示、读取 Android APK 文件等。
拿到 AnalysisInputLocation
实例后,可以基于它创建 JavaView
:
JavaView view = new JavaView(inputLocation);
这样就能访问输入中的所有类型信息。
5. 访问类
在我们分析完代码并基于它构建了一个 JavaView
实例后,就可以开始访问代码的详细信息了,这首先从访问类开始。
如果我们已经知道要查找的具体类,可以直接通过全限定类名访问。SootUp 使用多种 Signature
类来描述我们想要访问的元素。在这里,我们需要一个 ClassType
实例。幸运的是,可以通过 SootUp 提供的 IdentifierFactory
,利用全限定类名轻松生成:
IdentifierFactory identifierFactory = view.getIdentifierFactory();
ClassType javaClass = identifierFactory.getClassType("com.baeldung.sootup.ClassUnitTest");
构建好 ClassType
实例后,就可以用它来访问该类的详细信息:
Optional<JavaSootClass> sootClass = view.getClass(javaClass);
也可以直接获取类,不存在时抛出异常:
SootClass sootClass = view.getClassOrThrow(javaClass);
拿到 SootClass
实例后,可以用它来检查类的各种属性,例如可见性、是否为具体类或抽象类等:
assertTrue(classUnitTest.isPublic());
assertTrue(classUnitTest.isConcrete());
assertFalse(classUnitTest.isFinal());
assertFalse(classUnitTest.isEnum());
还可以遍历已解析的代码,比如访问父类或接口:
Optional<? extends ClassType> superclass = sootClass.getSuperclass();
Set<? extends ClassType> interfaces = sootClass.getInterfaces();
注意,这些方法返回的是 ClassType
而不是 SootClass
实例,因为无法保证这些类的定义一定在当前视图中,仅能获取它们的名称。
6. 访问字段和方法
除了类本身,还可以访问类的内容,比如字段和方法。
如果已经有了 SootClass
实例,可以直接查询其字段和方法:
Set<? extends SootField> fields = sootClass.getFields();
Set<? extends SootMethod> methods = sootClass.getMethods();
与类之间的导航不同,这里可以安全地返回字段或方法的完整表示,因为它们一定在当前视图中。
如果已知具体字段名,可以直接访问字段:
Optional<? extends SootField> field = sootClass.getField("aField");
访问方法稍微复杂一些,需要方法名和参数类型:
Optional<? extends SootMethod> method = sootClass.getMethod("someMethod", List.of());
如果方法有参数,需要通过 IdentifierFactory
提供参数类型的 Type
实例:
Optional<? extends SootMethod> method = sootClass.getMethod("anotherMethod",
List.of(identifierFactory.getClassType("java.lang.String")));
这样可以在有重载方法时获取正确的实例。还可以列出所有同名重载方法:
Set<? extends SootMethod> method = sootClass.getMethodsByName("someMethod");
同样,拿到 SootMethod
或 SootField
实例后,可以进一步检查其属性:
assertTrue(sootMethod.isPrivate());
assertFalse(sootMethod.isStatic());
7. 分析方法体
拿到 SootMethod
实例后,可以分析方法体本身,包括方法签名、局部变量和调用图。
首先需要获取方法体:
Body methodBody = sootMethod.getBody();
有了方法体后,就可以访问方法体的所有细节。
7.1 访问局部变量
首先可以访问方法内所有可用的局部变量:
Set<Local> methodLocals = methodBody.getLocals();
这会返回方法内所有可访问的变量。需要注意,这实际上是 Jimple 表示中的变量列表,因此会包含一些解析过程中生成的额外变量,且变量名可能与原始代码不同。
例如,下面的方法有 5 个局部变量:
private void someMethod(String name) {
var capitals = name.toUpperCase();
System.out.println("Hello, " + capitals);
}
这些变量包括:
- •
this
- •
I1
—— 方法参数 - •
I2
—— 变量“capitals” - •
$stack3
—— 指向System.out
的局部变量 - •
$stack4
—— 表示“Hello, ” + capitals 的局部变量
其中 $stack3
和 $stack4
是 Jimple 生成的,并不直接出现在原始代码中。
7.2 访问方法语句图
除了局部变量,还可以分析整个方法的语句图,即方法将执行的每条语句的详细信息:
StmtGraph<?> stmtGraph = methodBody.getStmtGraph();
List<Stmt> stmts = stmtGraph.getStmts();
这样可以获得方法将要执行的所有语句,顺序与实际执行一致。每个语句都实现了 Stmt
接口,代表方法可以执行的操作。
例如,前面的方法会生成如下结构:

看起来比我们实际写的代码多很多——实际上只有两行。这是因为这里展示的是 Jimple 的表示。我们可以逐步拆解其含义:
首先有两个 JIdentityStmt
,分别代表传入方法的 this
和 I1
(即第一个参数)。
接下来有三个 JAssignStmt
,分别表示对变量的赋值:将 I1.toUpperCase()
的结果赋给 I2
,将 System.out
赋给 $stack3
,将“Hello, ” + I2
的结果赋给 $stack4
。
之后是一个 JInvokeStmt
,表示在 $stack3
上调用 println()
方法,并传入 $stack4
。
最后是一个 JReturnVoidStmt
,代表方法末尾的隐式返回。
这是一个没有分支和控制语句的简单方法,但可以清楚看到方法执行的每一步。对于 Java 应用中的任何操作,Jimple 都能完整表示。
8. 总结
本文简要介绍了 SootUp。这个库还有很多强大的功能等待探索。下次需要分析 Java 代码时,不妨试试 SootUp 吧!
没有回复内容