【问题标题】:Tracking method implementation changes in class bytecode跟踪类字节码中的方法实现变化
【发布时间】:2019-07-23 10:00:58
【问题描述】:

我在一些 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 进行比较。

这里我们有以下变化:

  • getjson() 方法已更改;
  • abc() 方法已被删除;
  • newMethod() 已创建。

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

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

【问题讨论】:

    标签: java bytecode java-bytecode-asm


    【解决方案1】:

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

    我不知道它有多少适用于 Kotlin 编译器,但正如 Is the creation of Java class files deterministic? 中所述,即使使用相同版本编译完全相同的源代码,Java 编译器也不需要生成相同的字节码.虽然他们可能有一个尽可能确定性的实现,但在查看不同版本或替代实现时情况会发生变化,如 Do different Java Compilers (where the vendor is different) produce different bytecode 中所述。

    即使我们假设 Kotlin 编译器具有出色的确定性,即使跨版本,它也不能忽视 JVM 的演变。例如。 the removal of the jsr/ret instructions 不能被任何编译器忽略,即使试图保守一点。但它很可能还会包含其他改进,即使不是强制的¹。

    因此,简而言之,即使整个源代码没有更改,也不能假设编译后的表单必须保持不变。即使使用明确的确定性编译器,我们也必须为使用新版本重新编译时的变化做好准备。

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

    另一方面,仅在一个方法中使用的常量值的简单更改可能根本不会影响方法的字节码,如果新常量恰好与旧常量在池中的相同位置结束.

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

    ¹ 命名一些非强制性更改,the compilation of class literals 更改,同样字符串连接从使用 StringBuffer 更改为使用 StringBuilderchanged again to use StringConcatFactory,使用 getClass() for intrinsic null checks changed to requireNonNull(…) 等。语言不必跟随,但没有人愿意被抛在后面……

    还有一些错误需要修复,例如 obsolete instructions,没有编译器会为了保持确定性而保留这些错误。

    【讨论】:

    • 非常感谢您提供如此详细的回复。我们现在可以重新考虑我们项目中这项任务的主要思想。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-26
    相关资源
    最近更新 更多