如何使用 Javassist 生成循环字节码?
How to generate looping bytecode using Javassist?
我正在尝试为一种编译为 Java 字节码的深奥编程语言编写编译器。我正在尝试使用 Javassist 生成字节码。
我在尝试生成 branching/looping 代码时卡住了。例如,假设我正在生成代码:
while (true) System.out.println("Hello World!");
这是我的尝试:
var mainClass = new ClassFile(false, "Main", null);
var constPool = mainClass.getConstPool();
var mainMethodCode = new Bytecode(constPool);
int label = mainMethodCode.currentPc();
mainMethodCode.addGetstatic(ClassPool.getDefault().get("java.lang.System"), "out", "Ljava/io/PrintStream;");
mainMethodCode.addLdc("Hello World!");
mainMethodCode.addInvokevirtual("java.io.PrintStream", "println", "(Ljava/lang/String;)V");
mainMethodCode.addOpcode(Opcode.GOTO);
// I know that branch instructions take a PC-relative offset
// and after some trial and error, this seems to be the correct formula
var offset = label - mainMethodCode.currentPc() + 1;
mainMethodCode.addIndex(offset);
mainMethodCode.setMaxLocals(1);
var mainMethodInfo = new MethodInfo(constPool, "main", "([Ljava/lang/String;)V");
mainMethodInfo.setCodeAttribute(mainMethodCode.toCodeAttribute());
mainClass.addMethod(mainMethodInfo);
mainClass.setAccessFlags(AccessFlag.PUBLIC);
mainMethodInfo.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
ClassPool.getDefault().makeClass(mainClass).writeFile(...);
通过检查 class 文件,我可以看到生成了预期的字节码:
Code:
stack=2, locals=1, args_size=1
0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14 // String Hello World!
5: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: goto 0
但是,当 运行 使用 java Main
连接 class 文件时,我得到一个 VerifyError
。我显然需要将 goto
目标添加到堆栈映射中(无论那意味着什么)。
Expecting a stackmap frame at branch target 0
我在 Javassist 中找到了一个 StackMap.Writer
class,所以我尝试了
var stackMap = new StackMap.Writer();
stackMap.write16bit(label); // does this add 0 (the value of label) to the stack map?
...
var codeAttr = mainMethodCode.toCodeAttribute();
codeAttr.setAttribute(stackMap.toStackMap(constPool));
mainMethodInfo.setCodeAttribute(codeAttr);
...
但是,当我尝试 运行 class 时,同样的 VerifyError
发生了。
在 Javassist 中生成分支代码的预期方式是什么?
多亏了 ,我才知道我实际上需要 StackMapTable
,而不是 StackMap
。我确实需要在每个分支目的地都有一个堆栈映射 table 条目。
var stackMap = new StackMapTable.Writer(0);
stackMap.sameFrame(0);
// ...
var codeAttr = mainMethodCode.toCodeAttribute();
codeAttr.setAttribute(stackMap.toStackMapTable(constPool));
请注意,有不同类型的帧,它们都对操作数堆栈和可用的局部变量有不同的说明。 sameFrame
是表示局部变量与上一帧相同,操作数栈为空的类型。其他类型包括 appendFrame
、chopFrame
、fullFrame
。有关详细信息,请参阅 JVMS 4.7.4。
通常,传递给 sameFrame
的参数 0
不是我希望应用堆栈映射 table 条目的字节码偏移量。相反,它是“偏移增量”。该条目适用的字节码偏移量是通过将 (offset delta + 1) 添加到前一帧适用的字节码偏移量来计算的。 仅针对第一帧, 偏移量增量与其应用的字节码偏移量相同。
ASM 库似乎更适合 table 这样的字节码生成。我不需要手动计算 PC 偏移量。它甚至可以选择 (COMPUTE_FRAMES
) 来确定您应该使用哪种类型的框架,但要以性能为代价。
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_6,
ACC_PUBLIC + ACC_SUPER,
"Main",
null,
"java/lang/Object",
null);
cw.visitSource("Main.java", null);
var mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
"main",
"([Ljava/lang/String;)V",
null,
null);
mv.visitCode();
var start = new Label();
mv.visitLabel(start);
mv.visitFieldInsn(GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false);
mv.visitJumpInsn(GOTO, start);
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
var bytes = cw.toByteArray();
var stream = new FileOutputStream("...");
stream.write(bytes);
stream.close();
我正在尝试为一种编译为 Java 字节码的深奥编程语言编写编译器。我正在尝试使用 Javassist 生成字节码。
我在尝试生成 branching/looping 代码时卡住了。例如,假设我正在生成代码:
while (true) System.out.println("Hello World!");
这是我的尝试:
var mainClass = new ClassFile(false, "Main", null);
var constPool = mainClass.getConstPool();
var mainMethodCode = new Bytecode(constPool);
int label = mainMethodCode.currentPc();
mainMethodCode.addGetstatic(ClassPool.getDefault().get("java.lang.System"), "out", "Ljava/io/PrintStream;");
mainMethodCode.addLdc("Hello World!");
mainMethodCode.addInvokevirtual("java.io.PrintStream", "println", "(Ljava/lang/String;)V");
mainMethodCode.addOpcode(Opcode.GOTO);
// I know that branch instructions take a PC-relative offset
// and after some trial and error, this seems to be the correct formula
var offset = label - mainMethodCode.currentPc() + 1;
mainMethodCode.addIndex(offset);
mainMethodCode.setMaxLocals(1);
var mainMethodInfo = new MethodInfo(constPool, "main", "([Ljava/lang/String;)V");
mainMethodInfo.setCodeAttribute(mainMethodCode.toCodeAttribute());
mainClass.addMethod(mainMethodInfo);
mainClass.setAccessFlags(AccessFlag.PUBLIC);
mainMethodInfo.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
ClassPool.getDefault().makeClass(mainClass).writeFile(...);
通过检查 class 文件,我可以看到生成了预期的字节码:
Code:
stack=2, locals=1, args_size=1
0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14 // String Hello World!
5: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: goto 0
但是,当 运行 使用 java Main
连接 class 文件时,我得到一个 VerifyError
。我显然需要将 goto
目标添加到堆栈映射中(无论那意味着什么)。
Expecting a stackmap frame at branch target 0
我在 Javassist 中找到了一个 StackMap.Writer
class,所以我尝试了
var stackMap = new StackMap.Writer();
stackMap.write16bit(label); // does this add 0 (the value of label) to the stack map?
...
var codeAttr = mainMethodCode.toCodeAttribute();
codeAttr.setAttribute(stackMap.toStackMap(constPool));
mainMethodInfo.setCodeAttribute(codeAttr);
...
但是,当我尝试 运行 class 时,同样的 VerifyError
发生了。
在 Javassist 中生成分支代码的预期方式是什么?
多亏了 StackMapTable
,而不是 StackMap
。我确实需要在每个分支目的地都有一个堆栈映射 table 条目。
var stackMap = new StackMapTable.Writer(0);
stackMap.sameFrame(0);
// ...
var codeAttr = mainMethodCode.toCodeAttribute();
codeAttr.setAttribute(stackMap.toStackMapTable(constPool));
请注意,有不同类型的帧,它们都对操作数堆栈和可用的局部变量有不同的说明。 sameFrame
是表示局部变量与上一帧相同,操作数栈为空的类型。其他类型包括 appendFrame
、chopFrame
、fullFrame
。有关详细信息,请参阅 JVMS 4.7.4。
通常,传递给 sameFrame
的参数 0
不是我希望应用堆栈映射 table 条目的字节码偏移量。相反,它是“偏移增量”。该条目适用的字节码偏移量是通过将 (offset delta + 1) 添加到前一帧适用的字节码偏移量来计算的。 仅针对第一帧, 偏移量增量与其应用的字节码偏移量相同。
ASM 库似乎更适合 table 这样的字节码生成。我不需要手动计算 PC 偏移量。它甚至可以选择 (COMPUTE_FRAMES
) 来确定您应该使用哪种类型的框架,但要以性能为代价。
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_6,
ACC_PUBLIC + ACC_SUPER,
"Main",
null,
"java/lang/Object",
null);
cw.visitSource("Main.java", null);
var mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
"main",
"([Ljava/lang/String;)V",
null,
null);
mv.visitCode();
var start = new Label();
mv.visitLabel(start);
mv.visitFieldInsn(GETSTATIC,
"java/lang/System",
"out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello World!");
mv.visitMethodInsn(INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false);
mv.visitJumpInsn(GOTO, start);
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
var bytes = cw.toByteArray();
var stream = new FileOutputStream("...");
stream.write(bytes);
stream.close();