Android进阶之路Android开发Android开发经验谈

ASM字节码插桩详解

2019-12-17  本文已影响0人  Joker_Wan

1、ASM概述

2、Java 类文件概述

所谓 Java 类文件,就是通常用 javac 编译器产生的 .class 文件。这些文件具有严格定义的格式。Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件。

Java 类文件是 8 位字节的二进制流。数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减少存储空间。一个简单的Hello World程序

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

经过 javac 编译后,得到的类文件HelloWorld.class,该文件中是由十六进制符号组成的,这一段十六进制符号组成的长串是严格遵守 Java 虚拟机规范。用vim查看HelloWorld.class

vim HelloWorld.class

打开文件后输入

:%!xxd

按回车即可看到如下一串串十六进制符号

HelloWorld.class文件构成如下:


从上图中可以看到,一个 Java 类文件大致可以归为 10 个项:

3、ASM库的结构

4、ASM Core API

我们来重点看下ClassVisitor
ClassVisitor类的API如下

image.png

4.1 visit

    /**
     * 可以拿到类的详细信息
     *
     * @param version jdk的版本: 52 代表jdk版本 1.8;51 代表jdk版本 1.7
     * @param access 类的修饰符:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED、ACC_FINAL、ACC_SUPER
     * @param name 类的名称:以路径的形式表示 com/joker/demo/TestClass
     * @param signature 泛型信息:未定义泛型,则该参数为null
     * @param superName 表示当前类所继承的父类
     * @param interfaces 表示类所实现的接口列表
     */
    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
    }

类的修饰符
类的修饰符以“ACC_开头”,可以作用到类级别上的修饰符主要有下面这些

修饰符 含义
ACC_PUBLIC public
ACC_PRIVATE private
ACC_PROTECTED protected
ACC_FINAL final
ACC_SUPER extends
ACC_INTERFACE 接口
ACC_ABSTRACT 抽象类
ACC_ANNOTATION 注解类型
ACC_ENUM 枚举类型
ACC_DEPRECATED 标记了@Deprecated注解的类
ACC_SYNTHETIC javac生成

4.2 visitAnnotation

    /**
     * 当扫描器扫描到类注解声明时进行调用
     *
     * @param desc 注解类型(签名类型)
     * @param visible 注解是否可以在 JVM 中可见
     * @return
     */
    @Override
    AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return super.visitAnnotation(desc, visible)
    }

4.3 visitField

/**
     * 当扫描器扫描到类中字段时进行调用
     *
     * @param access 修饰符
     * @param name 字段名
     * @param desc 字段类型
     * @param signature 泛型描述
     * @param value 默认值
     * @return
     */
    @Override
    FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return super.visitField(access, name, desc, signature, value)
    }

4.4 visitMethod

    /**
     * 当扫描器扫描到类的方法时调用
     *
     * @param access 方法的修饰符
     * @param name 方法名
     * @param desc 方法签名
     * @param signature 表示泛型相关的信息
     * @param exceptions 表示将会抛出的异常,如果方法没有抛出异常,则参数为空
     * @return
     */
    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return super.visitMethod(access, name, desc, signature, exceptions)
    }

方法的修饰符
可以作用到方法级别上的修饰符主要有下面这些

修饰符 含义
ACC_PUBLIC public
ACC_PRIVATE private
ACC_PROTECTED protected
ACC_STATIC static
ACC_FINAL final
ACC_SYNCHRONIZED 同步的
ACC_VARARGS 不定参数个数的方法
ACC_NATIVE native类型方法
ACC_ABSTRACT 抽象的方法
ACC_DEPRECATED 标记了@Deprecated注解的类
ACC_SYNTHETIC javac生成

方法的签名格式
(参数列表)返回值类型

在ASM中不同的类型对应不同的代码,详细的对应关系如下表

代码 类型
I int
B byte
C char
D double
F float
J long
S short
Z boolean
V void
[...; 数组
[[...; 二维数组
[[[...; 三维数组

方法参数列表对应的方法签名示例如下

参数列表 方法参数
String[] [Ljava/lang/String;
String[][] [[Ljava/lang/String;
int,String,String[] ILjava/lang/String;[Ljava/lang/String;
int,boolean,long,String[],double IZJ[Ljava/lang/String;D
Class<?>, String, Object...paramType Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;
int[] [I

4.5 visitEnd

    /**
     * 当扫描器完成类扫描时才会调用
     */
    @Override
    void visitEnd() {
        super.visitEnd()
    }

5、ASM练手demo实现统计方法时长代码插桩

5.1添加ASM依赖

implementation 'org.ow2.asm:asm-all:5.2'

5.2定义一个HelloWorld类

public class HelloWorld {

    public void sayHello() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5.3通过javac命令执行HelloWorld.java得到HelloWorld.class

目前桌面上已经生成了HelloWorld.class字节码文件

5.4新建一个ASMTest类,从桌面读取HelloWorld.class文件,通过ASM读取HelloWorld.class文件,并将打印sayHello()方法调用时长的代码插桩到sayHello()方法中,输出新的字节码文件OutputHelloWorld.class到桌面

public class ASMTest {

    public static void redefineHelloWorldClass() {
        try {
            InputStream inputStream = new FileInputStream("/Users/jokerwan/Desktop/HelloWorld.class");
            // 1. 创建 ClassReader 读入 .class 文件到内存中
            ClassReader reader = new ClassReader(inputStream);
            // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            // 3. 创建自定义的 ClassVisitor 对象
            ClassVisitor change = new ChangeVisitor(writer);
            // 4. 将 ClassVisitor 对象传入 ClassReader 中
            reader.accept(change, ClassReader.EXPAND_FRAMES);

            System.out.println("Success!");
            // 获取修改后的 class 文件对应的字节数组
            byte[] code = writer.toByteArray();
            try {
                // 将二进制流写到本地磁盘上
                FileOutputStream fos = new FileOutputStream("/Users/jokerwan/Desktop/OutputHelloWorld.class");
                fos.write(code);
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("Failure!");
        }
    }

    static class ChangeVisitor extends ClassVisitor {

        ChangeVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
            if (name.equals("<init>")) {
                return methodVisitor;
            }
            return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
        }
    }

    static class ChangeAdapter extends AdviceAdapter {
        private int startTimeId = -1;

        private String methodName = null;

        ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
            methodName = name;
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            startTimeId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitIntInsn(LSTORE, startTimeId);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            int durationId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, startTimeId);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationId);
            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("The cost time of " + methodName + "() is ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationId);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(" ms");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)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);

        }
    }
}

5.5通过单元测试执行ASMTest.redefineHelloWorldClass();

public class ExampleUnitTest {
    @Test
    public void testASM() {
        ASMTest.redefineHelloWorldClass();
    }
}

OutputHelloWorld.class已经输出到桌面

OutputHelloWorld.class拖到Android Studio中,Android Studio会将字节码文件反编译为java文件,反编译后的代码如下

可以看到我们成功通过ASM将统计运行时长的代码插入到sayHello()方法中。

demo代码如下
https://github.com/isJoker/ASM_Demo

参考文章
https://asm.ow2.io/developer-guide.html#classreader
https://www.ibm.com/developerworks/cn/java/j-lo-asm30/

上一篇 下一篇

猜你喜欢

热点阅读