JVM 中的 If(true)。如何生成合适的指令?
If(true) in JVM. How to generate an appropriate instruction?
我正在尝试生成一个简单的条件跳转指令。这是 class:
public static Class<?> getKlass2(){
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, "m", "()Z",null, null);
Label trueLable = new Label();
Label afterFalseLable = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class), "TRUE", "Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL, getInternalName(Boolean.class), "booleanValue", "()Z", false);
mv.visitJumpInsn(IFEQ, trueLable);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLable);
mv.visitLabel(trueLable);
mv.visitInsn(ICONST_0);
mv.visitFrame(F_APPEND, 0, null, 0, null);
mv.visitLabel(afterFalseLable);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
}
加载 class 时出现以下错误:
Expecting a stackmap frame at branch target 13
Exception Details:
Location:
TestClass.m()Z @6: ifeq
Reason:
Expected stackmap frame at this location.
Bytecode:
0x0000000: b200 0cb6 000f 9900 0704 a700 0403 ac
Stackmap Table:
same_frame_extended(@14)
但是 class 的代码对我来说似乎没问题:
public class TestClass {
public static boolean m();
Code:
0: getstatic #12 // Field java/lang/Boolean.TRUE:Ljava/lang/Boolean;
3: invokevirtual #15 // Method java/lang/Boolean.booleanValue:()Z
6: ifeq 13
9: iconst_1
10: goto 14
13: iconst_0
14: ireturn
}
所以我尝试手动添加一个框架:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
但它也因同样的异常而失败。他们是否希望重新创建具有相同操作数堆栈的帧?
但这似乎没有意义,因为我无法从 Java 代码直接访问 opearand 堆栈。
我做错了什么?
您可以简单地将 COMPUTE_FRAMES
指定给 ClassWriter
的构造函数,让 ASM 为您计算 max stack&locals 和堆栈映射 table 帧条目。正如文档所述,“…computeFrames 意味着 computeMaxs”。
但是,我总是建议尝试理解堆栈映射,因为从头开始计算不仅代价高昂,而且存在基本限制(如 this answer 中详述)。由于您应该已经知道堆栈框架应该是什么样子,因此对这些知识进行编码应该不会太难。由于这也意味着知道局部变量和操作数堆栈条目的最大数量,因此手动指定它们也是一致的。
但是,您尝试的解决方案还差得远:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
F_APPEND
暗示已添加新的 变量 ,这与您添加 堆栈 的明显意图不符入口。此外,仅当您引用 NEW
指令的位置时,将标签指定为堆栈条目才有效,以表示未初始化的对象。但是在这里,您已经推送了一个 INTEGER
值。
正确的代码如下:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame matches the initial frame (no variables, no stack entries)
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, now having an INTEGER on the stack
mv.visitFrame(F_SAME1, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
注意,对于特殊的压缩帧类型,大部分参数都是隐含的,对于F_SAME
,所有其他参数都是无关紧要的,对于F_SAME1
,只有指定的新堆栈条目类型在最后一个参数很重要。
但是你不需要处理不同类型的压缩帧。如果有疑问,您始终可以指定 F_NEW
并提供对假定堆栈框架布局的完整描述。唯一的区别是(稍微)更大的 class 文件。对于动态 class 生成,这可能完全不相关,甚至对于在部署之前添加到应用程序中的生成的 classes,差异也可以忽略不计:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame state is "no variables, no stack entries"
mv.visitFrame(F_NEW, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, frame state is "no variables, one INTEGER on the stack"
mv.visitFrame(F_NEW, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
顺便说一下,我发现将抽象名称生成(如 getInternalName( Boolean.class)
)与硬编码签名(如 "Ljava/lang/Boolean;"
)结合起来有点奇怪。两者都是有效的,但最好始终如一地决定哪种方式。
我正在尝试生成一个简单的条件跳转指令。这是 class:
public static Class<?> getKlass2(){
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, "m", "()Z",null, null);
Label trueLable = new Label();
Label afterFalseLable = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class), "TRUE", "Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL, getInternalName(Boolean.class), "booleanValue", "()Z", false);
mv.visitJumpInsn(IFEQ, trueLable);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLable);
mv.visitLabel(trueLable);
mv.visitInsn(ICONST_0);
mv.visitFrame(F_APPEND, 0, null, 0, null);
mv.visitLabel(afterFalseLable);
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
}
加载 class 时出现以下错误:
Expecting a stackmap frame at branch target 13
Exception Details:
Location:
TestClass.m()Z @6: ifeq
Reason:
Expected stackmap frame at this location.
Bytecode:
0x0000000: b200 0cb6 000f 9900 0704 a700 0403 ac
Stackmap Table:
same_frame_extended(@14)
但是 class 的代码对我来说似乎没问题:
public class TestClass {
public static boolean m();
Code:
0: getstatic #12 // Field java/lang/Boolean.TRUE:Ljava/lang/Boolean;
3: invokevirtual #15 // Method java/lang/Boolean.booleanValue:()Z
6: ifeq 13
9: iconst_1
10: goto 14
13: iconst_0
14: ireturn
}
所以我尝试手动添加一个框架:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
但它也因同样的异常而失败。他们是否希望重新创建具有相同操作数堆栈的帧? 但这似乎没有意义,因为我无法从 Java 代码直接访问 opearand 堆栈。
我做错了什么?
您可以简单地将 COMPUTE_FRAMES
指定给 ClassWriter
的构造函数,让 ASM 为您计算 max stack&locals 和堆栈映射 table 帧条目。正如文档所述,“…computeFrames 意味着 computeMaxs”。
但是,我总是建议尝试理解堆栈映射,因为从头开始计算不仅代价高昂,而且存在基本限制(如 this answer 中详述)。由于您应该已经知道堆栈框架应该是什么样子,因此对这些知识进行编码应该不会太难。由于这也意味着知道局部变量和操作数堆栈条目的最大数量,因此手动指定它们也是一致的。
但是,您尝试的解决方案还差得远:
mv.visitFrame(F_APPEND, 0, new Object[]{ }, 0, new Object[]{ trueLabel });
F_APPEND
暗示已添加新的 变量 ,这与您添加 堆栈 的明显意图不符入口。此外,仅当您引用 NEW
指令的位置时,将标签指定为堆栈条目才有效,以表示未初始化的对象。但是在这里,您已经推送了一个 INTEGER
值。
正确的代码如下:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame matches the initial frame (no variables, no stack entries)
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, now having an INTEGER on the stack
mv.visitFrame(F_SAME1, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
//converting classWriter.toByteArray() to Class<?> instance
注意,对于特殊的压缩帧类型,大部分参数都是隐含的,对于F_SAME
,所有其他参数都是无关紧要的,对于F_SAME1
,只有指定的新堆栈条目类型在最后一个参数很重要。
但是你不需要处理不同类型的压缩帧。如果有疑问,您始终可以指定 F_NEW
并提供对假定堆栈框架布局的完整描述。唯一的区别是(稍微)更大的 class 文件。对于动态 class 生成,这可能完全不相关,甚至对于在部署之前添加到应用程序中的生成的 classes,差异也可以忽略不计:
String className = "TestClass";
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(V1_8, ACC_PUBLIC, className, null, getInternalName(Object.class), null);
MethodVisitor mv = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "m", "()Z",null, null);
Label trueLabel = new Label();
Label afterFalseLabel = new Label();
mv.visitFieldInsn(GETSTATIC, getInternalName(Boolean.class),"TRUE","Ljava/lang/Boolean;");
mv.visitMethodInsn(INVOKEVIRTUAL,getInternalName(Boolean.class),"booleanValue","()Z",false);
mv.visitJumpInsn(IFEQ, trueLabel);
mv.visitInsn(ICONST_1);
mv.visitJumpInsn(GOTO, afterFalseLabel);
// target of IFEQ, the frame state is "no variables, no stack entries"
mv.visitFrame(F_NEW, 0, null, 0, null);
mv.visitLabel(trueLabel);
mv.visitInsn(ICONST_0);
// merge point of the two branches, frame state is "no variables, one INTEGER on the stack"
mv.visitFrame(F_NEW, 0, null, 1, new Object[]{ INTEGER });
mv.visitLabel(afterFalseLabel);
mv.visitInsn(IRETURN);
// no variable at all, at most one stack entry (the integer)
mv.visitMaxs(1, 0);
mv.visitEnd();
顺便说一下,我发现将抽象名称生成(如 getInternalName( Boolean.class)
)与硬编码签名(如 "Ljava/lang/Boolean;"
)结合起来有点奇怪。两者都是有效的,但最好始终如一地决定哪种方式。