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

文章目录

      • 一 ASM介绍
      • 二 Tree API
        • 2.1、ClassNode
          • 2.1.1、accept(ClassVisitor classVisitor)
          • 2.1.2、FieldNode
          • 2.1.3、MethodNode
            • MethodNode.InsnList
      • 三 Tree API 实践
        • 3.1、统计方法耗时
      • 四 参考

一 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 interfaces;public String outerClass;public String outerMethod;public List attrs;public List innerClasses;public List fields;public List methods;
}

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

类型名称说明
intversionjdk版本
intaccess访问级
Stringname类名,采用全地址,如java/lang/String
Stringsignature签名,通常是null
StringsuperName父类类名,采用全地址
Listinterfaces实现的接口,采用全地址
StringsourceFile源文件,可能为null
StringsourceDebugdebug源,可能为null
StringouterClass外部类
StringouterMethod外部方法
StringouterMethodDesc外部方法描述(包括方法参数、返回值)
ListvisibleAnnotations可见的注解
ListinvisibleAnnotations不可见的注解
Listattrs类的Attribute
ListinnerClasses内部类列表
Listfields字段列表
Listmethods方法列表
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 visibleAnnotations;public List invisibleAnnotations;public List visibleTypeAnnotations;public List invisibleTypeAnnotations;public List 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,可以用于遍历或生成字段,内部变量说明:

类型名称说明
intaccess访问级
Stringname字段名
Stringsignature签名,通常是 null
Stringdesc类型描述,例如 Ljava/lang/String、D(double)、F(float)Objectvalue初始值,通常为 null
ListvisibleAnnotations可见的注解
ListinvisibleAnnotations不可见的注解
Listattrs字段的 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 exceptions;//2、标识方法的指令集合:方法体public InsnList instructions; //方法中的有序指令集public List tryCatchBlocks;//方法中的try catch异常处理数组public List parameters;public int maxStack; // 栈最大值public int maxLocals; //局部变量的最大值
}

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

类型名称说明
intaccess方法访问级
Stringname方法名
Stringdesc方法描述,其包含方法的返回值、参数
Stringsignature泛型签名,通常是null
Listexceptions可能返回的异常列表
ListvisibleAnnotations可见的注解列表
ListinvisibleAnnotations不可见的注解列表
Listattrs方法的Attribute列表
ObjectannotationDefault默认的注解
ListvisibleParameterAnnotations可见的参数注解
ListinvisibleParameterAnnotations不可见的参数注解列表
InsnListinstructionsOpcode操作码列表
ListtryCatchBlocktry-catch块列表
intmaxStack最大操作数栈的深度
intmaxLocals最大局部变量表的大小
ListlocalVariables本地(局部)变量节点列表

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

MethodNode.InsnList
// InsnList本质上是一个双链表
public class InsnList implements Iterable {private int size; //链表内数据的sizeprivate 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可以表示的指令集合:

类型名称说明
FieldInsnNodeGETFIELD、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 节点
LdcInsnNodeLDC 用于加载常量池中引用值并进行插入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 = 500companion 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("") || methodNode.name.equals("")) continueval instructions = methodNode.instructions//方法开头插入val clzName = nameval methodName = methodNode.nameval access = methodNode.accessinstructions.insert(createMethodStartInstructions(clzName, methodName, access))//退出方法之前插入methodNode.instructions.forEach { insnNode ->val opcode = insnNode.opcodeif ((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 != 0return 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 != 0return 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() }/*** 对象方法* @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] ?: 0Lval costTime = System.currentTimeMillis() - startTimeMETHODS_MAP.remove(methodName)//方法耗时超过了阈值if (costTime >= thresholdTime) {val threadName = Thread.currentThread().nameLog.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 = 0while (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 {//1TimeCostUtil.INSTANCE.recordStaticMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");Thread.sleep(1000L);//2TimeCostUtil.INSTANCE.recordStaticMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");}public void timeCostMonitor() throws InterruptedException {//3TimeCostUtil.INSTANCE.recordMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);Thread.sleep(1000L);//4TimeCostUtil.INSTANCE.recordMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);}
}

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

四 参考

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


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部