Learn · concepts
Что такое байт-код JVM? Объяснение файлов классов Java
Байткод JVM — это компактный набор инструкций, который выполняет виртуальная машина Java. Узнайте, как работают файлы .class, почему моды переписывают байткод и какое место ASM занимает в этом процессе.
Что такое байт-код JVM?
Байт-код JVM — это набор инструкций, который выполняет виртуальная машина Java. Когда вы компилируете исходный код Java, компилятор не создает машинный код для вашего процессора. Он создает байт-код — компактный, переносимый формат, хранящийся в файлах .class. JVM считывает эти инструкции во время выполнения и преобразует их в машинный код по мере необходимости.
Именно такой многослойный подход — от исходного кода к байт-коду и затем к машинному коду — обеспечивает переносимость Java. Один и тот же файл .class может работать в Windows, macOS, Linux или любой другой системе с совместимой JVM, поскольку JVM — это слой, который знает, как выполнять инструкции на локальной машине. Напишите один раз, запускайте где угодно.
Как исходный код становится байт-кодом
Файл .java — это текст, написанный человеком. Файл .class — это скомпилированный вывод, который запускает JVM. Компилятор считывает ваш исходный код, проверяет его и записывает один файл .class на каждый класс, содержащий инструкции байт-кода, а также таблицу констант, таблицы методов и другие метаданные.
Байт-код не привязан к какому-либо конкретному процессору. Один и тот же файл .class может работать в Windows, macOS, Linux или на любом устройстве с Java runtime, поскольку JVM — это слой, который знает, как выполнять инструкции на локальной машине. Именно эта переносимость является сутью формата.
Как выглядят инструкции
Байткод — это набор инструкций, основанный на стеке. Большинство операций помещают значения в операндную стек, извлекают их из неё и помещают результат обратно. Нет необходимости рассматривать распределение регистров; модель намеренно проста, чтобы JVM могла быстро проверять и выполнять её.
Несколько примеров опкодов дают представление об этом:
| Опкод | Что он делает |
|---|---|
iload | Помещает локальную переменную типа int в стек |
iadd | Извлекает два int из стека, помещает в стек их сумму |
invokevirtual | Вызывает метод экземпляра |
getfield | Читает поле экземпляра |
return | Возвращается из метода |
Вы редко читаете или пишете их вручную. Инструменты генерируют и потребляют их, и здесь вступает в игру такая библиотека, как ASM.
Почему байткод важен для модов
Байткод важен, потому что он позволяет изменять поведение без исходного кода. Мод для Minecraft почти никогда не имеет исходный код игры. Вместо этого у него есть скомпилированная игра на диске. Чтобы добавить или изменить функцию, мод считывает существующий байткод, переписывает его и передаёт результат обратно в JVM при загрузке классов.
Именно так фреймворки патчинга вставляют хуки в методы, которые им не принадлежат. Они находят правильное место в скомпилированном методе и вставляют вызов кода мода. Работа на уровне байткода означает, что мод может подключаться к игре, даже не видя ни одной строки оригинального Java.
На практике именно так Terminus и другие клиенты на базе Fabric изменяют Minecraft. Вместо наличия исходного кода Minecraft, они работают с скомпилированными файлами .class и переписывают байткод, добавляя такие функции, как модули, экраны настроек и пользовательская отрисовка.
Где ASM вписывается
ASM — это Java-библиотека для чтения, записи и преобразования байткода. Она разбирает файл .class на события или дерево, позволяет коду изменять инструкции и выдаёт обратно валидный файл .class. Она небольшая, быстрая и работает в непосредственной близости от необработанного формата, поэтому более высокоуровневые инструменты моддинга строятся на её основе, а не разбирают файлы классов самостоятельно.
Большинство моддеров никогда напрямую не вызывают ASM. Они используют более высокоуровневый слой, который скрывает опкоды и предоставляет более удобный способ сказать: «запусти мой код в начале этого метода». Под этим слоем происходит редактирование байткода, часто с использованием ASM.
Декомпиляция байткода обратно в исходный код
Декомпиляторы, такие как Fernflower и CFR, берут скомпилированные файлы .class и воссоздают читаемый Java-код из байткода. Процесс не идеален: некоторые имена и комментарии теряются, некоторые конструкции сложнее восстановить, чем другие, а обфусцированный код становится нечитаемым. Но для обычного, неофусцированного кода декомпиляторы обычно выдают результат, достаточно близкий к исходному, чтобы его понять и с ним работать.
Это полезно для изучения работы игры, но не заменяет наличие самого исходного кода. Декомпилированный код по-прежнему является скомпилированной логикой без комментариев, а воссозданные имена переменных могут быть вводящими в заблуждение. Это инструмент для чтения, а не для написания.
Часто задаваемые вопросы
Нет. Машинный код — это собственные инструкции для конкретного ЦП. Байткод — это собственный набор инструкций JVM, который JVM преобразует в машинный код во время выполнения. Именно этот дополнительный уровень позволяет файлу .class работать на любой платформе.
Частично. Декомпиляторы восстанавливают читаемый Java-код из байт-кода, и у них неплохо получается с обычным кодом. Результат не всегда идентичен исходному, и комментарии, а также некоторые локальные имена теряются, но этого обычно достаточно, чтобы разобраться.
Поскольку моды обычно не имеют исходного кода, они распространяются поверх скомпилированной игры. Переписывание байт-кода позволяет моду изменять поведение, не затрагивая и даже не имея доступа к исходным файлам Java.
Обычно нет. Модификационные фреймворки и слои скриптинга выполняют работу с байт-кодом за вас. Понимание принципов работы помогает, когда нужно отладить сложный патч или создать инструменты.