Android编译时技术(三)ASM 基础使用之代码修改
一、前言
在上一篇已经介绍了ASM如何生成代码,接下来我们将介绍如何用ASM工具对代码进行插入和修改。先说下我们这章节的任务,如下:
- 对已有的Class文件的方法里插入方法执行时间的统计
二、加载Class文件
想要修改一个Class文件的内容,那肯定要先把Class文件加载进内存,不然你修改个&…(%¥。正好,在ASM工具里,有一个ClassReader类就可以帮助我们读取Class文件。接下来让我们看看如何使用
2.1 准备一个Class文件
准备一个Class文件,你可以先写一个Java文件,通过编译器编译或者自己用javac命令都可以。
2.2 读取Class文件
直接上代码(具体路径看自己Class文件所在)
public class ASMLoadDemo { public static void main(String[] args) throws Exception { insertCodeByLoadClass("D:\\20210426\\code\\JavaProjec\\JMMDemo\\out\\production\\JMMDemo\\asm\\InjectTest.class"); } /** * 加载Class插入代码 * * @param path */ public static void insertCodeByLoadClass(String path) { try { //1.获取Class文件 FileInputStream fis = new FileInputStream(path); //2.读取Class文件 ClassReader classReader = new ClassReader(fis); //3.准备 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //4.重点!! 开始插桩 对代码进行扫描,进行代码插入。 classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES); //5.插桩结束 输出字节码 byte[] bytes = classWriter.toByteArray(); //6.保存文件 FileOutputStream fos = new FileOutputStream("src/asm/InjectTest2.class"); fos.write(bytes); fos.close(); fis.close(); } catch (Exception e) { e.printStackTrace(); } }}
MyClassVisitor代码
/** * 用来观察(扫描)类信息,包括变量、方法 */ static class MyClassVisitor extends ClassVisitor { public MyClassVisitor(int api) { super(api); } public MyClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } /** * 当有一个方法,就执行这个回调一次,类中有多个方法,这里就会执行多次 */ @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("读到一个方法visitMethod: " + name); MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); return new MyMethodVisitor(api, methodVisitor, access, name, descriptor); } }
MyMethodVisitor代码
/** * 用来观察(扫描)方法里的代码 */ static class MyMethodVisitor extends AdviceAdapter { protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { super(api, methodVisitor, access, name, descriptor); } int startTimeIndex;//startTime在局部变量表的位置 int endTimeIndex;//endTime在局部变量表的位置 /** * 方法进入的时候调用 */ @Override protected void onMethodEnter() { super.onMethodEnter(); //调用System.currentTimeMillis方法,调用完后将返回值推到栈顶 invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); //获取当前局部变量表可插入的位置,不能乱传,不然会影响下一个值的索引获取,这里你传DOUBLE_TYPE或者LONG_TYPE,都是一样的 startTimeIndex = newLocal(Type.LONG_TYPE); //把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表startTimeIndex位置 storeLocal(startTimeIndex); System.out.println("onMethodEnter getName: " + getName() + " startIndex " + startTimeIndex); } /** * 当方法退出的时候调用 */ @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); //调用System.currentTimeMillis方法,调用完后将返回值推到栈顶 invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); //获取当前局部变量表可插入的位置 endTimeIndex = newLocal(Type.LONG_TYPE); //把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表endTimeIndex位置 storeLocal(endTimeIndex); //调用System.out,调用完后栈顶会有一个 getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;")); //new 一个 StringBuilder();并将其引用值压入栈顶 newInstance(Type.getType("Ljava/lang/StringBuilder;")); //复制栈顶数值并将复制值压入栈顶(对象StringBuilder的引用) dup(); //调用StringBuilder的构造方法 invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("", "()V")); //将"execute time=" 从常量池中推送至栈顶 visitLdcInsn("execute time="); //调用StringBuilder的append方法,将栈顶的值添加进StringBuilder invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;")); //把局部变量表中的endTimeIndex位置元素,压入栈顶 loadLocal(endTimeIndex); //把局部变量表中的startTimeIndex位置元素,压入栈顶 loadLocal(startTimeIndex); //将栈顶两元素进行相减,把结果再压入栈顶 math(SUB, Type.LONG_TYPE); //调用append方法,将栈顶值添加 invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(J)Ljava/lang/StringBuilder;")); //将" ms" 从常量池中推送至栈顶 visitLdcInsn(" ms"); 调用StringBuilder的append方法,将栈顶的值添加进StringBuilder invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;")); 调用StringBuilder的toString方法,调用完后,将结果推到栈顶 invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("toString", "()Ljava/lang/String;")); //调用println,打印栈顶的值 invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V;")); } }
先看看效果
可以看到,每个方法都插入方法执行时间的统计(如何优化的问题留到最后后面🤭)。
看完结果,我们先抛开MyClassVisitor和MyMethodVisitor流程很简单,总共就三步
- 读取Class文件
- 使用ClassVisitor类对Class文件进行代码扫描,这一步就可以进行代码的修改和插入了。
- 代码修改插入结束后,自然就可以重新输出Class文件,这里我们给Class改个名为InjectTest2,方便观察。
可以看到,最重要的其实就是第二步,对字节码进行插桩(你也可以叫代码观察,代码扫描,你喜欢就行😀),如果你有第一节ASM如何生成代码的经验,上面的代码唯一不懂的也就是MyClassVisitor,其实这就一个对类里面代码进行扫描的类。相应的,还会MethodVisitor,FiledVisitor,知道了ClassVisitor是什么意思后,这两个也不难理解。
接下来让我们一起看看ClassVisitor和MethodVisitor的用法解释。如果知道的朋友,可以直接跳到优化章节了。
三、ClassVisitor
看名字就能看出来,这是一个对Class文件进行观察(扫描)的工具类。因为它是一个抽象类,所以我们只能对其进行继承重写。
并且ClassVisitor所有的调用都是由ClassReader来进行回调,就是我们前面accept方法。而这个方法里,执行了对ClassVisitor源码扫描的回调,截图如下:
接下来我们看看ClassVisitor的构造方法
3.1 构造方法
protected ClassVisitor(final int api, final ClassVisitor classVisitor)
参数解释:
- api:代表是ASM API的版本 ,必须是如下的版本号,默认填最新(ASM9)即可。
注意:编写代码和代码读取的版本号最好保持一致,不然可能会有一些兼容性的错误,例如下面,一些API版本的判断。
- classVisitor:传一个ClassVisitor的子类,对字节码修改的内容进行保存。
上面这个解释可能会让你感到不可思议,我们可以先看我们上面代码这个参数传的是ClassWriter的实例,也就是说ClassWriter继承于ClassVisitor。其实也不难理解,这里ClassWriter的作用就是保存我们修改的信息,然后重新输出字节码,因为ClassVisitor就是一个纯粹的代码扫描类(可以自行去看他的源码,没有保存修改代码的地方)。那么这个字段的作用其实就是用来对字节码修改进行保存的,我们简单来看下ClassVisitor某个方法的回调。例如:visitMethod方法,因为我们就是在这里进行字节码修改的
public MethodVisitor visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { if (cv != null) { return cv.visitMethod(access, name, descriptor, signature, exceptions); } return null; }
可以看到,方法最终的调用都给了cv,而这个cv就是我们传进来的ClassWriter 。不仅是这个方法,在ClassVisitor中,除了构造方法外,其他所有的方法最终的执行都是给了cv,也就是说,ClassVisitor就是一个代理类。
接下来介绍几个常用回调方法。
3.2 visitMethod
作用:回调Class中的每一个方法
源代码如下:
@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { return super.visitMethod(access, name, descriptor, signature, exceptions); }
参数的解释这里就不过说明了,在上章节中有具体说明,这里说下如何使用来修改字节码。
其实也很简单,固定用法,删除某个方法
@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("读到Class的一个方法visitMethod: " + name); if (name.equals("test")){ //删除test方法 return null; } return super.visitMethod(access, name, descriptor, signature, exceptions); }
想对方法体修改:
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
为什么需要拿到super.visitMethod呢?
因为这个拿到的methodVisitor不就是我们传入的ClassWriter的MethodVisitor,不拿到它,最后怎么把代码合成一份。
3.3 visitField
作用:回调Class中的每一个变量
使用如下,删除/修改一个变量
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { System.out.println("读到Class的一个变量visitField: " + name); if (name.equals("a")){ //删除a变量 return null; } if (name.equals("b")){ //修改b变量名词为d,值为10 return super.visitField(access, "d", descriptor, signature, 10); } return super.visitField(access, name, descriptor, signature, value); }
添加一个变量
@Override public void visitEnd() { super.visitEnd(); FieldVisitor fv = cv.visitField(Opcodes.ACC_PRIVATE, "e", "I", null, null); if (fv != null) { fv.visitEnd(); } }
3.4 visitAnnotation
作用:回调Class上的每一个注解
@Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { return super.visitAnnotation(descriptor, visible); }
参数解释:
- descriptor:注解的描述符
- visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false
四、MethodVisitor
类似于ClassVisitor,MethodVisitor是对方法进行扫描,包括方法上的注解,方法内的所有代码。
在本文中,我们使用的是继承于AdviceAdapter的类,其实它最终也是继承于MethodVisitor,相比于MethodVisitor,它有更多封装好的功能,方便我们对代码进行查看修改(具体可以看源码),所以这里主要介绍AdviceAdapter的用法。
当然,你也可以直接继承MethodVisitor使用。
4.1 onMethodEnter
作用:方法进入时调用,可以在方法执行前插入代码
用法:根据JVM指令,使用下列指令进行代码插件修改
4.2 onMethodExit
作用:方法退出时调用,可以在方法执行后插入代码
用法:根据JVM指令,使用下列指令进行代码插件修改
4.3 invokeStatic
作用:调用一个静态方法,也就是static。实际调用的还是MethodVisitor的visitMethodInsn
例如:Java代码,调用系统静态方法
long start = System.currentTimeMillis();
对应的JVM字节码
INVOKESTATIC java/lang/System.currentTimeMillis ()J LSTORE 1
注意1:如果调用的方法有返回值,会自动压入栈顶!!
注意2:Long和Double类型的值保存进局部变量表会占用2个位置,也就是两个slot,一个slot是32位。slot是局部变量表的单位
用法如下:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); int startIndex = newLocal(Type.LONG_TYPE); storeLocal(startIndex); //上面两句等同于下面这一句,不推荐下面这种 storeLocal(1)
注意1:这里newLocal只是为了方便类统计局部变量表的索引,传Type_LONG_TYPE和随便一个值他的返回值都是一样的,影响的只会是下一个调用的人,所以说要传正确的值类型。
同理,invokeConstructor,invokeVirtual,invokeDynamic,invokeInterface调用的还是MethodVisitor的visitMethodInsn。
4.4 newLocal
作用:创建一个局部变量的类型,类会自动加上类型的Size,返回值是局部变量表里的索引。
用法如下:
newLocal(Type.LONG_TYPE);
提示:如果此时局部变量表是空的,第一次调用上面语句后,会返回0的索引,下一次再调用上面语句,就是返回2的索引,因为Long占用两个slot。
4.5 storeLocal
作用:将栈顶的值保存到局部变量表的指令,实际调用的还是MethodVisitor的visitVarInsn,最好就是搭配newLocal方法使用
例如上面的invokeStatic 例子,当调用完inkeStatic命令完,栈顶有值,就可以执行storeLocal命令。如下:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); int startIndex = newLocal(Type.LONG_TYPE); storeLocal(startIndex);
提示:也不一定要调用newLocal,不过就需要你手动计算索引,填入正确的索引
4.6 push
作用:将给定的值压入栈顶,一般要搭配storeLocal使用,实际调用的还是MethodVisitor的visitInsn
例如:Java代码,创建一个变量
String a = "hhhh"; long b = 2L; double c = 2.22D;
对应的JVM字节码
L0 LINENUMBER 16 L0 LDC "hhhh" ASTORE 1 L1 LINENUMBER 17 L1 LDC 2 LSTORE 2 L2 LINENUMBER 18 L2 LDC 2.22 DSTORE 4
用法如下:
push("hhhh"); storeLocal(newLocal(Type.CHAR_TYPE)); push(2L); storeLocal(newLocal(Type.LONG_TYPE)); push(2.22); storeLocal(newLocal(Type.LONG_TYPE));
问题:我试过push(1),但是写入Class里的内容却是Boolean,没搞懂,有知道的小伙伴欢迎评论。
4.7 getStatic
作用:获取一个静态的实例变量,实际调用的还是MethodVisitor的visitFieldInsn
例如:
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
同理,getFiled就是获取非静态的实例变量,但是这场景没见过。
相应的,putStatic和putFiled就是给静态实例变量赋值和非静态变量赋值。
提示:不是直接调用就能赋值的,赋值前栈顶需要有对应的变量和值。
4.8 newInstance
作用:创建一个对象,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitTypeInsn
4.9 dup
作用:从栈顶的对象赋值一个引用,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitInsn
例如下面代码:
StringBuilder stringBuilder = new StringBuilder();
用法:
//new 一个 StringBuilder();并将其引用值压入栈顶 newInstance(Type.getType("Ljava/lang/StringBuilder;")); //复制栈顶元素(对象StringBuilder的引用) dup();
4.10 visitLdcInsn
作用:将传入的值从常量池中推送至栈顶
4.11 loadLocal
作用:把局部变量表中的元素,压入栈顶
4.12 math
作用:将栈顶的两个元素进行计算(例如:加减乘除等),并把结果值重新压入栈顶。
五、FieldVisitor
类似于ClassVisitor,FieldVisitor是对变量的信息进行扫描,包括变量上的注解,变量的参数。
5.1 visitAnnotation
作用:回调变量上的每一个注解
源代码:
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
参数解释:
- descriptor:注解的描述符
- visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false
六、代码优化
可以看到,生成出来的Class文件对每个方法都进行了代码插入,这很明显不是我们需要的。解决方法如下:
第一种,进行方法过滤
@Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("读到Class的一个方法visitMethod: " + name); if (!name.equals("test")) { return super.visitMethod(access, name, descriptor, signature, exceptions); } MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); return new MyMethodVisitor(api, methodVisitor, access, name, descriptor); }
这种虽然很方便,但是好像也太Low了。我们再来看第二种
第二种,注解方式
很简单,创建一个注解(@ASMTarget),然后在对应需要插入代码的方法上写上我们注解。例如:
ASMTarget代码
@Retention(RetentionPolicy.CLASS)@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE})public @interface ASMTarget {}
提前在需要插入代码的方法上写上@ASMTarget
@ASMTarget public static void main(String[] args) throws InterruptedException { Thread.sleep(1234); }
然后我们看看MethodVisitor代码要如何修改,如下::
boolean needInsertCode = false;//标记是否需要插入代码 @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { if ("Lasm/ASMTarget;".equals(descriptor)) { needInsertCode = true; } return super.visitAnnotation(descriptor, visible); }
然后在onMethodEnter和onMethodExit进行拦截即可,代码如下。
@Override protected void onMethodEnter() { super.onMethodEnter(); if (!needInsertCode) { return; } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (!needInsertCode) { return; }
好了,本篇到此结束。