有什么办法可以从字节码中重新生成堆栈图?

Eri*_* B. 3 java bytecode bytecode-manipulation java-bytecode-asm

我有一个旧库(大约 2005 年),它执行字节码操作,但不涉及堆栈图。因此,我的 jvm (java 8) 抱怨它们是无效的类。规避错误的唯一方法是使用-noverify. 但这对我来说不是一个长期的解决方案。

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

Hol*_*ger 6

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

当您在转换之前检测具有有效堆栈映射的较新类文件时,您将不会遇到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
Run Code Online (Sandbox Code Playgroud)

访问者 API 旨在允许轻松链接阅读器与编写器,并且仅添加代码来拦截您想要更改的那些工件。

请注意,由于我们知道我们将使用 重新生成堆栈图帧ClassWriter.COMPUTE_FRAMES,因此我们可以传递ClassReader.SKIP_FRAMES给读取器以告诉它不要处理我们无论如何都将忽略的源帧。

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

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
Run Code Online (Sandbox Code Playgroud)

通过这种方式,可以将常量池等未更改的工件直接复制到目标字节码,同时仍然重新计算堆栈图帧。

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

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

通常,建议首先重构旧库以使用 ASM,而不需要后续的适配步骤。如上所述,执行利用链代码转换时ClassReader,并ClassWriter与优化启用,ASM将能够复制所有不变的方法,包括其stackmaps,只有重新计算的实际改变方法stackmaps。在上面的代码中,在后续步骤中进行重新计算,我们不得不禁用优化,因为我们不再知道实际更改了哪些方法。

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