一、SootUp快速入门体验

本文更新于 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 三地址码。

另外你可能会有疑问,为什么会同时有 SootMethodBody 两个结构?
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 编译器获得字节码后再转为 Jimple
  • JavaSourcePathAnalysisInputLocation 实验性功能 - 像指向字节码那样指向源码的根目录

3.4 Jimple

支持的文件类型:.jimple

  • JimpleAnalysisInputLocation - 指向 .jimple 文件或它们的目录
  • JimpleStringAnalysisInputLocation - 直接传入 .jimple 文件内容的字符串

3.5 安卓字节码(Android Bytecode)

支持的文件类型:.apk

  • ApkAnalysisInputLocation - 目前内部使用了 dex2jar,直接生成 Jimple 的 SootUp 解决方案正在开发中(据称)。

4 示例

一些来自官方的使用 SootUp 分析 Java 程序的例子,内容上略有改动。

4.1 分析是否输出了 Hello World!

官方的待分析样例代码:

Basicsetup

分析代码:

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!")))

在遍历方法中的语句时,遵循以下层层递进的逻辑,在调用相应方法时才不会出错:

  1. 是否属于 Jimple 方法调用语句(JInvokeStmt)
  2. 如果是,其调用表达式是否属于 virtualinvoke(JVirtualInvokeExpr,因为 println 对应一个 invokevirtual 指令)
  3. 如果是,其第一个参数是否为字符串 “Hello World!”

可以看出,其判断逻辑并不缜密,只能说明存在一个 invokevirtual 的方法调用,且其第一个参数为字符串 “Hello World!”。
想进一步精细分析,还需要限定类、方法等等。

但作为初步使用的示例自然应从简,无伤大雅。

4.2 消除无用赋值语句

官方的待分析样例代码:

BodyInterceptor

分析代码:


笔者使用的 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 生成调用图

官方的待分析样例代码:

Callgraph

分析代码:


官方示例添加了额外的 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 类层次结构的构建和检查

官方的待分析样例代码:

Classhierarchy

分析代码:


官方示例添加了额外的 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 改造类

官方的待改造样例代码:

Mutatingclasssoot

测试代码:


这个代码有逻辑 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 方法

旧的类只有两个方法,而新的类有重复的 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 方法:

原 JavaSootMethod

新解析的列表中的 main 方法:

新 JavaSootMethod

更进一步,第二个条件根本就是多余的,除非 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 这些类也设计了细分的子类,后续有机会可能研究一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值