什么样的Java代码需要stackmap框架?

我正在尝试编写unit testing以解决有关缺少stackmap帧的问题的解决方法,但为此目的,我将需要生成一个在Java 8上无法validation的类,如果它缺少stackmap帧。

下面你可以看到我的测试用例(依赖项:ASM,Guava,JUnit)。 它从GuineaPig类中删除了stackmap帧,希望导致其字节码无法validation。 我遇到问题的部分是使用最少的代码来填充GuineaPig中的TODO,这需要堆栈图帧,以便测试通过。

import com.google.common.io.*; import org.junit.*; import org.junit.rules.ExpectedException; import org.objectweb.asm.*; import java.io.*; import static org.objectweb.asm.Opcodes.ASM5; public class Java6MissingStackMapFrameFixerTest { @Rule public final ExpectedException thrown = ExpectedException.none(); public static class GuineaPig { public GuineaPig() { // TODO: make me require stackmap frames } } @Test public void example_class_cannot_be_loaded_because_of_missing_stackmap_frame() throws Exception { byte[] originalBytecode = getBytecode(GuineaPig.class); ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ClassVisitor(ASM5, cw) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return new MethodVisitor(ASM5, super.visitMethod(access, name, desc, signature, exceptions)) { @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { // remove the stackmap frames in order to cause a VerifyError // super.visitFrame(type, nLocal, local, nStack, stack); } }; } }; new ClassReader(originalBytecode).accept(cv, 0); byte[] transformedBytecode = cw.toByteArray(); // Files.asByteSink(new File("test.class")).write(transformedBytecode); thrown.expect(VerifyError.class); thrown.expectMessage("Expecting a stackmap frame"); Class clazz = new TestingClassLoader().defineClass(transformedBytecode); clazz.newInstance(); } private static byte[] getBytecode(Class clazz) throws IOException { String classFile = clazz.getName().replace(".", "/") + ".class"; try (InputStream b = clazz.getClassLoader().getResourceAsStream(classFile)) { return ByteStreams.toByteArray(b); } } private static class TestingClassLoader extends ClassLoader { public Class defineClass(byte[] bytecode) { ClassReader cr = new ClassReader(bytecode); String className = cr.getClassName().replace("/", "."); return this.defineClass(className, bytecode, 0, bytecode.length); } } } 

理论

Java VM规范§4.10.1 (按类型检查validation)指定何时需要堆栈映射帧。 首先,它给出了一个非正式的描述:

目的是堆栈映射帧必须出现在方法中每个基本块的开头。 堆栈映射帧指定每个操作数堆栈条目的validation类型以及每个基本块开始时的每个局部变量的validation类型。

详细规范在§4.10.1.6 (带代码的类型检查方法)中给出。 goto命令需要堆栈映射帧:

在无条件分支之后使用代码而不为其提供堆栈映射帧是非法的。

和所有其他分支命令:

如果目标具有关联的堆栈帧,则分支到目标是类型安全的,并且当前堆栈帧StackFrame可分配给Frame。

此外,exception处理程序的开头需要一个堆栈映射框:

如果指令的传出类型状态是ExcStackFrame,则指令满足exception处理程序,并且处理程序的目标(处理程序代码的初始指令)是类型安全的,假设传入类型状态T.

最后, §4.10.1.9 (类型检查指令)指定哪些指令需要具有堆栈映射帧的分支目标。 在类型规则中查找targetIsTypeSafe ; 说明gotoif*lookupswitchtableswitch有它。

即使以下代码也需要stackmap框架:

 public static class GuineaPig { public GuineaPig() { int i = 1; if (i > 0) { // code branch to require stackmap frames } } } 

如果它们丢失,代码将失败并出现exception:

 java.lang.VerifyError: Expecting a stackmap frame at branch target 10 Exception Details: Location: net/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig.()V @7: ifle Reason: Expected stackmap frame at this location. Bytecode: 0000000: 2ab7 000c 043c 1b9e 0003 b1 at java.lang.Class.getDeclaredConstructors0(Native Method) at java.lang.Class.privateGetDeclaredConstructors(Class.java:2658) at java.lang.Class.getConstructor0(Class.java:2964) at java.lang.Class.newInstance(Class.java:403) 

这是字节码:

  public net.orfjackal.retrolambda.Java6MissingStackMapFrameFixerTest$GuineaPig(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: iconst_1 5: istore_1 6: iload_1 7: ifle 10 10: return LineNumberTable: line 22: 0 line 23: 4 line 24: 6 line 27: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lnet/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig; 6 5 1 i I StackMapTable: number_of_entries = 1 frame_type = 255 /* full_frame */ offset_delta = 10 locals = [ class net/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig, int ] stack = [] 

PS我花了一些时间来解决这个问题,因为默认情况下我使用代码覆盖率运行我的unit testing,而IDEA的代码覆盖工具显然会自动重新计算所有类的stackmap帧,从而解除了我的测试删除它们的努力。