Search code examples
javajvmjava-bytecode-asm

How define class after byte code transformation with ASM (class file version 0.0)


I cannot load a class after byte code modification with ASM library.

Here it is identity transformer and I expect to get modified array with the same size as bytes one, but it 2 times shorter! (439 vs 278)

    String path = SimpleClass.class.getName().replace(".", "/") + ".class";
    ClassLoader classLoader = SimpleClass.class.getClassLoader();
    InputStream is = classLoader.getResourceAsStream(path);

    byte[] bytes = IOUtils.toByteArray(is);

    ClassReader reader = new ClassReader(bytes);
    ClassWriter writer = new ClassWriter(reader, 0);
    byte[] modified = writer.toByteArray();

There is nothing wonderful that it fails to loads. I suspect that header is truncated, but first bytes are the same in both arrays.

-54, -2, -70

static class ByteClassLoader extends ClassLoader {
    public Class define(String name, byte[] body) {
        return defineClass(name, body, 0, body.length);
    }
}

ByteClassLoader myLoader = new ByteClassLoader();
Class myClass = myLoader.define("Ooo", modified);

Fails with error:

java.lang.UnsupportedClassVersionError: Ooo has been compiled by a more  recent version of the Java Runtime (class file version 0.0), this version of the Java Runtime only recognizes class file versions up to 52.0

at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at PrintTest$ByteClassLoader.define(PrintTest.java:24)
at PrintTest.x(PrintTest.java:48)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:117)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:42)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:253)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:84)

Solution

  • Those bytes correspond to 0xcafeba which matches the Java class file magic number, 0xcafebabe.

    The problem here is that new ClassWriter(reader, 0) does not do what you presumably believe it does; see the API documentation:

    classReader - the ClassReader used to read the original class. It will be used to copy the entire constant pool from the original class and also to copy other fragments of original bytecode where applicable.

    We still need to actually have the writer visit the reader via ClassReader.accept, as follows:

    reader.accept(writer, 0);
    

    As a side note, you don't need IOUtils.toByteArray as ClassReader has a constructor taking InputStream.