Hello World из байт-кода для JVM

    Скомпилируем простенькую программу выводящую "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
    • +28
    • 6,2k
    • 8
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +1

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

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

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

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


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

          +1

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

          +1

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

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

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое