文章大纲

  • 引言
  • 一、ASM 使用前的准备
  • 二、ASM的核心流程图
  • 三、ASM的基本操作
  • 1、ASM创建新的Java class
  • 2、修改方法体(函数插桩)
  • 2.1、函数插桩的主要流程图
  • 2.2、函数插桩的实现
  • 2.2.1、读取要进行插桩的函数所在的class
  • 2.2.2、 创建ClassReader 字节码分析器并通过class文件实例化
  • 2.2.3、 继承org.objectweb.asm.ClassVisitor实现具体的字节码分析器并重写对应的方法
  • 2.2.4、通过ClassWriter构造方法创建并实例化
  • 2.2.5、字节码分析器调用accept方法,开始解析class文件。
  • 2.2.6、继承org.objectweb.asm.commons.AdviceAdapter实现具体的MethodVisitor并重写对应的方法
  • 3、删除类的字段、方法等
  • 4、修改类、字段、方法的名字或修饰符等完善


引言

前面两篇文章概述了Java字节码相关知识以及ASM的核心知识点,这一篇就总结下ASM的基本用法,如果没有看过前面的文章建议先去阅读下。

一、ASM 使用前的准备

  • 需要引入两个库:核心库org.ow2.asm:asm:7.1和工具类库org.ow2.asm:asm-common:7.1
  • 安装ASM Bytecode Viewer 插件,便于快速查看字节码指令

android字节计算 android字节码插桩_字节码操作

  • 查看对应的字节码指令,具体见 Android 进阶——代码插桩必知必会之初识ASM7字节码操作库完全攻略(一)

二、ASM的核心流程图

android字节计算 android字节码插桩_ASM_02


ASM操作字节码核心步骤只有2步:

  • 拿到操作对应的字节码指令
  • 调用对应的方法传入字节码指令,根据需求进行I/O操作。

三、ASM的基本操作

1、ASM创建新的Java class

使用ASM 创建全新的Java class文件很简单,只需要几步:

  • 通过ASM Bytecode Viewer插件直接查看生成代码(当然也可以直接传入对应的字节码指令)
  • 创建ClassWriter对象
  • 调用ClassWriter对应的visit方法创建各部分
  • 执行并保存到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 <init>()V
        MethodVisitor methodVisitor = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                "()V", null, null);
        //传入生成构造方法方法体的字节码指令,将索引值为0的本地变量入栈
        methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                "<init>", "()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);
            }
        }
    }

2、修改方法体(函数插桩)

ClassReader 读取class文件时以类似事件驱动的形式,以回调的形式告知给我们(和xml的解析思想有点类似)。

2.1、函数插桩的主要流程图

android字节计算 android字节码插桩_android字节计算_03

2.2、函数插桩的实现

如上图所示,实现函数插桩需要以下几个步骤:

2.2.1、读取要进行插桩的函数所在的class
FileInputStream inputStream=new FileInputStream("E:\\ASM\\basicUse\\build\\intermediates\\javac\\debugUnitTest\\classes\\com\\crazymo\\asm\\basic\\LogInject.class");
2.2.2、 创建ClassReader 字节码分析器并通过class文件实例化

ClassReader 是用于解析class文件的,拥有很多重写的构造函数。

android字节计算 android字节码插桩_android字节计算_04

//可以直接传入fis也可以传入String name=HelloASM.class.getName()创建字节码分析器
ClassReader classReader=new ClassReader(inputStream);
2.2.3、 继承org.objectweb.asm.ClassVisitor实现具体的字节码分析器并重写对应的方法

要修改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);
        }
    }
2.2.4、通过ClassWriter构造方法创建并实例化

ClassWriter是用于获取插桩后的class字节码的数据的

//自动计算栈帧
ClassWriter classWriter=new ClassWriter(ClassWriter.COMPUTE_FRAMES);
2.2.5、字节码分析器调用accept方法,开始解析class文件。
//引入的是7就可以传递4、5、6,都是向下兼容的,而ClassReader.EXPAND_FRAMES需要打包进APK就必须传入这个值
     classReader.accept(new InjectClassVisitor(Opcodes.ASM7,classWriter),ClassReader.EXPAND_FRAMES);
2.2.6、继承org.objectweb.asm.commons.AdviceAdapter实现具体的MethodVisitor并重写对应的方法

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("<init>","()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的相关知识,当然如果想要修改内部类的思路也类似。

3、删除类的字段、方法等

删除类的字段、方法等的本质是在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor对象

  • 继承ClassVisitor,实现自定义的ClassVisitor
  • 构造ClassReader对象
  • 调用ClassReader对象的accept方法传入自定义的ClassVisitor
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();
    }
}

android字节计算 android字节码插桩_android字节计算_05

4、修改类、字段、方法的名字或修饰符等完善

修改类、字段、方法的名字或修饰符等的本质就是在职责链传递过程中替换调用参数

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);
    }
}