ASM学习系列(一)

一、介绍

ASM是一个很好的静态代码操作工具,既可以用于分析静态代码,也可以用来修改静态代码,在学习的时候十分推荐阅读他的官方文档(https://asm.ow2.io/asm4-guide.pdf),中文版本见网盘(https://pan.baidu.com/s/1s4jkodxaKqb9Rt6aphRBpA ,提取码:zwy1 )。

二、关键类

在使用ASM的时候,最常用到的三个类为ClassReader,ClassVisitor和ClassWriter。ClassReader是用来获取类信息的类;ClassVisitor是ClassReader用于具体操作操作类的对象,是一个抽象类,可以看成是解析操作静态代码的一个工具;ClassWriter是ClassVisitor的一个子类,可以用于直接操作类中的信息。接下来,先介绍一下ClassVisitor这个类:

public abstract class ClassVisitor {
    ...
     public void visit(int version, int access, String name, 
     String signature, String superName, String[] interfaces) {
         ...
     }   
    public void visitSource(String source, String debug) {
        ...
    }
    public ModuleVisitor visitModule(String name, int access, String version) {
        ...
    }
    public void visitOuterClass(String owner, String name, String descriptor) {
        ...
    }
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        ...
    }
    public void visitAttribute(Attribute attribute) {
        ...
    }
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        ...
    }
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        ...
    }
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        ...
    }
    public void visitEnd() {
        ...
    }
    ...
}

以上列举了一些比较重要的方法,在进行代码执行的时候一定会调用visit方法,然后会调用visitSource,接下来调用visitOuterClass,之后可按任意顺序调用剩下的方法,在最后会调用visitEnd方法。可以看出相对字节码的哪一部分进行操作只需要调用相应的方法就可以了,比如说要修改方法可以在visitMethod中修改相应的逻辑。

三、Demo

1、分析类

对类进行分析,只需要用到上述所说到的ClassReader和ClassVisitor即可,首先创建一个ClassReader对象:

try {
    ClassPrinter cp = new ClassPrinter();
    ClassReader cr = new ClassReader("java.lang.Boolean");
} catch (IOException e) {
    throw new RuntimeException(e);
}

他的构造方法中传入的是我们需要进行解析的类,注意这里要输入完整包名!然后,需要去继承ClassVisitor类,在里面加上我们需要的功能

lass ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(ASM4);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        System.out.println(name + " extends " + superName + "{");
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        System.out.println(" " + descriptor + " " + name);
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println(" " + name + descriptor);
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        System.out.println("}");
    }
}

由于这里进行的操作是解析类,在visit方法类中可以拿到当前的类名和对应父类的名字。visitField可以拿到成员变量的信息,其中access是访问权限;name为变量名;descriptor为描述符,表示变量的类型;value为变量的值。visitMethod方法中access为访问权限;name为函数名;descriptor为函数原型。
现在创建一个ClassPrinter实例,再通过调用ClassReader中的accept调用即可,完整代码如下:

ClassPrinter cp = new ClassPrinter();
try {
    ClassReader cr = new ClassReader("java.lang.Boolean");
    cr.accept(cp, 0);
} catch (IOException e) {
    throw new RuntimeException(e);
}

2、创建类

ClassReader主要用于字节码的分析,而ClassWriter主要用于对字节码进行一些修改,这里很简单,直接看代码

ClassWriter cw = new ClassWriter(0);
//版本号 访问权限 类名 范型 超类 继承类型
cw.visit(V1_8,
        ACC_PUBLIC,
        "com/gloomy/B",
        null,
        "com/gloomy/A",
        new String[]{});
cw.visitField(ACC_PUBLIC, "param1", "I", null, Integer.valueOf(163)).visitEnd();
cw.visitField(ACC_PUBLIC, "param2", "I", null, Integer.valueOf(162)).visitEnd();
cw.visitField(ACC_PUBLIC, "param3", "I", null, Integer.valueOf(164)).visitEnd();
cw.visitEnd();
byte[] byteArray = cw.toByteArray();

这里使用ClassReader创建了一个叫做B的类,继承自A类,并且在B类中有三个成员变量param1,param2,param3,最后调用他的toByteArray方法可以拿到他的字节码。

3、对现有类进行操作

现在,创建一个A类,如下:

public class A {
    int p1 = 1000;
    int p2 = 2000;
    int p3 = 3000;
}

首先,使用ClassReader读取类A的字节码:

ClassReader cr = new ClassReader("com.gloomy.A");
  • 增加成员变量
    假如现在,我们想要对A类增加一个成员变量,需要同时使用ClassWriter和ClassVisitor的子类,ClassWriter用于字节码操作的实现,ClassVisitor的子类用于操作时机的控制。
    首先创建一个ClassVisitor的子类AddFieldAdapter:
class AddFieldAdapter extends ClassVisitor {
    ClassVisitor cv;
    String paramName;
    int access;
    String desc;
    boolean getPresentParam;
    public AddFieldAdapter(ClassVisitor cv, String paramName, int access, String desc) {
        super(ASM4, cv);
        this.cv = cv;
        this.paramName = paramName;
        this.access = access;
        this.desc = desc;
        this.getPresentParam = false;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        if(name.equals(paramName)) {
            getPresentParam = true;
        }
        return cv.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public void visitEnd() {
        if(!getPresentParam) {
            FieldVisitor fv = cv.visitField(access, paramName, desc, null, null);
            fv.visitEnd();
        }
        cv.visitEnd();
    }
}

构造方法中需要传入一些参数,其中cv表示的就是ClassWriter(ClassVisitor的子类,用于操作字节码),其他参数用于描述我们需要增加的参数信息。
这里需要注意下增加参数的时机,是在visitEnd方法中,同时需要在visitField方法进行判断,新增的参数是否存在!完整代码如下:

try {
    ClassReader cr = new ClassReader("com.gloomy.A");
    ClassWriter cw = new ClassWriter(cr, 0);    //ClassWriter cw = new ClassWriter(0);
    PrintWriter pw = new PrintWriter("output1.txt");
    TraceClassVisitor cv = new TraceClassVisitor(cw, pw);
    AddFieldAdapter af = new AddFieldAdapter(cv, "p4", ACC_PUBLIC, "java/lang/Boolean");
    cr.accept(af, 0);
    byte[] b = cw.toByteArray();
} catch (IOException e) {
    e.printStackTrace();
}

这里还使用到了一个工具类TraceClassVisitor,可以将类的字节码文件解析后输出到文件中,便于观察最后的结果。代码的第二行起始对字节码操作还进行了优化,因为我们需要操作的字节码只有一小部分,不需要对所有的字节码都进行一遍扫描,这里如果将ClassReader和ClassWriter进行相互引用,最后在调用accpt方法的时候,只会对ClassWriter操作的部分进行字节码扫描,其他部分都是进行直接复制的操作。同样,最后可以通过toByteArray拿到字节码。
通过TraceClassVisitor可以看到,在生成的字节码最后,新增上了我们增添的变量p4!!

在这里插入图片描述

  • 删除成员变量
    删除成员变量和增添成员变量的操作大同小异,只有对字节码进行操作的ClassVisitor的子类的对应方法有所不同,直接看代码:
class RemoveFieldAdapter extends  ClassVisitor {
    ClassVisitor cv;
    String paramName;
    RemoveFieldAdapter(ClassVisitor cv, String paramName) {
        super(ASM4, cv);
        this.cv = cv;
        this.paramName = paramName;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        if(name.equals(paramName)) {
            return null;
        }
        return cv.visitField(access, name, descriptor, signature, value);
    }
}

可以看出这里,删除操作起始就是获取对应变量的时候返回null即可,完整代码如下:

try {
    ClassReader cr = new ClassReader("com.gloomy.A");
    ClassWriter cw = new ClassWriter(cr, 0);
    TraceClassVisitor tc = new TraceClassVisitor(cw, new PrintWriter("output2.txt"));
    RemoveFieldAdapter rf = new RemoveFieldAdapter(tc, "p1");
    cr.accept(rf, 0);
} catch (IOException e) {
    throw new RuntimeException(e);
}

看最后反编译后的字节码为,p1变量果然不见了:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值