В рамках текущей статьи будет рассказано о способах инструментации байт-кода java или, другим языком, внесения изменений в компилированные файлы java .class. Здесь будут приведены примеры работы с фреймворками Javaassist и ASM и базовое описание байт-кода. Инструментация байт-кода проводится путем внедрения необходимых инструкций перед определенными операциями. Каждая операция описывается собственным набором инструкций. Список инструкций можно посмотреть здесь, на страничке официальной документации. В результате компиляции кода создаются файлы формата .class, которые можно упаковать в jar-файл. Формат .class файла имеет следующий вид: подробное описание формата можно найти в официальной документации или в демонстрационном коде разбора формата файла  Заголовок class всегда начинается с сигнатуры CAFEBABE, после чего следует версия формата класса и размер его заголовка. Этими полями, соответственно, являются magic, version, constant_pool_size. Описание некоторых интересных полей файла: magic — 0xCAFEBABE; constant_pool — набор констант, содержащих в себе описание классов, методов, полей класса, статических строк и другие значения, которые были заданы в коде; attributes — содержат в себе информацию, которая расширяет заданные в constant_pool значения. В файле находятся после массива constant_pool. Формат поля attributes:  Теперь создадим тестовый класс и декодируем его: Из декодированного класса интересуют следующие атрибуты: Code — содержит в себе непосредственно байт-код, который будет выполняться; StackMapTable — содержит в себе размер стека, необходимый для вычисления размера frame. минутка компиляции документации  [1, 2, 3] Java требует проверки всех загружаемых классов на уровне байт-кода на то, что байт-код имеет смысл в соответствии с его правилами. Помимо прочего, проверка байт-кода гарантирует, что инструкции правильно сформированы, что все переходы осуществляются к действительным инструкциям внутри метода и что все инструкции работают со значениями правильного типа.  Проверка байт-кода замедляет загрузку классов в Java. Oracle решила эту проблему, добавив новый, более быстрый верификатор, который может проверять байт-код за один проход. Для этого они потребовали, чтобы все новые классы, начиная с Java 7 (с Java 6 в переходном состоянии), содержали метаданные о своих типах. Поскольку сам формат байт-кода изменить нельзя, информация об этом типе хранится отдельно в атрибуте с именем StackMapTable. Каждый поток виртуальной машины Java имеет частный стек виртуальной машины Java, созданный одновременно с потоком. Стек виртуальной машины Java хранит фреймы(frame). Frame используется для хранения локальных данных и частичных результатов выполнения байт-кода, динамического связывания, возврата значений для методов и отправки исключений. На каждый вызов метода jrm создает новый фрейм, который содержит в себе локальный стек, выделенный из стека виртуальной машины java.  Каждый фрейм имеет свой собственный массив локальных переменных, свой собственный стек операндов и ссылку на пул констант времени выполнения класса текущего метода. Размеры массива локальных переменных и стека операндов определяются во время компиляции и предоставляются вместе с кодом метода, связанного с фреймом. Заданы различные типы фреймов:  same_frame;  same_locals_1_stack_item_frame;  same_locals_1_stack_item_frame_extended;  chop_frame;  same_frame_extended;  append_frame;  full_frame. Данные типы фрейма определяют его размер и смещение в зависимости от определенных в них размеров массива локальных переменных, стека операндов и пула констант.  Структура StackMapTable имеет следующий формат: StackMapTable_attribute {      u2              attribute_name_index;      u4              attribute_length;      u2              number_of_entries;      stack_map_frame entries[number_of_entries]; } Каждая stack_map_frame структура из структуры StackMapTable определяет состояние типа фрейма по определенному смещению в байт-коде. Каждый тип фрейма задает значение offset_delta, которое используется для вычисления фактического смещения байт-кода, при котором применяется фрейм. Смещение байт-кода, при котором применяется фрейм, вычисляется путем прибавления offset_delta + 1 к смещению байт-кода предыдущего фрейма, если только предыдущий фрейм не является начальным, в этом случае смещение байт-кода равно offset_delta. При использовании дельты смещения, а не фактического смещения байт-кода гарантируется, что кадры карты стека находятся в правильном порядке и дубликаты фрейма отсутствуют. Следовательно, при ручном вмешательстве в байт-код необходимо произвести пересчет фреймов. Приведем пример декодирования одного поля Code: На картинке выше видно поле Code = 0x1b36420375b1, его декодирование равно следующей строке опкодов: iload_1, istore, 4, lload_2, lstore 5, return, что, соответственно, равно следующему коду: Имя, тип возвращаемого значения, аргументы находятся в других атрибутах. Наиболее популярными фреймворками для инструментации байт-кода являются: javaassist;  ASM. По сравнению данных фреймворков есть хорошие статьи: Короткая Подробная Здесь приведем краткую выдержку. API Javaassist значительно проще в использовании и лучше документировано, чем фреймворк ASM. Javaassist фактически скрывает в себе операции манипулирования байт-кодом, программист пишет код java, который уже средствами библиотеки транслируется в байт-код и внедряется в уже существующие классы. Но за фактической легкостью использования javaassit скрывается его ограниченность в задачах по манипуляции байт-кода, например, данный фреймворк не позволяет производить внедрение исполняемых инструкций в произвольное место класса. Учитывая, что javaassist использует механизм отражения кода java в байт-код, это делает работу фреймворка значительно более медленной, чем фреймворка ASM, разница в скорости работы может составлять более 5-ти раз. Таким образом, область приложения Javassist — это динамическое управление и создание классов Java, которое не требует высокой производительности. Для других же задач, связанных с манипуляцией байт-кода, где также требуется высокая скорость работы выбирается фреймворк ASM.   Javassist.  Пример простого приложения, которое внедряет код в jar-файл.  Разберем его.  На вход приложению поступает jar-файл, в который необходимо внедрить код. Данный файл необходимо загрузить в classloader: Далее производится инициализация javassist. ClassPool создает пул классов CtClass из classloader, передача загрузчика в пул осуществляется классом LoaderClassPath: Необходимо дать уточнение, что по документации предлагается использовать javaagent, но так как в данном случае создается статический инструментатор, приложение пишется в обычной форме.    Получив пул классов, которые мы будем инструментировать, необходимо их перебрать и передать на инструментацию. В случае с javaagent данное действие не требуется, так как агент берет классы под инструментацию из classloader. Перебор классов будем делать по jar-файлу. Каждый класс, прочитанный из jar, необходимо привести к виду package.class. По имени класса извлекается объект из пула классов: Внедрим код в начало каждого метода заданного класса, для этого получаем массив методов CtMethod ctm[] = cc.getDeclaredMethods();.  Для вставки кода в начало метода используется метод insertBefore. Обратим внимание, что внедряемый код описывается строкой: String.format( "System.out.println(" injected code to method %s ");", mName ) После того как было проведено внедрение, необходимо заново сформировать байт-код класса и записать его изменения.  Производится это следующим образом: В аргументах writeFile указывается имя директории, в которую будет сохранен модифицированный класс. Соответственно, при запуске кода из репозитория с примером в стартовом каталоге появится папка instrumentedClasses , в которой будут модифицированные классы; чтобы закончить инструментацию, необходимо внедрить их в jar-файл: Внедрение модифицированных классов в тестовое приложение test-1.0.jar: jar uf test-1.0.jar * Продемонстрируем работу инструментатора. Запуск тестового приложения: Запуск того же приложения после инструментации: По аналогии с примером javaassist на вход инструментатора подается jar-файл, далее производятся манипуляции с классами, и их измененные копии сохраняются в указанную директорию. Пример приложения, реализующего фреймворк ASM. Чтобы проводить манипуляции с классами, необходимо преобразовать их в байтовый поток InputStream: Байтовый массив byte[] out содержит в себе результат преобразования класса, который необходимо сохранить в директорию: Теперь опишем работу функции преобразования класса Transformer.transform: Класс ClassReader  выполняет чтение инструментируемого класса, а затем — его преобразование с использованием класса ClassVisitor. Класс ClassWriter (public class ClassWriter extends ClassVisitor)  производит трансляцию модифицированного байт-кода в непосредственно двоичный файл. Абстрактный класс ClassVisitor  предоставляет реализацию API ASM и позволяет производить модификацию байт-кода. Реализация класса построена на механизме методов «визиторов»: посещение каждого опкода байт-кода регистрируется фреймворком и формируется событие, которое может перехватить пользовательский обработчик и произвести манипуляцию с кодом. Класс ClassInstrumentation (public class ClassInstrumentation extends ClassVisitor) — пользовательский класс, реализующий абстрактный класс ClassVisitor: Реализация класса строится путем перекрытия методов суперкласса ClassVisitor. Например, метод public MethodVisitor visitMethod реализует событие посещения первой инструкции метода. При посещении метода его преобразование производится классом MethodTransformer: Класс MethodTransformer реализует абстрактный класс MethodVisitor , в котором мы добавляем вызов System.out.println("this instrumented method " + name); , для этого перегружается метод public void visitCode(). В нем добавляется вызов пользовательского метода модификации байт-кодаinstr();.Итак, запустим инструментатор, на вход подается тестовый файл test-1.0.jar: Результат инструментации сохраняется в формате .class в каталог ModifyClasses, где необходимо записать изменения в jar-файл (jar uf test-1.0.jar *): Запуск неинструментированного jar-файла test-1.0.jar: И результат запуска инструментированного jar: Инструментация байт-кода во время выполнения или динамическая инструментация выполняется, используя опцию: java: -javaagent  Javaagent представляет из себя программу, которая запускается перед запуском основной программы. Пример: java: -javaagent:myagent.jar -jar myjar.jar Javaagent реализует пакет java.lang.instrument, который позволяет производить инструментацию загружаемого класса в jrm.  Каждый javaagent должен реализовывать метод premain, имеющий следующие определения: Класс Instrumentation [Instrumentation (Java SE 17 & JDK 17) (oracle.com)] предоставляет интерфейс для инструментации. Получая экземпляр класса, javaagent регистрирует инструментатор (JavaTransformer jTransformer) в jrm, который будет запускаться каждый раз, когда classloader будет загружать исполняемый класс в java машину: Пользовательский инструментатор байт-кода должен реализовывать класс ClassFileTransformer [ClassFileTransformer (Java SE 17 & JDK 17) (oracle.com)], а конкретно его метод public byte[] transform(...) Метод transform выполняет непосредственную инструментацию байт-кода, получая байт-массив класса: Далее, получив байт-код класса, выполняется инструментация по одному из описанных выше методов инструментации: Стоит вопрос о применении инструментации: зачем она нужна? В данном случае работа велась для создания фаззера Java, скрещения ежа и ужа, WinAFL и Spring. О чем и будет рассказано позже. Спасибо за внимание ;) Мера покрытия кода: https://docs.microsoft.com/ru-ru/previous-versions/visualstudio/visual-studio-2015/test/using-code-coverage-to-determine-how-much-code-is-being-tested?view=vs-2015&redirectedfrom=MSDN; Инструкции байт-кода: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html; Описание формата class файла: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1; Демонстрационный код разбора class файла: https://github.com/yrime/BytecodeDecoder.git; Описание фрейма байт кода: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6; https://stackoverflow.com/questions/25109942/what-is-a-stack-map-frame; https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.15; Сравнение фреймворков ASM и JavaAssist: https://blog.spacepatroldelta.com/a?ID=01550-464f0477-df0a-44a4-8e9d-fabe8aafdbe7; https://newrelic.com/blog/best-practices/diving-bytecode-manipulation-creating-audit-log-asm-javassist; Документация фреймворка ASM: https://asm.ow2.io/documentation.html; Документация фреймворка javaassit: https://www.javassist.org/tutorial/tutorial.html; Демонстрационный код использования фреймворка javaassit: https://github.com/yrime/JAssist_test.git; Демонстрационный код использования фреймворка ASM: https://github.com/yrime/JASM_test.git;  Документация на опцию javaagent:https://docs.oracle.com/en/java/javase/17/docs/api/java.instrument/java/lang/instrument/package-summary.html;https://docs.oracle.com/en/java/javase/17/docs/api/java.instrument/java/lang/instrument/Instrumentation.html;https://docs.oracle.com/en/java/javase/17/docs/api/java.instrument/java/lang/instrument/ClassFileTransformer.html;