- Версия(и) Minecraft
- all
Предисловие
Етак, добрый вечер. Данная статья преследует цель в общих чертах дать вам широкое представление о том как работать с байт-кодом JVM при помощи библиотеки ASM.
Эта статья не является гайдом для новичков, для понимания того что тут происходит вам нужно написать хотя бы один простейший трансформер.
В этой статье я не буду приводить примеры кода, но буду довольно часто неявным образом описывать архитектуру ASM. Когда я буду говорить, что A является частью B или A лежи в B, я буду иметь ввиду, что при помощи визитора A вы можете получить B. Например имя является частью класса означает, что при помощи
Каждая часть этой статьи независима друг от друга, так что можете пропускать любую, если она кажется вам не интересной.
Немного приводим мысли в порядок
Для начала давайте начнем говорить на одном языке. В этом параграфе составим общее представление что есть что.
Вот тут по верхам объяснена структура файла класса. Все вродь как понятно пулы, методы, поля, но есть такая новая штука как атрибуты класса. Атрибуты класса - это очень мощная штука, которая дает крайне вкусные возможности по части анализа классов. Прочитать можно тут. ASM подает нам их в самых неожиданых местах неявным образом. Я буду постоянно явным образом делать референсы на них, чтобы вы проводили для себя праллели и могли в будущем быстрее находить нужную информацию.
Так вот поехали.
Класс - это имя, родитель, интерфейсы, сигнатура, методы и поля
Родитель - это другой класс от которого наследует наш класс. Он есть у всех классов кроме Object и module-info
Сигнатура - это строка с информацией о генериках класса или метода . Тут указаны границы генериков или, если вы наследуете от класса с генериком, генерик с которым вы наследуете. Типа
Метод - это имя, дескриптор, тело, дефолтное значение и сигнатура
Дескриптор - описывает тип метода или поля. Структура довольно сложная. С ней можно ознакомится тут.
Тело метода - набор опкодов и их параметров. Ну и таблица try-catch блоков. Кстати это тоже атрибут класса.
Дефолтное значение - это интересная штука которая объединяет в себе значения из двух атрибутов. Атрибут значений констант и Атрибут дефолтных значений аннотации. То есть эта штука появляется как у полей так и у методов, но означает совершенно разные вещи.
Поле - имя, дескриптор и дефолтное значение.
Аннотации
Аннотации появились не так давно и по этой причине очень дружно попали в атрибуты класса. Как опытные жава программисты вы, вероятно, отлично знаете как и где их можно применять. Мы же тут поговорим как они мапяться на байт-код.
Аннотации есть двух категорий:
Обычные Видимые - атрибут
Обычные Невидимые - атрибут
Типовые Видимые - атрибут
Типовые Невидимые - атрибут
Помимо этого завели отдельный атрибут для аннотаций, которые навешиваются на параметры метода. И они конечно тоже есть двух типов.
Аннотации параметров Видимые - атрибут
Аннотации параметров Невидимые - атрибут
ASM выдает обычные аннотации как часть классов, методов, полей и параметров. То есть он опять неявно объединил атрибут и сущности класса.
ASM выдает аннотации типов как обычные аннотации но с путем до типа. В целом пожелаю вам удачи если вам когда-нибудь понадобится с этим разбираться.
Из интересного про аннотации:
Оптимизации жавы или когда ломается ASM
Начнем издалека.
max_stack и max_locals
Обе эти переменные изменяются через
Вам не обязательно высчитывать их самостоятельно. Достаточно просто передать флаг
Забавность
Но нас конечно же все это не волнует до тех пор пока
Начинайте просматривать каждую строчку вашего кода и сверяться с этой табличкой. Скорее всего у вас где то ошибка типов или количество переменных на стеке ушло в минус.
Пожалуйста не пытайтесь отключать
stack_map
Сказать честно я очень мало знаю про эту штуку, но я знаю, что она может ломать.
Опять же
Из-за устройства
Так вот хочу предостеречь таких же мечтателей. Так нельзя поверьте на слово. Слишком долго объяснять почему. Просто попробуйте потом теренарные операторы с аперкастом.
INVOKE_DYNAMIC
Есть такой очень страшный опкод INVOKE_DYNAMIC.
В Java 8 он применяется только для лямбд, а в Java 9+ еще и для конкатенации строк.
Мы будем говорить только про лямбды, про строки я задал вопрос на стэке и там накидали референсов.
У
такой код считается валидным и в рантайме он попытаеся найти метод
Так вот, изначальная идея работы
Дело в том, что Oracle решили генерировать лямбды в рантайме. То есть многие думают, что
является не более чем синтаксическим сахаром над
Но на самом деле первый случай, внезапно, породит следующий байт-код
Собственно если это перевести в реалии (name, desc, args) и
name =
desc =
args = (
Вот и, если не вдаваться в подробности,
Двойные параметры
Есть одна интересная штука с числами в байткоде. Если конкретней у
Если например в статичном методе с дескриптором
P.S.
У меня еще есть много чего рассказать, но так как только на это я потратил 4 часа, мне интересно услышать ваше мнение. Нужна ли вам эта информация и понятен ли такой стиль повествования?
Етак, добрый вечер. Данная статья преследует цель в общих чертах дать вам широкое представление о том как работать с байт-кодом JVM при помощи библиотеки ASM.
Эта статья не является гайдом для новичков, для понимания того что тут происходит вам нужно написать хотя бы один простейший трансформер.
В этой статье я не буду приводить примеры кода, но буду довольно часто неявным образом описывать архитектуру ASM. Когда я буду говорить, что A является частью B или A лежи в B, я буду иметь ввиду, что при помощи визитора A вы можете получить B. Например имя является частью класса означает, что при помощи
ClassVisitor
мы можем узнать имя класса.Каждая часть этой статьи независима друг от друга, так что можете пропускать любую, если она кажется вам не интересной.
Немного приводим мысли в порядок
Для начала давайте начнем говорить на одном языке. В этом параграфе составим общее представление что есть что.
Вот тут по верхам объяснена структура файла класса. Все вродь как понятно пулы, методы, поля, но есть такая новая штука как атрибуты класса. Атрибуты класса - это очень мощная штука, которая дает крайне вкусные возможности по части анализа классов. Прочитать можно тут. ASM подает нам их в самых неожиданых местах неявным образом. Я буду постоянно явным образом делать референсы на них, чтобы вы проводили для себя праллели и могли в будущем быстрее находить нужную информацию.
Так вот поехали.
Класс - это имя, родитель, интерфейсы, сигнатура, методы и поля
Родитель - это другой класс от которого наследует наш класс. Он есть у всех классов кроме Object и module-info
Сигнатура - это строка с информацией о генериках класса или метода . Тут указаны границы генериков или, если вы наследуете от класса с генериком, генерик с которым вы наследуете. Типа
class MyAwesomeList extends AbstractList<Fruit>
. В общем оч классная штука. Никогда не пытайтесь парсить ее самостоятельно. Используйте SignatureReader
. Кстати сигнатура это один из атрибутов класса.Метод - это имя, дескриптор, тело, дефолтное значение и сигнатура
Дескриптор - описывает тип метода или поля. Структура довольно сложная. С ней можно ознакомится тут.
Тело метода - набор опкодов и их параметров. Ну и таблица try-catch блоков. Кстати это тоже атрибут класса.
Дефолтное значение - это интересная штука которая объединяет в себе значения из двух атрибутов. Атрибут значений констант и Атрибут дефолтных значений аннотации. То есть эта штука появляется как у полей так и у методов, но означает совершенно разные вещи.
Поле - имя, дескриптор и дефолтное значение.
Аннотации
Аннотации появились не так давно и по этой причине очень дружно попали в атрибуты класса. Как опытные жава программисты вы, вероятно, отлично знаете как и где их можно применять. Мы же тут поговорим как они мапяться на байт-код.
Аннотации есть двух категорий:
- Обычные аннотации
- Аннотации типов (А вот про них вы, вероятно, не в курсе. Загуглите Type Annotations)
- Видимые - можно достать рефлексией
- Невидимые - нельзя достать рефлексией
Обычные Видимые - атрибут
Обычные Невидимые - атрибут
Типовые Видимые - атрибут
Типовые Невидимые - атрибут
Помимо этого завели отдельный атрибут для аннотаций, которые навешиваются на параметры метода. И они конечно тоже есть двух типов.
Аннотации параметров Видимые - атрибут
Аннотации параметров Невидимые - атрибут
ASM выдает обычные аннотации как часть классов, методов, полей и параметров. То есть он опять неявно объединил атрибут и сущности класса.
ASM выдает аннотации типов как обычные аннотации но с путем до типа. В целом пожелаю вам удачи если вам когда-нибудь понадобится с этим разбираться.
Из интересного про аннотации:
- При использовании TreeApi выдает их значение в виде одного листа у которого каждый 2n элемент это имя параметра а 2n + 1 его значение.
- Значения в аннотации всегда в ну очень непредсказуемом формате и там нужно опытным путем определять что во что вырождается.
- Если вы прочитаете аннотацию класса в ней не будет дефолтов этой аннотации. Вам придется доставать байт-код класса аннотации и читать дефолтные значения оттуда.
Оптимизации жавы или когда ломается ASM
Начнем издалека.
max_stack и max_locals
max_stack
и max_locals
- это 2 переменные метода и являются частью метода.max_stack
- максимальный размер стека в любой момент времени исполнения функцииmax_locals
- максимальное колличество локальных переменных в любой момент времени исполнения функцииОбе эти переменные изменяются через
visitMaxs
.Вам не обязательно высчитывать их самостоятельно. Достаточно просто передать флаг
COMPUTE_MAXS
в ClassWriter
и ASM сам все высчитает. Точнее когда вы вызовете visitMaxs
он проигнорит переданные параметры и запишет, то что сам посчитает.Забавность
max_stack
и max_locals
заключается в том, что даже если вы просто возьмете и вызовете visitMaxs(999, 999)
оно нормально сработает и ничего не сломается и даже если visitMaxs(0, 0)
тоже все будет окей, но от такого подхода ломаются обфускаторы.Но нас конечно же все это не волнует до тех пор пока
COMPUTE_MAXS
не ломается. А ломается он только в одном случае: когда вы наложали в опкодах в методе. К сожалению других вариантов нет и вам нужно смирится, что дело в вас.Начинайте просматривать каждую строчку вашего кода и сверяться с этой табличкой. Скорее всего у вас где то ошибка типов или количество переменных на стеке ушло в минус.
Пожалуйста не пытайтесь отключать
COMPUTE_MAXS
и сувать visitMaxs(999, 999)
. Оно, конечно, поможет просто записать класс но в рантайме упадет.stack_map
Сказать честно я очень мало знаю про эту штуку, но я знаю, что она может ломать.
Опять же
stack_map
- это атрибут.stack_map
- это довольно сложная штука, и я ни разу не генерировал ее ручками. Я всегда передавал флаг COMPUTE_FRAMES
в ClassWriter
, чего и вам советую. С этим подходом есть несколько подводных камней:COMPUTE_FRAMES
ломается с очень не красивыми ошибками, но по тем же причинам что иCOMPUTE_MAXS
.- Для создания
stack_map
ASM'у необходима возможность узнать общего предка двух классов. Эта возможность реализована вClassWriter#getCommonSuperClass
и работает она при помощи рефлексии.
Из-за устройства
ClassWriter#getCommonSuperClass
также возникают несколько проблем:- Из-за того что рефлексия загружает классы могут возникнуть дед локи.
- Если класса нет в класс пазе будет выброшено исключение
java/lang/Object
является родителем для всех и просто переопределил ClassWriter#getCommonSuperClass
так чтоб он всегда возвращал java/lang/Object
.Так вот хочу предостеречь таких же мечтателей. Так нельзя поверьте на слово. Слишком долго объяснять почему. Просто попробуйте потом теренарные операторы с аперкастом.
INVOKE_DYNAMIC
Есть такой очень страшный опкод INVOKE_DYNAMIC.
В Java 8 он применяется только для лямбд, а в Java 9+ еще и для конкатенации строк.
Мы будем говорить только про лямбды, про строки я задал вопрос на стэке и там накидали референсов.
У
INVOKE_DYNAMIC
есть несколько параметров- String: name
- String: desc
- Handle: bootstrap method handle
- Object[]: bootstrap method args
Gradle (Groovy):
def function(Object value) {
value.add(5)
}
add
рефлексией, но так как это медленно придумали INVOKE_DYNAMIC
.Так вот, изначальная идея работы
INVOKE_DYNAMIC
такая:- Вызывается метод обозначенный в
bootstrap method handle
с параметрами (name, desc, args). Этот метод, кстати, обычный жава метод в класспазе. Никакой магии. - Этот метод исходя из переданных аргументов возвращает нам
CallSite
- JVM запоминает его и вызывает метод, обозначенный в нем.
- На следующем разе JVM использует уже созданный
CallSite
Дело в том, что Oracle решили генерировать лямбды в рантайме. То есть многие думают, что
Java:
Runnable r = () -> {};
Java:
Runnable r = new Runnable() {
public void run() {
}
};
Но на самом деле первый случай, внезапно, породит следующий байт-код
Код:
INVOKEDYNAMIC run()Ljava/lang/Runnable; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
test/Main.lambda$main$0()V,
()V
]
Собственно если это перевести в реалии (name, desc, args) и
bootstrap method handle
получитсяname =
"run"
desc =
"()Ljava/lang/Runnable;"
bootstrap method handle
= "java/lang/invoke/LambdaMetafactory.metafactory"
args = (
"()V"
, "test/Main.lambda[imath]main[/imath]0()V"
, "()V"
)Вот и, если не вдаваться в подробности,
LambdaMetafactory.metafactory
сделает следующее:- Достанет тип из возвращаемое значение из
desc
. Это тип нашей лямбды. Параметры тут будут только если лямбда захватила какую то переменную в себя. - Возьмет второй параметр из
args
. Это метод, который является телом нашей лямбды. - Сгененрирует в рантайме класс и вернет
CallSite
, который указывает на его конструктор.
LambdaMetafactory.metafactory
. Но в целом как можете видеть это довольно безобидная аанотация и можно даже придумать ей применениеДвойные параметры
Есть одна интересная штука с числами в байткоде. Если конкретней у
long
и double
Если например в статичном методе с дескриптором
(II)V
первую переменную можно достать с помощью ILOAD 0
, а вторую при помощи ILOAD 1
(напоминаю что I это int), то вот в методе с дескриптором (JJ)V
(j это long) LLOAD 1
выкинет ошибку, а вот LLOAD 2
нет и отработает так как должен был бы LLOAD 1
.P.S.
У меня еще есть много чего рассказать, но так как только на это я потратил 4 часа, мне интересно услышать ваше мнение. Нужна ли вам эта информация и понятен ли такой стиль повествования?