Java Dynamic Instrumentation #1



        Instrumentation is the process of injecting code into a compiled program. In Java, this can be done statically and dynamically. Using static intrumentation, a class’ bytecode is modified and saved to disk; permanently modifying the class. With dynamic instrumentation, the class’ bytecode is modified in memory right before being loaded.

        This is the first in a series of several ways to go about doing dynamic instrumentation in Java. I will be making use of the Javassist bytecode manipulation library for this series. In this first post, I will be going over Java dynamic instrumentation used within the main program. First, you will need Java installed (of course) and the Javassist jar file (I am using version 3.15). While the Javassist API documentation will provide a thorough description of the classes and functions involved, I will be covering the basics.

The following contains no instrumentation: HelloWorld.java:

public class HelloWorld {
	public void do1() {
		System.out.println("Hello World!");
	}

	public void do2(String tosay){
		System.out.println(tosay);
	}

}

Inst0.java:

public class Inst0 {
	public static void main(String[] args) {
		HelloWorld hw = new HelloWorld();
		hw.do1();
		hw.do2("Goodbye world");
	}
}
$ javac Inst0.java HelloWorld.java
$ java Inst0
Hello World!
Goodbye world

We are now going to add some instrumentation in to output some basic tracing.

Inst1.java:

import javassist.*;

public class Inst1 {
	public static void main(String[] args) throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass hw_ctc = pool.get("HelloWorld");
		CtMethod hw_ctm = hw_ctc.getDeclaredMethod("do1");
		hw_ctm.insertBefore("System.out.println(\"HelloWorld.do1() Start\");");
		hw_ctm.insertAfter("System.out.println(\"HelloWorld.do1() End\");");
		Class hw_class = hw_ctc.toClass();
		HelloWorld hw = (HelloWorld)hw_class.newInstance();
		hw.do1();
		hw.do2("Goodbye world");
	}
}
    
$ javac -classpath /path/to/javassist.jar: Inst1.java HelloWorld.java
$ java -cp /path/to/javassist.jar: Inst1
HelloWorld.do1() Start
Hello World!
HelloWorld.do1() End
Goodbye world

*Note: The ':'s are necessary.

        In this code, the classes to take note of are ClassPool, CtClass, and CtMethod. A ClassPool object contains CtClass objects. CtClass objects represent class files. CtMethod objects represent methods.         To modify a class, we must first obtain a reference to a CtClass object representing the class from a ClassPool object. pool.get("HelloWorld") returns a reference to the CtClass object for the HellowWorld class. To modify a class’ method, we must first get a reference to its corresponding CtMethod from the class’ CtClass instance. In the above code we do this by using hw_ctc.getDeclaredMethod("do1") to get the CtMethod of HelloWorld’s do1() method.         To add code to the beginning and end of a method we use the insertBefore() and insertAfter() methods of CtMethod. They allow us to place the corresponding bytecode of the string argument at the beginning and end (before any returns). It should be noted that the access of the bytecode is limited (see the API entry for more information) and any code that exceeds these limits will throw a CannotCompileException. The toClass() method of CtClass creates a java.lang.Class object from the CtClass and loads the class. The code will now use the modified class definition of HelloWorld as the regular one from this point on.white-space: -pre-wrap;         When we call hw.do1() we see that there are extra lines printed before and after the System.out.println("Hello World!") in the original do1().

        If we want to add this tracing to every method in a class, using getDeclaredMethod() for each method is not nearly as useful as getDeclaredMethods() is:

import javassist.*;

public class Inst2 {
	public static void main(String[] args) throws Exception {
		ClassPool pool = ClassPool.getDefault();
		CtClass hw_ctc = pool.get("HelloWorld");
		CtMethod[] hw_ctms = hw_ctc.getDeclaredMethods();
		for(int i=0; i<hw_ctms.length;i++){
			hw_ctms[i].insertBefore("System.out.println(\"HelloWorld." + hw_ctms[i].getName() + " Start\");");
			hw_ctms[i].insertAfter("System.out.println(\"HelloWorld." + hw_ctms[i].getName() +" End\");");
		}
		Class hw_class = hw_ctc.toClass();
		HelloWorld hw = (HelloWorld)hw_class.newInstance();
		hw.do1();
		hw.do2("Goodbye world");
	}
}
$ javac -classpath /path/to/javassist.jar: Inst2.java HelloWorld.java
$ java -cp /path/to/javassist.jar: Inst2
HelloWorld.do1 Start
Hello World!
HelloWorld.do1 End
HelloWorld.do2 Start
Goodbye world
HelloWorld.do2 End

        getDeclaredMethods() returns an array of references to all CtMethod objects in a CtClass object. We now loop through all of the methods in HellowWorld and apply the insertBefore() and insertAfter() to all of them.

        At this point we can add functionality to the methods in classes that doesn’t affect the flow of logic in the middle of the methods, such as tracing method calls. In the next post, I will go over more advanced and powerful forms class and method manipulation.