深入探索编译插桩技术(四、ASM 探秘)
前言
成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。
在
《深入探索编译插桩技术(二、AspectJ)》:juejin.im/post/5e8438…
一文中我们深入学习了 AspectJ 在 Android 下的使用。可以看到 AspectJ 非常强大,但是它也只能实现 50% 的字节码操作场景,如果想要实现 100% 的字节码操作场景,那么就不得不使用 ASM。
此外,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) {// 1if(fieldNode.name.equals("password")) {// 2fieldNode.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、获取操作码的位置
获取指定操作码位置的代码如下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode)ainNode).operand == 16) {....//进行操作}
}
由于一般情况下我们都无法确定操作码在列表中的具体位置,因此 通常会通过遍历的方式去判断其关键特征,以此来定位指定的操作码,上述代码就能定位到一个 SIPUSH 16 的字节码,需要注意的是,有时一个方法中会有多个相同的指令,这是我们需要靠判断前后字节码识别其特征来定位,也可以记下其命中次数然后设定在某一次进行操作,一般情况下我们都是使用的第二种。
2、替换指定的操作码
替换指定的操作码的代码如下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));}
}
这里我们 直接调用了 InsnList 的 set 方法就能替换指定的操作码对象,我们在获取了 "BIPUSH 64" 字节码的位置后,便将封装它的操作码替换为一个新的 VarInsnNode 操作码,这个新操作码封装了 "ALOAD 1" 字节码, 将原程序中 将值设为16
替换为 将值设为局部变量1
。
3、删除指定的操作码
methodNode.instructions.remove(xxx);
xxx 表示的是要删除的操作码实例,我们直接调用用 InsnList 的 remove 方法将它移除掉即可。
4、插入指定的操作码
InsnList 主要提供了 四类 方法用于插入字节码,如下所示:
- 1)、
add(AbstractInsnNode insn)
: 将一个操作码添加到 InsnList 的末尾。 - 2)、
insert(AbstractInsnNode insn)
: 将一个操作码插入到这个 InsnList 的开头。 - 3)、
insert(AbstractInsnNode insnNode,AbstractInsnNode insn)
: 将一个操作码插入到另一个操作码的下面。 - 4)、
insertBefore(AbstractInsnNode insnNode,AbstractInsnNode insn)
将一个操作码插入到另一个操作码的上面
接下来看看如何使用这些方法插入指定的操作码,代码如下所示:
for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {methodNode.instructions.insert(ainNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/awt/image/BufferedImage", "getWidth", "(Ljava/awt/image/ImageObserver;)I"));methodNode.instructions.insert(ainNode, new InsnNode(Opcodes.ACONSTNULL));methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));}
}
这样,我们就能将
BIPUSH 16
```java替换为```java
ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I
```java**当我们操控完指定的类节点之后,就可以使用 ASM 的 ClassWriter 类来输出字节码**,代码如下所示:```java
// 1、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();
关于 ClassWriter 的具体用法,我们会在 ASM Core API 这部分来进行逐步讲解。下面
深入探索编译插桩技术(四、ASM 探秘)相关推荐
- 深入探索编译插桩技术(一、编译基础)
前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 现如今,Gradle + 编译插桩 的应用场景越来越多,无论是 各种性能优化中的插件工具制作 ...
- aspectj 获取方法入参_深入探索编译插桩技术(二、AspectJ)
本文来自jsonchao的投稿,个人微信:bcce5360 现如今,编译插桩技术已经深入 Android 开发中的各个领域,而 AOP 技术正是一种高效实现插桩的模式,它的出现正好给处于黑暗中的我们带 ...
- 深入探索编译插桩技术(二、AspectJ)
前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 现如今,编译插桩技术已经深入 Android 开发中的各个领域,而 AOP 技术正是一种高效 ...
- 深入探索编译插桩技术(四、ASM 探秘,android中文api文档
从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类.接口.属性.域和方法等等.当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件.因此 ASM 提供了一个 类 ...
- 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...
- Android编译插桩
背景:这一次分享一下关于android编译插桩这个话题,在正常编写代码实现程序的逻辑外,还要使用一点点黑科技,拿起操作代码无所不能的武器. 一.Android常用的能动态改变代码逻辑的方法有两种 1. ...
- 使用插桩技术解决慢查询测试问题
原文由zlulu发表于TesterHome社区,原文链接 缘起 前段时间,我负责测试的系统在生产环境运行出现问题.该系统对于响应时间要求较高,问题发生的时候并发很高,出现大量请求超时,超时请求比例随时 ...
- 调研字节码插桩技术,用于系统监控设计和实现
作者:小傅哥 博客:https://bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获!???? ❞ 目录 一.来自深夜的电话! 二.准备工作 三.使用 AOP 做个切面监控 1. ...
- 【网上的都不靠谱?还是得改源码】用Javasisst的字节码插桩技术,彻底解决Gson转Map时,Int变成double问题...
一.探究原由 首先申明一下,我们要解决的问题有两个: Json串转Map时,int变double问题 Json串转对象时,对象属性中的Map,int变double问题 然后,我们来了解一下,Gson实 ...
最新文章
- [重构到模式-Chain of Responsibility Pattern]把Fizz Buzz招式重构到责任链模式
- neutron plugin 笔记
- Linux Centos下SQL Server 2017安装和配置
- 我的欧拉工程之路_3
- Github | 商汤出品-可在视频里追踪单个对象PySOT
- C语言的EOF是什么?getchar()!=EOF返回的是什么?
- FPGA RAM时分复用减少逻辑资源
- 腾讯网游加速器大升级!5月31日起仅支持国服游戏加速 你用过吗?
- centos php rpm下载源,CentOS 6.2 使用第三方yum源安装更多rpm软件包 | 系统运维
- 实验二+065+方绎杰
- PB调用WebService
- 基于matlab的平面切割、旋转曲面等动图制作
- pytorch中为Module和Tensor指定GPU
- Android 调用第三方地图类App (高德 百度 百度网页版)
- Android中跳转到系统设置界面
- 【Linux记录】Linux 可以telnet通localhost,不能telnet ip,telnet localhost正常,telnet ip失败。
- js设置延时加载事件
- 百度网盘网页版倍速播放
- 【续】数学模型——人口增长模型
- UI设计培训技术教程之字体排版规则