2017-10-07 74 views
1

我有一个旧的库(大约在2005年),它执行字节码操作,但不会触摸堆栈图。因此,我的jvm(java 8)抱怨说它们是无效的类。解决这些错误的唯一方法是使用-noverify运行jvm。但这对我来说不是一个长期的解决方案。任何方式从字节码重新生成堆栈映射?

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

回答

2

当你测试没有堆叠地图的旧类并保留其旧版本号时,不会有任何问题,因为它们将由JVM以与之前相同的方式进行处理,而不需要堆叠地图。当然,这意味着你不能注入更新的字节码功能。

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

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从零开始重新生成堆栈映射帧,因此我们可以将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将能够复制所有不变的方法,包括其stackmaps,只有重新计算的实际改变方法stackmaps。在上面的代码中,在后续步骤中进行重新计算时,我们不得不禁用优化,因为我们不知道哪些方法实际发生了更改。

下一个合乎逻辑的步骤是将堆栈映射处理整合到仪器中,因为经常关于他实际转换的知识可以保留99%的现有帧并轻松地适应其他帧,而不需要进行昂贵的重新计算刮。

1

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

至于关于自动将堆栈地图添加到旧类的可行性的更一般问题,在大多数情况下,它是可能的。但是,有一些不太可能的情况,主要是由于推理验证器比堆栈映射验证器松弛。请注意,这些仅适用于将堆栈映射添加到从未有过的旧代码的情况。如果您正在修改现有的Java 8代码,则可以忽略所有这些。

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

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

另一个潜在的问题是,在推理验证中,根本不会检查死代码,而堆栈映射验证器仍然要求您为所有内容指定堆栈映射。在这种情况下,修复很简单 - 删除所有死代码。

最后,有问题的是stackmaps需要你的时候,他们在控制流合并,而与推理验证,你并不需要明确指定超类型到前期指定类型的公共超类。大多数情况下,这并不重要,因为您拥有已知的继承层次结构,但理论上可以从仅在运行时通过ClassLoader定义的类继承。

当然,stackmaps需要在常量池中有相应的条目,这意味着在常量池中对于其他任何东西的空间都较少。如果您有一个接近最大常量池大小的类,则可能无法添加堆栈图。这是非常罕见的,但可能会发生在自动生成的代码中。

P.S.也有可能走向另一个方向。如果您的代码不使用任何版本51.052.0特定功能(基本上只是invokedynamic,又名lambdas),则可以将类文件版本设置为50.0,从而不需要堆栈映射。当然,这是一种向后的解决方案,随着将来的类文件版本添加更多有吸引力的功能(如lambda表达式),这将变得越来越困难。

+0

感谢您的教程;我不知道它是如此参与 - 我希望我可以在课堂上阅读并用堆栈图重写它。这就是说,基本代码是用J8编译器编译的(尽管它最好只是J6代码)。然后有一个持久性库正在进行字节增强 - 我相信它会添加字段和getter/setter并重新编写类文件。鉴于你所说的话,看起来我会有一大堆工作来确保新的方法和领域符合标准。有没有简单的方法来验证将涉及多少工作? –

+0

如果我将代码生成/编译为J6代码,是否可以缓解我的问题?有没有办法将J7 +代码编译为J6目标?将自己限制为J6代码,是否会限制使用任何JEE7或JEE8概念?我无法想象,除非JEE API是为J7 +编写的吗? –

+0

@Eric B.几乎可以肯定只使用ClassReader和ClassWriter,它应该可以工作。我提到的所有问题都是理论上的问题,您不可能在现实世界的代码中看到这些问题。 – Antimony