ASM

386 阅读5分钟

1 概念

1.1是什么

简单来说, ASM是一个操作Java字节码的类库。

1.2 如何处理

拆分修改合并”。
ASM处理字节码(ByteCode)数据的思路是这样的:

  1. 第一步,将.class文件拆分成多个部分;
  2. 第二步,对某一个部分的信息进行修改;
  3. 第三步,将多个部分重新组织成一个新的.class文件。

1.3 能做什么

  • 父类:修改成一个新的父类
  • 接口:添加一个新的接口、删除已有的接口
  • 字段:添加一个新的字段、删除已有的字段
  • 方法:添加一个新的方法、删除已有的方法、修改已有的方法
  • ……(省略)

2 结构

2.1 两个模型

  • 基于事件的形式 来表示类 (内存低,速度快,重点讲解)
  • 另一个是树 API,以基于对象的形式来表示类。 (内存占用大,类转换方便)

API结构

2.2 事件模型

是用一系列事件来表示的,每个事件表示类的一个元素
流程处理图如下:

这三个类的作用,可以简单理解成这样:

  • ClaassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。
  • ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。
    • FieldVisitor类,负责对字段的信息进行修改。
    • MethodVisitor类,负责对方法的信息进行修改。
  • ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。

3. ClassVisitor

3.1 概述

用于生成和变转已编译类的ASM API是基于ClassVisitor抽象类的。这个类中的每个方法都对应于同名的类文件结构部分
三个基于 ClassVisitor API 的核心组件,用于生成和变化类

  • ClassReader类分析以字节数组形式给出的已编译类,并针对在其accept方法参数 中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看作一个事件产生器。 
  • ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成编 译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用 toByteArray方法来提取。这个类可以看作一个事件使用器。 
  • ClassVisitor类将它收到的所有方法调用都委托给另一个ClassVisitor类。这个 类可以看作一个事件筛选器。 (可以从网络中间人攻击理解)

事件序列图

例子

object ByteCodeModify {
    @JvmStatic
    fun main(args: Array<String>) {
        val readClassFilePath = "buildSrc/build/classes/java/main/ByteCodeTest.class"
        val writeClassFilePath = "buildSrc/src/main/java/ByteCodeTestModify.class"
        // 1 定义ClassReader,并且读取字节码
        val classReader = ClassReader(File(readClassFilePath).inputStream())
        //2 定义中间人ClassWriter,用于获取字节码
        val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
        //3 定义中间人ClassVisitor,这里用系统TraceClassVisitor,并委托给classWriter
        val classVisitor = TraceClassVisitor(classWriter, Textifier(),PrintWriter(System.*out*, true))
        //4 开始事件调用
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES or ClassReader.SKIP_FRAMES)
        File(writeClassFilePath).*outputStream*().write(classWriter.toByteArray())
    }
}

3.2 事件调用顺序

visit visitSource? visitOuterClass? ( visitAnnotation |
   visitAttribute )*
   ( visitInnerClass | visitField | visitMethod )*
   visitEnd

重点关注

public abstract class ClassVisitor {
    public void visit(     // 只调用一次
        final int version,
        final int access,
        final String name,
        final String signature,    
        final String superName,
        final String[] interfaces);
        
    public FieldVisitor visitField( // 访问字段 , 调用一次或多次
        final int access,    
        final String name,
        final String descriptor,   //描述符
        final String signature,
        final Object value);

    public MethodVisitor visitMethod( // 访问方法,调用一次或多次
        final int access,         //访问标识
        final String name,        //名称
        final String descriptor,    //描述符
        final String signature,       //属性表的一部分信息
        final String[] exceptions);    //属性表的一部分信息

    public void visitEnd();    //结束,调用一次
    // ......
}

这里以visitMethod时间分析,对应着class文件的方法属性

method_info {
    u2             access_flags;       //对于visitMethod  access
    u2             name_index;        //对于visitMethod  name
    u2             descriptor_index;  //对于visitMethod  descriptor
    u2             attributes_count;
    attribute_info attributes[attributes_count];  、//对于visitMethod exceptions

}

4 MethodVisitor

4.1 概述

用于生成和转换已编译方法的 ASM API 是基于 MethodVisitor ,它 由 ClassVisitor的 visitMethod 方法返回。

abstract class MethodVisitor { // public accessors ommited MethodVisitor(int api);
    MethodVisitor(int api, MethodVisitor mv);
    AnnotationVisitor visitAnnotationDefault();
    AnnotationVisitor visitAnnotation(String desc, boolean visible); AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);
    void visitAttribute(Attribute attr);
    void visitCode();
    void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);
    void visitInsn(int opcode);
    void visitIntInsn(int opcode, int operand);
    void visitVarInsn(int opcode, int var);
    void visitTypeInsn(int opcode, String desc);
    void visitFieldInsn(int opc, String owner, String name, String desc);
    void visitMethodInsn(int opc, String owner, String name, String desc); void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);
    void visitJumpInsn(int opcode, Label label);
    void visitLabel(Label label);
    void visitLdcInsn(Object cst);
    void visitIincInsn(int var, int increment);
    void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels); void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels); void visitMultiANewArrayInsn(String desc, int dims);
    void visitTryCatchBlock(Label start, Label end, Label handler,
    String type);
    void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);
    void visitLineNumber(int line, Label start);
    void visitMaxs(int maxStack, int maxLocals);
    void visitEnd();
}

从ClassFile的角度来讲,这些visitXxxInsn()方法的本质就是组装instruction的内容。我们可以参考 Java Virtual Machine Specification的 Chapter 6. The Java Virtual Machine Instruction Set部分。

4.2 生成方法体

以一个类的默认构造函数为例

    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    methodVisitor.visitCode();              //方法体的开始
    methodVisitor.visitVarInsn(ALOAD, 0);
    methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitMaxs(1, 1);         //方法体的结束
    methodVisitor.visitEnd();          //方法的结束

visitCode和 visitMaxs 方法可用于检测该方法的字节代码在一个事件序列中的 开始与结束。和类的情况一样,visitEnd 方法也必须在最后调用,用于检测一个方法在一个事件序列中的结束
必须调用3个指令

  • videocode
  • .......
  • videoMaxs
  • visitEnd

4.3 转化方法

方法转化时机 其实就是在ClassVisitorvisitMethod定义新的MethodVisitor对象 MethodVisitor子类

MethodVisitor               //抽象类
    LocalVariablesSorter      //解决局部变量的下标问题
        GeneratorAdapter      //封装指令
            AdviceAdapter     //提供两个方法,方法体的开始和结束

AdciceAdapter,提供两个方法方便插入指令

val newMethodVisitor =
    object : AdviceAdapter(Opcodes.ASM9, methodVisitor, access, name, descriptor) {
        @Override
        override fun onMethodEnter() {
            // 方法开始
            super.onMethodEnter()
            println("aAdviceAdapter  onMethodEnter  name:${name} ")
        }

        @Override
        override fun onMethodExit(opcode: Int) {
            // 方法结束
            super.onMethodExit(opcode)
            println("AdviceAdapter onMethodExit name:${name} opcode:${opcode}")
        }
    }

GeneratorAdapter

LocalVariablesSorter
特点是:可以引入新的局部变量,并且能够对局部变量进行排序 newLocal 方法,它会根据指定的类型创建一个新的本地变量,并直接分配一个本地变量的引用 index,其优势在于可以尽量复用以前的局部变量,而不需要我们考虑本地变量的分配和覆盖问题**

参考

官网
中文使用手册
Java ASM系列