Скомпилируем простенькую программу выводящую "Hello World" и пройдемся по его структуре


Не думаю, что статья будет достаточно информативной для тех, кто поверхностно не знает как выглядит байт-код и как с ним работает JVM (например, хотя бы простейшие инструкции (знание об их существовании)).


На самом деле, это не так сложно. Достаточно использовать инструмент javap из JDK и рассмотреть дизассемблированный код.


А мы приступим к разбору самой структуры байт-кода для JVM


Очень полезной книгой для этого стала официальная спецификация JVM — The Java Virtual Machine Specification на сайте oracle


Для начала создадим простенькую программу:


    public class Main {
        public static void main(String ... args) {
            System.out.println("Hello World");
        }
    }

Скомпилируем её командой javac Main.java и собственно сделаем дизассемблинг


    javap -c -v Main

Main.class


Classfile /C:/Users/Arthur/playground/java/jvm/Main.class
  Last modified 26.10.2019; size 413 bytes
  MD5 checksum 6449121a3bb611fee394e4f322401ee1
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World
   #4 = Methodref          #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // Main
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Main.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               Main
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  public Main();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3        // String Hello World
         5: invokevirtual #4// Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Main.java"

Это просто представление байт-кода, которое человеку видеть легче, чем оригинальный байт-код, но сам он выглядит иначе:


    cafe babe 0000 0034 001d 0a00 0600 0f09
    0010 0011 0800 120a 0013 0014 0700 1507
    0016 0100 063c 696e 6974 3e01 0003 2829
    5601 0004 436f 6465 0100 0f4c 696e 654e
    756d 6265 7254 6162 6c65 0100 046d 6169
    6e01 0016 285b 4c6a 6176 612f 6c61 6e67
    2f53 7472 696e 673b 2956 0100 0a53 6f75
    7263 6546 696c 6501 0009 4d61 696e 2e6a
    6176 610c 0007 0008 0700 170c 0018 0019
    0100 0b48 656c 6c6f 2057 6f72 6c64 0700
    1a0c 001b 001c 0100 044d 6169 6e01 0010
    6a61 7661 2f6c 616e 672f 4f62 6a65 6374
    0100 106a 6176 612f 6c61 6e67 2f53 7973
    7465 6d01 0003 6f75 7401 0015 4c6a 6176
    612f 696f 2f50 7269 6e74 5374 7265 616d
    3b01 0013 6a61 7661 2f69 6f2f 5072 696e
    7453 7472 6561 6d01 0007 7072 696e 746c
    6e01 0015 284c 6a61 7661 2f6c 616e 672f
    5374 7269 6e67 3b29 5600 2100 0500 0600
    0000 0000 0200 0100 0700 0800 0100 0900
    0000 1d00 0100 0100 0000 052a b700 01b1
    0000 0001 000a 0000 0006 0001 0000 0001
    0089 000b 000c 0001 0009 0000 0025 0002
    0001 0000 0009 b200 0212 03b6 0004 b100
    0000 0100 0a00 0000 0a00 0200 0000 0400
    0800 0500 0100 0d00 0000 0200 0e

(Можете открыть ваш .class файл через Sublime Text указав при этом File-> Save with Encoding -> Hexademical)


С этим кодом мы и будем работать.


Но для начала нам нужно его отформатировать, чтобы не путаться что где находится, а байт-код, на самом деле, имеет вполне жесткую структуру:


    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];
    }

Её вы можете найти в спецификации JVM Chapter 4.1 The ClassFile Structure


Тут все просто — слева указана размерность в байтах, а справа описание.


Разбирать байт-код мы будем в hexadecimal, где каждая цифра занимает 4 бита, а следовательно, на два байта — 4 цифры и на четыре байта — 8 цифр.


magic


magic — это значение, которое идентифицирует формат нашего класса. Он равен 0xCAFEBABE, который имеет свою историю создания.


minor_version, major_version


Это версии вашего class файла. Если мы назовем major_version M и minor_version m, то получаем версию нашего class файла как M.m


Сейчас я сразу буду приводить примеры из нашей программы "Hello World", чтобы посмотреть как они используются:


    cafe babe   -- magic
    0000         -- minor_version
    0034         -- major_version

Его же мы можем видеть в дизассемблированном коде, но уже в десятичной системе счисления:


    ...
    public class Main
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, 
    ...

constant_pool_count


Здесь указывается количество переменных в пуле констант. При этом, если вы решили писать код на чистом байт-коде, то вам обязательно нужно следить за его значением, так как если вы укажете не то значение, то вся программа полетит к чертям (проверено!).


Также следует не забывать, что вы должны писать туда количество_переменных_в_пуле + 1


Итого, получаем:


    cafe babe   -- magic
    0000 0034   -- version
    001d        -- constant_pool_count

constant_pool[]


Каждый тип переменной в пуле констант имеет свою структуру:


    cp_info {
     u1 tag;
     u1 info[];
    }

Здесь все нужно делать последовательно. Сначала считываем tag, чтобы узнать тип переменной и по типу этой переменной смотрим какую структуру имеет последующее его значение info[]


Таблица с тэгами можно найти в спецификации Table 4.3 Constant pool tags


Собственно, вот табличка:


Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

Как ранее уже говорилось, каждый тип константы имеет свою структуру.


Вот, например, структура CONSTANT_Class:


    CONSTANT_Class_info {
        u1 tag;
        u2 name_index;
    }

Структура поля и метода:


    CONSTANT_Fieldref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }

    CONSTANT_Methodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }

Тут важно заметить, что разные структуры, могут иметь разную длину.


Рассмотрим часть нашего кода:


    cafe babe 
    0000 0034 
    001d -- constant_pool_count 
    0a00 0600 0f09 0010 0011 0800 12 ...

Итак, смотрим на структуру константы и узнаем, что первый байт отведен под тип константы. Здесь мы видим 0a (10) — а, следовательно, это CONSTANT_Methodref


Смотрим его структуру:


    CONSTANT_Methodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }

После одного байта для тэга, нам нужно еще 4 байта для class_index и name_and_type_index


    cafe babe 
    0000 0034 
    001d -- constant_pool_count 

    0a 0006 000f -- CONSTANT_Methodref
    0900 1000 1108 0012 ...

Отлично, мы нашли одну из значений пула констант. Идем дальше. Смотрим, 09 — значит тип CONSTANT_Fieldref


Получаем:


    cafe babe 
    0000 0034 
    001d -- constant_pool_count 

    0a 0006 000f -- CONSTANT_Methodref
    09 0010 0011 -- CONSTANT_Fieldref
    08 0012 ...

Вам может показаться, что большинство типов имеет одинаковую форму, но это не так.
Например, структура следующего типа выглядит так, CONSTANT_String:


    CONSTANT_String_info {
        u1 tag;
        u2 string_index;
    }

Все эти структуры можно посмотреть в Chapter 4.4 The Constant Pool


Теперь разберем, что значат типы внутри самого info


Методы, которые попадают под паттерн *_index обычно содержат адрес из таблицы пула констант. Например, class_index на значение с типом CONSTANT_Class_info, а string_index на CONSTANT_Utf8_info


Это же мы можем видеть в дизассемблированном коде:


    #1 = Methodref       #6.#15         // java/lang/Object."<init>":()V
    #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
    #3 = String             #18

    0a 0006 000f  -- CONSTANT_Methodref
    09 0010 0011 -- CONSTANT_Fieldref
    08 0012      -- CONSTANT_String

Также можно выделить представление чисел и строк.


Про представление чисел можно прочитать начиная с главы 4.4.4, а мы пока разберем лишь строки, так как числа пока не входят в программу Hello World


Собственно, вот так представляется строка:


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

Например, наш Hello World:


    01      -- tag
    000b    -- length
    48 65 6c 6c 6f 20 57 6f 72 6c 64    -- bytes[length] // H e l l o   W o r l d

Разбирая весь пул констант байт-кода, получим:


Весь пул констант
    -- [Constant Pool]

    -- methodref 
    0a 0006 000f

    -- fieldref
    09 0010 0011 

    -- string
    08 0012 

    -- methodref
    0a 0013 0014

    -- Class
    07 0015

    -- Class
    07 0016

    -- Utf8
    01 0006 
    3c 69 6e 69 74 3e 

    -- Utf8
    01 0003 
    28 29 56 

    -- Utf8
    01 0004 
    43 6f 64 65 

    -- Utf8
    01 000f 
    4c 69 6e 65 4e 75 6d
    62 65 72 54 61 62 6c 65

    -- Utf8
    01 0004 
    6d 61 69 6e 

    -- Utf8
    01 0016 
    28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
    2f 53 74 72 69 6e 67 3b 29 56 

    -- Utf8
    01 000a 
    53 6f 75 72 63 65 46 69 6c 65

    -- Utf8
    01 0009 
    4d 61 69 6e 2e 6a 61 76 61 

    -- NameAndType
    0c 0007 0008 

    -- Class
    07 0017 

    -- NameAndType
    0c 0018 0019

    -- Utf8
    01 000b 
    48 65 6c 6c 6f 20 57 6f 72 6c 64 

    -- Class
    07 001a

    -- NameAndType
    0c 001b 001c 

    -- Utf8
    01 0004 
    4d 61 69 6e 

    -- Utf8
    01 0010
    6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74

    -- Utf8
    01 0010 
    6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d

    -- Utf8
    01 0003 
    6f 75 74 

    -- Utf8
    01 0015 
    4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 
    72 65 61 6d 3b

    -- Utf8
    01 0013 
    6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 
    65 61 6d 

    -- Utf8
    01 0007 
    70 72 69 6e 74 6c 6e

    -- Utf8
    01 0015 
    28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 
    6e 67 3b 29 56 

    -- [Constant Pool END]

Также, мы можем сравнить его с дизассемблированным кодом:


    Constant pool:
       #1 = Methodref       #6.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #18            // Hello World
       #4 = Methodref       #19.#20  // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #21            // Main
       #6 = Class              #22            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               main
      #12 = Utf8               ([Ljava/lang/String;)V
      #13 = Utf8               SourceFile
      #14 = Utf8               Main.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = Class                     #23            // java/lang/System
      #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
      #18 = Utf8               Hello World
      #19 = Class                      #26            // java/io/PrintStream
      #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
      #21 = Utf8               Main
      #22 = Utf8               java/lang/Object
      #23 = Utf8               java/lang/System
      #24 = Utf8               out
      #25 = Utf8               Ljava/io/PrintStream;
      #26 = Utf8               java/io/PrintStream
      #27 = Utf8               println
      #28 = Utf8               (Ljava/lang/String;)V

Тем самым проверив, что все совпадает, ведь по сути javap просто обрабатывает этот байт-код и показывает нам его в форматированном виде.


Пул констант нужен для инструкций. Например:


      public Main();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1  // ссылается на адрес 1 в пуле констант
             4: return

Подробнее обо всех типах в пуле констант можно узнать в Chapter 4.4 The Constant Pool


Идем дальше по структуре ClassFile


access_flags


Это битовая маска для свойств модификаторов


Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

this_class


Должна содержать адрес на this класса. В нашем случае, она находится по адресу 5:


    Constant pool:
       #1 = Methodref          #6.#15        // java/lang/Object."<init>":()V
       #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #18               // Hello World
       #4 = Methodref          #19.#20     // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #21               // Main
       #6 = Class              #22               // java/lang/Object
      ...

Следует заметить, что структуру этой переменной должна соответствовать CONSTANT_Class_info


super_class


Адрес предка класса. В нашем случае, значение по адресу #6. Ну, и также обязательным является структура значения CONSTANT_Class_info


Имена этих классов заданы в структуре константы CONSTANT_Utf8_info. Если мы посмотрим ячейки #21 и #22, то увидим:


      ...
      #21 = Utf8               Main
      #22 = Utf8               java/lang/Object
      ...

То есть в этих ячейках указан name_index из структуры:


    CONSTANT_Class_info {
        u1 tag;
        u2 name_index;
    }

interfaces_count, fields_count


Их в нашей программе нет, поэтому их значения будут равны 0000, а последующих значений fields[], interfaces[] просто не будет.


Читайте подробнее 4.1 The ClassFile Structure


methods_count


Количество методов. Хоть и в коде мы видим один метод в классе, но, на самом деле, их два. Кроме main метода еще есть конструктор по умолчанию. Поэтому их количество равно двум, в нашем случае.


methods[]


Каждый элемент должен соответствовать структуре method_info описанной в Chapter 4.6 Methods


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

В нашем байт-коде (отформатированном, с комментариями) выглядит это так:


    -- [methods]

    -- public Main();

    0001 --access_flags
    0007 -- name_index
    0008 -- descriptor_index
    0001 -- attributes_count

    -- attribute_info
    0009 -- attribute_name_index (Code)
    0000 001d - attribute_length
    0001 -- max_stack
    0001 -- max_locals
    0000 0005 -- code_length 
    2a b7 00 01 b1  -- code[]

    0000 -- exception_table_length
    0001 --  attributes_count;
    000a -- attribute_name_index
    0000 0006 -- attribute_length
    00 01 00 00 00 01

    -- public static void main(java.lang.String...);

    0089  --access_flags
    000b  -- name_index
    000c  -- descriptor_index
    0001  -- attributes_count

    -- attribute_info
    0009          -- attribute_name_index (Code)
    0000 0025  -- attribute_length
    0002          -- max_stack
    0001          -- max_locals 
    0000 0009  -- code_length 
    b2 00 02 12 03 b6 00 04 b1 -- code[]

    0000 -- exception_table_length
    0001 -- attributes_count
    000a -- attribute_name_index
    0000 000a -- attribute_length
    00 02 00 00 00 04 00 08 00 05 

    -- [methods END]

Разберем по-подробнее структуру методов:


access_flags


Маска модификаторов. Table 4.5 Method access and property flags


Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its package.
ACC_PRIVATE 0x0002 Declared private; accessible only within the defining class.
ACC_PROTECTED 0x0004 Declared protected; may be accessed within subclasses.
ACC_STATIC 0x0008 Declared static.
ACC_FINAL 0x0010 Declared final; must not be overridden (§5.4.5).
ACC_SYNCHRONIZED 0x0020 Declared synchronized; invocation is wrapped by a monitor use.
ACC_BRIDGE 0x0040 A bridge method, generated by the compiler.
ACC_VARARGS 0x0080 Declared with variable number of arguments.
ACC_NATIVE 0x0100 Declared native; implemented in a language other than Java.
ACC_ABSTRACT 0x0400 Declared abstract; no implementation is provided.
ACC_STRICT 0x0800 Declared strictfp; floating-point mode is FP-strict.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.

Как мы можем видеть из байт-кода, в методе public Main(); (конструктор) стоит маска 0001, который означает ACC_PUBLIC.


А теперь сами попробуем собрать метод main. Вот, что у него есть:


  • public — ACC_PUBLIC — 0x0001
  • static — ACC_STATIC — 0x0008
  • String… args — ACC_VARARGS — 0x0080

Собираем маску: 0x0001 + 0x0008 + 0x0080 = 0x0089. Итак, мы получили access_flag


К слову, ACC_VARARGS здесь необязательный, в том плане, что, если бы мы
использовали String[] args вместо String… args, то этого флага бы не было

name_index


Адрес имени метода (CONSTANT_Utf8_info) в пуле констант. Здесь важно заметить, что имя конструктора это не Main, а <init>, расположенная в ячейке #7.


Подробнее о <init> и <clinit> в Chapter 2.9 Special Methods


descriptor_index


Грубо говоря, это адрес указывающий на дескриптор метода. Этот дескриптор содержит тип возвращаемого значения и тип его сигнатуры.


Также, в JVM используются интерпретируемые сокращения:


BaseType Character Type Interpretation
B byte signed byte
C char Unicode character code point in the Basic Multilingual Plane, encoded with UTF-16
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L ClassName ; reference an instance of class ClassName
S short signed short
Z boolean true or false
[ reference one array dimension

В общем случае это выглядит так:


    ( ParameterDescriptor* ) ReturnDescriptor

Например, следующий метод:


    Object method(int i, double d, Thread t) {..}

Можно представить в виде


    (IDLjava/lang/Thread;)Ljava/lang/Object

Собственно, I — это int, D — это double, а Ljava/lang/Thread; класс Thread из стандартной библиотеки java.lang.


Далее, идут атрибуты, которые также имеют свою структуру.


Но сначала, как и всегда, идет его количество attributes_count


Затем сами атрибуты со структурой описанной в Chapter 4.7 Attributes


    attribute_info {
        u2 attribute_name_index;
        u4 attribute_length;
        u1 info[attribute_length];
    }

attribute_name_index


Указание имени атрибута. В нашем случае, у обоих методов это Code. Атрибуты это отдельная большая тема, в котором можно по спецификации создавать даже свои атрибуты. Но нам пока следует знать, что attribute_name_index просто указывает на адрес в пуле констант со структурой CONSTANT_Utf8_info


attribute_length


Содержит длину атрибута, не включая attribute_name_index и attribute_length


info


Далее, мы будем использовать структуру Code, так как в значении attribute_name_index мы указали на значение в пуле констант Code.


Подробнее: Chapter 4.7.3 The Code Attribute


Вот его структура:


    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];
    }

max_stack


Мне кажется, что имя этого атрибута может ввести в заблуждение из-за приставки max. На самом деле, это минимальный размер стека нужный для выполнения операции. Ну, это имя приобретает логику, если сказать, максимальный размер стека, который будет достигнут во время выполнения операции.


Упрощенно говоря, JVM выделит место для стека операндов. Там можно указывать значение, которое больше, чем нужно, но определение в этом атрибуте значения меньше, чем нужно приведет к ошибке.


На тему стека можно почитать "О стеке и куче в контексте мира Java" или в "JVM Internals"


max_locals


Максимальный размер локальных переменных


Ознакомится с локальными переменными можно либо в Mastering Java Bytecode at the Core of the JVM или в том же JVM Internals


code_length


Размер кода, который будет исполнятся внутри метода


code[]


Каждый код указывает на какую-то инструкцию. Таблицу соотношения optcode и команды с мнемоникой можно найти в википедии — Java bytecode instruction listings или в самой спецификации в конце книги


Для примера, возьмем наш конструктор:


    -- public Main();

    0001 --access_flags
    0007 -- name_index
    0008 -- descriptor_index
    0001 -- attributes_count

    -- attribute_info
    0009 -- attribute_name_index (Code)
    0000 001d - attribute_length
    00 01 -- max_stack
    00 01 -- max_locals
    00 00 00 05 -- code_length 
    2a b7 00 01 b1  -- code[]

    0000 -- exception_table_length
    0001 --  attributes_count;
    00 0a -- attribute_name_index
    0000 0006 -- attribute_length
    00 01 00 00 00 01

Здесь мы можем найти наш код:


    2a b7 00 01 b1

Ищем в таблице команды и сопоставляем:


    2a - aload_0
    b7 0001 - invokespecial #1
    b1 - return

Также описания этих команд можно найти здесь: Chapter 4.10.1.9. Type Checking Instructions


exception_table_length


Задает число элементов в таблице exception_table. У нас пока нет перехватов исключений поэтому разбирать его не будем. Но дополнительно можно почитать Chapter 4.7.3 The Code Attribute


exception_table[]


Имеет вот такую структуру:


    {   
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    }

Если упрощать, то нужно указать начало, конец (start_pc, end_pc) кода, который будет обрабатывать handler_pc и тип исключения catch_type


attributes_count


Количество атрибутов в Code


attributes[]


Атрибуты, часто используются анализаторами или отладчиками.




Средства для работы с байт-кодом


Это немного не та тема, которая относится к данной статье, но все же косвенно связанная с ней.


Средств для работы с байт-кодом, на самом деле, достаточно много. Здесь я бы хотел рассмотреть Byte Code Engineering Library (BCEL) от Apache Commons.


Для начала, с помощью него мы может получить некоторые атрибуты байт-кода:


        // read file from resources/complied/ClassA.class
        InputStream inputStream = ClassParserExample.class.getResourceAsStream("/compiled/ClassA.class");
        if (inputStream == null) throw new FileNotFoundException();

        ClassParser parser = new ClassParser(inputStream, "ClassA.class");

        JavaClass clazz = parser.parse();

        final String HEX_BYTECODE = getHex(clazz.getBytes());

        System.out.println("Hex bytecode: ");
        System.out.println(HEX_BYTECODE);
        System.out.println();

        final String MINOR_VER      =   getHex(clazz.getMinor());
        final String MAJOR_VER      =   getHex(clazz.getMajor());
        final String CONSTANT_POOL  =   getHex(clazz.getConstantPool().getConstantPool());
        final String ACCESS_FLAGS   =   getHex(clazz.getAccessFlags());
        final String THIS_CLASS     =   getHex(clazz.getClassName().getBytes());
        final String SUPER_CLASS    =   getHex(clazz.getSuperClass().getBytes());
        final String INTERFACES     =   getHex(clazz.getInterfaces());
        final String FIELDS         =   getHex(clazz.getFields());
        final String METHODS        =   getHex(clazz.getMethods());
        final String ATTRIBUTES     =   getHex(clazz.getAttributes());

Полный листинг кода
import org.apache.bcel.classfile.*;
import org.apache.commons.codec.binary.Hex;

import java.io.*;

public class ClassParserExample {

    public static void main(String... args) throws IOException, ClassNotFoundException {

        // read file from resources/complied/ClassA.class
        InputStream inputStream = ClassParserExample.class.getResourceAsStream("/compiled/ClassA.class");
        if (inputStream == null) throw new FileNotFoundException();

        ClassParser parser = new ClassParser(inputStream, "ClassA.class");

        JavaClass clazz = parser.parse();

        final String HEX_BYTECODE = getHex(clazz.getBytes());

        System.out.println("Hex bytecode: ");
        System.out.println(HEX_BYTECODE);
        System.out.println();

        final String MINOR_VER      =   getHex(clazz.getMinor());
        final String MAJOR_VER      =   getHex(clazz.getMajor());
        final String CONSTANT_POOL  =   getHex(clazz.getConstantPool().getConstantPool());
        final String ACCESS_FLAGS   =   getHex(clazz.getAccessFlags());
        final String THIS_CLASS     =   getHex(clazz.getClassName().getBytes());
        final String SUPER_CLASS    =   getHex(clazz.getSuperClass().getBytes());
        final String INTERFACES     =   getHex(clazz.getInterfaces());
        final String FIELDS         =   getHex(clazz.getFields());
        final String METHODS        =   getHex(clazz.getMethods());
        final String ATTRIBUTES     =   getHex(clazz.getAttributes());

        System.out.println( "minor: "           + MINOR_VER    );   // 0
        System.out.println( "major: "           + MAJOR_VER    );   // 34
        System.out.println( "constant pool: "   + CONSTANT_POOL);   // not correctly
        System.out.println( "access flags: "    + ACCESS_FLAGS );   // 21
        System.out.println( "this class: "      + THIS_CLASS   );
        System.out.println( "super class: "     + SUPER_CLASS  );   // Object
        System.out.println( "interfaces: "      + INTERFACES   );   // <empty>
        System.out.println( "fields: "          + FIELDS       );   // <empty>
        System.out.println( "methods: "         + METHODS      );   // one method: psvm hello world
        System.out.println( "attributes: "      + ATTRIBUTES   );   // 536f7572636546696c65 - I think it's instructions for Java tools

    }

    private static String getHex(byte[] bytes){
        return Hex.encodeHexString(bytes);
    }

    private static String getHex(int intNum){
        return Integer.toHexString(intNum);
    }

    private static String getHex(Constant[] constants){
        if (constants == null) return null;

        StringBuilder sb = new StringBuilder();
        for (Constant c : constants){
            if (c == null) continue;
            sb.append(getHex(c.getTag())).append(" ");
        }

        return sb.toString();
    }

    private static String getHex(JavaClass[] clazzes){
        if (clazzes == null) return null;

        StringBuilder sb = new StringBuilder();
        for (JavaClass c : clazzes){
            sb.append(getHex(c.getClassName().getBytes())).append(" ");
        }

        return sb.toString();
    }

    private static String getHex(Field[] fields){
        if (fields == null) return null;

        StringBuilder sb = new StringBuilder();
        for (Field c : fields){
            sb.append(getHex(c.getName().getBytes())).append(" ");
        }

        return sb.toString();
    }

    private static String getHex(Method[] methods){
        if (methods == null) return null;

        StringBuilder sb = new StringBuilder();
        for (Method c : methods){
            sb.append(getHex(c.getName().getBytes())).append(" ");
        }

        return sb.toString();
    }

    private static String getHex(Attribute[] attributes){
        if (attributes == null) return null;

        StringBuilder sb = new StringBuilder();
        for (Attribute c : attributes){
            sb.append(getHex(c.getName().getBytes())).append(" ");
        }

        return sb.toString();
    }

}

/*
    Class A:
    public class ClassA {
        public static void main(String[] args) {
            System.out.println("Hello world");
        }
    }
 */

/*
    Class A bytecode:
    cafe babe 0000 0034 0022 0a00 0600 1409
    0015 0016 0800 170a 0018 0019 0700 1a07
    001b 0100 063c 696e 6974 3e01 0003 2829
    5601 0004 436f 6465 0100 0f4c 696e 654e
    756d 6265 7254 6162 6c65 0100 124c 6f63
    616c 5661 7269 6162 6c65 5461 626c 6501
    0004 7468 6973 0100 1d4c 636f 6d2f 6170
    706c 6f69 6478 7878 2f70 6172 7365 2f43
    6c61 7373 413b 0100 046d 6169 6e01 0016
    285b 4c6a 6176 612f 6c61 6e67 2f53 7472
    696e 673b 2956 0100 0461 7267 7301 0013
    5b4c 6a61 7661 2f6c 616e 672f 5374 7269
    6e67 3b01 000a 536f 7572 6365 4669 6c65
    0100 0b43 6c61 7373 412e 6a61 7661 0c00
    0700 0807 001c 0c00 1d00 1e01 000b 4865
    6c6c 6f20 776f 726c 6407 001f 0c00 2000
    2101 001b 636f 6d2f 6170 706c 6f69 6478
    7878 2f70 6172 7365 2f43 6c61 7373 4101
    0010 6a61 7661 2f6c 616e 672f 4f62 6a65
    6374 0100 106a 6176 612f 6c61 6e67 2f53
    7973 7465 6d01 0003 6f75 7401 0015 4c6a
    6176 612f 696f 2f50 7269 6e74 5374 7265
    616d 3b01 0013 6a61 7661 2f69 6f2f 5072
    696e 7453 7472 6561 6d01 0007 7072 696e
    746c 6e01 0015 284c 6a61 7661 2f6c 616e
    672f 5374 7269 6e67 3b29 5600 2100 0500
    0600 0000 0000 0200 0100 0700 0800 0100
    0900 0000 2f00 0100 0100 0000 052a b700
    01b1 0000 0002 000a 0000 0006 0001 0000
    0006 000b 0000 000c 0001 0000 0005 000c
    000d 0000 0009 000e 000f 0001 0009 0000
    0037 0002 0001 0000 0009 b200 0212 03b6
    0004 b100 0000 0200 0a00 0000 0a00 0200
    0000 0800 0800 0900 0b00 0000 0c00 0100
    0000 0900 1000 1100 0000 0100 1200 0000
    0200 13
 */

/*
    Assembled code:
    Classfile /C:/java/BCEL/src/main/resources/compiled/ClassA.class
      Last modified 08.12.2019; size 563 bytes
      MD5 checksum bcd0198f6764a1dc2f3967fef701452e
      Compiled from "ClassA.java"
    public class com.apploidxxx.parse.ClassA
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
       #3 = String             #23            // Hello world
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #26            // com/apploidxxx/parse/ClassA
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/apploidxxx/parse/ClassA;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               SourceFile
      #19 = Utf8               ClassA.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = Class              #28            // java/lang/System
      #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
      #23 = Utf8               Hello world
      #24 = Class              #31            // java/io/PrintStream
      #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
      #26 = Utf8               com/apploidxxx/parse/ClassA
      #27 = Utf8               java/lang/Object
      #28 = Utf8               java/lang/System
      #29 = Utf8               out
      #30 = Utf8               Ljava/io/PrintStream;
      #31 = Utf8               java/io/PrintStream
      #32 = Utf8               println
      #33 = Utf8               (Ljava/lang/String;)V
    {
      public com.apploidxxx.parse.ClassA();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 6: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/apploidxxx/parse/ClassA;
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String Hello world
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 8: 0
            line 9: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    SourceFile: "ClassA.java
 */

Помимо этого мы можем сгенерировать, изменить или дизассемблировать (например, в Jasmin) байт-код.


Парочку примеров можно найти в моем репозитории или в официальных примерах


Также, я уделил внимание и Jasmin. На самом деле, я не знаю, чем оно может быть полезно, но я её использовал при изучении механизма работы JVM с байт-кодом.


С помощью неё можно писать на упрощенном ассемблерном коде:


Hello World
.bytecode 52.0

.source Main.j
.class public Main
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
        .limit stack 2
        .limit locals 2

    getstatic java/lang/System/out Ljava/io/PrintStream;
    ldc "Hello world!"
    invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V
    return
.end method

Создание объекта
; ClassCreating.j

.bytecode 52.0
.source ClassCreating.java
.class public ClassCreating
.super java/lang/Object

.method public <init>()V
    .limit stack 1
    .limit locals 1
    .line 1
    0: aload_0
    1: invokespecial java/lang/Object/<init>()V
    4: return
.end method

.method public static main([Ljava/lang/String;)V
    ; Flag ACC_VARARGS set, see JVM spec
    .limit stack 2
    .limit locals 3
    .line 3
    0: new java/lang/String
    3: dup
    4: invokespecial java/lang/String/<init>()V
    7: astore_1
    .line 4
    8: new ClassCreating
    11: dup
    12: invokespecial ClassCreating/<init>()V
    15: astore_2
    .line 5
    16: aload_2
    17: invokevirtual ClassCreating/sayHello()V
    .line 6
    20: return
.end method

.method public sayHello()V
    .limit stack 2
    .limit locals 1
    .line 9
    0: getstatic java/lang/System/out Ljava/io/PrintStream;
    3: ldc "Hello, User!"
    5: invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
    .line 10
    8: return

.end method



Вот мы и разобрали простую программку Hello World


Листинг байт-кода с комментариями можно найти на моем гисте: gist.github


Если есть ошибки прошу писать в комментариях или в сообщениях.


Использованная литература


  • The Java Virtual Machine Specification — docs.oracle

Комментарии (8)


  1. Foxek
    18.12.2019 17:04

    Уже было, гуглится по первой ссылке. Такой же разбор "Hello World" Зачем копия?
    https://habr.com/ru/post/264919/


    1. apploid_offical Автор
      18.12.2019 11:53

      Я сейчас прочитал статью, которую вы приложили. Лично мне кажется, что официальная спецификация JVM будет более понятной, чем эта приложенная статья. По крайней мере, считаю, что она, возможно, была нацелена на людей, которые уже знают (хорошо) структуру байт-кода. Если вас просто смутило одинаковое название и тема статей, то не думаю, что это то из-за чего статью можно назвать копией. Так как у нас совершенно разные методы изложения.

      Ну, раз уж вы считаете эту статью копией, то может думать о моей статье, как улучшенной версией (мне так кажется, хотя кому как. Возможно, та статья для кого-то будет понятней). Во всяком случае, советую вам прочитать обе статьи и сравнить уже их контексты, а не названия или темы статей.


      1. Foxek
        18.12.2019 12:41
        +1

        Это конечно сугубо мое мнение, просто не понимаю, зачем, видя что на ресурсе уже есть статья с разбором "Hello World", делать разбор "Hello World" другими словами. Ну сделайте вы разбор программы, которая на 2 строчки больше, вопросов бы наверное не было.


        Читал обе статьи. Ни в коем случае не пытаюсь Вас обидеть и уж тем более не хочу судить о том, какая лучше, так как это сугубо субъективное мнение. Контекст, как вы выразились, у этих статей как раз идентичен, а вот содержимое действительно отличается. Про копию, я не совсем корректно выразился, за что прошу меня извинить.
        Статьи отличаются, но исключительно в контексте оформления. В обоих чуть более человеческое и простая для читателя трактовка документации.


    1. lany
      18.12.2019 19:55
      +1

      Вот ещё я по теме писал, но только про пул констант. Зато с красивой картинкой!


  1. vvvvvvv
    18.12.2019 17:38
    +1

    Вдогонку — Hexing the technical interview


  1. esavin
    19.12.2019 22:15

    Я заморочился лет 10 назад и перевел спецификацию байткода Java на формальный язык Java. Пришлось придумать несколько анатаций, с помощью которых можно описывать бинарные форматы. Далее я написал универсальный загрузчик/сериалайзер который умеет загружать бинарные файлы (.class файлы как частный случай) с использованием описания формата (читай набор java классов со специальными анатациями). К сожалению, на опкоды меня не хватило — они просто как массив байтов хронятся (или что-то типа того, уже не помню).
    Итак, Java bytecode specification, выраженная на языке Java: github.com/esavin/annotate4j-classfile
    Классы, которые умеют это все загружать/выгружать тут: github.com/esavin/annotate4j-core
    Несколько раз пытался написать статью тут, но так и не осилил.


  1. esavin
    19.12.2019 22:19

    Примерно год назад с помощью этой библиотеки смог зареверсить бинарный формат от устройства, которое снимает суточную активность сердца (датчик Холтера вроде это называется).


  1. esavin
    19.12.2019 22:32

    А самое неприятное в спеке для меня было вот что:
    All 8-byte constants take up two entries in the constant_pool table of the class
    file. If a CONSTANT_Long_info or CONSTANT_Double_info structure is the entry
    at index n in the constant_pool table, then the next usable entry in the table is
    located at index n+2. The constant_pool index n+1 must be valid but is considered
    unusable.
    То есть для long и double info в constant pool используется две ячейки, причем во второй — всегда пусто.
    Позже это было признано неудачным дизайнерским решением:
    In retrospect, making 8-byte constants take two constant pool entries was a poor choice.