ASM插桩

59 阅读5分钟

ASM(Apache Software Foundation的字节码操纵库)是一个轻量级的Java字节码操控框架,允许开发者在编译期或运行时动态地生成、转换或分析类文件(.class文件)。ASM修改class文件以实现插桩(Instrumentation)的原理大致可以概括为以下几个步骤:

  1. 解析Class文件: ASM提供了一套API,可以读取已存在的class文件,将其转换为ASM可以理解和操作的形式。这个过程涉及到将字节码指令解析为ASM可以处理的抽象结构,如类节点(ClassNode)、方法节点(MethodNode)等。

  2. 遍历和修改字节码: 开发者可以通过遍历类节点和其包含的方法节点来访问到每一个指令。ASM提供了访问者模式(Visitor Pattern)的实现,使得开发者可以方便地遍历这些抽象语法树(AST)的各个部分,并插入、删除或修改特定的字节码指令。

    • 插入代码:通过在方法的入口、出口或者特定指令前后插入新的字节码指令,实现监控、计数、日志记录等功能。
    • 修改已有代码:可以修改现有指令的操作数、跳转目标等,以改变程序的行为。
    • 删除代码:在某些情况下,也可以删除不需要的指令来优化或简化代码。
  3. 生成新Class文件: 完成修改后,ASM可以将修改后的抽象结构重新编译回字节码格式,并输出到一个新的class文件中。这个过程涉及到了解并生成符合Java虚拟机规范的二进制格式。

  4. 类加载: 最后,修改后的class文件需要被Java虚拟机加载。这可以通过自定义类加载器实现,在加载时动态替换原有的类定义,或者在应用打包阶段直接替换原有class文件。

ASM的优势在于其高度的灵活性和性能,它允许在字节码级别精细地控制修改过程,而不必了解复杂的Java字节码细节。此外,ASM是一个纯Java实现,不依赖反射,因此在运行时性能损失极小,适合用于性能敏感的场景,如AOP(面向切面编程)、性能监控、代码动态生成等。

二、要在Android项目中使用ASM进行插桩,通常会涉及到以下几个步骤:

1. 添加ASM依赖

首先,你需要在项目的build.gradle文件中添加ASM库的依赖。ASM有多个版本,包括ASM、ASM-Commons、ASM-Tree和ASM-Analysis等,具体根据需求选择。对于大多数插桩任务,ASM-Tree(提供了一个更加友好的API来操作字节码)和ASM-Commons(一些常用工具类)是常用的。

  • Groovy
1dependencies {
2    implementation 'org.ow2.asm:asm:9.0'
3    implementation 'org.ow2.asm:asm-tree:9.0'
4    implementation 'org.ow2.asm:asm-commons:9.0'
5}
2. 创建自定义Transform

在Android Gradle插件中,Transform API允许你在编译流程中插入自定义的变换操作,这正是ASM插桩的关键环节。你需要创建一个继承自Transform的类,并覆盖其方法,如transform(),在这个方法内部执行ASM代码注入。

  • Java
public class MyASMTransform extends Transform {

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 遍历输入的class文件,使用ASM修改字节码
    }

    // 其他需要重写的方法,如 isIncremental() 等
}
3. 注册Transform

在你的build.gradle文件或者自定义的Gradle插件中注册上述的Transform。

  • Groovy
project.afterEvaluate {
    android.registerTransform(new MyASMTransform())
}
4. 编写ASM逻辑

transform()方法中,利用ASM的API读取、修改、写回字节码。这通常涉及创建一个ClassReader读取输入的.class文件,然后用ClassWriter来生成修改后的字节码。你可以通过访问和修改类、方法、字段等来插入监控代码或进行其他修改。

  • Java
ClassReader cr = new ClassReader(transformInvocation.getInputProvider().openInputStream(input));
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new MyClassVisitor(cw); // 自定义的ClassVisitor来处理字节码修改
cr.accept(cv, ClassReader.EXPAND_FRAMES);
5. 测试和调试

完成ASM逻辑编写后,需要在实际项目中测试确保插桩逻辑正确无误,并不会引入新的错误或性能问题。调试ASM插桩可能相对复杂,因为错误通常会在运行时出现,而且错误信息可能不直观。使用日志和调试工具是必要的。

注意事项
  • 确保ASM版本与Android Gradle插件兼容。
  • 插桩应尽量减少对原有代码的影响,避免引入过多的性能开销。
  • 考虑增量编译的兼容性,确保Transform支持增量变换。
  • 安全性和稳定性是首要考虑因素,特别是在生产环境中使用ASM插桩时。

三、ASM字节码插桩 以及 AutoRegister

  • 安装插件(字节码)
  • 实现字节码插桩
  • 问题、效果
  • ASM插件使用
  • 实现效果
  • 运行App看效果

安装字节码插件

image.png

字节码插桩

private void generateCodeIntoJarFile(File jarFile){
         def optJar = new File(jarFile.getParent(), jarFile.name + ".opt")
         if (optJar.exists())
             optJar.delete()
         def file = new JarFile(jarFile)
         Enumeration enumeration = file.entries()
         JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
         while (enumeration.hasMoreElements()) {
             JarEntry jarEntry = (JarEntry) enumeration.nextElement()
             String entryName = jarEntry.getName()
             ZipEntry zipEntry = new ZipEntry(entryName)
             InputStream inputStream = file.getInputStream(jarEntry)
             jarOutputStream.putNextEntry(zipEntry)
             if (isNeedInsertClass(entryName)) {
                 project.logger.error('generate code into:--- '+entryName)
                 def bytes = doGenerateCode(inputStream)
                 jarOutputStream.write(bytes)
             } else {
                 jarOutputStream.write(IOUtils.toByteArray(inputStream))
             }
             inputStream.close()
             jarOutputStream.closeEntry()
         }
         jarOutputStream.close()
         file.close()

         if (jarFile.exists()) {
             jarFile.delete()
         }
         optJar.renameTo(jarFile)

     }

  boolean isNeedInsertClass(String entryName) {
         if (entryName == null || !entryName.endsWith(".class"))
             return false
         if (needInsertClassNameLeft) {
             entryName = entryName.substring(0, entryName.lastIndexOf('.'))
             return needInsertClassNameLeft == entryName
         }
         return false
     }
  • 实现字节码插桩关键代码
    /**
     * 就这里利用ASM生成新的字节码
     * @param inputStream
     * @return
     */
    private byte[] doGenerateCode(InputStream inputStream) {
        //套路和扫描哪里一样
        ClassReader cr = new ClassReader(inputStream)
        ClassWriter cw = new ClassWriter(cr, 0)
        //这里是扫描的类访问器,需要需要新定义一个代码插入的类访问器
        ClassVisitor cv = new MyTestClassVisitor(project,Opcodes.ASM5, cw)
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

问题1

image.png

缺少文件,需要复制产物文件,解决问题

image.png

问题2

image.png

visitMethod方法需要返回结果

image.png

效果 (1) : 方法ASM码

image.png

四、ASM插件使用

1、查看Class文件 ASM码

image.png

2、Class源文件写入要插入的代码

image.png

3、查看上面两部分不同

image.png

image.png

4、复制不同内容,简单修改

   Label label1 = new Label();
   methodVisitor.visitLabel(label1);
   methodVisitor.visitLineNumber(14, label1);
   methodVisitor.visitTypeInsn(NEW, "com/billy/app_lib_interface/CategoryD");
   methodVisitor.visitInsn(DUP);
   methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/billy/app_lib_interface/CategoryD", "<init>", "()V", false);
   methodVisitor.visitMethodInsn(INVOKESTATIC, "com/billy/app_lib_interface/CategoryManager", "register", "(Lcom/billy/app_lib_interface/ICategory;)V", false);
   Label label2 = new Label();
   methodVisitor.visitLabel(label2);
   methodVisitor.visitLineNumber(15, label2);

实现效果

  • 未ASM代码注入时代码

public class CategoryManager {
    private static HashMap<String, ICategory> CATEGORIES = new HashMap<>();

    static void register(ICategory category) {
        if (category != null) {
            CATEGORIES.put(category.getName(), category);
        }
    }

    public static Set<String> getCategoryNames() {
        return CATEGORIES.keySet();
    }
}
  • 实现ASM代码注入后代码

public class CategoryManager {
    private static HashMap<String, ICategory> CATEGORIES = new HashMap<>();
    
    static { //静态代码注入
        CategoryManager.register(new CategoryD());
    }

    static void register(ICategory category) {
        if (category != null) {
            CATEGORIES.put(category.getName(), category);
        }
    }

    public static Set<String> getCategoryNames() {
        return CATEGORIES.keySet();
    }
}

运行效果

1、原有效果

image.png

2、运行后效果

image.png