ASM bytecode staking

First, what is pile insertion

In the hot fix solution published by Qzone, the Javaassist library is used to insert a code into the constructor of the class to resolve the CLASS_ISPREVERIFIED problem. Including the realization of Instant Run and the thermal repair meituan Robus based on Instant Run, all use the piling technology.

Staking is inserting or replacing a piece of code. Bytecode piling, as its name implies, is the operation of modifying the Class file and modifying or enhancing the original code logic after the source code is compiled into bytecode (Class) and before dex is generated under Android.

We need to check the method execution time. If each method needs to be added manually, we need to delete the corresponding code one by one when it is not needed. One or two methods is fine, but 10 or 20 are much more troublesome! So annotations can be used to mark the methods that need to be staked, combined with compiled bytecode manipulation to help us automatically insert and turn off the staked when not needed. This AOP thinking lets us focus only on the poked code itself.

Bytecode manipulation framework

As mentioned above, Qzone uses Javaassist for bytecode piling. Besides Javaassist, there is a more widely used ASM framework, which is also a bytecode manipulation framework. Instant Run and AspectJ use ASM to achieve their respective functions.

JSON data, which we are familiar with, is text-based, and we can easily generate and modify JSON data by knowing its rules. Similarly, Class bytecodes have their own rules (formats). Manipulating JSON makes it very easy to generate and modify JSON data with GSON. Bytecode Class can also be modified with Javassist/ASM.

The bytecode manipulation framework is used to generate or modify Class files, so in Android the bytecode framework itself does not need to be packaged into APK, only the generated/modified classes need to be packaged into APK. It works after the Class is generated in the Android packaging process above and before the dex is packaged.

3. Use of ASM

Because ASM has better performance and more flexible lines than Javassist, this article will focus on using ASM. We can test bytecode modification in A Java application before we actually use Android.

3.1. Introduce ASM into AS

ASM can be imported directly from the JCenter () repository, so you can search through :bintray.com/

Click the workpiece marked in the figure to enter, you can see that the latest official version is: 7.1.

Therefore, we can add AS:

Also, note that we introduced the testImplementation, which means that we can only use this framework in Our Java unit tests and has no impact on our dependencies in Android.

Android projects that use Gradle in AS automatically create Java unit tests and Android unit tests. The test code is in Test and androidTest respectively.

3.2. Prepare piles to be inserted Class

Create a Java class under test/ Java:

public class InjectTest {
	
    public static void main(String[] args) {}}Copy the code

Since we are working with bytecode staking, we can go to test/ Java and compile this class using Javac to generate the corresponding class file.

javac InjectTest.java
Copy the code

3.3. Pile insertion

Since there is no output code in the main method, we type the command: Java InjectTest execution of this Class will not produce any output. Next, we use ASM to insert into the main method the log output from the initial diagram that records the execution time of the function.

Write test methods in unit tests

 		/** * 1, class */
        FileInputStream fis = new FileInputStream
                ("xxxxx/test/java/InjectTest.class");

        /** ** /
        //class bytecode reading and parsing engine
        ClassReader cr = new ClassReader(fis);
        // The writer COMPUTE_FRAMES automatically COMPUTE_FRAMES everything, making subsequent operations easier
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        // Parse and write the result to CW EXPAND_FRAMES: the stack graph is accessed in extended format
        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();
Copy the code

We won’t discuss the design of the ASM framework itself here. The code above takes the class generated in the previous step, and when ASM completes the staking, prints the result to the test/java2 directory. The key point lies in the second step, how to insert the pile.

The class data is handed to the ClassReader and parsed, similar to XML parsing, in an event-driven form to the ClassAdapterVisitor, the first parameter to accept.

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(Methods: "" + name + "Signature." + desc);

            MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                    exceptions);
            return newMethodAdapterVisitor(api,mv, access, name, desc); }}Copy the code

The analysis results are obtained through the ClassAdapterVisitor, where a class has methods, annotations, properties, and so on, So ClassReader will call the visitXX methods in the ClassAdapterVisitor that correspond to visitMethod, visitAnnotation, visitField.

Our goal is to do a function peg, so override the visitMethod method method, where we return a MethodVisitor method parser object. A method’s parameters, annotations, and method body need to be analyzed and processed in the MethodVisitor.

package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/** * AdviceAdapter: subclass * extends methodVisitor to make method analysis easier */
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);
    }


    /** ** the annotation above the analysis method * is here?? * <p> * Determine if the current method uses injectTime. If it does, we need to pin the method. * *@param desc
     * @param visible
     * @return* /
    @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(a) {
        super.onMethodEnter();
        if (inject) {
            // What happens when you finish executing? To a local variable
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis"."()J"));

            start = newLocal(Type.LONG_TYPE); // Create a local LONG variable
            // Records the result of the method execution to the created local variablestoreLocal(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;"));

            // Allocate memory and push the dUP onto the top of the stack to let the INVOKESPECIAL below know whose constructor to execute to create 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;"));

            / / subtraction
            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")); }}}Copy the code

MethodAdapterVisitor descends from AdviceAdapter, which is a subclass of MethodVisitor that wraps instruction insertion methods, making it more intuitive and simple.

In the code above, onMethodEnter calls back when it enters a method, so inserting instructions into the method is adding some code at the beginning of the whole method. We need to insert long s = System.currentTimemillis (); . In onMethodExit the output code is inserted at the end of the method.

@Override
    protected void onMethodEnter(a) {
        super.onMethodEnter();
        if (inject) {
            // What happens when you finish executing? To a local variable
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis"."()J"));

            start = newLocal(Type.LONG_TYPE); // Create a local LONG variable
            // Records the result of the method execution to the created local variablestoreLocal(start); }}Copy the code

How do I write this code? Long s = system.currentTimemillis (); The relative instruction of this code. We can start by writing a code

void test(a){
    // Insert the code
    long s = System.currentTimeMillis();
    /** * method implementation code.... * / 
    // Insert the code
	long e = System.currentTimeMillis();
	System.out.println("execute:"+(e-s)+" ms.");
}
Copy the code

Then use thejavacCompile to Class and use it againjavap -cView bytecode instructions. Plug-ins can also be used to view, so we do not need to manually execute various commands.

After the installation is complete, you can right click on the source code of the class that needs to be staked:

Click the ASM Bytecode Viewer and it will pop up

So line 20 :long s = system.currentTimemillis (); It contains two instructions :INVOKESTATIC and LSTORE.

Back to the onMethodEnter method

@Override
    protected void onMethodEnter(a) {
        super.onMethodEnter();
        if (inject) {
            //invokeStatic, invokes static methods
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis"."()J"));
		   // Create a local LONG variable
            start = newLocal(Type.LONG_TYPE); 
            The store directive stores the result of method execution from the operand stack to local variablesstoreLocal(start); }}Copy the code

OnMethodExit also writes code according to instructions. Finally, after the insertion, we can obtain the modified class data.

Iv. Implementation in Android

In Android, the first problem we need to consider is how to get all the Class files to determine whether we need to pile. Transform does just that.