SootUp简介

2025/04/25

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/cn/tuyucheng/taketoday/sootup/AnalyzeUnitTest.java"));

这也有一个替代构造函数,用于一次性分析整个源文件列表:

AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(List.of(.....));

我们还可以使用它来分析内存中作为字符串的源代码:

Path javaFile = Path.of("src/test/java/cn/tuyucheng/taketoday/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("cn.tuyucheng.taketoday.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 访问方法语句图

除了局部变量之外,我们还可以分析整个方法语句图,这是该方法将执行的每个语句的详细信息

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应用程序中可以实现的任何功能,情况也是如此。

8. 总结

以上是对SootUp的简要介绍,这个库还能发挥更多功能。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章