Написать пост

Недоступные в языке возможности байткода Java

Аватар Пётр Соковых

Обложка поста Недоступные в языке возможности байткода Java

Рассказывает Rafael Winterhalter

После того, как я достаточно продолжительное время работал с байткодом Java и проводил некоторые изыскания в этой области, я хочу поделиться выводами, которые я сделал.

Выполнение в конструкторе кода до вызова суперконструктора или вспомогательного конструктора

В Java как языке программирования (далее — JPL, от Java Programming Language) вызов super() или this() должен быть первым выражением в конструкторе, но в байткоде Java (далее — JBC, от Java Byte Code) это не так. Вы можете добавить код перед этими вызовами, если выполняются следующие условия:

  • другой конструктор всё-таки вызван далее;
  • он вызван не внутри условного;
  • до вызова конструктора не считываются поля и не вызываются методы конструируемого объекта. Из этого происходит следующий вопрос.

Работа с полями сущности до вызова super() или this()

До шестой версии в Java был следующий эксплоит, который позволял сделать вышеописанное:

			class Foo {
  public String s;
  public Foo() {
    System.out.println(s);
  }
}

class Bar extends Foo {
  public Bar() {
    this(s = "Hello World!");
  }
  private Bar(String helper) {
    super();
  }
}
		

Теперь в JPL такая работа с полями невозможна, но всё ещё возможна средствами JBC.

Выбор вызываемого конструктора (до Java 7u23)

Java не позволяет создавать конструкторы вроде такого:

			class Foo {
  Foo() { }
  Foo(Void v) { }
}

class Bar() extends Foo {
  Bar(){
    if(System.currentTimeMillis() % 2 == 0) {
      super();
    } else {
      super(null);
    }
  }
}
		

Тем не менее, до Java 7u23 верификатор HotSpot VM’s пропускал эту проверку. Теперь это пофиксили.

Создание класса вообще без какого-либо конструктора

В JPL это не возможно — хоть один конструктор, но всегда наследуется. С помощью JBC можно сделать так, что создать экземпляр класса будет невозможно, даже если использовать рефлексию (правда, sun.misc.unsafe всё равно позволяет сделать это).

Создание методов с одинаковыми сигнатурами, но разными возвращаемыми типами

В JPL методы идентифицируются по их имени и набору параметров, а в JBC — ещё и возвращаемому типу.

Вызов проверяемых (checked) исключений без указания throws или конструкции try...catch

Проверку того, все ли проверяемые исключения пойманы (или указаны с помощью throws), осуществляет компилятор, это не зависит от Java Runtime или JBC.

Использование динамического вызова методов вне лямбда-выражений

Так называемый динамический вызов методов могут быть использован для чего угодно, а не только для лямбда-выражений. Его использование позволяет, например, менять логику программы во время исполнения. Многие динамические языки программирования, которые основываются на JBC, улучшились за счёт этой инструкции. В JBC вы можете также использовать лямбда-выражения в Java 7, когда компилятор ещё не мог обрабатывать динамический вызов методов, а JVM — могла.

Использование идентификаторов, которые обычно запрещены

Хотели добавить в имя вашего метода пробел или перенос строки? Создайте свой JBC, и удачной отладки! Запрещены только символы «.», «;», «[» и «/». Вдобавок, если метод называется не <init> или <clinit>, его имя не может содержать символов «<» и «>».

Переназначение final полей, final параметров и this

Параметра final в JBC не существует, и любой параметр, включая this (в нулевом индексе), может быть изменён. Константное поле также может быть изменено, если оно уже было определено в конструкторе (для static полей этого не требуется).

Вызов метода у null

В JBC вы можете вызвать любой нестатический метод у null, и он будет отлично работать, если в нём нет вызова this.

Использование конструкторов и инициализаторов, будто это методы

В JBC конструкторы и инициализаторы ничем не отличаются от методов, единственное — они должны иметь имена <init> и <clinit> соответственно, — чтобы JVM могла проверять, вызывает ли один конструктор другой корректный конструктор.

Невиртуальный вызов метода из того же класса

			class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}
		

В JPL вызов new Bar::foo() всегда будет вызывать Runtime Exception. Нельзя сделать так, чтобы метод Foo::foo() всегда вызывал bar() из своего класса. Но вы можете реализовать такое поведение на JBC с помощью опкода INVOKESPECIAL, который обычно используется для вызова родительских методов.

Назначение произвольного мета-атрибута

В JPL вы можете добавлять аннотации только к полям, методам и классам. В JBC вы можете встроить любую информацию в класс. Правда, для её использования вам придётся извлекать её самостоятельно, не полагаясь на механизм загрузки классов Java.

Перенаполнение и неявное определение значений byte, short, char и boolean

Технически, внутри байткода существуют только типы int, float, long и double, что даёт простор для многих недопустимых для JPL операций.

Рекурсивный блок catch

В байткоде Java вы можете сделать что-то вроде

			try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}
		

Вызов любого «метода по умолчанию»

В JBC можно вызвать default-метод, даже если он был переопределён.

Вызов метода родителя у объекта, не являющегося this

В JPL можно вызывать только методы своего ближайшего родителя или default-методы интерфейсов. В JBC возможен код наподобие:

			class Foo {
  void m(Foo f) {///Тип должен быть одним и тем же
    f.super.toString(); // вызывает Object::toString
  }
  public String toString() {
    return "foo";
  }
}
		

Доступ к синтетическим членам

В байткоде к синтетическим членам можно обращаться напрямую.

			class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}
		

Справедливо также для синтетических полей, методов и классов.

Добавление некорректной информации о дженериках

Информация о дженериках хранится в классе в виде строк. Верификатор никак не проверяет её, а значит, можно сделать так, что следующие ассерты будут верными:

			Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
		

Вызов несуществующего метода и краш JVM

Вы можете вызвать любой метод у любого экземпляра. Обычно верификатор распознаёт это, но я заметил, что иногда, когда вы вызываете метод у (элемента) массива, некоторые версии JVM не распознают, что его не существует. Это, конечно, сомнительная возможность, но с помощью кода, который прошёл через javac, вы этого точно сделать не сможете. Разумеется, когда JVM дойдёт до этого места, её ждёт краш.

Оригинал: «Bytecode features not available in Java Language»

Следите за новыми постами
Следите за новыми постами по любимым темам
8К открытий8К показов