likes
comments
collection
share

ASM学习指导03:如何对字节码下手

作者站长头像
站长
· 阅读数 20

ClassVisitor访问顺序 & 部分函数作用

在尝试对字节码文件进行操控之前,首先要了解ClassVisitor有固定的调用顺序

visit visitSource? visitOuterClass? 
(visitAnnotation|visitAttribute)*
(visitInnerClass|visitField|visitMethod)*
visitEnd

每个方法都对应字节码文件中的一部分。visit方法是最先被调用的,可以获取类型声明信息,如可访问性,父类和实现的接口等。之后visitSource和visitOuterClass方法至多调用一次,最后调用visitEnd方法结束调用链。中间则会调用任意次数和任意顺序的其他方法,其中visitField和visitMethod会返回FieldVisitor和MethodVisitor对象,从而帮助我们实现对字段或方法的修改。

ClassReader

作为整个链路的起点,通过accept方法接收其他ClassVisitor实例,从而传递字节码文件的数据。对于read -> visitor -> write结构,可以通过如下伪代码实现

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 链路

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开始就可以实现一些好玩的东西了,例如生成一个接口文件

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中则是根据参数写入。运行结果如下

public interface CuteGirl {
    String time = "Sat Jul 29 17:52:37 CST 2023";
    int sxm = 520;

    int getSxm();
}

除了生成全新的文件,我们还可以对已有文件进行修改,例如为Iterable接口的每一个方法生成一个对应的字段。

@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哦。

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的字段。

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也有其调用顺序

visitAnnotationDefault?
(visitAnnotation|visitParameterAnnotation|visitAttribute)*
(visitCode
	(visitTryCatchBlock|visitLabel|visitFrame|visitXxxInsn|visitLocalVariable|visitLineNumber)*
visitMaxs)?
visitEnd

至于MethodVisitor怎么用,再举个栗子。比如现在我们想统计方法执行的耗时,这段代码基本是固定的

public void func1() {
    time = time - System.currentTimeMillis();
    ...
    time = time + System.currentTimeMillis();
    System.out.println("method used time: " + time);
}

可以通过MethodVisitor将这段代码的字节码插入需要统计耗时的方法中。首先开始访问代码时,加入第一句。其中owner就是需要改动方法所在类的类名,timerName就是计数器。注意这里再读取时已经指定了计数器的类型为long,如果类型不对会出现字段不存在的异常。

@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只会调用一次,不会产生重复。然后我们需要在函数返回前补全剩余代码,考虑到函数正常返回和异常退出

@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一起使用

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;
}

最后生成的代码为

    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加载

public class MyClassLoader extends ClassLoader {
    public Class defineClass(String name, byte[] b) {
        return defineClass(name, b, 0, b.length);
    }
}

不过在使用自己生成的类还会遇到一个问题,因为生成的类并没有实体,而是直接加载到JVM中,所以编译期找不到这个类导致报错,因此生成类最好继承现有类,或者实现已有接口

// 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