Thursday, July 26, 2007

Having fun with java.lang.Object

java.lang.Object is the mother of all objects in the Java world. While being the core of Java, it is not a native object, but rather stored as compiled class file that is located in $JAVA_HOME/jre/lib/rt.jar. This means that if you try overwriting the file at java/lang/Object.class within the jar file, your Object will get loaded instead of the original Object definition.

The idea here is to overwrite java.lang.Object which will give you first access to any other object before anybody else. However, it's probably not a wise idea to modify the original jar file itself, as Java will break if you make some bad changes to rt.jar. I recommend that you copy rt.jar instead, and tell java to use it instead of the original by using the -Xbootclasspath flag. For this purpose, I'd assume that you've named your new jar file as modified-rt.jar.

Now look for src.zip in your $JAVA_HOME directory and extract java/lang/Object.java. Compile that and add the class file into your new jar:


$ javac java/lang/Object.java
$ jar uvf modified-rt.jar java/lang/Object.class


The compiled class won't be any different from the standard lang.Object within the java class. (Do a 'diff' on it if you like). A triva on the compiled java.lang.Object, is that javac actually treats it differently when it comes to compiling the constructor. If you run javap on it, you'll find out that it doesn't have an INVOKESPECIAL constructor call that is present for all other objects, an implicit constraint that is defined by the Java Machine Specification. It made sense, after all there is no other object that is its superclass.

To make sure that the new modified-rt.jar runs ok, write a test application to make sure that the JVM will run as normal. I'm going to use the following example class file, and use it illustrate what I'm going to do later, so you might want to do the same as well. Save the following file as ObjectFieldReflection.java:


import java.lang.reflect.*;

public class ObjectFieldReflection {
public static void main(String args[]) throws Exception {
Field[] f_a = Object.class.getDeclaredFields();
for (Field f : f_a) {
f.setAccessible(true);
System.out.println("Fieldname="+f.getName()+" value="+f.get(null));
}
}
}


The above application will try to cycle through all the fields of java.lang.Object and print out the value within the fields. Since the default java.lang.Object doesn't have any fields at all, there's really nothing to print out, and the application will just terminate as normal:


$ java -Xbootclasspath:rt.jar ObjectFieldReflection


One of the things that may be fun to do is to count the number of objects that is instantiated by Java over the lifecycle of the application. An easy way to do that is to create a static field in java.lang.Object, and use the default constructor to count every time an object is created. Edit the java/lang/Object.java file, and add the following static field declaration and a default constructor so that it looks like this:


public static int createdCount = 0;

public Object() {
createdCount++;
}


Recompile and re-add that into the jar file, and run it again. Now you should be able to find out the number of objects created in the lifecycle of your application, which the output looks something like this:


$ java -Xbootclasspath:rt.jar ObjectFieldReflection
Fieldname=createdCount value=1200


It tells you that 1200 objects have been created just for the example simple application to run. Imagine how many more classes are created for a large application? Let's say you want to find out about the number of objects garbaged in the lifecycle of your application instead. Modify Object.java like this:


public static int garbagedCount = 0;

public void finalize() {
garbagedCount++;
}


finalize() is a special method that all Java object calls before being garbaged collected. It is similar to destructors in C++, although there are many subtle differences between them in reality. If you tried running with the new code, the JVM crashes with a core dump:


#
# An unexpected error has been detected by HotSpot Virtual Machine:
#
# SIGSEGV (0xb) at pc=0xb79df93f, pid=25096, tid=3085408944
#
# Java VM: Java HotSpot(TM) Client VM (1.5.0_08-b03 mixed mode)
# Problematic frame:
# V [libjvm.so+0x30793f]
#
# An error report file with more information is saved as hs_err_pid25096.log
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#
Aborted


So what's going on with that? Your guess is as good as mine, but here's what I think: Java has to rely on the Garbage Collector(GC) to handle memory collection and because the GC uses complex algorithms to make reclaiming memory efficient, it is probably expecting that java.lang.Object is by default the easily reclaimed sort. But since we're putting code into finalize(), it actually changes that assumption, since all objects have to be individually 'collected', thus requiring the less efficient 'Mark and Sweep' GC method, something that the JVM is not expecting, hence crashing it.

So while tampering with Java core class files may be a nice, unintrusive way of collecting information without having to modify existing applications, or applications which you do not have the source to, be mindful that sometimes it may not be worth the trouble when it comes to dealing with the unintended consequences that arise, especially when coded in a manner that contravene the rules of the Java specification.

4 comments:

Дмитрий said...

Hi, Vince!

I just found this article on google, while looking for possible resolution of the issue I recently encountered. And since I see you were doing things close to what I did, I decided to ask for your advice, maybe you can help me.

I tried to hack java.lang.object:)

Specifically, what I need, is to make its wait() & notify() methods non-final. Yes, I really need to override them.

When I make notify() & notifyAll(0 non-final and substitute the system class with my hacked one, it's ok, it works, it lets me override the methods. But if I try to make wait() non-final, java refuses to start, crashing with unhandled native exception on startup. I tried both recompiling Object from source and even patch specific access flag bytes with hex editor (in strive to make as minimal changes as possible). But the result is the same, JVM crashes.

I'm using Sun JVM 1.6 on windows.

Do you have any ideas what can I try to resolve the issue?


Thanks a lot.

Vincent Liu said...

It's pretty cool to learn that somebody else is doing some hacking within Java itself as well!

It's interesting that you are actually trying to override the wait() and notify() methods, which as I imagine, that on the Java source level, you'll have no problem in recompiling: there's nothing in the language specification that mandates notify or wait to be treated specially in any way.

But in many ways, java.lang.Object is special, in terms of (not) adhering strictly to the JVM specification, and unlike native JNI calls, both notify() and wait() are hardwired into the JVM directly, which is may be the likely cause of the issue.

While I don't know exactly what is the cause of it, I'll proffer a guess that from the unhandle native exception that you've received, your custom 'function' for wait() being non-native and non-final, gets written to Java's table of virtual function (rather than the place where non-final unoverride-able functions are stored), and leaving the existing non-virtual function unlinked with anything and failing the JVM's internal assertion.

In this case, I don't think you can omit the 'final' access modifier. But if it allows you to write your own method in place of native without crashing, a possible fix might be to direct wait() to wait0() shell method that you can override, and write your applications to override wait0() instead?

Anyway I'm really curious on your need for having to override notify() and wait() itself, given that by overwriting it, you've effectively lost all your native synchronization/thread coherency capabilities within Java, something that cannot be replaced by pure Java code alone.

My experience is that, unless you're trying to link these calls to some third party threading/synchronization libraries, most likely, your Java apps running with such modifications are guaranteed to not maintain thread coherency, so yeah, do satisfy my curiosity on how you are actually using these overrides for?

Good luck!

Anonymous said...

Hello, Vincent:
Have you met with the problem that the jvm can not be loaded again.
I followed the steps specified above, but seems the shell would tell me the class format error, and through one exception concerning magic number.

Anonymous said...

Pretty cool thanks for this post. I want to go one step further in dependecy injection. Instead of injector.getInstance I want to hook on every new object created having a certian annontation. And hacking the Object constructor is doing the job very well :-)

Post a Comment