美文网首页
ASM系列学习笔记

ASM系列学习笔记

作者: 丹丹无敌 | 来源:发表于2023-04-20 09:58 被阅读0次

    ASM的版本发展

    ASM Release Release Date Java Support
    2.0 2005-05-17 Java 5 language support
    3.2 2009-06-11 support for the new invokedynamic code.
    4.0 2011-10-29 Java 7 language support
    5.0 2014-03-16 Java 8 language support
    6.0 2017-09-23 Java 9 language support
    6.1 2018-03-11 Java 10 language support
    7.0 2018-10-27 Java 11 language support
    7.1 2019-03-03 Java 13 language support
    8.0 2020-03-28 Java 14 language support
    9.0 2020-09-22 Java 16 language support
    9.1 2021-02-06 Java 17 language support

    ASM最重要的三个类关系

    ClassReader、ClassVisitor和ClassWriter类。这三个类的关系,可以描述成下图:


    image.png

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

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

    Java ClassFile

    ClassFile {
        u4             magic;
        u2             minor_version;
        u2             major_version;
        u2             constant_pool_count;
        cp_info        constant_pool[constant_pool_count-1];
        u2             access_flags;
        u2             this_class;
        u2             super_class;
        u2             interfaces_count;
        u2             interfaces[interfaces_count];
        u2             fields_count;
        field_info     fields[fields_count];
        u2             methods_count;
        method_info    methods[methods_count];
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    
    • u1: 表示占用1个字节
    • u2: 表示占用2个字节
    • u4: 表示占用4个字节
    • u8: 表示占用8个字节
      而cp_info、field_info、method_info和attribute_info表示较为复杂的结构,但它们也是由u1、u2、u4和u8组成的。

    在.class文件当中,定义的字段,要遵循field_info的结构。

    field_info {
        u2             access_flags;
        u2             name_index;
        u2             descriptor_index;
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    同样的,在.class文件当中,定义的方法,要遵循method_info的结构。

    method_info {
        u2             access_flags;
        u2             name_index;
        u2             descriptor_index;
        u2             attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    在method_info结构中,方法当中方法体的代码,是存在于Code属性结构中,其结构如下:

    Code_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 max_stack;
        u2 max_locals;
        u4 code_length;
        u1 code[code_length];
        u2 exception_table_length;
        {   u2 start_pc;
            u2 end_pc;
            u2 handler_pc;
            u2 catch_type;
        } exception_table[exception_table_length];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    

    ASM生成新的类学习

    ClassVisitor类

    ClassVisitor是一个抽象类,要想使用它,就必须有具体的子类来继承它。比较常见的ClassVisitor子类有ClassWriter类(Core API)和ClassNode类(Tree API)。

    三个类关系如下:

    • org.objectweb.asm.ClassVisitor
      • org.objectweb.asm.ClassWriter
      • org.objectweb.asm.tree.ClassNode
    public abstract class ClassVisitor {
        protected final int api;
        protected ClassVisitor cv;
    }
    
    • api字段:它是一个int类型的数据,指出了当前使用的ASM API版本,其取值有Opcodes.ASM4、Opcodes.ASM5、Opcodes.ASM6、Opcodes.ASM7、Opcodes.ASM8和Opcodes.ASM9。我们使用的ASM版本是9.0,因此我们在给api字段赋值的时候,选择Opcodes.ASM9就可以了。
    • cv字段:它是一个ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来。
    ClassVisitor类当中,这些visitXxx()方法,遵循一定的调用顺序。这个调用顺序如下:
    visit
    [visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
    (
     visitAnnotation |
     visitTypeAnnotation |
     visitAttribute
    )*
    (
     visitNestMember |
     visitInnerClass |
     visitRecordComponent |
     visitField |
     visitMethod
    )* 
    visitEnd
    

    其中,涉及到一些符号,它们的含义如下:

    • []: 表示最多调用一次,可以不调用,但最多调用一次。
    • ()和|: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序。
    • *: 表示方法可以调用0次或多次。
    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();
        // ......
    }
    

    在ClassVisitor类当中,有许多visitXxx()方法,但是,我们只需要关注这4个方法:visit()、visitField()、visitMethod()和visitEnd()。

    在ClassVisitor的visit()方法、visitField()方法和visitMethod()方法中都带有signature参数。这个signature参数“泛型”密切相关;换句话说,如果处理的是一个带有泛型信息的类、字段或方法,那么就需要给signature参数提供一定的值;如果处理的类、字段或方法不带有“泛型”信息,那么将signature参数设置为null就可以了。

    visit(version, access, name, signature, superName, interfaces)方法的各个参数:
    • version: 表示当前类的版本信息。在上述示例代码中,其取值为Opcodes.V1_8,表示使用Java 8版本。
    • access: 表示当前类的访问标识(access flag)信息。在上面的示例中,access的取值是ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,也可以写成ACC_PUBLIC | ACC_ABSTRACT | ACC_INTERFACE。
    • name: 表示当前类的名字,它采用的格式是Internal Name的形式。
    • signature: 表示当前类的泛型信息。因为在这个接口当中不包含任何的泛型信息,因此它的值为null。
    • superName: 表示当前类的父类信息,它采用的格式是Internal Name的形式。
    • interfaces: 表示当前类实现了哪些接口信息。
    visitField()和visitMethod()方法的各个参数:
    • visitField(access, name, descriptor, signature, value)
    • visitMethod(access, name, descriptor, signature, exceptions)

    这两个方法的前4个参数是相同的,不同的地方只在于第5个参数。

    • access参数:表示当前字段或方法带有的访问标识(access flag)信息,例如ACC_PUBLIC、ACC_STATIC和ACC_FINAL等。
    • name参数:表示当前字段或方法的名字。
    • descriptor参数:表示当前字段或方法的描述符。这些描述符,与我们平时使用的Java类型是有区别的。
    • signature参数:表示当前字段或方法是否带有泛型信息。换句话说,如果不带有泛型信息,提供一个null就可以了;如果带有泛型信息,就需要给它提供某一个具体的值。
    • value参数:是visitField()方法的第5个参数。这个参数的取值,与当前字段是否为常量有关系。如果当前字段是一个常量,就需要给value参数提供某一个具体的值;如果当前字段不是常量,那么使用null就可以了。
    • exceptions参数:是visitMethod()方法的第5个参数。这个参数的取值,与当前方法声明中是否具有throws XxxException相关。
    public class HelloWorld {
        // 这是一个常量字段,使用static、final关键字修饰
        public static final int constant_field = 10;
        // 这是一个非常量字段
        public int non_constant_field;
    
        public void test() throws FileNotFoundException, IOException {
            // do nothing
        }
    }
    

    对于上面的代码,

    • constant_field字段:对应于visitField(ACC_PUBLIC | ACC_FINAL | ACC_STATIC, "constant_field", "I", null, new Integer(10))
    • non_constant_field字段:对应于visitField(ACC_PUBLIC, "non_constant_field", "I", null, null)
    • test()方法:对应于visitMethod(ACC_PUBLIC, "test", "()V", null, new String[] { "java/io/FileNotFoundException", "java/io/IOException" })

    在ClassFile当中,描述符(descriptor)是对“类型”的简单化描述。

    • 对于字段(field)来说,描述符就是对字段本身的类型进行简单化描述。
    • 对于方法(method)来说,描述符就是对方法的接收参数的类型和返回值的类型进行简单化描述。
    Java类型 ClassFile描述符
    boolean Z(Z表示Zero,零表示'false',非零表示'true')
    byte B
    char C
    double D
    float F
    int I
    long J
    short S
    void V
    non-array reference L<InternalName>;
    array reference [

    对字段描述符的举例:

    • boolean flag: Z
    • byte byteValue: B
    • int intValue: I
    • float floatValue: F
    • double doubleValue: D
    • String strValue: Ljava/lang/String;
    • Object objValue: Ljava/lang/Object;
    • byte[] bytes: [B
    • String[] array: [Ljava/lang/String;
    • Object[][] twoDimArray: [[Ljava/lang/Object;

    对方法描述符的举例:

    • int add(int a, int b): (II)I
    • void test(int a, int b): (II)V
    • boolean compare(Object obj): (Ljava/lang/Object;)Z
    • void main(String[] args): ([Ljava/lang/String;)V
    <init>()和<clinit>()方法

    对于一个类(Class)来说,如果没有提供任何构造方法,Java编译器会自动生成一个默认构造方法。在所有的.class文件中,构造方法的名字是<init>()。

    另外,如果在.class文件中包含静态代码块,那么就会有一个<clinit>()方法。

    public class HelloWorld {
        static {
            System.out.println("static code block");
        }
    }
    

    上面的静态代码码,对应于visitMethod(ACC_STATIC, "<clinit>", "()V", null, null)的调用。

    FieldVisitor类

    FieldVisitor类内定义的多个visitXxx()方法,也需要遵循一定的调用顺序,如下所示:

    (
     visitAnnotation |
     visitTypeAnnotation |
     visitAttribute
    )*
    visitEnd
    

    FieldWriter类

    FieldWriter类的父类是FieldVisitor类。需要注意的是,FieldWriter类并不带有public修饰,因此它的有效访问范围只局限于它所处的package当中,不能像其它的public类一样被外部所使用。

    final class FieldWriter extends FieldVisitor {
    }
    

    MethodVisitor类

    在MethodVisitor类当中,定义了许多的visitXxx()方法,这些方法的调用,也要遵循一定的顺序。

    (visitParameter)*
    [visitAnnotationDefault]
    (visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
    [
        visitCode
        (
            visitFrame |
            visitXxxInsn |
            visitLabel |
            visitInsnAnnotation |
            visitTryCatchBlock |
            visitTryCatchAnnotation |
            visitLocalVariable |
            visitLocalVariableAnnotation |
            visitLineNumber
        )*
        visitMaxs
    ]
    visitEnd
    

    我们可以把这些visitXxx()方法分成三组:

    • 第一组,在visitCode()方法之前的方法。这一组的方法,主要负责parameter、annotation和attributes等内容。
    • 第二组,在visitCode()方法和visitMaxs()方法之间的方法。这一组的方法,主要负责当前方法的“方法体”内的opcode内容。其中,visitCode()方法,标志着方法体的开始,而visitMaxs()方法,标志着方法体的结束。
    • 第三组,是visitEnd()方法。这个visitEnd()方法,是最后一个进行调用的方法。

    对这些visitXxx()方法进行精简之后,内容如下:

    [
        visitCode
        (
            visitFrame |
            visitXxxInsn |
            visitLabel |
            visitTryCatchBlock
        )*
        visitMaxs
    ]
    visitEnd
    

    这些方法的调用顺序,可以记忆如下:

    • 第一步,调用visitCode()方法,调用一次。
    • 第二步,调用visitXxxInsn()方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
    • 第三步,调用visitMaxs()方法,调用一次。
    • 第四步,调用visitEnd()方法,调用一次。
    需要注意的一点,ClassVisitor类有自己的visitXxx()方法,MethodVisitor类也有自己的visitXxx()方法,两者是不一样的,要注意区分。另外,ClassVisitor.visitMethod()方法提供的是“方法声明”所需要的信息,它会返回一个MethodVisitor对象,这个MethodVisitor对象就用来实现“方法体”里面的代码逻辑。

    MethodWriter类

    MethodWriter类的父类是MethodVisitor类。需要注意的是,MethodWriter类并不带有public修饰,因此它的有效访问范围只局限于它所处的package当中,不能像其它的public类一样被外部所使用。

    final class MethodWriter extends MethodVisitor {
    }
    

    方法的初始Frame

    在方法刚开始的时候,operand stack是空,不需要存储任何的数据,而local variables的初始状态,则需要考虑三个因素:

    • 当前方法是否为static方法。如果当前方法是non-static方法,则需要在local variables索引为0的位置存在一个this变量;如果当前方法是static方法,则不需要存储this。
    • 当前方法是否接收参数。方法接收的参数,会按照参数的声明顺序放到local variables当中。
    • 方法参数是否包含long或double类型。如果方法的参数是long或double类型,那么它在local variables当中占用两个位置。

    static方法

    假设HelloWorld当中有一个静态add(int, int)方法,如下所示:

    public class HelloWorld {
        public static int add(int a, int b) {
            return a + b;
        }
    }
    

    我们可以通过运行HelloWorldFrameCore类,来查看add(int, int)方法的初始Frame:

    [int, int] []
    

    在上面的结果中,第一个[]中存放的是local variables的数据,在第二个[]中存放的是operand stack的数据。

    该方法包含的Instruction内容如下(使用javap -c HelloWorld命令查看):

    public static int add(int, int);
      Code:
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
    

    该方法整体的Frame变化如下:

    add(II)I
    [int, int] []
    [int, int] [int]
    [int, int] [int, int]
    [int, int] [int]
    [] []
    

    non-static方法

    假设HelloWorld当中有一个非静态add(int, int)方法,如下所示:

    public class HelloWorld {
        public int add(int a, int b) {
            return a + b;
        }
    }
    

    我们可以通过运行HelloWorldFrameCore类,来查看add(int, int)方法的初始Frame:

    [***/HelloWorld, int, int] []
    

    该方法包含的Instruction内容如下:

    public int add(int, int);
      Code:
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
    

    该方法整体的Frame变化如下:

    add(II)I
    [sample/HelloWorld, int, int] []
    [sample/HelloWorld, int, int] [int]
    [sample/HelloWorld, int, int] [int, int]
    [sample/HelloWorld, int, int] [int]
    [] []
    

    long和double类型

    假设HelloWorld当中有一个非静态add(long, long)方法,如下所示:

    public class HelloWorld {
        public long add(long a, long b) {
            return a + b;
        }
    }
    

    我们可以通过运行HelloWorldFrameCore类,来查看add(long, long)方法的初始Frame:

    [sample/HelloWorld, long, top, long, top] []
    

    该方法包含的Instruction内容如下:

    public long add(long, long);
      Code:
         0: lload_1
         1: lload_3
         2: ladd
         3: lreturn
    

    该方法整体的Frame变化如下:

    add(JJ)J
    [sample/HelloWorld, long, top, long, top] []
    [sample/HelloWorld, long, top, long, top] [long, top]
    [sample/HelloWorld, long, top, long, top] [long, top, long, top]
    [sample/HelloWorld, long, top, long, top] [long, top]
    [] []
    

    方法初始的Frame总结:

    • 第一点,在JVM当中,每一个方法的调用都会分配一个Stack Frame内存空间;在Stack Frame内存空间当中,有local variables和operand stack两个重要结构;在Java文件进行编译的时候,方法对应的local variables和operand stack的大小就决定了。
    • 第二点,如何计算方法的初始Frame。在方法刚开始的时候,Stack Frame中的operand stack是空的,而只需要计算local variables的初始状态;而计算local variables的初始状态,则需要考虑当前方法是否为static方法、是否接收方法参数、方法参数中是否有long和double类型。

    相关文章

      网友评论

          本文标题:ASM系列学习笔记

          本文链接:https://www.haomeiwen.com/subject/mderjdtx.html