Тайна байт-кода Java: неправильный порядок операций в конструкторах

Я возился с обратным проектированием приложения 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 {
         }
     }


Настройки компилятора также все еще находились в запутанной банке:

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

Я рассмотрел возможность того, что мог быть задействован статический метод, ответственный за логику, которая в конечном итоге была встроена во время компиляции. Несмотря на мои попытки, мне не удалось воспроизвести аналогичный результат. Кроме того, я заметил наличие синтетического поля и тот факт, что этот класс был внутренним классом. Эти факторы, кажется, играют роль в необычной структуре байт-кода.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
0
69
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Загрузка this и назначение полей его экземпляра перед вызовом другого конструктора полностью разрешена JVM. Это язык Java, который не позволяет устанавливать поля экземпляра перед вызовом this(...) или super(...).

Из спецификации JVM:

Каждый метод инициализации экземпляра, за исключением метода инициализации экземпляра, полученного из конструктора класса 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
    }
}

Мой компилятор генерирует putfield перед вызовом конструктора суперкласса, который присваивает первый параметр конструктора синтетическому полю. В коде Java класс Inner будет выглядеть примерно так (это недопустимый код Java, просто для иллюстрации):

class Inner extends Thread {
    private final Outer $this0;

    Inner(Outer arg0) {
        this.$this0 = arg0;
        super();
    }
}

Требуется генерация putfield перед вызовом super (спасибо Хольгеру за то, что он сообщил мне об этом!), потому что конструктор суперкласса может вызвать метод, который подкласс переопределяет. Этот метод может получить доступ к окружающему экземпляру.

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

Я подозревал, что это разрешено JVM, поскольку это рабочее приложение, спасибо за ответ, не могли бы вы дать какое-либо представление о том, почему компилятор упорядочил код таким образом?

Tamatea Schofield 18.07.2023 10:16

@TamateaSchofield Одна возможность, о которой я только что подумал, - это может быть сделано обфускатором, чтобы запутать декомпиляторы? Декомпилятор, глядя на это, может создать this.someField = ...; в качестве первой строки в конструкторе, который не будет компилироваться.

Sweeper 18.07.2023 10:21

@TamateaSchofield Я пропустил, что поле синтетическое! Это означает, что это, вероятно, поле для включающего экземпляра. Смотрите редактирование для получения дополнительной информации.

Sweeper 18.07.2023 10:49

Инициализация ссылки на внешний экземпляр до вызова суперконструктора становится актуальной, если суперконструктор вызывает переопределенный метод, который может получить доступ к внешнему объекту. Отсутствие инициализации ссылки на внешний экземпляр до того, как суперконструктор вызывал ошибки в прошлом, поэтому их ранняя инициализация стала стандартной в Java 1.4. На самом деле, это поведение было бы реализовано и в более ранних версиях в качестве исправления ошибок, но ошибка верификатора в старых JVM не позволила этому.

Holger 18.07.2023 11:32

В дополнение к ответу от Sweeper исходный код мог выглядеть так:

class ka {
    static int a = 0;
    class a extends Thread {
        a(String arg1, boolean arg2) {
            super(arg1 + ".pool["+ a++ + "]");
            setDaemon(arg2);
        }
    }
}

Компиляция этого фрагмента с помощью Java 8 даст байт-код, похожий на ваш.

Другие вопросы по теме