字节码插桩框架ASM(一)
本文大纲:
- ams是做什么的
- asm使用
1.ASM是做什么的?
简单来说,asm是用来进行字节码插桩的。什么是字节码插桩? 字节码插桩就是修改节码文件(.class). 如同 gson框架是用来做操json数据结构的,那么asm框架就是用来操作.class文件结构的。
那么这有什么用处呢?
这个是一个很强大而且很高级的功能。我们可能知道反射hook技术,aspectJ技术,动态代理技术,我们一一来看他们的特点:
- 反射hook: 在运行时通过反射某些属性或方法来修改原有流程。局限性:需要能够找到合适的hook点。
- aspectJ:通过添加注解的方式在运行时或编译时进行插入代码逻辑。局限:需要添加注解,有一些侵入性。
- 动态代理:在运行时代理一切实现某个接口的类,并在方法中可以添加自己的代码逻辑。局限:只能代理接口,并且只能修改接口声明的方法。
那么现在假如我们有这样一个需求,在每个方法中记录方法的执行时间。代码如下:
public class InjectTest {public static void main(String arg[]) {}public void sayGo(){System.out.println("大家好aaaaaaaaaaaa");}
}
- 方案一:可以使用aspectJ,定义一个注解,并在注解处理器中写入记录时间的逻辑。 然后将注解放到方法上。 的确,在我们了解字节码插桩之前,这也许是最好的方法。
- 方案二:使用asm字节码插桩。首先获取编译后的InjectTest.class文件,进行分析,在每个方法时中加入记录时间的逻辑,然后重新生成新的InjectTest.class文件。
可以发现,使用字节码插桩这种方式,在编译成class文件之前的逻辑我们不用考虑也不用修改,仅仅对class文件进行了修改,假如某天要去掉这个功能,那么我们直接注释掉插桩代码就ok了,而使用AspectJ方式要手动去掉注解。
还有很多场景能用到字节码插桩,比如第三方库的修改,尤其是当你无法拿到源码只能拿到字节码的时候。还有android4.4一下热修复时 CLASS_ISPREVERIFIED的问题 https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a。 总之,字节码插桩不仅是一个逼格很高的技术,还是一个十分实用的技术。
2.字节码插桩框架ASM的使用:
本次我们来完成一个字节码插桩的测试,目标就是对下面一段代码 在每个方法中插入计算方法执行时间的逻辑:
public class InjectTest {public static void main(String arg[]) {System.out.println("今晚上山打老虎");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}public void sayGo(){System.out.println("大家好aaaaaaaaaaaa");}
}
2.1.准备工作:
①准备待插桩的class
因为asm操作的class文件,所以先将.java编译成.class
命令行执行命令: javac -encoding UTF-8 InjectTest.java
此时生成 D:\projects\DragonForest\ASMStudy\app\src\test\java\com\example\asmstudy\InjectTest.class
②引入asm依赖:
在AS中引入ASM
ASM可以直接从 jcenter()仓库中引入,所以我们可以进入:https://bintray.com/进行搜索
点击图中标注的工件进入,可以看到最新的正式版本为:7.1。
因此,我们可以在AS中加入:
同时,需要注意的是:我们使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。
AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。
2.2.使用asm框架,执行插桩。
在此之前,先来介绍ASM框架中几个重要的类和方法:
ClassReader 类
这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
ClassWriter 类
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
ClassVisitor 抽象类
- void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口)
- AnnotationVisitor visitAnnotation(String desc, boolean visible) 该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。
- FieldVisitor visitField(int access, String name, String desc, String signature, Object value)该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
- MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常)
- void visitEnd() 该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法
MethodVisitor & AdviceAdapter
MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。其中比较重要的几个方法如下:
- void visitCode():表示 ASM 开始扫描这个方法
- void onMethodEnter():进入这个方法
- void onMethodExit():即将从这个方法出去
- void onVisitEnd():表示方法扫码完毕
FieldVisitor 抽象类
FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍
ASM 操作流程
- 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
- 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
- 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
接下来进行编码:
首先在android 项目的 test 下创建 AsmUtil类:
/*** 1、准备待分析的class*/FileInputStream fis = new FileInputStream("xxxxx/test/java/InjectTest.class");/*** 2、执行分析与插桩*///class字节码的读取与分析引擎ClassReader cr = new ClassReader(fis);// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);/*** 3、获得结果并输出*/byte[] newClassBytes = cw.toByteArray();File file = new File("xxx/test/java2/");file.mkdirs();FileOutputStream fos = new FileOutputStream("xxx/test/java2/InjectTest.class");fos.write(newClassBytes);fos.close();
重点在于第2步,把class数据交给 ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor。
public class ClassAdapterVisitor extends ClassVisitor {public ClassAdapterVisitor(ClassVisitor cv) {super(Opcodes.ASM7, cv);}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature,String[] exceptions) {System.out.println("方法:" + name + " 签名:" + desc);MethodVisitor mv = super.visitMethod(access, name, desc, signature,exceptions);return new MethodAdapterVisitor(api,mv, access, name, desc);}
}
分析结果通过 ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader会将调用 ClassAdapterVisitor中对应的 visitMethod、 visitAnnotation、 visitField这些 visitXX方法。
我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中我们返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理
package com.enjoy.asminject.example;import com.enjoy.asminject.ASMTest;import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;/*** AdviceAdapter: 子类* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析*/
public class MethodAdapterVisitor extends AdviceAdapter {// 是否需要插桩private boolean inject;protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {super(api, methodVisitor, access, name, descriptor);}/*** 分析方法上面的注解* <p>* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩* 没使用,就不管了。** @param desc* @param visible* @return*/@Overridepublic AnnotationVisitor visitAnnotation(String desc, boolean visible) {// 如果方法加上了ASMTest.class的注解,才会执行插桩if (Type.getDescriptor(ASMTest.class).equals(desc)) {System.out.println(desc);inject = true;}return super.visitAnnotation(desc, visible);}// 记录开始时间 定义成全局变量以便在后面执行加减法的时候可以方便获取到private int start;/*** 在方法开始时插入代码:* long start = System.currentTimeMillis();*/@Overrideprotected void onMethodEnter() {super.onMethodEnter();if (inject) {//执行完了怎么办?记录到本地变量中invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis", "()J"));start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量//记录 方法执行结果给创建的本地变量storeLocal(start);}}/*** 在方法结束时插入代码:* long end = System.currentTimeMillis();* System.out.println("方法耗时:"+(end-start));*/@Overrideprotected void onMethodExit(int opcode) {super.onMethodExit(opcode);if (inject){invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis", "()J"));int end = newLocal(Type.LONG_TYPE);storeLocal(end);getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;"));//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuildernewInstance(Type.getType("Ljava/lang/StringBuilder;"));dup();invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));visitLdcInsn("execute:");invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));//减法loadLocal(end);loadLocal(start);math(SUB,Type.LONG_TYPE);invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));}}
}
其中我还在visitAnnotation()中判断了该方法是否有AmsTest注解,通过此来判断是否需要插桩。
看到这里,onMethodEnter() 和 onMethodExit() 里面的代码可能会看不懂。
其实onMethodEnter()就是插入了
long start = System.currentTimeMillis();
而 onMethodExit() 就是插入了
long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end-start));
只不过这些java代码是翻译成汇编指令的形式了。
现在我们查看一下汇编指令。
首先安装 ASM Bytecode Viewer插件:
然后我们写好一个记录时间的代码,如下:
public void sayHello(){long start = System.currentTimeMillis();System.out.println("大家好");long end = System.currentTimeMillis();System.out.println("方法耗时:"+(end-start));
}
查看他的汇编指令,右键--ASM ByteCode Viewer:
右侧显示出汇编指令和对应的行号:
// access flags 0x1public sayHello()VL0/*** long start = System.currentTimeMillis();对应的汇编指令*/LINENUMBER 19 L0INVOKESTATIC java/lang/System.currentTimeMillis ()JLSTORE 1L1LINENUMBER 20 L1GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "\u5927\u5bb6\u597d"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL2/*** long end= System.currentTimeMillis();* System.out.println("方法耗时:"+(end-start));对应的汇编指令*/LINENUMBER 21 L2INVOKESTATIC java/lang/System.currentTimeMillis ()JLSTORE 3L3LINENUMBER 22 L3GETSTATIC java/lang/System.out : Ljava/io/PrintStream;NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VLDC "\u65b9\u6cd5\u8017\u65f6\uff1a"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;LLOAD 3LLOAD 1LSUBINVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL4LINENUMBER 23 L4RETURNL5LOCALVARIABLE this Lcom/example/asmstudy/InjectTest; L0 L5 0LOCALVARIABLE start J L1 L5 1LOCALVARIABLE end J L3 L5 3MAXSTACK = 6MAXLOCALS = 5
}
再去找onMethodEnter() 和 onMethodExit() 中的代码,他们是对应的。其中的每个指令的具体含义,可以去参考jvm中文指令手册。
值得注意的是
1.对象的类型的Type 前面需要加L, 比如 ‘java/lang/StringBuilder’ ,它的type是 ‘Ljava/lang/StringBuilder’
2.Type 后面必须加 ‘;’,不然编译出来的class会报错。
关于java类型签名,可以参考一下下表:
然后在单元测试中加入插桩的代码:
public class ExampleUnitTest {@Testpublic void addition_isCorrect() {assertEquals(4, 2 + 2);}@Testpublic void testASM(){AsmUtil asmUtil=new AsmUtil();asmUtil.inject();}
}
一顿操作之后,查看生成的class,已经成功插入了。
查看 编译生成的InjectTest.class:
字节码插桩框架ASM(一)相关推荐
- Java ASM框架与字节码插桩的常见用法(生成类,修改类,方法插桩,方法注入)
前言 ASM 是一款读写Java字节码的工具,可以达到跳过源码编写,编译,直接以字节码的形式创建类,修改已经存在类(或者jar中的class)的属性,方法等. 通常用来开发一些Java开发的辅助框架, ...
- aop 获取方法入参出参_ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称及入参和出参结果并记录方法耗时...
作者:小傅哥 博客:bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获! ❞ 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了 ...
- ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时
作者:小傅哥 博客:bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了多少代码 ...
- Android程序员的硬通货——ASM字节码插桩
作者:享学课堂Lance老师 转载请声明出处! 一.什么是插桩 QQ空间曾经发布的<热修复解决方案>中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPR ...
- 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...
- 字节码插桩(javassist)之插入代码块|IOC框架(Hilt)之对象注入~研究
Hilt对象注入 | javassist插桩 研究 Hilt对象注入 javassist字节码插桩 创建buildSrc的module 重写Transform 熟悉TransformInvocatio ...
- 调研字节码插桩技术,用于系统监控设计和实现
作者:小傅哥 博客:https://bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获!???? ❞ 目录 一.来自深夜的电话! 二.准备工作 三.使用 AOP 做个切面监控 1. ...
- Android AOP之字节码插桩
背景 本篇文章基于<网易乐得无埋点数据收集SDK>总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实 ...
- 看完这一篇,你也可以自如地掌握字节码插桩
/ 今日科技快讯 / 近日,一些国家的黑客频繁对俄罗斯发动网络攻击,以阻止它们正常运行.未来几天,俄罗斯可能与全球互联网断开.针对网络威胁,俄罗斯政府准备启动自己的"大局域网&quo ...
最新文章
- 查找重复文件_重复文件快速查找删除
- WebX5 button tabs的bind-text属性设置
- android tools ignore,android 中tools:ignore=UselessParent这个属性的含义是什么?
- 力士乐伺服电机编码器调零_力士乐伺服电机故障与维修排除备份构成
- Spark SQL(九)之基于用户的推荐公式
- IMP-00041: 警告: 创建的对象带有编译警告解决办法
- Ubuntu16.04下部署 nginx+uwsgi+django1.9.7(虚拟环境pyenv+virtualenv)
- isbool php,PHP PHPUnit assertIsBool()用法及代码示例
- 浅谈设备租赁管理系统的选型之路
- java sendredirect 参数_使用response.sendRedirect()传递隐藏参数
- ylbtech-LanguageSamples-Indexers(索引器)
- 版本管理-SVN本地版本管理
- 从零开始学UC(1)之Microsoft Lync Server介绍
- HenCoder自定义View学习 - 自定义绘制学习笔记
- MD5加密——使用Java自带的MessageDigest工具类实现
- Python实时爬取斗鱼弹幕
- Chrome浏览器查看Axure原型图文件,提示Axure RP Extension for Chrome
- Java Instrument(一) Java Agent
- 创业公司做数据分析(一)开篇
- 网络基础(二)之HTTP与HTTPS