ASM 中的栈模型

本文详细解释了ASM中的Label如何与Frame关联,以及inputStack和outputStack在方法执行中的作用。通过实例展示了栈的初始化过程和如何在方法中操作变量,强调了使用STORE和LOAD指令的必要性。
摘要由CSDN通过智能技术生成

Label 介绍

在 ASM 中,每一个 Label 必须对应一个 Frame,两个 Label 可以共享一个 Frame,可以理解为将两个 Label 合并了,而一个 Frame 只对应一个 Label,就是创建它的 Label。每一次定义一个方法,即执行 ClassWriter#visitMethod 方法时,调用 MethodWriter 构造方法,都会在构造方法中创建一个 Label,作为 firstBasicBlock 使用,接着访问切换到这个 Label。

在 Frame 中,会存在

inputLocals : 方法参数存放于此,对应 LOAD 指令

inputStack :类似于一个中转

outputLocals : 各种 STORE 指令会操作这个栈

outputStack :方法的运行操作主要在这个栈中执行,各种指令,其实最后都是转化成了针对这个outputStack 的 pop 和 push 操作

栈的初始化

此处 MethodWriter.compute 为 COMPUTE_ALL_FRAMES

inputLocals: 

inputStack:

  final void setInputFrameFromDescriptor(
      final SymbolTable symbolTable,
      final int access,
      final String descriptor,
      final int maxLocals) {
    inputLocals = new int[maxLocals];
    inputStack = new int[0];
    int inputLocalIndex = 0;
    if ((access & Opcodes.ACC_STATIC) == 0) {
      if ((access & Constants.ACC_CONSTRUCTOR) == 0) {
        inputLocals[inputLocalIndex++] =
            REFERENCE_KIND | symbolTable.addType(symbolTable.getClassName());
      } else {
        inputLocals[inputLocalIndex++] = UNINITIALIZED_THIS;
      }
    }
    for (Type argumentType : Type.getArgumentTypes(descriptor)) {
      int abstractType =
          getAbstractTypeFromDescriptor(symbolTable, argumentType.getDescriptor(), 0);
      inputLocals[inputLocalIndex++] = abstractType;
      if (abstractType == LONG || abstractType == DOUBLE) {
        inputLocals[inputLocalIndex++] = TOP;
      }
    }
    while (inputLocalIndex < maxLocals) {
      inputLocals[inputLocalIndex++] = TOP;
    }
  }

可以看到,非 static 方法,非构造方法,第一个参数对应抽REFERENCE_KIND,表示这个类,即我们常用的 this,接着遍历参数,放入每个参数对应的抽象类型,这点与Java虚拟机栈保持一致。上面描述的方法,会在自定义字节码操作完成,执行 MethodWriter#visitMax 时进行调用。

outputLocals:

  private void setLocal(final int localIndex, final int abstractType) {
    // Create and/or resize the output local variables array if necessary.
    if (outputLocals == null) {
      outputLocals = new int[10];
    }
    int outputLocalsLength = outputLocals.length;
    if (localIndex >= outputLocalsLength) {
      int[] newOutputLocals = new int[Math.max(localIndex + 1, 2 * outputLocalsLength)];
      System.arraycopy(outputLocals, 0, newOutputLocals, 0, outputLocalsLength);
      outputLocals = newOutputLocals;
    }
    // Set the local variable.
    outputLocals[localIndex] = abstractType;
  }

当执行各种 STORE 指令时,会进行 outputLocals 的初始化。

outputStack:

  private void push(final int abstractType) {
    // Create and/or resize the output stack array if necessary.
    if (outputStack == null) {
      outputStack = new int[10];
    }
    int outputStackLength = outputStack.length;
    if (outputStackTop >= outputStackLength) {
      int[] newOutputStack = new int[Math.max(outputStackTop + 1, 2 * outputStackLength)];
      System.arraycopy(outputStack, 0, newOutputStack, 0, outputStackLength);
      outputStack = newOutputStack;
    }
    // Pushes the abstract type on the output stack.
    outputStack[outputStackTop++] = abstractType;
    // Updates the maximum size reached by the output stack, if needed (note that this size is
    // relative to the input stack size, which is not known yet).
    short outputStackSize = (short) (outputStackStart + outputStackTop);
    if (outputStackSize > owner.outputStackMax) {
      owner.outputStackMax = outputStackSize;
    }
  }

当执行涉及到入栈的指令时,例如获取属性 GETFIELD、加载 ALOAD、方法调用 INVOKEVIRTUAL 等指令时,会进行 outputStack 的初始化。

并且从上面可以看到,各种栈都是 int 型,所以当遇到转化为抽象类型为 LONG 和 DOUBLE 的变量类型时,在各种栈中占两位,即 2 个 int,8 个 字节。

模型简介

在 ASM 的栈,分为 inputStack 和 outputStack,outputStack 紧接着 inputStack。还有一个参数outputStackStart,通过这个参数控制 inputStack 的大小,这个参数只能为 0 或 负数,为负数表明用到了上一任 Label 中的变量。

参照源码中对 outputStackStart 的注释:

outputStackStart:输出栈相对于输入栈的起始位置。这个偏移量总是负的或为空。空偏移量意味着输出栈必须追加到输入栈上。-n偏移量意味着前n个输出栈元素必须替换前n个输入栈栈顶元素,而其他元素必须追加到输入栈上。

所以当前 Label 输入栈大小:

numInputStack = inputStack.length + outputStackStart

不同的 Label 之间,通过设置 successor 这种关系,可以使用前任 Label 的输入栈和输出栈。其中,前任的输入栈和输出栈会作为 successor 的输入栈,所以:

前任的输入栈大小:numInputStack

前任的输出栈大小:outputStackTop

successor 的输入栈大小:numInputStack + outputStackTop

应用

利用设置的 successor 关系,操作变量

public class Generate49 implements Opcodes {
    public static void main(String[] args) {
        String generateClassName = "ASM$Generate49";
        ClassLoaderUtils.outputClass(generate(generateClassName), generateClassName);
    }

    private static byte[] generate(String generateClassName) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        // declare_class
        cw.visit(V1_8, ACC_PUBLIC, generateClassName, null, "java/lang/Object", null);
        // declare_field
        cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
        // declare_method
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "toUppercaseName", "()Ljava/lang/String;", null, null);

        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, generateClassName, "name", "Ljava/lang/String;");
        mv.visitInsn(DUP);
        Label l1 = new Label();
        mv.visitJumpInsn(IFNULL, l1);

        mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(String.class), "toUpperCase", "()Ljava/lang/String;", false);
        mv.visitInsn(ARETURN);

        mv.visitLabel(l1);
        mv.visitInsn(ACONST_NULL);
        mv.visitInsn(ARETURN);

        mv.visitMaxs(0, 0);
        return cw.toByteArray();
    }
}

上面代码中的 DUP 指令,就是为了保证在执行 INVOKEVIRTUAL 指令时,可以利用到前任 Label的输出(在执行 visitJumpInsn 时,已经发生了 Label 的切换),即通过 GETFIELD 指令获取到的name 属性,接着执行方法调用。

利用 STORE 操作变量

public class Generate491 implements Opcodes {
    public static void main(String[] args) {
        String generateClassName = "ASM$Generate491";
        ClassLoaderUtils.outputClass(generate(generateClassName), generateClassName);
    }

    private static byte[] generate(String generateClassName) {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        // declare_class
        cw.visit(V1_8, ACC_PUBLIC, generateClassName, null, "java/lang/Object", null);
        // declare_field
        cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
        // declare_method
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "toUppercaseName", "()Ljava/lang/String;", null, null);

        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, generateClassName, "name", "Ljava/lang/String;");
        mv.visitVarInsn(ASTORE, 1);
        Label l1 = new Label();
        mv.visitVarInsn(ALOAD, 1);
        mv.visitJumpInsn(IFNULL, l1);

        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(String.class), "toUpperCase", "()Ljava/lang/String;", false);
        mv.visitInsn(ARETURN);

        mv.visitLabel(l1);
        mv.visitInsn(ACONST_NULL);
        mv.visitInsn(ARETURN);

        mv.visitMaxs(0, 0);
        return cw.toByteArray();
    }
}

可以看到,使用 STORE 命令后,每一次操作使用到变量,都需先进行 LOAD,增加了代码量,但又直观的反映了栈操作的过程,即:先入栈,再出栈。

生成的代码如下:

public class ASM$Generate49 {
    private String name;

    public String toUppercaseName() {
        String var10000 = this.name;
        return var10000 != null ? var10000.toUpperCase() : null;
    }
}

可以看到,即使是很简单的一个类和方法,通过ASM操作起来,代码量都是生成类中代码的好几倍。而为了便于使用,衍生出了 CGLIB 这样的开源项目,其实就是ASM的一个具体应用。

附上上述代码中的工具类ClassLoaderUtils:

public class ClassLoaderUtils extends ClassLoader {

    public static final String CLASS_SUFFIX = ".class";


    public Class<?> defineClass(String name, byte[] bytes) {
        return super.defineClass(name, bytes,0, bytes.length);
    }


    public static void outputClass(byte[] bytes, String name) {
        FileOutputStream fos = null;
        try {
            String pathName = ClassLoaderUtils.class.getResource("/").getPath() + name + CLASS_SUFFIX;
            fos = new FileOutputStream(new File(pathName));
            fos.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潭影空人心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值