Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add static final field with initializer using ASM?

I want to add static final field into .class file using ASM, and the source file is

public class Example {

    public Example(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }

    private final int code;

}

and generated class which is decompiled should be like this:

public class Example {

    public static final Example FIRST = new Example(1);

    public static final Example SECOND = new Example(2);

    public Example(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }

    private final int code;

}

And as a conclusion, I want to add FIRST and SECOND constants to .class file using ASM, how can I do?

like image 889
Christian Gann Avatar asked Jun 16 '12 16:06

Christian Gann


1 Answers

This answer shows how it can be done using the visitor api of ASM (see section 2.2 of the ASM 4.0 A Java bytecode engineering library on the ASM homepage) because it's the most familiar api for me. ASM also has an object model api (see Part II in the same document) variant which might be generally easier to use in this case. The object model is assumably a bit slower since it constructs a tree of the whole class file in memory, but if there are only a small amount of classes that need transforming the performance hit should be negligible.

When creating static final fields whose values aren't constants (like numbers), their initialization actually go to a "static initializer block". Thus, your second (transformed) code listing is equivalent to the following java code:

public class Example {

  public static final Example FIRST;

  public static final Example SECOND;

  static {
    FIRST = new Example(1);
    SECOND = new Example(2);
  }

  ...
}

In a java file you are allowed to have multiple such static { ... } blocks, while in class files there can only be one. The java compiler automatically merges multiple static blocks into one to meet this requirement. When manipulating bytecode this means if there is no static block from before then we create a new one, while if there already exists a static block we need to prepend our code to the beginning of the existing one (prepending is easier than appending).

With ASM, the static block looks like a static method with the special name <clinit>, just like constructors look like methods with the special name <init>.

When using the visitor api, the way to know whether a method has been defined from before is to listen to all the visitMethod() calls and check the method name in each call. After all methods have been visited the visitEnd() method is called, so if no method has been visited by then, we know we need to create a new method.

Assuming we have a the orignal class in byte[] format, the requested transformation can be done like this:

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

public static byte[] transform(byte[] origClassData) throws Exception {
  ClassReader cr = new ClassReader(origClassData);
  final ClassWriter cw = new ClassWriter(cr, Opcodes.ASM4);

  // add the static final fields 
  cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "FIRST", "LExample;", null, null).visitEnd();
  cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "SECOND", "LExample;", null, null).visitEnd();

  // wrap the ClassWriter with a ClassVisitor that adds the static block to
  // initialize the above fields
  ClassVisitor cv = new ClassVisitor(ASM4, cw) {
    boolean visitedStaticBlock = false;

    class StaticBlockMethodVisitor extends MethodVisitor {
      StaticBlockMethodVisitor(MethodVisitor mv) {
        super(ASM4, mv);
      }
      public void visitCode() {
        super.visitCode();

        // here we do what the static block in the java code
        // above does i.e. initialize the FIRST and SECOND
        // fields

        // create first instance
        super.visitTypeInsn(NEW, "Example");
        super.visitInsn(DUP);
        super.visitInsn(ICONST_1); // pass argument 1 to constructor
        super.visitMethodInsn(INVOKESPECIAL, "Example", "<init>", "(I)V");
        // store it in the field
        super.visitFieldInsn(PUTSTATIC, "Example", "FIRST", "LExample;");

        // create second instance
        super.visitTypeInsn(NEW, "Example");
        super.visitInsn(DUP);
        super.visitInsn(ICONST_2); // pass argument 2 to constructor
        super.visitMethodInsn(INVOKESPECIAL, "Example", "<init>", "(I)V");
        super.visitFieldInsn(PUTSTATIC, "Example", "SECOND", "LExample;");

        // NOTE: remember not to put a RETURN instruction
        // here, since execution should continue
      }

      public void visitMaxs(int maxStack, int maxLocals) {
        // The values 3 and 0 come from the fact that our instance
        // creation uses 3 stack slots to construct the instances
        // above and 0 local variables.
        final int ourMaxStack = 3;
        final int ourMaxLocals = 0;

        // now, instead of just passing original or our own
        // visitMaxs numbers to super, we instead calculate
        // the maximum values for both.
        super.visitMaxs(Math.max(ourMaxStack, maxStack), Math.max(ourMaxLocals, maxLocals));
      }
    }

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
      if (cv == null) {
        return null;
      }
      MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
      if ("<clinit>".equals(name) && !visitedStaticBlock) {
        visitedStaticBlock = true;
        return new StaticBlockMethodVisitor(mv);
      } else {
        return mv;
      }
    }

    public void visitEnd() {
      // All methods visited. If static block was not
      // encountered, add a new one.
      if (!visitedStaticBlock) {
        // Create an empty static block and let our method
        // visitor modify it the same way it modifies an
        // existing static block
        MethodVisitor mv = super.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
        mv = new StaticBlockMethodVisitor(mv);
        mv.visitCode();
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
      }
      super.visitEnd();
    }
  };

  // feed the original class to the wrapped ClassVisitor
  cr.accept(cv, 0);

  // produce the modified class
  byte[] newClassData = cw.toByteArray();
  return newClassData;
}

Since your question did not give further indications of what exactly your end goal is, I decided to go with a basic example hard-coded to work for your Example class case. Should you want to create instances of the class being transformed you'll have to change all strings containing "Example" above to use the full class name of the class actually being transformed instead. Or if you specifically want two instances of the Example class in every transformed class, the above example works as is.

like image 115
Jonas Berlin Avatar answered Sep 20 '22 14:09

Jonas Berlin