Jun 10, 2012

byte code instrumentation in java...

Hi!

Are you a
"low level",  developer? Have you ever disassembled a class byte code just to see what's under the hood? If not, I urge you to give it a try; IMO having at least basic knowledge about how the platform running your app works at low level may help us (developers) to write better code.
Since I joined db4o team I had the opportunity to play a lot with Mono.Cecil, a great .Net byte code manipulation lib, but I've been kind of lazy when it comes to the Java side (sure, we do employ byte code manipulation on Java code but, to be sincere, I've never touched that area of the code).
This has been slowly changing since I have joined another project at Versant (the company behind db4o) and this lead me to this post: last weekend I was debugging some Java class library code that I had no source code for1 and I wanted to check some parameter values.
Since I was alone in my house (wife and kids went to do some shopping with my mother in law :) I decided to try 2 related technologies in the Java world: Java agents and ASM library (you can read more about Java agents here and here)2.
Simply put, Java agents is a technology used to change class bytecode when it enters into the system, meaning, instead of doing the manipulation in the bytecode and saving it to the disk using java agents one can change the in memory representation of the class (without touching the actual bytes in the disk) when the JVM tries to resolve the class. If you use Sun JVM you can simply type the following command in the command line:
java -javaagent:jar-containing-agent-code.jar[=agent-args] ...
ASM is a library that allows developers to manipulate Java bytecode in a flexible, yet relatively easy way so it is a great match to use with Java agents (if you want more details I recommend to read this ASM tutorial).

Bellow you can find the code that I wrote for the agent 
(you can download a zipped eclipse project from here); it expects the class name and method which should be instrumented in the form className#methodName (if you omit the method name all methods will be instrumented):
package com.thinkingsoftware.diagnostics;

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

import org.objectweb.asm.*;

public class DumperClassTransformer implements ClassFileTransformer {

	private final String className;
	private final String methodName;

	public DumperClassTransformer(String... args) {
		this.className = args.length > 0 ? args[0] : null;
		this.methodName = args.length > 1 ? args[1] : null;
	}
	
	private static Map> wrapperMapping = new HashMap>();
	
	{
		wrapperMapping.put(Type.INT_TYPE.getDescriptor(), Integer.class);
		wrapperMapping.put(Type.FLOAT_TYPE.getDescriptor(), Float.class);
		wrapperMapping.put(Type.DOUBLE_TYPE.getDescriptor(), Double.class);
		wrapperMapping.put(Type.BOOLEAN_TYPE.getDescriptor(), Boolean.class);
	}

	@Override
	public byte[] transform(ClassLoader loader, final String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		if (className.startsWith("java") || className.startsWith("sun")) {
			return classfileBuffer;
		}
		
		if (this.className == null || className.equals(this.className)) {
			
			ClassReader cr = new ClassReader(classfileBuffer);
			ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
			
			ClassVisitor cv = new ClassVisitor(0, 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 (methodName != null && !name.equals(methodName)) {
						return mv;
					}
					
					try {
						String printlnStringMethodDesc = Type.getMethodDescriptor(System.err.getClass().getMethod("println", String.class));
						
						mv.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(System.class), "err", Type.getDescriptor(System.err.getClass()));
						mv.visitLdcInsn(name + " :");
						mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(System.err.getClass()), "println", printlnStringMethodDesc);
						
						Type[] args = Type.getArgumentTypes(desc);						
						for(int i = 0; i < args.length; i++) {
							
							if (args[i].getSort() == Type.ARRAY) { // not supported... :(
								continue;
							}
							
							mv.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(System.class), "err", Type.getDescriptor(System.err.getClass()));
							mv.visitInsn(Opcodes.DUP);
							
							mv.visitInsn(Opcodes.DUP);
							mv.visitLdcInsn("\t");
							mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(System.err.getClass()), "print", printlnStringMethodDesc);
							
							mv.visitIntInsn(Opcodes.BIPUSH, i);
							mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(System.err.getClass()), "print", "(I)V");
							
							mv.visitInsn(Opcodes.DUP);
							mv.visitLdcInsn(" = ");
							mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(System.err.getClass()), "print", printlnStringMethodDesc);							
							
							Class wrapperClass = wrapperMapping.get(args[i].getDescriptor());
							
							if (wrapperClass != null) {
								
								mv.visitVarInsn(args[i].getOpcode(Opcodes.ILOAD), i + 1);

								Type.getMethodDescriptor(Type.getType(wrapperClass), args[i]);
								mv.visitMethodInsn(
										Opcodes.INVOKESTATIC, 
										Type.getDescriptor(wrapperClass), 
										"valueOf", 
										Type.getMethodDescriptor(Type.getType(wrapperClass), args[i]));
							} else {
								mv.visitVarInsn(Opcodes.ALOAD, i + 1);
							}
							
							mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getType(Object.class).getDescriptor(), "toString", Type.getMethodDescriptor(Object.class.getMethod("toString")));
							mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(System.err.getClass()), "println", printlnStringMethodDesc);
						}						
					} catch (Exception ex) {
						ex.printStackTrace();
					}
					
					return mv;
				}
			};
			
			cr.accept(cv, 0);
				
			
			byte[] byteArray = cw.toByteArray();
			
			writeFile(className, byteArray);
			
			return byteArray;			
		}
		
		return classfileBuffer;
	}

	private void writeFile(String className, byte[] byteArray) {
		FileOutputStream f;
		try {
			String tempDir = System.getProperty("java.io.tmpdir");
			String outputPath = tempDir + className + "Inst.class";
			
			System.err.println();
			System.err.println("Instrumented class stored at: " + outputPath);
			System.err.println();
			System.err.println("You can look into it actuals bytecode with the following: ");
			System.err.println("javap -v -private -classpath " + tempDir + " " + className);
			System.err.println();
			
			f = new FileOutputStream(outputPath);
			f.write(byteArray, 0,  byteArray.length);
			f.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}


If you want to try it you simply compile it and run some Java application; you should get  an output looking like:
running my agent against the following Java application

public class TestAgent {

	public TestAgent(String msg, int n) {
		this.msg = msg;	
	}

	public static void main(String []args) {
		TestAgent t = new TestAgent("Hallo Welt", -1);
		String msg = t.getMessage("Hello World", 42, 11.0f, true, t);
	}
	
	private String getMessage(String msg, int i, float f, boolean b, Object o) {
		return msg;
	}
	
	public String toString() {
		return "OLA MUNDO";
	}
	
	private String msg;
}


Fell free to use it anyway you like but keep in mind that this code is far from being complete or bug free or production ready (for instance it does not handle arrays) so strange errors may happen if you find one of these not supported cases.

As a last comment, if you want to play with ASM I do recommend you to take a look in the
Bytecode outline plugin (but be careful to use the one in the previous link instead of the one in the Objectweb page since the later seems to have issues with newer Eclipse versions).

Hope you find this interesting!


Adriano



----

1 Of course I could either download it from the web - after all it is an open source project- or disassembled it, but I wouldn't loose the change to play with new toys and possibly learn something new - not to mention that I severely suffer from the NIH syndrome :)


2 Since I am in no way an expert in the subject, take every word with a grain of salt.

1 comment:

Balaramakrishna rachumallu said...

i always try to write such type of big codes but i failed too many times. really superb