I've been tinkering with reverse engineering a Java app, and I've stumbled upon something interesting. The bytecode I found seems to break the rules by not initializing the superclass first in a constructor.
I'm trying to figure out how this is possible. Could it be a normal behavior of Java compilers, or is it some sneaky obfuscation technique (Note: It's worth mentioning that the original class name hasn't been stripped by the obfuscator, which indicates that the obfuscation process might not have been very thorough. So, it's less likely that the bytecode structure is a deliberate result of obfuscation.)
Could anyone perhaps offer some insight on what could the original code have looked like to generate such unconventional bytecode? I'm eager to learn and unravel this mystery. Thanks a bunch!
Here is the bytecode.
final class a/ka$a extends java/lang/Thread {
<ClassVersion=51>
<SourceFile=CLThreadPool.java>
private synthetic a.ka a;
public ka$a(a.ka arg0, java.lang.String arg1, boolean arg2) { // <init> //(La/ka;Ljava/lang/String;Z)V
<localVar:index=0 , name=this , desc=La/ka$a;, sig=null, start=L0, end=L4>
<localVar:index=2 , name=name , desc=Ljava/lang/String;, sig=null, start=L0, end=L4>
<localVar:index=3 , name=daemon , desc=Z, sig=null, start=L0, end=L4>
L0 {
aload 0 // reference to self
aload 1 // reference to arg0
putfield a/ka$a.a:a.ka
}
L1 {
aload 0 // reference to self
aload 1 // reference to arg0
new java/lang/StringBuilder
dup
aload 2 // reference to arg1
invokestatic java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;
invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)V
ldc ".pool[" (java.lang.String)
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
aload 1 // reference to arg0
dup
invokestatic a/ka.a(La/ka;)I
dup_x1
iconst_1
iadd
invokestatic a/ka.a(La/ka;I)V
invokevirtual java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder;
ldc "]" (java.lang.String)
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
invokespecial java/lang/Thread.<init>(Ljava/lang/ThreadGroup;Ljava/lang/String;)V
}
L2 {
aload 0 // reference to self
iload 3
invokevirtual a/ka$a.setDaemon(Z)V
}
L3 {
return
}
L4 {
}
}
The compiler preferences were also still in the obfuscated jar:
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.7
I've considered the possibility that there might have been a static method involved, responsible for the logic that ended up getting inlined during compilation. Despite my attempts, I haven't been able to reproduce a similar output. Additionally, I noticed the presence of a synthetic field and the fact that this class was a inner class. These factors seem to play a role in the unusual bytecode structure.
Loading this
and assigning its instance fields before calling another constructor is perfectly allowed by the JVM. It is the Java language that does not allow setting instance fields before calling this(...)
or super(...)
.
From the JVM spec:
Each instance initialization method, except for the instance initialization method derived from the constructor of class
Object
, must call either another instance initialization method ofthis
or an instance initialization method of its direct superclasssuper
before its instance members are accessed.However, instance fields of
this
that are declared in the current class may be assigned byputfield
before calling any instance initialization method.
So the JVM does not allow reading instance fields before calling another constructor, but it allows writes. Implementations of a Java compiler can totally rearrange the code to produce something like this, given that it can prove that the behaviours are the same.
It could also be the work of an obfuscator. One possibility is that this is intended to cause (naive) decompilers to output illegal code. A decompiler looking at this might read the line numbers and assume that there is a this.a = arg0;
statement on the first line, causing the decompiler's output to not compile.
In your particular case, based on the fact that the field is synthetic and this is an inner class, this field is highly likely to store the enclosing instance.
For example, the Inner
class below would need to store an instance Outer
, and a synthetic field would be created for that.
class Outer {
class Inner extends Thread {
// the JVM representation of Inner's constructor takes a parameter of Outer
// so that it can be assigned to the field storing the enclosing instance
}
}
My compiler generates a putfield
before the superclass constructor call, that assigns the first constructor parameter to the synthetic field. In Java code, the Inner
class would look something like this (this is invalid Java code, just for illustrative purposes):
class Inner extends Thread {
private final Outer $this0;
Inner(Outer arg0) {
this.$this0 = arg0;
super();
}
}
Generating putfield
before the super
call is required (credits to Holger for informing me of this!), because the superclass constructor could call a method that the subclass overrides. That method could access the enclosing instance.
class Outer {
public void outerMethod() {
}
class Inner extends SomeSuperClass {
@Override
public void superclassConstructorWillCallThis() {
// This accesses the enclosing instance, i.e. Outer.this
outerMethod();
// if the enclosing instance field is not set before the superclass constructor call,
// this call will throw a NullPointerException
}
}
}