ASM字节码编辑

ASM

Java字节码库允许我们通过字节码库的API动态创建或修改Java类、方法、变量等操作而被广泛使用,本节将讲解ASM库的使用。

ASM是一种通用Java字节码操作和分析框架,它可以直接以二进制形式修改一个现有的类或动态生成类文件。

ASM提供了三个基于ClassVisitor API的核心API,用于生成和转换类:

ClassReader类用于解析class文件或二进制流;
ClassWriter类是ClassVisitor的子类,用于生成类二进制;
ClassVisitor是一个抽象类,自定义ClassVisitor重写visitXXX方法,可获取捕获ASM类结构访问的所有事件;

ClassVistor

ClassVistor用于访问class,本身是抽象类。定义在读取Class字节码时会触发的事件。只要将所需执行的操作写入对应方法下,调用ClassVistor的其他类就能在对应的条件下触发他们。
以下为一些API:

public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc); 
AnnotationVisitor visitAnnotation(String desc, boolean visible); 
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
public MethodVisitor visitMethod(int access, String name,String desc,String signature, String[] exceptions);
void visitEnd();

MethodVisitor

MethodVisitorClassVisitor,重写MethodVisitor类方法可获取捕获到对应的visit事件,MethodVisitor会依次按照如下顺序调用visit方法:

( visitParameter )* 
[ visitAnnotationDefault ] 
( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )* 
[ visitCode 
( visitFrame | visit<i>X</i>Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* 
visitMaxs 
] 
visitEnd

AdviceAdapter

AdviceAdapter的父类是GeneratorAdapterLocalVariablesSorter,在MethodVisitor类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter来修改字节码。

AdviceAdapter类实现了一些非常有价值的方法,如:onMethodEnter(方法进入时回调方法)、onMethodExit(方法退出时回调方法),如果我们自己实现很容易掉进坑里面,因为这两个方法都是根据条件推算出来的。比如我们如果在构造方法的第一行直接插入了我们自己的字节码就可能会发现程序一运行就会崩溃,因为Java语法中限制我们第一行代码必须是super(xxx)

GeneratorAdapter封装了一些栈指令操作的方法,如loadArgArray方法可以直接获取方法所有参数数组、invokeStatic方法可以直接调用类方法、push方法可压入各种类型的对象等。

比如LocalVariablesSorter类实现了计算本地变量索引位置的方法,如果要在方法中插入新的局部变量就必须计算变量的索引位置,我们必须先判断是否是非静态方法、是否是long/double类型的参数(宽类型占两个位),否则计算出的索引位置还是错的。使用AdviceAdapter可以直接调用mv.newLocal(type)计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。

ClassReader

ClassReader类用于解析类字节码,创建ClassReader对象可传入类名、类字节码数组或者类输入流对象。

创建完ClassReader对象就会触发字节码解析(解析class基础信息,如常量池、接口信息等),所以可以直接通过ClassReader对象获取类的基础信息,如下:

// 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
final ClassReader cr = new ClassReader(className);

        System.out.println(
        "解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
        ",实现接口:" + Arrays.toString(cr.getInterfaces())
        );

调用ClassReader类的accpet方法需要传入自定义的ClassVisitor对象,ClassReader会按照如下顺序,依次调用该ClassVisitor的类方法。

visit
[ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedclass ][ visitOuterClass ]
( visitAnnotation | visitTypeAnnotation | visitAttribute )*
( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )*
visitEnd

除了ClassVisitor也可以传入AnnotationVisitorFieldVisitorMethodVisitor,每当有事件发生时,调用注册的Visitor做相应的处理。

ClassReader方法

构造方法
在这里插入图片描述

其他方法
ClassReader提供了一系列get方法获取类信息:
在这里插入图片描述
不过,它最重要的方法还是accept方法:
在这里插入图片描述
accept可以接受一个ClassVisitor,接收后便开始读取数据。当满足一定条件时,就会触发ClassVisitor下的方法。

示例

示例一:读取父类

使用InputStreambyte[]获取类的字节,或直接通过String的类名称来获取类。
例:

ClassReader cr = new ClassReader("java.util.ArrayList");
System.out.println(cr.getSuperName());

我们使用了String作为输入的构造器,ClassReader接下来会处理ArrayList这个类。在第二行中,其便打印出了它的超类。以下为输出:

java/util/AbstractList

ClassReader也可分析用户类。例如IDEA中,我们需要访问clazz包下的Hello.class

ClassReader cr = new ClassReader("clazz.Hello");

示例二:读取类方法信息

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;

import java.io.IOException;

import static jdk.internal.org.objectweb.asm.Opcodes.*;

public class Test {

    public static void main(String[] var0) throws IOException {
        ClassReader cr = new ClassReader("java.util.ArrayList");
        cr.accept(new MyClassVisitor(ASM4),0);
    }

    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int i) {
            super(i);
        }

        @Override
        public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
            System.out.println("访问方法 " + s);
            return super.visitMethod(i, s, s1, s2, strings);
        }
    }
}

我们继承了ClassVisitor类并重写了visitMethod方法。还记得我们之前所说的吗?ClassVistor定义了在读取Class字节码时会触发的事件。我们通过accept方法,建立了ClassVisitorClassReader之间的连接。因此,当ClassReader访问对象的方法时,它将触发ClassVisitor内的visitMethod方法,这时由于我们在visitMethod下添加了一条println语句,这样我们就能获取所有的方法名了。
上述代码执行结果如下:

访问方法 <init>
访问方法 <init>
访问方法 <init>
访问方法 trimToSize
访问方法 ensureCapacity
访问方法 calculateCapacity
访问方法 ensureCapacityInternal
访问方法 ensureExplicitCapacity
访问方法 grow
访问方法 hugeCapacity
访问方法 size
访问方法 isEmpty
访问方法 contains
访问方法 indexOf
访问方法 lastIndexOf
访问方法 clone
访问方法 toArray
访问方法 toArray
访问方法 elementData
访问方法 get
访问方法 set
访问方法 add
访问方法 add
访问方法 remove
访问方法 remove
访问方法 fastRemove
访问方法 clear
访问方法 addAll
访问方法 addAll
访问方法 removeRange
访问方法 rangeCheck
访问方法 rangeCheckForAdd
访问方法 outOfBoundsMsg
访问方法 removeAll
访问方法 retainAll
访问方法 batchRemove
访问方法 writeObject
访问方法 readObject
访问方法 listIterator
访问方法 listIterator
访问方法 iterator
访问方法 subList
访问方法 subListRangeCheck
访问方法 forEach
访问方法 spliterator
访问方法 removeIf
访问方法 replaceAll
访问方法 sort
访问方法 access$000
访问方法 <clinit>

这样我们就获取了ArrayList下的所有方法。

更进一步:MethodVisitorClassVisitor的本质是一样的。既然我们通过了visitMethod能够返回MethodVisitor对象,我们能不能更进一步,找出ArrayList下的方法内又使用了那些方法呢?提示:MethodVisitor下的visitMethodInsn方法也会在访问方法时触发。建议读者在上述的代码的基础上自己尝试。以下是参考答案:

import com.sun.xml.internal.ws.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;

import java.io.IOException;
import java.util.ArrayList;

import static jdk.internal.org.objectweb.asm.Opcodes.*;

public class Test {

    public static void main(String[] var0) throws IOException {
        ClassReader cr = new ClassReader("java.util.ArrayList");
        cr.accept(new MyClassVisitor(ASM4),0);

    }

    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int i) {
            super(i);
        }

        @Override
        public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
            System.out.println("访问方法 " + s);
            MethodVisitor mv = super.visitMethod(i, s, s1, s2, strings);
            return new MyMethodVisitor(ASM4,mv);
        }
    }

    static class MyMethodVisitor extends MethodVisitor {

        public MyMethodVisitor(int i, MethodVisitor methodVisitor) {
            super(i, methodVisitor);
        }

        @Override
        public void visitMethodInsn(int i, String s, String s1, String s2, boolean b) {
            System.out.println("\t-> " + s1);
            super.visitMethodInsn(i, s, s1, s2, b);
        }
    }
}

输出如下:

访问方法 <init>
	-> <init>
	-> <init>
	-> append
	-> append
	-> toString
	-> <init>
访问方法 <init>
	-> <init>
访问方法 <init>
	-> <init>
	-> toArray
	-> getClass
	-> copyOf
访问方法 trimToSize
	-> copyOf
访问方法 ensureCapacity
	-> ensureExplicitCapacity
访问方法 calculateCapacity
	-> max
访问方法 ensureCapacityInternal
	-> calculateCapacity
	-> ensureExplicitCapacity
访问方法 ensureExplicitCapacity
	-> grow
访问方法 grow
	-> hugeCapacity
	-> copyOf
访问方法 hugeCapacity
	-> <init>
访问方法 size
访问方法 isEmpty
访问方法 contains
	-> indexOf
访问方法 indexOf
	-> equals
访问方法 lastIndexOf
	-> equals
访问方法 clone
	-> clone
	-> copyOf
	-> <init>
访问方法 toArray
	-> copyOf
访问方法 toArray
	-> getClass
	-> copyOf
	-> arraycopy
访问方法 elementData
访问方法 get
	-> rangeCheck
	-> elementData
访问方法 set
	-> rangeCheck
	-> elementData
访问方法 add
	-> ensureCapacityInternal
访问方法 add
	-> rangeCheckForAdd
	-> ensureCapacityInternal
	-> arraycopy
访问方法 remove
	-> rangeCheck
	-> elementData
	-> arraycopy
访问方法 remove
	-> fastRemove
	-> equals
	-> fastRemove
访问方法 fastRemove
	-> arraycopy
访问方法 clear
访问方法 addAll
	-> toArray
	-> ensureCapacityInternal
	-> arraycopy
访问方法 addAll
	-> rangeCheckForAdd
	-> toArray
	-> ensureCapacityInternal
	-> arraycopy
	-> arraycopy
访问方法 removeRange
	-> arraycopy
访问方法 rangeCheck
	-> outOfBoundsMsg
	-> <init>
访问方法 rangeCheckForAdd
	-> outOfBoundsMsg
	-> <init>
访问方法 outOfBoundsMsg
	-> <init>
	-> append
	-> append
	-> append
	-> append
	-> toString
访问方法 removeAll
	-> requireNonNull
	-> batchRemove
访问方法 retainAll
	-> requireNonNull
	-> batchRemove
访问方法 batchRemove
	-> contains
	-> arraycopy
	-> arraycopy
访问方法 writeObject
	-> defaultWriteObject
	-> writeInt
	-> writeObject
	-> <init>
访问方法 readObject
	-> defaultReadObject
	-> readInt
	-> calculateCapacity
	-> getJavaOISAccess
	-> checkArray
	-> ensureCapacityInternal
	-> readObject
访问方法 listIterator
	-> <init>
	-> append
	-> append
	-> toString
	-> <init>
	-> <init>
访问方法 listIterator
	-> <init>
访问方法 iterator
	-> <init>
访问方法 subList
	-> subListRangeCheck
	-> <init>
访问方法 subListRangeCheck
	-> <init>
	-> append
	-> append
	-> toString
	-> <init>
	-> <init>
	-> append
	-> append
	-> toString
	-> <init>
	-> <init>
	-> append
	-> append
	-> append
	-> append
	-> append
	-> toString
	-> <init>
访问方法 forEach
	-> requireNonNull
	-> accept
	-> <init>
访问方法 spliterator
	-> <init>
访问方法 removeIf
	-> requireNonNull
	-> <init>
	-> test
	-> set
	-> <init>
	-> nextClearBit
	-> <init>
访问方法 replaceAll
	-> requireNonNull
	-> apply
	-> <init>
访问方法 sort
	-> sort
	-> <init>
访问方法 access$000
访问方法 <clinit>

示例三:读取类/成员变量/方法信息

我们写一个简单的读取类、成员变量、方法信息的一个示例,需要重写ClassVisitor类的visitvisitFieldvisitMethod方法。

ASM读取类信息示例代码:

package com.anbai.sec.bytecode.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

import java.io.IOException;
import java.util.Arrays;

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;

public class ASMClassVisitorTest {

    public static void main(String[] args) {
        // 定义需要解析的类名称
        String className = "com.anbai.sec.bytecode.TestHelloWorld";

        try {
            // 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
            final ClassReader cr = new ClassReader(className);

            System.out.println(
                    "解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
                            ",实现接口:" + Arrays.toString(cr.getInterfaces())
            );

            System.out.println("-----------------------------------------------------------------------------");

            // 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
            cr.accept(new ClassVisitor(ASM9) {
                @Override
                public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                    System.out.println(
                            "变量修饰符:" + access + "\t 类名:" + name + "\t 父类名:" + superName +
                                    "\t 实现的接口:" + Arrays.toString(interfaces)
                    );

                    System.out.println("-----------------------------------------------------------------------------");

                    super.visit(version, access, name, signature, superName, interfaces);
                }

                @Override
                public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                    System.out.println(
                            "变量修饰符:" + access + "\t 变量名称:" + name + "\t 描述符:" + desc + "\t 默认值:" + value
                    );

                    return super.visitField(access, name, desc, signature, value);
                }

                @Override
                public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

                    System.out.println(
                            "方法修饰符:" + access + "\t 方法名称:" + name + "\t 描述符:" + desc +
                                    "\t 抛出的异常:" + Arrays.toString(exceptions)
                    );

                    return super.visitMethod(access, name, desc, signature, exceptions);
                }
            }, EXPAND_FRAMES);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

程序执行后输出:

解析类名:com/anbai/sec/bytecode/TestHelloWorld,父类:java/lang/Object,实现接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:131105     类名:com/anbai/sec/bytecode/TestHelloWorld    父类名:java/lang/Object    实现的接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:26     变量名称:serialVersionUID   描述符:J   默认值:-7366591802115333975
变量修饰符:2  变量名称:id     描述符:J   默认值:null
变量修饰符:2  变量名称:username   描述符:Ljava/lang/String;  默认值:null
变量修饰符:2  变量名称:password   描述符:Ljava/lang/String;  默认值:null
方法修饰符:1  方法名称:<init>     描述符:()V     抛出的异常:null
方法修饰符:1  方法名称:hello  描述符:(Ljava/lang/String;)Ljava/lang/String;  抛出的异常:null
方法修饰符:9  方法名称:main   描述符:([Ljava/lang/String;)V  抛出的异常:null
方法修饰符:1  方法名称:getId  描述符:()J     抛出的异常:null
方法修饰符:1  方法名称:setId  描述符:(J)V    抛出的异常:null
方法修饰符:1  方法名称:getUsername    描述符:()Ljava/lang/String;    抛出的异常:null
方法修饰符:1  方法名称:setUsername    描述符:(Ljava/lang/String;)V   抛出的异常:null
方法修饰符:1  方法名称:getPassword    描述符:()Ljava/lang/String;    抛出的异常:null
方法修饰符:1  方法名称:setPassword    描述符:(Ljava/lang/String;)V   抛出的异常:null
方法修饰符:1  方法名称:toString   描述符:()Ljava/lang/String;    抛出的异常:null

通过这个简单的示例,我们可以通过ASM实现遍历一个类的基础信息。

ClassWriter

ClassWriter用于创建和修改类。注意ClassWriter继承了ClassVisitor

ClassReader方法

构造方法

ClassWriter具有以下构造器:
在这里插入图片描述

其中,intflag,为可选标志,用于修改该类的默认行为,可以设置COMPUTE_MAXSCOMPUTE_FRAMES
建议设置这两种之和COMPUTE_FRAMES|COMPUTE_MAXS,性能虽然差点,但是可以自动更新操作数栈和方法调用帧计算。

其他方法
在这里插入图片描述
ClassWriter提供了一类visit方法来编写类。使用visit方法开始编写,visitEnd方法结束编写。visit方法能够为需要修改的类添加属性和方法。例如,visitMethod可以为类添加方法,visitAttribute可以为类添加属性。
完成一个visit周期,即类创建完成后,可使用toByteArray()将生成的类以Byte数组导出。

接下来是一些具体的方法解释,以下内容来源于ASM4.0文档:

public final void visit(int version,
         int access,
         String name,
         String signature,
         String superName,
         String[] interfaces)

version:编辑的类的java版本,例如V1_8;

access:访问标识,即该类的修饰,如ACC_PUBLIC。若一个类具有多个修饰符,将Opcode码相加即可;

name:类名;

signature:签名,可为null

superName:描述它的超类,即extends的类;

superName:描述它的接口,即implements的类;

例:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC,"clazz/MyClass", null, "java/lang/Object",null);

这将创建一个名为MyClass的自定类。我们将该类输出,查看结果:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC,"clazz/MyClass", null, "java/lang/Object",null);
cw.visitEnd();
byte[] b = cw.toByteArray();
FileOutputStream fos = new FileOutputStream(new File("src/clazz/MyClass.class"));
fos.write(b);
fos.close();

这段代码将在clazz包下生成MyClass.class。 经IDEA反编译结果如下:

package clazz;

public class MyClass {
}

示例

示例一:生成一个类

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC,"clazz/TreeNode", null, "java/lang/Object",null);
cw.visitField(0,"left","Lclazz/TreeNode;",null,null);
cw.visitField(0,"right","Lclazz/TreeNode;",null,null);
cw.visitMethod(ACC_PUBLIC,"print","()V",null,null);
cw.visitEnd();
byte[] b = cw.toByteArray();
FileOutputStream fos = new FileOutputStream(new File("src/clazz/TreeNode.class"));
fos.write(b);
fos.close();

输出类如下:

package clazz;

public class TreeNode {
    TreeNode left;
    TreeNode right;

    public void print() {
    }
}

我们尚未在print方法下写入代码,这需要使用MethodVisitor

示例二:生成一个类

在某些业务场景下我们需要动态一个类来实现一些业务,这个时候就可以使用ClassWriter来动态创建出一个Java类的二进制文件,然后通过自定义的类加载器就可以将我们动态生成的类加载到JVM中。假设我们需要生成一个TestASMHelloWorld类,代码如下:

示例TestASMHelloWorld类:

package com.anbai.sec.classloader;

public class TestASMHelloWorld {
    public static String hello() {
        return "Hello World~";
    }
}

使用ClassWriter生成类字节码示例:

package com.anbai.sec.bytecode.asm;

import org.javaweb.utils.HexUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class TestASMHelloWorldDump implements Opcodes {

   private static final String CLASS_NAME = "com.anbai.sec.classloader.TestASMHelloWorld";

   private static final String CLASS_NAME_ASM = "com/anbai/sec/classloader/TestASMHelloWorld";

   public static byte[] dump() throws Exception {
      // 创建ClassWriter,用于生成类字节码
      ClassWriter cw = new ClassWriter(0);

      // 创建MethodVisitor
      MethodVisitor mv;

      // 创建一个字节码版本为JDK1.7的com.anbai.sec.classloader.TestASMHelloWorld类
      cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME_ASM, null, "java/lang/Object", null);

      // 设置源码文件名
      cw.visitSource("TestHelloWorld.java", null);

      // 创建一个空的构造方法,
      // public TestASMHelloWorld() {
      // }
      {
         mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
         mv.visitCode();
         Label l0 = new Label();
         mv.visitLabel(l0);
         mv.visitLineNumber(5, l0);
         mv.visitVarInsn(ALOAD, 0);
         mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
         mv.visitInsn(RETURN);
         Label l1 = new Label();
         mv.visitLabel(l1);
         mv.visitLocalVariable("this", "L" + CLASS_NAME_ASM + ";", null, l0, l1, 0);
         mv.visitMaxs(1, 1);
         mv.visitEnd();
      }

      // 创建一个hello方法,
      // public static String hello() {
      //     return "Hello World~";
      // }
      {
         mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "hello", "()Ljava/lang/String;", null, null);
         mv.visitCode();
         Label l0 = new Label();
         mv.visitLabel(l0);
         mv.visitLineNumber(8, l0);
         mv.visitLdcInsn("Hello World~");
         mv.visitInsn(ARETURN);
         mv.visitMaxs(1, 0);
         mv.visitEnd();
      }

      cw.visitEnd();

      return cw.toByteArray();
   }

   public static void main(String[] args) throws Exception {
      final byte[] classBytes = dump();

      // 输出ASM生成的TestASMHelloWorld类HEX
      System.out.println(new String(HexUtils.hexDump(classBytes)));

      // 创建自定义类加载器,加载ASM创建的类字节码到JVM
      ClassLoader classLoader = new ClassLoader(TestASMHelloWorldDump.class.getClassLoader()) {
         @Override
         protected Class<?> findClass(String name) {
            try {
               return super.findClass(name);
            } catch (ClassNotFoundException e) {
               return defineClass(CLASS_NAME, classBytes, 0, classBytes.length);
            }
         }
      };

      System.out.println("-----------------------------------------------------------------------------");

      // 反射调用通过ASM生成的TestASMHelloWorld类的hello方法,输出返回值
      System.out.println("hello方法执行结果:" + classLoader.loadClass(CLASS_NAME).getMethod("hello").invoke(null));
   }

}

程序执行结果如下:

0000019F CA FE BA BE 00 00 00 33 00 14 01 00 2B 63 6F 6D .......3....+com
000001AF 2F 61 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 /anbai/sec/class
000001BF 6C 6F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 loader/TestASMHe
000001CF 6C 6C 6F 57 6F 72 6C 64 07 00 01 01 00 10 6A 61 lloWorld......ja
000001DF 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 07 00 va/lang/Object..
000001EF 03 01 00 13 54 65 73 74 48 65 6C 6C 6F 57 6F 72 ....TestHelloWor
000001FF 6C 64 2E 6A 61 76 61 01 00 06 3C 69 6E 69 74 3E ld.java...<init>
0000020F 01 00 03 28 29 56 0C 00 06 00 07 0A 00 04 00 08 ...()V..........
0000021F 01 00 04 74 68 69 73 01 00 2D 4C 63 6F 6D 2F 61 ...this..-Lcom/a
0000022F 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 6C 6F nbai/sec/classlo
0000023F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 6C 6C ader/TestASMHell
0000024F 6F 57 6F 72 6C 64 3B 01 00 05 68 65 6C 6C 6F 01 oWorld;...hello.
0000025F 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 ..()Ljava/lang/S
0000026F 74 72 69 6E 67 3B 01 00 0C 48 65 6C 6C 6F 20 57 tring;...Hello W
0000027F 6F 72 6C 64 7E 08 00 0E 01 00 04 43 6F 64 65 01 orld~......Code.
0000028F 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C ..LineNumberTabl
0000029F 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C e...LocalVariabl
000002AF 65 54 61 62 6C 65 01 00 0A 53 6F 75 72 63 65 46 eTable...SourceF
000002BF 69 6C 65 00 21 00 02 00 04 00 00 00 00 00 02 00 ile.!...........
000002CF 01 00 06 00 07 00 01 00 10 00 00 00 2F 00 01 00 ............/...
000002DF 01 00 00 00 05 2A B7 00 09 B1 00 00 00 02 00 11 .....*..........
000002EF 00 00 00 06 00 01 00 00 00 05 00 12 00 00 00 0C ................
000002FF 00 01 00 00 00 05 00 0A 00 0B 00 00 00 09 00 0C ................
0000030F 00 0D 00 01 00 10 00 00 00 1B 00 01 00 00 00 00 ................
0000031F 00 03 12 0F B0 00 00 00 01 00 11 00 00 00 06 00 ................
0000032F 01 00 00 00 08 00 01 00 13 00 00 00 02 00 05    ...............

-----------------------------------------------------------------------------
hello方法执行结果:Hello World~

程序执行后会在TestASMHelloWorldDump类同级的包下生成一个TestASMHelloWorld类,如下图:
在这里插入图片描述

示例三:修改类内容

我们已经了解了如何使用ClassWriter来编写一个类。但如果需要进行修改类的操作,则需要与ClassReaderClassVisitor一起使用。

这之前,我们来了解一下ClassVisitor的事件转发:

我们知道,ClassVisitor还有一个构造器,传入两个参数,一个int类型的flag,还有一个ClassVisitor。我们通过下面的代码解释:

	ClassVisitor cv1 = new ClassVisitor(ASM4) {};
 	ClassVisitor cv2 = new ClassVisitor(ASM4,cv1) {};

其中,cv2使用了两个参数的构造器,实现了事件转发。即,当cv2开始处理事件时,cv2除了自己进行处理,还会将事件转发给cv1

那事件转发有什么用呢?

我们知道,ClassWriter本身继承了ClassVisitor,在调用它的visit类型的方法时,它会操纵类,向内插入字节码。我们来看下面的代码会做些什么:

ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader("clazz/Hello");
cr.accept(cw, 0);
byte[] b = cw.toByteArray();

这里我们添加了一个cv中间层,这里用到了事件转发。首先cr会触发cv的方法,cv再将事务转发给cw。这样做虽然对于这个例子依然没有什么意义,但是为我们提供了修改类的契机:我们可以重写ClassVisitor,修改它的事件转发,让我们人为的筛选需要修改的内容。我们看下面的例子:

现在我们有一个Hello类,代码如下:

public class Hello {
    static int a = 3;
    static int b = 5;
    static double c = 7.0;


    public Hello() {
    }

    public static void main(String[] args) {

    }
}

假设这个Hello类非常复杂繁琐,根本无法手动修改。现在为了扩展使用,我们想修改这个类,将所有的int变量设为long变量。该如何做呢?下面是一个示例:

package clazz;

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.FieldVisitor;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test2 {

    public static void main(String[] var0) throws IOException {
        ClassWriter cw = new ClassWriter(0);
        ClassVisitor cv = new MyClassVisitor(ASM4, cw) { };
        ClassReader cr = new ClassReader("clazz/Hello");
        cr.accept(cv, 0);
        byte[] b = cw.toByteArray();
        FileOutputStream fos = new FileOutputStream(new File("src/clazz/Hello.class"));
        fos.write(b);
        fos.close();
    }

    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int i, ClassVisitor classVisitor) {
            super(i, classVisitor);
        }

        @Override
        public FieldVisitor visitField(int i, String s, String s1, String s2, Object o) {
            if (s1.equals("I")) s1 = "J";
            return super.visitField(i, s, s1, s2, o);
        }
    }

}

我们重写了visitField方法。当cv接收到visitField方法时,它会筛选出所有描述符为I(即int)的变量,将其描述符修改为J(即long)。这样将事件转发给ClassWriter后,cw就能据此修改class,修改就完成了。

输出的class文件如下:

package clazz;

public class Hello {
    static long a;
    static long b;
    static double c;

    public Hello() {
    }

    public static void main(String[] args) {
    }

    static {
        a = 3;
        b = 5;
        c = 7.0D;
    }
}

用类似的方法还能为类添加成员或删除成员。做法也很简单,如果想删除一个方法,就再其转发时返回null即可。若想在main方法后添加一个方法,则在读取main方法时额外访问一个visitMethod即可。

示例四:修改方法内内容

假设我们需要修改com.anbai.sec.bytecode.TestHelloWorld类的hello方法,实现以下两个需求:

  1. 在原业务逻辑执行前打印出该方法的参数值;
  2. 修改该方法的返回值;

原业务逻辑:

public String hello(String content) {
   String str = "Hello:";
   return str + content;
}

修改之后的业务逻辑代码:

public String hello(String content) {
    System.out.println(content);
    String var2 = "javasec.org";
  
    String str = "Hello:";
    String var4 = str + content;
  
    System.out.println(var4);
    return var2;
}

借助ASM我们可以实现类方法的字节码编辑。

修改类方法字节码实现代码:

package com.anbai.sec.bytecode.asm;

import org.javaweb.utils.FileUtils;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

import java.io.File;
import java.io.IOException;

import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;

public class ASMMethodVisitorTest {

   public static void main(String[] args) {
      // 定义需要解析的类名称
      String className = "com.anbai.sec.bytecode.TestHelloWorld";

      try {
         // 创建ClassReader对象,用于解析类对象,可以根据类名、二进制、输入流的方式创建
         final ClassReader cr = new ClassReader(className);

         // 创建ClassWriter对象,COMPUTE_FRAMES会自动计算max_stack和max_locals
         final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

         // 使用自定义的ClassVisitor访问者对象,访问该类文件的结构
         cr.accept(new ClassVisitor(ASM9, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
               if (name.equals("hello")) {
                  MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

                  // 创建自定义的MethodVisitor,修改原方法的字节码
                  return new AdviceAdapter(api, mv, access, name, desc) {
                     int newArgIndex;

                     // 获取String的ASM Type对象
                     private final Type stringType = Type.getType(String.class);

                     @Override
                     protected void onMethodEnter() {
                        // 输出hello方法的第一个参数,因为hello是非static方法,所以0是this,第一个参数的下标应该是1
                        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        mv.visitVarInsn(ALOAD, 1);
                        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

                        // 创建一个新的局部变量,newLocal会计算出这个新局部对象的索引位置
                        newArgIndex = newLocal(stringType);

                        // 压入字符串到栈顶
                        mv.visitLdcInsn("javasec.org");

                        // 将"javasec.org"字符串压入到新生成的局部变量中,String var2 = "javasec.org";
                        storeLocal(newArgIndex, stringType);
                     }

                     @Override
                     protected void onMethodExit(int opcode) {
                        dup(); // 复制栈顶的返回值

                        // 创建一个新的局部变量,并获取索引位置
                        int returnValueIndex = newLocal(stringType);

                        // 将栈顶的返回值压入新生成的局部变量中
                        storeLocal(returnValueIndex, stringType);

                        // 输出hello方法的返回值
                        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                        mv.visitVarInsn(ALOAD, returnValueIndex);
                        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

                        // 压入方法进入(onMethodEnter)时存入到局部变量的var2值到栈顶
                        loadLocal(newArgIndex);

                        // 返回一个引用类型,即栈顶的var2字符串,return var2;
                        // 需要特别注意的是不同数据类型应当使用不同的RETURN指令
                        mv.visitInsn(ARETURN);
                     }
                  };
               }

               return super.visitMethod(access, name, desc, signature, exceptions);
            }
         }, EXPAND_FRAMES);

         File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/"), "TestHelloWorld.class");

         // 修改后的类字节码
         byte[] classBytes = cw.toByteArray();

         // 写入修改后的字节码到class文件
         FileUtils.writeByteArrayToFile(classFilePath, classBytes);
      } catch (IOException e) {
         e.printStackTrace();
      }
   }

}

程序执行后会在com.anbai.sec.bytecode包下创建一个TestHelloWorld.class文件:
在这里插入图片描述
命令行运行TestHelloWorld类,可以看到程序执行的逻辑已经被成功修改,输出结果如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值