class 字节码中跟踪方法实现的变化
Tracking method implementation changes in class bytecode
我在一些 kotlin 代码中有一些抽象项目(让我们称之为项目)字节码(它的每个 class),每个 class 字节码都存储为 ByteArray;任务是告诉每个 class 中的哪些特定方法正在从项目的构建到构建进行修改。也就是说,同一个class的The Project有两个ByteArray,但是属于不同的版本,我需要比较准确。一个简单的例子。假设我们有一个微不足道的 class:
class Rst {
fun getjson(): String {
abc("""ss""");
return "jsonValid"
}
public fun abc(s: String) {
println(s)
}
}
它的字节码存储在oldByteCode中。现在 class:
发生了一些变化
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 分析器,它将 class 字节码解析为 "bundles"。在这些包中,我有包的层次结构、classes、方法,但只有它们的签名,所以我无法判断方法的主体是否有任何变化。我只能跟踪签名差异。
是否有任何工具、库可以将 class 字节码拆分为其方法字节码?例如,我可以用它们计算哈希值并进行比较。也许 asm 库对此有任何处理?
欢迎任何想法。
TL;DR 你只比较字节码甚至哈希值的方法不会产生可靠的解决方案,事实上,对于这类问题根本没有合理的解决方案。
我不知道,其中有多少适用于 Kotlin 编译器,但正如 Is the creation of Java class files deterministic?, Java compilers are not required to produce identical bytecode even if the same version is used to compile exactly the same source code. While they may have an implementation that tries to be as deterministic as possible, things change when looking at different versions or alternative implementations, as explained in Do different Java Compilers (where the vendor is different) produce different bytecode 中所阐述的那样。
即使我们假设 Kotlin 编译器具有出色的确定性,即使跨版本,也不能忽视 JVM 的演变。例如。 the removal of the jsr
/ret
instructions 不能被任何编译器忽略,即使试图保守。但它很可能也会包含其他改进,即使不是被迫的¹。
所以简而言之,即使整个源代码没有改变,假设编译后的形式必须保持不变也是不安全的。即使使用显式确定性编译器,我们也必须在使用较新版本重新编译时为更改做好准备。
更糟糕的是,如果一种方法发生变化,它可能会对其他方法的编译形式产生影响,因为只要需要常量或链接信息,指令就会引用常量池中的项目,并且这些索引可能会发生变化,具体取决于如何其他方法使用常量池。在访问前 255 个池索引之一时,某些指令也有优化的形式,因此编号的更改可能需要更改指令的形式。这反过来可能会对其他指令产生影响,例如switch 指令有填充字节,具体取决于它们的字节码位置。
另一方面,仅在一个方法中使用的常量值的简单更改可能不会对该方法的字节码产生任何影响,如果新常量碰巧在池中的相同位置结束而不是旧常量。
因此,要判断两种方法的代码是否实际上相同,无法在一定程度上解析指令并理解其含义。只比较字节或哈希是行不通的。
¹ 列举一些 non-mandatory 变化,the compilation of class literals changed, likewise string concatenation changed from using StringBuffer
to use StringBuilder
and , the use of ,等等。不同语言的编译器不必遵循,但 no-one 想要落后……
还有一些错误需要修复,例如 obsolete instructions,没有编译器会为了保持确定性而保留这些错误。
我在一些 kotlin 代码中有一些抽象项目(让我们称之为项目)字节码(它的每个 class),每个 class 字节码都存储为 ByteArray;任务是告诉每个 class 中的哪些特定方法正在从项目的构建到构建进行修改。也就是说,同一个class的The Project有两个ByteArray,但是属于不同的版本,我需要比较准确。一个简单的例子。假设我们有一个微不足道的 class:
class Rst {
fun getjson(): String {
abc("""ss""");
return "jsonValid"
}
public fun abc(s: String) {
println(s)
}
}
它的字节码存储在oldByteCode中。现在 class:
发生了一些变化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 分析器,它将 class 字节码解析为 "bundles"。在这些包中,我有包的层次结构、classes、方法,但只有它们的签名,所以我无法判断方法的主体是否有任何变化。我只能跟踪签名差异。 是否有任何工具、库可以将 class 字节码拆分为其方法字节码?例如,我可以用它们计算哈希值并进行比较。也许 asm 库对此有任何处理? 欢迎任何想法。
TL;DR 你只比较字节码甚至哈希值的方法不会产生可靠的解决方案,事实上,对于这类问题根本没有合理的解决方案。
我不知道,其中有多少适用于 Kotlin 编译器,但正如 Is the creation of Java class files deterministic?, Java compilers are not required to produce identical bytecode even if the same version is used to compile exactly the same source code. While they may have an implementation that tries to be as deterministic as possible, things change when looking at different versions or alternative implementations, as explained in Do different Java Compilers (where the vendor is different) produce different bytecode 中所阐述的那样。
即使我们假设 Kotlin 编译器具有出色的确定性,即使跨版本,也不能忽视 JVM 的演变。例如。 the removal of the jsr
/ret
instructions 不能被任何编译器忽略,即使试图保守。但它很可能也会包含其他改进,即使不是被迫的¹。
所以简而言之,即使整个源代码没有改变,假设编译后的形式必须保持不变也是不安全的。即使使用显式确定性编译器,我们也必须在使用较新版本重新编译时为更改做好准备。
更糟糕的是,如果一种方法发生变化,它可能会对其他方法的编译形式产生影响,因为只要需要常量或链接信息,指令就会引用常量池中的项目,并且这些索引可能会发生变化,具体取决于如何其他方法使用常量池。在访问前 255 个池索引之一时,某些指令也有优化的形式,因此编号的更改可能需要更改指令的形式。这反过来可能会对其他指令产生影响,例如switch 指令有填充字节,具体取决于它们的字节码位置。
另一方面,仅在一个方法中使用的常量值的简单更改可能不会对该方法的字节码产生任何影响,如果新常量碰巧在池中的相同位置结束而不是旧常量。
因此,要判断两种方法的代码是否实际上相同,无法在一定程度上解析指令并理解其含义。只比较字节或哈希是行不通的。
¹ 列举一些 non-mandatory 变化,the compilation of class literals changed, likewise string concatenation changed from using StringBuffer
to use StringBuilder
and
还有一些错误需要修复,例如 obsolete instructions,没有编译器会为了保持确定性而保留这些错误。