本文更新于 2024 年 12 月 02 日,不保证对该时间往后的软件版本生效。
如有谬误,敬请指出,十分感谢!
SootUp 快速入门体验(一些简单的使用方式)
1 安装(导入)
作为 Java 项目的依赖库,可根据自身习惯或项目使用的构建工具,选择 Maven 或 Gradle 其中一种导入方式。
依赖库的来源也有不同选择,可使用官方在 Maven Central 的发布版本,也可使用 Jitpack.io 直接对 GitHub 项目打包,甚至自行克隆项目到本地打包安装(想使用最新的开发分支时一般这么做)。
SootUp 的使用仍较少,使用了国内其它 Maven 镜像时,不排除存在缺失该库的情况,此处仅以 Maven Central 为例。
和 Soot 就一个包不同,SootUp 分为了 8 个不同的依赖包,在具体项目中可根据需求缩减。在这里就暂且……
务必注意,以下说明均基于 SootUp 1.3.0,该库的实现、使用在后续完全可能发生变动。
1.1 使用 Maven
1.1.1 从 Maven Central 获取
在项目的 pom.xml 中添加以下依赖:
<dependencies>
<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>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.callgraph</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.analysis</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.qilin</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
添加完成后重新加载一下项目即可下载相关依赖。
1.1.2 从 Jitpack 获取
从 Jitpack 获取需要添加该仓库,在项目的 pom.xml 中添加以下配置:
<project>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.soot-oss.SootUp</groupId>
<artifactId>sootup</artifactId>
<version>develop-SNAPSHOT</version>
</dependency>
</project>
添加完成后重新加载一下项目即可下载相关依赖。
1.1.3 使用 Maven 自行打包
自行打包需要本地 Java 8+ 和 Maven(支持 Java 8+ 即可)环境,在你用来存放项目的目录执行以下命令:
git clone https://github.com/soot-oss/SootUp.git
cd SootUp
mvn clean install
命令执行成功后,依赖应已安装到本地 Maven 仓库,根据构建过程中的包名按 1.1.1 的方式导入即可。
1.2 使用 Gradle
1.2.1 从 Maven Central 获取
Gradle 支持 Kotlin、Groovy 两种 DSL 来配置脚本,下面仅以 Kotlin 为例,在项目的 build.gradle.kts 中添加以下依赖:
dependencies {
implementation("org.soot-oss:sootup.core:1.3.0")
implementation("org.soot-oss:sootup.java.core:1.3.0")
implementation("org.soot-oss:sootup.java.sourcecode:1.3.0")
implementation("org.soot-oss:sootup.java.bytecode:1.3.0")
implementation("org.soot-oss:sootup.jimple.parser:1.3.0")
implementation("org.soot-oss:sootup.callgraph:1.3.0")
implementation("org.soot-oss:sootup.analysis:1.3.0")
implementation("org.soot-oss:sootup.qilin:1.3.0")
}
官方文档使用的是 compile
来导入,在较新的 Gradle 版本中该形式已被弃用,转为 implementation
。
添加完成后重新加载一下项目即可下载相关依赖。
1.2.2 从 Jitpack 获取
从 Jitpack 获取需要添加该仓库,在项目的 build.gradle.kts 中添加以下配置:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url("https://jitpack.io}") }
}
}
dependencies {
implementation("com.github.soot-oss.SootUp:sootup:develop-SNAPSHOT")
}
添加完成后重新加载一下项目即可下载相关依赖。
1.2.3 使用 Gradle 自行打包
暂无。截至目前,官方项目代码中仅提供了 pom.xml,也就是 Maven 的构建方式。
2 基础知识
在开始使用 SootUp 之前,先了解以下核心数据结构会有所帮助:
AnalysisInputLocation
:指向应加载到View
的目标代码。View
:承载你要分析的代码,如其命名般相当于总视图一览无余,加载的代码均会记录在此。SootClass
:表示一个类,可通过对应的ClassType
标识符从View
中加载获取。SootMethod
:表示一个方法,可通过对应的MethodSignature
标识符从View
中加载获取。SootField
:表示一个字段,可通过对应的FieldSignature
标识符从View
中加载获取。Body
:表示一个SootMethod
的方法体。StmtGraph
:表示一个Body
的控制流图,其中的一条条Stmt
表示实际的指令。
实际上大部分数据结构和字节码结构有对应关系,比如类、方法、字段的命名比较明显,Stmt
(Statement)一般可认为是一条 Jimple
三地址码,
对应字节码指令,更宽泛地说可以视为一行结构极其简单的 Java
代码,只不过实际情形是一行 Java
代码往往包含不止一条字节码指令,对应不止
一条 Jimple
三地址码。
另外你可能会有疑问,为什么会同时有 SootMethod
和 Body
两个结构?
SootMethod
仅包含一个方法的基本信息,比如:修饰符、参数……;而 Body
包含的是方法体的部分,即一行行代码的集合。
这两个数据结构可通过相应方法互相获取。
(U1S1,作为从 Soot 过来的,除了 Soot*** 和 Body 是“老熟人”,其它的变化是真大)
2.1 View 的创建
View 支持多种方式创建,比如:Java 源码(实验性功能)、字节码、Jimple 等。
2.1.1 分析字节码(推荐)
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("path2Binary");
JavaView view = new JavaView(inputLocation);
路径指向代码包所在目录,假设有路径 .../target/org/example/ABC.class
,类 ABC 的全名为 org.example.ABC,那么分析路径指向 target
即
可,而不是 org、example 等。
2.1.2 分析 Java 源代码(实验性功能)
分析 Java 源码为实验性功能,仅适用于测试目的,通常更推荐使用编译后的字节码。
AnalysisInputLocation inputLocation = new JavaSourcePathAnalysisInputLocation("path2Source");
JavaView view = new JavaView(inputLocation);
2.1.3 分析 Jimple 码
如果你已经有 Jimple 文件了,那么是可以直接分析它的。
需要注意 JimpleAnalysisInputLocation
接受 Path 类型参数,而不直接接收字符串。
Path pathToJimple = Paths.get("path2Jimple");
AnalysisInputLocation inputLocation = new JimpleAnalysisInputLocation(pathToJimple);
JimpleView view = new JimpleView(inputLocation);
2.2 类的检索
检索过的类默认会永久存储在缓存中,如果不想无限制的存储,可以在创建 View 时额外提供 CacheProvider
。
比如下面使用 LRUCacheProvider
的例子,至多存储 50 个类,超过时新的类会替换最近使用最少的类。
使用该构造方法时,输入位置不接受单个 AnalysisInputLocation
,需要转为 List 类型。
JavaView view = new JavaView(inputLocations, new LRUCacheProvider(50));
每个类都有一个符合 Java 标识符规则的唯一签名,
因此首先需要指定类的签名(ClassType),以下面的代码为例:
package zzz;
public class Burnice {
private String name = "Burnice";
public Burnice() {
}
public void fire() {
System.out.println("Three, two, one, fire!!!");
}
public void go() {
System.out.println("Burnice, Burnice, Burnice, Burnice, Burnice, Burnice, GO!!!GO!!!");
}
public static void main(String[] args) {
Burnice burnice = new Burnice();
burnice.fire();
burnice.go();
}
}
通过包名路径和类名,可以获取该类的 ClassType
,进而获取对应的 SootClass
。
JavaClassType classType = view.getIdentifierFactory().getClassType("zzz.Burnice");
// JavaSootClass sootClass = view.getClass(classType).get();
JavaSootClass sootClass = view.getClass(classType).orElseThrow();
此处发现,光是 view.getClass(classType)
并不足以得到 JavaSootClass,而是返回一个 Optional 泛型对象。
这是因为 SootUp 在设计上强调“必须验证(Validation)”的要求,避免返回 null,因此官方文档中会使用 get() 获取 JavaSootClass,即注释部分。
而 get() 方法遇到 null 其实是会抛出异常的,因此新版本 Java 会建议你使用 orElseThrow() 使方法副作用更为直观,这里就更改过来了。
后续涉及 Optional 使用的代码同理。
2.3 方法的检索
2.3.1 构造 MethodSignature(坑点预警)
按照基础知识所说,方法可以通过 MethodSignature
获取到,而 MethodSignature 的构建有两种方式:
1)调用 getMethodSignature(...)
方法
MethodSignature methodSignature = view.getIdentifierFactory()
.getMethodSignature(
classType,
"main", // 方法名
"void", // 返回类型
Collections.singletonList("java.lang.String[]")); // 参数列表
没错,这才是 1.3.0(或者说当下最新)的方法签名获取方法~~(有点绕)~~
官文:方法名字符串 类的类型字符串/实例 返回类型的字符串/实例 形参列表
实际:类的类型字符串/实例 方法名字符串 返回类型的字符串/实例 形参列表
其它方法这里不多赘述,这里并非冗余设计,而是完全变更了(或者文档写错了)。概念由大到小,这样实现较为合理也好记。
2)解析字符串
MethodSignature methodSignature = view.getIdentifierFactory()
.parseMethodSignature(
"<zzz.Burnice: void main(java.lang.String[])>");
其中 zzz.Burnice
对应包名和类名,即 packageName.classType
的形式。
两种方法都是为了锁定唯一一个方法,避免不同包下可能出现同名(甚至同参数)方法时的冲突。
2.3.2 获取 SootMethod
有了 MethodSignature 下一步就可以获取到 SootMethod 了,可以从 View 或 SootClass 获取。
1)从 View 获取
JavaSootMethod method = view.getMethod(methodSignature).orElseThrow();
2)从 SootClass 获取
MethodSubSignature mss = methodSignature.getSubSignature();
JavaSootMethod method = sootClass.getMethod(mss).orElseThrow();
2.4 字段的检索(坑点回收)
虽然相比类和方法,直接获取字段的情形较少,但还是想写全面一些。
于是就遇到比较 Disgusting 的事情了,先看代码:
FieldSignature fieldSignature = view.getIdentifierFactory().getFieldSignature("name", classType, "java.lang.String");
SootField field = view.getField(fieldSignature).orElseThrow();
是的,获取字段签名的方法参数使用了“字段名、类的类型、字段类型”的次序,而前面方法签名获取的设计却不是这种次序。
感觉是改了一个地方别的地方又没改,或开发设计原则不统一。
2.5 控制流图的检索
每个 SootMethod
都包含一个以 StmtGraph
来表示的控制流图(Control-Flow Graph),程序分析通常会用到该数据结构。
可通过以下方式获取和使用:
StmtGraph<?> graph = method.getBody().getStmtGraph();
graph.getNodes().forEach(stmt -> log.info(stmt.toString()));
graph.getBlocks().forEach(blk -> log.info(blk.toString()));
System.out.println(DotExporter.createUrlToWebeditor(graph));
官文中说明的方法为 nodes(),但目前版本已无该方法。
除了使用 StmtGraph
获取 Jimple 语句,也可以获取基本块, 生成 .dot 图等。
2.6 实例代码相关内容及运行结果
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>studio.sparkle</groupId>
<artifactId>SootUp-Tutorial</artifactId>
<version>1.3.0</version>
<name>SootUp Tutorial</name>
<description>Tutorial project for SootUp</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SootUp 8 artifacts -->
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.core</artifactId>
<version>1.3.0</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</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>
<exclusions>
<exclusion>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.runtime</artifactId>
</exclusion>
</exclusions>
</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>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.callgraph</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.analysis</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.qilin</artifactId>
<version>1.3.0</version>
</dependency>
<!-- Safe updates for SootUp dependencies -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.runtime</artifactId>
<version>3.31.100</version>
</dependency>
<!-- Code annotation -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<!-- Logger -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.24.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
<version>2.24.2</version>
</dependency>
<!-- For YAML configuration -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.18.1</version>
</dependency>
</dependencies>
</project>
Configuration:
# status: warn
# name: YAMLConfigTest
properties:
property:
name: log_dir
value: ./logs
thresholdFilter:
level: debug
appenders:
Console:
name: STDOUT
PatternLayout:
# Pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n"
Pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%5p} %style{%5pid}{bright,magenta} --- [%15.15t] %style{%-40.40logger{39}}{bright,cyan}: %m%n"
disableAnsi: "false"
File:
name: File
filename: ${log_dir}/test.log
PatternLayout:
Pattern: "%d %p %C{1.} [%t] %m%n"
Filters:
ThresholdFilter:
level: info
Loggers:
Root:
# 默认 error
level: info
AppenderRef:
ref: STDOUT
package zzz;
public class Burnice {
private String name = "Burnice";
public void fire() {
System.out.println("Three, two, one, fire!!!");
}
public void go() {
System.out.println("Burnice, Burnice, Burnice, Burnice, Burnice, Burnice, GO!!!GO!!!");
}
public static void main(String[] args) {
Burnice burnice = new Burnice();
burnice.fire();
burnice.go();
}
}
package studio.sparkle.starter;
import lombok.extern.slf4j.Slf4j;
import sootup.core.cache.provider.LRUCacheProvider;
import sootup.core.graph.StmtGraph;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.signatures.FieldSignature;
import sootup.core.signatures.MethodSignature;
import sootup.core.util.DotExporter;
import sootup.java.bytecode.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.JavaSootClass;
import sootup.java.core.JavaSootField;
import sootup.java.core.JavaSootMethod;
import sootup.java.core.types.JavaClassType;
import sootup.java.core.views.JavaView;
import java.util.List;
import java.util.NoSuchElementException;
@Slf4j
public class QuickStart {
private static void quickStart() throws NoSuchElementException {
// View 的创建
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("[待分析字节码位置]");
// JavaView view = new JavaView(inputLocation);
JavaView view = new JavaView(List.of(inputLocation), new LRUCacheProvider(50));
// SootClass 的获取
JavaClassType classType = view.getIdentifierFactory().getClassType("zzz.Burnice");
JavaSootClass sootClass = view.getClass(classType).orElseThrow();
log.info("{}: {}", sootClass.getName(), sootClass.getModifiers());
// SootMethod 的获取 - 调用 getMethodSignature(...) 方法
MethodSignature methodSignature = view.getIdentifierFactory().getMethodSignature(
classType,
"main",
"void", // VoidType 亦可
List.of("java.lang.String[]"));
// 从 View 获取
JavaSootMethod method = view.getMethod(methodSignature).orElseThrow();
log.info("{}: {}", method.getName(), method.getModifiers());
// SootField 的获取
FieldSignature fieldSignature = view.getIdentifierFactory().getFieldSignature("name", classType, "java.lang.String");
JavaSootField field = view.getField(fieldSignature).orElseThrow();
log.info("{}: {}", field.getName(), field.getModifiers());
// StmtGraph 的获取
StmtGraph<?> graph = method.getBody().getStmtGraph();
graph.getNodes().forEach(stmt -> log.info(stmt.toString()));
graph.getBlocks().forEach(blk -> log.info(blk.toString()));
log.info("生成 .dot 图网页编辑器 URL:");
log.info(DotExporter.createUrlToWebeditor(graph));
}
public static void main(String[] args) {
try {
quickStart();
} catch (NoSuchElementException e) {
log.error("Execution failed", e);
}
}
}
结果:
3 SootUp 的输入
跟随前面的示例,我们知道可以通过一个 AnalysisInputLocation
实例指向待分析的代码位置。
它有多个不同的子类,帮助你定义不同的输入代码类型(由于支持程度的差异,可能导致分析能力有所不同)。
你也可以指定一系列的方法体拦截器(BodyInterceptor
),在输入代码转换为 Jimple IR 后、载入 SootUp 前,这些工具类会“拦截”Jimple IR,进行
一些优化等,其具备改写整个方法内容的能力,参考 Java Instrumentation 机制,区别是前者拦截加载到 SootUp 的 IR,后者拦截加载到 JVM 的字节码。
一般情况下,我们写的代码只是运行代码的一部分,其余的还有诸如框架、Java 本身的一些包等等,SootUp 在这里就可以分别引入它们。
3.1 Java 运行时库(Java Runtime)
指向执行中的 JVM 的运行时库。我们知道 Java 9 发生了一项重要变化——引入了模块化的设计,其文件编排随之发生了变化,因此使用时有一定区别。
- Java ≤ 8:
DefaultRTJarAnalysisInputLocation
可以指向 rt.jar 这个基本依赖,Java 的许多核心类都在其中 - 非当前执行中的 Java 版本:有时分析的代码依赖的 Java 版本和 SootUp 运行时的不一致,可以通过
JavaClassPathAnalysisInputLocation
来
指定任意版本的 rt.jar,因为就内容而言它也是一个普通 jar 包 - Java ≥ 9:
JrtFileSystemAnalysisInputLocation
可以指向 Java 9 及以上的基本依赖 jrt 文件系统
如果出现了包含 java.lang.String、java.lang.Object 的错误,那么很可能是因为缺失了这个输入。
3.2 Java 字节码(Java Bytecode)
支持的文件类型:.class
、.jar
、.war
JavaClassPathAnalysisInputLocation
- 和给 JVM 传递 ClassPath 使用的路径一样,通过这个类输入即可PathBasedAnalysisInputLocation
- 一个特殊的抽象类,通过 create 方法构造输入,内部能自动识别来源是目录、jar、war从而构造对应的
输入子类。
3.3 Java 源码
支持的文件类型:.java
OTFCompileAnalysisInputLocation
- 可以将 .java 文件或源码的字符串传给该类,SootUp 通过 Java 编译器获得字节码后再转为 JimpleJavaSourcePathAnalysisInputLocation
实验性功能 - 像指向字节码那样指向源码的根目录
3.4 Jimple
支持的文件类型:.jimple
JimpleAnalysisInputLocation
- 指向 .jimple 文件或它们的目录JimpleStringAnalysisInputLocation
- 直接传入 .jimple 文件内容的字符串
3.5 安卓字节码(Android Bytecode)
支持的文件类型:.apk
ApkAnalysisInputLocation
- 目前内部使用了 dex2jar,直接生成 Jimple 的 SootUp 解决方案正在开发中(据称)。
4 示例
一些来自官方的使用 SootUp 分析 Java 程序的例子,内容上略有改动。
4.1 分析是否输出了 Hello World!
官方的待分析样例代码:
分析代码:
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.jimple.common.expr.JVirtualInvokeExpr;
import sootup.core.jimple.common.stmt.JInvokeStmt;
import sootup.core.model.SootClass;
import sootup.core.model.SootMethod;
import sootup.core.model.SourceType;
import sootup.core.signatures.MethodSignature;
import sootup.core.types.ClassType;
import sootup.core.views.View;
import sootup.java.bytecode.inputlocation.PathBasedAnalysisInputLocation;
import sootup.java.core.language.JavaJimple;
import sootup.java.core.views.JavaView;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
/** 创建和使用 SootUp 来分析 Java 程序是否输出了 "Hello World" */
@Slf4j
public class BasicSetup {
public static void main(String[] args) {
// 创建一个 AnalysisInputLocation 指向 class 文件路径
Path pathToBinary = Paths.get("src/test/resources/Basicsetup/binary");
AnalysisInputLocation inputLocation = PathBasedAnalysisInputLocation.create(pathToBinary, SourceType.Application);
// 为项目创建一个 View 用以检索类
View view = new JavaView(inputLocation);
// 创建需要分析的类的类型
ClassType classType = view.getIdentifierFactory().getClassType("HelloWorld");
// 创建需要分析的方法的签名
MethodSignature methodSignature =
view.getIdentifierFactory()
.getMethodSignature(
classType, "main", "void", List.of("java.lang.String[]"));
// 检查 "HelloWorld" 类是否存在
if (view.getClass(classType).isEmpty()) {
// System.out.println("Class not found!");
log.info("Class not found!");
return;
}
// 获取 "HelloWorld" 类
SootClass sootClass = view.getClass(classType).get();
// 检查 "main" 方法是否存在
if (sootClass.getMethod(methodSignature.getSubSignature()).isEmpty()) {
// System.out.println("Method not found!");
log.info("Method not found!");
return;
}
// 获取 "main" 方法
SootMethod sootMethod = sootClass.getMethod(methodSignature.getSubSignature()).get();
// 读取方法的 jimple 代码
log.info("Jimple code of method \"{}\":", sootMethod.getName());
System.out.println(sootMethod.getBody());
// 检查方法是否包含输出 ("Hello World!") 的语句.
boolean helloWorldPrintPresent =
sootMethod.getBody().getStmts().stream()
.anyMatch(
stmt ->
stmt instanceof JInvokeStmt
&& stmt.getInvokeExpr() instanceof JVirtualInvokeExpr
&& stmt.getInvokeExpr()
.getArg(0)
.equivTo(JavaJimple.getInstance().newStringConstant("Hello World!")));
// 输出检查结果
if (helloWorldPrintPresent) {
// System.out.println("Hello World print is present.");
log.info("Hello World print is present.");
} else {
// System.out.println("Hello World print is not present.");
log.info("Hello World print is not present.");
}
}
}
执行结果:
简要解析:
核心的判断部分是以下代码段:
stmt instanceof JInvokeStmt
&& stmt.getInvokeExpr() instanceof JVirtualInvokeExpr
&& stmt.getInvokeExpr()
.getArg(0)
.equivTo(JavaJimple.getInstance().newStringConstant("Hello World!")))
在遍历方法中的语句时,遵循以下层层递进的逻辑,在调用相应方法时才不会出错:
- 是否属于 Jimple 方法调用语句(JInvokeStmt)
- 如果是,其调用表达式是否属于 virtualinvoke(JVirtualInvokeExpr,因为 println 对应一个 invokevirtual 指令)
- 如果是,其第一个参数是否为字符串 “Hello World!”
可以看出,其判断逻辑并不缜密,只能说明存在一个 invokevirtual 的方法调用,且其第一个参数为字符串 “Hello World!”。
想进一步精细分析,还需要限定类、方法等等。
但作为初步使用的示例自然应从简,无伤大雅。
4.2 消除无用赋值语句
官方的待分析样例代码:
分析代码:
注:
笔者使用的 1.3.0 版本中,官方示例的 import sootup.java.bytecode.interceptors.DeadAssignmentEliminator;
已被移除,转为
import sootup.java.core.interceptors.DeadAssignmentEliminator;
。
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.jimple.common.constant.IntConstant;
import sootup.core.jimple.common.stmt.JAssignStmt;
import sootup.core.model.SootMethod;
import sootup.core.model.SourceType;
import sootup.core.signatures.MethodSignature;
import sootup.core.types.ClassType;
import sootup.java.bytecode.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.interceptors.DeadAssignmentEliminator;
import sootup.java.core.views.JavaView;
import java.util.List;
/** 使用 BodyInterceptor 消除无用赋值的示例 */
@Slf4j
public class BodyInterceptor {
public static void main(String[] args) {
// 创建一个 AnalysisInputLocation 指向 class 文件路径
AnalysisInputLocation inputLocation =
new JavaClassPathAnalysisInputLocation(
"src/test/resources/BodyInterceptor/binary",
SourceType.Application,
List.of(new DeadAssignmentEliminator()));
// 为项目创建一个 View
JavaView view = new JavaView(inputLocation);
// 创建需要分析的类的类型
ClassType classType = view.getIdentifierFactory().getClassType("File");
// 创建需要分析的方法的签名
MethodSignature methodSignature =
view.getIdentifierFactory()
.getMethodSignature(classType, "someMethod", "void", List.of());
// 检查目标类是否存在
if (view.getClass(classType).isEmpty()) {
// System.out.println("Class not found.");
log.info("Class not found.");
return;
}
// 检查 "someMethod" 方法是否存在
if (view.getMethod(methodSignature).isEmpty()) {
// System.out.println("Method not found.");
log.info("Method not found.");
return;
}
// 获取 "someMethod" 方法
SootMethod method = view.getMethod(methodSignature).get();
// 读取方法的 jimple 代码
log.info("Jimple code of method \"{}\":", method.getName());
System.out.println(method.getBody());
// 检查无用的 l1 = 3 是否已消除,即 BodyInterceptor 是否发挥了作用
boolean interceptorWorked =
method.getBody().getStmts().stream()
.noneMatch(
stmt ->
stmt instanceof JAssignStmt
&& ((JAssignStmt) stmt).getRightOp().equivTo(IntConstant.getInstance(3)));
if (interceptorWorked) {
// System.out.println("Interceptor worked as expected.");
log.info("Interceptor worked as expected.");
} else {
// System.out.println("Interceptor did not work as expected.");
log.info("Interceptor did not work as expected.");
}
}
}
执行结果:
简要解析:
在被分析的 File.class 中,存在以下代码:
int a = 3; // unused
System.out.println(3);
变量 a 初始化后从未被使用。在构造输入时,指定 DeadAssignmentEliminator
(BodyInterceptor 的一个实现类),从而对解析字节码得到
的 Jimple 代码优化,消除其中无用的赋值语句。
4.3 生成调用图
官方的待分析样例代码:
分析代码:
注:
官方示例添加了额外的 rt.jar 输入源,但实测下来这个示例中不是必须的,因此注释掉了。另外不清楚是否是版本变更问题,subclassesOf
方法返回的并非
子类列表,而是 Stream<ClassType>
,直接使用 System.out.println 打印的是 Stream 实例信息,故改为了使用 forEach。
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.callgraph.CallGraph;
import sootup.callgraph.CallGraphAlgorithm;
import sootup.callgraph.ClassHierarchyAnalysisAlgorithm;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.signatures.MethodSignature;
import sootup.core.typehierarchy.ViewTypeHierarchy;
import sootup.core.types.ClassType;
import sootup.core.types.VoidType;
import sootup.java.bytecode.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.JavaIdentifierFactory;
import sootup.java.core.views.JavaView;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class CallGraphExample {
public static void main(String[] args) {
// 创建 AnalysisInputLocation 列表指向 class 文件路径
List<AnalysisInputLocation> inputLocations = new ArrayList<>();
inputLocations.add(
new JavaClassPathAnalysisInputLocation("src/test/resources/Callgraph/binary"));
/* 如果使用 Java 8,使用下面的代码(部分 JDK 的 rt.jar 可能不在 lib/,而在 jre/lib 下)
// inputLocations.add(new JavaClassPathAnalysisInputLocation(
// System.getProperty("java.home") + "/lib/rt.jar", SourceType.Library));
// 使用 Java ≥ 9 时,可以另外指定 8 的 rt.jar
// inputLocations.add(new JavaClassPathAnalysisInputLocation(
// "D:/Env/Java/jdk-1.8.0_421/jre/lib/rt.jar", SourceType.Library)); */
JavaView view = new JavaView(inputLocations);
// 获取方法签名
ClassType classTypeA = view.getIdentifierFactory().getClassType("A");
ClassType classTypeB = view.getIdentifierFactory().getClassType("B");
MethodSignature entryMethodSignature =
JavaIdentifierFactory.getInstance()
.getMethodSignature(
classTypeB,
JavaIdentifierFactory.getInstance()
.getMethodSubSignature(
"calc", VoidType.getInstance(), List.of(classTypeA)));
// 创建 TypeHierarchy 和 CHA
final ViewTypeHierarchy typeHierarchy = new ViewTypeHierarchy(view);
log.info("Subclasses of {}:", classTypeA.getClassName());
typeHierarchy.subclassesOf(classTypeA).forEach(ct -> log.info(ct.toString()));
CallGraphAlgorithm cha = new ClassHierarchyAnalysisAlgorithm(view);
// 用入口方法(可有多个)初始化 CHA 来创建调用图
CallGraph cg = cha.initialize(List.of(entryMethodSignature));
log.info("All methods that could be called by \"{}\":", entryMethodSignature);
cg.callsFrom(entryMethodSignature).forEach(ms -> log.info(ms.toString()));
}
}
执行结果:
简要解析:
通过查看待分析类可以知道,C、D 是 A 的子类,并且重写了 A 的方法,B 类含有一个入口方法调用 A 类形参的方法。通过构建调用图分析,从而得知从 B 类的
入口方法开始执行,有可能会调用到 A、C、D 三个类的方法。
此外,可能有人也注意到了,标识符工厂实例在本代码中出现了两种不同的获取途径。
分析 View 的构造方法可知,除非我们额外指定一个特殊的实例来初始化 View,否则其默认就是通过 JavaIdentifierFactory.getInstance()
获取的
实例来初始化自身的,因此二者返回的是同一个对象,通过程序断点也能证明这一点。
4.4 类层次结构的构建和检查
官方的待分析样例代码:
分析代码:
注:
官方示例添加了额外的 rt.jar 输入源,但实测下来这个示例中依然不是必须的,因此注释掉了。另外可能是版本变更问题,directSubtypesOf
方法
和 superClassesOf
方法内部虽然使用了 Set 和 List,但返回的均改成了 Stream,故稍稍改动。
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.typehierarchy.ViewTypeHierarchy;
import sootup.core.types.ClassType;
import sootup.java.bytecode.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.JavaIdentifierFactory;
import sootup.java.core.types.JavaClassType;
import sootup.java.core.views.JavaView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 此示例的测试文件组成如下类层次结构:
*
* <pre>
* |-- B
* A <--|
* |-- C <-- D
* </pre>
*
* 此示例将向您展示如何使用 SootUp 构建和检查类层次结构。
*/
@Slf4j
public class ClassHierarchy {
public static void main(String[] args) {
// 创建 AnalysisInputLocation 列表指向 class 文件路径
List<AnalysisInputLocation> inputLocations = new ArrayList<>();
inputLocations.add(
new JavaClassPathAnalysisInputLocation("src/test/resources/Classhierarchy/binary"));
// inputLocations.add(new DefaultRTJarAnalysisInputLocation()); // add rt.jar
JavaView view = new JavaView(inputLocations);
// 创建 TypeHierarchy
final ViewTypeHierarchy typeHierarchy = new ViewTypeHierarchy(view);
// 获取类型 A 和 C
JavaClassType clazzTypeA = JavaIdentifierFactory.getInstance().getClassType("A");
JavaClassType clazzTypeC = JavaIdentifierFactory.getInstance().getClassType("C");
// 检查 C 直接的子类型
List<ClassType> subtypes = typeHierarchy.directSubtypesOf(clazzTypeC).toList();
boolean allSubtypesAreD = subtypes.stream().allMatch(type -> type.getClassName().equals("D"));
boolean allSubtypesFullyQualifiedAreD =
subtypes.stream().allMatch(type -> type.getFullyQualifiedName().equals("D"));
if (allSubtypesAreD && allSubtypesFullyQualifiedAreD) {
log.info("All direct subtypes of Class C are correctly identified as Class D.");
} else {
log.info("Direct subtypes of Class C are not correctly identified.");
}
// 检查 C 的父类
List<ClassType> superClasses = typeHierarchy.superClassesOf(clazzTypeC).toList();
if (superClasses.equals(
Arrays.asList(
clazzTypeA, JavaIdentifierFactory.getInstance().getClassType("java.lang.Object")))) {
log.info("Superclasses of Class C are correctly identified.");
} else {
log.info("Superclasses of Class C are not correctly identified.");
}
}
}
执行结果:
简要解析:
此示例检查的 C 的子类时,一方面检查子类是否只有 D,另一方面检查子类全称是否也是 D;
检查父类时,检查是否完全符合 A 和 Object(SootUp 的实现中,除了 Object 自身,所有类包括接口都是 Object 的子类)。
4.5 改造类
官方的待改造样例代码:
测试代码:
注:
这个代码有逻辑 bug,后面会有分析和修正的版本,感兴趣也可以先跑一下。
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.core.frontend.OverridingBodySource;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.jimple.basic.Local;
import sootup.core.model.Body;
import sootup.core.model.SootClass;
import sootup.core.model.SootMethod;
import sootup.core.model.SourceType;
import sootup.core.signatures.MethodSignature;
import sootup.core.signatures.MethodSubSignature;
import sootup.core.signatures.PackageName;
import sootup.core.types.ArrayType;
import sootup.core.types.PrimitiveType.IntType;
import sootup.core.types.VoidType;
import sootup.java.bytecode.inputlocation.PathBasedAnalysisInputLocation;
import sootup.java.core.JavaSootClass;
import sootup.java.core.JavaSootMethod;
import sootup.java.core.OverridingJavaClassSource;
import sootup.java.core.language.JavaJimple;
import sootup.java.core.types.JavaClassType;
import sootup.java.core.views.JavaView;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
/**
* 此示例展示了如何使用 OverridingBodySource 和 OverridingClassSource 更改 SootClass 中的方法。
*/
@Slf4j
public class MutatingSootClass {
public static void main(String[] args) {
// 创建一个 AnalysisInputLocation 指向 class 文件路径
AnalysisInputLocation inputLocation =
PathBasedAnalysisInputLocation.create(
Paths.get("src/test/resources/Mutatingclasssoot/binary"), SourceType.Application);
JavaView view = new JavaView(inputLocation);
// 获取目标类型
JavaClassType classType = view.getIdentifierFactory().getClassType("HelloWorld");
// 获取 main 方法签名
MethodSignature methodSignature =
view.getIdentifierFactory()
.getMethodSignature(
classType, "main", "void", List.of("java.lang.String[]"));
// 获取目标类
if (view.getClass(classType).isEmpty()) {
log.info("Class not found.");
return;
}
JavaSootClass sootClass = view.getClass(classType).orElseThrow();
// 获取目标方法并输出
if (view.getMethod(methodSignature).isEmpty()) {
log.info("Method not found.");
return;
}
JavaSootMethod method = view.getMethod(methodSignature).orElseThrow();
Body oldBody = method.getBody();
log.info("Original body of method \"{}\":", methodSignature);
System.out.println(oldBody);
// 构造新的 JavaSootMethod
Local newLocal = JavaJimple.newLocal("helloWorldLocal", IntType.getInstance());
Body newBody = oldBody.withLocals(Set.of(newLocal));
// 等价于 JavaSootMethod newMethod = method.withBody(newBody);
OverridingBodySource newBodySource = new OverridingBodySource(method.getBodySource()).withBody(newBody);
JavaSootMethod newMethod = method.withOverridingMethodSource(old -> newBodySource);
// 构造新的 SootClass
OverridingJavaClassSource overridingJavaClassSource = new OverridingJavaClassSource(sootClass.getClassSource());
OverridingJavaClassSource newClassSource = overridingJavaClassSource.withReplacedMethod(method, newMethod);
SootClass newClass = sootClass.withClassSource(newClassSource);
SootMethod newMethodViaStream = newClass.getMethods().stream().findFirst().orElseThrow();
log.info("New body of method \"{}\":", newMethodViaStream.getSignature());
System.out.println(newMethodViaStream.getBody());
// 使用全新构造的方法子签名
MethodSubSignature newMss = new MethodSubSignature(
"main",
List.of(new ArrayType(
new JavaClassType("String", new PackageName("java.lang")),
1)),
VoidType.getInstance());
if (newClass.getMethod(newMss).isEmpty()) {
log.info("Method not found.");
return;
}
// 检查是否存在新加入的变量
Set<Local> locals = newClass.getMethod(newMss).orElseThrow().getBody().getLocals();
if (locals.stream().anyMatch(local -> local.equals(newLocal))) {
log.info("New local exists in the modified method.");
} else {
log.info("New local does not exist in the modified method.");
}
}
}
执行结果:
踩坑记录:
1)结果的随机性异常
有时会出现预期的正确结果,有时出现以上的失败结果图。
示例的 findFirst 严格来说并不能确保获得的就是 main 方法;根据签名依旧获取错误的原因在第二点说明。
2)没有成功修改 SootClass
说没有修改吧,其实改了;但说改了吧,改得又不符合预期,下面详细分析一下吧。
在代码中加几个断点来调试,原本修改前后都应只包含两个方法(含一个默认构造方法),但修改后的类惊现了三个方法,有两个签名一模一样的 main
方法。
旧的类只有两个方法,而新的类有重复的 main
,那只可能是替换过程出了问题,导致旧方法未移除。
重点在示例的这行代码:
OverridingJavaClassSource newClassSource = overridingJavaClassSource.withReplacedMethod(method, newMethod);
其内部实现为:
@Nonnull
public OverridingJavaClassSource withReplacedMethod(
@Nonnull JavaSootMethod toReplace, @Nonnull JavaSootMethod replacement) {
Set<JavaSootMethod> newMethods = new HashSet<>(resolveMethods());
CollectionUtils.replace(newMethods, toReplace, replacement);
return withMethods(newMethods);
}
由于此时的 OverridingJavaClassSource 仅仅使用了 JavaSootClassSource 来构造,很多字段都是 null,包括关键的 overriddenSootMethods。
而这时调用 resolveMethods(),将会导致重新解析类的信息,包括方法列表里的方法。最终形成的 SootMethod 即便内容上是一模一样的,他们的内存空间地址
也会不一样。
CollectionUtils.replace 是 SootUp 自己的集合操作类,源码如下:
public static <T> void replace(Set<T> set, T oldValue, T newValue) {
set.remove(oldValue);
set.add(newValue);
}
可以看出使用的是 Java 的集合接口,那么移除和添加时进行对象比较使用的就是 Object.equals。JavaSootMethod 是没有重写这个方法的,
它继承了 SootMethod 重写的方法,我们来看一下它是怎么写的:
@Override
public boolean equals(Object obj) {
if (!(obj instanceof SootMethod)) {
return false;
}
return getBodySource() == ((SootMethod) obj).getBodySource()
&& getBodySource().getSignature() == ((SootMethod) obj).getBodySource().getSignature()
&& getModifiers() == ((SootMethod) obj).getModifiers()
&& getParameterTypes() == ((SootMethod) obj).getParameterTypes();
}
前面的 if 还算正常,看到后面估计一部分人内心已经是 WTF 了,全是引用比较。书接上回,由于重新构造了 SootMethod,它们的引用地址自然不同。
经过调试发现,重新解析的方法只有 bodySource 和 bodySource.lazyMethodSignature 不变,毕竟是直接传入的;
尽管 modifiers 和 parameterTypes 内容是一致的,却由于地址不同会判为 false。
原先的 main 方法:
新解析的列表中的 main 方法:
更进一步,第二个条件根本就是多余的,除非 getBodySource 这个 getter 方法每调用一次就会变更对象的内容(实际上还是没有这么离谱的);
既然顶层对象地址都一致了,那么按照它 getSignature 的实现,只可能返回同样的结果。
最后回到 equals 方法的逻辑本身,普遍理性而论都是应对比对象的内容而非地址;但 SootUp 可能有它自己的需求而强制使用引用比较,比如
一个 SootClass、SootMethod 仅可能且必须只解析一次,这样它们的地址就是唯一的,在单次运行生命周期里地址可视为它们的 ID;
假设遵循以上特性,那这个示例的代码逻辑就有问题;假如根本没有这种需求,那这个 equals 的方法逻辑就有问题。
既然 withReplacedMethod 内调用链涉及的替换方式有问题,那就不用它;查阅了一下 OverridingJavaClassSource 的源码,其 withMethods 方法
相对合适,直接接受外部提供的方法列表,那么我们自行替换一下 main 方法即可。另外打印新的方法体时也补充了过滤器,确保获取的是 main 方法。
package studio.sparkle.example;
import lombok.extern.slf4j.Slf4j;
import sootup.core.frontend.OverridingBodySource;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.jimple.basic.Local;
import sootup.core.model.Body;
import sootup.core.model.SootClass;
import sootup.core.model.SootMethod;
import sootup.core.model.SourceType;
import sootup.core.signatures.MethodSignature;
import sootup.core.signatures.MethodSubSignature;
import sootup.core.signatures.PackageName;
import sootup.core.types.ArrayType;
import sootup.core.types.PrimitiveType.IntType;
import sootup.core.types.VoidType;
import sootup.java.bytecode.inputlocation.PathBasedAnalysisInputLocation;
import sootup.java.core.JavaSootClass;
import sootup.java.core.JavaSootMethod;
import sootup.java.core.OverridingJavaClassSource;
import sootup.java.core.language.JavaJimple;
import sootup.java.core.types.JavaClassType;
import sootup.java.core.views.JavaView;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
/**
* 此示例展示了如何使用 OverridingBodySource 和 OverridingClassSource 更改 SootClass 中的方法。
*/
@Slf4j
public class MutatingSootClass {
public static void main(String[] args) {
// 创建一个 AnalysisInputLocation 指向 class 文件路径
AnalysisInputLocation inputLocation =
PathBasedAnalysisInputLocation.create(
Paths.get("src/test/resources/Mutatingclasssoot/binary"), SourceType.Application);
JavaView view = new JavaView(inputLocation);
// 获取目标类型
JavaClassType classType = view.getIdentifierFactory().getClassType("HelloWorld");
// 获取 main 方法签名
MethodSignature methodSignature =
view.getIdentifierFactory()
.getMethodSignature(
classType, "main", "void", List.of("java.lang.String[]"));
// 获取目标类
if (view.getClass(classType).isEmpty()) {
log.info("Class not found.");
return;
}
JavaSootClass sootClass = view.getClass(classType).orElseThrow();
// 获取目标方法并输出
if (view.getMethod(methodSignature).isEmpty()) {
log.info("Method not found.");
return;
}
JavaSootMethod method = view.getMethod(methodSignature).orElseThrow();
Body oldBody = method.getBody();
log.info("Original body of method \"{}\":", methodSignature);
System.out.println(oldBody);
// 构造新的 JavaSootMethod
Local newLocal = JavaJimple.newLocal("helloWorldLocal", IntType.getInstance());
Body newBody = oldBody.withLocals(Set.of(newLocal));
// 等价于 JavaSootMethod newMethod = method.withBody(newBody);
OverridingBodySource newBodySource = new OverridingBodySource(method.getBodySource()).withBody(newBody);
JavaSootMethod newMethod = method.withOverridingMethodSource(old -> newBodySource);
// 构造新的 SootClass
OverridingJavaClassSource overridingJavaClassSource = new OverridingJavaClassSource(sootClass.getClassSource());
// 自行替换方法
List<JavaSootMethod> methods = overridingJavaClassSource.resolveMethods().stream().map(
m -> {
if (methodSignature.equals(m.getSignature())) {
return newMethod;
}
return m;
}
).toList();
OverridingJavaClassSource newClassSource = overridingJavaClassSource.withMethods(methods);
SootClass newClass = sootClass.withClassSource(newClassSource);
// 输出确认新的类的方法列表情况
newClass.getMethods().forEach(m -> log.info(m.toString()));
// 补充 filter 过滤条件
SootMethod newMethodViaStream = newClass.getMethods().stream()
.filter(m -> methodSignature.equals(m.getSignature())).findFirst().orElseThrow();
log.info("New body of method \"{}\":", newMethodViaStream.getSignature());
System.out.println(newMethodViaStream.getBody());
// 使用全新构造的方法子签名
MethodSubSignature newMss = new MethodSubSignature(
"main",
List.of(new ArrayType(
new JavaClassType("String", new PackageName("java.lang")),
1)),
VoidType.getInstance());
if (newClass.getMethod(newMss).isEmpty()) {
log.info("Method not found.");
return;
}
// 检查是否存在新加入的变量
Set<Local> locals = newClass.getMethod(newMss).orElseThrow().getBody().getLocals();
locals.forEach(l -> log.info(l.toString()));
if (locals.stream().anyMatch(local -> local.equals(newLocal))) {
log.info("New local exists in the modified method.");
} else {
log.info("New local does not exist in the modified method.");
}
}
}
执行结果:
3)由于构造输入来源使用的是 JavaView,获取类时可以获取到 JavaSootClass,但本例修改类的相关方法却只能返回 SootClass,在前身 Soot 中没有分
这一级子类,后续有可能继续研究这些设计的区别和意义。
4)示例为了演示各种工具类的不同方法,所以部分代码略显冗余。
5 总结
本篇算是使用 SootUp 的一个入门和铺垫,为进一步的使用打下基础。其中介绍了一些基本概念和基本使用方式,还有一些实际运行示例,这些主要来自官方的
英文文档和 GitHub 项目。由于涉及一些专有名词及文档版本落后于项目版本(即便网页上标为最新),直接机翻文档是远不足以让用户理解并上手的。在示例
这一块还发现并解决了一些问题。
总的来说,除了模块化架构利于项目引用资源的最小化,使用起来感觉这个新版的 SootUp 依然远不够成熟和健壮,部分代码和注释也是直接从 Soot 搬运过来的。
另一方面,SootClass 和 SootMethod 这些类也设计了细分的子类,后续有机会可能研究一下。