一起来学字节码插桩:ASM Tree API

一 ASM介绍

ASM是一个通用的Java字节码操作和分析框架。它可用于修改现有类直接以二进制形式动态生成类ASM提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。

ASM提供了与其他Java字节码框架类似的功能,但更关注性能。因为它的设计和实现尽可能的小和快,它非常适合在动态系统中使用(但当然也可以以静态的方式使用,例如在编译器中)。关于ASM更多介绍,可以参见 ASM官网

ASM 从组成结构上可以分成两部分,一部分为Core API,另一部分为Tree API

  • Core API 包括asm.jarasm-util.jarasm-commons.jar
  • Tree API 包括 asm-tree.jarasm-analysis.jar

本文主要讲解Tree API 的相关使用。

二 Tree API

我们知道Class字节码 是由无符号数两种数据结构组成。而 ASM Tree API 可以认为是对上面两种结构进行了进一步封装以简化使用。

使用时首先需要引入 tree api,这里以9.2为例,最新版本请查看官网 :

implementation 'org.ow2.asm:asm-commons:9.2' //Tree API

通过 ./gradlew app:dependencies 查看依赖关系:

+--- org.ow2.asm:asm-commons:9.2
|    +--- org.ow2.asm:asm:9.2
|    +--- org.ow2.asm:asm-tree:9.2
|    |    \--- org.ow2.asm:asm:9.2
|    \--- org.ow2.asm:asm-analysis:9.2
|         \--- org.ow2.asm:asm-tree:9.2 (*)

其中 org.ow2.asm:asm-tree:9.2 的内部类有:
asm-tree
主要用到的类有:

  • ClassNode
    • FieldNode
    • MethodNode
      • InsnList
        • AbstractInsnNode
        • TypeInsnNode
        • VarInsnNode
        • FieldInsnNode
        • MethodInsnNode
        • IincInsnNode
        • InsnNode
        • IntInsnNode
        • InvokeDynamicInsnNode
        • JumpInsnNode
        • LabelNode
        • LdcInsnNode
        • LookupSwitchInsnNode
        • MultiANewArrayInsnNode
        • TableSwitchInsnNode
      • TryCatchBlockNode

用类图表示为
ClassNode

整理一下他们之间的关系:

  • ClassNode 包含 FieldNodeMethodNode
  • MethodNode 中包含有序指令集合InsnList 及其异常处理TryCatchBlockNode
  • InsnList 由很多个有序的单指令 AbstractInsnNode 组合而成。
  • AbstractInsnNode是一个抽象类,表示字节码指令的节点。一条指令最多只能在一个 InsnList 中出现一次。AbstractInsnNode的子类实现如下:
    InsnNode
    下面分别来看下每个类的含义。

2.1、ClassNode

ClassNode的注释为A node that represents a class,表示一个类的节点。可以通俗理解为:一个Class类经过Tree API解析后,可以将其转换成一个ClassNode(内部包含类的所有信息)。

public class ClassNode extends ClassVisitor {
  public int version;
  public int access;
  public String name;
  public String signature;
  public String superName;
  public List<String> interfaces;
  
  public String outerClass;
  public String outerMethod;
  public List<Attribute> attrs;
  public List<InnerClassNode> innerClasses;
  
  public List<FieldNode> fields;
  public List<MethodNode> methods;
}

ClassNode继承自ClassVisitor,可以看到ClassNode内部的字段正好对应Class中的所有信息。

类型 名称 说明
int version jdk版本
int access 访问级
String name 类名,采用全地址,如java/lang/String
String signature 签名,通常是null
String superName 父类类名,采用全地址
List interfaces 实现的接口,采用全地址
String sourceFile 源文件,可能为null
String sourceDebug debug源,可能为null
String outerClass 外部类
String outerMethod 外部方法
String outerMethodDesc 外部方法描述(包括方法参数、返回值)
List visibleAnnotations 可见的注解
List invisibleAnnotations 不可见的注解
List attrs 类的Attribute
List innerClasses 内部类列表
List fields 字段列表
List methods 方法列表
2.1.1、accept(ClassVisitor classVisitor)

ClassNode#accept(classVisitor) 传入一个ClassVisitor,内部实现如下:

  // Makes the given class visitor visit this class.
  public void accept(final ClassVisitor classVisitor) {
    // Visit the header.
    String[] interfacesArray = new String[this.interfaces.size()];
    //......
    // Visit the fields.
    for (int i = 0, n = fields.size(); i < n; ++i) {
      fields.get(i).accept(classVisitor);
    }
    // Visit the methods.
    for (int i = 0, n = methods.size(); i < n; ++i) {
      methods.get(i).accept(classVisitor);
    }
    classVisitor.visitEnd();
  }

看下这个方法的注释:Makes the given class visitor visit this classaccept(classVisitor) 可以让传入的classVisitor访问ClassNode中的所有内容。而ClassWriter继承自ClassVisitor,那么就可以将ClassWriter作为入参传入ClassNode中,进而调用classNode.accept(classWriter) ClassNode中的数据传入ClassWriter中,最终调用classWriter.toByteArray()ClassNode转为byte[]

示例:

//自定义ClassNode类
class AOutLibClassNode(val api: Int, private val classWriter: ClassWriter) : ClassNode(api) {

    override fun visitEnd() {
        //允许classWriter访问ClassNode类中的信息
        accept(classWriter)
    }
}

ClassReader classReader = new ClassReader(xxx.class.getName());
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//1、将ClassWriter传入ClassNode中
ClassNode classNode = new AOutLibClassNode(BConstant.ASM9, classWriter);
classReader.accept(classNode, ClassReader.EXPAND_FRAMES);

//2、ClassNode -> ClassWriter -> bytes[]
bytes[] classNodeToBytes = classWriter.toByteArray();
2.1.2、FieldNode
public class FieldNode extends FieldVisitor {

  public int access;
  public String name;
  public String desc;
  public String signature;
  public Object value; //字段的初始化值
  
  public List<AnnotationNode> visibleAnnotations;
  public List<AnnotationNode> invisibleAnnotations;
  public List<TypeAnnotationNode> visibleTypeAnnotations;
  public List<TypeAnnotationNode> invisibleTypeAnnotations;
  public List<Attribute> attrs;

  //构造方法
  public FieldNode( final int api, final int access, final String name,
      final String descriptor, final String signature, final Object value) {
         super(api);
         this.access = access;
         this.name = name;
         this.desc = descriptor;
         this.signature = signature;
         this.value = value;
   } 
}

FieldNode继承自FieldVisitor,可以用于遍历或生成字段,内部变量说明:

类型 名称 说明
int access 访问级
String name 字段名
String signature 签名,通常是 null
String desc 类型描述,例如 Ljava/lang/String、D(double)、F(float)Objectvalue初始值,通常为 null
List visibleAnnotations 可见的注解
List invisibleAnnotations 不可见的注解
List attrs 字段的 Attribute

FieldNode生成代码示例:

ClassNode node = new ClassNode(Opcodes.ASM9);
//public + final + static
int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL| Opcodes.ACC_STATIC;
//long类型,value设置为1
node.fields.add(new FieldNode(access, "timer", "J",null, 1)); 

//ClassWriter转化为byte[]
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
node.accept(writer);
byte[] byteArr = writer.toByteArray();

执行结果:

public static final long timer = 1;

可以看到生成了新的字段。

2.1.3、MethodNode
public class MethodNode extends MethodVisitor {
  
  //1、标识方法的基本属性:方法头
  public int access;
  public String name; //方法名
  public String desc;
  public String signature;
  public List<String> exceptions;
  
  //2、标识方法的指令集合:方法体
  public InsnList instructions; //方法中的有序指令集
  public List<TryCatchBlockNode> tryCatchBlocks;//方法中的try catch异常处理数组
  public List<ParameterNode> parameters;
  public int maxStack; // 栈最大值
  public int maxLocals; //局部变量的最大值
}

MethodNode用于访问或修改方法。相比于FieldNode的单一,MethodNode内部可能涉及多条指令,方法执行时,也会涉及到局部变量表操作数栈,具体可参见:Java内存模型MethodNode 内部变量说明:

类型 名称 说明
int access 方法访问级
String name 方法名
String desc 方法描述,其包含方法的返回值、参数
String signature 泛型签名,通常是null
List exceptions 可能返回的异常列表
List visibleAnnotations 可见的注解列表
List invisibleAnnotations 不可见的注解列表
List attrs 方法的Attribute列表
Object annotation Default默认的注解
List visibleParameterAnnotations 可见的参数注解
List invisibleParameterAnnotations 不可见的参数注解列表
InsnList instructions Opcode操作码列表
List tryCatchBlock try-catch块列表
int maxStack 最大操作数栈的深度
int maxLocals 最大局部变量表的大小
List localVariables 本地(局部)变量节点列表

MethodNode 继承自 MethodVisitor,所以内部会有各种 visitxxx() 方法,而在调用MethodNode#visitxxx() 方法后,会将指令存储到instructions (InsnList)

MethodNode.InsnList
// InsnList本质上是一个双链表
public class InsnList implements Iterable<AbstractInsnNode> {

  private int size; //链表内数据的size
  private AbstractInsnNode firstInsn; //链表头指针
  private AbstractInsnNode lastInsn; //链表尾指针
}

InsnList类实现了Iterable< AbstractInsnNode>接口,内部存储了方法执行时的指令集。其中 AbstractInsnNode 表示单条指令,而InsnList类是一个有序存储AbstractInsnNode指令的双向链表。

当我们想插入操作码指令从而达到修改字节码时,可以对InsnList进行如下操作

  • add(final AbstractInsnNode insnNode):将给定的insnNode指令添加到InsnList列表的末尾;
  • add(final InsnList insnList):将一组指令添加到InsnList列表的末尾;
  • insert(final AbstractInsnNode insnNode):将给定的insnNode指令添加到InsnList列表的开头;
  • insert(final InsnList insnList):将一组指令添加到InsnList列表的开头;
  • insert(final AbstractInsnNode previousInsn, final AbstractInsnNode insnNode):将insnNode指令插入到previousInsn指令之后;
  • insert(final AbstractInsnNode previousInsn, final InsnList insnList):将insnList多个指令插入到previousInsn指令之后;
  • insertBefore(final AbstractInsnNode nextInsn, final AbstractInsnNode insnNode):将insnNode指令插入到nextInsn之前;
  • insertBefore(final AbstractInsnNode nextInsn, final InsnList insnList): 将insnList多个指令插入到nextInsn之前。

继续看AbstractInsnNode

public abstract class AbstractInsnNode {
    protected int opcode; //当前指令

    AbstractInsnNode previousInsn; //指向上一个指令
    AbstractInsnNode nextInsn; //指向下一个指令

    int index; //当前指令在InsnList中的索引
}

AbstractInsnNode可以表示的指令集合:

类型 名称 说明
FieldInsnNode GETFIELD、PUTFIELD 等变量操作的字节码 操作变量
FrameNode 栈映射帧
IincInsnNode 用于 IINC 变量自加操作 int var:目标局部变量的位置 int incr: 要增加的数
InsnNode 无参数值操作的字节码,如 ALOAD_0
IntInsnNode 用于 BIPUSH、SIPUSH 和 NEWARRAY 这三个直接操作整数的操作 int operand:操作的整数值
InvokeDynamicInsnNode 用于 Java7 新增的 INVOKEDYNAMIC 操作的字节码 String name:方法名称; String desc:方法描述; Handle bsm:句柄; Object bsmArgs:参数常量
JumpInsnNode 用于 IFEQ 或 GOTO 等跳转操作 LabelNode lable:目标lable
LabelNode 用于表示跳转点的 Label 节点
LdcInsnNode LDC 用于加载常量池中引用值并进行插入 Object cst:引用值
LineNumberNode 表示行号的节点 int line:行号;LabelNode start:对应的第一个
LabelLookupSwitchInsnNode 用于实现 LOOKUPSWITCH 操作的字节码 LabelNode dflt:default 块对应的 LableList keys 键列表;List labels:对应的 Label 节点列表
MethodInsnNode 用于 INVOKEVIRTUAL 等传统方法调用操作的字节码 不适用于 Java7 新增的 INVOKEDYNAMIC,String owner :方法所在的类;String name :方法名称;String desc:方法描述
MultiANewArrayInsnNode 用于 MULTIANEWARRAY 操作的字节码 String desc:类型描述;int dims:维数
TableSwitchInsnNode 用于实现 TABLESWITCH 操作的字节码 int min:键的最小值;int max:键的最大值;LabelNode dflt:default 块对应的 LableList labels:对应的 Label 节点列表
TypeInsnNode 用于 NEW、ANEWARRAY 和 CHECKCAST 等类型操作 String desc:类型;VarInsnNode用于实现 ALOAD、ASTORE 等局部变量操作;int var:局部变量

上述的指令按特定顺序存储在InsnList中。当方法执行时,InsnList中的指令集合也会按顺序执行。

三 Tree API 实践

3.1、统计方法耗时

假设要对下面这个类的方法进行耗时统计:

//MethodTimeCostTestJava.java
public class MethodTimeCostTestJava {
    
    //static静态方法
    public static void staticTimeCostMonitor() throws InterruptedException {
        Thread.sleep(1000);
    }
    
    //非静态方法
    public void timeCostMonitor() throws InterruptedException {
        Thread.sleep(1000);
    }
}

实现

自定义ClassNode对方法进行插桩操作:

class AOutLibClassNode(val api: Int, private val classWriter: ClassWriter) : ClassNode(api) {
    private val mThresholdTime = 500

    companion object {
        const val owner = "org/ninetripods/lib_bytecode/common/TimeCostUtil"
        const val descripter = "Lorg/ninetripods/lib_bytecode/common/TimeCostUtil"
    }

    override fun visitEnd() {
        processTimeCost()
        //允许classWriter访问ClassNode类中的信息
        accept(classWriter)
    }

    private fun processTimeCost(clzName: String? = "", methodName: String? = "", access: Int = 0) {
        for (methodNode: MethodNode in methods) {
            if (methodNode.name.equals("<init>") || methodNode.name.equals("<clinit>")) continue
            val instructions = methodNode.instructions

            //方法开头插入
            val clzName = name
            val methodName = methodNode.name
            val access = methodNode.access
            instructions.insert(createMethodStartInstructions(clzName, methodName, access))

            //退出方法之前插入
            methodNode.instructions.forEach { insnNode ->
                val opcode = insnNode.opcode
                if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                    val endInstructions = createMethodEndInstructions(clzName, methodName, access)
                    methodNode.instructions.insertBefore(insnNode, endInstructions)
                }
            }
        }
    }

    /**
     * 在method中创建入口指令集
     */
    private fun createMethodStartInstructions(
        clzName: String?,
        methodName: String?,
        access: Int,
    ): InsnList {
        val isStaticMethod = access and Opcodes.ACC_STATIC != 0
        return InsnList().apply {
            if (isStaticMethod) {
                add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))
                //操作数栈中传入下面两个参数
                add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))
                add(LdcInsnNode("$clzName&$methodName"))
                add(
                    MethodInsnNode(
                        Opcodes.INVOKEVIRTUAL,
                        owner,
                        "recordStaticMethodStart",
                        "(ILjava/lang/String;)V",
                        false
                    )
                )
            } else {
                add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))
                //操作数栈中传入对应的三个入参
                add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))
                add(LdcInsnNode("$clzName&$methodName"))
                add(VarInsnNode(Opcodes.ALOAD, 0))
                //将上面的三个参数传入下面的方法中
                add(
                    MethodInsnNode(
                        Opcodes.INVOKEVIRTUAL,
                        owner,
                        "recordMethodStart",
                        "(ILjava/lang/String;Ljava/lang/Object;)V",
                        false
                    )
                )
            }
        }
    }

    /**
     * 在method中退出时的指令集
     */
    private fun createMethodEndInstructions(
        clzName: String?,
        methodName: String?,
        access: Int,
    ): InsnList {
        val isStaticMethod = access and Opcodes.ACC_STATIC != 0
        return InsnList().apply {
            if (isStaticMethod) {
                add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))
                //调用
                add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))
                add(LdcInsnNode("$clzName&$methodName"))
                add(
                    MethodInsnNode(
                        Opcodes.INVOKEVIRTUAL,
                        owner,
                        "recordStaticMethodEnd",
                        "(ILjava/lang/String;)V",
                        false
                    )
                )
            } else {
                add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))
                //栈中传入对应的三个入参
                add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))
                add(LdcInsnNode("$clzName&$methodName"))
                add(VarInsnNode(Opcodes.ALOAD, 0))
                //将上面的三个参数传入下面的方法中
                add(
                    MethodInsnNode(
                        Opcodes.INVOKEVIRTUAL,
                        owner,
                        "recordMethodEnd",
                        "(ILjava/lang/String;Ljava/lang/Object;)V",
                        false
                    )
                )
            }
        }
    }
    
}

方法耗时处理TimeCostUtil类:

package org.ninetripods.lib_bytecode.common

/**
 * 全局方法耗时Util
 */
object TimeCostUtil {
    private const val TAG = "METHOD_COST"

    private val staticMethodObj by lazy { StaticMethodObject() }

    /**
     * 方法Map,其中key:方法名,value:耗时时间
     */
    private val METHODS_MAP by lazy { ConcurrentHashMap<String, Long>() }

    /**
     * 对象方法
     * @param thresholdTime 阈值
     * @param methodName 方法名
     * @param clz 类名
     */
    fun recordMethodStart(thresholdTime: Int, methodName: String, clz: Any?) {
        try {
            METHODS_MAP[methodName] = System.currentTimeMillis()
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
    }

    /**
     * 静态方法
     * @param thresholdTime 阈值
     * @param methodName 方法名
     */
    fun recordStaticMethodStart(thresholdTime: Int, methodName: String){
        recordMethodStart(thresholdTime, methodName, staticMethodObj)
    }

    /**
     * 对象方法
     * @param thresholdTime 阈值时间
     * @param methodName 方法名
     * @param clz 类名
     */
    fun recordMethodEnd(thresholdTime: Int, methodName: String, clz: Any?) {
        Log.e(
            TAG,
            "\t methodName=>$methodName thresholdTime=>$thresholdTime method=>recordMethodEnd"
        )
        synchronized(TimeCostUtil::class.java) {
            try {
                if (METHODS_MAP.containsKey(methodName)) {
                    val startTime: Long = METHODS_MAP[methodName] ?: 0L
                    val costTime = System.currentTimeMillis() - startTime
                    METHODS_MAP.remove(methodName)

                    //方法耗时超过了阈值
                    if (costTime >= thresholdTime) {
                        val threadName = Thread.currentThread().name
                        Log.e(
                            TAG,
                            "\t methodName=>$methodName threadNam=>$threadName thresholdTime=>$thresholdTime costTime=>$costTime"
                        )
                    }
                }
            } catch (ex: Exception) {
                ex.printStackTrace()
            }
        }
    }

    /**
     * 静态方法
     * @param thresholdTime 阈值
     * @param methodName 方法名
     */
    fun recordStaticMethodEnd(thresholdTime: Int, methodName: String) {
        recordMethodEnd(thresholdTime, methodName, staticMethodObj)
    }

}

class StaticMethodObject 

然后是执行总入口:

public static void main(String[] args) {
    try {
        //使用示例
        ClassReader classReader = new ClassReader(MethodTimeCostTestJava.class.getName());
        //ClassWriter.COMPUTE_MAXS 自动计算帧栈信息(操作数栈 & 局部变量表)
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

        //-------------------------方式1:ASM Tree API -------------------------
        ClassNode classNode = new AOutLibClassNode(Opcodes.ASM9, classWriter);

        //访问者模式:将ClassVisitor传入ClassReader中,从而可以访问ClassReader中的私有信息;类似一个接口回调。
        classReader.accept(classNode, ClassReader.EXPAND_FRAMES);

        FileUtil.INSTANCE.byte2File("lib_bytecode/files/MethodTimeCostTestJava.class",classWriter.toByteArray());

         } catch (IOException e) {
            e.printStackTrace();
        }
    }


//将最终修改的字节码写入指定路径
object FileUtil {
    fun byte2File(outputPath: String, sourceByte: ByteArray) {
        try {
            val file = File(outputPath)
            if (file.exists()) {
                file.delete()
            } else {
                file.parentFile.mkdir()
                file.createNewFile()
            }

            val inputStream = ByteArrayInputStream(sourceByte)
            val outputStream = FileOutputStream(file)
            val buffer = ByteArray(1024)
            var len = 0
            while (inputStream.read(buffer).apply { len = this } != -1) {
                outputStream.write(buffer, 0, len)
            }
            outputStream.flush()
            outputStream.close()
            inputStream.close()
        } catch (ex: Exception) {
            ex.printStackTrace()
        }
    }
}

执行结果:

public class MethodTimeCostTestJava {
    public MethodTimeCostTestJava() {
    }

    public static void staticTimeCostMonitor() throws InterruptedException {
        //1
        TimeCostUtil.INSTANCE.recordStaticMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");
        Thread.sleep(1000L);
        //2
        TimeCostUtil.INSTANCE.recordStaticMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");
    }

    public void timeCostMonitor() throws InterruptedException {
        //3
        TimeCostUtil.INSTANCE.recordMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);
        Thread.sleep(1000L);
        //4
        TimeCostUtil.INSTANCE.recordMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);
    }
}

可以看到最终生成类的方法中已经包含了我们想要插入的代码,包括静态方法和非静态方法。这里只是简单的写个示例,其中AOutLibClassNode中的写法是参考的 滴滴DoKit 中的代码。

四 参考

【1】Tree API介绍
【2】ASM 修改字节码
【3】Java ASM详解:MethodVisitor和Opcode

猜你喜欢

转载自blog.csdn.net/u013700502/article/details/129106238