如何使用 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是表示局部变量与上一帧相同,操作数栈为空的类型。其他类型包括 appendFramechopFramefullFrame。有关详细信息,请参阅 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();