1. Introduce the ASM

ASM is a Java bytecode manipulation framework that can be used to dynamically generate classes or enhance the functionality of existing classes. ASM can generate binary class files directly or dynamically change the behavior of classes before they are loaded into the Java virtual machine. The ASM framework provides common bytecode analysis and generation tools that can quickly generate or analyze classes.

In Android development, the Transform mechanism provided after Android Gradle version 1.5 allows third-party plugins to dynamically modify.class files before packaging them into dex. This provides an entry point for dynamically modifying bytecode files. Derived from a lot of “piling” functions, such as burying, insert log and so on.

ASM is used in many excellent projects:

  • OpenJDK, used to generateLambdaThe invocation of the point
  • The Groovy and Kotlin compilers
  • Gradle, for generating code at run time

ASM official provided an introduction to the e-book (pure English), of course, there are also Chinese version of the domestic, baidu search. Because involve a lot of code snippets in the book, I’ve been in the process of learning to code snippet into executable Java programs, and in making warehouse, warehouse address at https://github.com/dengshiwei/asm-module.

1.1 Framework Structure

Two major frameworks are provided in ASM for class generation and parsing. One is to represent classes in event-based form, called the core API, and the other is to represent classes in object-based form, called the tree API. This is similar to SAX and DOM parsing in XML file parsing.

In the core API, a class is represented by a series of events, each representing an element of the class, such as its header, field, method declaration, instruction, and so on. The event-based API defines a set of possible events and the order in which they must occur, provides a class parser (ClassReader) that generates an event for each parsed element, and a ClassWriter that generates compiled classes from the sequence of these events.

In the tree API, classes are represented by a tree of objects, such as a class represented by a ClassNode object and a method represented by a MethodNode object. Each object contains references to its constituent objects. For example, ClassNode contains a lot of methodNodes that make it up. The object-based API provides a way to translate a sequence of events representing a class into a tree of objects representing the same class, or vice versa, to represent an object tree as an equivalent sequence of events. In other words, object-based apis are built on top of event-based apis.

1.2 ASM source directory structure

We divide ASM by package name path, and the general class diagram is as follows.

It can be seen that there are roughly:

  • org.objectweb.asm: Classes defined in packages are core basedAPIRelated operation classes of;
  • org.objectweb.asm.commons: package that provides a useful class or methodAdapterMethod converter;
  • Org. Objectweb. Asm. Signature: take care class defines a generic related operation;
  • org.objectweb.asm.treeTree-based is defined in the: packageAPIClass, and some for events and treesAPIUtility classes for transformations;
  • Org. Objectweb. Asm. Tree. Analysis: package provides a common analytical framework and analyzer;
  • org.objectweb.asm.utilCore-based is provided in the packageAPICommon utility classes for.

org.objectweb.asm

The core API classes are defined in the package, with four abstract classes ClassVisitor, FieldVisitor, MethodVisitor, and AnnotationVisitor, Instructions for accessing fields, methods, and Annotations from.class bytecode files. In addition, the core API provides a ClassReader for converting a Java file to be accessed by the ASM API. The ClassWriter is used to generate bytecode files.

org.objectweb.asm.commons

The warranty provides useful utility classes and adapters. Such as AdaviceAdapter, LocalVariablesSorter, AnalyzerAdapter and so on. When mixing these utility classes, it is recommended that you extend functionality through delegates rather than inheritance.

org.objectweb.asm.signature

SignatureReader, SignatureVisitor, and SignatureWriter are used for read conversion, access, and generation of generics, respectively.

org.object.web.tree

Tree API operations are provided in the package. Such as ClassNode, which identifies a class, and MethodNode, which identifies a method.

org.objectweb.asm.tree.analysis

The package defines tree-based API class analysis and validation frameworks, such as the BasicInterpreter base parser and BasicVerifier base validator, which verify that byte instructions are called correctly.

org.object.asm.util

The package provides several utility classes for use during development debugging. For example, the CheckClassAdapter verifies that a bytecode file is correct, the CheckMethodAdapter verifies that a method call is correct, the Textifier outputs bytecode instructions, and the TraceClassVisitor prints out all access to a class. These classes are not typically used at runtime, which is why they are separated from the asM.jar core API.

2. The core API

In the ASM core API, class file processing is divided into: Class processing (ClassVisitor, ClassReader), AnnotationVisitor, MethodVisitor, and field processing.

2.1 ClassVisitor

Used to access Java class files.

When accessing a class file, its callback methods must be accessed in the following order: Visit visitSource visitModule 】 【 】 【 visitNestHost 】 【 visitPermittedclass 】 【 visitOuterClass 】 (visitAnnotation | visitTypeAnnotation | visitAttribute )* ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | * the visitEnd visitMethod).

Method description:

  • visit: Accesses the header of the class, whereversionRefers to the one used when the class is createdJDKFor example50On behalf ofJDK1.6,51On behalf ofJDK1.7.accessRepresents the access rights of a class, such aspublic ,private.nameRepresents the class name.signatureRepresents the signature of the class if the class is not generic or does not inherit from a generic classsignatureValue is empty.superNameRepresents the name of the parent class.interfacesRepresents the interface of the implementation;
  • visitSource: Access class source files;
  • visitModule: visitModuleModule,Java9To add the keywordmoduleA wrapper for defining code and data;
  • visitNestHost: Access nested classes;
  • visitOuterClass: Access external classes;
  • visitAnnotation: Access class annotations;
  • visitTypeAnnotationAnnotation to access the generic signature of a class
  • visitField: access classFieldField;
  • visitMethod: access class methods;
  • visitEnd: the end.

In ASM’s core API, there are three components based on ClassVisitor:

  • ClassReaderUsed to parse parse class files, converted toASMThe format that the framework can analyze. Then, inacceptMethod to receive aClassVisitorObject is used to access the class;
  • ClassWriterClassVisitorSubclass, directly through the binary form of class generation;
  • ClassVisitorA class can be thought of as an event filter that performs access traversal over the class.

Sample code:

fun main(a) {
    In the first case, if the version version is changed only in the visit method using classwrite in the ConvertVisitor, only the visitor will be in the processed information log output. Need to be
    // Call classWrite in visitMethod instead of super
    val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
    val classReader = ClassReader("com.andoter.asm_example.part2.ConvertDemo")
    val classVisitor = ChangeVersionVisitor(Opcodes.ASM7, classWriter)
    classReader.accept(classVisitor, ClassReader.SKIP_CODE)
}
Copy the code
1. ClassReader

Use to convert a Java class file into a structure that ClassVisitor can access. It has four constructors, respectively supporting byte[], InputStream and File Path. The constructors of these three input types are as follows:

  • ClassReader(byte[] classFile): builds with an array of file bytes as argumentsClassReader;
  • ClassReader(java.io.InputStream inputStream): takes file byte stream as parameter;
  • ClassReader(java.lang.String className): Takes the full file path name as the parameter.

The ClassReader Class also provides methods for reading Class file information, such as getClassName(), getAccess(), and readByte(int offset). One of the more important methods is the Accept method, which receives a ClassVisitor object to complete the method to the bytecode.

accept(ClassVisitor classVisitor, int parsingOptions)
Copy the code

ParsingOptions Conversion parameters can be:

  • EXPAND_FRAMES: handlingStackMapTableProperty information is expanded by default for class files smaller than JDK1.6, and compressed by default for others. With this configuration,StackMapTableProperty information is all expanded and accessible;
  • SKIP_CODE: ignoreCodeAttribute information (attribute_info);
  • SKIP_DEBUG: ignoreSourceFile,SourceDebugExtension,LocalVariableTable,LocalVariableTypeTable,LineNumberTableMethodParametersAttribute information (attribute_info) at the same timevisitXXMethod will not be called, for examplevisitSource,visitLocalVariable;
  • SKIP_FRAMES: ignoreStackMapStackMapTableAttribute information corresponding tovisitFrame()Method will not be called.

Sample code:

fun main(a) {
    val classReader = ClassReader("java.util.ArrayList")
    val classVisitor = ClassPrintVisitor(Opcodes.ASM7)
    classReader.accept(classVisitor, ClassReader.SKIP_DEBUG)
}
Copy the code
2. ClassWriter

Used to generate bytecode files that conform to JVM specifications, either alone or in conjunction with the ClassReader or ClassVisitor adapter to modify existing class files. ClassWriter provides two ways to create a ClassWriter, either by itself or with a ClassReader as an argument.

  • ClassWriter(int flags): Create a newClassWriterObject;
  • ClassWriter(ClassReader classReader, int flags)To:ClassReaderCreating new objects as parameters can greatly improve the efficiency of modification.

In the above constructor, Flag has two types of values:

  • COMPUTE_FRAMES: automatically computes stack frame information, i.e. computes all, must be calledvisitMaxs(int), but you don’t have to call itvisitFrame();
  • COMPUTE_MAXS: automatically calculates the largest operand stack and the largest local variable table, but must be calledvisitMaxs(int)Method, which can take any argument and will be overridden by recalculation, but requires the computation of the frame itself;
  • 0: does not calculate anything, the size of frames, local variables, operand stacks must be calculated manually by the developer.

The COMPUTE_MAXS option reduces ClassWriter’s speed by 10%, while using the COMPUTE_FRAMES option reduces it by half.

ClassWriter is a subclass of ClassVisitor, so the visitXX methods have the ability to generate fields or methods in ClassWriter. For example, to generate a field, we call visitField(), and to generate a method that calls visitMethod. Finally, you must call the visitEnd() method to signal the end.

Follow the example in the ASM book:

fun main(a) {
    val classWriter = ClassWriter(0)
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE,
        "pkg/Comparable".null."java/lang/Object", arrayOf("pkg/Mesureable"))
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS"."I".null,  -1).visitEnd()
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL"."I".null.0).visitEnd()
    classWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER"."I".null.1).visitEnd()
    classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo"."(Ljava/lang/Object;) I" ,null.null).visitEnd()
    classWriter.visitEnd()
    val writeByte = classWriter.toByteArray()

    // Output the bytecode, and then view the output through the javap directive
    ClassOutputUtil.byte2File("asm_example/files/Comparable.class", writeByte)
}
Copy the code

Now you can install the ASM Bytecode Outline plugin in your IDE. When enabled, select the file and double-click right on it or in the “Code for your IDE” TAB. Select Show Bytecode Outline to view the transformed Bytecode and ASM Bytecode instructions directly.

3. TraceClassVisitor

ClassWriter outputs an array of bytes, which is not very useful for verifying that the modified bytecode is as expected. TraceClassVisitor is provided in ASM to output bytecode instructions. Or you can output the ClassWriter byte array to a File via the File File stream operation, and then use the JavAP directive to see if it meets expectations. This is done by delegating a ClassWriter object to the TraceClassVisitor.

Sample code:

fun main(a) {
    val classWriter = ClassWriter(0)
    /* Use the TraceClassVisitor and use the System.out stream to output the results. There's another way to write it when you're testing, which is to print it to a file. PrintWriter (" asm_example/files/TraceClassVisitorDemo. Class "), but the output file by javap -v command view complains. You may refer to details: https://stackoverflow.com/questions/63443099/asm-traceclassvisitor-output-file-is-error * /
    val traceClassWriter =
        TraceClassVisitor(classWriter, PrintWriter(System.out))
    traceClassWriter.visit(
        Opcodes.V1_7,
        Opcodes.ACC_PUBLIC + Opcodes.ACC_INTERFACE + Opcodes.ACC_ABSTRACT,
        "com.andoter.asm_example.part2/TraceClassVisitorDemo".null."java/lang/Object".null
    )
    traceClassWriter.visitSource("TraceClassVisitorDemo.class".null)
    traceClassWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "className"."Ljava/lang/String;".null."").visitEnd()
    traceClassWriter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "classVersion"."I".null.50).visitEnd()
    traceClassWriter.visitMethod(
        Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,
        "getTraceInfo"."()Ljava/lang/String;".null.null
    ).visitEnd()
    traceClassWriter.visitEnd()

    ClassOutputUtil.byte2File("asm_example/files/TraceClassVisitorDemo1.class", classWriter.toByteArray())
}
Copy the code
4. CheckClassAdapter

The ClassWriter class does not verify that its methods are called in the right order and that the arguments are valid. Therefore, it is possible to generate invalid classes that are rejected by the Java virtual machine validator. In ASM you can use the CheckClassAdapter class for advance detection. This is done by delegating a ClassWriter object to the CheckClassAdapter.

Sample code:

fun main(a) {
    val classWriter = ClassWriter(0)
    val checkClassAdapter = CheckClassAdapter(classWriter)
    checkClassAdapter.visit(
        Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE,
        "pkg/Comparable".null."java/lang/Object1", arrayOf("pkg/Mesureable"))
    checkClassAdapter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS"."I".null,  -1).visitEnd()
    checkClassAdapter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL"."I".null.0).visitEnd()
    checkClassAdapter.visitField(Opcodes.ACC_PUBLIC+ Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER"."I".null.1).visitEnd()
    // For example, we changed Ljava/lang/Object to Ljava/lang/Object1
    checkClassAdapter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo"."(Ljava/lang/Object;) I" ,null.null).visitEnd()
    checkClassAdapter.visitEnd()
    val writeByte = classWriter.toByteArray()

    // Output the bytecode, and then view the output through the javap directive
    ClassOutputUtil.byte2File("asm_example/files/Comparable.class", writeByte)
}
Copy the code

Other knowledge involved in the ClassVisitor, such as converting a class, removing a class member, or adding a class member, can be found in the ASM Introduction.

2.2 MethodVisitor

Used for processing methods, such as accessing a method, or generating methods.

Methods are accessed in the following order: (visitParameter) * (visitAnnotationDefault 】 【 visitAnnotation | visitAnnotableParameterCount | 【 visitCode visitParameterAnnotation visitTypeAnnotation | visitAttribute) * (visitFrame | visit < I > X < / I > Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | * visitMaxs 】 the visitEnd visitLocalVariableAnnotation | visitLineNumber).

Method description:

  • visitCode: start access method;
  • visitParameter(String name, int access): Access method parameters;
  • visitAnnotation: Annotation of access methods;
  • visitParameterAnnotation: annotation of access method parameters;
  • visitFrame: Accesses the current stack frame, that is, the current state of the local variable table and operand stack;
  • visitFieldInsn: The instruction to access a field, that is, load a field (load) or save a field value (store);
  • visitIincInsn: Visit aIINCInstruction;
  • visitIntInsn(int opcode, int operand): Visit aintNumeric type instruction whenintThe values1 ~ 5usingICONSTInstruction, value- 128 ~ 127usingBIPUSHInstruction, value- 32768 ~ 32767usingSIPUSHInstruction, value- 2147483648 ~ 2147483647usingldcInstruction;
  • visitInvokeDynamicInsn: Visit ainvokedynamicInstructions, generallyLambdaDuring a visit;
  • visitJumpInsn(int opcode, Label label): Visit aJumpInstruction;
  • visitLdcInsn(Object value): Visit aLDCConstant and loaded onto the operand stack;
  • visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface): instructions to access a method, that is, instructions to call a method, such as calling another method within a method;
  • visitVarInsn(int opcode, int var): Access local variable instruction, local variable instruction is to load or store local variable value instruction;
  • visitLineNumber(int line, Label start): Access method line number declaration;
  • visitTryCatchBlock: visittry.. catchBlock;
  • visitInsn(int opcode): Accesses a bytecode instruction, such asIADD,ISUB,F2L,LSHRAnd so on;
  • visitMax(int maxStack, int maxLocals): the maximum number of local variable tables and operand stacks of a method;
  • visitEnd: Method access ends and must be invoked when it ends.

MethodVisitor provides a lot of visitXXXInsn instructions for working with the local variable table and operand stack. However, in the process of use, these visitXXXInsn instructions have strict order requirements of bytecode instructions when accessing, for example, visitInsnAnnotation instruction must be called after Annotation instruction. VisitTryCatchBlock must be accessed before the Label to be accessed is used as a parameter, VisitLocalVariable, visitLocalVariableAnnotation and visitLineNumber must be accessed after the Label as a parameter.

We use ClassWriter in conjunction with MethodVisitor to generate the getF() method of the Bean class. Sample code:

class Bean{
    private var f:Int =1
    fun getF(a): Int {
        return this.f
    }

    fun setF(value: Int) {
        this.f = value
    }
}

/* // access flags 0x11 public final getF()I L0 LINENUMBER 9 L0 ALOAD 0 GETFIELD com/andoter/asm_example/part3/Bean.f : I IRETURN L1 LOCALVARIABLE this Lcom/andoter/asm_example/part3/Bean; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 */
fun main(a) {
    val classWriter = ClassWriter(0)
    classWriter.visit(
        Opcodes.V1_7,
        Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL,
        "pkg/Bean".null."java/lang/Object".null
    )
    val methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL, "getF"."()I".null.null)
    methodVisitor.visitCode() // Start generating methods
    methodVisitor.visitVarInsn(Opcodes.ALOAD, 0)	// From the local variable load position 0 to the operand stack, the first one being this
    methodVisitor.visitFieldInsn(Opcodes.GETFIELD, "pkg/Bean"."f"."I") // Read the f field of the Bean
    methodVisitor.visitInsn(Opcodes.IRETURN) / / return
    methodVisitor.visitMaxs(1.1)// Define the size of the local variator and operand stack that executes the stack frame
    methodVisitor.visitEnd() // The method access is complete
    classWriter.visitEnd()
    /* * output bytecode file: * package PKG; public final class Bean { public final int getF() { return this.f; }} * /
    ClassOutputUtil.byte2File("asm_example/files/Bean.class", classWriter.toByteArray())
}
Copy the code

In this sample code, we first get a MethodVisitor object from the ClassWriter’s visitMethod, then call visitCode to start generating, then read this, then read f, Finally, execute IREATUREN to return, call visitMaxs() to define the maximum of the local variable table and operand stack during the whole process, and call visitEnd to indicate the end.

1. Execution structure

The concept of local variable tables and operand stacks was mentioned many times in the previous introduction. In a Java virtual machine, thread stack data structure is exclusive, namely the implementation of each thread has its own stack, stack is composed of a lot of stack frames, each stack frame said a method call, when call a method, can be carried in medium voltage into a stack frame, at the same time, the method has been completed will pop up in this frame from the execution stack. So the stack frame for the currently executing method is at the top.

The main components of stack frame: local variable table, operand stack, dynamic link and method return address information.

A. Local variation scale

A local variable table is a storage space for a set of variable values used to store method parameters and local variables defined within a method. When.java is compiled into a.class file, the size of the local variable table and operand stack that needs to be allocated is determined.

** The capacity unit of a local Variable table is a Variable Slot. ** The maximum storage length of each variable slot is 32 bits. Therefore, one variable slot is used for byte, CHAR, Boolean, short, int, float, and reference, and two variable slots are used for double and long.

In the local variable table, the position of the local variable is located through the index, and the index value ranges from 0 to the maximum number of slots occupied by the local variable. If you are executing a member method of an object instance (rather than a static-modified method), the variable slot in the zeroth index of the local variable table is by default a reference to the object instance (this).

Variable slot multiplexing

In order to save the stack frame space, the variable slots in the local variable table can be reused. When one slot is used up, the variable slots can be handed over to other variables for reuse.

  • Storing a value in a local variable table is an illegal operation to load it in a different type, such as storeISTOREType, usingFLOADloading
  • It is legal to store a value to a location in a local variable table that is different from the original storage type

** These two features mean that the type of a local variable can change during method execution. For example, if you read a local variable at the end of the method and store an int in the first slot, it may be reused later, causing the value to change.

B. Operand stack

Like the local variable table, the maximum size of the operand stack to be allocated is determined at compile time. Each position in the operand stack can be any Java data type, with 32-bit data types occupying 1 and 64-bit data types occupying 2.

When a method is executed, the corresponding operand stack is empty. During the execution of the method, various bytecode instructions are written and fetched into the operand stack, namely, out/on operations, and the value of the final operand stack changes.

C. Bytecode instructions

A bytecode instruction consists of an opcode identifying the instruction and a fixed number of parameters:

  • An opcode is an unsigned byte value, that is, a byte code name, identified by a notation;
  • Parameters are static values that determine the exact instruction behavior, immediately following the opcode.

Bytecode instructions can be roughly divided into two classes, one for passing values between local variables and operand stacks. The other is used to pop and evaluate the values of the operand stack and push them onto the stack.

Common local variable manipulation instructions are:

  • ILOAD: used for loadingboolean,int,byte,shortcharType local variable to operand stack;
  • FLOAD: used for loadingfloatType local variable to operand stack;
  • LLOAD: used for loadinglangType local variable to operand stack, need to load two slotsslot;
  • DLOAD: used for loadingdoubleType local variable to operand stack, need to load two slotsslot;
  • ALOAD: used to load local variables of non-basic types into the operand stack, such as objects.

Common operand stack instructions are:

  • ISTORE: pops from the operand stackboolean,int,byte,shortcharType of a local variable and store it by its indexiIn the specified local variable;
  • FSTORE: pops from the operand stackfloatType of a local variable and store it by its indexiIn the specified local variable;
  • LSTORE: pops from the operand stacklongType of a local variable and store it by its indexiIn the specified local variable;
  • DSTORE: pops from the operand stackdoubleType of a local variable and store it by its indexiIn the specified local variable;
  • ASTORE: Used to pop up a local variable of a non-base type and store it in its indexiIn the specified local variable.
2. CheckMethodAdapter

Like the ClassVisitor, the MethodVisitor itself doesn’t check for the proper order of calls, so for Method operations, ASM provides a CheckMethodAdapter to check that the Method calls are in the correct order. This is done by delegating the MethodVisitor object to the CheckMethodAdapter.

Sample code:

fun main(a) {
    val classReader = ClassReader("com.andoter.asm_example.part3.MyMethodAdapter")
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    val classVisitor = object : ClassVisitor(Opcodes.ASM7, classWriter) {
        override fun visitMethod(
            access: Int,
            name: String? , descriptor:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor {
            var methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
            // Delegate MethodVisitor to the CheckMethodAdapter
            methodVisitor = CheckMethodAdapter(methodVisitor)
            return MyMethodAdapter(methodVisitor)
        }
    }
    classReader.accept(classVisitor, ClassReader.SKIP_DEBUG)
}
Copy the code
3. LocalVariablesSorter

The utility class renumbers local variables in the order in which they appear and makes it easy to create a newLocal variable using the newLocal method. For example, for a method with two arguments, when a new local variable is inserted, the new local variable index is 3. So this method is useful for inserting local variables into methods.

Sample code:

class AddTimerMethodAdapter4(
    private var owner: String,
    access: Int, descriptor: String? , methodVisitor: MethodVisitor? ) : LocalVariablesSorter(Opcodes.ASM7, access, descriptor, methodVisitor ){private var time:Int = -1

    override fun visitCode(a) {
        super.visitCode()
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
        time = newLocal(Type.LONG_TYPE) // Create a local variable
        mv.visitVarInsn(Opcodes.LSTORE, time)
    }

    override fun visitInsn(opcode: Int) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || Opcodes.ATHROW == opcode) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
            mv.visitVarInsn(Opcodes.LLOAD, time)
            mv.visitInsn(Opcodes.LSUB)
            mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer"."J")
            mv.visitInsn(Opcodes.LADD)
            mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer"."J")}super.visitInsn(opcode)
    }

    override fun visitMaxs(maxStack: Int, maxLocals: Int) {
        super.visitMaxs(maxStack + 4, maxLocals)
    }
}
Copy the code
4. AdaviceAdapter

This class is an abstract class that makes it easy to insert code at the beginning or at any end of a method. The main benefit is that you can detect constructors, which is what most of the adapter code is for. This class is a subclass of LocalVariablesSorter, so you can also create newLocal variables directly with the newLocal() method.

Sample code:

class AddTimerMethodAdapter6(
    private varowner: String, methodVisitor: MethodVisitor? , access:Int, name: String? , descriptor: String? ) : AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {override fun onMethodEnter(a) {
        mv.visitFieldInsn(GETSTATIC, owner, "timer"."J")
        mv.visitMethodInsn(
            INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false
        )
        mv.visitInsn(LSUB)
        mv.visitFieldInsn(PUTSTATIC, owner, "timer"."J")}override fun onMethodExit(opcode: Int) {
        mv.visitFieldInsn(GETSTATIC, owner, "timer"."J")
        mv.visitMethodInsn(
            INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false
        )
        mv.visitInsn(LADD)
        mv.visitFieldInsn(PUTSTATIC, owner, "timer"."J")}override fun visitMaxs(maxStack: Int, maxLocals: Int) {
        mv.visitMaxs(maxStack + 4, maxLocals)
    }
}
Copy the code

2.3 FieldVisitor

For detailed access to the Field Field, such as annotations on the Field.

When access according to the following order: (visitAnnotation | visitTypeAnnotation | visitAttribute) * visitEnd.

Method description:

  • visitAnnotation(String descriptor, boolean visible): Access annotations on fields
  • visitAttribute(Attribute attribute): Accesses properties on a field
  • visitEnd(): End
  • visitTypeAnnotation(): access fieldTypeAnnotation of type

FieldVisitor interface demo code:

class FiledVisitorPrinter(fieldVisitor: FieldVisitor?) : FieldVisitor(Opcodes.ASM7, fieldVisitor) {
    override fun visitEnd(a) {
        super.visitEnd()
        ADLog.info("visitEnd")}override fun visitAnnotation(descriptor: String? , visible:Boolean): AnnotationVisitor {
        ADLog.info("visitAnnotation, des = $descriptor, visiable = $visible")
        return super.visitAnnotation(descriptor, visible)
    }

    override fun visitTypeAnnotation(
        typeRef: Int,
        typePath: TypePath? , descriptor:String? , visible:Boolean
    ): AnnotationVisitor {
        return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible)
    }

    override fun visitAttribute(attribute: Attribute?). {
        super.visitAttribute(attribute)
    }
}
Copy the code

Example code for adding a field using FieldVisitor:

class AddFieldAdapter(private var version:Int, classVisitor: ClassVisitor) : ClassVisitor(version, classVisitor){
    var isExists = false
    lateinit var filedName: String
    private var filedAccessFlag: Int = Opcodes.ACC_PUBLIC
    lateinit var fieldDescription: String
    lateinit var classVisitor: ClassVisitor
    constructor(version: Int, classVisitor: ClassVisitor, filedName:String, filedAccess:Int, fieldDescription: String) :this(version, classVisitor){
        this.classVisitor = classVisitor
        this.filedAccessFlag = filedAccess
        this.fieldDescription = fieldDescription
        this.filedName = filedName
    }

    override fun visit(version: Int, access: Int, name: String? , signature:String? , superName:String? , interfaces:Array<out String>? {
        ADLog.info("visit, version = $version, access = ${AccessCodeUtils.accCode2String(access)}, name = ${name}, signature = $signature")
        super.visit(version, access, name, signature, superName, interfaces)
    }

    override fun visitField(access: Int, name: String? , descriptor:String? , signature:String? , value:Any?).: FieldVisitor? {
        ADLog.info("visitField: access=${AccessCodeUtils.accCode2String(access)},name=$name,descriptor=$descriptor," +
                "signature=$signature,value=$value")
        if (name == "name") {
            println("Name,value =")}if (this.filedName == name) {
            this.isExists = true
        }
        return super.visitField(access, name, descriptor, signature, value)
    }

    override fun visitEnd(a) {
        super.visitEnd()
        if(! isExists) {val filedVisitor = cv.visitField(filedAccessFlag, filedName, fieldDescription, null.null) filedVisitor? .visitEnd() } ADLog.info("visitEnd")}}Copy the code

In the sample code above, the presence of the field is checked in visitField, and if it is, no insert is done in visitEnd, and if not, the corresponding field is inserted.

1. CheckFieldAdapter

The FieldVisitor itself, like the ClassVisitor, does not check that the order of calls is correct, so for Field operations, ASM provides a CheckFieldAdapter to check that the order of calls is correct. This is done by delegating the FieldVisitor object to the CheckFieldAdapter.

fun main(a) {
    val classReader = ClassReader("com.andoter.asm_example.field.CheckFieldInsn")
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    classReader.accept(object: ClassVisitor(Opcodes.ASM7, classWriter){
        var isExist = false
        override fun visitField(
            access: Int,
            name: String? , descriptor:String? , signature:String? , value:Any?).: FieldVisitor? {
            if (name == "TAG") {
                isExist = true
            }

            return super.visitField(access, name, descriptor, signature, value)
        }

        override fun visitEnd(a) {
            super.visitEnd()
            if(! isExist) {val fieldVisitor = cv.visitField(Opcodes.ACC_PUBLIC, "TAG"."Ljava/lang/String;".null."CheckField")
                val checkFieldAdapter = CheckFieldAdapter(fieldVisitor)
                checkFieldAdapter.visitAnnotation("Lcom/andoter/Interface;".true)
                checkFieldAdapter.visitEnd()
            }
        }
    }, ClassReader.SKIP_DEBUG)
    
    ClassOutputUtil.byte2File("asm_example/files/CheckFieldInsn.class", classWriter.toByteArray())
}
Copy the code

In the sample code, we add a new field in visitEnd.

2. TraceFieldVisitor

Function function with the use of TraceClassVisitor. It is used by delegating the FieldVisitor object to TraceFieldVisitor. Referring to the example code above, let’s make some adjustments.

override fun visitEnd(a) {
    super.visitEnd()
    if(! isExist) {val fieldVisitor = cv.visitField(Opcodes.ACC_PUBLIC, "TAG"."Ljava/lang/String;".null."CheckField")
        val traceFieldVisitor = TraceFieldVisitor(fieldVisitor, Textifier())
        traceFieldVisitor.visitAnnotation("Lcom/andoter/Interface;".true)
        traceFieldVisitor.visitEnd()
    }
}
Copy the code

2.4 AnnotationVisitor

Used to access annotations.

The call order is as follows: (the visit | visitEnum | visitAnnotation | visitArray) * visitEnd

Method description:

  • visit(String name, Object value): Accesses the name and value of the annotation
  • visitEnum(String name, String descriptor, String value): access annotatedEnum
  • visitAnnotation(String name, String descriptor): Access nested annotations
  • visitArray(String name): access annotatedArray
  • visitEnd: End

Sample code:

@Deprecated(message = "deprecated")
class AnnotationPrinter {}fun main(a) {
    val classReader = ClassReader("com.andoter.asm_example.part4.AnnotationPrinter")
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    classReader.accept(object : ClassVisitor(Opcodes.ASM7, classWriter) {
        override fun visitAnnotation(descriptor: String? , visible:Boolean): AnnotationVisitor {
            return AnnotationPrinterVisitor(cv.visitAnnotation(descriptor, visible))
        }
    }, ClassReader.SKIP_DEBUG)
}


class AnnotationPrinterVisitor(annotationVisitor: AnnotationVisitor) : AnnotationVisitor(Opcodes.ASM7, annotationVisitor) {

    override fun visitEnd(a) {
        super.visitEnd()
        ADLog.info("visitEnd")}override fun visitAnnotation(name: String? , descriptor:String?).: AnnotationVisitor {
        ADLog.info("visitAnnotation, name = $name, descriptor = $descriptor")
        return super.visitAnnotation(name, descriptor)
    }

    override fun visitEnum(name: String? , descriptor:String? , value:String?). {
        ADLog.info("visitEnum, name = $name, descriptor = $descriptor, value = $value")
        super.visitEnum(name, descriptor, value)
    }

    override fun visit(name: String? , value:Any?). {
        super.visit(name, value)
        ADLog.info("visit, name = $name, value = $value")}override fun visitArray(name: String?).: AnnotationVisitor {
        ADLog.info("visitArray, name = $name")
        return super.visitArray(name)
    }
}
Copy the code

Also for annotations, the corresponding CheckAnnotationAdapter and TraceAnnotationVisitor are provided in ASM to help us detect the Annotation generated, and the usage is basically the same as that of ClassVisitor. Sample code for the CheckAnnotationAdapter and the TraceAnnotationVisitor are provided as references.

2.5 SignatureVisitor

Type signature processing for generics.

This Class handles signature processing for Method, Class, and Type.

  • Class signature access order:( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* (visitSuperclass visitInterface* )
  • Access order for method signatures: ( visitFormalTypeParameter visitClassBound? visitInterfaceBound* )* (visitParameterType* visitReturnType visitExceptionType* )
  • TypeAccess order of signatures: visitBaseType | visitTypeVariable | visitArrayType | ( visitClassType visitTypeArgument* ( visitInnerClassType visitTypeArgument* )* visitEnd ) )

This is currently used sparingly, and the sample code can be found in the ASM tutorial.

3. The tree API

In the tree API, classes are represented by a tree of objects, such as a class represented by a ClassNode object and a method represented by a MethodNode object. It works by passing each Node through a Node object representation, so there are many classes involved under the tree API package. Another thing to note is that many of the classes in the tree API are implemented as classes that inherit from the core API, such as ClassNode which inherits from ClassVisitor, and MethodNode which inherits from MethodVisitor implementation. So the tree API can be converted to an equivalent sequence of events.

This includes a number of XXXInsnNode command operation nodes, which are subclasses of AbstractInsnNode. At the same time, the tree API uses an InsnList object to represent a collection of directives. An AbstractInsnNode directive object can only appear in one InsnList.

3.1 ClassNode

Used to generate and represent a class object that inherits from ClassVisitor for implementation.

As you can see from its basic structure, each of the fields of this class corresponds to a class structure. For example, name indicates the name, signature indicates the type signature, fields indicates the constituent fields of a class, and methods indicates the constituent methods of a class.

We use ClassNode to generate a class with the following example code:

/* The process of generating a class using the tree API is to create a ClassNode object and initialize its fields. Again, take the example in 2.2.3: Package PKG; public interface Comparable extends Measurable { int LESS = -1; int EQUAL = 0; int GREATER = 1; int compareTo(Object o); } It takes about 30% more time and more memory to generate classes using the tree API. However, elements can be generated in any order, which may be convenient in some cases. * /
fun main(a) {
    valclassNode = ClassNode() classNode.version = Opcodes.V1_5 classNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_INTERFACE +  Opcodes.ACC_ABSTRACT classNode.name ="pkg/Comparable"
    classNode.superName = "java/lang/Object"
    classNode.interfaces.add("pkg/Measurable")
    classNode.fields.add(
        FieldNode(
            Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
            "LESS"."I".null,
            -1
        )
    )
    classNode.fields.add(
        FieldNode(
            Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
            "EQUAL"."I".null.0
        )
    )
    classNode.fields.add(
        FieldNode(
            Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
            "GREATER"."I".null.1
        )
    )
    classNode.methods.add(
        MethodNode(
            Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,
            "compareTo"."(Ljava/lang/Object;) I".null.null))}Copy the code

Generating classes using the tree API takes about 30% more time and consumes more memory. However, elements can be generated in any order, which may be convenient in some cases.

3.2 FieldNode

Fields used to generate and represent classes, implemented by inheriting FieldVisitor.

The sample code can be referenced to the code shown in ClassNode.

3.3 ClassVisitor Interacts with a ClassNode

In section 3.1, we saw how to create a ClassNode object. Let’s see how to convert a ClassNode object from a byte array read by a ClassReader.

1. The ClassWriter ClassNode
val classNode = ClassNode()
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
classNode.accept(classWriter)
Copy the code

Create a ClassNode object and pass the ClassWriter in using the Accept method.

2. ClassReader and ClassNode
val classNode = ClassNode()
val classReader = ClassReader("")
classReader.accept(classNode, FLAG)
Copy the code

Create a ClassNode object and pass it in using the Accept method of ClassReader. Because the ClassNode object itself is also a ClassVisitor object.

3.4 MethodNode

Methods used to generate and represent classes.

We will focus on the InsnList Instructions field. InsnList is a bidirectional linked list of directives whose links are stored in the AbstractInsnNode object. The AbstractInsnNode class is a superclass of the class that represents the bytecode instruction. It subclasses the Xxx InsnNode class that corresponds to the visitXxx Insn method of the MethodVisitor interface, for example, the VarInsnNode class that corresponds to the visitVarInsn method. So for an AbstractInsnNode object, it has the following characteristics:

  • aAbstractInsnNodeObject appears at most once in a list of instructions
  • aAbstractInsnNodeObjects cannot belong to more than one instruction list at the same time
  • If aAbstractInsnNodeAn instruction list must be removed from the original instruction list before it can be added to another instruction list
  • Adding all elements from one list to another instruction list empties the first list.

Tags and frames, as well as line numbers, although they are not directives, are represented by subclasses of the AbstractInsnNode class: LabelNode, FrameNode, and LineNumberNode.

Generates a method with MethodNode, and in the following example, generates the checkAndSetF method. Example code is as follows:

class MakeMethod {
    private var f : Int = 0
    fun checkAndSetF(f: Int) {
        if (f >= 0) {
            this.f = f
        } else {
            throw IllegalArgumentException()
        }
    }
}
Public void checkAndSetF(int f) {if (f >= 0) {this.f = f; } else { throw new IllegalArgumentException(); ILOAD 1 IFLT label ALOAD 0 ILOAD 1 PUTFIELD PKG /Bean f I GOTO end label: NEW java/lang/IllegalArgumentException DUP INVOKESPECIAL java/lang/IllegalArgumentException 
      
        ()V ATHROW end: RETURN */
      
fun main(a) {
    val mn = MethodNode()
    val insnList = mn.instructions
    insnList.add(VarInsnNode(Opcodes.ILOAD, 1))
    val label = LabelNode()
    insnList.add(JumpInsnNode(Opcodes.IFLE, label))
    insnList.add(VarInsnNode(Opcodes.ALOAD, 0))
    insnList.add(VarInsnNode(Opcodes.ILOAD, 1))
    insnList.add(FieldInsnNode(Opcodes.PUTFIELD, "pkg/Bean"."f"."I"))
    val endLabel = LabelNode()
    insnList.add(JumpInsnNode(Opcodes.GOTO, endLabel))
    insnList.add(endLabel)
    insnList.add(FrameNode(Opcodes.F_SAME, 0.null.0.null))
    insnList.add(TypeInsnNode(Opcodes.NEW,  "java/lang/IllegalArgumentException"))
    insnList.add(InsnNode(Opcodes.DUP))
    insnList.add(MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException"."<init>"."()V"))
    insnList.add(InsnNode(Opcodes.ATHROW))
    insnList.add(endLabel)
    insnList.add(FrameNode(Opcodes.F_SAME, 0 ,null.0.null))
    insnList.add(InsnNode(Opcodes.RETURN))
    mn.maxLocals = 2
    mn.maxStack = 2
}
Copy the code

You can also do a lot with MethodNode, such as converting methods, removing self-assignment from fields, and so on.

4. Method analysis

A method analysis module based on tree API is provided in ASM to analyze methods. Method analysis can be roughly divided into two types:

  • Data flow analysis: calculating the state of execution frames for each instruction of a method;
  • Control flow analysis: Calculating and analyzing the control flow diagram of a method. Control flow diagram node is instruction, if instructionjCan be followediWhen executed, the directed edge of the graph connects the two instructionsI and j.

At the same time, data flow analysis can be divided into forward analysis and reverse analysis.

  • Forward analysis: for each instruction, according to the state of the execution frame before the execution of this instruction, the state of the execution frame after the execution of this instruction is calculated;
  • Reverse analysis: For each instruction, the state of the execution frame before the execution of this instruction is calculated according to the state of the execution frame after the execution of this instruction.

Basic class diagram

  • Analyzer: Analyze the main class;
  • BasicInterpreter: Basic data flow analyzer, primarily as an empty implementation, for build purposesAnalyzerObject;
  • BasicVerifier: Basic data stream validator,BasicVerifierBasicInterpreterIs used to implement the bytecode instruction is correct verification;
  • SimpleVerifier: Simple data stream validator,SimpleVerifierBasicVerifierSubclass, which uses more collections to simulate the execution of bytecode instructions, so it can detect more errors;
  • InterpreterA class is an abstract class that uses theBasicValueClass defined in7To simulate the effect of bytecode instructions:
    • UNINITIALIZED_VALUEAll possible values
    • INT_VALUERefers to “allInt, short, byte, BooleancharValue”
    • FLOAT_VALUERefers to “allfloatValue”
    • LONG_VALUERefers to “alllongValue”
    • DOUBLE_VALUERefers to “alldoubleValue”
    • REFERENCE_VALUEAll objects and array values
    • RETURNADDRESS_VALUEUsed of subcoroutines

The basic use

ClassReader classReader = new ClassReader(bytecode);
ClassNode classNode = new ClassNode();
classReader.accept(classNode, ClassReader.SKIP_DEBUG);

for (MethodNode method : classNode.methods) {
  if (method.instructions.size() > 0) {
    Analyzer analyzer = new Analyzer(new BasicInterpreter()); // Arguments can be BasicVerifier or SimpleVerifier
    analyzer.analyze(classNode.name, method);
    Frame[] frames = analyzer.getFrames();
    // Elements of the frames array now contains info for each instruction
    // from the analyzed method. BasicInterpreter creates BasicValue, that
    // is using simplified type system that distinguishes the UNINITIALZED,
    // INT, FLOAT, LONG, DOUBLE, REFERENCE and RETURNADDRESS types.. }}Copy the code

After analysis, the calculated frames returned by the Analyzer.getFrames method are null for unreachable instructions, regardless of the Interpreter implementation. This feature can be used to make it very easy to implement a RemoveDeadCodeAdapter class to remove unwanted code, as shown in the following example:

class RemoveDeadCodeAdapter(
    varowner: String? .var access: Int.varname: String? .vardesc: String? .var methodVisitor: MethodVisitor
) : MethodVisitor(Opcodes.ASM7, MethodNode(access, name, desc, null.null)) {

    override fun visitEnd(a) {
        val methodNode = mv as MethodNode
        val analyzer = Analyzer<BasicValue>(BasicInterpreter())
        try {
            analyzer.analyze(owner, methodNode)
            val frames = analyzer.frames
            val insns = methodNode.instructions.toArray()
            for (i in insns.indices) {
                if (frames[i] == null && insns[i] !is LabelNode) {
                    methodNode.instructions.remove(insns[i])
                }
            }
        } catch (ignore: AnalyzerException) {

        }
        methodNode.accept(methodVisitor)
    }
}

fun main(a) {
    val classReader = ClassReader("com.andoter.asm_example.part8.RemoveDeadCode")
    val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
    classReader.accept(object : ClassVisitor(Opcodes.ASM7, classWriter) {
        private var name: String? = ""
        override fun visit(
            version: Int,
            access: Int,
            name: String? , signature:String? , superName:String? , interfaces:Array<out String>? {
            this.name = name
            super.visit(version, access, name, signature, superName, interfaces)
        }

        override fun visitMethod(
            access: Int,
            name: String? , descriptor:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor? {
            ADLog.info("visitMethod, name = $name, desc = $descriptor")
            val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
            if(methodVisitor ! =null) {
                return RemoveDeadCodeAdapter(this.name, access, name, descriptor, methodVisitor)
            }
            return methodVisitor
        }
    }, ClassReader.SKIP_DEBUG)

    ClassOutputUtil.byte2File("asm_example/files/RemoveDeadCode.class", classWriter.toByteArray())
}
Copy the code

5. To summarize

This article only provides a brief introduction to the basic functions of ASM for a quick start. For more detailed guidance, you can refer to asM4 – Guide for system learning. Finally, I recommend two open source libraries for using ASM applications.

  • Sa-sdk-android-plugin2 is the practice of intelligent data burying point industry
  • Bytedance tiktokAndroidteamByteXProject:Github.com/bytedance/B…

Refer to the article

  1. ASMOfficial guidance manual:Asm. Ow2. IO/asm4 – guide….
  2. Blog.csdn.net/qian520ao/a…