- Версия(и) Minecraft
- любая
Итак, согласитесь, частенько бывают ситуации когда очень хочеться изменить стандартное поведение майна добавив всего одно дополнительную строку или изменить какую нибудь переменую или модификаторы доступа. Не спорю для этого есть глуми с его либой да и AT не плохи, но я уверен вам интересно как это работает изнутри, не так ли?
Так вот, прежде чем начать нам нужно познакомиться с понятием байт-кода. Ни для кого не секрет, что Java имеет свой компилятор, который явно что-то делает. А делает он преобразование ваших крутых лямбд выражений с анонимными классами в понятный и простой для машины язык. Согласитесь было бы не рацианально если бы JVM каждый раз парсировала ваш код. Так вот, байт-код он потому байт, что состоит из байтов, которые являються указаниями для JVM. В результате использования байт-кода мы достигаем сразу 2 целей: скорость, умеьшение веса файла.
Перейдем же к примерам!
Самый простой Java класс:
Как это выглядит в байт-коде:
Прежде чем я объясню как работает байт-код, вам нужно четко понимать, что букафы, которые вы видите в байт-коде это не более чем удобство предоставляемое нам просмотращиком байт кода, встроенным в InteliJ IDEA. На самом деле тот же RETURN, в байт-коде, будет записан как 177(Один байт 10110001).
Так вот, перейдем же к рассмотрению байт-кода.
Что ж надеюсь вы поняли. Перейдем к более сложному объекту.
Java
Выглядит довольно просто. Теперь перейдем к байт-коду.
Что ж, теперь вы примерно понимаете как работает байт-код JVM.
Настоятельно рекомендую самостоятельно поизучать/поглазеть на байт код своих или чужих классов. В IntelliJ IDEA для этого есть очень удобный инструмент. Чтобы его включить перейдите во вкладку панели быстрого доступа View->Byte Code Viewer.
Теперича мы перейдем к самому интересному. А делать мы будем следующие:
- Изменим модификатор доступа поля какого-нибудь класса
- Сгенерируем метод, который будет нам возвращать значение этого поля.
Зачем нам так извращаться? Все просто! Даже если мы в процессе работы изменим модификатор доступа этого поля, компилятор не даст нам скомпилировать программу, а вот сделать синтетический метод мы можем легко! Кто-то назовет это костылем, а я назову метапрограммированием.
Что ж так как народ тут не простой, мы для начала разберем как этот массив байтов преобразуеться в понтовый класс со своим поведением на примере FML.
Существует в java такой класс как ClassLoader. Он как раз и загружает классы из байт-кода. ClassLoader'ы бывают разные и деляться на виды, но мы не будем об этом пока говорить. Я лишь скажу, что чтобы загрузить класс вам нужно вызвать метод defineClass у ClassLoader. После того как класс был объявлен, хоть вы тресните, но изменить его не получиться. Что ж так как же фордж обслуживает наши хук лоадеры? Очень просто! Фордж делает свой класс лоадер, которым заменяет дефолтный джавовский. Прежде чем в первые выдать класс этот класс лоадер вызывает все зарегестрированые хук лоадеры. Передавай байт код от одного к другому. И потом уже загружает измененный байт-код в класс. На сиим окончем предысторию и перейдем к делу.
Первое, что нам нужно сделать это создать IFMLLoadingPlugin. И выглядеть он будет так:
Так как регистрация хук лоадеров происходит до поиска аннотаций, вам нужно указать путь до вашего класса в манифесте вашего мода. Вы можете это сделать вручную архиватором после компиляции, но лучше использовать градл.
Вот так вот:
Так же, если вы в среде разработки вам нужно добавить аргумент JVM
Что ж, все готово к работе. Поздравляю! Преступим.
Метод который нас больше всего интересует в данный момент это getASMTransformerClass. Он должен вернуть полные имена ваших классов которые будет заниматься изменениями байт-кода. Мы не будем особо тут париться и сделаем все в одном классе. Вот так вот:
Давайте рассмотрим метод transform. На вход нам дают 3 параметра: name, transformedName и basicClass. name - это обфусцированый вариант названия класса, transformedName - деобфусцированный а basicClass как раз тот самый байт-код. Так как мы пока никаких изменений не делаем, наш метод просто возвращает изначальный байт-код.
Что ж возвращаясь к нашей идеи альтернативных AT, нам нужно создать класс Acessor в который как раз поместим наш синтетический метод, допустим, doGet. Зачем нам создавать дополнительный класс, спросите вы. А все просто мы будем изменять этот класс перед его загрузкой, а наш класс к тому времени уже будет загружен, в результате чего ничего работать не будет. Допустим мы хотим получить переменную major в классе cpw.mods.fml.common.Loader. Тогда наш Acessor будет выглядить так:
Пишем мы тут return 0; только из-за уважения к компилятору.
Предварительная подготовка завершена!
Переходим к ASM!
Значит так. ASM - это просто понтовая библиотека для работы с байт кодом. При желании вы можете легко сами находить не нужные вам байты и заменять на нужные, но ASM делает этот процесс просто более удобным.
Как же она работает? Для начала немного бесполезной информации. У ASM есть 2 API: Core API (О нем и пойдет речь) и Tree API. Core API проходит класс по мере поступления, а Tree API похож на Javac сначала все анализирует и записывает, а потом дает полную информацию о классе. Core API работает быстрее по понятным причинам. Камень средоточия в ASM это ClassVisitor. Давайте я приведу код использования, а потом объясню что да как.
Дамы и господа, перед вами абсолютно бесполезный кусок кода, но щас объясню. ClassReader наследует от Reader так что он в принципе может вам по байтам считать, но это было бы глупо, поэтому мы используем его прекрасный метод accept. accept проходиться по всему классу вызывая нужные методы. Допустим когда он видит метод он вызывает visitMethod когда поле visitField И так далее. ClassWriter являеться ClassVisitor. Звучит стремно, но это довольно удобно. Вы можете методу accept передать ClassWriter и он просто запишет то что ему дадут посетить. Я в коде оборачиваю ClassWriter в свой ClassVisitor. Пока в этом смысла мало, но это пока. Давайте напишем свой ClassVisitor!
Вуаля:
Соответсвенно наш трансфарматор тоже изменился
Для начала попробуйте сами допетрить, что здесь происходит. Подумайте минутку.
Все минута прошла! Как я и говорил у ClassVisitor есть методы типа visitMethod, но я не сказал, что они возвращают MethodVisitor. По мне так это довольно логичный шаг так как в методе довольно много отличий от класса. Наш ClassVisitor получился довольно костыльным, но он работать будет так как мы точно знаем что у класса всего один метод и у него всего две инструкции.
Но приватность поля major никто не отменял, поэтому мы сделаем еще один ClassVisitor!
Вот так вот:
Здесь если имя поля 'major' мы делаем подмену модификаторов доступа на public static (см. Бинарные операции). Так же в трансформатор допишем пару строк:
Теперь вы можете в любой момент работы программы вызвать метод doGet и он вернет вам поле major класса Loader.
На этом все. Спасибо за внимание.
Код вы можете найти тут.
Так вот, прежде чем начать нам нужно познакомиться с понятием байт-кода. Ни для кого не секрет, что Java имеет свой компилятор, который явно что-то делает. А делает он преобразование ваших крутых лямбд выражений с анонимными классами в понятный и простой для машины язык. Согласитесь было бы не рацианально если бы JVM каждый раз парсировала ваш код. Так вот, байт-код он потому байт, что состоит из байтов, которые являються указаниями для JVM. В результате использования байт-кода мы достигаем сразу 2 целей: скорость, умеьшение веса файла.
Перейдем же к примерам!
Самый простой Java класс:
Java:
public abstract class SimpleModule {
private void method() {
}
}
Код:
// class version 52.0 (52)
// access flags 0x421
public abstract class ru/justagod/justacore/example/initializationexample/SimpleModule {
// compiled from: SimpleModule.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x2
private method()V
L0
LINENUMBER 10 L0
RETURN
L1
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
}
Так вот, перейдем же к рассмотрению байт-кода.
Код:
// class version 52.0 (52)
// access flags 0x421 // Модификаторы доступа в джаве записываються 2 байтами. Какой конкретно модификатор стоит обычно узнают при помощи логического умножения
public abstract class ru/justagod/justacore/example/initializationexample/SimpleModule { // Полное имя класса так же записываеться букафами
// compiled from: SimpleModule.java
// access flags 0x1 // Опкод публичного модификатора как раз являеться 1
public <init>()V // Всегда когда у класса нет конструктора, создветься дефолтный. Это один из них.
L0
LINENUMBER 6 L0 // Строке 6 исходного файла будет соответствовать этот блок байт-кода. Нужно только для дебага.
ALOAD 0 // Оператор ALOAD загружает указатель на объект(Экземпляр класса). Так как этот метод у нас не статичен, в индексе 0 будет лежать указатель на это объект. Тому кто писал на Python или Pascal понять это будет легко.
INVOKESPECIAL java/lang/Object.<init> ()V // Вызов констрктора родителя
RETURN // Просто return;
L1
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x2
private method()V // Обратите внимание на сигнатуру метода. Она такова ()V. Пустые скобки, означет, что параметры не нужны, а V символизирует возвращаемое значение. V - это void. Функция ничего не возвращает.
L0
LINENUMBER 10 L0 // Опять дебагер
RETURN // Опять return;
L1
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
}
Что ж надеюсь вы поняли. Перейдем к более сложному объекту.
Java
Java:
public class SimpleModule {
int someValue = 80;
static int someStaticValue = 90;
public SimpleModule() {
System.out.println(method(50));
}
private int method(int someParameter) {
int someLocalValue = someValue + someParameter + someStaticValue;
return someLocalValue;
}
}
Код:
public class ru/justagod/justacore/example/initializationexample/SimpleModule {
// compiled from: SimpleModule.java
// access flags 0x0
I someValue // У полей так же есть свои сигнатуры. У этого сигнатура I, где I - это integer.
// access flags 0x8
static I someStaticValue // У этого тоже самое только static
// access flags 0x1 // Наш уже не дефолтный конструктор
public <init>()V
L0
LINENUMBER 12 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 9 L1
ALOAD 0 // Положили в стэк указатель на наш объект
BIPUSH 80 // Положили в стэк байт 90
PUTFIELD ru/justagod/justacore/example/initializationexample/SimpleModule.someValue : I // Вот тут уже интересней. Давайте для начала разберем параметры опкода PUTFIELD. 1 параметр это путь до поля в которое мы хотим положить значение <класс>.<имя поля>. Второй параметр сигнатура самого поля. Итак внимание, на верху стэка у нас лежит байт 90, а чуть ниже ссылка на наш объект. JVM возмет указатель на объект и положит в нужное поле нужно значение. Видите, как все просто?
L2
LINENUMBER 13 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream; // Возмет из __статичного__ поля значение и положит в стэк
ALOAD 0 // Инстанс
BIPUSH 50 // Кладет в стэк 90
INVOKESPECIAL ru/justagod/justacore/example/initializationexample/SimpleModule.method (I)I // INVOKESPECIAL это опкод который нужен чтобы вызвать приватную функцию или функцию родителя. 1 параметр - полное имя функции. 2 - сигнатура функции. Привет, полиморфизм.
INVOKEVIRTUAL java/io/PrintStream.println (I)V // Привет, C++. Если вы знаете CPP вы, скорее всего, сразу поняли почему VIRTUAL. Для тех кто в танке объясню просто функционал. INVOKEVIRTUAL вызывает метод потомка, если метод перегружен или метод родителя в противном.
L3
LINENUMBER 14 L3
RETURN
L4
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L4 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x2 // Модификатор доступа private
private method(I)I // Сигнатура (I)I. I - это integer. Соответственно, функция принимает и отдает цифру.
L0
LINENUMBER 17 L0
ALOAD 0 // Метод не статичен поэтому по индексу 0 лежит указатель this.
GETFIELD ru/justagod/justacore/example/initializationexample/SimpleModule.someValue : I
ILOAD 1 // Есть ALOAD - загружает объект, а есть ILOAD - загружает цифру.
IADD // Итак, у нас на верхушке стэка лежит число и чуть ниже тоже число. IADD сложет эти два числа и положет его в стэк.
GETSTATIC ru/justagod/justacore/example/initializationexample/SimpleModule.someStaticValue : I
IADD // Та же ситуация
ISTORE 2 // Посмотрите на исходники. Там у нас есть переменая. Так вот мы в нее и записали число.
L1
LINENUMBER 18 L1
ILOAD 2 // Мы загрузили это число в стэк из переменой
IRETURN // Мы его вернули
L2
LOCALVARIABLE this Lru/justagod/justacore/example/initializationexample/SimpleModule; L0 L2 0
LOCALVARIABLE someParameter I L0 L2 1
LOCALVARIABLE someLocalValue I L1 L2 2
MAXSTACK = 2
MAXLOCALS = 3
// access flags 0x8
static <clinit>()V // Привет статичная инициализация
L0
LINENUMBER 10 L0
BIPUSH 90
PUTSTATIC ru/justagod/justacore/example/initializationexample/SimpleModule.someStaticValue : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0
}
Что ж, теперь вы примерно понимаете как работает байт-код JVM.
Настоятельно рекомендую самостоятельно поизучать/поглазеть на байт код своих или чужих классов. В IntelliJ IDEA для этого есть очень удобный инструмент. Чтобы его включить перейдите во вкладку панели быстрого доступа View->Byte Code Viewer.
Теперича мы перейдем к самому интересному. А делать мы будем следующие:
- Изменим модификатор доступа поля какого-нибудь класса
- Сгенерируем метод, который будет нам возвращать значение этого поля.
Зачем нам так извращаться? Все просто! Даже если мы в процессе работы изменим модификатор доступа этого поля, компилятор не даст нам скомпилировать программу, а вот сделать синтетический метод мы можем легко! Кто-то назовет это костылем, а я назову метапрограммированием.
Что ж так как народ тут не простой, мы для начала разберем как этот массив байтов преобразуеться в понтовый класс со своим поведением на примере FML.
Существует в java такой класс как ClassLoader. Он как раз и загружает классы из байт-кода. ClassLoader'ы бывают разные и деляться на виды, но мы не будем об этом пока говорить. Я лишь скажу, что чтобы загрузить класс вам нужно вызвать метод defineClass у ClassLoader. После того как класс был объявлен, хоть вы тресните, но изменить его не получиться. Что ж так как же фордж обслуживает наши хук лоадеры? Очень просто! Фордж делает свой класс лоадер, которым заменяет дефолтный джавовский. Прежде чем в первые выдать класс этот класс лоадер вызывает все зарегестрированые хук лоадеры. Передавай байт код от одного к другому. И потом уже загружает измененный байт-код в класс. На сиим окончем предысторию и перейдем к делу.
Первое, что нам нужно сделать это создать IFMLLoadingPlugin. И выглядеть он будет так:
Java:
package ru.justagod.asmtutor;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin.MCVersion;
import java.util.Map;
@MCVersion(value = "1.7.10")
@SuppressWarnings("unused")
public class HookLoader implements IFMLLoadingPlugin {
@Override
public String[] getASMTransformerClass() {
return new String[0];
}
@Override
public String getModContainerClass() {
return null;
}
@Override
public String getSetupClass() {
return null;
}
@Override
public void injectData(Map<String, Object> data) {
}
@Override
public String getAccessTransformerClass() {
return null;
}
}
Вот так вот:
Kotlin:
jar {
manifest {
attributes 'FMLCorePlugin': 'ru.justagod.asmtutor.HookLoader'
attributes 'FMLCorePluginContainsFMLMod': 'true'
}
}
Код:
-Dfml.coreMods.load=ru.justagod.asmtutor.HookLoader
Метод который нас больше всего интересует в данный момент это getASMTransformerClass. Он должен вернуть полные имена ваших классов которые будет заниматься изменениями байт-кода. Мы не будем особо тут париться и сделаем все в одном классе. Вот так вот:
Java:
@MCVersion(value = "1.7.10")
@SuppressWarnings("unused")
public class HookLoader implements IFMLLoadingPlugin, IClassTransformer {
@Override
public String[] getASMTransformerClass() {
return new String[] {getClass().getName()};
}
@Override
public String getModContainerClass() {
return null;
}
@Override
public String getSetupClass() {
return null;
}
@Override
public void injectData(Map<String, Object> data) {
}
@Override
public String getAccessTransformerClass() {
return null;
}
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
return basicClass;
}
}
Что ж возвращаясь к нашей идеи альтернативных AT, нам нужно создать класс Acessor в который как раз поместим наш синтетический метод, допустим, doGet. Зачем нам создавать дополнительный класс, спросите вы. А все просто мы будем изменять этот класс перед его загрузкой, а наш класс к тому времени уже будет загружен, в результате чего ничего работать не будет. Допустим мы хотим получить переменную major в классе cpw.mods.fml.common.Loader. Тогда наш Acessor будет выглядить так:
Java:
public class Acessor {
public static int doGet() {
return 0;
}
}
Предварительная подготовка завершена!
Переходим к ASM!
Значит так. ASM - это просто понтовая библиотека для работы с байт кодом. При желании вы можете легко сами находить не нужные вам байты и заменять на нужные, но ASM делает этот процесс просто более удобным.
Как же она работает? Для начала немного бесполезной информации. У ASM есть 2 API: Core API (О нем и пойдет речь) и Tree API. Core API проходит класс по мере поступления, а Tree API похож на Javac сначала все анализирует и записывает, а потом дает полную информацию о классе. Core API работает быстрее по понятным причинам. Камень средоточия в ASM это ClassVisitor. Давайте я приведу код использования, а потом объясню что да как.
Java:
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
switch (transformedName) {
case "ru.justagod.asmtutor.Acessor": {
ClassReader cr = new ClassReader(basicClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
};
cr.accept(cv, ClassReader.SKIP_FRAMES);
return cw.toByteArray();
}
}
return basicClass;
}
Вуаля:
Java:
public class JustAVisitor extends ClassVisitor {
public JustAVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(Opcodes.ASM5, super.visitMethod(access, name, desc, signature, exceptions)) {
@Override
public void visitCode() {
super.visitFieldInsn(Opcodes.GETSTATIC, "cpw/mods/fml/common/Loader", "major", "I");
super.visitInsn(Opcodes.IRETURN);
}
};
}
}
Java:
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
switch (transformedName) {
case "ru.justagod.asmtutor.Acessor": {
ClassReader cr = new ClassReader(basicClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
JustAVisitor cv = new JustAVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES);
return cw.toByteArray();
}
}
return basicClass;
}
Все минута прошла! Как я и говорил у ClassVisitor есть методы типа visitMethod, но я не сказал, что они возвращают MethodVisitor. По мне так это довольно логичный шаг так как в методе довольно много отличий от класса. Наш ClassVisitor получился довольно костыльным, но он работать будет так как мы точно знаем что у класса всего один метод и у него всего две инструкции.
Кстати вот так выгляит байт-код аксесора
Код:
public static doGet()I
L0
LINENUMBER 9 L0
ICONST_0
IRETURN
MAXSTACK = 1
MAXLOCALS = 0
Вот так вот:
Java:
public class JustALoaderVisitor extends ClassVisitor {
public JustALoaderVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
if (name.equals("major"))
return super.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, name, desc, signature, value);
else
return super.visitField(access, name, desc, signature, value);
}
}
Java:
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
switch (transformedName) {
case "ru.justagod.asmtutor.Acessor": {
ClassReader cr = new ClassReader(basicClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
JustAVisitor cv = new JustAVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES);
return cw.toByteArray();
}
case "cpw.mods.fml.common.Loader": {
ClassReader cr = new ClassReader(basicClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
JustALoaderVisitor cv = new JustALoaderVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES);
return cw.toByteArray();
}
}
return basicClass;
}
На этом все. Спасибо за внимание.
Код вы можете найти тут.
Acess transformers