是否可以将 Java 字节码反编译回原始泛型类型参数

Is it possible to decompile Java bytecode back to original generic type parameters

我知道 Java 编译器会用它们的边界替换泛型类型中的所有类型参数,或者 Object 如果类型参数在类型擦除过程中是无边界的。生成的机器字节码将反映替换的边界或 Object.

有没有办法获取生成的机器字节码并将其反编译回 Java 文件,该文件包含泛型类型中的原始类型参数?是否存在可以实现此目的的反编译器?或者由于编译过程的性质,这个过程根本无法逆转?

是的,这被称为转换机器码的反编译过程,或者我们可以将其称为字节码到其原始源代码,但在某种程度上! 确实存在一些反编译器!
您需要的是获得反编译器的一些帮助,并付出一些努力才能将此字节代码转换为您所说的通用类型。 但是不可能以高准确率进行这种逆向工程过程,因为现代编译器的设计方式是通过几个步骤将源代码转换为其机器代码,因此逆向后可以得到的是只是非人类可读形式的汇编代码,但在反编译器的帮助下,可以在一定程度上轻松完成相同的工作。 "The java decompiler project "我说的还是京东项目 http://jd.benow.ca 希望它能让你的概念清晰!

这主要取决于代码是否已被混淆。虽然泛型确实使用了类型擦除,但出于各种原因,编译器通常会在类文件中包含源级别信息,例如泛型类型作为元数据——反射、调试、针对闭源库的编译等。

所以对于一个行为良好的类文件,应该可以取回信息。是否有任何现成的工具,我不知道。很多反编译器确实尝试恢复泛型类型,但我不知道它们有多可靠。

如果代码被混淆了,那么所有的元数据都会被剥离,所以没有希望恢复原来的通用类型。

您是正确的,在字节码级别,当您定义泛型类型并与之交互时,很多信息会丢失。类型擦除对于保持兼容性非常有用:如果您主要在编译时强制执行类型安全,则不需要在 运行 时做太多事情,因此您可以将泛型类型减少到它们的 'raw' 等价物。

这就是关键:编译时验证。如果您想要泛型的灵活性和类型安全性,您的编译器必须非常了解您与之交互的泛型类型。在许多情况下,您不会获得那些 class 的源代码,因此它必须从 某处 获取信息。它确实如此:元数据。 .class 文件与字节码一起嵌入了丰富的信息:编译器需要知道的一切信息,您正在安全地使用泛型库类型。那么保留了什么样的泛型信息?

类型变量和约束

为了使用泛型类型,编译器需要知道的最基本的事情是类型变量列表。对于任何泛型类型或泛型方法,都会保留类型变量的名称和位置。此外,任何约束(上限或下限)也包括在内。

通用超类型签名

有时您会编写 class 来扩展通用 class 或实现通用接口。如果您编写扩展 ArrayList<String>StringList,您将继承很多功能。如果有人想按预期使用你的 StringList 而没有源代码,编译器知道你扩展了 ArrayList 是不够的;它必须知道你扩展了 ArrayList<String>。这在层次结构中向上传递:它必须知道 ArrayList<> 扩展 AbstractList<>,依此类推。因此,此信息得到保留。您的 class 文件将包含任何通用超类型(classes 或接口)的完整通用签名。

会员签名

如果编译器不知道字段、方法参数和 return 类型的完整通用类型,则无法验证您是否正确使用了通用类型。所以,您猜对了:该信息已包含在内。如果 class 成员的任何部分包含泛型类型、通配符或类型变量,该成员将获得保存在元数据中的签名信息。

局部变量

没有必要为了使用类型而保留有关局部变量类型的信息。它对于调试很有用,仅此而已。有元数据表可以用来记录变量的名称和类型,以及它们存在的字节码范围。根据编译器的不同,它们可能会或可能不会默认编写。您可以通过传递 -g:vars 强制 javac 发出它们,但我相信默认情况下会省略它们

呼叫站点

反编译器的最大问题之一,主要影响方法体内的泛型推断,是调用泛型方法的调用站点保留没有关于类型参数的信息。这给像 Java 8 Streams 这样的 APIs 带来了巨大的麻烦,其中泛型运算符被链接在一起,每个都接受匿名类型的 lambda(它们的参数类型可能是逆变的,而它们的 return 类型)。这是一个类型推断的噩梦,但对于碰巧 泛型交互的任何代码来说都是一个问题。这种代码并不会因为它存在于 泛型类型中而变得难以反编译。

这对反编译有何影响

Procyon 和 CFR 等现代 Java 反编译器应该能够很好地重建泛型类型。如果局部变量元数据可用,结果应该非常接近原始代码。如果没有,他们将不得不尝试根据数据流分析来推断方法主体中的泛型类型参数。本质上,反编译器必须查看哪些数据流入和流出通用实例化,并使用它所知道的关于该数据类型的信息来猜测类型参数。有时效果很好;其他时候,没那么多(参见之前关于 Java 8 条流的评论)。

虽然在 API 级别——类型和成员签名——结果应该是正确的。

注意事项

严格来说,这里描述的所有元数据都是可选的:它只在编译时(或反编译时)需要。如果有人 运行 通过混淆器、优化器或其他实用程序编译了 classes,则所有这些信息都可能被删除。它不会在 运行 时间产生影响。

tldr;结论

是的,当然可以在类型参数完整的情况下反编译泛型类型和方法。假设存在所需的元数据,获得正确的类型和成员签名是 'easy' 部分。正确推断泛型实例和方法调用的类型参数是一个棘手的问题,但这对碰巧与泛型交互的任何代码来说都是一个问题。

如前所述,Procyon 和 CFR 都应该在恢复泛型类型和方法方面做得相当不错。