ASM 字节码插桩入门

一、概述

1.1 什么是字节码插桩

字节码插桩属于编译插桩范畴之内,编译插桩是指在代码编译期间修改已有的代码或者生成新代码。Android 在编译过程中:

Android 编译过程
Java 源文件经过 javac 编译成 Java 字节码的 class 文件,再经过 dx/d8 工具处理成 Android 虚拟机字节码的 dex 文件。那么编译插桩的时机可以大致分为两种:

  1. Java 文件:利用 APT 技术生成 Java 源文件,在编译的最开始介入。
  2. 字节码文件:可以操作 Java 字节码的 class 文件,也可以是 Android 虚拟机的 dex 文件,取决于我们所使用的方法(框架),一般用于代码监控、修改、分析等场景。这种方式就是字节码插桩。

字节码插桩的使用场景有很多:

  1. ARouter 插件通过字节码插桩优化启动速度
  2. Robust 插件通过字节码插桩插入 Hook 点
  3. 第三方的 SDK 有 bug,SDK 不开源只有 jar 包,想修复 bug 可以用字节码插桩

1.2 认识字节码文件与指令

由于我们将要使用的 ASM 框架操作的是 class 字节码,而不是 dex 字节码,所以后续我们主要讨论的都是 class 字节码文件。但是你最起码需要知道,JVM 是基于栈的虚拟机,而 Dalvik/ART 是基于寄存器的虚拟机,后者的指令数以及数据的移动次数要比前者少。

在 Android Studio 中看到的 class 文件是这样的:

public class MainActivity extends AppCompatActivity {
    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.test();
    }

    private void test() {
    }
}

这其实是 Android Studio 按照 class 文件的格式进行反编译后的产物,用 010 Editor 查看 class 文件的二进制数据看到的才是庐山真面目:

class文件二进制数据
其中前 4 个字节的 CAFEBABE 表示文件格式,所有 class 文件的开头都是 CAFEBABE。

那字节码插桩就是直接去操作这些二进制数据吗?当然不是,如果直接修改二进制文件需要弄清楚每一位二进制都表示的含义,成本太高,会改到吐血的……虽然不用去研究二进制,但是虚拟机的指令集多少还是要了解下的。

我们写的 Java 源代码都会转换成虚拟机指令之后才交给虚拟机执行,比如说:

请添加图片描述

图片左侧是 Java 源代码,右侧是使用 Android Studio 的 ASM Bytecode Viewer 插件转换出的对应的指令。比如说 ICONST_N 表示将 int 型常量压入【操作数栈】的位置 N,ISTORE K 表示将栈顶 int 型数值存入【局部变量表】的位置 K,IADD 表示执行 int 型加法。指令执行过程如下:


上面的 test() 还有一处需要注意,就是红框标记的 test()V,它是 Java 的方法签名,分成三个部分:

  1. test 表示方法名称
  2. () 内存放的是方法参数的类型
  3. V 处表示的是该方法的返回值类型,V 表示无返回值,即 Void

换一个方法看的能更清楚些:


方法签名所有类型的表示方法:


当然你也可以用 javap 命令反编译 class 文件,与 ASM 插件显示的结果大致相同:

javap -c xxx.class

二、ASM 框架

2.1 基本信息

ASM 框架可以帮助我们进行字节码插桩,即便我们不熟悉 class 文件格式也可以操作字节码文件,修改已经存在的 class 文件(jar 包中的 class 也可以)中的属性、方法等,也可创建一个全新的 class。

ASM 的官方网站:ASM,目前最新版本是 ASM 9.3。

使用前需要在项目中引入 ASM 依赖:

	implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

ASM 主要有以下三个类:

  1. ClassReader:读取并分析字节码,accept() 会接收一个 ClassVisitor
  2. ClassVisitor:抽象类,访问类的解析,如注解、方法、成员变量的解析,对应的具体子类分别是 AnnotationVisitor、MethodVisitor(会解析方法上的注解、参数、代码等)、FieldVisitor
  3. ClassWriter:ClassVisitor 的子类,可以获得解析结果,不进行任何修改

2.2 基本用法

两种基本用法,生成一个全新的类、修改已经存在的类。

生成全新的类

假设要生成的字节码反编译后是这样的:

package com.example.old;

public class Demo1 {
    private static final String TAG = "Demo1";
    private String name;

    public Demo1() {
    }

    public void print() {
        System.out.println("name:" + this.name);
    }

    public void setName(String var1) {
        this.name = var1;
    }
}

ASM 代码可以这样写:

	// 生成一个类文件,最后用 classWriter.toByteArray() 转换成 byte[] 返回
	public byte[] createClass() {
		// ClassWriter 的参数还可以传 COMPUTE_MAXS 和 COMPUTE_FRAMES,
		// 分别表示自动更新操作数栈和方法调用帧计算
        ClassWriter classWriter = new ClassWriter(0);
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;

        // 1.生成一个 public 的类,全类名为 com.example.asm.Demo1
        classWriter.visit(V1_7, ACC_PUBLIC | ACC_SUPER, "com/example/asm/Demo1", null, "java/lang/Object", null);

        // 2.生成字符串常量 TAG,并赋值为 Demo1
        fieldVisitor = classWriter.visitField(ACC_PRIVATE | ACC_FINAL | ACC_STATIC, "TAG", "Ljava/lang/String;", null, "Demo1");
        fieldVisitor.visitEnd();

        // 3.生成私有成员变量 name
        fieldVisitor = classWriter.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
        fieldVisitor.visitEnd();

        // 4.生成默认构造方法
        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        // 开始方法访问
        methodVisitor.visitCode();
        methodVisitor.visitVarInsn(ALOAD, 0);
        // 执行 INVOKESPECIAL 指令调用 java/lang/Object 的构造方法 <init>,参数为空没有返回值,不是接口方法
        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        methodVisitor.visitInsn(RETURN);
        // 更新操作数栈
        methodVisitor.visitMaxs(1, 1);
        // 结束方法访问
        methodVisitor.visitEnd();

        // 5.生成 print 方法
        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "print", "()V", null, null);
        methodVisitor.visitCode();
        // 执行 GETSTATIC 指令获取 java/lang/System 的成员 out,该成员类型为 java/io/PrintStream
        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
        methodVisitor.visitInsn(DUP);
        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        methodVisitor.visitLdcInsn("name:");
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        methodVisitor.visitVarInsn(ALOAD, 0);
        methodVisitor.visitFieldInsn(GETFIELD, "com/example/asm/Demo1", "name", "Ljava/lang/String;");
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        methodVisitor.visitInsn(RETURN);
        methodVisitor.visitMaxs(3, 1);
        methodVisitor.visitEnd();

        // 6.生成 setName 方法
        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "setName", "(Ljava/lang/String;)V", null, null);
        methodVisitor.visitCode();
        Label label0 = new Label();
        methodVisitor.visitLabel(label0);
        methodVisitor.visitLineNumber(13, label0);
        methodVisitor.visitVarInsn(ALOAD, 0);
        methodVisitor.visitVarInsn(ALOAD, 1);
        methodVisitor.visitFieldInsn(PUTFIELD, "com/example/asm/Demo1", "name", "Ljava/lang/String;");
        Label label1 = new Label();
        methodVisitor.visitLabel(label1);
        methodVisitor.visitLineNumber(14, label1);
        methodVisitor.visitInsn(RETURN);
        Label label2 = new Label();
        methodVisitor.visitLabel(label2);
        methodVisitor.visitLocalVariable("this", "Lcom/example/asm/Demo1;", null, label0, label2, 0);
        methodVisitor.visitLocalVariable("name", "Ljava/lang/String;", null, label0, label2, 1);
        methodVisitor.visitMaxs(2, 2);
        methodVisitor.visitEnd();

        // 结束访问
        classWriter.visitEnd();

        // 转换成 byte[]
        return classWriter.toByteArray();
    }

然后用 IO 输出到指定路径的 Demo1.class 文件中即可:

	public void run() throws Exception {
        FileOutputStream fos = new FileOutputStream("...\\com\\example\\asm\\Demo1.class");
        fos.write(createClass());
        fos.close();
    }

修改已有的类

需要先用 IO 读取需要修改的 class 文件,并将数据(输入流/byte[])传入 ClassReader 再进行操作:

	public void modify() throws Exception {
		// 从 Demo1.class 读取,修改后输出到 Demo2.class 中
        FileOutputStream fos = new FileOutputStream("...\\com\\example\\asm\\Demo2.class");
        FileInputStream fis = new FileInputStream("...\\com\\example\\asm\\Demo1.class");
        fos.write(modifyClass(fis));
        fis.close();
        fos.close();
    }

	private byte[] modifyClass(FileInputStream fis) throws IOException {
        ClassReader classReader = new ClassReader(fis);
        ClassWriter classWriter = new ClassWriter(0);
        // 在 ClassVisitor 中修改
        ClassVisitor classVisitor = new ClassVisitor(ASM7, classWriter) {
            // 要修改方法,就在 visitMethod() 中进行
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                // 删除 print 方法
                if ("print".equals(name)) {
                    return null;
                }

                // 将 setName 方法的访问负设为 private
                if ("setName".equals(name)) {
                    access = ACC_PRIVATE;
                }

                // super 内调用的其实是 ClassVisitor 构造方法中 classWriter 的 visitMethod()
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }

            // 要增加方法或字段,最好在 visitEnd() 中进行,避免破坏之前排好的类结构
            @Override
            public void visitEnd() {
                // 增加一个字段 private String address
                FieldVisitor fieldVisitor = cv.visitField(ACC_PRIVATE, "address", "Ljava/lang/String;", null, null);
                fieldVisitor.visitEnd();

                // 增加一个方法 public String getAddress()
                MethodVisitor methodVisitor = cv.visitMethod(ACC_PUBLIC, "getAddress", "()Ljava/lang/String;", null, null);
                methodVisitor.visitCode();
                methodVisitor.visitVarInsn(ALOAD, 0);
                methodVisitor.visitFieldInsn(GETFIELD, "com/example/asm/Demo1", "address", "Ljava/lang/String;");
                methodVisitor.visitInsn(IRETURN);
                methodVisitor.visitMaxs(1, 1);
                methodVisitor.visitEnd();

                super.visitEnd();
            }
        };
        classReader.accept(classVisitor, 0);
        return classWriter.toByteArray();
    }

我们做的修改是将 Demo1.class 中的 print() 删除,将 setName() 修改为 private 的,同时增加 address 字段和 getAddress(),效果如下:


通过以上代码可能你已经发现了,使用 ASM 框架实现字节码插桩其实需要对虚拟机指令有一定的了解才可以,否则很难将简单的 Java 源代码转换成指令。在确实对指令没有熟练掌握的前提下,可以通过 ASM Bytecode 插件自动生成 ASM 框架代码:


当然最好还是掌握虚拟机指令,把插件当成一种辅助手段。

三、简单应用

3.1 统计方法执行耗时

字节码插桩最常用的例子就是统计方法的执行时间:


方法跟前面的例子大致相同,主要在于如何找准方法开始和结束的时机,示例代码如下:

	private boolean useAdviceAdapter = false;

    public void test() throws Exception {
        // 1.创建 ClassReader、ClassWriter 对象
        FileInputStream fis = new FileInputStream("...\\com\\example\\old\\Test.class");
        ClassReader classReader = new ClassReader(fis);
        // COMPUTE_FRAMES 会自动计算局部变量和操作数栈等
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // 2.创建 ClassVisitor 对象,构造方法参数为当前所使用的 ASM 库的版本号以及 ClassWriter
        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                // 使用自定义的 MethodVisitor 以达到修改方法内容的目的
                MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
                if (!useAdviceAdapter) {
                    return new CalTimeVisitor1(Opcodes.ASM7, methodVisitor, name);
                } else {
                    return new CalTimeVisitor2(Opcodes.ASM7, methodVisitor, access, name, descriptor);
                }
            }
        };

        // 3.开始解析
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);

        // 4.输出到字节码文件中
        byte[] bytes = classWriter.toByteArray();
        FileOutputStream fos = new FileOutputStream("...\\com\\example\\old\\Test.class");
        fos.write(bytes);
        fos.close();
    }

MethodVisitor 的子类定义了两个,效果是相同的,实现方式有差别,CalTimeVisitor1 是传统的实现方式,继承 MethodVisitor:

	static class CalTimeVisitor1 extends MethodVisitor {

        private String name;

        public CalTimeVisitor1(int api, MethodVisitor methodVisitor, String name) {
            super(api, methodVisitor);
            this.name = name;
        }

        // 在开始访问方法时回调
        @Override
        public void visitCode() {
            super.visitCode();
            // 构造方法不插桩
            if (!"<init>".equals(name)) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitVarInsn(LSTORE, 1);
            }
        }

        @Override
        public void visitInsn(int opcode) {
            // 在 return 之前添加代码
            if (!"<init>".equals(name) && (opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                mv.visitVarInsn(LSTORE, 3);

                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                mv.visitInsn(DUP);

                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                mv.visitLdcInsn("execute:");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                mv.visitVarInsn(LLOAD, 3);
                mv.visitVarInsn(LLOAD, 1);
                mv.visitInsn(LSUB);

                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            super.visitInsn(opcode);
        }
    }

CalTimeVisitor2 则继承 AdviceAdapter,AdviceAdapter 是 MethodVisitor 的子类,它可以直接提供方法入口和出口的回调方法 onMethodEnter() 和 onMethodExit():

	static class CalTimeVisitor2 extends AdviceAdapter {

        private int start;

        protected CalTimeVisitor2(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
        }

        // 方法入口回调
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            if ("<init>".equals(getName())) return;
            // 调用静态方法 java/lang/System.currentTimeMillis()
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            // 创建一个 Long 型的局部变量用来接收静态方法的返回值
            start = newLocal(Type.LONG_TYPE);
            // 将 invokeStatic() 运行的结果存入 start 中
            storeLocal(start);
        }

        // 方法出口回调
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            if ("<init>".equals(getName())) return;
            // 1.获取方法执行结束的时间并赋值给 end
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J")); // ()J 表示空参数,返回值为 Long 型
            int end = newLocal(Type.LONG_TYPE);
            storeLocal(end);

            // 2.获取 System.out 静态成员
            getStatic(Type.getType("Ljava/lang/System;"), "out",
                    Type.getType("Ljava/io/PrintStream;"));

            // 3.创建 StringBuilder 实例,用来保存拼接的字符串
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
            dup();
            // 调用 StringBuilder 的构造方法
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("<init>", "()V"));
            // 将字符串 execute: 压入操作数栈
            visitLdcInsn("execute:");
            // 执行 StringBuilder 的 append()
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder")); // 方法签名最后的分号一定别忘了
            // end - start
            loadLocal(end);
            loadLocal(start);
            math(SUB, Type.LONG_TYPE);
            // 将结果通过 append() 拼接到后面
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(J)Ljava/lang/StringBuilder;"));
            // StringBuilder.toString()
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("toString", "()Ljava/lang/String;"));
            // 执行 PrintStream 的 println()
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"),
                    new Method("println", "(Ljava/lang/String;)V"));
        }
    }

实现效果:


有个细节需要注意下,填写方法签名时,如果是引用类型,不要忘记结尾的分号,否则插桩的代码可能会有问题,比如像下面这样:

3.2 ASM 结合注解

字节码插桩可以帮我们实现 AOP,面向切面思想,将需要字节码插桩的方法视为一个切面,不需要的视为另一个切面,借助注解来完成切面划分,即打了注解的插桩,没打的不插:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ASMAnnotation {
}

重写 MethodVisitor 的 visitAnnotation(),这里仅以 CalTimeVisitor2 为例:

static class CalTimeVisitor2 extends AdviceAdapter {
	
	// 是否对当前方法进行插桩
	private boolean inject = false;

	// 方法入口回调
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            // 构造方法与 inject = false 的方法不插桩
            if ("<init>".equals(getName()) || !inject) return;
            ...
        }

        // 方法出口回调
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            // 构造方法与 inject = false 的方法不插桩
            if ("<init>".equals(getName()) || !inject) return;
            ...
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        	// 只有方法上打了 ASMAnnotation 注解时才插桩
            if ("Lcom/example/test/ASMAnnotation;".equals(descriptor)) {
                inject = true;
            }
            return super.visitAnnotation(descriptor, visible);
        }
}

效果是在源码中标记了 @ASMAnnotation 注解的方法会被插桩:


此外,ASM 可以与 Gradle 插件结合使用实现自动化补丁,具体可以参考 Android 热修复 的【三、自定义 Gradle 插件打补丁包】这一节。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值