ASM Core Api学习系列1:Class的使用

asm是一款字节码操作框架,许多在java代码中难以实现的功能,借助字节码修改技术能够轻松实现,比如app中进行隐私接口调用检测,如果在java代码中进行hook不仅实现困难,其通用性也差,而在编译是修改字节码则可以轻松实现。除此之前,在性能监控时也可以通过修改字节码实现各种监控,因此学习如何使用asm是非常有必要的。这一系列文章是学习asm过程中所做的总结,希望能够对大家有所帮助。

class文件格式

class结构

class文件包含如下几个部分:

  • 一部分主要用于描述类修饰符(如public、private)、类名、父类名、实现的接口以及类注解等信息
  • 一部分声明类中的所有字段,每个字段都包含了详细的类型、修饰符、名称注解等信息
  • 最后一部分用于描述类的方法,包含构造和类的所有方法,除了方法参数、返回值相关信息外,还包含编译之后的字节码指令

详细信息如下图所示:

image.png

类型描述符

java中的类型与字节码中类型对应关系如下:

image.png

方法描述符

方法描述符以左括号开始,其后是方法参数对应的类型描述符,接着是右括号,最后加载返回值的描述符,也就是说方法描述括号中表示参数类型,而括号外是返回值类型,可参考如下示例:

image.png

Class接口和相关组件

ASM利用ClassVisitor来生成和转换字节码,其结构如下

image.png

ClassVisitor的方法分为两类:一类较简单,这些方法没有返回值,其参数就代表具体的字节码内容,如visit方法,从参数就能知道类名、父类和实现的接口等信息;另一类则比较复杂,通常返回新的Visitor,如visitAnnotationvisitFieldvisitMethod,通过递归调用这些方法所返回的子Visitor来访问具体内容,比如ClassVisitor的visitField方法会返回FieldVisitor,其结构如下图所示:

image.png

ASM提供如下三种组件进行字节码的生成和转换:

  • ClassReader: 解析字节码,可以将其看做事件生产者。
  • ClassWriter: 生成字节码,可将其看做事件消费者。
  • ClassVisitor:作为另一个ClassVisitor的代理,可重写任何方法,从而对字节码进行修改,可以看做事件过滤器。

ClassVisitor的访问顺序如下图所示:

image.png

按字面顺序访问,?表示如果没有则不访问,*表示访问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并重写visitvisitFieldvisitMethod三个放方法将类名、父类、实现接口、方法名及其描述符、字段名及其描述符打印出来,接下去要将其应用到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;
    ...

}

从这个例子中可以看出,转换数据流向如下图所示:

image.png

即Reader将class数据读取到Adapter中,经由adapter处理后把数据传递给Writer进行处理,在上面的demo中,adapter就是FiledValModifyTransform,其详细时序图如下:

image.png

移除类成员

在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中方法的访问顺序限制(前面介绍),所以我们不能在visitvisitSourcevisitOuterClassvisitAnnotationvisitAttribute中调用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");
}

参考

asm4-guide

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYMRf9lG' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片