有什么方法可以从字节码重新生成stackmap吗?

时间:2017-10-07 15:57:00

标签: java bytecode java-bytecode-asm bytecode-manipulation

我有一个旧库(大约2005年),它执行字节代码操作,但不触及stackmap。因此我的jvm(java 8)抱怨它们是无效的类。绕过错误的唯一方法是使用-noverify运行jvm。但这不是我的长期解决方案。

有没有我可以在已经生成类之后重新生成堆栈映射?我看到ClassWriter类有一个重新生成堆栈映射的选项,但我不确定如何读取字节类并重写一个新的。这可行吗?

2 个答案:

答案 0 :(得分:2)

当您检测没有堆栈映射的旧类并保留旧版本号时,将没有问题,因为JVM将以与以前相同的方式处理它们,而不需要堆栈映射。当然,这意味着您无法注入更新的字节码功能。

当您在转换之前检测具有有效堆栈映射的较新类文件时,您将不会遇到这些问题described by Antimony。因此,您可以使用ASM重新生成堆栈映射:

byte[] bytecode = … // result of your instrumentation
ClassReader cr = new ClassReader(bytecode);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cr.accept(cw, ClassReader.SKIP_FRAMES);
bytecode = cw.toByteArray(); // with recalculated stack maps

访问者API的设计允许使用编写器轻松链接阅读器,只添加代码来拦截您想要更改的工件。

请注意,既然我们知道我们将使用ClassWriter.COMPUTE_FRAMES从头开始重新生成stackmap帧,我们可以将ClassReader.SKIP_FRAMES传递给读者,告诉它不要处理我们要去的源帧无论如何都要忽视。

当我们知道类结构没有改变时,还有另一种优化。我们可以将ClassReader传递给ClassWriter的构造函数,以从未更改的结构中获益,例如目标常量池将使用源常量池的副本进行初始化。但是,必须小心处理此选项。如果我们根本不拦截方法,它也会得到优化,即代码被完全复制而不用重新计算堆栈帧。因此,我们需要一个自定义方法访问者来假装代码可能会发生变化:

byte[] bytecode = … // result of your instrumentation
ClassReader cr = new ClassReader(bytecode);
// passing cr to ClassWriter to enable optimizations
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        MethodVisitor writer=super.visitMethod(access, name, desc, signature, exceptions);
        return new MethodVisitor(Opcodes.ASM5, writer) {
            // not changing anything, just preventing code specific optimizations
        };
    }
}, ClassReader.SKIP_FRAMES);
bytecode = cw.toByteArray(); // with recalculated stack maps

这样,可以将像常量池这样的未更改的工件直接复制到目标字节代码,同时仍然可以重新计算stackmap帧。

但是,有一些警告。从头开始生成堆栈映射意味着不利用有关原始代码结构或转换性质的任何知识。例如。编译器会知道局部变量声明的形式类型,而ClassWriter可能会看到不同的实际类型,它必须找到公共基类型。这种搜索可能非常昂贵,导致在正常执行期间加载延迟或甚至不使用的类。结果类型甚至可能与原始代码中声明的常见类型不同。它将是一个正确的类型,但可能会再次更改结果代码中类的使用。

如果您在不同的环境中执行检测,ASM尝试加载用于确定公共类型的类可能会失败。然后,您必须使用可以在该环境中执行操作的实现覆盖ClassWriter.getCommonSuperClass(…)。如果您对代码有更多了解并且无需通过类型层次结构进行昂贵的搜索,那么这也是添加优化的地方。

通常,建议首先重构旧库以使用ASM,而不是需要后续的自适应步骤。如上所述,当使用启用了优化的ClassReaderClassWriter链执行代码转换时,ASM将能够复制所有未更改的方法,包括其堆栈映射,并且仅重新计算实际更改的堆栈映射方法。在上面的代码中,在后续步骤中重新计算,我们不得不禁用优化,因为我们不知道哪些方法实际更改了。

下一个合乎逻辑的步骤是将stackmap处理结合到检测中,因为通常关于实际转换的知识允许保留99%的现有帧并轻松地适应其他帧,而不需要从头开始进行昂贵的重新计算

答案 1 :(得分:1)

至于如何在课堂上阅读,您应该只能使用ClassReader

关于自动将堆栈映射添加到旧类的可行性这一更普遍的问题,在大多数情况下,它是可能的。然而,有一些模糊的情况,这是不可能的,主要是因为推理验证器比堆栈图验证器更宽松。请注意,这些仅适用于将堆栈映射添加到从未拥有堆栈映射的旧代码的情况。如果要修改现有的Java 8代码,则可以忽略所有这些。

首先是jsrret指令,只允许在类文件版本< = 49(对应于Java 5)中使用。如果要使用它们移植代码,则必须重写代码以复制并内联所有子例程主体。

除此之外,还有更多小问题。例如,推理验证器允许您自由混合布尔和字节数组(验证程序将它们视为相同类型),但堆栈映射验证程序将它们视为不同类型。

另一个潜在的问题是,通过推理验证,永远不会检查死代码,而堆栈映射验证程序仍然需要您为所有内容指定堆栈映射。在这种情况下,修复很容易 - 删除所有死代码。

最后,存在一个问题,即堆栈映射要求您在控制流中合并时预先指定类型的公共超类,而使用推理验证,您不需要显式指定超类型。大多数情况下,这不重要,因为你有一个已知的继承层次结构,但理论上可以继承只在运行时通过ClassLoader定义的类。

当然,堆栈映射需要常量池中的相应条目,这意味着在其他所有内容中,常量池中的空间更少。如果您的类接近于达到最大常量池大小,则可能无法添加堆栈映射。这种情况非常罕见,但可能会在自动生成的代码中发生。

P.S。还有可能向另一个方向前进。如果您的代码没有使用任何版本的51.052.0特定功能(基本上只是invokedynamic,也就是lambdas),那么您可以将类文件版本设置为{{1 ,删除了对堆栈映射的需要。当然,这是一种向后的解决方案,并且随着未来的类文件版本添加更具吸引力的功能(例如lambdas),这将变得越来越难。