什么样的 Java 代码需要堆栈映射帧?理论例

2022-09-03 02:25:37

我正在尝试编写一个单元测试来解决有关缺少堆栈映射帧的问题,但为此,我需要生成一个类,如果它缺少堆栈映射帧,则该类将无法在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);
        }
    }
}

答案 1

理论

Java VM 规范 §4.10.1(通过类型检查进行验证)指定了何时需要堆栈地图框。起初,它给出了一个非正式的描述:

其目的是堆栈地图框必须显示在方法中每个基本块的开头。堆栈地图框指定每个操作数堆栈条目的验证类型以及每个基本块开头的每个局部变量的验证类型。

§4.10.1.6(使用代码的类型检查方法)中给出了详细的规范。以下命令需要堆叠地图框:goto

在没有为其提供堆栈地图框的情况下,在无条件分支之后使用代码是非法的。

和所有其他分支命令:

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

此外,异常处理程序的开头需要一个堆栈地图框:

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

最后,§4.10.1.9(类型检查说明)指定了哪些指令需要具有堆栈地图框的分支目标。在类型规则中查找;说明 ,,并拥有它。targetIsTypeSafegotoif*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的代码覆盖率工具显然会自动重新计算所有类的堆栈映射帧,这取消了我的测试删除它们的努力。


答案 2

推荐