Android ASM字节码插桩

1.ASM

ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。

插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。

67649b728c5a411b94b084c231547789.png

 349110f71c7f448c94ff04febc42766d.png

比如需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。

ASM框架就是字节码操作框架。

我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。

10e89025cfb1483ea20cc779abefe636.jpg

 字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。

2.ASM的使用

①引入依赖:

6669b21ba51d4879865095de3abe2be1.png

 使用testImplementation引入,表示只能在Java的单元测试中使用这个框架,对Android中的依赖关系没有任何影响。

注:AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。

②准备待插桩Class

在 test/java下面创建一个Java类:

public class InjectTest {

       public static void main(String[] args) {

    }

}

由于我们操作的是字节码插桩,所以可以进入 test/java下面使用 javac对这个类进行编译生成对应的class文件。

javac InjectTest.java

③执行插桩

因为main方法中没有任何输出代码,我们输入命令:javaInjectTest执行这个Class不会有任何输出。那么我们接下来利用 ASM,向 main方法中插入一开始图中的记录函数执行时间的日志输出。

在单元测试中写入测试方法:

//1、准备待分析的class

FileInputStream fis = new FileInputStream

("xxxxx/test/java/InjectTest.class");

//2、执行分析与插桩

//class字节码的读取与分析引擎

ClassReader cr = new ClassReader(fis);

// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问

cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);

//3、获得结果并输出

byte[] newClassBytes = cw.toByteArray();

File file = new File("xxx/test/java2/");

file.mkdirs();

FileOutputStream fos = new FileOutputStream

("xxx/test/java2/InjectTest.class");

fos.write(newClassBytes);

fos.close();

上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java2目录下。其中关键点就在于第2步中,即如何进行插桩。

把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor。

public class ClassAdapterVisitor extends ClassVisitor {

    public ClassAdapterVisitor(ClassVisitor cv) {

        super(Opcodes.ASM7, cv);

    }

    @Override

    public MethodVisitor visitMethod(int access, String name, String desc, String signature,String[] exceptions) {

       System.out.println("方法:" + name + " 签名:" + desc);

        MethodVisitor mv = super.visitMethod( access, name, desc, signature, exceptions);

        return new MethodAdapterVisitor(api,mv, access, name, desc);

    }

}

分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader将会调用ClassAdapterVisitor中对应的visitMethod、 visitAnnotation、 visitField这些 visitXX方法。

我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理。

//AdviceAdapter: 子类,对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析

public class MethodAdapterVisitor extends AdviceAdapter {

    private Boolean inject;

    protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {

        super(api, methodVisitor, access, name, descriptor);

    }

 //分析方法上面的注解,判断当前这个方法是不是使用了injecttime,如果使用了,就需要对这个方法插桩;没使用,就不管了

    @Override

    public AnnotationVisitor visitAnnotation(String desc, Boolean visible) {

        if(Type.getDescriptor( ASMTest.class).equals(desc)) {

            System.out.println(desc);

           inject = true;

        }

        return super.visitAnnotation(desc, visible);

    }

    private int start;

    @Override

    protected void onMethodEnter() {

        super.onMethodEnter();

        if (inject) {

            //执行完了,记录到本地变量中

            invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));

            start = newLocal(Type.LONG_TYPE);

            //创建本地 LONG类型变量,记录 方法执行结果给创建的本地变量

            storeLocal(start);

        }

    }

    @Override

     protected void onMethodExit(int opcode) {

        super.onMethodExit(opcode);

        if (inject){

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

            //分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder

            newInstance(Type.getType( "Ljava/lang/StringBuilder;"));

            dup();

            invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V"));

            visitLdcInsn("execute:"); 

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

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

        }

    }

}

MethodAdapterVisitor继承自 AdviceAdapter,其实就是 MethodVisitor 的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。

上述代码中 onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。

@Override

protected void onMethodEnter() {

    super.onMethodEnter();

    if (inject) {

       //执行完了怎么办?记录到本地变量中

        invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));

        start = newLocal(Type.LONG_TYPE);

        //创建本地 LONG类型变量,记录 方法执行结果给创建的本地变量

        storeLocal(start);

   }

}

这里面的代码怎么写?其实就是 longs=System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码:

void test(){

     //插入的代码

     long s = System.currentTimeMillis();

     System.out.println("execute:"+(e-s)+" ms.");

}

然后使用 javac编译成Class再使用 javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

093831f1632a4e4eb1ba93b5df2b3b8e.jpg

 安装完成之后,可以在需要插桩的类源码中点击右键:

d133b44836a14bc892836db7a15340c1.png

 点击ASM Bytecode Viewer之后会弹出

cebbad0afcd44ea3ad75f96ab46bc146.jpg

 所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATIC与 LSTORE。

再回到 onMethodEnter方法中:

@Override

protected void onMethodEnter() {

    super.onMethodEnter();

        if (inject) {

            //invokeStatic指令,调用静态方法

            invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));

          //创建本地 LONG类型变量

          start = newLocal(Type.LONG_TYPE);

         //store指令 将方法执行结果从操作数栈存储到局部变量

         storeLocal(start);

     }

}

而 onMethodExit也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/125586333