JacoDB文档翻译

https://jacodb.org/documentation/installation/

JacoDB官方使用指南的简单翻译。

有些例子已经不符合最新版本。

一 安装

项目中添加JacoDB。

Gradle

在build.gradle.kts文件的dependencies节点添加以下内容

implementation(group = "org.jacodb", name = "jacodb-api", version = jacodbVersion)
implementation(group = "org.jacodb", name = "jacodb-core", version = jacodbVersion)
implementation(group = "org.jacodb", name = "jacodb-analysis", version = jacodbVersion)

Maven

在pom.xml的dependencies节点添加以下依赖:

<dependencies>
    <dependency>
        <groupId>org.jacodb</groupId>
        <artifactId>jacodb-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jacodb</groupId>
        <artifactId>jacodb-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jacodb</groupId>
        <artifactId>jacodb-analysis</artifactId>
    </dependency>
</dependencies>

二 基本使用

JacoDB API的两个级别:

🍸一个是提供代表文件系统级别的信息,例如bytecode和classes信息。

🍸另一个提供运行时信息,例如types。

bytecode和classes信息来自.class 文件:包含方法、字段等的类。

types表示可以是可空的、带有参数的类型。例如:

类图:

类型图:

这两个层次都与 JcClasspath 相关联。您无法修改从纯字节码中检索的类。types可能通过泛型替换手动构建。

JcClasspath 是classes和types的入口。

JcClassOrInterface 代表着一个类文件。数组或者原始类型没有JcClassOrInterface。

JcType 代表来自 JVM 运行时的类型。

JcClassType#methods包括:

封装类的public/protected/private方法。

在编译时可见的所有祖先方法。

仅来自声明类的构造方法。

JcClassType#fields包括:

封装类的public/protected/private字段。

在编译时可见的所有祖先字段。

获取已声明的方法

class Example {
    public static MethodNode findNormalDistribution() throws Exception {
        File commonsMath32 = new File("commons-math3-3.2.jar");
        File commonsMath36 = new File("commons-math3-3.6.1.jar");
        File buildDir = new File("my-project/build/classes/java/main");
        JcDatabase database = JacoDB.async(
                new JcSettings()
                        .useProcessJavaRuntime()
                        .persistent("/tmp/compilation-db/" + System.currentTimeMillis()) // persist data
        ).get();

        // Let's load these three bytecode locations
        database.asyncLoad(Arrays.asList(commonsMath32, commonsMath36, buildDir));

        // This method just refreshes the libraries inside the database. If there are any changes in libs then 
        // the database updates data with the new results.
        database.asyncLoad(Collections.singletonList(buildDir));

        // Let's assume that we want to get bytecode info only for `commons-math3` version 3.2.
        JcClassOrInterface jcClass = database.asyncClasspath(Arrays.asList(commonsMath32, buildDir))
                .get().findClassOrNull("org.apache.commons.math3.distribution.NormalDistribution");
        System.out.println(jcClass.getDeclaredMethods().size());
        System.out.println(jcClass.getAnnotations().size());
        System.out.println(JcClasses.getConstructors(jcClass).size());

        // At this point the database read the method bytecode and return the result.
        return jcClass.getDeclaredMethods().get(0).asmNode();
    }
}

如果待处理的 JAR 文件被更改或移除,body 方法将返回 null。如果类环境不完整(即,类路径中找不到方法的超类、接口、返回类型或参数),则 API 在运行时会抛出 NoClassInClasspathException

监听文件系统更新

数据库可以在后台监视文件系统的变化,并显式刷新 JAR 文件:

public static void watchFileSystem() throws Exception {
JcDatabase database = JacoDB.async(new JcSettings()
    .watchFileSystem()
    .useProcessJavaRuntime()
    .loadByteCode(Arrays.asList(lib1, buildDir))
    .persistent("", false)).get();
    }


    // A user rebuilds the buildDir folder.
    // The database re-reads the rebuilt directory in the background.

获取type信息

types包括:

基本类型,

类,

数组,

有界和无界通配符

types级别表示根据给定泛型类型中的参数替代所呈现的运行时行为。

public static class A<T> {
    T x = null;
}

public static class B extends A<String> {
}

public static void typesSubstitution() {
    JcClassType b = (JcClassType)classpath.findTypeOrNull("org.jacodb.examples.JavaReadMeExamples.B");
    JcType xType = b.getFields()
            .stream()
            .filter(it -> "x".equals(it.getName()))
            .findFirst().get().getFieldType();
    JcClassType stringType = (JcClassType) classpath.findTypeOrNull("java.lang.String");
    System.out.println(xType.equals(stringType)); // will print "true"
}

多线程

JcClassOrInterfaceJcMethodJcClasspath 的实例是线程安全且不可变的。

JcClasspath 表示类的一个独立快照,自创建以来无法修改。删除或修改库文件不会影响 JcClasspath 实例的结构。JcClasspath#close 方法释放所有快照并清理持久化的数据,如果某些库已过时。

public static void refresh() throws Exception {
        JcDatabase database = JacoDB.async(
            new JcSettings()
            .watchFileSystem()
            .useProcessJavaRuntime()
            .loadByteCode(Arrays.asList(lib1, buildDir))
            .persistent("...")
        ).get();

        JcClasspath cp = database.asyncClasspath(Collections.singletonList(buildDir)).get();
        database.asyncRefresh().get(); // does not affect cp classes

        JcClasspath cp1 = database.asyncClasspath(Collections.singletonList(buildDir)).get(); // will use new version of compiled results in buildDir
    }

如果对 JcClasspath 实例的请求包含尚未索引的库,则会触发索引过程,并返回 JcClasspath 集合的新实例。

public static void autoProcessing() throws Exception {
    JcDatabase database = JacoDB.async(
        new JcSettings()
            .loadByteCode(Arrays.asList(lib1))
            .persistent("...")
    ).get();
        
    JcClasspath cp = database.asyncClasspath(Collections.singletonList(buildDir)).get(); // database will automatically process buildDir
        
}

JacoDB 是线程安全的。如果在另一个线程加载 JAR 文件时请求 JcClasspath 实例,JcClasspath 可以表示正在加载的 JAR 文件的一致状态。在 JcClasspath 中显示的是完全加载的 JAR 文件。请注意:不能保证提交用于加载的所有 JAR 文件实际上都会被加载。

class Example {
    public static void main(String[] args) {
        val db = JacoDB.async(new JcSettings()).get();

        new Thread(() -> db.asyncLoad(Arrays.asList(lib1, lib2)).get()).start();

        new Thread(() -> {
            // maybe created when lib2 or both are not loaded into database
            // but buildDir will be loaded anyway
            var cp = db.asyncClasspath(buildDir).get();
        }).start();
    }
}

Bytecode加载

字节码加载包括两个步骤:

从 JAR 文件或构建目录中检索有关类名的信息,

从 JAR 文件或构建目录中读取类的字节码并进行处理(持久化数据,设置 JcFeature 实现等)。

在执行第一步后,JacoDB 或 JcClasspath 实例将立即返回。您在第二步期间检索类的最终表示。在这两个步骤之间的某个时刻,.class 文件可能会发生变化,类的表示将相应地受到影响。

三 数据库特性

Feature 是一个允许基于字节码存储和查询附加信息的接口。Feature 需要在创建 JacoDB 实例时设置。

var db = JacoDb.async(new JcSettings()
    .useProcessJRE()
    .persistent("/tmp/compilation-db/${System.currentTimeMillis()}") // persist data
    .installFeatures(Usages.INSTANCE, InMemoryHierarchy.INSTANCE)
).get();

InMemoryHierarchy

默认情况下,JacoDB 将类层次结构的信息存储在 SQL 数据库中(ClassHierarchies 表,包含列:class_idsuper_idis_interface)。这允许我们使用递归 SQL 查询检索特定类的层次结构。递归查询相当常见但也相当慢。

InMemoryHierarchy 解决了内置解决方案的性能问题。它引入了一个快速搜索的内存缓存。

内存开销为 O(类的数量)。对于约 50K 个类的项目(包括运行时),这样一个缓存的内存消耗约为 ~6.5 Mb 堆内存。

Usages

您可以找到使用方法和字段的地方。为了获得更好的性能,请使用InMemoryHierarchy

var db = JacoDb.async(new JcSettings()
    .useProcessJRE()
    .persistent("/tmp/compilation-db/${System.currentTimeMillis()}") // persist data
    .installFeatures(Usages.INSTANCE, InMemoryHierarchy.INSTANCE)
).get();
var method = run; // java.lang.Runnable#run method
var field = field; // java.lang.String#value field
var cp = db.asyncClasspath(allClasspath).get();
cp.findUsages(method); // sequence of methods which calls method
cp.findUsages(field, FieldUsageMode.READ); // sequence of fields which reads field value

Usages 索引器遍历所有指令,收集关于方法调用或字段访问的信息,并将其存储在一个表中:

列名列描述
callee_class_symbol_id被调用类名的唯一标识符
callee_name_symbol_id方法/字段名的唯一标识符
callee_desc_hash字段使用为 null;方法的字节码的 Sip 哈希,方法的描述
opcode指令操作码
caller_class_symbols_id调用者类名的唯一标识符
caller_method_offsets此用法的方法编号
location_id位置标识符

四 类路径特性

Classpath features

特性用于基于 JcClasspath 字节码构建表示。在创建类路径时,应该传递这些特性。

JcClasspath cp = db.asyncClasspath(files,
    Arrays.asList(new AppendMainMethodToAllClasses())
).get();

JcClasspathFeature

这个类是所有特性的基类。它有两个方法:

on(event) 允许接收来自其他特性的事件;

event(result, input) 为其他特性生成事件。

基本场景是缓存:当扩展获取有关代码结构的信息时,可以将此信息缓存以供将来使用。

JcClasspathExtFeature

可用于模拟或缓存:

  • tryFindClass(classpath, name): Optional<JcClassOrInterface>?

  • tryFindType(classpath, name): Optional<JcType>? 返回值应符合以下逻辑:

  • 对于找到的类或类型,返回非空的 Optional

  • 对于在类路径中不存在的类或类型,返回空的 Optional

  • 当我们不知道类或类型是否存在时,返回 null

JcClassExtFeature

可用于对类进行修补,添加新字段或方法:

  • fieldsOf(clazz) 返回特定类的附加字段列表;默认为 null

  • methodsOf(clazz) 返回特定类的附加方法列表;默认为 null

  • extensionValuesOf(clazz) 返回特定类的附加信息的映射。

extensionValuesOf 可以暴露有关类的特定信息,例如,来自 Kotlin 或 Scala 编译器。

JcInstExtFeature

  • transformRawInstList(method: JcMethod, list) 转换原始指令列表。

  • transformInstList(method, list) 转换指令列表 — 原始指令保持不变。

UnknownClass

可用于优雅地处理一些类未保留在类路径中的情况。例如:

class Bar {

    int x = 0;

    public void run() {
       System.out.println("Hello world");
    }
}

class Foo extends Bar {

    Bar f = new Bar();

    public void call() {
       System.out.println(f.x);
       run();
    }
}

假设我们有一个包含类 Foo 但不包含 Bar 的类路径。类路径的默认行为是在尝试访问不存在的类时失败。例如,解析方法指令将失败,读取类层次结构将失败,解析方法将失败。

UnknownClasses 特性修复了这种行为。所有指向无处的引用将解析为 JcClassOrInterface 实例的特殊实现。这样的实例将具有空的 JcClassOrInterface.declaredFieldsJcClassOrInterface.declaredMethods,但通过 JcClassOrInterface.lookup 接口完成的所有解析将返回模拟实例。

五 指令

原始指令列表 API

JcRawInstList 表示三地址 Java 字节码指令的原始列表表示。 "原始" 意味着此表示不反映程序的类型和控制流信息。此表示是 JVM 字节码指令与三地址指令列表的一对一匹配。

这个表示的基类是 JcRawInstList。它生成一个类似列表的指令集合,因此您可以修改此列表,迭代指令或通过索引访问它们。

三地址指令

JcRawInst 是原始指令的基础接口。所有指令都由对象标识,不能使用 equals 进行比较。

以下是 JcRawInst 实现的列表:

  • JcAssignInst — 赋值指令。指令的左侧只能是 JcRawValue,右侧可以是任何表达式(JcRawExpr)。

  • JcRawEnterMonitorInstJcRawExitMonitorInst — 监视器指令。直接对应它们现有的模拟。监视器属性只能是 JcRawSimpleValue

  • JcRawCallInst — 调用指令。表示不将其返回变量保存到任何本地变量的方法。返回值的方法调用通过 JcRawAssignInst 表示。

  • JcRawLabelInst — 标签指令。用于标记代码中的一些程序点。主要用于分支指令中。标签通过名称标识,对标签的所有引用通过 JcRawLabelRef 类表示。

  • JcRawReturnInst — 返回指令。当方法不返回任何内容时,returnValue 属性为 null

  • JcRawThrowInst — 抛出指令。

  • JcRawCatchInst — 捕获指令。表示代码中 try...catch 块的条目。不直接映射到字节码指令,但表示方法的 TryCatchBlock。存储与捕获的可抛出异常对应的值,以及它从 startInclusiveendExclusive 的指令范围。

  • JcRawGotoInst — 跳转指令。

  • JcRawIfInst — 条件跳转指令。指令的条件必须是 JcRawConditionExpr,因为我们在更高级别的编程语言中使用的并非所有条件表达式都可以轻松表达为 JVM 字节码。

  • JcRawSwitchInst — 开关指令。LookupSwitchTableSwitch 字节码指令的组合表示。

  • JcRawLineNumberInst—标识在源代码中的位置

  • JcRawAssignInst

原始表达式

JcRawExpr 是所有可以在 JVM 字节码中表示的表达式类型和值类型的基础接口。JcRawExpr 将其类型存储为 TypeName 对象,它只是一个表示为字符串的 Java 类型名称(这就是为什么它是 "raw")。raw:原始的

以下是 JcRawExpr 实现的列表:

  • JcRawBinaryExpr — 允许实现所有算术表达式(例如 JcRawAddJcRawMul 等)、条件表达式(JcRawEqJcRawGt 等)、逻辑表达式(JcRawAndJcRawOrJcRawXor)的二元表达式。

  • JcRawConditionExpr — 条件表达式。可用作 JcRawIfInst 中的条件。

  • JcRawLengthExpr — 数组长度表达式。

  • JcRawNegExpr — 取反表达式。

  • JcRawCastExpr — 强制转换表达式。可用于转换引用类型和原始类型。

  • JcRawNewExpr — 新建表达式。创建单个对象。

  • JcRawNewArrayExpr — 新建数组表达式。创建给定类型的(多维)数组。

  • JcRawInstanceOfExpr — instanceof 检查。

  • JcRawCallExpr — 方法调用表达式。

  • JcRawDynamicCallExpr — invokedynamic 指令表示。保留所有信息。

  • JcRawVirtualCallExpr

  • JcRawInterfaceCallExpr

  • JcRawStaticCallExpr

  • JcRawSpecialCallExpr

  • JcRawValue — 单个值的表示。

  • JcRawSimpleValue — 没有任何子值的简单值的表示。

  • JcRawThis

  • JcRawArgument

  • JcRawLocal

  • JcRawConstant

  • JcRawComplexValue — 具有子值的复杂值。

  • JcRawFieldRef — 字段引用。可用作字段读取访问(例如 a = x.y)和字段存储访问(例如 x.y = a)。

  • JcRawArrayAccess — 数组元素引用。可用作数组读取访问(例如 a = x[y])和数组存储访问(例如 x[y] = a)。

要获得方法的三地址指令列表表示,需要调用 JcMethod::instructionList。指令列表构建需要 JcClasspath,因为某些阶段使用子类型信息。

构建指令列表:实现细节

原始指令列表构建器用于从字节码表示(即从 MethodNode)构建 JcRawInstList。要构建指令列表表示,MethodNode 应包含帧信息(即 FrameNode 实例),并且不应包含 JSR(跳转子例程)指令。因此,每次创建 ClassNode 时,我们调用 ClassNode.computeFrames() 扩展函数,该函数计算每个方法的帧信息。computeFrames 函数使用 ASM 功能来计算帧:它使用 ClassWriter 将 ClassNode 转换回字节码(实际上在转换期间执行帧计算),然后使用 ClassReader 再次读取该字节码。这不是最有效的方式,但是是最简单的方式:手动计算帧是相当困难的。另一件事是内联 JSR 指令。我们通过调用 MethodNode.jsrInlined 扩展来实现这一点,该扩展返回一个新的 MethodNode 实例。它使用 ASM 的 JSRInlinerAdapter 实用程序来创建一个带有内联 JRS 指令的新方法节点。

RawInstListBuilder 将 JVM 字节码指令列表转换为三地址指令列表。大多数转换过程都很简单:字节码指令映射到三地址表达式和指令。最复杂的部分是帧合并。JVM 帧描述了虚拟机在每个指令时的状态,即声明的局部变量和堆栈状态。当一条指令有多个前任时,我们需要将多个传入帧合并为一个。有时 JVM 在指令之前添加一个特殊的帧节点,以描述在合并后帧应该是什么样子。

在帧合并过程中有四种可能的情况。

(1)只有一个传入帧,并且在转换前几个阶段已经完全定义(即我们已经收集了所有的帧信息)。在这种情况下,一切都相当简单,我们可以直接复制帧信息。但是,我们还可以为局部变量的类型信息进行细化。考虑以下字节码:

NEW java/lang/ArrayList
ASTORE 0
...
FRAME FULL [ java/lang/List ]
...
NEW java/lang/LinkedList
ASTORE 0

在转换前两条指令时,我们为单元 0 创建了一个局部变量 %0,其类型为 java.lang.ArrayList。但是,在转换帧信息时,我们看到 JVM 将单元 0 的类型视为 java.lang.List,然后使用单元 0 存储 Java List 的其他实现。我们可以将 %0 的类型细化为 java.lang.List,然后将所有 %0 的出现替换为新版本。这是通过使用 RawInstListBuilder 的 localTypeRefinement 属性和 ExprMapper 实用程序类执行的。

(2)只有一个传入帧,尚未定义。这是一种罕见的情况,但在以下情况下可能会发生:

GOTO L2
L1
FRAME FULL [ java/lang/List ]
...
GOTO L3
L2
...
GOTO L1
L3
RETURN

在这种情况下,我们使用帧信息创建一个具有定义类型的新局部变量,并记得在前任中添加一个赋值,该赋值将使用正确的值初始化新变量。这是通过使用 laterAssignments 和 laterStackAssignments 映射以及 buildRequiredAssignments 函数在最后执行的。对于堆栈变量和局部变量,该过程类似。

(3)存在多个前任帧,所有这些前任帧都已定义(例如,在 if...else 块之后合并时)。在这种情况下,我们在当前帧中创建一个具有定义类型的新局部变量,并在前任块中添加必要的赋值。

(4)存在多个前任帧,其中不是所有帧都已定义(例如,在循环标题中合并时)。在这种情况下,我们在当前帧中创建一个具有定义类型的新局部变量,将必要的赋值添加到前任块中,并记得在最后向未定义的前任块添加所需的赋值。

RawInstListBuilder 还简化了生成的指令列表。这是必要的,因为列表构建过程自然引入了很多冗余。

主要的简化阶段包括:

1删除基本块内重复的赋值。

2删除未使用变量的声明。

3删除未使用变量的相互依赖声明(例如 a = b 和 b = a)。

4简单的单元传播。

5使用 JcClasspath 进行类型规范化。

Visitor API

chatGPT描述

Visitor API 是 JacoDB 提供的一组 API,用于遍历和分析 JVM 字节码。该 API 允许你创建访问者(Visitor),并在访问 JVM 字节码时执行特定的操作。以下是该 API 的一些关键概念和类:
JcVisitor:
	这是 Visitor API 的主要接口。
	提供了许多 visit 方法,用于处理不同类型的元素,如类、方法、字段等。
	你可以实现此接口并重写相关的 visit 方法,以在访问字节码元素时执行自定义的逻辑。
JcNode:
	JcNode 是所有字节码元素的通用父类。
	表示字节码中的一个节点,可以是类、方法、字段等。
JcClassNode:
	表示类节点,继承自 JcNode。
	包含有关类的信息,如类名、父类、接口等。
JcMethodNode:
	表示方法节点,继承自 JcNode。
	包含有关方法的信息,如方法名、参数、返回类型等。
JcFieldNode:
	表示字段节点,继承自 JcNode。
	包含有关字段的信息,如字段名、类型等。
通过实现 JcVisitor 接口,你可以创建一个遍历字节码的自定义访问者,并在需要时执行相应的操作。这使得 JacoDB 成为一个强大的工具,用于分析和处理 Java 字节码。

这里有一个用于遍历和修改 JcRawInstList 的访问者 API。访问者具有标准接口 - 它们可以通过在指令和表达式上使用 accept 方法来调用:

val a = jcRawInst.accept(MyInstVisitor())
val b = jcRawExpr.accept(MyExprVisitor())

我们还提供了一些类似"函数式"的扩展,用于将访问者应用于 JcRawInstList

  • filter(visitor: JcRawInstVisitor<Boolean>): JcRawInstList

  • filterNot(visitor: JcRawInstVisitor<Boolean>): JcRawInstList

  • map(visitor: JcRawInstVisitor<JcRawInst>): JcRawInstList

  • mapNotNull(visitor: JcRawInstVisitor<JcRawInst?>): JcRawInstList

  • flatMap(visitor: JcRawInstVisitor<Collection<JcRawInst>>): JcRawInstList

  • apply(visitor: JcRawInstVisitor<Unit>): JcRawInstList

  • applyAndGet(visitor: T, getter: (T) -> R): R

  • collect(visitor: JcRawInstVisitor<T>): Collection<T>

jcdb-core 包含一些用于处理指令列表的实用访问者:

ExprMapper(val mapping: Map<JcRawExpr, JcRawExpr>) — 遍历指令列表,并将所有出现在映射中的表达式替换为相应的属性。

FullExprSetCollector() — 收集在给定对象(指令列表、单个指令或表达式)中出现的所有表达式。

InstructionFilter(val predicate: (JcRawInst) -> Boolean) — 通过给定的谓词对指令进行过滤。

JcRawInstList可以使用MethodNodeBuilder.build()方法转换回ASM MethodNode。转换过程非常直接,不需要额外的注释。

例子

try...catch的二分搜索实现的块图

try...catch的二分搜索实现的控制流图:

六 图

控制流图API

方法的控制流图表示为一个JcGraph对象。要创建方法的JcGraph,可以调用三地址指令列表的graph函数:

fun createGraph(classpath: JcClasspath, method: JcMethod): JcGraph {
    val instructionList = method.instructionList(classpath)
    return instructionList.graph(classpath, method)
}

JcGraph

JcGraph的中间表示使用了已解析的类型信息(即JcType层次结构)和类路径信息,因此需要一个类路径实例。与JcRawInstList类似,JcGraph存储了方法指令的列表。然而,它还尝试解析方法中的所有执行路径。JcGraph操作JcInst类层次结构(在许多情况下类似于JcRawInst),并提供以下API:

  • entry: JcInst — 获取方法的入口点:只能有一个入口点。

  • exits: List<JcInst> — 获取方法的所有“正常”退出点,即所有返回和抛出指令。

  • throwExits: Map<JcType, List<JcInst>> — 方法的所有潜在异常退出点。

  • ref(inst: JcInst): JcInstRef — 获取指令的JcInstRef。它是一个轻量级的包装器,允许在需要时引用指令。

  • inst(ref: JcInstRef): JcInst — 将JcInstRef转换为JcInst。

  • previous(inst: JcInst): JcInst — 获取列表中的前一条指令。

  • next(inst: JcInst): JcInst — 获取列表中的下一条指令。

  • successors(inst: JcInst): Set<JcInst> — 获取CFG中指令的所有后继。不包括任何异常控制流。

  • predecessors(inst: JcInst): Set<JcInst> — 获取CFG中指令的所有前驱。不包括任何异常控制流。

  • throwers(inst: JcInst): Set<JcInst> — 获取可能抛出被inst捕获的异常的所有指令。表示方法的异常控制流。对所有指令返回一个空集,除了JcCatchInst

  • catchers(inst: JcInst): Set<JcCatchInst> — 获取可能捕获inst抛出的异常的所有指令。表示方法的异常控制流。

  • exceptionExits(inst: JcInst): Set<JcClassType> — 获取指令可能抛出的所有异常类型,方法不会捕获。

  • blockGraph(): JcBlockGraph — 创建CFG的基本块表示。

  • iterator(): Iterator<JcInst> — 在图的指令上创建一个迭代器。

JcBlockGraph

JcBlockGraph是用于CFG的基本块API。它操作JcBasicBlock实例——每个基本块只表示一系列指令,具有以下属性:

  • 基本块的指令在正常执行时(即没有抛出异常时)是按顺序执行的。

  • 基本块的所有指令具有相同的异常处理程序,即对于基本块的每个指令调用jcGraph.catchers(inst)将返回相同的结果。

JcBlockGraph提供以下API:

  • entry: JcBasicBlock — 方法的入口。只能有一个入口。

  • exits: List<JcBasicBlock> — 方法的出口。

  • instructions(block: JcBasicBlock): List<JcInst> — 获取基本块的指令。

  • predecessors(block: JcBasicBlock): Set<JcBasicBlock> — 获取CFG中基本块的所有前驱。不包括任何异常控制流。

  • successors(block: JcBasicBlock): Set<JcBasicBlock> — 获取CFG中基本块的所有后继。不包括任何异常控制流。

  • throwers(block: JcBasicBlock): Set<JcBasicBlock> — 获取可能抛出被block捕获的异常的所有基本块。表示方法的异常控制流。对于所有块,返回一个空集,除了以JcCatchInst开始的块。

  • catchers(block: JcBasicBlock): Set<JcBasicBlock> — 获取可能捕获block抛出的异常的所有基本块。表示方法的异常控制流。

我们还提供了用于可视化JcGraph和JcBlockGraph的API:

  • JcGraph.view(dotCmd: String, viewerCmd: String, viewCatchConnections: Boolean = false) — 使用DOT生成SVG文件(dotCmd需要指定DOT可执行文件的路径),并使用viewerCmd程序(指定浏览器可执行文件)查看。viewCatchConnections标志定义是否在图中显示throw...catch连接。

  • JcBlockGraph.view(dotCmd: String, viewerCmd: String) — 类似,但显示JcBlockGraph。

CFG API在JcInst指令上操作。JcInst与JcRawInst相似,但有一些小的差异。主要区别在于JcInst使用JcType实例来表示类型。另一个区别是JcInst不需要标签来表示指令之间的连接(因为它们存储在JcGraph中)。在所有其他情况下,JcInst层次结构(包括JcExpr和JcValue)与JcRawInst层次结构(包括JcRawExpr和JcRawValue)基本相同。

值得注意的另一件事是,JcGraph表示一个不可变结构,并且不提供修改它的API。这是有意为之的,因为修改CFG需要用户了解图中的所有连接。用户在更改CFG时应正确管理这些连接。但是,用户始终可以创建带有所有必要修改的新副本的JcGraph。

例子

StringConcatSimplifier类中,您可以找到对CFG的修改示例:它创建了一个新的JcGraph,其中所有的invokedynamic字符串连接指令都被替换为简单的String.concat方法调用。

ReachingDefinitionsAnalysis类是使用基本块API的示例。它使用简单的工作列表算法执行基本块的标准到达定义分析。

Visitor API

至于三地址指令列表,我们为遍历和修改JcGraph提供了访问者API。访问者具有标准接口 - 可以通过指令和表达式上的accept方法调用它们:

val a = jcInst.accept(MyInstVisitor())
val b = jcExpr.accept(MyExprVisitor())

我们还提供了类似"函数式"的扩展,用于将访问者应用于JcGraph:

  • filter(visitor: JcInstVisitor<Boolean>): JcGraph

  • filterNot(visitor: JcInstVisitor<Boolean>): JcGraph

  • map(visitor: JcInstVisitor<JcInst>): JcGraph

  • mapNotNull(visitor: JcInstVisitor<JcInst?>): JcGraph

  • flatMap(visitor: JcInstVisitor<Collection<JcInst>>): JcGraph

  • apply(visitor: JcInstVisitor<Unit>): JcGraph

  • applyAndGet(visitor: T, getter: (T) -> R): R

  • collect(visitor: JcInstVisitor<T>): Collection<T>

七 静态数据流分析

数据流分析

使用 jacodb-analysis 模块,可以基于三地址码中间表示执行静态数据流分析。它实现了 Reps、Horwitz 和 Sagiv(1995)论文中描述的 IFDS 求解器。IFDS 是指类别为 interprocedural(过程间)、finite(有限)、distributive(可分配的)、subset(子集) 问题的首字母缩写。

jacodb-analysis 模块为您提供了多个现成的分析工具,同时还提供了 API 用于构建您自己的分析工具。

当前的实现将代码分割为单元,以便 IFDS 框架可以并发地分析它们。信息通过摘要在单元之间共享,但每个单元的生命周期是独立控制的。这使得实现具有高度可扩展性且仍然精确。

基本用法

您可以直接从您的代码中进行分析,也可以通过命令行界面执行。

代码中调用

分析的入口点是 AnalysisMain 中声明的 runAnalysis 方法。该方法接受以下参数:

  • graph — 用于分析的应用程序图,即原始论文中所称的超图。要获取此图,应调用 ApplicationGraphFactory 中的 newApplicationGraphForAnalysis 方法。

  • unitResolver — 将方法分组到单元的对象。了解有关单元解析器的更多信息。

  • ifdsUnitRunner — 用于分析每个单元的运行实例。这是定义具体分析的部分。现成的运行实例位于 RunnersLibrary 中。

  • methods — 要分析的方法列表。

  • timeoutMillis — 可选的超时时间(以毫秒为单位)。

例如,要检测给定 analyzedClass 方法中未使用的变量,您可以运行以下代码(假设 classpathJcClasspath 的一个实例):

List<JcMethod> methodsToAnalyze = analyzedClass.getDeclaredMethods();
JcApplicationGraph applicationGraph = ApplicationGraphFactory
        .asyncNewApplicationGraphForAnalysis(classpath, null)
        .get();
UnitResolver<?> resolver = UnitResolversLibrary.getMethodUnitResolver();
IfdsUnitRunner runner = RunnersLibrary.getUnusedVariableRunner();
    
AnalysisMain.runAnalysis(
        applicationGraph,
        resolver,
        runner,
        methodsToAnalyze,
        Integer.MAX_VALUE
);

示例代码与1.4.2版本接口对不上

命令行界面方式

要执行分析,可以使用 jacodb-cli 模块的命令行界面(CLI)。请指定以下参数:

--analysisConf, -a — 包含以 JSON 格式提供的分析配置的文件路径(详细描述见下文)。

--start, -s — 指定分析开始的类。

--classpath, -cp — JacoDB 分析所需的类路径。 [可选]

--dbLocation, -l — 存储字节码数据的 SQLite 数据库的位置。如果未指定,将不会将数据存储在数据库中。 [可选]

--output, -o — 用于存储分析报告的文件。默认为 report.json。

分析配置文件应声明一个 analyses 对象,其中每个键是分析的名称,每个值是一个具有自定义设置的对象。对于指定的分析,runAnalysis 会执行一次。到目前为止,在设置中,您只能指定 unit resolver,它默认为 MethodUnitResolver。

一个配置文件的示例:

{
  "analyses": {
    "NPE": {},
    "Unused": {
      "UnitResolver": "class"
    },
    "SQL": {}
  }
}

单元解析器

UnitResolver 是一个简单的接口,具有将 JcMethod 映射到自定义的 UnitType 领域的 resolve 函数。它将所有方法分为可以并发分析的组或单元。一般而言,更大的单元意味着更精确但也更耗费资源的分析,因此 UnitResolver 允许您找到平衡点。您可以创建自己的 UnitResolver,但通常可以使用 UnitResolversLibrary 类中预定义的解析器,例如 methodUnitResolver 和 singletonUnitResolver。以下是预定义解析器的列表:

  • methodUnitResolver — 每个单元包含确切一个方法。此解析器提供最快但也最不精确的分析。如果您要分析大量代码,如大型项目或库,请使用此解析器。

  • classUnitResolver — 每个单元对应一个类:一个类中的所有方法属于一个单元。

  • packageUnitResolver — 与前一个相同,但每个单元对应它所声明的包。

  • singletonUnitResolver — 所有现有方法都属于同一个单元。使用此解析器提供最精确但也最耗费资源的分析。如果您要分析少量代码,如一个类或一个小项目,请使用此解析器。

Application graph

在分析过程中,通过 JcApplicationGraph 实例提供源代码信息。该接口将程序的控制流图(CFG)调用图结合在一起,从而提供所谓的超图。创建该实例的最简单方式是从 ApplicationGraphFactory 中调用 newApplicationGraphForAnalysis。

它有一个 bannedPackagePrefixes 参数,是一个字符串列表。如果某个方法声明在一个包中,其名称以这些字符串之一开头,该方法不会包含在应用程序图中,因此也不会被分析。如果传递 null,则使用默认的 defaultBannedPackagePrefixes 值,该值会阻止分析大多数 Java 和 Kotlin 标准库方法。以下是允许额外禁用自定义包的代码示例(假设我们已经有一个作为 JcClasspath 实例化的类路径):

List<String> bannedPackages = new ArrayList<>();
bannedPackages.addAll(ApplicationGraphFactory.getDefaultBannedPackagePrefixes());
bannedPackages.add("my.package.that.wont.be.analyzed");

JcApplicationGraph customGraph = ApplicationGraphFactory
    .asyncNewApplicationGraphForAnalysis(classpath, bannedPackages)
    .get();
   
// Launch some analysis using customGraph...

Runners library

以下是位于 RunnersLibrary 中实现的运行实例的列表:

  1. NpeRunner — 查找可能导致 NullPointerException 的位置。

  2. UnusedVariableRunner — 查找声明未使用变量的所有语句。

  3. TaintRunner — 提供通用的污点分析。要构建它,您需要提供 sourceMethods(产生污点的方法),sinkMethods(不应将受污染的值作为参数或接收者的方法)和 sanitizeMethods(将受污染的值转换为未受污染的方法)。如果源和汇之间存在追踪(未经过消毒方法),则报告为漏洞。

  4. SqlInjectionRunner — 执行具体的污点分析,查找可能发生 SQL 注入的位置。

Writing custom runners

指定自己的分析比使用预定义的分析更为复杂。为了实现这一点,您应该熟悉数据流分析、IFDS 框架和流函数。

One-pass runner

要实现一个简单的单遍分析器,请使用 IfdsBaseUnitRunner。为了实例化它,您需要一个 AnalyzerFactory 实例,这是一个可以通过 JcApplicationGraph 创建 Analyzer 的对象。Analyzer 接口包含必须实现的以下方法(请注意,此接口是实验性的,可能会在不久的将来进行更改):

  1. getFlowFunctions() — 应该返回一个描述四种流函数的 FlowFunctionsSpace 对象,如原始论文中定义的那样。

  2. List<SummaryFact> getSummaryFacts(IfdsEdge edge) — 每次找到新的路径边缘时,IfdsBaseUnitRunner 将调用此方法。该方法应返回由此边缘产生的所有 SummaryFact 元素。如果检测到某些漏洞,则应将其作为 VulnerabilityLocation 返回。当分析完成时,将解析此位置的 TraceGraph,并将 VulnerabilityInstance 添加到结果中。这是返回摘要事实的首选方法。

  3. List<SummaryFact> getSummaryFacts(IfdsResults ifdsResults) — 与上述方法相同,但此方法仅由 IfdsBaseUnitRunner 在事实传播完成(正常或由于取消而完成)时调用一次。它不应返回已由前一方法返回的事实。

  4. getSaveSummaryEdgesAndCrossUnitCalls() — 当为 true 时,将自动将摘要边和 CrossUnitCalleeFact 元素添加到摘要中。这对于前向分析以提高精度和还原跟踪是必要的,但对于反向分析通常可以将其设置为 false。

Composite runners

为了获得更高的精度,通常执行双向分析。要实现这一点,您可以按照上述说明创建后向和前向运行器,并使用现有的复合运行器之一将它们连接起来:

  1. SequentialBidiIfdsUnitRunner — 接受两个运行器,即前向和后向运行器,并按顺序运行它们:首先在反转的图上运行后向分析,然后在正常的图上运行前向分析。

  2. ParallelBidiIfdsUnitRunner — 与前一个相同,但同时启动两个运行器。

八 迁移

如何从soot迁移过来

术语

TermSootJacoDB
bytecode storage-JacoDB
scope of visible classesSceneJcClasspath
classSootClassJcClassOrInterface
class methodSootMethodJcMethod
class fieldSootFieldJcField
type (with generic substitution)-JcJvmType
3-address bytecode representationJimpleBodyJcRawInstList
control flow graphClassicCompleteUnitGraphJcGraph
class hierarchyHierarchyHierarchyExt
call graphCallGraphUsagesExt

Inst = Instruction = 指令

建议

  • 记得关闭资源:JcClasspath、JacoDB。创建类路径是一个涉及 I/O 操作的繁重过程。所有传递给类路径的 JAR 文件或文件夹都会被检查,我们会检查:

    • 它们是否已经被处理过,

    • 它们是否自上次处理以来发生了变化,

    • 它们是否正在被处理,因为它们之前尚未被处理。因此,代码应该尽量重复使用类路径实例。之后,我们建议调用 close 方法,因为 JacoDB 能够删除已处理的资源,这些资源在文件系统中似乎已经过时。

  • 如果代码可能调用类层次结构,请最好设置 InMemoryHierarchy 功能。

  • 如果代码库足够庞大或者在进程重新启动时存活是可能的话,可以在文件系统中使用持久化数据。

  • 仅安装对于此数据库而言是必需的那些功能。

Operations

Create storage

SOOT

// points to specific runtime version
G.v().initJdk(new G.JreInfo(location, version));
Options options = Options.v();
options.set_soot_classpath(files);
Scene.v().loadNecessaryClasses();
PackManager.v().runPacks();

jacodb(java)

var db = JacoDB.async(new JcSettings()
    // points to specific runtime version
    .useJavaRuntime(runtimeFolder)
    // jars to process
    .loadByteCode(Arrays.asList(jar1, jar2))
    // persist all information to improve performance between restarts
    .persistent("/home/user/jcdb.db", false)
).get();
var classpath = db.asyncClasspath(listOf(jar1)).get();

jacodb(kotlin)

val db = jacodb {
    // points to specific runtime version
    useJavaRuntime(runtimeFolder)
    // jars to process
    loadByteCode(listOf(jar1, jar2))
    // persist all information to improve performance between restarts
    persistent(location = "/home/user/jcdb.db", clearOnStart = false)
}
val classpath = db.classpath(listOf(jar1))
Find class

Soot

SootClass clazz = Scene.v().getSootClass("java.lang.String");

JacoDB(Java)

var clazz = classpath.findClassOrNull("java.lang.String")

JacoDB(Kotlin)

val clazz = classpath.findClassOrNull("java.lang.String")

Get 3-address bytecode representation

Soot

SootClass clazz = Scene.v().getSootClass("java.lang.String");
clazz.getMethod("length", Lists.emptyList()).retrieveActiveBody()

JacoDB(java)

var clazz = classpath.findClassOrNull("java.lang.String");
JcClasses.findDeclaredMethodOrNull(clazz, "length", null).getInstructionList();

实际使用没有“getInstructionList”这个方法,有函数“getInstList”方法。

JacoDB(Kotlin)

val clazz = classpath.findClass("java.lang.String")
classpath.findMethodOrNull("length").instructionList

Get control flow graph

Soot

new ClassicCompleteUnitGraph(sootMethod.getActiveBody());

JacoDB(Java)

var cfg = jcMethod.flowGraph()

JacoDB(Kotlin)

val cfg = jcMethod.flowGraph()

Get hierarchy

Soot

Hierarchy h = new Hierarchy();
h.getDirectSubclassesOf(clazz);
h.getDirectSubinterfacesOf(clazz);

JacoDB(Java)

var db = JacoDB.async(new JcSettings()
    .install(InMemoryHierarchy.INSTANCE)
).get();
val ext = classpath.asyncHierarchy().get();
ext.findSubClasses(clazz, allHierarchy = true)
ext.findOverrides(method)

JacoDB(Kotlin)

val db = jacodb {
    // highly recommend to install this extension
    install(InMemoryHierarchy)
}
val ext = classpath.hierarchyExt()
ext.findSubClasses(clazz, allHierarchy = true)
ext.findOverrides(method)

Get CallGraph/Usages

Soot

CallGraph cg = new CallGraph();
cg.edgesInto(edge);
cg.edgesOutOf(edge);

JacoDB(Java)

var db = JacoDB.async(new JcSettings()
    // highly recommend to install InMemoryHierarchy extension
    .install(InMemoryHierarchy.INSTANCE, Usages.INSTANCE)
).get();
var ext = classpath.asyncUsages();
ext.findUsages(field, FieldUsageMode.READ);
ext.findUsages(field, FieldUsageMode.WRITE);
ext.findUsages(method);

JacoDB(Kotlin)

val db = jacodb {
    // highly recommend to install InMemoryHierarchy extension
    install(Usages, InMemoryHierarchy)
}
val ext = classpath.usagesExt()
ext.findUsages(field, FieldUsageMode.READ)
ext.findUsages(field, FieldUsageMode.WRITE)
ext.findUsages(method)

九 API

All modules

十 用法示例

类型求解

测试类型求解器

项目仓库的链接将稍后添加,因为项目本身尚未宣布。

用例

一种基于符号执行的多用途工具将解决类型约束作为其核心算法的一部分。通常,这个任务需要SMT求解,而这是相当耗时的。为了使工具更快运行,应该实现一个独立的TypeSolver,对于这个任务不涉及SMT求解。

为了测试TypeSolver,应该提供关于类、它们的父类、实现的接口、类型参数和方法的信息,以便TypeSolver能够找到适当的类型。

使用 JacoDB,您可以轻松获取这些信息。

实现

TODO

近似

实现近似

项目仓库的链接将稍后添加,因为该项目尚未宣布。

用例

实际代码中的许多过程都经过了优化:这些优化在实际执行时有助于性能,但会妨碍程序分析。近似方法用“模拟”替代实际的方法实现,这些模拟返回所需的值同时为我们提供所需类型的实现。

例如,对于基于符号执行的多用途工具,重要的是近似某些方法:

  • 近似有助于简化方法实现,这些实现对于基于符号执行的分析来说过于复杂。

  • 标准库中的本地方法有时可能根本没有实现。近似方法允许我们为这类方法“编写源代码”。

实现

TODO

符号执行

创建一个用于 JVM 字节码的符号分析器

项目仓库的链接将稍后添加,因为该项目尚未宣布。

用例

要开发一个用于 JVM 字节码的符号分析器,需要获取程序中每个方法的三地址码中间表示(3-address code IR)。以下是使用 JacoDB 和 KSMT 库实现的方法:

实现

首先,我们需要获取方法的三地址码中间表示(3-address code IR)。JacoDB具有类似于 Jimple 的自己的三地址码中间表示。

val jcClass = TODO("obtain target class somehow")
val method = jcClass.declaredMethods.first { it.name == TODO("Target method name") }
val instList = method.instList // 3-address IR

其次,我们需要一个符号状态:

data class State(
    val path: List<JcInst>, // Execution path
    val memory: Map<String, KExpr<out KSort>>, // Symbolic memory
    val pathCondition: List<KExpr<KBoolSort>>, // Path condition
)

要解释符号状态,我们需要获取路径中的最后一条指令,并根据其类型进行相应的处理:

fun interpret(state: State): Collection<State> {
    // 0. Get the last instruction in the path
    val inst = state.path.last()
    // 1. Process the last instruction symbolically
    return when (inst) {
        is JcAssignInst -> interpretAssignInst(state, inst) // Assign instruction, i.e. n = i + 42;
        is JcIfInst -> interpretIfStmt(state, inst) // Branching instruction, i.e. if (n == 101)
        else -> TODO("Process other instructions as well")
    }
}

例如,要处理赋值语句,我们需要解析赋值指令的右操作数(符号表达式)和赋值操作的左操作数(内存位置):

private fun interpretAssignInst(state: State, inst: JcAssignInst): Collection<State> {
    // 0. We need to resolve symbolic expression from [inst.rhv]
    val expr = resolveExpr(state, inst.rhv)

    // 1. We need to write the [expr] to the symbolic memory
    val nextState = when (val lhv = inst.lhv) {
        is JcLocalVar -> {
            // find the next instruction
            val nextInst = inst.location.method.instList[inst.location.lineNumber + 1]
            state.copy(
                // update the symbolic memory with the new write
                memory = state.memory + (lhv.name to expr),
                // update the execution path
                path = state.path + nextInst
            )
        }

        else -> TODO("Process other JcValues as well")
    }
    return listOf(nextState)
}

解析 JcExpr 是直截了当的。我们根据表达式类型匹配,并使用 KSMT 构建函数。

private fun resolveExpr(state: State, expr: JcExpr): KExpr<out KSort> =
    when (expr) {
        is JcAddExpr -> {
            // resolve the left operand recursively
            val leftOperand = resolveExpr(state, expr.lhv) as KExpr<KBvSort>
            // resolve the right operand recursively
            val rightOperand = resolveExpr(state, expr.rhv) as KExpr<KBvSort>
            // make a symbolic add expression with KSMT builder functions
            leftOperand.ctx.mkBvAddExpr(leftOperand, rightOperand)
        }

        // resolve an int constant
        is JcInt -> ctx.mkBv(expr.value, 32U)

        is JcEqExpr -> {
            // resolve the left operand recursively
            val leftOperand = resolveExpr(state, expr.lhv)
            // resolve the right operand recursively
            val rightOperand = resolveExpr(state, expr.rhv)
            // make a symbolic eq expression with KSMT builder functions
            when (leftOperand.sort) {
                // both operands of the boolean sort
                is KBoolSort -> leftOperand.ctx.mkEq(
                    leftOperand as KExpr<KBoolSort>,
                    rightOperand as KExpr<KBoolSort>
                )
                // both operands of the bv sort
                is KBoolSort -> leftOperand.ctx.mkEq(
                    leftOperand as KExpr<KBvSort>,
                    rightOperand as KExpr<KBvSort>
                )

                else -> TODO("Process other sorts as well")
            }
        }

        is JcLocalVar -> state.memory.getValue(expr.name) // get the variable from the current symbolic memory
        else -> TODO("Process other JcExprs as well")
    }

要处理分支指令,我们需要解析一个符号条件并使用求解器检查条件和!条件。

private fun interpretIfStmt(state: State, inst: JcIfInst): Collection<State> {
    val nextStates = mutableListOf<State>()

    // 0. We need to resolve symbolic condition, which should be of the boolean sort
    val expr = resolveExpr(state, inst.condition) as KExpr<KBoolSort>

    // 1. Resolve the positive branch
    val positivePathCondition = state.pathCondition + expr
    // 1.0. Check for satisfiability
    if (solver.check(positivePathCondition)) {
        // 1.1. Find the positive instruction
        val positiveInst = inst.location.method.instList[inst.trueBranch.index]
        // 1.2. Add the instruction to the path
        val positivePath = state.path + positiveInst
        // 1.3. Add the positive state to the result list
        val positiveState = state.copy(path = positivePath, pathCondition = positivePathCondition)
        nextStates += positiveState
    }

    // 2. Resolve the negative branch
    val negativePathCondition = state.pathCondition + expr.run { ctx.mkNot(this) }
    // 1.0. Check for satisfiability
    if (solver.check(negativePathCondition)) {
        // 2.1. Find the negative instruction
        val negativeInst = inst.location.method.instList[inst.falseBranch.index]
        // 2.2. Add the instruction to the path
        val negativePath = state.path + negativeInst
        // 2.3. Add the negative state to the result list
        val negativeState = state.copy(path = negativePath, pathCondition = negativePathCondition)
        nextStates += negativeState
    }

    // 3. Return the result states
    return nextStates
}

完整代码

package org.jacodb.example

import io.ksmt.KContext
import io.ksmt.expr.KExpr
import io.ksmt.sort.KBoolSort
import io.ksmt.sort.KBvSort
import io.ksmt.sort.KSort
import org.jacodb.api.JcMethod
import org.jacodb.api.cfg.JcAddExpr
import org.jacodb.api.cfg.JcAssignInst
import org.jacodb.api.cfg.JcEqExpr
import org.jacodb.api.cfg.JcExpr
import org.jacodb.api.cfg.JcIfInst
import org.jacodb.api.cfg.JcInst
import org.jacodb.api.cfg.JcInt
import org.jacodb.api.cfg.JcLocalVar

/**
 * A data class, representing symbolic state.
 *
 * Let's assume we use KSMT for storing symbolic expressions.
 */
data class State(
    val path: List<JcInst>, // Execution path
    val memory: Map<String, KExpr<out KSort>>, // Symbolic memory
    val pathCondition: List<KExpr<KBoolSort>>, // Path condition
)

/**
 * A solver interface
 */
interface Solver {
    /**
     * @return true, if [pathCondition] is satisfiable, and false otherwise.
     */
    fun check(pathCondition: List<KExpr<KBoolSort>>): Boolean
}

class Analyzer(
    ctx: KContext,
    solver: Solver,
) {
    // 0.1. Initialize an interpreter
    private val interpreter = Interpreter(solver, ctx)

    fun analyze(method: JcMethod) {
        // 1.0. Find the first instruction
        val inst = method.instList.first()
        // 1.1. Initialize an initial symbolic state
        val initialState = State(listOf(inst), emptyMap(), emptyList())
        // 1.2. Add the initial state to the BFS-order queue
        val queue = ArrayDeque(listOf(initialState))
        while (queue.isNotEmpty()) {
            // 2.0. Pop the next state from the queue
            val state = queue.removeFirst()
            // 2.1 Interpret the state symbolically and obtain next states
            val nextStates = interpreter.interpret(state)
            queue.addAll(nextStates)
            // 2.2. here you can check **interesting** states and do something with them
            // your own code
        }
    }
}

/**
 * An example interpreter.
 */
class Interpreter(
    val solver: Solver,
    val ctx: KContext,
) {
    /**
     * Takes a [state] and returns the next states.
     */
    fun interpret(state: State): Collection<State> {
        // 0. Get the last instruction in the path
        val inst = state.path.last()
        // 1. Process the last instruction symbolically
        return when (inst) {
            is JcAssignInst -> interpretAssignInst(state, inst) // Assign instruction, i.e. n = i + 42;
            is JcIfInst -> interpretIfStmt(state, inst) // Branching instruction, i.e. if (n == 101)
            else -> TODO("Process other instructions as well")
        }
    }

    private fun interpretAssignInst(state: State, inst: JcAssignInst): Collection<State> {
        // 0. We need to resolve symbolic expression from [inst.rhv]
        val expr = resolveExpr(state, inst.rhv)

        // 1. We need to write the [expr] to the symbolic memory
        val nextState = when (val lhv = inst.lhv) {
            is JcLocalVar -> {
                // find the next instruction
                val nextInst = inst.location.method.instList[inst.location.lineNumber + 1]
                state.copy(
                    // update the symbolic memory with the new write
                    memory = state.memory + (lhv.name to expr),
                    // update the execution path
                    path = state.path + nextInst
                )
            }

            else -> TODO("Process other JcValues as well")
        }
        return listOf(nextState)
    }

    private fun interpretIfStmt(state: State, inst: JcIfInst): Collection<State> {
        val nextStates = mutableListOf<State>()

        // 0. We need to resolve symbolic condition, which should be of the boolean sort
        val expr = resolveExpr(state, inst.condition) as KExpr<KBoolSort>

        // 1. Resolve the positive branch
        val positivePathCondition = state.pathCondition + expr
        // 1.0. Check for satisfiability
        if (solver.check(positivePathCondition)) {
            // 1.1. Find the positive instruction
            val positiveInst = inst.location.method.instList[inst.trueBranch.index]
            // 1.2. Add the instruction to the path
            val positivePath = state.path + positiveInst
            // 1.3. Add the positive state to the result list
            val positiveState = state.copy(path = positivePath, pathCondition = positivePathCondition)
            nextStates += positiveState
        }

        // 2. Resolve the negative branch
        val negativePathCondition = state.pathCondition + expr.run { ctx.mkNot(this) }
        // 1.0. Check for satisfiability
        if (solver.check(negativePathCondition)) {
            // 2.1. Find the negative instruction
            val negativeInst = inst.location.method.instList[inst.falseBranch.index]
            // 2.2. Add the instruction to the path
            val negativePath = state.path + negativeInst
            // 2.3. Add the negative state to the result list
            val negativeState = state.copy(path = negativePath, pathCondition = negativePathCondition)
            nextStates += negativeState
        }

        // 3. Return the result states
        return nextStates
    }

    /**
     * Resolves an [expr] inside of a [state] into symbolic representation [KExpr] of some sort.
     */
    private fun resolveExpr(state: State, expr: JcExpr): KExpr<out KSort> =
        when (expr) {
            is JcAddExpr -> {
                // resolve the left operand recursively
                val leftOperand = resolveExpr(state, expr.lhv) as KExpr<KBvSort>
                // resolve the right operand recursively
                val rightOperand = resolveExpr(state, expr.rhv) as KExpr<KBvSort>
                // make a symbolic add expression with KSMT builder functions
                leftOperand.ctx.mkBvAddExpr(leftOperand, rightOperand)
            }

            // resolve int constant
            is JcInt -> ctx.mkBv(expr.value, 32U)

            is JcEqExpr -> {
                // resolve the left operand recursively
                val leftOperand = resolveExpr(state, expr.lhv)
                // resolve the right operand recursively
                val rightOperand = resolveExpr(state, expr.rhv)
                // make a symbolic eq expression with KSMT builder functions
                when (leftOperand.sort) {
                    // both operands of the boolean sort
                    is KBoolSort -> leftOperand.ctx.mkEq(
                        leftOperand as KExpr<KBoolSort>,
                        rightOperand as KExpr<KBoolSort>
                    )
                    // both operands of the bv sort
                    is KBoolSort -> leftOperand.ctx.mkEq(
                        leftOperand as KExpr<KBvSort>,
                        rightOperand as KExpr<KBvSort>
                    )

                    else -> TODO("Process other sorts as well")
                }
            }

            is JcLocalVar -> state.memory.getValue(expr.name) // get the variable from the current symbolic memory
            else -> TODO("Process other JcExprs as well")
        }
}
}

十二 关于

关于项目

维护

该库由 UnitTestBot 社区维护。

贡献指引

贡献指引详细信息

贡献代码的一般流程:

  1. 创建你自己的代码分支(fork)。

  2. 将分支库克隆到本地机器。

  3. 实现变更。

  4. 测试你的代码:

    • 在创建拉取请求之前,执行对你的代码变更认为必要的测试。

    • 当实现新功能时,最好找到真实的用户并请他们尝试你的功能,以证明你建议的功能的必要性和质量。

  5. 创建拉取请求,你将在 GitHub 上看到自动化测试是否通过。你的审查者可能会建议你进行更多测试。

  6. 请选择 @lehvolk 作为审阅者。如果有必要,他会将你的拉取请求重新分配给核心团队的其他成员。我们会尽力进行审查,但我们很难指定确切的审查时间。请放心,我们一定会回复你的拉取请求!

基准测试

基准测试在不同范围的 Java 字节码上运行:

  • runtime — 仅使用 Java 运行时,没有额外的依赖关系

  • runtime + guava — 使用一个 guava 的 JAR 文件的 Java 运行时

  • runtime + project classpath — 使用 JacoDB 项目的所有可见依赖关系的 Java 运行时

  • runtime + Idea community — 使用 IntelliJ IDEA Community 项目的所有可见依赖关系的 Java 运行时

    JacoDB 的基准测试还包括已安装 Usages 功能的方案。

环境

OSWindows 10 Pro
Processor11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80 GHz
RAM16 GB
StorageSSD

JacoDB 基准测试结果

运行指令:

<span style="background-color:#f8f8f8"><span style="color:#333333">./gradlew jcdbBenchmark</span></span>
BenchmarkRepeatsAvg time per operation
runtime51064 ms
runtime + guava51090 ms
runtime + project dependencies51551 ms
runtime + IDEA community dependecies59054 ms
runtime + Usages51790 ms
runtime + guava + Usages52590 ms
runtime + project dependencies + Usages52915 ms
runtime + IDEA community + Usages59798 ms

Soot基准测试结果

运行指令:

<span style="background-color:#f8f8f8"><span style="color:#333333">./gradlew sootBenchmark</span></span>
BenchmarkRepeatsAvg time per operation
runtime520985 ms
runtime + guava523323 ms
runtime + project dependencies524027 ms
runtime + IDEA community dependecies530286 ms

SootUp基准测试结果

createInDemandView方式

运行指令:

<span style="background-color:#f8f8f8"><span style="color:#333333">./gradlew sootupBenchmark</span></span>
BenchmarkRepeatsAvg time per operation
runtime50.23 ms
runtime + guava51 ms
runtime + project dependencies560 ms
runtime + IDEA community dependecies5167 ms
createFullView方式

几乎消耗了全部的16GB RAM内存(在12GB时由于OOM错误而失败)。

BenchmarkRepeatsAvg time per operation
runtime50.23 ms
runtime + guava589 ms
runtime + project dependencies54247 ms
runtime + IDEA community dependecies5114 723 ms (fails on 12Gb)

对比

Soot

Soot 框架只是读取所有的 JAR 文件并将字节码存储在内存中。与 Soot 不同,JacoDB 从 JAR 文件中读取可用的 .class 文件并并行构建文件夹。因此,在处理多个 JAR 文件时(由于并行执行),它更快。请注意,JacoDB 在几乎所有 API 准备好供使用时执行许多后台任务(层次结构和用法需要后台活动完成)。

SootUp

SootUp 框架忽略运行时库。这就是为什么“仅运行时”条件的启动时间几乎等于零的原因。

JacoDB

JacoDB 在几乎所有 API 已准备好供使用时执行后台任务。以下是执行不带 Usages 功能的后台活动的结果:

<span style="background-color:#f8f8f8"><span style="color:#333333">./gradlew awaitBackgroundBenchmark</span></span>
BenchmarkRepeatsAvg time per operation
runtime: wait for background jobs53182 ms
runtime + project dependencies: wait for background jobs514737 ms
runtime + IDEA community dependencies: wait for background jobs2137528 ms

对于 IntelliJ IDEA Community 代码库,生成的 SQLite 数据库文件大小约为 3.5 GB。

测试API

Swagger UI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值