ClassVisitor访问顺序 & 部分函数作用
在尝试对字节码文件进行操控之前,首先要了解ClassVisitor有固定的调用顺序
scss
visit visitSource? visitOuterClass?
(visitAnnotation|visitAttribute)*
(visitInnerClass|visitField|visitMethod)*
visitEnd
每个方法都对应字节码文件中的一部分。visit方法是最先被调用的,可以获取类型声明信息,如可访问性,父类和实现的接口等。之后visitSource和visitOuterClass方法至多调用一次,最后调用visitEnd方法结束调用链。中间则会调用任意次数和任意顺序的其他方法,其中visitField和visitMethod会返回FieldVisitor和MethodVisitor对象,从而帮助我们实现对字段或方法的修改。
ClassReader
作为整个链路的起点,通过accept方法接收其他ClassVisitor实例,从而传递字节码文件的数据。对于read -> visitor -> write结构,可以通过如下伪代码实现
java
public void func1() throws IOException {
String className = "";
ClassWriter cw = new ClassWriter();
ClassVisitor visitor = new ClassVisitor(cw);
ClassReader reader = new ClassReader(className);
reader.accept(visitor, 0);
}
结合前文提及的ClassVisitor调用顺序,reader首先调用visit方法,随后调用visitor的visit方法,最后是write的visit方法。依照调用顺序,接下来就是reader的visitSource,然后是visit的visitSource,依次类推,直到write的visitEnd方法调用,完成整个调用链。 在实际的使用过程种,可能需要用到不止一个ClassVisitor,如以下代码示例,实现cr -> classVisitor2 -> classVisitor1 -> cw 链路
java
public void func2() throws IOException {
String className;
ClassReader cr = new ClassReader(className);
ClassWriter cw = new ClassWriter();
ClassVisitor1 classVisitor1 = new ClassVisitor1(cw);
ClassVisitor2 classVisitor2 = new ClassVisitor2(classVisitor1);
cr.accept(classVisitor2);
// cr -> classVisitor2 -> classVisitor1 -> cw
}
ClassWriter
从ClassWriter开始就可以实现一些好玩的东西了,例如生成一个接口文件
java
public class Generator {
private final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
public byte[] generaInterface(String name, String superName) {
// write class file
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,
name, null, superName ,null);
// write field
Date time = new Date(System.currentTimeMillis());
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_STATIC+Opcodes.ACC_FINAL+Opcodes.ACC_ABSTRACT, "time", Type.getType(String.class).getDescriptor(), null, time.toString()).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_STATIC+Opcodes.ACC_FINAL+Opcodes.ACC_ABSTRACT, "sxm", Type.INT_TYPE.getDescriptor(), null, 520).visitEnd();
// write method
cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "getSxm", "()I", null, null).visitEnd();
return cw.toByteArray();
}
}
可以看到,虽然ClassReader和ClassWriter调用过程一致,但是相同的方法在不同的类中有着不同的作用。visitField在reader中是读取,而在writer中则是根据参数写入。运行结果如下
java
public interface CuteGirl {
String time = "Sat Jul 29 17:52:37 CST 2023";
int sxm = 520;
int getSxm();
}
除了生成全新的文件,我们还可以对已有文件进行修改,例如为Iterable接口的每一个方法生成一个对应的字段。
java
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cnt = 0;
this.visitField(Opcodes.ACC_PUBLIC, "fieldnameBuild_sxm", Type.INT_TYPE.getDescriptor(), null, -1);
super.visit(version, access, this.name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
this.visitField(Opcodes.ACC_PRIVATE, fieldNameBuild(name), Type.INT_TYPE.getDescriptor(), null, 0);
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
这里需要注意visit和visitMethod的调用次数,由于visit方法只会调用一次,所以对应也只会生成一个字段;而visitMethod每一方法调用一次,因此会生成多个字段。此外,visitMethod在使用还需要注意,类构造器也会visitMethod方法读取,如果不想访问构造器,要记得exclude哦。
java
public interface IterableCount<T> {
int fieldnameBuild_sxm;
private int iterator_count$1;
private int forEach_count$2;
private int spliterator_count$3;
...
}
ClassVisitor
有了ClassVisitor就可以对现成的字节码文件进行二次加工了。举个栗子,如果我们不想要IterableCount中所有的private字段,但是保留public的字段。
java
public class PrivateFieldRemove extends ClassVisitor {
public PrivateFieldRemove(int api , ClassVisitor visitor) {
super(Opcodes.ASM4, visitor);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (access == Opcodes.ACC_PRIVATE)
return null;
return super.visitField(access, name, descriptor, signature, value);
}
}
根据前文提到过的调用顺序,即使不是ClassVisitor调用visitField方法,但是最终也会经过该ClassVisitor的visitField方法再交由ClassWriter处理。所以这个ClassVisitor可以过滤所有想要写入private字段的请求,但是经过该ClassVisitor再调用visitField去写入privae字段则不会被拦截。
MethodVisitor
除了生成字段,ASM也支持我们改动方法的代码。在前文中提及过,visitMethod方法返回MethodVisitor对象,ASM通过MethodVisitor来完成对具体代码的复杂操作。同样,MethodVisitor也有其调用顺序
scss
visitAnnotationDefault?
(visitAnnotation|visitParameterAnnotation|visitAttribute)*
(visitCode
(visitTryCatchBlock|visitLabel|visitFrame|visitXxxInsn|visitLocalVariable|visitLineNumber)*
visitMaxs)?
visitEnd
至于MethodVisitor怎么用,再举个栗子。比如现在我们想统计方法执行的耗时,这段代码基本是固定的
java
public void func1() {
time = time - System.currentTimeMillis();
...
time = time + System.currentTimeMillis();
System.out.println("method used time: " + time);
}
可以通过MethodVisitor将这段代码的字节码插入需要统计耗时的方法中。首先开始访问代码时,加入第一句。其中owner就是需要改动方法所在类的类名,timerName就是计数器。注意这里再读取时已经指定了计数器的类型为long,如果类型不对会出现字段不存在的异常。
java
@Override
public void visitCode() {
super.visitCode();
// time = time - System.currentTimeMillis();
super.mv.visitVarInsn(Opcodes.ALOAD, 0);
super.mv.visitVarInsn(Opcodes.ALOAD, 0);
super.mv.visitFieldInsn(Opcodes.GETFIELD, owner, timerName, "J");
super.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
super.mv.visitInsn(Opcodes.LSUB);
super.mv.visitFieldInsn(Opcodes.PUTFIELD, owner, timerName, "J");
}
根据调用顺序可知,visitCode只会调用一次,不会产生重复。然后我们需要在函数返回前补全剩余代码,考虑到函数正常返回和异常退出
java
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
// time = time + System.currentTimeMillis();
super.mv.visitVarInsn(Opcodes.ALOAD, 0);
super.mv.visitVarInsn(Opcodes.ALOAD, 0);
super.mv.visitFieldInsn(Opcodes.GETFIELD, owner, timerName, "J");
super.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
super.mv.visitInsn(Opcodes.LADD);
super.mv.visitFieldInsn(Opcodes.PUTFIELD, owner, timerName, "J");
// System.out.println("method used time: " + time);
super.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
super.mv.visitInsn(Opcodes.DUP);
super.mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.mv.visitLdcInsn("method " + methodName + " used time: ");
super.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.mv.visitVarInsn(Opcodes.ALOAD, 0);
super.mv.visitFieldInsn(Opcodes.GETFIELD, owner, timerName, "J");
super.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
super.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
只有MethodVisitor还不够,还需要把配合ClassVisitor一起使用
java
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!isInterface && !name.startsWith("<")) {
String timerName = fieldNameBuild(name);
this.visitField(Opcodes.ACC_PRIVATE, timerName, "J", null, 0);
AddTimerMethodAdapter addTimerMethodAdapter = new AddTimerMethodAdapter(methodVisitor);
addTimerMethodAdapter.setOwner(this.owner);
addTimerMethodAdapter.setTimerName(timerName);
addTimerMethodAdapter.setMethodName(name);
methodVisitor = addTimerMethodAdapter;
}
return methodVisitor;
}
最后生成的代码为
java
private long getNum_count$1;
...
public int getNum() {
this.getNum_count$1 -= System.currentTimeMillis();
int var10000 = super.num;
this.getNum_count$1 += System.currentTimeMillis();
System.out.println("method getNum used time: " + this.getNum_count$1);
return var10000;
}
如何使用生成的代码
最后就是怎么使用生成的java类。简单点,可以使用ClassLoader加载
java
public class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
不过在使用自己生成的类还会遇到一个问题,因为生成的类并没有实体,而是直接加载到JVM中,所以编译期找不到这个类导致报错,因此生成类最好继承现有类,或者实现已有接口
java
// load to jvm
byte[] bytes = cw.toByteArray();
MyClassLoader myClassLoader = new MyClassLoader();
Class defineClass = myClassLoader.defineClass(name, bytes);
Object obj = defineClass.newInstance();
if (obj instanceof Sxm) {
Sxm timer = (Sxm) obj;
timer.addNum(10);
timer.addNum(10);
timer.addNum(10);
timer.addNum(10);
timer.getNum();
}
ASM自带工具可以检查我们自己改动的类是否有问题CheckClassAdapter
如何使全局生效 ClassFileTransformer & 自定义classloader