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:
{% highlight java %}
public class HelloWorld {
public void do1() {
System.out.println("Hello World!");
}
public void do2(String tosay){
System.out.println(tosay);
}
}
{% endhighlight %}
Inst0.java:
{% highlight java %}
public class Inst0 {
public static void main(String[] args) {
HelloWorld hw = new HelloWorld();
hw.do1();
hw.do2("Goodbye world");
}
}
{% endhighlight %}
$ 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:
{% highlight 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");
}
}
{% endhighlight %}
$ 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:
{% highlight java %}
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");
}
}
{% endhighlight %}
$ 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.