我正在尝试为an issue about missing stackmap frames的变通办法编写单元测试,但为此,我将需要生成一个类,如果缺少堆栈映射框架,该类将无法在Java 8上进行验证。

在下面,您可以查看我的测试用例(相关性:ASM,Guava,JUnit)。它从GuineaPig类中删除了堆栈映射框架,以希望其字节码无法通过验证。我遇到的问题是用最少的代码(需要堆栈映射框架)填充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(通过类型检查进行验证)指定何时需要堆栈映射框架。首先,它给出了一个非正式的描述:



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



以及所有其他分支命令:



同样,异常处理程序的开头也需要一个堆栈映射框架:



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



即使以下代码也需要堆栈映射框架:

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

如果缺少它们,则代码将失败并出现异常:
java.lang.VerifyError: Expecting a stackmap frame at branch target 10
Exception Details:
  Location:
    net/orfjackal/retrolambda/Java6MissingStackMapFrameFixerTest$GuineaPig.<init>()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."<init>":()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 = []

附言我花了一些时间弄清楚这一点,因为默认情况下,我以代码覆盖率运行我的单元测试,而IDEA的代码覆盖率工具显然会自动重新计算所有类的堆栈映射框架,这消除了我的测试为删除它们所做的努力。

07-28 01:40