从ASM入门字节码增强
ASM作为Java字节码层次的处理框架,能够直接对字节码进行操作,使用ASM能够轻松完成代码注入等字节码增强的相关操作。
基础使用
这里使用一个基础调用展示ASM的使用
val cr = ClassReader(inputStream)val cw = ClassWriter(cr, 0)val cv = MyClassVisitor(Opcodes.ASM6, cw)cr.accept(cv, ClassReader.EXPAND_FRAMES)inputStream.close()val byteArray = cw.toByteArray()
在这里用到了ClassReader,ClassWriter以及一个自定义的ClassVIsitor,首先通过ClassReader读入输入流,之后定义ClassWriter,初始化ClassVisitor,再通过accept开启整个流程,最后通过ClassWriter获取修改过后的输出流。
自定义ClassVisitor如下,这里在Activity的onCreate方法中插入了方法System.out.println(System.currentTimeMills());
class MyClassVisitor(api: Int, cv: ClassVisitor) : ClassVisitor(api, cv) { override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor? { val mv = super.visitMethod(access, name, desc, signature, exceptions) if (name == "onCreate") { mv.visitMaxs(0,0) return BingyanMethodVisitor(api, mv) } return mv } private inner class BingyanMethodVisitor(api: Int, mv: MethodVisitor) : MethodVisitor(api, mv) { override fun visitCode() { super.visitCode() mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false); } }}
基础准备(字节码)
Class文件结构
ASM作为操作字节码的工具,那么首先会介绍相关字节码的简单结构。用一张表格来表示class文件的结构,class文件中分成了以下十个部分:
字节码结构 | ||
名称 | 大小 | 描述 |
魔数 | 4字节 | 0xcafebabe |
版本号 | 4字节 | 次版本号+主版本号 |
常量池 | 2字节+N | 计数器+数据区 |
访问标志 | 2字节 | 类or接口,以及修饰符 |
当前类索引 | 2字节 | 当前类全限定名(常量池索引) |
父类索引 | 2字节 | 父类的全限定名(常量池索引) |
接口索引 | 2字节+N | 计数器+常量池索引 |
字段表 | 2字节+N | 计数器+类和接口中声明的变量字段的详细信息 |
方法表 | 2字节+N | 计数器+方法信息 |
附加属性 | 类或接口所定义属性的基本信息 |
1、魔数
前四个字节是魔数,固定值为:0xCAFEBABE,用来标识一个文件是class文件。
2、版本号
四个字节标示版本号,其中两个字节为次版本号,后两个字节为主版本号。例如 “00 00 00 34” 转化为16进制后分别为 0 和 52。对应版本1.8.0。
3、常量池
计数器
在常量池中前两位是计数器,用来标示在当前class文件中有多少个常量。将16进制转化成10进制之后-1。
数据区
由指定数量的cp_info结构组成,每一个cp_info有两部分或三部分组成。储存字面量与符号引用,字面量即代码中定义的文本字符串等,而符号引用包括类和接口的全限定名,字段的名称和描述符号,方法的名称和描述符。
CONSTANT_Utf8_info | tag (1 byte) | length (2 byte) | |
1 | 字符串长度 | 长度为length的字符串 | |
CONSTANT_Integer_info | tag (1 byte) | 4 byte | |
3 | 高位在前存储的int值 | ||
CONSTANT_Float_info | tag (1 byte) | 4 byte | |
4 | 高位在前存储的float值 | ||
CONSTANT_Long_info | tag (1 byte) | 8 byte | |
5 | 高位在前存储的long值 | ||
CONSTANT_Double_info | tag (1 byte) | 8 byte | |
6 | 高位在前存储的double值 | ||
CONSTANT_Class_info | tag (1 byte) | index (2 byte) | |
7 | 全限定名常量项索引 | ||
CONSTANT_String_info | tag (1 byte) | index (2 byte) | |
8 | 字符串字面量索引 | ||
CONSTANT_Fieldref_info | tag (1 byte) | index (2 byte) | index (2 byte) |
9 | 声明字段的类或接口描述符索引 | 字段描述符索引 | |
CONSTANT_Methodref_info | tag (1 byte) | index (2 byte) | index (2 byte) |
10 | 声明方法的类描述符索引 | 名称及类描述符索引 | |
CONSTANT_InterfaceMethodref_info | tag (1 byte) | index (2 byte) | index (2 byte) |
11 | 声明方法的接口描述符索引 | 名称及类描述符索引 | |
CONSTANT_NameAndType_info | tag (1 byte) | index (2 byte) | index (2 byte) |
12 | 字段和方法名称常量索引 | 字段和方法名称描述符常量索引 | |
CONSTANT_MethodHandle_info | tag (1 byte) | reference_kind (1 byte) | reference_kind (2 byte) |
15 | 1~9 方法句柄类型 | 常量池索引 | |
CONSTANT_MethodType_info | tag (1 byte) | descripto_index (2 byte) | |
16 | 常量池索引,方法描述符 | ||
CONSTANT_InvokeDyanmic_info | tag (1 byte) | (2 byte) | (2 byte) |
18 |
4、访问标志
两个字节标示是类还是接口,以及pulic,abstract,final等。
5、类名
两个字节,当前类的全限定名在常量池中的索引
6、父类名称
两个字节,父类的全限定名在常量池中的索引
7、接口信息
计数器:两个字节,描述接口数量
n个字节,接口名称的字符串常量的索引
8、字段表
描述类和接口中声明的变量
2 bytes | ||||
计数器 | ||||
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 字段索引 | 描述符索引 | 属性个数 | 属性列表 |
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 字段索引 | 描述符索引 | 属性个数 | 属性列表 |
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 字段索引 | 描述符索引 | 属性个数 | 属性列表 |
9、方法表
描述方法
2 bytes | ||||
计数器 | ||||
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 方法名索引 | 描述符索引 | 属性个数 | 属性列表 |
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 方法名索引 | 描述符索引 | 属性个数 | 属性列表 |
2 bytes | 2 bytes | 2 bytes | 2 bytes | n bytes |
权限修饰符 | 方法名索引 | 描述符索引 | 属性个数 | 属性列表 |
方法的属性由三部分组成
Code:JVM对应的操作指令码
LineNumberTable:对应java文件中的行号
LocalVariableTable:本地变量表
10、附加属性表
文件中类或接口所定义属性的基本信息。
操作码
java文件编译后会生成让jvm执行的十六进制操作码,这里不列举具体操作码对应的十六进制表示和含义,仅用一个例子做为说明:
public final void add(int, int); descriptor: (II)V flags: ACC_PUBLIC, ACC_FINAL Code: stack=2, locals=5, args_size=3 0: iload_1 //变量1入栈 1: iload_2 //变量2入栈 2: iadd //相加 3: istore_3 //将值存入变量3 4: iconst_0 //this入栈 5: istore 4 // 7: getstatic #35 //调用索引35,这里是System.out 10: iload_3 //变量3 11: invokevirtual #46 //调用System.out的索引为46的方法,参数为变量3 14: return LineNumberTable: line 39: 0 line 40: 4 line 41: 14 LocalVariableTable: Start Length Slot Name Signature 4 11 3 c I 0 15 0 this Lcom/example/flint/MainActivity; 0 15 1 a I 0 15 2 b I
ASM设计流程
通过ASM的调用可以看出ASM整体流程,ClassReader用来读入字节码流并调用accept开启整个流程,作为典型的访问者设计模式去进一步调用ClassVisitor中的相应的方法。在ClassVisitor中则进一步分成了annotationvisitor,fieldvisitor,methodvisitor。而ClassWriter是ClassVisitor的子类,在调用ClassVisitor的相应的方法时,同时也会调用ClassWriter中的方法,在ClassWriter中,保存了对输入流进行修改过后的字节流,在整体流程结束后,通过toByteArray获取修改过后的输入流。
这里用简单的流程图说明:
ASM类图
ClassReader
ClassReader可以看出作用就是用来读取一个class文件,并且调用accept加载ClassVisitor并开始整个流程。
public ClassReader(final byte[] b, final int off, final int len) { this.b = b; // checks the class version if (readShort(off + 6) > Opcodes.V9) { throw new IllegalArgumentException(); } // parses the constant pool items = new int[readUnsignedShort(off + 8)]; int n = items.length; strings = new String[n]; int max = 0; int index = off + 10; for (int i = 1; i < n; ++i) { items[i] = index + 1; int size; switch (b[index]) { case ClassWriter.FIELD: case ClassWriter.METH: ...... case ClassWriter.HANDLE: size = 4; break; default: size = 3; break; } index += size; } maxStringLength = max; // the class header information starts just after the constant pool header = index;}
在ClassReader的构造方法中,首先检查版本号,之后创建items数组用来存储输入流(即字节码)中的信息,在这里通过iteam保存每一个类型的index。
// visits the class declarationclassVisitor.visit(readInt(items[1] - 7), access, name, signature, superClass, interfaces);// visits the source and debug infoif ((flags & SKIP_DEBUG) == 0 && (sourceFile != null || sourceDebug != null)) { classVisitor.visitSource(sourceFile, sourceDebug);}// visits the module info and associated attributesif (module != 0) { readModule(classVisitor, context, module, moduleMainClass, packages);}// visits the outer classif (enclosingOwner != null) { classVisitor.visitOuterClass(enclosingOwner, enclosingName, enclosingDesc);}// visits the class annotations and type annotationsif (anns != 0) { for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) { v = readAnnotationValues(v + 2, c, true, classVisitor.visitAnnotation(readUTF8(v, c), true)); }}
accept方法太长了,这里只截取调用方法中的一小部分。在类初始化后,紧接着会调用accept函数并传入ClassVisitor,之后遵循Class文件的格式读取在构造函数中传入的二进制流,之后解析并通过visit方法将读取到的信息全部交给ClassVisitor处理。这里的这些visit方法可以在自定义ClassVisitor中重写。
ClassVisitor
ClassVisitor是一个抽象类,而在传入时需继承这个类并重写响应的visit方法。
public ClassVisitor(final int api, final ClassVisitor cv) { if (api Opcodes.ASM6) { throw new IllegalArgumentException(); } this.api = api; this.cv = cv;}public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (cv != null) { cv.visit(version, access, name, signature, superName, interfaces); }}
首先是ClassVisitor的构造方法,这里有一个ClassVisitor参数,也就是说在ClassVisitor内部还可以有另一个ClassVisitor。
ClassVisitor中的visit方法都是相似的,这里只看visit,会继续调用传入的ClassVisitor的visit方法。
而在实例中传入的是ClassWriter方法,那么毫无疑问ClassWriter方法也是ClassVisitor的一个子类。
ClassVisitor
ClassWriter继承了ClassVisitor,同时也重写了ClassVisitor中的相应的方法,但更多了对class内容的保存。
public ClassWriter(final ClassReader classReader, final int flags) { this(flags); classReader.copyPool(this); this.cr = classReader;}void copyPool(final ClassWriter classWriter) { char[] buf = new char[maxStringLength]; int ll = items.length; Item[] items2 = new Item[ll]; ······ int off = items[1] - 1; classWriter.pool.putByteArray(b, off, header - off); classWriter.items = items2; classWriter.threshold = (int) (0.75d * ll); classWriter.index = ll;}
在ClassWriter的初始化函数中多了一步,copyPool,在copyPool方法中可以看到ClassReader将读取的class信息加载到了ClassWriter之中,也就是说此时ClassWriter中也保存了一份class的数据。
@Overridepublic final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { return new MethodWriter(this, access, name, desc, signature, exceptions, compute);}
再看MethodVisit方法,当ClassVisitor中的visit方法被调用后,会继续调用ClassWriter中的visit方法,在这个方法中会返回一个Writer对象,而在重写的ClassVisitor中,获取了这个对象之后返回了新的继承了MethodVisitor的自定义内部类。(MethodVisitor是MethodWriter的父类)
private inner class BingyanMethodVisitor(api: Int, mv: MethodVisitor) : MethodVisitor(api, mv) { override fun visitCode() { super.visitCode() mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false); } }}
在这里重写了visitCode方法,在其中完成了字节码注入。这里的mv就是MethodWriter,在visitCode中调用了一系列MethodWriter的方法。
@Overridepublic void visitInsn(final int opcode) { lastCodeOffset = code.length; // adds the instruction to the bytecode of the method code.putByte(opcode); // update currentBlock // Label currentBlock = this.currentBlock; if (currentBlock != null) { if (compute == FRAMES || compute == INSERTED_FRAMES) { currentBlock.frame.execute(opcode, 0, null, null); } else { // updates current and max stack sizes int size = stackSize + Frame.SIZE[opcode]; if (size > maxStackSize) { maxStackSize = size; } stackSize = size; } // if opcode == ATHROW or xRETURN, ends current block (no successor) if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { noSuccessor(); } }}
以一个方法举例,在visitInsn中,将opcode保存在了MethodWriter之中。而对于ClassWriter,通过了链式完成了对MethodWriter的保存。
if (cw.firstMethod == null) { cw.firstMethod = this;} else { cw.lastMethod.mv = this; }cw.lastMethod = this;
在MethodWriter的构造函数中有这样一步,cw就是ClassWriter,这里的firstMethod就是ClassWriter创建的第一个MethodWriter,而之后的每一个MethodWriter都会保存在上一个MethodWriter.mv中,也就是以链表的形式保存了所有的MethodWriter。
最后看toByteArray
public byte[] toByteArray() { if (index > 0xFFFF) { throw new RuntimeException("Class file too large!"); } // computes the real size of the bytecode of this class int size = 24 + 2 * interfaceCount; ...... int nbMethods = 0; MethodWriter mb = firstMethod; while (mb != null) { ++nbMethods; size += mb.getSize(); mb = (MethodWriter) mb.mv; } int attributeCount = 0; ...... // allocates a byte vector of this size, in order to avoid unnecessary // arraycopy operations in the ByteVector.enlarge() method ByteVector out = new ByteVector(size); out.putInt(0xCAFEBABE).putInt(version); out.putShort(index).putByteArray(pool.data, 0, pool.length); int mask = Opcodes.ACC_DEPRECATED | ACC_SYNTHETIC_ATTRIBUTE | ((access & ACC_SYNTHETIC_ATTRIBUTE) / TO_ACC_SYNTHETIC); out.putShort(access & ~mask).putShort(name).putShort(superName); out.putShort(interfaceCount); for (int i = 0; i < interfaceCount; ++i) { out.putShort(interfaces[i]); } ...... out.putShort(nbMethods); mb = firstMethod; while (mb != null) { mb.put(out); mb = (MethodWriter) mb.mv; } out.putShort(attributeCount); ...... if (moduleWriter != null) { out.putShort(newUTF8("Module")); moduleWriter.put(out); moduleWriter.putAttributes(out); } ...... return out.data;}
删除了大部分代码只保留了Method相关的,在toByteArray中首先统计了方法数与所需要的空间大小,之后创建ByteVector以class格式填充数据并输出。而在Method中,通过了链式调用完成了Method数据的填充。
总结
字节码增强技术在性能检测及减少冗余方面都有广泛的应用,而ASM相较于其他方法具有高性能,小巧的特点,但同时因为直接在字节码层面操作,同时也要求掌握一定的字节码语法。
注:图片及资料来源于互联网