我试图为an issue about missing stackmap frames的变通方法编写单元测试,但为了这个目的,我需要生成一个在Java 8上无法验证的类,如果它丢失了stackmap frames。
下面你可以看到我的测试用例(依赖项:ASM,Guava,JUnit)。它从GuineaPig类中删除了stackmap帧,希望导致其字节码无法验证。我遇到问题的部分是用最少的代码填写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);
}
}
}
答案 0 :(得分:11)
Java VM规范§4.10.1(按类型检查验证)指定何时需要堆栈映射帧。首先,它给出了一个非正式的描述:
意图是堆栈映射框必须出现在方法中每个基本块的开头。堆栈映射帧指定每个操作数堆栈条目的验证类型以及每个基本块开头的每个局部变量的验证类型。
详细规范在§4.10.1.6(带代码的类型检查方法)中给出。 goto
命令需要堆栈映射帧:
在没有为其提供堆栈映射帧的情况下,在无条件分支之后使用代码是非法的。
和所有其他分支命令:
如果目标具有关联的堆栈帧Frame,并且当前堆栈帧StackFrame可分配给Frame,则分支到目标是类型安全的。
异常处理程序的开头还需要一个堆栈映射框:
如果指令的传出类型状态是ExcStackFrame,则指令满足异常处理程序,并且处理程序的目标(处理程序代码的初始指令)是类型安全的,假定传入类型状态T. / p>
最后,§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的代码覆盖工具显然会自动重新计算所有类的stackmap框架,从而解除了我的测试工作。删除它们。