深入探索编译插桩技术(四、ASM 探秘)

本文深入探讨了ASM库在Java字节码操作中的优势和劣势,包括ASM Tree API的对象模型和ASM Core API的事件模型。ASM Tree API通过树状结构简化了对字节码的操作,适合简单的类修改,但处理复杂信息时代码会变复杂。事件模型采用访问者模式,允许细粒度的字节码修改,但上手难度较高。文章通过实例展示了如何获取和操控类、字段、方法节点,以及如何插入、替换和删除操作码。
摘要由CSDN通过智能技术生成

本文来自jsonchao的投稿,个人微信:bcce5360   

        AspectJ 有着一系列弊端:由于其基于规则,所以其切入点相对固定,对于字节码文件的操作自由度以及开发的掌控度就大打折扣。并且,他会额外生成一些包装代码,对性能以及包大小有一定影响

ASM 基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高。它提供了 访问者模式来访问字节码文件,并且只注入我们想要注入的代码

ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。并且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优势要远超其它的字节码操作库如 javassist、AspectJ

思维导图大纲目录

  • 一、ASM 的优势和逆势

    • 1、ASM 的优势

    • 2、ASM 的逆势

  • 二、ASM 的对象模型(ASM Tree API)

    • 1、优点

    • 2、缺点

    • 3、获取节点

    • 4、操控操作码

  • 三、ASM 的事件模型(ASM Core API))

    • 2、类读取(解析)者 ClassVisitor

    • 3、小结

  • 四、综合实战训练

    • 1、使用 ASM Bytecode Outline

    • 2、使用 ASM 编译插桩统计方法耗时

    • 3、全局替换项目中所有的 new Thread

  • 五、总结

一、ASM 的优势和逆势

使用 ASM 操作字节码的优势与逆势都 比较明显,其分别如下所示。

1、ASM 的优势

  • 1)、内存占用很小

  • 2)、运行速度非常快

  • 3)、操作灵活:对于字节码的操作非常地灵活,可以进行插入、删除、修改等操作

  • 4)、想象空间大,能够借用它提升生产力

  • 5)、丰富的文档与众多社区的支持

2、ASM 的逆势

上手难度较大,需要对 Java 字节码有比较充分的了解

对于 ASM 而言,它提供了 两种模型:对象模型和事件模型

下面,我们就先来讲讲 ASM 的对象模型。

二、ASM 的对象模型(ASM Tree API)

对象模型的 本质 是一个 被封装过后的事件模型,它 使用了树状图的形式来描述一个类,其中包含多个节点,例如方法节点、字段节点等等,而每个节点又有子节点,例如方法节中有操作码子节点 等等。下面我们先来了解下由这种树状图模式实现的对象模型的利弊。

1、优点

  • 1)、适宜处理简单类的修改

  • 2)、学习成本较低

  • 3)、代码量较少

2、缺点

  • 1)、处理大量信息会使代码变得复杂

  • 2)、代码难以复用

在对象模型下的 ASM 有 两类操作纬度,分别如下所示:

  • 1)、获取节点获取指定类、字段、方法节点

  • 2)、操控操作码(针对方法节点)获取操作码位置、替换、删除、插入操作码、输出字节码

下面我们就来分别来了解下 ASM 的这两类操作。

3、获取节点

1)、获取指定类的节点

获取一个类节点的代码如下所示:

ClassNode classNode = new ClassNode();
// 1
ClassReader classReader = new ClassReader(bytes);
// 2
classReader.accept(classNode, 0);


在注释1处,将字节数组传入一个新创建的 ClassReader,这时 ASM 会使用 ClassReader 来解析字节码。接着,在注释2处,ClassReader 在解析完字节码之后便可以通过 accept 方法来将结果写入到一个 ClassNode 对象之中

那么,一个 ClassNode 具体又包含哪些信息呢?

如下所示:

类节点信息
类型 名称 说明
int version class文件的major版本(编译的java版本)
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)、获取指定字段的节点

获取一个字段节点的代码如下所示:

for(FieldNode fieldNode : (List)classNode.fields) {
    // 1
    if(fieldNode.name.equals("password"))  {
        // 2
        fieldNode.access = Opcodes.ACC_PUBLIC;
    }
}


字段节点列表 fields 是一个 ArrayList,它储存着类节点的所有字段。在注释1处,我们通过遍历 fields 集合的方式来找到目标字段节点。接着,在注释2处,我们将目标字段节点的访问权限置为 public。

除此之外,我们还可以为类添加需要的字段,代码如下所示:

FieldNode fieldNode = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "JsonChao", "I", null, null);
classNode.fields.add(fieldNode);


在上述代码中,我们直接给目标类节点添加了一个 "public static int JsonChao" 的字段,需要注意的是,第三个参数的 "I" 表示的是 int 的类型描述符。

那么,对于一个字段节点,又包含有哪些字段信息呢?

如下所示:

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

接下来,我们看看如何获取一个方法节点。

3、获取指定的方法节点

获取指定的方法节点的代码如下所示:

for(MethodNode methodNode : (List)classNode.methods) {
    // 1、判断方法名是否匹配目标方法
    if(methodNode.name.equals("getName")) {
        // 2、进行操作
    }
}


methods 同 fields 一样,也是一个 ArrayList,通过遍历并判断方法名的方式即可匹配到目标方法。

对于一个方法节点来说,它包含有如下信息:

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

4、操控操作码

在操控字节码之前,我们必须先了解下 instructions,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码

ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点  VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中

对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点  AbstractInsnNode。其所有的派生类使用详情如下所示。

所有的指令码节点说明

名称 说明 参数
FieldInsnNode 用于 GETFIELD 和 PUTFIELD 之类的字段操作的字节码 String owner 字段所在的类
String name 字段的名称
String desc 字段的类型
FrameNode 栈映射帧的对应的帧节点 待补充
IincInsnNode 用于 IINC 变量自加操作的字节码 int var:目标局部变量的位置
int incr:要增加的数
InsnNode 一切无参数值操作的字节码,例如 ALOAD_0,DUP(注意不包含 POP)
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:对应的第一个 Label
LookupSwitchInsnNode 用于实现 LOOKUPSWITCH 操作的字节码 LabelNode dflt:default 块对应的 Lable
List 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 块对应的 Lable
List labels:对应的 Label 节点列表
TypeInsnNode 用于实现 NEW、ANEWARRAY 和 CHECKCAST 等类型相关操作的字节码 String desc:类型
VarInsnNode 用于实现 ALOAD、ASTORE 等局部变量操作的字节码 int var:局部变量
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值