ASM方法转换栈映射帧异常排查求助
问题场景
通过premain方法修改用户字节码,期望在每次遇到LineNumberNode时,将特定字符串形式的信息添加到一个List中,但运行代理和应用JAR包时抛出VerifyError,提示分支目标位置缺少栈映射帧。
异常信息
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target
134
Exception Details:
Location:
sketches/UserCode.main([Ljava/lang/String;)V @24: if_icmpge
Reason:
Expected stackmap frame at this location.
Bytecode:
0x0000000: b200 12b8 0018 b600 1cba 0037 0000 b900
0x0000010: 2e02 0057 033c 1b07 a200 6eb2 0012 b800
0x0000020: 18b6 001c ba00 3a00 00b9 002e 0200 57bb
0x0000030: 0014 59ba 004a 0000 1bb2 0012 b800 18b6
0x0000040: 001c ba00 4d00 00b9 002e 0200 57b8 0053
0x0000050: b700 564d b200 12b8 0018 b600 1cba 0059
0x0000060: 0000 b900 2e02 0057 2cb6 005c b200 12b8
0x0000070: 0018 b600 1cba 0037 0000 b900 2e02 0057
0x0000080: 8401 01a7 ff93 b200 12b8 0018 b600 1cba
0x0000090: 005f 0000 b900 2e02 0057 b1
Stackmap Table:
append_frame(@22,Integer)
chop_frame(@154,1)
相关代码(premain中的transform方法)
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // bootstrap loader is not what we're looking for and will be signified by null if (loader == null) { return null; } ClassNode cn = new ClassNode(ASM9); ClassReader cr1 = new ClassReader(classfileBuffer); cr1.accept(cn, 0); ... // make the synchronized list field cn.fields.add(new FieldNode(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_VOLATILE, listName, "Ljava/util/List;", null, null)); // go through all MethodNodes and all instruction lists in methods and add synchronized list insert op // also, add initialisation for synchronized list (may include creating a static initialiser) for (MethodNode mn : cn.methods) { InsnList insns = mn.instructions; if (insns.size() == 0) { continue; } for (AbstractInsnNode node : insns) { if (node instanceof LineNumberNode) { InsnList addedInsns = new InsnList(); //TODO: fix this addedInsns.add(new FieldInsnNode(GETSTATIC, cn.name, "list", "Ljava/util/List;")); addedInsns.add(new MethodInsnNode(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false)); addedInsns.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Thread", "getId", "()J", false)); addedInsns.add(new InvokeDynamicInsnNode("makeConcatWithConstants", "(J)Ljava/lang/String;", new Handle(H_INVOKESTATIC, "java/lang/invoke/StringConcatFactory", "makeConcatWithConstants", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;", false), "|" + ((LineNumberNode) node).line + "|" + className)); addedInsns.add(new MethodInsnNode(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true)); addedInsns.add(new InsnNode(POP)); insns.insert(node.getPrevious(), addedInsns); } } } ClassWriter cw1 = new ClassWriter(ClassWriter.COMPUTE_MAXS); cn.accept(cw1); return cw1.toByteArray(); }
解决建议
1. 根本原因:栈映射帧未正确生成
Java 7及以上版本默认启用严格字节码验证,要求分支目标(如if_icmpge这类跳转指令的目标位置)必须有栈映射帧。当前使用ClassWriter.COMPUTE_MAXS仅计算操作数栈和局部变量表大小,但不会自动生成栈映射帧,导致验证失败。
2. 直接修复:改用COMPUTE_FRAMES
将ClassWriter的构造参数从COMPUTE_MAXS改为ClassWriter.COMPUTE_FRAMES,它会自动计算栈映射帧,同时包含COMPUTE_MAXS的所有功能:
ClassWriter cw1 = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
3. 额外注意事项
- 静态字段初始化:代码中添加了静态List字段,但未实现初始化逻辑(比如在静态代码块中创建
Collections.synchronizedList(new ArrayList<>())),运行时会抛出NullPointerException,需补充该逻辑。 - 插入指令的栈状态合法性:当前在
LineNumberNode前插入指令,LineNumberNode本身不影响栈状态,所以位置合法,但需确保插入的指令不会破坏后续代码的栈平衡。 - ASM版本兼容性:ASM9对应Java 11+,若目标JDK版本更低,需调整ASM版本以避免API不兼容。
4. 栈映射帧原理补充
栈映射帧是字节码中描述特定位置局部变量表和操作数栈状态的结构,验证器通过它快速校验字节码合法性,无需执行代码。修改字节码(如插入指令、调整跳转)会导致原有帧结构失效,必须重新生成。COMPUTE_FRAMES会遍历方法指令流,自动计算所有必要的栈映射帧。
总结
核心修复是将ClassWriter构造参数改为COMPUTE_FRAMES,同时补充静态List的初始化逻辑,即可解决VerifyError问题。后续处理字节码修改时,只要涉及指令插入或跳转,优先使用COMPUTE_FRAMES自动维护栈映射帧。
内容的提问来源于stack exchange,提问作者Flipz

