Java SootUp 静态分析框架简介
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 的模块——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(.....));
我们还可以使用它来分析内存中作为字符串(String)的源代码:
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
完成的,我们可以将其指向任何可以被视为类路径的内容——包括 JAR 文件或包含类文件的目录。
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);
这里返回一个 Optional<JavaSootClass>
,因为这个类可能在我们的视图中不存在。或者,我们有一个 getClassOrThrow()
方法,它直接返回一个 SootClass
—— JavaSootClass
的超类——但如果这个类在我们的 JavaView
中不存在,就会抛出异常::
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. 访问方法语句图(Method Statement Graph)
除了局部变量之外,我们还可以分析整个方法语句图。这是该方法将执行的每个语句的详细信息:
StmtGraph<?> stmtGraph = methodBody.getStmtGraph();
List<Stmt> stmts = stmtGraph.getStmts();
这为我们提供了一个列表,其中列出了该方法将执行的所有语句,并按执行顺序排列。每个语句都将实现 Stmt
接口,表示该方法可以执行的操作。
例如,我们之前的方法将生成以下内容:

这看起来比我们实际编写的代码要多很多——实际代码只有两行。这是因为这是我们代码的 Jimple 表示。但我们可以将其分解开来,看看究竟发生了什么。
我们从两个 JIdentityStmt
实例开始。它们代表传递给我们方法的值—— this
值和我们之前看到的作为第一个参数的 I1。
接下来,我们有三个 JAssignStmt
实例。它们代表对方法中变量的赋值。在本例中,我们将 I1.toUpperCase()
的结果赋值给 I2
,将 System.out
的值赋值给 $stack3
,并将 “Hello, ” + I2
的结果赋值给 $stack4
。
之后,我们有一个 JInvokeStm
t 实例。它表示在 $stack3
上调用 println()
方法,并将 $stack4
的值传递给它。
最后,我们有一个 JReturnVoidStmt
实例,它代表方法末尾的隐式返回。
这是一个非常简单的方法,没有分支或控制语句,但我们可以清楚地看到,该方法所做的所有操作都在这里体现。对于我们在 Java 应用程序中可以实现的任何功能,情况也是如此。
8. 小结
以上是对 SootUp 的简单介绍。这个库还能实现更多功能。下次需要分析 Java 代码时,不妨试试它。