Java ASM GeneratorAdapter变量命名

Java ASM GeneratorAdapter variable naming

我正在生成一个简单的 class,但无法注入正确的变量名。 ASM 版本为 5.2.

代码如下:

package com.test;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {

    public static void main(String[] args) throws Exception {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        String name = "com.test.Sub";
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name.replace('.', '/'), null, "java/lang/Object", null);
        Method ctor = Method.getMethod("void <init>()");
        GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, ctor, null, null, cw);
        mg.visitCode();
        mg.loadThis();
        mg.invokeConstructor(Type.getType(Object.class), ctor);
        int var = mg.newLocal(Type.INT_TYPE);
        mg.push(42.42);
        mg.storeLocal(var);
        Label varLabel = mg.mark();
        mg.returnValue();
        Label endLabel = mg.mark();
        mg.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
        mg.endMethod();
        cw.visitEnd();
        byte[] bytes = cw.toByteArray();
        Files.write(Paths.get(name + ".class"), bytes);
    }

}

我正在使用 GeneratorAdapter 来简化代码生成。由于 GeneratorAdapter 是从 LocalVariablesSorter 继承的,我假设允许使用它的 newLocal(Type) 方法。

除了变量名外,发出的字节码没有任何问题。当调用 visitLocalVariable() 方法时,它不会为变量分配名称,而是在字节码中创建一个新名称。

发出的字节码:

// class version 52.0 (52)
// access flags 0x1
public class com/test/Sub {
  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    LDC 42.42
    DSTORE 1
   L0
    RETURN
   L1
    LOCALVARIABLE x D L0 L1 3
    MAXSTACK = 2
    MAXLOCALS = 5
}

我正在使用 newLocal()visitLocalVariable() 中调用提供的相同变量索引。但是在字节码映射索引中是 3 而不是 1。如果变量具有 "shorter" 类型,例如 int,那么索引将是 2,但仍然不是它应该的 1

根据我的观察,发生这种情况的原因如下。 LocalVariablesSorter 维护从旧变量索引到新变量索引的映射。它还会覆盖方法 visitLocalVariable,并且在将调用委托给访问者链之前,它会根据映射计算 newIndexnewIndex 是通过另一个私有方法 remap() 计算得出的。此方法检查给定变量的映射是否已存在,如果不存在,则创建一个新映射。我看到的问题是 newLocal() 方法没有向映射添加任何内容。

我还可以从 ASM 源中看到 GeneratorAdapter 中的 storeInsn() 委托 visitVarInsn() 调用链而不是调用 LocalVariablesSorter 的实现。因为它在 LocalVariablesSorter 实现中,所以为变量索引调用 remap() 方法并更新映射。

因此我的问题是如何使用 GeneratorAdapter 以便在发出的字节码中正确命名变量,或者如何在链中将 GeneratorAdapterLocalVariablesSorter 组合以便它们一起正常工作?

由于 GeneratorAdapter extends LocalVariablesSorter,其目的是适应所有访问者调用,所有属于访问者 API 的方法都得到适应,这与引入的专用方法不同GeneratorAdapter。这种设计允许将新代码插入现有方法,其中旧代码通过访问者报告 API.

因此方法 visitLocalVariable 是访问者 API 的一部分,必须在目标 MethodVisitor 上调用,绕过 LocalVariablesSorter:

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
String name = "com.test.Sub";
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
         name.replace('.', '/'), null, "java/lang/Object", null);
Method ctor = Method.getMethod("void <init>()");
MethodVisitor direct = cw.visitMethod(
         Opcodes.ACC_PUBLIC, ctor.getName(), ctor.getDescriptor(), null, null);
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC, ctor, direct);
mg.visitCode();
mg.loadThis();
mg.invokeConstructor(Type.getType(Object.class), ctor);
int var = mg.newLocal(Type.DOUBLE_TYPE);
mg.push(42.42);
mg.storeLocal(var);
Label varLabel = mg.mark();
mg.returnValue();
Label endLabel = mg.mark();
direct.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
mg.endMethod();
cw.visitEnd();
byte[] bytes = cw.toByteArray();
Files.write(Paths.get(name + ".class"), bytes);

因为这可能会造成混淆,这里的替代方案直接在目标 MethodVisitor 上工作,完全没有像 GeneratorAdapter 这样的任何便利包装。它并不复杂,尽管它需要更多的知识,但是,开发人员在处理 Java 字节码和 class 文件时无论如何都应该掌握这些知识……

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
String name = "com.test.Sub";
String superClName = "java/lang/Object", ctorName = "<init>", ctorDesc = "()V";
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name.replace('.','/'), null, superClName, null);
MethodVisitor direct = cw.visitMethod(Opcodes.ACC_PUBLIC, ctorName, ctorDesc, null, null);
direct.visitCode();
// "this" is alway 0 (zero) and for parameterless methods the next var location is 1 (one)
int thisVar = 0, var = 1;
direct.visitVarInsn(Opcodes.ALOAD, thisVar);
direct.visitMethodInsn(Opcodes.INVOKESPECIAL, superClName, ctorName, ctorDesc, false);
direct.visitLdcInsn(42.42);
Label varLabel = new Label(), endLabel = new Label();
direct.visitVarInsn(Opcodes.DSTORE, var);
direct.visitLabel(varLabel);
direct.visitInsn(Opcodes.RETURN);
direct.visitLabel(endLabel);
direct.visitLocalVariable("x", "D", null, varLabel, endLabel, var);
direct.visitMaxs(-1, -1);// no actual values, using COMPUTE_FRAMES
direct.visitEnd();
cw.visitEnd();
byte[] bytes = cw.toByteArray();
Files.write(Paths.get(name + ".class"), bytes);

如果您不习惯直接将 ()V 用于无参数 void 方法,您仍然可以像以前一样使用 Method 对象或 Type.getMethodDescriptor(Type.VOID_TYPE)