asm是一款字节码操作框架,许多在java代码中难以实现的功能,借助字节码修改技术能够轻松实现,比如app中进行隐私接口调用检测,如果在java代码中进行hook不仅实现困难,其通用性也差,而在编译是修改字节码则可以轻松实现。除此之前,在性能监控时也可以通过修改字节码实现各种监控,因此学习如何使用asm是非常有必要的。这一系列文章是学习asm过程中所做的总结,希望能够对大家有所帮助。
class文件格式
class结构
class文件包含如下几个部分:
- 一部分主要用于描述类修饰符(如public、private)、类名、父类名、实现的接口以及类注解等信息
- 一部分声明类中的所有字段,每个字段都包含了详细的类型、修饰符、名称注解等信息
- 最后一部分用于描述类的方法,包含构造和类的所有方法,除了方法参数、返回值相关信息外,还包含编译之后的字节码指令
详细信息如下图所示:
类型描述符
java中的类型与字节码中类型对应关系如下:
方法描述符
方法描述符以左括号开始,其后是方法参数对应的类型描述符,接着是右括号,最后加载返回值的描述符,也就是说方法描述括号中表示参数类型,而括号外是返回值类型,可参考如下示例:
Class接口和相关组件
ASM利用ClassVisitor
来生成和转换字节码,其结构如下
ClassVisitor
的方法分为两类:一类较简单,这些方法没有返回值,其参数就代表具体的字节码内容,如visit方法,从参数就能知道类名、父类和实现的接口等信息;另一类则比较复杂,通常返回新的Visitor,如visitAnnotation
、visitField
和visitMethod
,通过递归调用这些方法所返回的子Visitor来访问具体内容,比如ClassVisitor的visitField方法会返回FieldVisitor,其结构如下图所示:
ASM提供如下三种组件进行字节码的生成和转换:
ClassReader
: 解析字节码,可以将其看做事件生产者。ClassWriter
: 生成字节码,可将其看做事件消费者。ClassVisitor
:作为另一个ClassVisitor的代理,可重写任何方法,从而对字节码进行修改,可以看做事件过滤器。
ClassVisitor
的访问顺序如下图所示:
按字面顺序访问,?表示如果没有则不访问,*表示访问0次或多次
解析class
考虑实现一个Class打印器,将类信息打印出来,实现如下:
public class ClassPrinter extends ClassVisitor {
protected ClassPrinter() {
super(Opcodes.ASM9);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
String accessStr = getAccessStr(access);
String abstractStr = "";
String finalStr = "";
String interfaceStr = "";
if ((access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL) {
finalStr = "final";
} else if ((access & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE) {
interfaceStr = "interface";
} else if ((access & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT) {
abstractStr = "abstract";
}
StringBuilder content = new StringBuilder();
if (accessStr.length() > 0) {
content.append(accessStr).append(" ");
}
if (finalStr.length() > 0) {
content.append(finalStr).append(" ");
}
if (interfaceStr.length() > 0) {
content.append(interfaceStr).append(" ");
} else if (abstractStr.length() > 0) {
content.append(abstractStr).append(" class ");
} else {
content.append("class ");
}
content.append(name).append(" extends ").append(superName);
if (interfaces != null && interfaces.length > 0) {
content.append(" implements ");
for (int i = 0; i < interfaces.length; i++) {
String interfaceName = interfaces[i];
content.append(interfaceName);
if (i != interfaces.length - 1) {
content.append(", ");
}
}
}
content.append("{");
System.out.println(content);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
String accessStr = getAccessStr(access);
StringBuilder content = new StringBuilder(" ");
if (accessStr.length() > 0) {
content.append(accessStr).append(" ");
}
content.append(descriptor)
.append(" ")
.append(name)
.append("");
System.out.println(content);
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String accessStr = getAccessStr(access);
StringBuilder content = new StringBuilder(" ");
if (accessStr.length() > 0) {
content.append(accessStr).append(" ");
}
content.append(name)
.append(descriptor)
.append("");
System.out.println(content);
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
@Override
public void visitEnd() {
System.out.println("}");
super.visitEnd();
}
private String getAccessStr(int access) {
String accessStr = "";
if ((access & Opcodes.ACC_PUBLIC) == Opcodes.ACC_PUBLIC) {
accessStr = "public";
} else if ((access & Opcodes.ACC_PRIVATE) == Opcodes.ACC_PRIVATE) {
accessStr = "private";
} else if ((access & Opcodes.ACC_PROTECTED) == Opcodes.ACC_PROTECTED) {
accessStr = "protected";
}
return accessStr;
}
}
demo中定义自己ClassPrinter类,继承ClassVisitor并重写visit
、visitField
和visitMethod
三个放方法将类名、父类、实现接口、方法名及其描述符、字段名及其描述符打印出来,接下去要将其应用到ClassReader中:
ClassPrinter printer = new ClassPrinter();
ClassReader reader = new ClassReader("java.lang.String");
reader.accept(printer, 0);
第二行指定解析java的String类,接着通过ClassReader的accept方法接收ClassPrinter作为参数,也可以直接读取class文件将InputStream传入ClassReader中。
生成class
ClassWriter
也是一个ClassVisitor,我们可以手动调用对应的放visiti方法生成字节码文件,如下
ClassWriter writer = new ClassWriter(0); // 1
writer.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "com/example/test/TestData", null, "java/lang/Object", null);// 2
writer.visitField(Opcodes.ACC_PUBLIC, "valInt", Type.INT_TYPE.getDescriptor(), null, 0);// 3
writer.visitField(Opcodes.ACC_PUBLIC, "valStr", Type.getDescriptor(String.class), null, null);// 4
writer.visitEnd();// 5
byte[] classBytes = writer.toByteArray();// 6
writeClass(classBytes, "test\\TestData.class");// 7
以上代码会生成名为TestData的java类,结构如下:
package com.example.test;
public class TestData {
public int valInt;
public String valStr;
}
第二行通过visit方法声明类的作用域为public,类名为com.example.test.TestData
,继承Object对象。
第三行声明了一个名为valIn的int字段,可见性为public。
第四行声明了一个名为valStr的String字段,可见性为public。
最后调用visitEnd结束访问,在通过toByteArray获取字节码数据。
修改class
可以重写ClassVisitor对应的visit方法实现修改类的字节码的功能,下面的demo展示了如何修改类字段的默认值:
public class FiledValModifyTransform extends ClassVisitor {
private boolean shouldTransform;
private String classname;
private String filedName;
private Object val;
protected FiledValModifyTransform(ClassVisitor classVisitor, String classname, String filed, Object val) {
super(Opcodes.ASM9, classVisitor);
this.classname = classname;
this.filedName = filed;
this.val = val;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
shouldTransform = classname.equals(name);
System.out.println("should transform class " + name + " " + shouldTransform);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (shouldTransform && filedName.equals(name)) {
return super.visitField(access, name, descriptor, signature, val);
}
return super.visitField(access, name, descriptor, signature, value);
}
}
核心就在于重写visitField
方法,将参数中的value替换成从构造器中传入的值,即通过super.visitField(access, name, descriptor, signature, val)
实现字段默认值的替换。
应用代码如下:
private byte[] transformFiled() throws IOException {
ClassWriter writer = new ClassWriter(0);
FiledValModifyTransform transform = new FiledValModifyTransform(
writer,
"com/example/test/CallRecordUtilsTest",
"FOREGROUND_ID_AutoSignin",
6789);
ClassReader reader = new ClassReader("com.example.test.CallRecordUtilsTest");
reader.accept(transform, 0);
return writer.toByteArray();
}
通过FiledValModifyTransform类修改CallRecordUtilsTest中的FOREGROUND_ID_AutoSignin字段,其原始定义如下:
public class CallRecordUtilsTest {
public final static int FOREGROUND_ID_AutoSignin = 4455;
...
}
修改后的class如下:
public class CallRecordUtilsTest {
public static final int FOREGROUND_ID_AutoSignin = 6789;
...
}
从这个例子中可以看出,转换数据流向如下图所示:
即Reader将class数据读取到Adapter中,经由adapter处理后把数据传递给Writer进行处理,在上面的demo中,adapter就是FiledValModifyTransform,其详细时序图如下:
移除类成员
在visitField中将visitField事件拦截下来并返回null来以成员,如下:
public class FieldRemoveTransform extends ClassVisitor {
private boolean shouldTransform;
private String classname;
private String filedName;
protected FieldRemoveTransform(ClassVisitor classVisitor, String classname, String filed) {
super(Opcodes.ASM9, classVisitor);
this.classname = classname;
this.filedName = filed;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
shouldTransform = classname.equals(name);
System.out.println("should transform class " + name + " " + shouldTransform);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (shouldTransform && filedName.equals(name)) {
return null;
}
return super.visitField(access, name, descriptor, signature, value);
}
}
这是一个删除CallRecordUtilsTest中的FOREGROUND_ID_AutoSignin字段的例子:
public void testRemoveField() throws IOException {
ClassWriter writer = new ClassWriter(0);
FieldRemoveTransform transform = new FieldRemoveTransform(writer, "com/example/test/CallRecordUtilsTest", "FOREGROUND_ID_AutoSignin");
ClassReader reader = new ClassReader("com.example.test.CallRecordUtilsTest");
reader.accept(transform, 0);
byte[] classBytes = writer.toByteArray();
writeClass(classBytes, "test\\CallRecordUtilsTest_r.class");
}
添加类成员
由于ClassVisitor通过visitField定义字段,因此我们可以手动调用此方法并设置正确的参数来给类新增字段,不过应该在哪里调用呢?
考虑ClassVisitor中方法的访问顺序限制(前面介绍),所以我们不能在visit
、visitSource
、visitOuterClass
、visitAnnotation
和visitAttribute
中调用visitField来添加字段,所以只能在visitInnerClass, visitField, visitMethod 或visitEnd中调用,但除visitEnd外,其他三个方法可能调用0此或多次,所以在visitEnd中调用是最合适的,如下:
public class FiledAddTransform extends ClassVisitor {
private boolean shouldTransform;
private String classname;
private String fieldName;
private String fDesc;
private int acces;
protected FiledAddTransform(ClassVisitor classVisitor, String classname, int access, String filed, String fDesc) {
super(Opcodes.ASM9, classVisitor);
this.classname = classname;
this.fieldName = filed;
this.fDesc = fDesc;
this.acces = access;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
shouldTransform = classname.equals(name);
System.out.println("should transform class " + name + " " + shouldTransform);
}
@Override
public void visitEnd() {
if (shouldTransform && cv != null) {
FieldVisitor fv = cv.visitField(acces, fieldName, fDesc, null, null);
if (fv != null) {
fv.visitEnd();
}
}
super.visitEnd();
}
}
public void testAddField() throws IOException {
ClassWriter writer = new ClassWriter(0);
FiledAddTransform transform = new FiledAddTransform(
writer,
"com/example/test/CallRecordUtilsTest",
Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
"testField",
Type.getDescriptor(String.class));
ClassReader reader = new ClassReader("com.example.test.CallRecordUtilsTest");
reader.accept(transform, 0);
byte[] classBytes = writer.toByteArray();
writeClass(classBytes, "test\\CallRecordUtilsTest_a.class");
}