Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What kind of Java code requires stackmap frames?

Tags:

java

bytecode

I'm trying to write a unit tests for a workaround to an issue about missing stackmap frames, but for that purpose I will need to generate a class that will fail to validate on Java 8 if it's missing stackmap frames.

Below you can see my test case (dependencies: ASM, Guava, JUnit). It removes the stackmap frames from the GuineaPig class in hopes of causing its bytecode to fail to validate. The part that I'm having problems with is filling in the TODO in GuineaPig with minimal code that requires stackmap frames, so that the test would pass.

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);
        }
    }
}
like image 470
Esko Luontola Avatar asked Jul 24 '14 07:07

Esko Luontola


1 Answers

Theory

The Java VM Specification §4.10.1 (Verification by Type Checking) specifies that when a stack map frame is required. At first it gives an informal description:

The intent is that a stack map frame must appear at the beginning of each basic block in a method. The stack map frame specifies the verification type of each operand stack entry and of each local variable at the start of each basic block.

A detailed specification is given in §4.10.1.6 (Type Checking Methods with Code). Stack map frames are required by the goto command:

It is illegal to have code after an unconditional branch without a stack map frame being provided for it.

and all other branching commands:

Branching to a target is type safe if the target has an associated stack frame, Frame, and the current stack frame, StackFrame, is assignable to Frame.

Also the beginning of a exception handler needs a stack map frame:

An instruction satisfies an exception handler if the instructions's outgoing type state is ExcStackFrame, and the handler's target (the initial instruction of the handler code) is type safe assuming an incoming type state T.

Finally, §4.10.1.9 (Type Checking Instructions) specifies that which instructions require a branching target with a stack map frame. Look for targetIsTypeSafe in the type rules; the instructions goto, if*, lookupswitch and tableswitch have it.

Example

Even the following code requires stackmap frames:

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

If they are missing, the code will fail with an exception:

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)         

Here is the bytecode:

  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 = []

P.S. It took me some time to figure this out, because by default I run my unit tests with code coverage and IDEA's code coverage tool apparently automatically recalculates the stackmap frames for all classes, which undid my test's efforts to remove them.

like image 82
Esko Luontola Avatar answered Nov 15 '22 13:11

Esko Luontola