字节码操作工具ASM API使用大全

3 篇文章 1 订阅

一、前言

        这篇文章主要介绍ASM的api使用和一些JVM的字节码知识。基本上每一个方法都是对应了一个或者多个JVM指令,所以需要的时候查查文档就行了。

先上一个思维导图,大致看下每个类的作用。

二、ClassReader

        对Class文件进行读取的Class,包括对里面的代码进行扫描。接下来我们看几个核心的方法

2.1 构造方法

作用:获取Class文件,输入源很多种,包括字节流,IO流或者直接加载Class。

  public ClassReader(final InputStream inputStream)

2.2  accept

  public void accept(final ClassVisitor classVisitor, final int parsingOptions)
  •  classVisitor:传入一个ClassVisitor类即可
  • parsingOptons: 用于跳过读取字节码时的一些信息选项,有以下四种选择可以选择
SKIP_CODE表示跳过代码扫描,如果你只需要只是类的结构,就可以使用这个。
SKIP_DEBUG

跳过调试信息,ClassReader不会去访问调试信息。

如果设置了这个标志,这些属性既不会被解析也不会被访问(例如ClassVisitor.visitSource, MethodVisitor.visitLocalVariable ,

MethodVisitor.visitLineNumber

MethodVisitor.visitParameter)。  

这个会比较常用,当你不需要上面这些方法时候。

SKIP_FRAMES

跳过堆栈映射帧.

如果设置了这个标志,这些属性既不会被解析也不会被访问(例如:MethodVisitor.visitFrame).

这个标志当ClassWriter.COMPUTE_FRAME选项被使用时,它会避免访问将被忽略并从头重新计算的帧。 

EXPAND_FRAMES用来展开堆栈映射帧的标志,会降低性能

三、ClassWriter

        很明显,这是一个对Class进行写入操作的类,例如对Class文件添加变量,方法。我们先看看他的构造方法。

3.1 构造方法

作用:用来定义类的属性

ClassWriter classWriter = new ClassWriter(0);
public ClassWriter(final int flags) 

关于flags的传值如下:

  • flag == 0, 你必须自己计算栈帧和局部变量以及操作数堆栈的大小 ,也就是你要自己调用visitmax和visitFrame方法。
  • flag == ClassWriter. COMPUTE_MAXS,局部变量和操作数堆栈部分的大小会为你计算,还需要调用visitFrame方法设置栈帧。
  • flag == ClassWriter.COMPUTE_FRAMES,所有的内容都是自动计算的。 你不必调用visitFrame和visitmax。换句话说,COMPUTE_FRAMES包含COMPUTE_MAXS

注意:使用ClassWriter. COMPUTE_MAXS他会使得ClassWriter的速度慢10%,ClassWriter.COMPUTE_FRAMES会慢20%。

3.2 visit方法 

作用:用来定义类的属性

方法使用如下:

例如:生成一个User类。

        classWriter.visit(
                V1_8,
                ACC_PUBLIC | ACC_SUPER,
                "asm/User",
                null,
                "java/lang/Object",
                null);

 源代码如下: 

public final void visit(
      final int version,
      final int access,
      final String name,
      final String signature,
      final String superName,
      final String[] interfaces)

参数解释:  

  • version:Java版本号,例如 V1_8 代表Java 8
  • access:Class访问权限,一般默认都是 ACC_PUBLIC | ACC_SUPER,部分字段解释如下

  • name:  Class文件名,例如:asm/User,包名加类名
  • signature:类的签名,除非你是泛型类或者实现泛型接口,一般默认null。
  • superName:继承的类,很明显所有类默认继承Object。例如:java/lang/Object ,如果是继承自己写的类Animal,那就是 asm/Animal
  • interfaces:实现的接口,例如实现自己写的接口IPrint,那就是new String[]{"asm/IPrint"}

3.3 visitMethod方法 

作用:用来定义类的方法

方法使用如下:

例如:生成User的构造方法 public User(){ }

classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);

 源代码如下:

  public final MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions)

参数解释: 

  • access:方法的访问权限,也就是public,private等
  • name:  方法名

        在Class中,有两个特殊方法名。<init><clinit>,其他的方法都是正常对应Java方法名。

        <init>代表的是类的实例构造方法初始化,也就是new 一个类时,肯定会调用的方法。

        <clinit>代表的类的初始化方法,不同于<init>,它不是显示调用的。因为Java虚拟机会自动调用<clinit>,并且保证在子类的<clinit>前调用父类的<clinit>。也就是说,Java虚拟机中,第一个被执行<clinit>方法的肯定是java.lang.Object。

        注意:<init><clinit>执行的时机不一样,<clinit>的时机早于<init>,<clinit>是在类被加载阶段的初始化过程中调用<clinit>方法,而<init>方法的调用是在new一个类的实例的时候。

  • descriptor:方法的描述符

        所谓方法的描述符,就是字节码对代码中方法的形参和返回值的一个描述。其实就是一个一一对应的模板。如下:

                   (IF)V =(表示方法的形参类型描述符)方法的返回值

                关于形参的类型描述符如下:

Java类型类型描述符
booleanZ        
char       C
byteB
shortS
intI
floatF
longJ
doubleD
ObjectLjava/lang/Object; (L + 包名+类名 + ; )
int [ ][I ( [ + I )
Object [ ] [ ][[Ljava/lang/Object; ( [ + [ + L + 包名 + 类名 +  ; )
User(自定义User类)Lasm/User; (L + 包名 + 类名 +  ; )

                方法描述符如下: 

代码中方法的声明方法描述符
void m(int i,float f)(IF)V
int m(Object o)(Ljava/lang/Object;)I
int[] m(int i,String s)(Ljava/lang/String;)[I
Object m(int [ ] i)([I)Ljava/lang/Object;
  • signature:方法签名,除非方法的参数、返回类型和异常使用了泛型,否则一般为 null。
  • exceptions:方法上的异常。这里我们没有抛出任何异常,所以为null。如果throws Exception,exceptions的值为:new String[]{"java/lang/Exception"}

3.4 visitField方法 

作用:用来定义一个变量

方法使用如下:

例如:生成一个  private int a = 10;

classWriter.visitField(ACC_PRIVATE, "a", "I", null, null);

 源代码如下: 

  public final FieldVisitor visitField(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final Object value) 

 参数解释: 

  • access:变量的访问权限,,也就是public,private等
  • name:变量名
  • descriptor:变量的描述符,可以参考上面的Java类型对应的描述符
  • signature:变量的签名,如果没有使用泛型则为null
  • value:变量的初始值。这个字段仅作用于被final修饰的字段,或者接口中声明的变量。其他默认为null,变量的赋值是通过MethodVisitor 的 visitFieldInsn方法。

3.5 visitEnd方法 

作用:用来通知Class已经使用完。

3.6 toByteArray方法 

作用:返回一个字节数组

 四、ClassVisitor

        看名字就能看出来,这是一个对Class文件进行观察(扫描)的工具类。因为它是一个抽象类,所以我们只能对其进行继承重写。

并且ClassVisitor所有的调用都是由ClassReader来进行回调,就是我们前面accept方法。而这个方法里,执行了对ClassVisitor源码扫描的回调,截图如下:

接下来我们看看ClassVisitor的构造方法

4.1 构造方法

  protected ClassVisitor(final int api, final ClassVisitor classVisitor)

参数解释:

  • api:代表是ASM API的版本 ,必须是如下的版本号,默认填最新(ASM9)即可。

        

      注意:编写代码和代码读取的版本号最好保持一致,不然可能会有一些兼容性的错误,例如下面,一些API版本的判断。

  • classVisitor:传一个ClassVisitor的子类,对字节码修改的内容进行保存。

上面这个解释可能会让你感到不可思议,我们可以先看我们上面代码这个参数传的是ClassWriter的实例,也就是说ClassWriter继承于ClassVisitor。其实也不难理解,这里ClassWriter的作用就是保存我们修改的信息,然后重新输出字节码,因为ClassVisitor就是一个纯粹的代码扫描类(可以自行去看他的源码,没有保存修改代码的地方)。那么这个字段的作用其实就是用来对字节码修改进行保存的,我们简单来看下ClassVisitor某个方法的回调。例如:visitMethod方法,因为我们就是在这里进行字节码修改的

  public MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    if (cv != null) {
      return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
    return null;
  }

可以看到,方法最终的调用都给了cv,而这个cv就是我们传进来的ClassWriter 。不仅是这个方法,在ClassVisitor中,除了构造方法外,其他所有的方法最终的执行都是给了cv,也就是说,ClassVisitor就是一个代理类。

接下来介绍几个常用回调方法。

4.2 visitMethod

作用:回调Class中的每一个方法

源代码如下:

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }

参数的解释这里就不过说明了,在上章节中有具体说明,这里说下如何使用来修改字节码。

其实也很简单,固定用法,删除某个方法

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            System.out.println("读到Class的一个方法visitMethod: " + name);
            if (name.equals("test")){
                //删除test方法
                return null;
            }
            return  super.visitMethod(access, name, descriptor, signature, exceptions);
        }

想对方法体修改:

  MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);

为什么需要拿到super.visitMethod呢? 

因为这个拿到的methodVisitor不就是我们传入的ClassWriter的MethodVisitor,不拿到它,最后怎么把代码合成一份。

4.3 visitField

作用:回调Class中的每一个变量

使用如下,删除/修改一个变量

        public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
            System.out.println("读到Class的一个变量visitField: " + name);
            if (name.equals("a")){
                //删除a变量
                return null;
            }
            if (name.equals("b")){
                //修改b变量名词为d,值为10
                return super.visitField(access, "d", descriptor, signature, 10);
            }
            return super.visitField(access, name, descriptor, signature, value);
        }

 添加一个变量

        @Override
        public void visitEnd() {
            super.visitEnd();
            FieldVisitor fv = cv.visitField(Opcodes.ACC_PRIVATE, "e", "I", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }

4.4 visitAnnotation

作用:回调Class上的每一个注解

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            return super.visitAnnotation(descriptor, visible);
        }

参数解释:

  • descriptor:注解的描述符
  • visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false 

五、MethodVisitor

        这是一个用来生成方法的类。

5.1 visitCode方法 

作用:表示开始生成字节码

通常第一个调用,固定格式

5.2 visitLabel方法 

作用:设置Label,Label的作用相当于表示方法在字节码中的位置

每一个方法都需要一个Label,用来保证方法调用顺序。

5.3 visitLineNumber方法 

作用:定义源代码中的行号与对应的指令

源代码如下: 

  public void visitLineNumber(final int line, final Label start)
  •  line:源代码中对应的行号
  • start:行号对应的字节码指令

5.4 visitVarInsn方法 

作用:用来对变量进行加载和存储的指令操作

源代码如下:

  public void visitVarInsn(final int opcode, final int varIndex)

参数解释:

  • opcode:对应的变量字节码指令

        例如:获取一个int数值类型的指令对应 iload 0

         有获取就肯定会有存储

  • varIndex:变量对应在局部变量表的下标

        例如下列代码:

        int a = 1;
        int b = 2;
        int d = a + b;

上面代码对应的字节码指令就是

   L5
    LINENUMBER 13 L5
    ICONST_1
    ISTORE 1
   L6
    LINENUMBER 14 L6
    ICONST_2
    ISTORE 2
   L7
    LINENUMBER 15 L7
    ILOAD 1
    ILOAD 2
    IADD
    ISTORE 3

解释下对应的字节码含义: 

  1. 将1变量加载到操作数栈,对应的指令就是ICONST_1
  2. 将栈顶的值保存到局部变量表第一个位置,对应的指令就是ISTORE_1
  3. 将2变量加载到操作数栈,对应的指令就是ICONST_2
  4. 将栈顶的值保存到局部变量表第二个位置,对应的指令就是ISTORE_2
  5. 取出局部变量表第一个元素到操作数栈(也就是变量a),对应的指令就是 ILOAD_1
  6. 取出局部变量表第二个元素到操作数栈(也就是变量b),对应的指令就是 ILOAD_2
  7. 此时操作数栈的栈顶就有a和b两个元素,执行指令IADD,就会把栈顶的两个元素相加并将结果压入栈顶
  8. 将栈顶的值保存到局部变量表第三个位置

5.5 visitMethodInsn方法 

作用:用来对一个方法执行指令操作

他可以执行的指令如下:

5.6 visitInsn方法 

作用:用来执行对操作数栈的指令

可以执行的指令如下:

NOP, ACONST_NULL, ICONST_M1, ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5, LCONST_0, LCONST_1, FCONST_0, FCONST_1, FCONST_2, DCONST_0, DCONST_1, IALOAD, LALOAD, FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, SWAP, IADD, LADD, FADD, DADD, ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, FDIV, DDIV, IREM, LREM, FREM, DREM, INEG, LNEG, FNEG, DNEG, ISHL, LSHL, ISHR, LSHR, IUSHR, LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, D2L, D2F, I2B, I2C, I2S, LCMP, FCMPL, FCMPG, DCMPL, DCMPG, IRETURN, LRETURN, FRETURN, DRETURN, ARETURN, RETURN, ARRAYLENGTH, ATHROW, MONITORENTER, or MONITOREXIT.

部分指令说明如下:

5.7 visitLocalVariable方法 

作用:给局部变量设置变量名

源代码如下:

  public void visitLocalVariable(
      final String name,
      final String descriptor,
      final String signature,
      final Label start,
      final Label end,
      final int index)

参数解释:

  • name:局部变量名
  •  descriptor:局部变量名的类型描述符
  • signature:局部变量名的签名,如果没有使用到泛型,则为null
  • start:第一个指令对应于这个局部变量的作用域(包括)。
  • end:最后一条指令对应于这个局部变量的作用域(排他)。
  • index:局部变量名的下标,也就是局部变量名的行号顺序(从0开始)。

例如代码:

    public void test(){
        int a = 1;
        int b = 2;
        int d = a + b;
    }

对应的使用方法如下:

            methodVisitor.visitLocalVariable("this", "Lasm/User;", 
                            "Lasm/User<TT;>;", label0, label4, 0);
            methodVisitor.visitLocalVariable("a", "I", null, label1, label4, 1);
            methodVisitor.visitLocalVariable("b", "I", null, label2, label4, 2);
            methodVisitor.visitLocalVariable("d", "I", null, label3, label4, 3);

注意:每个非静态方法都会默认有一个this的引用

5.8 visitMaxs方法 

作用:设置这个本地方法最大操作数栈和最大本地变量表

源代码如下:

  public void visitMaxs(final int maxStack, final int maxLocals)

例如代码:

    public void test(){
        int a = 1;
        int b = 2;
        int d = a + b;
    }

对应的使用方法如下:

methodVisitor.visitMaxs(2, 4);

 其中

  • maxStack == 2,分别是,ICONST_1, IAdd操作

        注意:maxStack == 2,不是代表只有两个对操作数栈的指令,而是操作数栈容量大小为2,可以满足上面代码,例如下面代码,操作数栈大小为2也可以满足。

    public void test(){
        int a = 1;
        int b = a + 2;
        int c = b * 2;
        int d = b * 2;
        int e = b * 2;
        int f = b * 2;
    }

再来一个例子, 它的操作数栈大小必须为3。

分别是,DUP, INVOKESPECIAL,ASTORE操作

    public void test(){
        Object user = new Object();
    }
  • maxLocals == 4,分别是局部变量this,a,b,d。

这些值的作用其实是用来决定操作数栈和本地变量表的大小,内存优化小知识😀

注意:visitMaxs方法的调用必须在所有的MethodVisitor指令结束后调用。

5.9 visitFrame方法

作用:设置一个新的栈帧

每个线程都有自己的执行堆栈,它由栈帧组成。每一个栈帧都表示一个方法调用:每次调用一个方法,新的栈帧会被推送到当前线程的执行堆栈上。 当方法正常或由于异常返回到原来的调用方法时,从执行堆栈中弹出,在调用方法中继续执行  (它的帧现在在堆栈的顶部)。 

5.10 visitFieldInsn

作用:获取或者存储一个对象的实例

 源码如下:

  public void visitFieldInsn(
      final int opcode, final String owner, final String name, final String descriptor)
  •  opcode:必须是下列这些指令,例如:GETSTATIC

  • owner:这个变量所在的类的类名(包名+类名),例如:java/lang/System
  • name:这个变量的名字,例如:out
  • descriptor:这个变量的描述符,例如:Ljava/io/PrintStream;

使用如下:

methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out","Ljava/io/PrintStream;");

5.11 visitEnd方法 

作用:通知MethodVisitor生成方法结束,表示结束生成字节码。

通常是作为MethodVisitor最后一个调用,固定格式。与visitCode,一个最前一个最后。

六、FieldVisitor

                类似于ClassVisitor,FieldVisitor是对变量的信息进行扫描,包括变量上的注解,变量的参数。

6.1 visitAnnotation

        作用:回调变量上的每一个注解

源代码:

public AnnotationVisitor visitAnnotation(String descriptor, boolean visible)

参数解释:

  • descriptor:注解的描述符
  • visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false 

相关技术使用文档:

Android编译时技术(二)ASM 基础使用之代码生成_I'm an Android Dev的博客-CSDN博客

Android编译时技术(三)ASM 基础使用之代码修改_I'm an Android Dev的博客-CSDN博客

参考文档:

ASM的官方文档

ASM使用指南

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值