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_classcw.visit(V1_8, ACC_PUBLIC, generateClassName, null, "java/lang/Object", null);// declare_fieldcw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);// declare_methodMethodVisitor 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_classcw.visit(V1_8, ACC_PUBLIC, generateClassName, null, "java/lang/Object", null);// declare_fieldcw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);// declare_methodMethodVisitor 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();}}}
}