top of page

Unlocking the Power of Bytecode Instrumentation for Advanced Software Analysis

We use Bytecode instrumentation in software analysis, particularly in programming languages compiling to bytecode, such as Java or .Net. 


Bytecode instrumentation means changing the bytecode before the runtime executes it. Profiling diagnostics and monitoring tools take advantage of this possibility. To measure the execution time of these methods, the bytecode is modified so that additional code can be inserted at both the method's beginning and end. When the method is executed, the additional instrumented code is executed to measure its execution time. This measured time can then be sent in for analysis.

 

How do we change the bytecode?


  1. Static before the JVM loads the class. In this scenario, we add a post-build step to change the bytecode before the virtual machine loads the classes.

  2. When loading the class into JVM: Another option is to use JVMTI to modify the bytecode before the virtual machine loads the class without changing the class file. 

  3. Dynamic instrumentation of already loaded classes: Java provides an instrumentation interface to modify already loaded classes, allowing dynamic modification of the bytecode without restarting the application.


What are the use cases for bytecode instrumentation?


Information gathering: We inject code (instrumentation code) into the program's bytecode for gathering information or performing specific tasks during the program's execution.


Transformation: We use bytecode instrumentation to add logging, monitor resource usage, enforce security policies, or apply optimizations dynamically at runtime.


Dynamic Analysis: We use bytecode instrumentation for dynamic analysis of the program, meaning that the program's behavior is observed and modified while running without requiring access to the source code.



Sample Programm to demonstrate the Bytecode instrumentation


public class SampleProgram {
    public static void main(String[] args) {
        System.out.println("Hello, Performetriks!");
        int result = add(5, 3);
        System.out.println("Result: " + result);
    }

    public static int add(int a, int b) {
        return a + b;
    }
}

Now, let's write bytecode instrumentation code to instrument this program. For this example, let's say we want to print a message every time the add method is called:


import java.lang.instrument.*;
import java.security.*;

public class AddMethodInstrumentationAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new AddMethodTransformer());
    }
}

class AddMethodTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        String targetClassName = "SampleProgram";
        if (className.equals(targetClassName)) {
            System.out.println("Instrumenting class: " + className);
            return instrumentClass(classfileBuffer);
        }
        return classfileBuffer;
    }

    private byte[] instrumentClass(byte[] classfileBuffer) {
        try {
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            ClassVisitor visitor = new ClassVisitor(Opcodes.ASM7, writer) {
                @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("add")) {
                        return new AddMethodVisitor(mv);
                    }
                    return mv;
                }
            };
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

class AddMethodVisitor extends MethodVisitor {
    public AddMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM7, mv);
    }

    @Override
    public void visitCode() {
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("Add method called!");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
}

  • AddMethodInstrumentationAgent is a Java agent that is invoked before the JVM starts. It attaches a ClassFileTransformer to the JVM.

  • AddMethodTransformer is a ClassFileTransformer that identifies the target class (SampleProgram) for instrumentation and calls instrumentClass to perform bytecode manipulation.

  • AddMethodVisitor is a MethodVisitor that visits methods in the bytecode. It overrides visitCode to insert bytecode instructions to print a message when the add method is called.


Compile the Code

javac -cp asm-<version>.jar:. *.java
jar cmf MANIFEST.MF instrumentation.jar *.class

Run the Code

java -javaagent:instrumentation.jar SampleProgram

Check the output

Instrumenting class: SampleProgram
Hello, Performetriks!
Add method called!
Result: 8


Bytecode instrumentation is used in various development and analysis tools, including profilers, debuggers, code coverage tools, security scanners, and dynamic analysis frameworks. It provides developers with valuable insights into the runtime behavior of their software and enables them to diagnose issues, optimize performance, and enhance security.


Keep up the great work! Happy Performance Engineering!


23 views0 comments
bottom of page