是否可能将Java字节码反编译回原始的通用类型参数?

9

我知道Java编译器在类型擦除过程中会用类型参数的边界或 Object 替换所有泛型类型中的类型参数。生成的机器字节码将反映被替换后的边界或Object

有没有一种方法可以将结果机器字节码反编译回包含原始类型参数的泛型类型的Java文件?是否存在可以实现这一点的反编译器?还是由于编译过程的本质,这个过程是不可逆转的?


由于类型已被擦除,您如何预计将字节码反编译为泛型? - lexicore
1
https://dev59.com/m6Tja4cB1Zd3GeqPC4Sz - Sotirios Delimanolis
@AndyTurner 这个问题是关于恢复原始类型参数的。 - lexicore
你是在谈论在类型定义中恢复类型变量,还是泛型类型已被实例化的类型参数?例如,您是否正在询问是否可以反编译ArrayList.class并获取泛型类型ArrayList<T>的源代码,包括T的声明?或者,您是在问,当反编译一个包含变量List<String> myList的方法时,您是否会在反编译的方法中看到变量被标记为List<String>,而不仅仅是List?这两种可能性截然不同。 - Mike Strobel
我指的是第一种情况。有没有一种方法可以反编译Java机器字节码,以便我可以在还原的代码中看到<T> - OLIVER.KOO
3个回答

6
您说得对,在字节码级别上,定义和使用泛型时会丢失很多信息。类型擦除的好处在于保持兼容性:如果您大部分时间都在编译时强制实施类型安全性,那么在运行时就不需要做太多操作,因此可以将泛型类型简化为其“原始”等效类型。
这就是关键所在:编译时验证。如果要具备泛型的灵活性和类型安全性,则编译器必须了解您与之交互的泛型类型的大量信息。在许多情况下,您可能没有这些类的源代码,因此它必须从“某个地方”获取信息。 而且它确实获取到了:元数据。在与字节码并列的.class文件中嵌入了大量信息:编译器需要知道您如何安全地使用通用库类型的所有内容。那么哪些泛型信息得以保留?
类型变量和约束
编译器为了消费泛型类型需要知道的最基本的事情是类型变量的列表。对于任何通用类型或通用方法,类型变量的名称和位置都将被保留。此外,任何约束(上限或下限)也包括在内。
泛型超类型签名
有时候,您编写一个扩展通用类或实现通用接口的类。 如果您编写了一个StringList扩展了ArrayList,则会继承很多功能。如果有人想要“按预期”使用您的StringList而没有源代码,则仅知道您扩展了ArrayList是不够的;它必须知道您扩展了ArrayList。这递归地应用于层次结构:它必须知道ArrayList<>扩展了AbstractList<>等等。因此,这些信息得到保留,并且您的类文件将包括任何通用超类型(类或接口)的完整泛型签名。
成员签名
如果编译器不知道字段、方法参数和返回类型的完整泛型类型,通用类型的正确使用就无法得到验证。所以,你猜对了:该信息也被包含在内。如果类成员的任何部分包含通用类型、通配符或类型变量,则该成员将保存其签名信息。
局部变量
为了消费某种类型,不需要保留有关局部变量类型的信息。这可能有助于调试,但仅限于此。可以使用元数据表记录变量的名称和类型以及它们存在的字节码范围。取决于编译器,默认情况下它们可能被写入或者被省略。您可以通过传递-g:vars来强制javac发出它们,但我认为它们默认被省略。
调用站点
反编译器面临的最大问题之一,主要影响方法体中通用推断的是调用泛型方法的调用点不保留有关类型参数的任何信息。这给像 Java 8 Streams 这样的 API 带来了巨大的麻烦,其中泛型运算符被链接在一起,每个操作符都接受匿名类型的 lambda(其参数类型可能协变,返回类型可能逆变)。这是推理类型的噩梦,但对于与泛型交互的任何代码都是一个问题。这种代码只因存在于泛型类型内部而并不会变得更难反编译。
这如何影响反编译?
现代 Java 反编译器(如 Procyon 和 CFR)应该能够合理地重构泛型类型。如果本地变量元数据可用,结果应该非常接近原始代码。否则,他们将不得不尝试基于数据流分析来推断方法体中的泛型类型参数。本质上,反编译器必须查看流入和流出泛型实例化的数据,使用它所知道的关于该数据类型的信息来猜测类型参数。有时它表现得非常好;其他时候则不然(参见前面有关 Java 8 Streams 的注释)。
然而,在 API 级别 - 类型和成员签名方面,结果应该是完美无瑕的。
需要注意的是,严格来说,这里描述的所有元数据都是可选的:它只在编译时(或反编译时)需要。如果有人通过混淆器、优化器或其他实用程序运行他们的已编译类,则所有这些信息都可能被剥离。这在运行时不会有任何影响。
总之,如果所需的元数据存在,确实可以反编译带有类型参数的泛型类型和方法。在正确推断泛型实例和方法调用的类型参数方面可能比较棘手,但这是与泛型交互的任何代码都会遇到的问题。正如前面提到的,Procyon 和 CFR 应该都能很好地恢复泛型类型和方法。

可以尝试在通用类型上使用 javap -v 命令,并查找 Signature 属性以查看输出内容。也可以使用 -r -v 参数强制启用详细反汇编模式(字节码输出)的 Procyon 工具。Procyon 在 ANSI 兼容终端上提供彩色输出,使得代码更易读。 - Mike Strobel

1
这主要取决于代码是否已被混淆。尽管泛型使用类型擦除,但编译器通常会将源级别信息(如泛型类型)作为元数据包含在类文件中,出于各种原因 - 反射、调试、编译针对闭源库等等。
因此,对于一个行为良好的类文件,应该可以获取信息。至于是否有现成的工具可用于此,我不知道。许多反编译器确实会尝试恢复泛型类型,但我不知道它们的可靠性如何。
如果代码已被混淆,则所有元数据都将被剥离,因此没有希望恢复原始泛型类型。

-2
是的,这被称为反编译过程,将机器代码或者字节码转换回原始源代码,但只能做到一定程度! 有一些反编译器是存在的! 您需要做的就是得到一些反编译器的帮助,并付出一点努力,以将此字节码转换为其通用类型,正如您所说。 但是,由于现代编译器设计了多个步骤来将源代码转换为机器代码,因此不可能以高准确性比率进行此类逆向工程过程。您可以得到的仅是非人类可读形式的汇编代码,但是在反编译器的帮助下,同样的工作可以轻松完成一定程度的任务。 “Java反编译器项目”或JD项目就是我所说的东西。 http://jd.benow.ca 希望它能让您清楚概念!

2
OP似乎非常清楚反编译器是什么,他们正在寻找针对泛型中原始参数类型的反编译可能性。因此,您在答案中没有解决问题的核心要点。 - lexicore
1
@lexicore 谢谢你的澄清,你说得对。我知道反编译器是什么。 - OLIVER.KOO

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接