Java字节码之谜:构造函数中的非法操作顺序

Tam*_*eld 1 java obfuscation bytecode reverse-engineering deobfuscation

我一直在对 Java 应用程序进行逆向工程,并且偶然发现了一些有趣的东西。我发现的字节码似乎违反了规则,因为没有首先在构造函数中初始化超类。

我正在尝试弄清楚这怎么可能。这可能是Java编译器的正常行为,还是某种偷偷摸摸的混淆技术(注:值得一提的是,原始类名尚未被混淆器剥离,这表明混淆过程可能不是很彻底。因此,字节码结构不太可能是故意混淆的结果。)

任何人都可以提供一些关于生成这种非常规字节码的原始代码是什么样子的见解吗?我渴望了解并解开这个谜团。非常感谢!

这是字节码。

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 {
         }
     }


Run Code Online (Sandbox Code Playgroud)

编译器首选项也仍然在混淆的 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

Run Code Online (Sandbox Code Playgroud)

我考虑过可能涉及一个静态方法,负责最终在编译期间内联的逻辑。尽管我尝试过,但我无法重现类似的输出。此外,我注意到合成字段的存在以及该类是内部类的事实。这些因素似乎在不寻常的字节码结构中发挥了作用。

Swe*_*per 6

thisJVM 完全允许在调用另一个构造函数之前加载并分配其实例字段。Java语言不允许在调用this(...)或之前设置实例字段super(...)

来自JVM 规范:

除了从 class 的构造函数派生的实例初始化方法之外,每个实例初始化方法都Object必须在访问其实例成员之前调用 的另一个实例初始化方法this或其直接超类的实例初始化方法。super

但是,this在当前类中声明的实例字段可以在putfield调用任何实例初始化方法之前进行分配。

因此,JVM 不允许在调用另一个构造函数之前读取实例字段,但允许写入。Java 编译器的实现可以完全重新排列代码以生成类似的代码,只要它可以证明行为是相同的。

它也可能是混淆器的工作。一种可能性是,这是为了导致(天真的)反编译器输出非法代码。查看此内容的反编译器可能会读取行号并假设this.a = arg0;第一行有一条语句,从而导致反编译器的输出无法编译。

在您的特定情况下,基于该字段是合成的并且这是一个内部类这一事实,该字段很可能存储封闭的实例

例如,Inner下面的类需要存储一个实例Outer,并且将为此创建一个合成字段。

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
    }
}
Run Code Online (Sandbox Code Playgroud)

我的编译器在超类构造函数调用之前生成一个putfield,它将第一个构造函数参数分配给合成字段。在 Java 代码中,该类Inner看起来像这样(这是无效的 Java 代码,仅用于说明目的):

class Inner extends Thread {
    private final Outer $this0;

    Inner(Outer arg0) {
        this.$this0 = arg0;
        super();
    }
}
Run Code Online (Sandbox Code Playgroud)

putfield需要在调用之前生成super(感谢Holger通知我这一点!),因为超类构造函数可以调用子类重写的方法。该方法可以访问封闭的实例。

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)