首页 > 解决方案 > 跟踪类字节码中的方法实现变化

问题描述

我在一些kotlin代码中有一些抽象项目(我们称之为项目)字节码(它是每个类),每个类字节码都存储为ByteArray;任务是告诉每个类中的哪些特定方法正在从项目的构建到构建进行修改。也就是说,The Project 的同一个类有两个 ByteArray,但它们属于不同的版本,我需要准确比较它们。一个简单的例子。假设我们有一个平凡的类:

class Rst {

    fun getjson(): String {
        abc("""ss""");
        return "jsonValid"
    }

    public fun abc(s: String) {
        println(s)
    }

}

它的字节码存储在 oldByteCode 中。现在类发生了一些变化:

class Rst {

        fun getjson(): String {
            abc("""ss""");
            return "someOtherValue"
        }

        public fun newMethod(s: String) {
            println("it's not abc anymore!")
        }

    }

它的字节码存储在 newByteCode 中。这是主要目标:将 oldByteCode 与 newByteCode 进行比较。

在这里,我们有以下变化:

因此,如果方法的签名保持不变,则方法会更改。如果没有,那已经是一些不同的方法了。

现在回到实际问题。我必须通过它的字节码知道每个方法的确切状态。我目前拥有的是 jacoco 分析器,它将类字节码解析为“包”。在这些包中,我有包、类、方法的层次结构,但只有它们的签名,所以我无法判断方法的主体是否有任何变化。我只能跟踪签名差异。是否有任何工具、库可以将类字节码拆分为其方法字节码?例如,我可以使用这些计算哈希并进行比较。也许 asm 库对此有任何处理?欢迎任何想法。

标签: javabytecodejava-bytecode-asm

解决方案


TL;DR 您仅比较字节码甚至哈希的方法不会导致可靠的解决方案,事实上,对于此类问题根本没有合理努力的解决方案。

我不知道,它有多少适用于 Kotlin 编译器,但正如Java 类文件的创建是确定性的吗?,即使使用相同的版本编译完全相同的源代码,Java 编译器也不需要生成相同的字节码。虽然他们可能有一个尝试尽可能确定性的实现,但是当查看不同的版本或替代实现时情况会发生变化,如Do different Java Compilers (where vendor is different) generate different bytecode中解释的那样。

即使我们假设 Kotlin 编译器具有出色的确定性,即使跨版本,它也不能忽视 JVM 的演变。例如,任何编译器都不能忽略/指令的删除,即使试图保守一点也是如此。jsrret但它很可能也会包含其他改进,即使没有被强制¹。

所以简而言之,即使整个源代码没有改变,假设编译后的形式必须保持不变也不是一个安全的赌注。即使使用显式确定性编译器,我们也必须为使用较新版本重新编译时的更改做好准备。

更糟糕的是,如果一种方法发生变化,它可能会对其他方法的编译形式产生影响,因为只要需要常量或链接信息,指令就会引用常量池中的项,并且这些索引可能会改变,具体取决于其他方法如何使用常量池。在访问前 255 个池索引之一时,某些指令还有一种优化的形式,因此编号的更改可能需要更改指令的形式。这反过来可能对其他指令产生影响,例如切换指令具有填充字节,这取决于它们的字节码位置。

另一方面,仅在一个方法中使用的常量值的简单更改可能对方法的字节码完全没有影响,如果新常量恰好与旧常量在池中的相同位置结束。

因此,要确定两种方法的代码是否真的相同,就无法在一定程度上解析指令并理解它们的含义。仅比较字节或哈希是行不通的。

¹要命名一些非强制性更改,类文字的编译发生了更改,同样,字符串连接从 using 更改StringBuffer为 useStringBuilder再次更改为 useStringConcatFactorygetClass()用于内部null检查的使用更改为requireNonNull(…),等等。不同语言的编译器不会必须跟随,但没有人愿意被抛在后面……</p>

还有一些错误需要修复,比如过时的指令,没有编译器会为了保持确定性而保留这些错误。


推荐阅读