美文网首页
ASM编译时操作字节码

ASM编译时操作字节码

作者: SunnyDay_ab5f | 来源:发表于2023-06-26 11:22 被阅读0次

    一、ASM简介

    ASM 是一个功能比较齐全的 Java 字节码操作与分析框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接 产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类的行为。

    二、ASM修改calss文件

    ASM的核心就是通过访问者模式返回的事件中,拿到对应的访问元素,并对其进行修改。

    ASM在代码中使用的一般流程如下:

    1.InputStream读取Class文件
    2.基于InputStream流,创建ClassReader实例,加载字节码文件
    3.创建ClassWriter实例,用于写入修改的字节码
    4.基于ClassWriter创建ClassVisitor实例
    5.触发ClassReader对象解析Class信息(accept classVisitor)
    6.ClassWriter接收传递过来的数据并写入新文件

    ClassVisitor
    ClassVisitor提供了对class内部的Annotation、Field、Method的访问机制事件。

    整个访问流程如下所示:
    visit —— visitSource —— visitModule —— visitNestHost —— visitOuterClass —— visitAnnotation —— visitTypeAnnotation —— visitAttribute —— visitNestMember —— visitInnerClass —— visitField —— visitMethod —— visitEnd

    其中,visit,visitEnd一定会调用一次,visitModule最多调用一次,而剩下的都有可能调用多次。

    ClassVisitor的各个访问器,对应了该类文件中的每个结构和访问节点,当访问到对应的内容时,就会触发相应的解析回调,在这些访问方法中,类似visit,visitEnd这样的方法,会直接返回void类型,而visitAnnotation、visitField、visitMethod则返回对应的AnnotationVisitor、FieldVisitor、MethodVisitor,通过这些辅助访问者来进一步深入访问更加精细的事件。

    通过ClassVisitor中的各种事件的回调,以及辅助Visitor的精细化事件,就可以让使用者在不用关系字节码内部偏移而方便的修改字节码,ClassVisitor会管理这些过程,使用者通过覆写对应的Visitor即可实现对字节码的修改。

    MethodVisitor
    MethodVisitor的调用流程如下:

    visitParameter —— visitAnnotationDefault —— visitAnnotation —— visitAnnotableParameterCount —— visitParameterAnnotation —— visitTypeAnnotation —— visitAttribute —— visitCode —— visitFrame —— visitInsn —— visitLabel —— visitInsnAnnotation —— visitTryCatchBlock —— visitTryCatchAnnotation —— visitLocalVariable —— visitLocalVariableAnnotation —— visitLineNumber —— visitMaxs —— visitEnd

    MethodVisitor提供了对方法的访问机制,例如onMethodEnter、onMethodExit等。

    visitCode:访问的开始
    visitMaxs:访问的结束
    visitInsn:访问无操作数指令,例如return
    visitLdcInsn:访问ldc指令,也就是访问常量池索引
    visitMethodInsn:访问方法指令,也就是调用某个方法

    ClassWriter
    ClassWriter如其所描述的这样,用于将字节码写回到文件中,如果仅仅是修改字节码,那么直接通过toByteArray方法,就可以将字节流传给FileOutputStream,如果是创建新的Class,那么则需要使用到ClassWriter的一些内部方法。

    ClassWriter是继承自ClassVisitor的,所以,它同样是通过各种Visitor来访问各个节点的。

    ClassWriter的常用方法如下,与ClassVisitor类似,但是作用不同。

    ClassReader
    ClassReader用于加载字节码文件,并开启访问者模式,将Class文件拆分成多个访问事件,并回调各种Visitor。

    ClassReader(byte[] b)
    ClassReader(byte[] b, int off, int len)
    ClassReader(InputStream is)
    ClassReader(String name)
    ClassReader提供了多种访问文件的方式,用来加载字节码。

    前面提到的各种VisitXXX方法的调用顺序,实际上就是在ClassReader的accept方法中的调用顺序。

    ClassReader的accept(final ClassVisitor classVisitor, final int parsingOptions)方法中的parsingOptions参数代表用于解析class的选项,有以下取值:

    ClassReader.SKIP_CODE:排除代码访问的所有方法,同时还通过方法参数属性和注释
    ClassReader.SKIP_FRAMES——跳过StackMap和*StackMapTable属性的标志,跳过MethodVisitor.visitFrame方法,对于我们开发者来说最好选这个
    ClassReader.SKIP_DEBUG——用于忽略debug信息,例如,源文件,行数和变量信息
    ClassReader.EXPAND_FRAMES——扩展StackMapTable数据,允许访问者获取全部本地变量类型与当前堆栈位置的信息,这会大大降低性能,不过建议使用这个标志

    下面是自定义的transform实现,需求是在使用自定义gradle插件的工程中对Test类的test方法中调用add方法

    package com.example.agptest2;
    public class Test {
    
        public void test(){
    
        }
    
    
        public void add(){
    
        }
    }
    

    定义一个MyPlugin 插件

    public class MyPlugin implements Plugin<Project> {
    
        @Override
        public void apply(Project project) {
            System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.");
            // 在Plugin中注册自定义的Transform
            AppExtension baseExtension = project.getExtensions().findByType(AppExtension.class);
            if (baseExtension != null) {
                baseExtension.registerTransform(new MyTransform(project));
            }
    
    //        boolean isApp = project.getPlugins().hasPlugin(AppPlugin.class);
    //        //只有application模块才需要本插件去生成注册代码
    //        if (isApp) {
    //            System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1111111.");
    //            AppExtension android = project.getExtensions().getByType(AppExtension.class);
    //            //注册transform
    //            android.registerTransform(new MyTransform());
    //            System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2222222.");
    //        }
    
        }
    
    }
    

    继承Transform 实现自己的MyTransform类

    public class MyTransform extends Transform {
        Project project;
    
        public MyTransform(Project project) {
            this.project = project;
            System.out.println("Hello MyTransform..1111.");
        }
    
    
        @Override
        public String getName() {
            //指定Transform的名称在gradle执行时会打印该任务
            return "MyTransform";
        }
    
        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            //用于指明 Transform 的输入类型,可以作为输入过滤的手段。这里我们过滤class文件即可
            return TransformManager.CONTENT_CLASS;
        }
    
        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            //用于指明 Transform 的作用域。这里指定为整个工程
            return TransformManager.SCOPE_FULL_PROJECT;
        }
    
        @Override
        public boolean isIncremental() {
            //指明该 Transform 是否支持增量编译
            return false;
        }
         //在这个方法中对字节码进行处理
        @Override
        public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation);
            System.out.println("Hello MyTransform..2222222222222222.");
            //是否支持增量编译
            boolean incremental = transformInvocation.isIncremental();
            //获取输出
            TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
            //如果不支持增量编译,需要把之前生成的都删除,不复用缓存
            if (!incremental) {
                outputProvider.deleteAll();
            }
            for (TransformInput input : transformInvocation.getInputs()) {
                //处理jar
                Collection<JarInput> jarInputs = input.getJarInputs();
                if (jarInputs != null && jarInputs.size() > 0) {
                    for (JarInput jarInput : jarInputs) {
                        System.out.println("-----------name = " + jarInput.getName());
                        File dest = outputProvider.getContentLocation(jarInput.getFile().getAbsolutePath(),jarInput.getContentTypes(),jarInput.getScopes(),Format.JAR);
                        FileUtils.copyFile(jarInput.getFile(),dest);
                    }
                }
            }
    
    
            for (TransformInput input : transformInvocation.getInputs()) {
                //处理dir
                Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
                if (directoryInputs != null && directoryInputs.size() > 0) {
                    for (DirectoryInput directoryInput : directoryInputs) {
                        File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                        transformDir(directoryInput.getFile(),dest);
                    }
                }
            }
        }
    
    
    
    
        private void transformDir(File src,File dest){
            String rootPath = src.getAbsolutePath();
            if (!rootPath.endsWith(File.separator)){
                rootPath += File.separator;
            }
            if (src.isDirectory()) {
                //获取目录下的所有文件
                FluentIterable<File> allFiles = FileUtils.getAllFiles(src);
                String finalRootPath = rootPath;
                 //遍历处理
                allFiles.forEach(new Consumer<File>() {
                    @Override
                    public void accept(File file) {
                        //截取替换字符
                        System.out.println("-----------allFiles.forEach  path = " + file.getPath());
                        String path = file.getAbsolutePath().replace(finalRootPath,"");
                        path = path.replaceAll("\\\\","/");
    
                        System.out.println("-----------allFiles.forEach 222222 path = " + path);
                        //判断是否是自己想处理的类
                        if (file.isFile()&& path != null && path.equals("com/example/agptest2/Test.class")){
                            System.out.println("-----------allFiles.forEach 333333 path = " + path);
                            ClassReader cr = null;
                            FileInputStream inputStream = null;
                            ClassWriter cw = null;
                            try {
                                //获取输入流
                                inputStream = new FileInputStream(file);
                                //读取分析字节码
                                cr = new ClassReader(inputStream);
                                cw = new ClassWriter(cr, 0);
                                //扫描该文件
                                ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM7, cw);
                                cr.accept(cv, ClassReader.EXPAND_FRAMES);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }finally {
                                try {
                                    inputStream.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                            //输出修改后的文件
                            byte[] code = cw.toByteArray();
                            FileOutputStream fos = null;
                            try {
                                fos = new FileOutputStream(file.getAbsolutePath());
                                fos.write(code);
                            } catch (FileNotFoundException e) {
                                e.printStackTrace();
                            } catch (IOException e) {
                                e.printStackTrace();
                            } finally {
                                try {
                                    fos.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
    
    
                    }
                });
                try {
                    //拷贝文件到dest,等待其他插件处理
                    FileUtils.copyDirectory(src,dest);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    
    public class ScanClassVisitor extends ClassVisitor {
       public ScanClassVisitor(int asm7, ClassWriter cw) {
          super(asm7,cw);
    
       }
    
       @Override
       public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
          super.visit(version, access, name, signature, superName, interfaces);
          System.out.println("visit name = "+name);
       }
    
       @Override
       public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
          System.out.println("visitMethod name = "+name);
          MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
          //如果方法名和我们要修改的方法相同则使用我们自定义的ScanMethodVisitor类进行处理
          if ("test".equals(name)) {
             methodVisitor = new ScanMethodVisitor(Opcodes.ASM7,methodVisitor);
          }
          return methodVisitor;
       }
    }
    
    public class ScanMethodVisitor extends MethodVisitor {
    
       public ScanMethodVisitor(int api, MethodVisitor methodVisitor) {
          super(api, methodVisitor);
       }
    
    
       @Override
       public void visitInsn(int opcode) {
          System.out.println("visitInsn-------------------"); 
          //方法return之前插入要添加的代码
          if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
             mv.visitVarInsn(Opcodes.ALOAD, 0);
             mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/example/agptest2/Test", "add", "()V", false);
             System.out.println("visitInsn-------------------111111");
          }
          super.visitInsn(opcode);
       }
    
       @Override
       public void visitIntInsn(int opcode, int operand) {
    
          super.visitIntInsn(opcode, operand);
       }
    
    //   @Override
    //   public void visitMaxs(int maxStack, int maxLocals) {
    //      super.visitMaxs(1, 1);
    //   }
    }
    

    ScanMethodVisitor 中要插入的那两行代码我们可以通过Androidstudio的插件ASM Bytecode Viewer进行文件修改前后的差异进行提取。
    下面是test方法中未调用add方法时的字节码

    public class com/example/agptest2/Test {
    
      // compiled from: Test.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 15 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x1
      public test()V
       L0
        LINENUMBER 19 L0
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L1 0
        MAXSTACK = 0
        MAXLOCALS = 1
    
      // access flags 0x1
      public add()V
       L0
        LINENUMBER 24 L0
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L1 0
        MAXSTACK = 0
        MAXLOCALS = 1
    }
    

    借助ASMified转换后的代码

    package asm.com.example.agptest2;
    
    import org.objectweb.asm.AnnotationVisitor;
    import org.objectweb.asm.Attribute;
    import org.objectweb.asm.ClassReader;
    import org.objectweb.asm.ClassWriter;
    import org.objectweb.asm.ConstantDynamic;
    import org.objectweb.asm.FieldVisitor;
    import org.objectweb.asm.Handle;
    import org.objectweb.asm.Label;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    import org.objectweb.asm.Type;
    import org.objectweb.asm.TypePath;
    
    public class TestDump implements Opcodes {
    
        public static byte[] dump() throws Exception {
    
            ClassWriter classWriter = new ClassWriter(0);
            FieldVisitor fieldVisitor;
            MethodVisitor methodVisitor;
            AnnotationVisitor annotationVisitor0;
    
            classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/example/agptest2/Test", null, "java/lang/Object", null);
    
            classWriter.visitSource("Test.java", null);
    
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(15, label0);
                methodVisitor.visitVarInsn(ALOAD, 0);
                methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(1, 1);
                methodVisitor.visitEnd();
            }
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(19, label0);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(0, 1);
                methodVisitor.visitEnd();
            }
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "add", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(24, label0);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(0, 1);
                methodVisitor.visitEnd();
            }
            classWriter.visitEnd();
    
            return classWriter.toByteArray();
        }
    }
    
    

    test方法中调用add方法后的字节码

    // class version 52.0 (52)
    // access flags 0x21
    public class com/example/agptest2/Test {
    
      // compiled from: Test.java
    
      // access flags 0x1
      public <init>()V
       L0
        LINENUMBER 15 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x1
      public test()V
       L0
        LINENUMBER 18 L0
        ALOAD 0
        INVOKEVIRTUAL com/example/agptest2/Test.add ()V
       L1
        LINENUMBER 19 L1
        RETURN
       L2
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L2 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x1
      public add()V
       L0
        LINENUMBER 24 L0
        RETURN
       L1
        LOCALVARIABLE this Lcom/example/agptest2/Test; L0 L1 0
        MAXSTACK = 0
        MAXLOCALS = 1
    }
    
    

    借助ASMified转换后的代码

    package asm.com.example.agptest2;
    
    import org.objectweb.asm.AnnotationVisitor;
    import org.objectweb.asm.Attribute;
    import org.objectweb.asm.ClassReader;
    import org.objectweb.asm.ClassWriter;
    import org.objectweb.asm.ConstantDynamic;
    import org.objectweb.asm.FieldVisitor;
    import org.objectweb.asm.Handle;
    import org.objectweb.asm.Label;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Opcodes;
    import org.objectweb.asm.Type;
    import org.objectweb.asm.TypePath;
    
    public class TestDump implements Opcodes {
    
        public static byte[] dump() throws Exception {
    
            ClassWriter classWriter = new ClassWriter(0);
            FieldVisitor fieldVisitor;
            MethodVisitor methodVisitor;
            AnnotationVisitor annotationVisitor0;
    
            classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/example/agptest2/Test", null, "java/lang/Object", null);
    
            classWriter.visitSource("Test.java", null);
    
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(15, label0);
                methodVisitor.visitVarInsn(ALOAD, 0);
                methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(1, 1);
                methodVisitor.visitEnd();
            }
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(18, label0);
                methodVisitor.visitVarInsn(ALOAD, 0);
                methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/example/agptest2/Test", "add", "()V", false);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLineNumber(19, label1);
                methodVisitor.visitInsn(RETURN);
                Label label2 = new Label();
                methodVisitor.visitLabel(label2);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label2, 0);
                methodVisitor.visitMaxs(1, 1);
                methodVisitor.visitEnd();
            }
            {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "add", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(24, label0);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(0, 1);
                methodVisitor.visitEnd();
            }
            classWriter.visitEnd();
    
            return classWriter.toByteArray();
        }
    }
    
    

    对比下两个文件中test方法的区别
    修改前

     {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(19, label0);
                methodVisitor.visitInsn(RETURN);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label1, 0);
                methodVisitor.visitMaxs(0, 1);
                methodVisitor.visitEnd();
            }
    

    修改后:

     {
                methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
                methodVisitor.visitCode();
                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(18, label0);
                methodVisitor.visitVarInsn(ALOAD, 0);
                methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "com/example/agptest2/Test", "add", "()V", false);
                Label label1 = new Label();
                methodVisitor.visitLabel(label1);
                methodVisitor.visitLineNumber(19, label1);
                methodVisitor.visitInsn(RETURN);
                Label label2 = new Label();
                methodVisitor.visitLabel(label2);
                methodVisitor.visitLocalVariable("this", "Lcom/example/agptest2/Test;", null, label0, label2, 0);
                methodVisitor.visitMaxs(1, 1);
                methodVisitor.visitEnd();
            }
    

    需要注意在比对时一下类似的代码可以进行忽略

                Label label0 = new Label();
                methodVisitor.visitLabel(label0);
                methodVisitor.visitLineNumber(18, label0);
    

    最终我们获取到的差异代码如下:

             mv.visitVarInsn(Opcodes.ALOAD, 0);
             mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/example/agptest2/Test", "add", "()V", false);
    

    我们再次执行使用插件的工程可以发现build目录下javac生成的Test.class文件中test方法中已经有调用add方法了。如下图:


    截取图片_20230627111929.png

    相关文章

      网友评论

          本文标题:ASM编译时操作字节码

          本文链接:https://www.haomeiwen.com/subject/mnnuedtx.html