从精准化测试看ASM在Android中的强势插入-ASM
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从精准化测试看ASM在Android中的强势插入-ASM相关的知识,希望对你有一定的参考价值。
好文推荐:
作者:xuyisheng
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,通过这些辅助访问者来进一步深入访问更加精细的事件。
通过ClassVisitor中的各种事件的回调,以及辅助Visitor的精细化事件,就可以让使用者在不用关系字节码内部偏移而方便的修改字节码,ClassVisitor会管理这些过程,使用者通过覆写对应的Visitor即可实现对字节码的修改。
常用的Visitor回调事件如下所示。
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类似,但是作用不同。
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的各种使用场景。
以上是关于从精准化测试看ASM在Android中的强势插入-ASM的主要内容,如果未能解决你的问题,请参考以下文章
从精准化测试看ASM在Android中的强势插入-读懂diff
从精准化测试看ASM在Android中的强势插入-JaCoco初探