java字节码及ASM编程

  • 时间:2015-08-14
  • 方式:内容来自《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》

java字节码


Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件。

每个合法的 Java 字节码文件都具备精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释所有的 Java 字节码文件。

如最简单的helloworld

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello world"); 
    } 
}

javac.png

图示为javac编译的过程;java文件经过javac编译成class文件;class文件遵守java类定义规范;

可以在这里下载到完整的书籍

编译后的class文件可以被加载到jvm中运行;

下图为class文件的几个重要组成部分:

helloworld

  • Magic:一个 Java 字节码文件的前 4 个字节被称为它的魔数。每个正确的 Java 字节码文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
  • Version:该项存放了 Java 字节码文件的版本信息,它对于一个 Java 文件具有重要的意义。高版本的虚拟机可以处理低版本的文件;反之则不能处理
  • Constant Pool:该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用;
  • Access_flag:该项指明了该文件中定义的是类还是接口,同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。访问标记定义如下:

    ACC_PUBLIC:是否为public类型
    ACC_PRIVATE:是否为private类型
    ACC_PROTECTED:是否为protected类型
    ACC_STATIC:是否为static类型
    ACC_VOLATILE:是否为volatile类型
    ACC_TRANSIENT:是否是transient类型
    ACC_SYNTHETIC:是否是编译器自动生成
    ACC_FINAL:是否是final
    ACC_SUPER:是否允许使用invokespecial的新语义;jdk1.0.2后一直为真
    ACC_INTERFACE:是否是接口
    ACC_ABSTRACE:是否是抽象类
    ACC_SYNTHETIC:是否由用户代码生成
    ACC_ANNOTATION:是否是注解
    ACC_ENUM:是否是枚举类型
    
  • This Class:指向表示该类全限定名称的字符串常量的指针。

  • Super Class:指向表示父类全限定名称的字符串常量的指针。

  • Interfaces:一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。

  • Fields:该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。字节码定义如下:

    B:对应byte类型
    J:对应long
    C:对应char
    S:对应short
    D:对应duble
    Z:对应boolean
    F:对应float
    V:对应void
    I:对应int
    L:类的全限定符开始,以;结束。如  Ljava.lang.String;
    [:数组的一个维度;如int[][] 就表示为 [[I
    
  • Methods:该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。

  • Class attributes:该项存放了在该文件中类或接口所定义的属性的基本信息。

ASM


ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。

ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

使用ASM框架需要引入ASM的包:可以在你的pom文件中加入下面这段来引入ASM;

<dependency>
    <groupId>asm</groupId>
    <artifactId>asm</artifactId>
    <version>3.3.1</version>
</dependency>

ASM核心类

ASM 提供了三个基于 ClassVisitor 接口的类来实现 class 文件的生成和转换:

  • ClassReader:ClassReader 解析一个类的 class 字节码,该类的 accept 方法接受一个 ClassVisitor 的对象,在 accept 方法中,会按上文描述的顺序逐个调用 ClassVisitor 对象的方法。它可以被看做事件的生产者。

  • ClassAdapter:ClassAdapter 是 ClassVisitor 的实现类。它的构造方法中需要一个 ClassVisitor 对象,并保存为字段 protected ClassVisitor cv。在它的实现中,每个方法都是原封不动的直接调用 cv 的对应方法,并传递同样的参数。可以通过继承 ClassAdapter 并修改其中的部分方法达到过滤的作用。它可以看做是事件的过滤器。

  • ClassWriter:ClassWriter 也是 ClassVisitor 的实现类。ClassWriter 可以用来以二进制的方式创建一个类的字节码。对于 ClassWriter 的每个方法的调用会创建类的相应部分。例如:调用 visit 方法就是创建一个类的声明部分,每调用一次 visitMethod 方法就会在这个类中创建一个新的方法。在调用 visitEnd 方法后即表明该类的创建已经完成。

HelloWorld

package com.violetgo.asm;

/**
 * @author weigao
 * @since 15/6/8
 */
public class HelloWorld {
    public void sayHello() {
        System.out.println("Hello World!");
    }
}



package com.violetgo.asm;

import org.objectweb.asm.*;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

/**
 * @author weigao
 * @since 15/6/8
 */
public class TestASM extends ClassLoader implements Opcodes {

    public static void main(String[] args) throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException,InstantiationException {
        ClassReader cr=new ClassReader(HelloWorld.class.getName());
        ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassAdapter adapter = new TestClassAdapter(cw);
        cr.accept(adapter, 0);

        byte[] code=cw.toByteArray();

        //自定义加载器
        TestASM loader=new TestASM();
        Class<?> appClass=loader.defineClass(null, code, 0,code.length);
        appClass.getMethods()[0].invoke(appClass.newInstance(), new Object[]{});

    }

    public static class TestClassAdapter extends ClassAdapter{

        public TestClassAdapter(ClassVisitor classVisitor) {
            super(classVisitor);
        }

        public MethodVisitor visitMethod(int arg, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor mv = super.visitMethod(arg, name, descriptor, signature, exceptions);
            if (name.equals("sayHello")) {
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V");
            }
            return mv;
        }
    }
}

输出结果如下、标红部分即为我们注入进去的代码::

testasm.png

java常用操作码


Tips:在使用asm编码时;如果遇到较为复杂的类;可以先使用java写一遍;然后再使用javap –c –p 进行反编译;然后针对javap的指令集转换成asm编码即可;

下面介绍下java常用指令;此处不是全部指令

  • aconst_null 将null推送至栈顶;相当于定义null
  • ldc 将int, float或String型常量值从常量池中推送至栈顶
  • iload 将指定的int型本地变量推送至栈顶;在方法内iload_n就是第几个变量;对应的还有lload、fload、dload、aload
  • iload_0 将第一个int型本地变量推送至栈顶;
  • aload0 将第一个引用类型本地变量推送至栈顶;在类中;aload0通常指向this指针
  • astore 将栈顶引用型数值存入指定本地变量;用于存储变量
  • areturn 从当前方法返回对象引用
  • return 从当前方法返回void
  • getstatic 获取指定类的静态域,并将其值压入栈顶
  • putstatic 为指定的类的静态域赋值
  • getfield 获取指定类的实例域,并将其值压入栈顶
  • putfield 为指定的类的实例域赋值
  • invokevirtual 调用实例方法
  • invokespecial 调用超类构造方法,实例初始化方法,私有方法
  • invokestatic 调用静态方法
  • invokeinterface 调用接口方法
  • arraylength 获得数组的长度值并压入栈顶
  • new 创建一个对象,并将其引用值压入栈顶
  • newarray 创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
  • goto 无条件跳转
  • ifeq 当栈顶int型数值等于0时跳转
  • pop 将栈顶数值弹出 (数值不能是long或double类型的)
  • dup 复制栈顶数值并将复制值压入栈顶
  • iadd 将栈顶两int型数值相加并将结果压入栈顶
  • isub 将栈顶两int型数值相减并将结果压入栈顶
  • imul 将栈顶两int型数值相乘并将结果压入栈顶
  • idiv 将栈顶两int型数值相除并将结果压入栈顶
  • if_icmpeq 比较栈顶两int型数值大小,当结果等于0时跳转
  • athrow 将栈顶的异常抛出
  • checkcast 检验类型转换,检验未通过将抛出ClassCastException
  • instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
  • monitorenter 获得对象的锁,用于同步方法或同步块
  • monitorexit 释放对象的锁,用于同步方法或同步块
  • ifnull 为null时跳转

ASM常用操作码


  • visitInsn 可以用于的操作指令:NOP, ACONSTNULL, ICONSTM1, ICONST0, ICONST1, ICONST2, ICONST3, ICONST4, ICONST5, LCONST0, LCONST1, FCONST0, FCONST1, FCONST2, DCONST0, DCONST1,IALOAD, LALOAD, FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IASTORE, LASTORE, FASTORE, DASTORE, AASTORE, BASTORE, CASTORE, SASTORE, POP, POP2, DUP, DUPX1,DUPX2, DUP2, DUP2X1, 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.
  • visitFieldInsn 可以用于的操作指令:GETSTATIC, PUTSTATIC, GETFIELD, or PUTFIELD.
  • visitIntInsn 可以用于的操作指令:BIPUSH, SIPUSH, or NEWARRAY.
  • visitJumpInsn 可以用于的操作指令:IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IFICMPEQ, IFICMPNE, IFICMPLT, IFICMPGE, IFICMPGT, IFICMPLE, IFACMPEQ, IFACMPNE, GOTO, JSR, IFNULL, or IFNONNULL.
  • visitTypeInsn 可以用于的操作指令:NEW, ANEWARRAY, CHECKCAST, or INSTANCEOF.
  • visitVarInsn 可以用于的操作指令:ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE, or RET.
  • visitMethodInsn 可以用于的操作指令:INVOKEVIRTUAL, INVOKESPECIAL, INVOKESTATIC, or INVOKEINTERFACE.
  • visitIincInsn 自增.
  • visitLdcInsn 定义变量LDC
  • visitLabel label.用于跳转

相关项目

Tprofile-依赖分析