这是对 SootUp 的简要介绍,我们可以使用这个库分析任何 Java 代码。
SootUp 是一个使用原始源代码或编译后的 JVM 字节码对 JVM 代码执行静态分析的库。它是对Soot 库的全面改造,旨在更加模块化、更易于测试、更易于维护和更易于使用。
在使用 SootUp 之前,我们需要在我们的构建中包含最新版本,撰写本文时为1.3.0 。
<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依赖项,因此我们需要单独管理这些依赖项的每个版本。
什么是Jimple?
SootUp 可以分析多种不同格式的代码 - 包括 Java 源代码、编译的字节码,甚至是 JVM 内部的类。
为此,它将各种输入转换为称为 Jimple 的中间表示。
Jimple 的存在是为了表示可以用 Java 源代码或字节码完成的所有事情,但以一种更易于分析的方式。这意味着它在某些方面有意与这两种可能的输入不同。
JVM 字节码在访问某些值的方式上是基于堆栈的。这对于运行时来说非常高效,但对于分析来说则困难得多。代码的 Jimple 表示将其转换为完全基于变量。这可以产生完全相同的功能,同时更容易理解。
相反,Java 源代码也是基于变量的,但其嵌套结构也使其更难分析。这对开发人员来说更容易处理,但对软件工具来说更难分析。Jimple 对此的表示将其转换为扁平结构。
Jimple 也作为一种语言存在,我们可以自己读写代码。例如,Java 源代码:
public void demoMethod() { |
也可以写成 Jimple,如下所示:
public void demoMethod() { |
这更冗长,但我们可以看到它具有相同的功能。如果我们需要以这种格式存储和转换它,SootUp 提供了直接解析和生成此 Jimple 代码的功能。
当我们分析代码时,无论原始来源是什么,它都会被转换成这种结构以供我们使用。然后我们将使用与此表示直接相关的类型,例如SootClass、SootField、SootMethod等。
分析代码
在我们使用 SootUp 做任何事情之前,我们需要分析一些代码。这是通过创建AnalysisInputLocation的适当实例并围绕它构建JavaView来完成的。
我们创建的AnalysisInputLocation的具体类型取决于我们想要分析的代码的来源。
最简单但可能最没用的方法就是能够分析 JVM 本身的类。
我们可以使用JrtFileSystemAnalysisInputLocation类来实现这一点:
AnalysisInputLocation inputLocation = new JrtFileSystemAnalysisInputLocation();
更有用的是,我们可以使用OTFCompileAnalysisInputLocation分析源文件:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation( |
这也有一个备用构造函数,用于一次性分析整个源文件列表:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(List.of(.....)); |
我们还可以用它来分析内存中以字符串形式保存的源代码:
Path javaFile = Path.of("src/test/java/com/baeldung/sootup/AnalyzeUnitTest.java"); |
最后,我们可以分析已经编译好的字节码。这是使用JavaClassPathAnalysisInputLocation完成的,我们可以将其指向任何可以被视为类路径的内容 - 包括 JAR 文件或包含类文件的目录。
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("target/classes"); |
还有其他几种标准方法可以访问我们想要分析的代码,包括直接解析 Jimple 表示,或读取 Android APK 文件。
一旦我们获得了AnalysisInputLocation实例,我们就可以围绕它创建一个JavaView :
JavaView view = new JavaView(inputLocation); |
这样我们就可以访问输入中存在的所有类型。
访问类
一旦我们分析了代码并围绕它构建了JavaView实例,我们就可以开始访问有关代码的详细信息。这从访问类开始。
如果我们知道我们想要的确切类,我们可以使用完全限定的类名直接访问它。SootUp 使用各种Signature类来描述我们想要访问的元素。在这种情况下,我们需要一个ClassType实例。幸运的是,我们可以使用SootUp 提供给我们的IdentifierFactory轻松使用完全限定的类名生成其中一个:
IdentifierFactory identifierFactory = view.getIdentifierFactory(); |
一旦我们构建了ClassType实例,我们就可以使用它来访问此类的详细信息:
Optional<JavaSootClass> sootClass = view.getClass(javaClass); |
这将返回一个Optional
SootClass sootClass = view.getClassOrThrow(javaClass); |
一旦我们得到了SootClass实例,我们就可以用它来检查类的细节。这让我们可以确定类本身的细节,比如它的可见性,它是具体的还是抽象的,等等:
assertTrue(classUnitTest.isPublic()); |
我们还可以导航已解析的代码,例如,通过访问我们类的超类或接口:
Optional<? extends ClassType> superclass = sootClass.getSuperclass(); |
注意,这些返回的是ClassType而不是SootClass实例。这是因为无法保证实际的类定义是我们视图的一部分,只是它们的名称。
访问字段和方法
除了类本身之外,我们还可以访问类的内容,例如字段和方法。
如果我们已经有一个可用的SootClass,那么我们可以直接查询它来找到字段和方法:
Set<? extends SootField> fields = sootClass.getFields(); |
与我们从一个类导航到另一个类不同,这可以安全地返回字段或方法的整个表示,因为它们保证在我们的视图中。
如果我们确切地知道我们想要什么,我们也可以直接访问它。例如,要访问一个字段,我们只需要知道它的名称:
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", |
这样,当我们有重载方法时,我们就可以获取正确的实例。我们还可以列出所有具有相同名称的重载方法:
Set<? extends SootMethod> method = sootClass.getMethodsByName("someMethod"); |
和以前一样,一旦我们获得了SootMethod或SootField实例,我们就可以使用它来检查详细信息:
assertTrue(sootMethod.isPrivate()); |
分析方法主体
一旦我们得到了SootMethod实例,我们就可以用它来分析方法体本身。这意味着方法签名、方法中的局部变量以及调用图本身。
在执行任何操作之前,我们需要访问方法主体本身:
Body methodBody = sootMethod.getBody(); |
使用这个,我们现在可以访问方法主体的所有细节。
1. 访问局部变量
我们可以做的第一件事是访问方法中可用的任何局部变量:
Set
这使我们能够访问方法中可访问的每个变量。此列表可能不是预期的,它实际上是来自方法的 Jimple 表示的变量列表,因此将包含解析过程中的一些额外条目,并且可能没有原始变量名称。
例如,以下方法有 5 个局部变量:
private void someMethod(String name) { var capitals = name.toUpperCase(); |
这些都是:
- this.
- I1——方法参数。
- I2——变量“capitals”。
- $stack3 – 指向System.out 的局部变量。
- $stack4 – 一个代表“Hello, ”+大写字母的局部变量。
$ stack3和$stack4局部变量由 Jimple 表示生成,并不直接存在于原始代码中。
2. 访问方法语句图
除了局部变量,我们还可以分析整个方法语句图。 这是方法将执行的每条语句的详细信息:
StmtGraph<?> stmtGraph = methodBody.getStmtGraph(); |
这将为我们提供方法将执行的每条语句的列表,并按照执行顺序排列。 每个语句都将实现 Stmt 接口,代表该方法能做的事情。
这似乎比我们实际编写的代码要多得多,因为我们实际编写的代码只有两行。 这是因为这是我们代码的 Jimple 表示法。 但我们可以将其分解,看看到底发生了什么。
我们从两个 JIdentityStmt 实例开始。 它们代表传入我们方法的值:
- this 值
- 和我们之前看到的作为第一个参数的 I1。
接下来,我们有三个 JAssignStmt 实例。 它们代表对方法中变量的赋值。 在本例中,我们将 I1.toUpperCase() 的结果赋值给 I2,System.out 的值赋值给 $stack3,"Hello, " + I2 的结果赋值给 $stack4。 这表示在 $stack3 上调用 println() 方法,并将 $stack4 的值传递给它。
最后,我们有一个 JReturnVoidStmt 实例,它表示方法结束时的隐式返回。 这是一个非常简单的方法,没有分支或控制语句,但我们可以清楚地看到,该方法所做的一切都在这里得到了体现。 我们在 Java 应用程序中可以实现的任何功能也是如此。