java字节码及ASM编程
- 方式:内容来自《深入理解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编译的过程;java文件经过javac编译成class文件;class文件遵守java类定义规范;
可以在这里下载到完整的书籍
编译后的class文件可以被加载到jvm中运行;
下图为class文件的几个重要组成部分:
- 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;
}
}
}
输出结果如下、标红部分即为我们注入进去的代码::
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型本地变量推送至栈顶;
- aload_0 将第一个引用类型本地变量推送至栈顶;在类中;aload_0通常指向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, 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.
- visitFieldInsn 可以用于的操作指令:GETSTATIC, PUTSTATIC, GETFIELD, or PUTFIELD.
- visitIntInsn 可以用于的操作指令:BIPUSH, SIPUSH, or NEWARRAY.
- visitJumpInsn 可以用于的操作指令:IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, 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.用于跳转