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




Загрузка 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
}
}
}
@TamateaSchofield Одна возможность, о которой я только что подумал, - это может быть сделано обфускатором, чтобы запутать декомпиляторы? Декомпилятор, глядя на это, может создать this.someField = ...; в качестве первой строки в конструкторе, который не будет компилироваться.
@TamateaSchofield Я пропустил, что поле синтетическое! Это означает, что это, вероятно, поле для включающего экземпляра. Смотрите редактирование для получения дополнительной информации.
Инициализация ссылки на внешний экземпляр до вызова суперконструктора становится актуальной, если суперконструктор вызывает переопределенный метод, который может получить доступ к внешнему объекту. Отсутствие инициализации ссылки на внешний экземпляр до того, как суперконструктор вызывал ошибки в прошлом, поэтому их ранняя инициализация стала стандартной в Java 1.4. На самом деле, это поведение было бы реализовано и в более ранних версиях в качестве исправления ошибок, но ошибка верификатора в старых JVM не позволила этому.
В дополнение к ответу от Sweeper исходный код мог выглядеть так:
class ka {
static int a = 0;
class a extends Thread {
a(String arg1, boolean arg2) {
super(arg1 + ".pool["+ a++ + "]");
setDaemon(arg2);
}
}
}
Компиляция этого фрагмента с помощью Java 8 даст байт-код, похожий на ваш.
Я подозревал, что это разрешено JVM, поскольку это рабочее приложение, спасибо за ответ, не могли бы вы дать какое-либо представление о том, почему компилятор упорядочил код таким образом?