Search code examples
javabytecodejava-bytecode-asmjavaagents

How to change static variable value using ASM?


I started learning Java Agent few days ago. But documentation is not very good and beginners like me struggling to understand the basics. I created a basic multiplier class and export it to runnable jar using eclipse. Here is the code snippet.

Main jar file:

public class Multiplier {

    public static void main(String[] args) {
        int x = 10;
            int y = 25;
            int z = x * y;

            System.out.println("Multiply of x*y = " + z);
    }
}

Bytecode for above class

Now I want to manipulate the value of x from an agent. I tried to create the Agent class like this

Agent:

package myagent;

import org.objectweb.asm.*;
import java.lang.instrument.*;

public class Agent {

    public static void premain(final String agentArg, final Instrumentation inst) {
        System.out.println("Agent Started");        
        int x_modified = 5;

        //Now How to push the new value (x_modified) to the multiplier class? 
        //I know I have to use ASM but can't figure it out how to do it.
        //Result should be 125

    }

}

My Question

How do I set the value of x from agent class to multiplier class using ASM? Result should be 125.


Solution

  • The first thing, your agent has to do, is registering a ClassFileTransformer. The first thing, the class file transformer should do in its transform method, is checking the arguments to find out whether the current request is about the class we’re interested in, to return immediately if not.

    If we are at the class we want to transform, we have to process the incoming class file bytes to return a new byte array. You can use ASM’s ClassReader to process to incoming bytes and chain it to a ClassWriter to produce a new array:

    import java.lang.instrument.*;
    import java.security.ProtectionDomain;
    import org.objectweb.asm.*;
    
    public class ExampleAgent implements ClassFileTransformer {
        private static final String TRANSFORM_CLASS = "Multiplier";
        private static final String TRANSFORM_METHOD_NAME = "main";
        private static final String TRANSFORM_METHOD_DESC = "([Ljava/lang/String;)V";
    
        public static void premain(String arg, Instrumentation instrumentation) {
            instrumentation.addTransformer(new ExampleAgent());
        }
    
        public byte[] transform(ClassLoader loader, String className, Class<?> cl,
                                ProtectionDomain pd, byte[] classfileBuffer) {
            if(!TRANSFORM_CLASS.equals(className)) return null;
    
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(cr, 0);
            cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String desc,
                                                 String signature, String[] exceptions) {
                    MethodVisitor mv = super.visitMethod(
                                           access, name, desc, signature, exceptions);
                    if(name.equals(TRANSFORM_METHOD_NAME)
                    && desc.equals(TRANSFORM_METHOD_DESC)) {
                        return new MethodVisitor(Opcodes.ASM5, mv) {
                            @Override
                            public void visitIntInsn(int opcode, int operand) {
                                if(opcode == Opcodes.BIPUSH && operand == 10) operand = 5;
                                super.visitIntInsn(opcode, operand);
                            }
                        };
                    }
                    return mv;
                }
            }, 0);
            return cw.toByteArray();
        }
    }
    

    Note that by passing the ClassWriter to our custom ClassVisitor’s constructor and passing the MethodVisitor returned by the super.visitMethod invocation to our MethodVisitor’s constructor, we enable a chaining that reproduces the original class by default; all methods we’re not overriding will delegate to the specified ClassWriter/MethodVisitor reproducing the encountered artifact. Compare with the tutorial about ASM’s event model.

    The example above enables an optimization by also passing the ClassReader instance to the ClassWriter’s constructor. This improves the efficiency of instrumenting a class when only making small changes, like we do here.

    The crucial part is overriding visitMethod to return our custom MethodVisitor when we are at the “hot” method and overriding visitIntInsn to change the desired instruction. Note how these methods delegate to the super calls when not altering the behavior, just like the methods we didn’t override.