前面两篇文章概述了Java字节码相关知识以及ASM的核心知识点,这一篇就总结下ASM的基本用法,如果没有看过前面的文章建议先去阅读下。
需要引入两个库:核心库org.ow2.asm:asm:7.1和工具类库org.ow2.asm:asm-common:7.1
安装ASM Bytecode Viewer 插件,便于快速查看字节码指令
拿到操作对应的字节码指令
调用对应的方法传入字节码指令,根据需求进行I/O操作。
使用ASM 创建全新的Java class文件很简单,只需要几步:
package com.crazymo.asm.basic;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
/**
* @author : Crazy.Mo
*/
public class BasicASM{
public static void generateClass() {
//生成一个类只需要ClassWriter组件即可
ClassWriter writer = new ClassWriter(0);
//通过visit方法确定类的头部信息
writer.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE,
"com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});
//定义类的属性
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"LESS", "I", null, 1000);//.visitEnd();
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"EQUAL", "I", null, new Integer(0));//.visitEnd();
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"GREATER", "I", null, new Integer(1)).visitEnd();
//定义类的方法
writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
writer.visitEnd(); //使writer类已经完成
//将writer转换成字节数组写到文件里面去
byte[] data = writer.toByteArray();
File file = new File("D://ASMDemo.class");
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(data);
fout.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 利用ASM 自动生成HelloASM.class
* public class HelloASM {
* public static final String USR = "CrazyMo_";
* public static void main(String[] args){
* System.out.println("Hello ASM From CrazyMo_!");
* }
* }
*/
public static void generateHelloASM() {
//生成一个类只需要ClassWriter组件即可
ClassWriter writer = new ClassWriter(0);
//通过visit方法确定类的头部信息,使用的Java 版本号定义类的头部信息
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloASM", null,
"java/lang/Object", null);
//定义类的属性
writer.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
"USR", "Ljava/lang/String;", null, "CrazyMo_").visitEnd();
//默认的构造方法=>public ()V
MethodVisitor methodVisitor = writer.visitMethod(Opcodes.ACC_PUBLIC, "" ,
"()V", null, null);
//传入生成构造方法方法体的字节码指令,将索引值为0的本地变量入栈
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"" , "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
//生成main方法的签名
methodVisitor = writer.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
"main","([Ljava/lang/String;)V",
null, null);
//生成main方法中的字节码指令
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,
"java/lang/System",
"out","Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM From CrazyMo_!");
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/io/PrintStream",
"println","(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(2, 2);
//字节码生成完成
methodVisitor.visitEnd();
writer.visitEnd();
// 获取生成的class文件对应的二进制流
byte[] code = writer.toByteArray();
//将class 二进制流保存到本地磁盘上
saveClassfile(code);
//直接将二进制流加载到内存中
invokeClzBytecode(code);
}
public static void saveClassfile(byte[] code) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream("HelloASM.class");
fos.write(code);
fos.close();
}catch (IOException e) {
e.printStackTrace();
}
}
private static void invokeClzBytecode(byte[] code) {
ByteClassLoader loader = new ByteClassLoader("HelloASM",code);
//通过反射调用main方法
try {
Class<?> exampleClass = loader.loadClass("HelloASM");
if(loader.classLoaded()) {
exampleClass.getMethods()[0].invoke(null, new Object[]{null});
}else{
throw new AssertionError("Class HelloASM" + " loaded from wrong source");
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/** A simple ClassLoader to test that a class can be loaded in the JVM. */
public static class ByteClassLoader extends ClassLoader {
/**
* 传入全类名"com.crazymo.asmdemo.HelloASM"
*/
private final String className;
private final byte[] classContent;
private boolean classLoaded;
ByteClassLoader(final String className, final byte[] classContent) {
this.className = className;
this.classContent = classContent;
}
boolean classLoaded() {
return classLoaded;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
if (name.equals(className)) {
classLoaded = true;
return defineClass(className, classContent, 0, classContent.length);
} else {
return super.loadClass(name, resolve);
}
}
}
}
定义一个用于把Class 字节码加载到虚拟机的ClassLoader
/** A simple ClassLoader to test that a class can be loaded in the JVM. */
private static class ByteClassLoader extends ClassLoader {
/**
* 要载入HelloASM.class直接传入"HelloASM"
*/
private final String className;
private final byte[] classContent;
private boolean classLoaded;
ByteClassLoader(final String className, final byte[] classContent) {
this.className = className;
this.classContent = classContent;
}
boolean classLoaded() {
return classLoaded;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve)
throws ClassNotFoundException {
if (name.equals(className)) {
classLoaded = true;
return defineClass(className, classContent, 0, classContent.length);
} else {
return super.loadClass(name, resolve);
}
}
}
ClassReader 读取class文件时以类似事件驱动的形式,以回调的形式告知给我们(和xml的解析思想有点类似)。
如上图所示,实现函数插桩需要以下几个步骤:
FileInputStream inputStream=new FileInputStream("E:\\ASM\\basicUse\\build\\intermediates\\javac\\debugUnitTest\\classes\\com\\crazymo\\asm\\basic\\LogInject.class");
ClassReader 是用于解析class文件的,拥有很多重写的构造函数。
//可以直接传入fis也可以传入String name=HelloASM.class.getName()创建字节码分析器
ClassReader classReader=new ClassReader(inputStream);
要修改class 成员(成员指的是类的方法、属性、类的注解、类的修饰符、内部类),就需要通过ClassVisitor。
/**
* 定义ClassVisitor(代理模式)类访问者,可用于访问类的各种成员,比如类的方法、属性、注解等,
* 但无法进入到方法的内部,解析时就会自动回调ClassVisitor里的方法
*/
static class InjectClassVisitor extends ClassVisitor{
public InjectClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
///return super.visitMethod(access, name, descriptor, signature, exceptions);
MethodVisitor methodVisitor=super.visitMethod(access, name, descriptor, signature, exceptions);
return new InjectMethodVisitor(api,methodVisitor,access, name, descriptor);
}
}
ClassWriter是用于获取插桩后的class字节码的数据的
//自动计算栈帧
ClassWriter classWriter=new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//引入的是7就可以传递4、5、6,都是向下兼容的,而ClassReader.EXPAND_FRAMES需要打包进APK就必须传入这个值
classReader.accept(new InjectClassVisitor(Opcodes.ASM7,classWriter),ClassReader.EXPAND_FRAMES);
org.objectweb.asm.commons.AdviceAdapter最终继承自MethodVisitor,是工具类库org.ow2.asm:asm-common:7.1提供给我们的便捷访问方法体内部的类,通过这个类可以更加便捷的操作字节码,因为它几乎是提供了与JVM 字节码指令同名或类似名称的方法,在我们修改字节码的时候直接从ASM 插件中拿到对应的字节码传入指定的方法即可。
static class InjectMethodVisitor extends AdviceAdapter{
private int start;
boolean isInject=false;
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
* @param methodVisitor the method visitor to which this adapter delegates calls.
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param descriptor the method's descriptor (see {@link Type Type}).
*/
protected InjectMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println(descriptor);
if("Lcom/crazymo/asm/basic/Inject;".equals(descriptor)){
isInject=true;
}else {
isInject=false;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if(!isInject){
return;
}
//插入一个执行静态方法的指令
invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
//接收返回值局部变量,先创建一个局部变量
//慎用 storeLocal(1),这代表的是接收索引为1的局部变量,因为局部变量是按照索引顺序加载的,第一个局部变量的索引为0,第二个为1
start = newLocal(Type.LONG_TYPE);
//接收一个局部变量
storeLocal(start);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if(!isInject){
return;
}
invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
///若存在与指令码相同的方法就直接调用
getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io/PrintStream;"));
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("" ,"()V"));
//压栈
visitLdcInsn("cmo:");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//减法
loadLocal(end);
loadLocal(start);
math(SUB,Type.LONG_TYPE);
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
visitLdcInsn("ms.");
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));
}
}
ASM修改方法体本质上都是通过MethodVisitor来创建和修改方法。
要想完全掌握方法插桩,就需要完全掌握字节码指令以及MethodVisitor的相关知识,当然如果想要修改内部类的思路也类似。
删除类的字段、方法等的本质是在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor对象。
public class DeleMethodVisitor extends ClassVisitor {
public DeleMethodVisitor(ClassVisitor classVisitor) {
///指定使用ASM7的api版本
super(Opcodes.ASM7,classVisitor);
}
public MethodVisitor visitMethod(final int access,final String name,final String desc,final String signature,final String[] exceptions){
if(name.equals("main")){
///访问到main方法时,就中断委托即删除了
return null;
}
return cv.visitMethod(access,name,desc,signature,exceptions);
}
}
public static void deleMethod(){
ClassReader reader = null;
try {
String fullName = HelloASM.class.getName();
String fullNameType = fullName.replace(".", "/");
reader = new ClassReader(fullNameType);
} catch (IOException e) {
e.printStackTrace();
}
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new DeleMethodVisitor(classWriter);
reader.accept(visitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
File file = new File("HelloASM.class");
FileOutputStream fout = null;
try {
fout = new FileOutputStream(file);
fout.write(data);
fout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
修改类、字段、方法的名字或修饰符等的本质就是在职责链传递过程中替换调用参数
package com.crazymo.asmdemo.modif.asm;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* @author : Crazy.Mo
*/
public class ModifyMethodVisitor extends ClassVisitor {
public ModifyMethodVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM7, classVisitor);
}
//修改方法名
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String newName=name;
if("main".equals(name)) {
newName = "TestMain";
}
return cv.visitMethod(access, newName, descriptor, signature, exceptions);
}
}