[Не просто][Не легко]Хуки

[Не просто][Не легко]Хуки

Версия(и) Minecraft
любая
Итак, согласитесь, частенько бывают ситуации когда очень хочеться изменить стандартное поведение майна добавив всего одно дополнительную строку или изменить какую нибудь переменую или модификаторы доступа. Не спорю для этого есть глуми с его либой да и AT не плохи, но я уверен вам интересно как это работает изнутри, не так ли?

Так вот, прежде чем начать нам нужно познакомиться с понятием байт-кода. Ни для кого не секрет, что 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
}
Прежде чем я объясню как работает байт-код, вам нужно четко понимать, что букафы, которые вы видите в байт-коде это не более чем удобство предоставляемое нам просмотращиком байт кода, встроенным в InteliJ IDEA. На самом деле тот же RETURN, в байт-коде, будет записан как 177(Один байт 10110001).
Так вот, перейдем же к рассмотрению байт-кода.
Код:
// 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'
   }
}
Так же, если вы в среде разработки вам нужно добавить аргумент JVM
Код:
-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;
    }
}
Давайте рассмотрим метод transform. На вход нам дают 3 параметра: name, transformedName и basicClass. name - это обфусцированый вариант названия класса, transformedName - деобфусцированный а basicClass как раз тот самый байт-код. Так как мы пока никаких изменений не делаем, наш метод просто возвращает изначальный байт-код.
Что ж возвращаясь к нашей идеи альтернативных AT, нам нужно создать класс Acessor в который как раз поместим наш синтетический метод, допустим, doGet. Зачем нам создавать дополнительный класс, спросите вы. А все просто мы будем изменять этот класс перед его загрузкой, а наш класс к тому времени уже будет загружен, в результате чего ничего работать не будет. Допустим мы хотим получить переменную major в классе cpw.mods.fml.common.Loader. Тогда наш Acessor будет выглядить так:
Java:
public class Acessor {
  
    public static int doGet() {
        return 0;
    }
}
Пишем мы тут 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;
    }
Дамы и господа, перед вами абсолютно бесполезный кусок кода, но щас объясню. ClassReader наследует от Reader так что он в принципе может вам по байтам считать, но это было бы глупо, поэтому мы используем его прекрасный метод accept. accept проходиться по всему классу вызывая нужные методы. Допустим когда он видит метод он вызывает visitMethod когда поле visitField И так далее. ClassWriter являеться ClassVisitor. Звучит стремно, но это довольно удобно. Вы можете методу accept передать ClassWriter и он просто запишет то что ему дадут посетить. Я в коде оборачиваю ClassWriter в свой ClassVisitor. Пока в этом смысла мало, но это пока. Давайте напишем свой ClassVisitor!
Вуаля:
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
Но приватность поля major никто не отменял, поэтому мы сделаем еще один ClassVisitor!
Вот так вот:
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);
    }
}
Здесь если имя поля 'major' мы делаем подмену модификаторов доступа на public static (см. Бинарные операции). Так же в трансформатор допишем пару строк:
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;
    }
Теперь вы можете в любой момент работы программы вызвать метод doGet и он вернет вам поле major класса Loader.

На этом все. Спасибо за внимание.

Код вы можете найти тут.
Автор
JustAGod
Просмотры
4,559
Первый выпуск
Обновление
Оценка
5.00 звёзд 4 оценок

Другие ресурсы пользователя JustAGod

Последние рецензии

Ну-с, можно начать сериалайзить ItemStack с интовым количеством... Там молодцы не дали доступа к капабилити тэгу, не скопируешь нормально.
JustAGod
JustAGod
Кхе.
Acess transformers
За старание, теперь GloomyFolkenHooksLibs станет в 10.000 раз больше.(нет)
JustAGod
JustAGod
Кажется я не понял шутку._.
Наконец займусь хуками. Спасибо)
Сверху