Компилируем класс вручную

10 янв 2024

Возьмем для примера следующий класс

public final class Test {
  public static void main(String[] args) {
  }
}

Попробуем "скомпилировать его вручную", создав class-файл без компилятора javac.

Об обозначениях

В данной статье будем использовать шеснадцатиричную систему для отображения чисел и массивов байтов. Общеупотребительного префикса "0x" не будет, это надо иметь ввиду.

Порядок байтов будет big-endian (от старшего к младшему). Поэтому, число 307 будет записано как 0133, а не 3301. Этот же порядок байтов используется и в самом class-файле (согласно спецификации).

В статье, также будет встречаться следующие обозначения:

Формат class-файла

Формат class-файла описан в спецификации JVM, которую можно найти по ссылке The class File Format. class-файл можно описать следующей структурой

ClassFile {
  u4             magic;
  u2             minor_version;
  u2             major_version;
  u2             constant_pool_count;
  cp_info        constant_pool[constant_pool_count-1];
  u2             access_flags;
  u2             this_class;
  u2             super_class;
  u2             interfaces_count;
  u2             interfaces[interfaces_count];
  u2             fields_count;
  field_info     fields[fields_count];
  u2             methods_count;
  method_info    methods[methods_count];
  u2             attributes_count;
  attribute_info attributes[attributes_count];
}

Поле magic является константой и принимает значение cafebabe. Поля minor_version и major_version указывают на версию данного class-файла. Мы будем пытаться создать class-файл для Java 8. Для данной версии java minor_version = 0000 и major_version = 0034.

Тут же, сразу и без дополнительных вычислений можно задать значения для полей: access_flags, interfaces_count, interfaces, fields_count, fields.

access_flags - поле, указывающее на модификаторы доступа и тип содержимого в class-файле (обычный это класс, либо enum, interface или аннотация). Данное поле это битовая маска из значений из следующей таблицы

flag name value description
ACC_PUBLIC 0x0001 public класс
ACC_FINAL 0x0010 final класс
ACC_SUPER 0x0020 для спец. обработки вызова методов родителя при invokespecial
ACC_INTERFACE 0x0200 это интерфейс
ACC_ABSTRACT 0x0400 абстрактный класс
ACC_SYNTHETIC 0x1000 synthetic; не присутствует в исходниках
ACC_ANNOTATION 0x2000 это аннотация
ACC_ENUM 0x4000 это enum

Наш тестовый класс объявлен публичным и финальным, поэтому access_flag = (0001 | 0010) = 0011.

Поля interfaces_count и fields_count будут равны 0000, т.к класс Test не имеет ни интерфейсов, ни полей класса. А массивы interfaces и fields будут пустыми.

Constant pool

constant_pool это специальная таблица, хранящая в себе константы, на которые ссылаются остальные части class-файла. В этой статье для constant pool будет использоваться таблица следующего вида

constant pool index value description

В колонке value будет находится содержимое constant pool в бинарном виде (в hex формате). Далее в статье эта таблица будет наполняться различными значениями. Пока она пустая.

Надо отметить, что constant pool индексируется с 1. Поэтому, когда constant pool будет полностью заполнен, поле constant_pool_count надо будет выставить в значение "количество записей + 1".

this_class

Данное поле содержит в себе индекс в constant pool, где лежит структура типа

CONSTANT_Class_info {
  u1 tag;
  u2 name_index;
}

содержащая описание текущего класса. Для данной структуры tag всегда равно значению 7. name_index это индекс в constant pool, где лежит структура типа

CONSTANT_Utf8_info {
  u1 tag;
  u2 length;
  u1 bytes[length];
}

, которая хранит в себе название данного класса. tag в CONSTANT_Utf8_info всегда равно 1. length это длина строки в байтах, а bytes содержит непосредственно строку в виде байтов.

Наш класс называется Test. Для строки "Test" структура CONSTANT_Utf8_info будет выглядеть следующим образом:

CONSTANT_Utf8_info {
  u1 tag = 01         // для CONSTANT_Utf8_info всегда равно 1
  u2 length = 0004    // Строка Test в utf-8 это 4 байта
  u1 bytes = 54657374 // Байты в utf-8
}

или в виде hex-строки 01000454657374. А CONSTANT_Class_info для поля this_class будет выглядеть следующим образом:

CONSTANT_Class_info {
  u1 tag        = 07   // для CONSTANT_Class_info всегда равно 7
  u2 name_index = 0001 // структуру для строки Test поместим в constant pool под индексом 1
}

В итоге в constant pool добавятся следующие записи

constant pool index value description
1 01000454657374 CONSTANT_Utf8_info для строки Test
2 070001 CONSTANT_Class_info для поля this_class

Поле this_class в class-файле получит значение 0002.

super_class

Тут все также, как и с this_class, только данное поле содержит название родительского класса. Для класса Test это класс Object. Нужно будет повторить те же процедуры что и для поля this_class, только в этот раз в структуре CONSTANT_Utf8_info будет строка "java/lang/Object". В результате всех манипуляций в constant pool появится две новых записи

constant pool index value description
3 0100106a6176612f6c616e672f4f626a656374 CONSTANT_Utf8_info для строки java/lang/Object
4 070003 CONSTANT_Class_info для поля super_class

Поле super_class в итоге получит значение 0004.

Метод main

Класс Test имеет только один метод - main. Поэтому поле methods_count примет значение 0001. Массив methods будет содержать одну структуру вида

method_info {
  u2             access_flags;
  u2             name_index;
  u2             descriptor_index;
  u2             attributes_count;
  attribute_info attributes[attributes_count];
}

Поле access_flag это битовая маска из значений из следующей таблицы

flag name value description
ACC_PUBLIC 0x0001 метод объявлен как public
ACC_PRIVATE 0x0002 метод объявлен как private
ACC_PROTECTED 0x0004 метод объявлен как protected
ACC_STATIC 0x0008 статичный метод
ACC_FINAL 0x0010 метод объявлен как final
ACC_SYNCHRONIZED 0x0020 метод объявлен с synchronized
ACC_BRIDGE 0x0040 bridge метод сгенерированный компилятором
ACC_VARARGS 0x0080 метод с varargs
ACC_NATIVE 0x0100 метод объявлен как native
ACC_ABSTRACT 0x0400 метод объявлен как abstract
ACC_STRICT 0x0800 метод объявлен с strictfp
ACC_SYNTHETIC 0x1000 synthetic метод; не присутствует в исходниках

Метод main объявлен как public static. access_flag будет равен (0001 | 0008) = 0009.

name_index - это индекс в constant pool, который содержит уже знакомую структуру CONSTANT_Utf8_info с именем метода. Для строки "main" эта структура будет выглядеть как

CONSTANT_Utf8_info {
  tag    = 01       // для CONSTANT_Utf8_info всегда равно 1
  length = 0004     // Строка main в utf-8 это 4 байта
  bytes  = 6d61696e // Байты в utf-8
}

или в виде hex-строки 0100046d61696e. Эту структуру надо добавить в constant pool

constant pool index value description
5 0100046d61696e CONSTANT_Utf8_info для строки main

В итоге, name_index получит значение 0005.

descriptor_index содержит индекс в constant pool, в котором хранится структура CONSTANT_Utf8_info с описанием метода в формате ({аргументы метода}){возвращаемый тип}. Аргументы и возвращаемый тип метода должны быть в специальном формате, который можно найти в спецификации в разделе "4.3.3. Method Descriptors". Для нашего примера {аргументы метода} будет строкой "[Ljava/lang/String;", а {возвращаемый тип} будет строкой "V". Получается, что в CONSTANT_Utf8_info должна хранится строка "([Ljava/lang/String;)V"

CONSTANT_Utf8_info {
  tag    = 01       // для CONSTANT_Utf8_info всегда равно 1
  length = 0016     // Строка ([Ljava/lang/String;)V в utf-8 это 22 байта
  bytes  = 285b4c6a6176612f6c616e672f537472696e673b2956 // Байты в utf-8
}

или в ввиде hex-строки 010016285b4c6a6176612f6c616e672f537472696e673b2956. В constant pool появится новая запись

constant pool index value description
6 010016285b4c6a6176612f6c616e672f537472696e673b2956 CONSTANT_Utf8_info для descriptor_index

descriptor_index получит значение 0006.

В attributes_count мы выставим значение 0001, т.к добавим в аттрибуты метода только один аттрибут - Code. Аттрибут Code будет содержать в себе код метода. Общий вид аттрибута Code следующий

Code_attribute {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 max_stack;
  u2 max_locals;
  u4 code_length;
  u1 code[code_length];
  u2 exception_table_length;
  {   u2 start_pc;
      u2 end_pc;
      u2 handler_pc;
      u2 catch_type;
  } exception_table[exception_table_length];
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

attribute_name_index - это ссылка на constant pool, где содержится CONSTANT_Utf8_info с именем аттрибута (в нашем случае это строка "Code").

CONSTANT_Utf8_info {
  tag    = 01       // для CONSTANT_Utf8_info всегда равно 1
  length = 0004     // Строка Code в utf-8 это 4 байта
  bytes  = 436f6465 // Байты в utf-8
}

В constant pool попадет новая запись под индексом 7

constant pool index value description
7 010004436f6465 CONSTANT_Utf8_info для строки "Code"

attribute_name_index будет иметь значение 0007.

Чтобы задать значение для attribute_length необходимо сперва вычислить содержимое остальных полей.

max_stack примет значение 0000, т.к наш метод пустой и не будет использовать стек. А так как в методе будет только одна локальная переменная (аргумент метода), то max_locals примет значение 0001.

Поле code хранит непосредственно инструкции JVM для метода. Наш метод main пустой и ничего не делает, но согласно спецификации, в список инструкции нужно добавить инструкцию return = (0xb1).

Поля exception_table_length и attributes_count мы выстами в значение 0000, т.к наш метод не содержит try-catch и ссылки на другие аттрибуты.

Учитывая все это, поле attribute_length примет значение 0000000d, а весь аттрибут Code_attribute в бинарном виде будет выглядеть следующим образом

Code_attribute {
  u2 attribute_name_index   = 0007
  u4 attribute_length       = 0000000d
  u2 max_stack              = 0000
  u2 max_locals             = 0001
  u4 code_length            = 00000001
  u1 code                   = b1
  u2 exception_table_length = 0000
  xx exception_table        = 
  u2 attributes_count       = 0000
  attribute_info attributes = 
}

В итоге method_info будет иметь следующий вид

method_info {
  u2 access_flags           = 0009
  u2 name_index             = 0005
  u2 descriptor_index       = 0006
  u2 attributes_count       = 0001
  attribute_info attributes = 00070000000d0000000100000001b100000000
}

а сам class-файл

ClassFile {
  magic               = cafebabe
  minor_version       = 0000
  major_version       = 0034
  constant_pool_count = 0008
  constant_pool       = 010004546573740700010100106a6176612f6c616e672f4f626a6563740700030100046d61696e010016285b4c6a6176612f6c616e672f537472696e673b2956010004436f6465
  access_flags        = 0011
  this_class          = 0002
  super_class         = 0004
  interfaces_count    = 0000
  interfaces[interfaces_count] = 
  fields_count        = 0000
  fields = 
  methods_count       = 0001
  methods             = 000900050006000100070000000d0000000100000001b100000000
  attributes_count    = 0000
  attributes = 
}

Теперь необходимо создать файл с именем Test.class и записать туда следующий массив байтов:

cafebabe000000340008010004546573740700010100106a6176612f6c616e672f4f626a6563740700030100046d61696e010016285b4c6a6176612f6c616e672f537472696e673b2956010004436f6465001100020004000000000001000900050006000100070000000d0000000100000001b1000000000000

Создание class-файла

Если есть утилита xxd

Необходимо создать файл Test.txt и записать туда получившуюся hex-строку (см. выше). Затем надо просто запустить команду:

xxd -r -p Test.txt /tmp/Test.class

Если установлена java17

Если у вас установлена Java 17 и выше, то можно просто открыть jshell и выполнить следующие команды:

String hexStr = "cafebabe00000034...0000"

java.nio.file.Files.write(java.nio.file.Paths.get("/tmp/Test.class"), java.util.HexFormat.of().parseHex(hexStr))

/exit

Проверка и запуск

java -cp /tmp Test

Т.к метод main был пустым, то ничего не должно произойти. Программа просто закончит свою работу и все. Тут главное, чтобы java не выдала ошибку.

Интереснее будет вывод команды javap. Необходимо добавить флаги -v и -c, и утилита распечатает подробную инфомацию о class-файле:

javap -v -c /tmp/Test.class