查看原文
其他

从精准化测试看ASM在Android中的强势插入-ASM

群英传 2022-12-21

点击上方蓝字关注我,知识会给你力量


该我上场表演啦,终于到了我们的核心——ASM。

官网镇楼

https://asm.ow2.io/developer-guide.html

ASM是一个操纵字节码的开源工具,可以在编译期间对原始字节码插入一些新的逻辑,它通常会和Gradle Transform配合使用。

ASM包含两种API使用方式——Core API和Tree API,大部分场景下都是使用Core API。

Core API是基于事件访问的形式来表示类,把类抽象为一系列事件,每个事件表示类的一种元素,例如它的一个标头、一个字段、一个方法声明、一条指令等。

Core API定义了一组可能事件,以及这些事件必须遵循的访问顺序,还提供了一个ClassVisitor,它为每个被分析元素生成一个事件,还提供一个ClassWriter,用于将这些事件的序列生成经过编译的类。

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

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

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

ASM的工作流程如下:

  1. ClassReader加载字节码文件并开启访问者模式
  2. 将Class文件拆分成访问事件
  3. 遍历ClassVisitor的Visitor方法,对事件进行回调
  4. 在ClassVisitor中遇到辅助Visitor,例如visitMethod时,进行深入遍历
  5. 循环整个流程直到访问结束

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,通过这些辅助访问者来进一步深入访问更加精细的事件。

类/接口说明
AnnotationVisitor定义在解析注解时会触发的一系列的事件,解析到一个基本值类型的注解、enum值类型的注解、Array值类型的注解、注解值类型的注解时,会调用对应的方法
FieldVisitor定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等
MethodVisitor定义在解析方法时触发的事件,如方法上的注解、属性、代码等

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

常用的Visitor回调事件如下所示。

方法名说明
visit访问class的头部信息时,version为class版本(编译版本),access为访问修饰符,name为类名称,signature为class的签名,可能是null,superName为超类名称,interfaces为接口的名称
visitAnnotation访问class的注解信息时,descriptor为签名描述信息,visible为是否运行时可见
visitAttribute访问该类的属性
visitInnerClass访问class中内部类的信息,而且这个内部类不一定是被访问类的成员(有可能是一段方法中的匿名内部类或者声明在一个方法中的类等等)。name为内部类的名称,outerName为内部类所在类的名称,innerName为内部类的名称
visitOuterClass访问该类的外部类,仅当类具有封闭类时,才必须调用此方法。owner为拥有该类的class名称,name为包含该类的方法的名称,如果该类未包含在其封闭类的方法中,则返回null,descriptor为签名描述信息
visitEnd结束访问class时调用
visitField访问class中字段的信息,返回一个FieldVisitor用于操作字段相关的信息,access为访问修饰符,name为类名称,signature为class的签名,可能是null,descriptor为描述信息
visitMethod访问class中方法的信息,返回一个MethodVisitor用于操作字段相关的信息,access为访问修饰符,name为方法名称,signature为方法的签名,可能是null,descriptor为描述信息,exceptions为异常
visitModule访问对应的模块
visitTypeAnnotation访问类的签名的注解

FieldVisitor

FieldVisitor的调用流程如下:

visitAnnotation —— visitTypeAnnotation —— visitAttribute —— visitEnd

  • visitAnnotation:访问Field的注解
  • visitTypeAnnotation:访问Field的类型上的注解
  • visitEnd:Field访问完成后调用该方法

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类似,但是作用不同。

部分方法说明
ClassWriter(final int flags)构造ClassWriter对象,flag取值为0、1、2(0时表示需要手动计算最大操作数栈、局部变量表、桢变化;ClassWriter.COMPUTE_MAXS表示自动计算局部变量表和操作数栈,但是必须要调用visitMaxs,方法参数会被忽略。桢变化需要手动计算ClassWriter.COMPUTE_FRAMES表示全自动计算,但是必须要调用visitMaxs,方法参数会被忽略。但ClassWriter.COMPUTE_MAXS比0慢10%,比COMPUTE_FRAMES慢一倍。)
visit(final int version, final int access,final String name,final String signature,final String superName,final String[] interfaces)构造class文件的头部信息,version为指定的JDK版本(取值为Opcodes定义的常量),access为类的修饰符(同version),name为类的名称,signature与泛型相关的,若传入null则表示该字段不是泛型的签名,superName为要继承父类的全限定名,Interfaces为要实现的接口全限定名
visitField(final int access,final String name,final String descriptor,final String signature,final Object value)构造class文件的成员属性,name为成员属性名,descriptor为属性的类型签名,value为属性的值,只适用于静态字段,若当前要生成的字段不是静态的则传入null
visitMethod(final int access,final String name,final String descriptor,final String signature,final String[] exceptions)构造class文件的方法签名并返回都构造方法体的对象(即方法的修饰符、方法名、返回值及全限定的参数)
visitAnnotation(final String descriptor, final boolean visible)构造class 的注解对象,descriptor为注解的描述名,visible为运行时是否可见
toByteArray()返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件

与visitAnnotation、visitField、visitMethod类似,下面的这些类,封装了更精细的操作节点。

类/接口说明
AnnotationWriter实现了AnnotationVisitor,用于创建注解相关字节码指令来创建注解部分字节码
FieldWriter实现了FieldVisitor,用于创建字段相关字节码
MethodWriter实现了MethodVisitor,用于创建方法相关字节码
SignatureWriter实现了SignatureVisitor,用于创建泛型相关字节码
AnnotationWriter实现了AnnotationVisitor,用于创建注解相关字节码

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数据,允许访问者获取全部本地变量类型与当前堆栈位置的信息,这会大大降低性能,不过建议使用这个标志

一般使用步骤

上面都是ASM的基本概念,由于ASM使用的是访问者模式,这种方式在平时的开发中使用的比较少,所以理解起来有一定的困难,但只有对这些概念理解清楚,才能更好的使用ASM。

ASM对代码的插桩,通常会放在Transform中,在遍历文件的时候,对指定的Class文件做修改。

一个标准的ASM使用代码如下所示。

for (file in files) {
    var inputStream: FileInputStream? = null
    var outputStream: FileOutputStream? = null
    try {
        val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
        // 通过ClassWriter构造ClassVisitor
        val classVisitor: ClassVisitor = CustomClassVisitor(classWriter)
        inputStream = FileInputStream(file)
        // 通过InputStream构造ClassReader
        val classReader = ClassReader(inputStream)
        // 通过accept触发调用ClassVisitor接口的各个方法
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
        // 将修改的字节码通过byte数组的方式返回
        val bytes = classWriter.toByteArray()
        // 将修改的代码重写回文件
        outputStream = FileOutputStream(file.path)
        outputStream.write(bytes)
        outputStream.flush()
    } catch (throwable: Throwable) {
        throwable.printStackTrace()
    } finally {
        closeQuietly(inputStream)
        closeQuietly(outputStream)
    }
}

这和我们文章开始列出的使用步骤是一致的,ASM通过访问者模式,将具体的访问逻辑放在了CustomClassVisitor中,实现了流程和逻辑的分离。

在CustomClassVisitor中,可以进一步利用ASM提供的回调,来进行字节码的筛选和改造。

如果你不需要对ClassVisitor做深入的自定义,那么在Transform中,可以用下面的代码快速使用。

try {
    val inputStream = Files.asByteSource(file).openBufferedStream()
    val classReader = ClassReader(inputStream)
    val classVisitor: ClassVisitor = object : ClassVisitor(Opcodes.ASM9) {
        override fun visitSource(source: String?, debug: String?) {
            // TODO
        }
    }
    classReader.accept(classVisitor, ClassReader.SKIP_CODE)
} catch (e: Exception) {
    log("${e.message}")
} finally {
    closeQuietly(inputStream)
}

ASM的使用其实比较简单,难点在于字节码的改造,所以,后面一篇文章,我们将分析ASM的各种使用场景。

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存