0

I'm trying to create some instrumentation tool. I want to track each object allocation. The simplest idea that came to my mind was to retransform Object constructor as each object calls it (I know that arrays are initialized differently).

I tried use java agent mechanism, but it caused java.lang.instrument.UnmodifiableClassException. Obviously java agent cannot transform Object class at it is unmodifable.

Then I tried use JDI, where my debugged program looked like:

public class First {

    public static int value = 1;

    public static void main(String... args) throws InterruptedException {
        while (true) {
            print();
            Thread.sleep(1000);
        }
    }

    public static void print() {
        System.out.println("Hello" + new Integer(value));

    }
}

And debugger did only this:

    VirtualMachine vm = new VMAcquirer().connect(8000);

    List<ReferenceType> referenceTypes1 = vm.classesByName("java.lang.Object");
    ReferenceType object = referenceTypes1.get(0);
    if (vm.canRedefineClasses()) {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get("java.lang.Object");
        CtConstructor constructor = ctClass.getConstructors()[0];
        constructor.insertAfter("First.value += 1;");

        HashMap<ReferenceType, byte[]> redefine = new HashMap<>();
        redefine.put(object, ctClass.toBytecode());
        ctClass.writeFile();

        vm.redefineClasses(redefine);
    }
    vm.resume();

After that target program exits with message:

ERROR: JDWP Transport dt_socket failed to initialize, OUT_OF_MEMORY(110)

Exception: java.lang.StackOverflowError thrown from the UncaughtExceptionHandler in thread "main"

Do I do something wrong here? Is it possible to transform Object class that way? I know about JVMTI but I wanted avoid C code, so is there any other way that does not require native code?

DISCLAIMER I'm aware of some similar questions already asked here (e.g. Hacking into java.lang.Object: calling custom external class crashes JVM or JDI, Java Byte code instrumentation and Java agents (JWDP, JVMTI)), but they doesn't explain to me everything.

---EDIT---

Transformed Object class looks like this:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package java.lang;

import jdk.internal.HotSpotIntrinsicCandidate;

public class Object {
    private static native void registerNatives();

    @HotSpotIntrinsicCandidate
    public Object() {
        Object var2 = null;
        ++First.value;
    }

    @HotSpotIntrinsicCandidate
    public final native Class<?> getClass();

    @HotSpotIntrinsicCandidate
    public native int hashCode();

    public boolean equals(Object obj) {
        return this == obj;
    }

    @HotSpotIntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

    public String toString() {
        return this.getClass().getName() + "@" + 
        Integer.toHexString(this.hashCode());
    }

    @HotSpotIntrinsicCandidate
    public final native void notify();

    @HotSpotIntrinsicCandidate
    public final native void notifyAll();

    public final void wait() throws InterruptedException {
        this.wait(0L);
    }

    public final native void wait(long var1) throws InterruptedException;

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0L) {
            throw new IllegalArgumentException("timeout value is negative");
        } else if (nanos >= 0 && nanos <= 999999) {
            if (nanos > 0) {
                ++timeout;
            }

            this.wait(timeout);
        } else {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
    }

    /** @deprecated */
    @Deprecated(
        since = "9"
    )
    protected void finalize() throws Throwable {
    }

    static {
        registerNatives();
    }
}

I did also more tests and if I put something like int i = 1; int j = 2 + i; it works.

I also tried modify Integer constructor - this caused another exception:

Exception in thread "main" java.lang.NoClassDefFoundError: First
    at java.base/java.lang.Integer.<init>(Integer.java:1075)
    at First.print(First.java:13)
    at First.main(First.java:7)

Class was successfully transformed, but at runtime there was a problem with linking to the class. I don't know if it is linked somehow. Maybe something similar happens with Object when some internal stuff tries to create new instance.

I was curious about Object var2 = null; line. Javaassist always puts ACONST_NULL bytecode, but it is not the cause of the problem.

---EDIT2---

I tried to transform another Object method. Transformation went succcessfully, but again error occured at runtime:

Exception in thread "main" java.lang.NoClassDefFoundError: First
    at java.base/java.lang.Object.toString(Object.java:246)
    at First.print(First.java:15)
    at First.main(First.java:7)

For me it looks like the real problem is with NoClassDefFoundError. My assumption is it somehow related with classloader system in java (?). Could I somehow avoid such error? I don't know much about classloaders :/

  • Did you check what Javassist did with the code? This lack of control is the reason why I strongly recommend not to use Javassist for Instrumentation, or at least not to use these convenience functions operating at the source code level. My guess is that Javassist failed to realize that you are operating at `java.lang.Object`, the only class whose constructor does not invoke another constructor, so it conveniently inserted a call to `java.lang.Object`’s constructor for you, resulting in a recursion. But that’s just a guess. You have to inspect the resulting byte code to find out. – Holger May 24 '18 at 14:13
  • Javaassist generated something like this `@HotSpotIntrinsicCandidate public Object() { Object var2 = null; ++First.value; }` It is generated by intellij from .class format. – Marek Tokarski May 30 '18 at 20:12
  • I added my new findings. I think`NoClassDefFoundError` is the main reason, but I don't know how fix this – Marek Tokarski May 30 '18 at 21:38
  • 1
    `Object` is loaded via bootstrap class loader and can’t access classes loaded by other class loaders. You have to add classes to the bootstrap loading path to make them accessible to the object class. E.g. when using the Instrumentation API, [`appendToBootstrapClassLoaderSearch`](https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/Instrumentation.html#appendToBootstrapClassLoaderSearch-java.util.jar.JarFile-) would be the way to go. I suppose, JDI offers something similar. Of course, when this class is also reachable via the application class path, you must do it before it is loaded – Holger May 31 '18 at 06:10
  • Thanks for help! I know now how to redefine `Object` class by JDI, however I would like to transform this class by javaagent. When using `ClassFileTransformer` I get `UnmodifableClassException`. Could I somehow retransform class other way? (perhaps calling JDI/JVMTI inside javaagent) – Marek Tokarski May 31 '18 at 11:44
  • 1
    Actually, I don’t know of a restriction for redefining `Object`. Did you specify the `Can-Redefine-Classes` manifest entry for the agent’s jar? – Holger May 31 '18 at 12:22
  • I have specified `Can-Retransform-Classes` in manifest as I use `Instrumentation.retransformClasses()`. I'm just dumbass and my code is garbage at the moment - I didn't try to retransform `Object` class but `[[Object` - `Object[][]` array... Array classes are generated at runtime and as far as I know they cannot be transformed and this is root of my problem :p With `Object` transformation is invoked. Many thanks @Holger for your help – Marek Tokarski May 31 '18 at 14:54

0 Answers0