Java-字节码插桩-ASM

600 阅读11分钟

字节码插桩

前言

文档来源于视频教学: b站(更多ub项目也在)

收获 -有逼格的技术 -面试吹b -能够操作class文件

-市面上教学内容杂乱不堪,无系统性

字节码插桩=操作class文件 -新增class文件 -修改class文件 -删除class文件

选择ASM库作为视频教学内容

字节码插桩库

ASM:central.sonatype.com/search?q=as…

ASM-原生字节码

尽量避免复杂的字节码相关 将另辟蹊径讲解如何利用ASM操作字节码

注:asm有俩个包: -jdk.internal.org.objectweb.asm jdk自己提供的,需要手动指定包路径,否则打包失败 -org.objectweb 引入asm pom即可

ASM

ASM中核心类: ClassVisitor:用于操作class ClassWriter:用于写出byte[] ClassReader: 用于读class文件 工作流程 image.png

-如果只是创建class,则不需要 ClassReader

利用ASM编写简单Demo(提前了解写法) 预期目标 创建一个HelloWorld类并且调用hello方法

public class HelloWorld {
    public void hello() {
        System.out.println("Hello ASM");
    }
}

实现 看不懂不要紧,混个眼熟,回到学jvm字节码的时候

public static void main(String[] args) throws Exception{

        String path = "org/xhy/HelloWorld.class";
        String filepath = FileUtils.getFilePath(path);
        byte[] bytes = dump();
        FileUtils.writeBytes(filepath, bytes);
    }

    public static byte[] dump() throws Exception {
        // (1) 创建 ClassWriter 对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用 visitXxx() 方法
        cw.visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
                null, "java/lang/Object", new String[]{});
        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        {
            methodVisitor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitLdcInsn("Hello ASM");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

输出字节码的方式 1.IDEA plugins : ASM Bytecode Outline 下载后右键会出现 show Bytecode Outline点击后即可出现 image.png 2.ASMPrint 通过ASM提供的API打印信息,暂且不关心该类 image.png

描述符

Java 类型ClassFile 描述符
booleanZ(Z 表示 Zero,零表示 false,非零表示 true
byteB
charC
doubleD
floatF
intI
longJ
shortS
voidV
non-array referenceL;
array reference[

对字段描述符的举例:

  • boolean flag: Z
  • byte byteValue: B
  • int intValue: I
  • float floatValue: F
  • double doubleValue: D
  • String strValue: Ljava/lang/String;
  • Object objValue: Ljava/lang/Object;
  • byte[] bytes: [B
  • String[] array: [Ljava/lang/String;
  • Object[][] twoDimArray: [[Ljava/lang/Object;

对方法描述符的举例:

  • int add(int a, int b): (II)I
  • void test(int a, int b): (II)V
  • boolean compare(Object obj): (Ljava/lang/Object;)Z
  • void main(String[] args): ([Ljava/lang/String;)V

ClassVisitor

ClassVisitor类介绍 field

public abstract class ClassVisitor {
    protected final int api; // 指定ASM版本
    protected ClassVisitor cv; // 可连接多个ClassVisitor
}

method image.png 常用的方法如下

public abstract class ClassVisitor {
    // 访问类
    public void visit(
        final int version,
        final int access,
        final String name,
        final String signature,
        final String superName,
        final String[] interfaces);
     // 访问字段
    public FieldVisitor visitField(
        final int access,
        final String name,
        final String descriptor,
        final String signature,
        final Object value);
    // 访问方法
    public MethodVisitor visitMethod( 
        final int access,
        final String name,
        final String descriptor,
        final String signature,
        final String[] exceptions);
    // 结束工作
    public void visitEnd();
    // ......
}

constructors

public abstract class ClassVisitor {
    public ClassVisitor(final int api) {
        this(api, null);
    }

    public ClassVisitor(final int api, final ClassVisitor classVisitor) {
        this.api = api;
        this.cv = classVisitor;
    }
}

方法介绍 目标类

public class HelloWorld {

    String xhy;

    public void hello() {
        System.out.println("Hello ASM");
    }
}

visit()

  • ClassVisitor.visit(int version, int access, String name, String signature, String superName, String[] interfaces)
    • version: 修改当前 Class 版本的信息
    • access: 修改当前类的访问标识(access flag)信息
    • name: 修改当前类的名字
    • signature: 修改当前类的泛型信息
    • superName: 修改父类
    • interfaces: 修改接口信息
visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
                null, "java/lang/Object", new String[]{});

visitField()

  • ClassVisitor.visitField(int access, String name, String descriptor, String signature, Object value)
    • access: 修改当前字段的访问标识(access flag)信息
    • name: 修改当前字段的名字
    • descriptor: 修改当前字段的描述符
    • signature: 修改当前字段的泛型信息
    • value: 修改当前字段的值,若access=0(static)则是常量
visitField(0, "xhy", "Ljava/lang/String;", null, null);

visitMethod()

用于构建方法头信息

  • ClassVisitor.visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
    • access: 修改当前方法的访问标识(access flag)信息
    • name: 修改当前方法的名字
    • descriptor: 修改当前方法的描述符。
    • signature: 修改当前方法的泛型信息
    • exceptions: 修改当前方法可以招出的异常信息
visitMethod(ACC_PUBLIC, "hello", "()V", null, null);

ClassVisitor中定义方法调用顺序

visit
[visitSource][visitModule][visitNestHost][visitPermittedSubclass][visitOuterClass]
(
    visitAnnotation |
    visitTypeAnnotation |
    visitAttribute
)*
(
    visitNestMember |
    visitInnerClass |
    visitRecordComponent |
    visitField |
    visitMethod
)* 
visitEnd
  • []: 表示最多调用一次,可以不调用,但最多调用一次。
  • () 和 |: 表示在多个方法之间,可以选择任意一个,并且多个方法之间不分前后顺序。
  • *: 表示方法可以调用 0 次或多次。

1.visit() 2.visitField() 3.visitMethod() 4.visitEnd()

MethodVisitor

public final MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    MethodWriter methodWriter =
        new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
    if (firstMethod == null) {
      firstMethod = methodWriter;
    } else {
      lastMethod.mv = methodWriter;
    }
    return lastMethod = methodWriter;
  }

ClassVisitor.visitorMethod()返回的对象,用于构建方法体

public abstract class MethodVisitor {
    public void visitCode();

    public void visitInsn(final int opcode);
    public void visitIntInsn(final int opcode, final int operand);
    public void visitVarInsn(final int opcode, final int var);
    public void visitTypeInsn(final int opcode, final String type);
    public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor);
    public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor,
                                final boolean isInterface);
    public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle,
                                       final Object... bootstrapMethodArguments);
    public void visitJumpInsn(final int opcode, final Label label);
    public void visitLabel(final Label label);
    public void visitLdcInsn(final Object value);
    public void visitIincInsn(final int var, final int increment);
    public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels);
    public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels);
    public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions);

    public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type);

    public void visitMaxs(final int maxStack, final int maxLocals);
    public void visitEnd();

    // ......
}

虽然有这么多方法,其实主要关心的核心方法已经方法调用顺序,因为在方法体当中需要声明变量,调用方法,传形参。

(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
    visitFrame |
    visitXxxInsn |
    visitLabel |
    visitInsnAnnotation |
    visitTryCatchBlock |
    visitTryCatchAnnotation |
    visitLocalVariable |
    visitLocalVariableAnnotation |
    visitLineNumber
)*
visitMaxs
]
visitEnd

方法调用顺序如下:

  • 第一步,调用 visitCode() 方法,调用一次。
  • 第二步,调用 visitXxxInsn() 方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
  • 第三步,调用 visitMaxs() 方法,调用一次。
  • 第四步,调用 visitEnd() 方法,调用一次。

例子:

// 构建方法头
methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
// 方法体...
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello ASM");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(RETURN);
// 方法体结束...
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();

ClassWriter

ClassWriter 继承了 ClassVisitor ,本质也就拥有几个visitxxx方法,不同的是多了toByteArray(),用于将设置好的vist()输出

public class ClassWriter extends ClassVisitor {
    /* A flag to automatically compute the maximum stack size and the maximum number of local variables of methods. */
    public static final int COMPUTE_MAXS = 1;
    /* A flag to automatically compute the stack map frames of methods from scratch. */
    public static final int COMPUTE_FRAMES = 2;

    // flags option can be used to modify the default behavior of this class.
    // Must be zero or more of COMPUTE_MAXS and COMPUTE_FRAMES.
    public ClassWriter(final int flags) {
        this(null, flags);
    }
}

flags可选值

  • 0:ASM 不会自动计算 max stacks 和 max locals,也不会自动计算 stack map frames。
  • ClassWriter.COMPUTE_MAXS:ASM 会自动计算 max stacks 和 max locals,但不会自动计算 stack map frames。
  • ClassWriter.COMPUTE_FRAMES:ASM 会自动计算 max stacks 和 max locals,也会自动计算 stack map frames。
  • Stack Map Frames:当前栈的状态以及局部变量表的状态
  • Max Stacks: 操作数栈大小
  • Max Locals:最大局部变量大小

ClassWriter作用是将调用后的visit()输出为byte[]数组

ClassReader

用于读取class文件

构造方法 最常用的:

  • ClassReader(final byte[] classFile)
  • ClassReader(final String className)
public ClassReader(final byte[] classFile) {
    this(classFile, 0, classFile.length);
  } 
public ClassReader(
      final byte[] classFileBuffer,
      final int classFileOffset,
      final int classFileLength) { // NOPMD(UnusedFormalParameter) used for backward compatibility.
    this(classFileBuffer, classFileOffset, /* checkClassVersion = */ true);
  }

public ClassReader(final String className) throws IOException {
    this(
        readStream(
            ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));
  }
public ClassReader(final InputStream inputStream) throws IOException {
    this(readStream(inputStream, false));
  }

常用方法: getxxx()用于获取class相关信息

public class ClassReader {
    public int getAccess() {
        return readUnsignedShort(header);
    }

    public String getClassName() {
        // this_class is just after the access_flags field (using 2 bytes).
        return readClass(header + 2, new char[maxStringLength]);
    }

    public String getSuperName() {
        // super_class is after the access_flags and this_class fields (2 bytes each).
        return readClass(header + 4, new char[maxStringLength]);
    }

    public String[] getInterfaces() {
        // interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each).
        int currentOffset = header + 6;
        int interfacesCount = readUnsignedShort(currentOffset);
        String[] interfaces = new String[interfacesCount];
        if (interfacesCount > 0) {
            char[] charBuffer = new char[maxStringLength];
            for (int i = 0; i < interfacesCount; ++i) {
                currentOffset += 2;
                interfaces[i] = readClass(currentOffset, charBuffer);
            }
        }
        return interfaces;
    }
}

accept() 接收一个 ClassVisitor 类型的参数,因此 accept() 方法是将 ClassReader 和 ClassVisitor 进行连接的“桥梁”。accept() 方法的代码逻辑就是按照一定的顺序来调用 ClassVisitor 当中的 visitXxx() 方法。

public class ClassReader {
    // A flag to skip the Code attributes.
    public static final int SKIP_CODE = 1;

    // A flag to skip the SourceFile, SourceDebugExtension,
    // LocalVariableTable, LocalVariableTypeTable,
    // LineNumberTable and MethodParameters attributes.
    public static final int SKIP_DEBUG = 2;

    // A flag to skip the StackMap and StackMapTable attributes.
    public static final int SKIP_FRAMES = 4;

    // A flag to expand the stack map frames.
    public static final int EXPAND_FRAMES = 8;


    public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
        accept(classVisitor, new Attribute[0], parsingOptions);
    }

    public void accept(
        final ClassVisitor classVisitor,
        final Attribute[] attributePrototypes,
        final int parsingOptions) {
       /*
       ...
       */
    }

}

parsingOptions 参数

在 ClassReader 类当中,accept() 方法接收一个 int 类型的 parsingOptions 参数。

public void accept(final ClassVisitor classVisitor, final int parsingOptions)

parsingOptions 参数可以选取的值有以下 5 个:

  • 0
  • ClassReader.SKIP_CODE
  • ClassReader.SKIP_DEBUG
  • ClassReader.SKIP_FRAMES
  • ClassReader.EXPAND_FRAMES

推荐使用:

  • 在调用 ClassReader.accept() 方法时,其中的 parsingOptions 参数,推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES。
  • 在创建 ClassWriter 对象时,其中的 flags 参数,推荐使用 ClassWriter.COMPUTE_FRAMES。

示例代码如下:

ClassReader cr = new ClassReader(bytes);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

为什么我们推荐使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 呢?因为使用这样的一个值,可以生成最少的 ASM 代码,但是又能实现完整的功能。

  • 0:会生成所有的 ASM 代码,包括调试信息、frame 信息和代码信息。
  • ClassReader.SKIP_CODE:会忽略代码信息,例如,会忽略对于 MethodVisitor.visitXxxInsn() 方法的调用。
  • ClassReader.SKIP_DEBUG:会忽略调试信息,例如,会忽略对于 MethodVisitor.visitParameter()、MethodVisitor.visitLineNumber() 和 MethodVisitor.visitLocalVariable() 等方法的调用。
  • ClassReader.SKIP_FRAMES:会忽略 frame 信息,例如,会忽略对于 MethodVisitor.visitFrame() 方法的调用。
  • ClassReader.EXPAND_FRAMES:会对 frame 信息进行扩展,例如,会对 MethodVisitor.visitFrame() 方法的参数有影响。

简而言之,使用 ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES 的目的是功能完整、代码少、复杂度低,

  • 不使用 ClassReader.SKIP_CODE,使代码的功能保持完整。
  • 使用 ClassReader.SKIP_DEBUG,减少不必要的调试信息,会使代码量减少。
  • 使用 ClassReader.SKIP_FRAMES,降低代码的复杂度
  • 不使用 ClassReader.EXPAND_FRAMES,降低代码的复杂度

说这么多不如看俩例子: 这是输出class字节码,其中选择了ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG

public static void print(String className){
        int parsingOptions = ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG;
        Printer printer = new ASMifier();
        PrintWriter printWriter = new PrintWriter(System.out, true);
        TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
        try {
            new ClassReader(className).accept(traceClassVisitor, parsingOptions);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

输出 image.png

去掉ClassReader.SKIP_DEBUG

public static void print(String className){
        int parsingOptions = ClassReader.SKIP_FRAMES;
        Printer printer = new ASMifier();
        PrintWriter printWriter = new PrintWriter(System.out, true);
        TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, printer, printWriter);
        try {
            new ClassReader(className).accept(traceClassVisitor, parsingOptions);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

image.png 平日用打印class的方式写字节码则用 ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG

accpet原理

修改类的信息

1.ClassReader 读取类信息 2.调用visitxxx填充字节码 3.ClassWriter输出byte[]

目标 使 HelloWorld 实现接口

public class HelloWorld{
   
    public HelloWorld() {
    }

    public void hello() {
        System.out.println("Hello ASM");
    }
}

如何实现 ClassReader.accept()会接收一个ClassVisitor并且调用该类的visitor()构造class。因此只需要创建类继承ClassVisitor并在visitor中修改类的信息即可 核心代码

public class ClassUpdateInfoVisitor extends ClassVisitor {

    private List<String> interfaces = new ArrayList<>();

    public void addInterfaces(Class... interfaces) {
        for (Class anInterface : interfaces) {
            this.interfaces.add(ClassNameTrans.trans(anInterface.getName()));
        }
    }
    
    public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
        super(api, cw);
    }
    
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, getInterfaces());
    }
    
    public String[] getInterfaces() {
        return interfaces.toArray(new String[interfaces.size()]);
    }
}

全部代码

public static byte[] update() throws Exception {
        // (1) 创建 ClassWriter 对象
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        // (2) 调用 visitXxx() 方法
        cw.visit(V1_8, ACC_PUBLIC , "org/xhy/HelloWorld",
                null, "java/lang/Object", new String[]{});
        MethodVisitor methodVisitor;
        {
            methodVisitor = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
            methodVisitor.visitCode();
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitLdcInsn("Hello ASM");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }

ClassNameTrans 将java.lang.Object转换java/lang/Object

public class ClassNameTrans {

    public static String trans(String name){
        return name.replace(".","/");
    }
}

添加字段

目标 使 HelloWorld 中添加String name 字段 如何实现 visitField是用于操作字段的 access:访问控制修饰符 name:变量名 descriptor:数据类型 signature:泛型 value:常量

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

核心代码 通过在visitEnd()进行添加字段

为什么不在visitField()添加? 因为这时的操作是在遍历所有的字段,可以在此对遍历到的字段进行操作,而并非添加行为

FieldInfo 只是将字段信息封装了一层,好进行存储

public class ClassUpdateInfoVisitor extends ClassVisitor {


    private List<FieldInfo> fieldInfos = new ArrayList<>();

    public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
        super(api, cw);
    }

    @Override
    public void visitEnd() {
        if(fieldInfos.size() > 0){
            for (FieldInfo fieldInfo : fieldInfos) {
                super.visitField(fieldInfo.getAccess(),fieldInfo.getName(),fieldInfo.getDescriptor(),fieldInfo.getSignature(),fieldInfo.getValue());
            }
        }
        super.visitEnd();
    }

    public void addFieldInfos(FieldInfo... fieldInfos) {
        for (FieldInfo fieldInfo : fieldInfos) {
            this.fieldInfos.add(fieldInfo);
        }
    }
}

删除字段

目标 删除HellodWorld中的xhy字段

如何实现 visit的工作原理: 依次调用visit进行传输,如果中途返回null,则数据终止 核心代码

 public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {

        if (delName.equales(name)){
            return null;
        }

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

实际代码 其中通过封装设计提高程序优雅 小tips:将不相关的代码删除提供可读性

public class ClassUpdateInfoVisitor extends ClassVisitor {

    private Set<String> delField = new HashSet<>();

    public ClassUpdateInfoVisitor(int api, ClassVisitor cw) {
        super(api, cw);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {

        if (containsDelField(name)){
            return null;
        }
        return super.visitField(access, name, descriptor, signature, value);
    }


    private boolean containsDelField(String name){
        return delField.contains(name);
    }

    public void addDelField(String... names){
        for (String name : names) {
            delField.add(name);

        }
    }
}

修改字段

和删除字段一样的操作,在visitFiled()判断字段如果存在则修改

添加方法

和字段一样

删除方法

和字段一样

修改方法

和字段一样