Какому Java-коду нужны фреймы стоп-кадров?

Я пытаюсь написать единичные тесты для обходного пути проблемы с отсутствующими кадрами стоп-кадров, но для этого мне нужно будет создать класс, который будет не удается проверить на Java 8, если у него отсутствуют фреймы в стеке.

Ниже вы можете увидеть мой тестовый пример (зависимости: ASM, Guava, JUnit). Он удаляет фреймы стека из класса GuineaPig в надежде заставить его байт-код не проверять. Часть, с которой у меня возникают проблемы, заполняет TODO в GuineaPig минимальным кодом, который требует фреймов стоп-кадров, чтобы тест прошел.

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 (Инструкции по проверке типов) указывает, какие инструкции требуют целевого объекта ветвления с фреймом карты стека. Ищите targetIsTypeSafe в правилах типа; инструкции goto, if*, lookupswitch и tableswitch имеют его.

Пример

Даже для следующего кода требуются фреймы стоп-кадров:

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

P.S. Мне потребовалось некоторое время, чтобы понять это, потому что по умолчанию я запускаю свои модульные тесты с охватом кода, а инструмент покрытия кода IDEA, по-видимому, автоматически пересчитывает фреймы стека для всех классов, что уменьшает мои усилия по тестированию, чтобы удалить их.