Как выглядит байт код
Перейти к содержимому

Как выглядит байт код

  • автор:

Заглядываем Под Капот Языка Java: Компиляция и Байт-Код

Мир программирования часто делим на компилируемые и интерпретируемые языки. Но что, если я скажу вам, что Java предлагает третий, более интригующий способ — магию байт-кода?

Вот как это работает:

Как работает Java фото

Сначала у нас есть исходный код Java в виде файла с расширением .java.

Этот код проходит через компилятор, тщательно проверяясь на ошибки и преобразуясь в байт-код. Забудьте о машинном коде — байт-код здесь находится на первом плане.

Компилированный байт-код — это наш переходный язык, абстракция, которая не привязана к какой-либо платформе. Этот файл можно передать любому устройству, способному интерпретировать Java, и вот тут на сцену выходит виртуальная машина Java (JVM).

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

Так что добро пожаловать в захватывающий мир Java, где каждая строка кода — это не просто инструкция, а часть увлекательного процесса, где байт-код волшебным образом оживает на виртуальной сцене!

Презентацию с видео можно скачать на Patreon .

Понимаем байт-код EVM: Часть 2

В первой части мы изучили часть создания контракта в байт-коде виртуальной машины. В этом разделе мы проанализируем рантайм байт-кода виртуальной машины. Мы по-прежнему будем использовать пример кода из предыдущей статьи.

Весь байткод выглядит так:

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

Эти инструкции фактически сохраняют адрес 0x80 со смещением 0x40 в памяти в качестве указателя свободной памяти для использования в будущем.

Существует новый код операции CALLDATASIZE, который мы раньше не встречали в 0x07. Он получит размер полезной нагрузки данных из этой транзакции. LT — это код операции для сравнения двух элементов в стеке, он вернет значение TRUE, если сравнение выполнено.

Итак, собрав все части вместе, мы можем получить эквивалентный ассемблерный код Solidity следующим образом:

mstore(0x40,0x80);

Мы можем видеть, что в основном эти инструкции инициализируют указатели памяти и проверяют, составляет ли размер полезной нагрузки данных не менее 4 байт. Причина этого заключается в том, что при обычном внешнем вызове смарт-контракта первые 4 байта в полезной нагрузке данных являются хэш-значением для подписи функции.

Это 4-байтовое значение будет использоваться контрактом для выбора функции для доставки остальных данных, которые являются параметрами для этой функции. Например, если вы вызовете функцию withdraw(0xABCD) в смарт-контракте, полезная нагрузка данных для этого вызова будет выглядеть следующим образом:

0x3823D66C000000000000000000000000000000000000000000000000000000000000ABCD

В этом примере первое 4-байтовое значение равно 0x3823D66C, что является хэш-значением SHA3 для “withdrawn(bytes32)”. Следующее 32-байтовое целое число является параметром вызова функции 0xABCD. Это простой пример параметров целочисленной функции. При обработке параметров переменного размера все усложнится. Мы поговорим о них позже.

А пока давайте вернемся к инструкциям, которые мы обсуждали. Размер полезной нагрузки данных должен составлять не менее 4 байт. Если нет, то все вернется на круги своя. Но будет ли это справедливо для всех смарт-контрактов? Что, если мы просто отправим эфир в этот смарт-контракт без вызова какой-либо функции?

Возможно, вы уже помните некоторые функциональные возможности в программах Solidity. Да, именно там реализована функция fallback. Чтобы подтвердить это, вы можете получить образец с реализованной функцией fallback, а затем проверить ветвь инструкции после кода проверки msg.data.length.

Продолжая разбор кода, мы видим следующие команды:

Если вы посмотрите больше байт-кода виртуальной машины из разных смарт-контрактов, вы всегда найдете один и тот же фрагмент кода или функционально эквивалентный фрагмент кода. Этот фрагмент кода не из того кода, который вы поместили в свой исходный код, и он был введен компилятором. Давайте пройдемся по каждой строчке.

Сначала код помещает 0xFFFFFFFF в стек. Это значение будет использоваться в качестве одного из операндов кода операции AND в 0x33. Затем код добавит еще одно огромное постоянное значение, которое будет использоваться в качестве разделителя DIV в 0x32. Для инструкций в 0x2F, 0x31 код получит первое 32-байтовое значение из полезной нагрузки данных, используя CALLDATALOAD(0x0).

В сочетании с набором команд более поздних DIV и AND мы можем видеть, что эти инструкции фактически получают первое 4-байтовое значение полезной нагрузки данных, которое является хэш-значением подписи функции. Вычисленный результат помещается в стек. Затем значение сравнивается с 0x1003e2d2, используя код операции EQ в 0x3A. Если это было правдой, выполнение будет переведено на адрес 0x4D с помощью JUMPI. В противном случае код продолжит работу, и результат будет сравнен с другим хэш-значением 0xb69ef8a8.

Теперь логика этого фрагмента кода довольно ясна. Он получает первое 4-байтовое значение из полезной нагрузки данных и решает, какая функция будет выбрана для запуска. Для кодовых адресов 0x4D и 0x74 они являются входом для каждой public функции, к которой вызывающий может получить доступ к смарт-контрактам. Если ни одно из хэш-значений в коде не удовлетворяется, то это приведет к срабатыванию fallback функции смарт-контрактов. Если он не был определен, он просто вернется.

До сих пор мы знали, как public функции могут получить доступ к внешним функциям. Теперь давайте подробнее рассмотрим конкретные функции. Адрес 0x4D является входом функции add():

На входе в функцию add() находится код операции JUMPDEST. Это специальный код операции, который помечает только адрес, на который можно перейти. Похоже, что это не играет важной роли для реализации EVM. Однако вы увидите, что это действительно помогает определить график потока управления (CFG) для байт-кода. Мы обсудим это в следующем разделе.

Инструкции, установленные в 0x4F-0x57, были просмотрены в предыдущем разделе. Они были введены компилятором для функции, не принимающей средства. После этого кода проверки код операции PUSH1 по адресу 0x5A легко проигнорировать. Однако это PUSH1 очень важно для того, чтобы код мог вернуться позже.

Давайте просто запомним, что адрес 0x62 пока помещен в стек. Затем вызывается CALLDATALOAD(0x04) для загрузки параметра из полезной нагрузки данных, который расположен со смещением 0x04. После получения параметра код перейдет к 0x86 для выполнения:

В приведенном выше фрагменте кода значение 0x0 в хранилище загружается с помощью SLOAD(0x0). Затем это значение будет добавлено к параметру, загруженному из полезной нагрузки данных, и сохранено обратно в то же место 0x0 в хранилище. Наконец, мы видим код, который мы поместили внутри функции add():

balance = balance + value;

Мы использовали только одну целочисленную переменную balance внутри смарт-контракта. Компилятор присваивает этой переменной смещение 0x0. Таким образом, любая операция чтения или записи с этой переменной баланса будет помещена в смещение 0x0 в хранилище.

В конце фрагмента кода используется JUMP для возврата к 0x62. Если вы все еще помните, где это значение 0x62 было помещено в стек. Эта операция может напомнить вам что-то об архитектуре X86. Да, это на самом деле call и ret для вызовов функций. Поскольку EVM не поддерживает вызовы функций на уровне байт-кода, он может использовать только коды операций PUSH и JUMP для вызовов функций. Таким образом, возникнет много проблем с созданием CFG из байт-кода.

Давайте вернемся к адресу 0x62, чтобы посмотреть, что произойдет дальше:

Этот фрагмент кода содержит несколько настроек элементов в стеке с помощью кодов операций DUP и SWAP. Для эквивалентного кода на ассемблере приведенный выше код выглядит следующим образом:

mstore(mload(0x40), value);

return(mload(0x40), 0x20);

Value внутри кода было вычисленным результатом предыдущей операции add(), которое является новым значением balance. Наконец, мы просмотрели весь байт-код внутри функции add().

Теперь давайте вернемся к фрагменту кода отправки функции для второй функции HASH 0xb69ef8a8. Вход для этой функции находится в 0x74:

Мы видим, что первая часть кода действительно похожа на предыдущую функцию, за исключением того, что параметр не был загружен. Затем функция вызовет фрагмент кода с 0x95. Инструкции в 0x95-0x98 просто загружают значение в 0x0 в хранилище и возвращают. Мы отметили, что фрагмент кода в 0x62 был повторно использован для обеих функций. Это связано с тем, что обе функции вернут переменную balance обратно.

Вы можете задаться вопросом, почему внутри кода отправки функции присутствует функция HASH 0xb69ef8a8? Разве внутри смарт-контракта нет только одной функции add()? Если вы используете базу данных в 4 байта для проверки этого хэша, вы получите balance(). По-видимому, переменная хранилища распознается компилятором как public функция без параметров.

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

Как это будет выглядеть для сопоставлений или массивов переменной длины? Как будут представлены параметры в полезной нагрузке данных для строк? Мы поговорим обо всем этом в следующем разделе.

Как посмотреть байт код java

Для просмотра байт-кода Java можно использовать утилиту javap , которая поставляется вместе с JDK . javap позволяет просмотреть байт-код любого класса, даже если он не содержит исходного кода.

Чтобы просмотреть байт-код класса, следует выполнить следующие шаги:

  • Скомпилировать Java-класс в .class файл. Это можно сделать, например, с помощью javac команды:
javac MyClass.java 
  • Запустить javap с флагом -c , чтобы вывести байт-код класса:
javap -c MyClass 

Эта команда выведет байт-код класса MyClass.

Compiled from "MyClass.java" public class MyClass  public MyClass(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public void myMethod(); Code: 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 > 

Этот вывод показывает байт-код двух методов класса MyClass : конструктора и метода myMethod()

Структура байт-кода

Сильноветвящиеся Б-деревья

Компиляция программы
Компилятор создает файл , содержащий версию кода виртуальной
машины программы. Код виртуальной машины Java — это промежуточное
представление программы, содержащее инструкции, которые будет выполнять
виртуальная машина Java. Результат работы компилятора не является
непосредственно исполняемым кодом.
В процессе компиляции исходного кода каждый отдельный класс
помещается в собственный выходной файл, названный по имени класса и
получающие расширение .class.
Текст
программы
Компилятор
Javac
Class файл
Кафедра защищенных систем связи

3.

ByteCode виртуальной машины Java
Байт-код Java — набор инструкций, исполняемых виртуальной машиной
Java. Каждый код операции байт-кода имеет размер один байт.
Виды инструкций:
• Загрузка и сохранение;
• Арифметические и логические операции;
• Преобразование типов;
• Создание и преобразование объекта;
• Управление стеком;
• Операторы перехода;
• Вызовы методов и возврат;
Кафедра защищенных систем связи

4.

ByteCode виртуальной машины Java
Если рассматривать более детально, то формат инструкций наглядно
выглядит так:
Биты
8
Формат
8
8
8
8
1. Операция
2.Операция
Байт
3. Операция
4. Операция
SHORT
Индекс
Константа
5. Операция
Индекс
Размерность
6. Операция
Индекс
Параметры
7. Операция
Индекс
8. Операция
Константа
32- битное смещение перехода
Кафедра защищенных систем связи
0

5.

ByteCode виртуальной машины Java
Загрузка из локальной переменной:
• iload — для типа int
• lload — для типа long
• fload — для типа float
• dload — для типа double
Сохранение локальной переменной типа int:
• istore_0 = 59 (0x3b)
• istore_1 = 60 (0x3c)
• istore_2 = 61 (0x3d)
• istore_3 = 62 (0x3e)
istore = 54 (0x36)
Не имеют аргументов
Имеет один аргумент
Кафедра защищенных систем связи

6.

ByteCode виртуальной машины Java
Математические операции:
• iadd = 96 (0x60) – сложение типа int
• ladd = 97 (0x61) – сложение типа long
• fadd = 98 (0x62) – сложение типа float
• dadd = 99 (0x63) – сложение типа double
• imul = 104 (0x68) – умножение типа int
public class Example public int plus(int a) int b = 1;
return a + b;
>
>
public int plus(int);
Code: Stack=2, Locals=3,
Args_size=2
0: iconst_1
1: istore_2
2: iload_1
3: iload_2
4: iadd
5: ireturn
LineNumberTable:
line 5: 0
line 6: 2
Кафедра защищенных систем связи

7.

ByteCode виртуальной машины Java
Байт – код
Исходный код
int a = 0;
int b = 1;
int c = a +b;
iconst_0
istore_1
Байт – код
iconst_0
istore_1
Исходный код
iconst_1
istore_2
int a = 0;
iload_1
iload_2
iadd
istore_3
int c = 1;
int b = 3;
Int d = a *b + c;
Кафедра защищенных систем связи
iconst_3
istore_2
iconst_1
istore_3
iload_1
iload_2
imull
iload_3
iadd
istore_4

8.

ByteCode виртуальной машины Java
Пример вычисления:
Исходный код
ByteCode
int a =2;
1. iload_1
int b =3;
2. iload_2
int c = 1;
3. imull
int d =a*b + c;
4. iload_3
5. iadd
6. istore_4
1
2 (iload_1)
2
• 3 (iload_2)
• 2 (iload_1)
3
6 (imull)
4
• 1 (iload_3)
• 6 (imull)
5
6
Кафедра защищенных систем связи
7
Стек пуст

9.

Class файл
• Содержит байт-код, который выполняется на виртуальной машине.
• Содержит информацию о классе.
• Генерируется компилятором из исходного кода (.java)
• Jar файл – представляет собой zip архив class файлов.
Main class
файл
.jar файл
Class файл
METAINF/MANIFE
ST.MF
Характеристики
данного .jar
файла
Кафедра защищенных систем связи

10.

Структура class файла
class файл состоит из 1,2,4 байтовых значений:
• u1 – byte ( 1 байт )
• u2 – long ( 2 байта)
• u4 – int ( 4 байта)
Кафедра защищенных систем связи

11.

Структура class файла
Разберемся на самом тривиальном примере:
package hello;
public class App public static void main(String[] args) System.out.println(«Hello world!»);
>
>
Скомпилировав данный код, мы получим class файл. С помощью любого hexредактора откроем его.
Кафедра защищенных систем связи

12.

Структура class файла
0000000
0000010
0000020
0000030
0000040
0000050
0000060
0000070
0000080
0000090
00000a0
00000b0
00000c0
00000d0
00000e0
00000f0
0000100
0000110
00001a0
ca
00
00
56
75
6e
2f
72
76
00
1a
70
62
67
15
74
2f
72
00
fe
10
16
01
6d
01
53
63
61
0c
0c
70
6a
2f
4c
72
50
69
0e
ba be 00
00 11 08
01 00 06
00 04 43
62 65 72
00 16 28
74 72 69
65 46 69
0c 00 07
48 65 6c
00 1b 00
01 00 10
65 63 74
53 79 73
6a 61 76
65 61 6d
72 69 6e
6e 74 6c
00001a2
00
00
3c
6f
54
5b
6e
6c
00
6c
1c
6a
01
74
61
3b
74
6e
00
12
69
64
61
4c
67
65
08
6f
01
61
00
65
2f
01
53
01
34
0a
6e
65
62
6a
3b
01
07
20
00
76
10
6d
69
00
74
00
00
00
69
01
6c
61
29
00
00
77
09
61
6a
01
6f
13
72
15
1d
13
74
00
65
76
56
08
17
6f
68
2f
61
00
2f
6a
65
28
0a
00
3e
0f
01
61
01
41
0c
72
65
6c
76
03
50
61
61
4c
00
14
01
4c
00
2f
00
70
00
6c
6c
61
61
6f
72
76
6d
6a
06
07
00
69
04
6c
0a
70
18
64
6c
6e
2f
75
69
61
01
61
Кафедра защищенных систем связи
00
00
03
6e
6d
61
53
2e
00
21
6f
67
6c
74
6e
2f
00
76
0f
15
28
65
61
6e
6f
6a
19
07
2f
2f
61
01
74
69
07
61
09
07
29
4e
69
67
75
61
01
00
41
4f
6e
00
53
6f
70
2f

13.

Структура class файла
Так выглядит байт код нашей простой программы. Есть два вида байт
кода. Собственно, сам байт код представленный на предыдущем слайде и его
мнемоническое представление для виртуальной машины Java, которое
будет показано позже. Рассмотрим байт код подробнее.
u4 magic
Это 4 байта для magic, который определяет формат class файла (ca fe ba be).
Позволяет идентифицировать .class файл.
Всегда принимает значение: 0xCAFEBABE.
u2 minor_version ( 00 00 )
u2 major_version ( 00 34 )
Вспомогательная и основная версии class файла.
u2 constant_pool_count ( 00 1d )
Размер массива констант. Эти два байта представляют constant_pool_count и
отвечают за размер constant_pool.
Кафедра защищенных систем связи

14.

Структура class файла
cp_info constant_pool [constant_pool_count-1]
Пул констант представлен в виде массива структур представляющих
различные строковые константы — имена классов и интерфейсов, полей,
методов и другие константы, на которые есть ссылки в файле класса.
Далее в байт коде идут элементы вида:
cp_info u1 tag; // 1 байт на тег
u1 info[]; // массив с описанием
>
Формат каждого элемента пула констант определяется первым байтом (tag).
Кафедра защищенных систем связи

15.

Структура class файла
Constant Type
Value
CONSTANT_Class
7
CONSTANT_Long
5
CONSTANT_Methodref
10
CONSTANT_String
8
CONSTANT_Float
4
CONSTANT_Fieldref
9
CONSTANT_Utf8
1
CONSTANT_MethodType
16
Constant_Long_info u1 tag;
u4 high_bytes;
u4 low_bytes;
>
Constant_utf8_info u1 tag;
u2 length;
u1 bytes[length];
>
Кафедра защищенных систем связи

16.

Структура class файла
u2 access_flags
Флаг доступа, отображающий модификаторы, заданные в определении класса
(public, final, abstract и т.д.), а также признак класса или интерфейса.
Значение элемента является маской флагов, используемых таким образом,
чтобы обозначить права доступа и свойства этого класса.
Flag Name
Value
ACC_PUBLIC
0x0001
ACC_FINAL
0x0010
ACC_SUPER
0x0020
ACC_INTERFACE
0x0200
ACC_ABSTRACT
0x0400
ACC_SYNTHETIC
0x1000
ACC_ANNOTATION
0x2000
ACC_ENUM
0x4000
Interpretation
Объявлен открытым. Может быть
доступен извне пакета.
Объявлен final. Классы
наследники не допускаются.
Обращение к методам
суперкласса. Когда вызывается
инструкция invokespecial.
Интерфейс. Не класс.
Объявлен абстрактным. Не может
иметь экземпляров.
Объявлен synthetic; Не
представлен в исходном коде.
Объявлен как тип аннтоция.
Объявлен в качестве типа
перечисления.
Кафедра защищенных систем связи

17.

Структура class файла
u2 this_class
u2 super_class
Ссылки на константу с названием класса и его суперкласса.
u2 interfaces_count
Размер массива интерфейсов.
u2 interfaces[interfaces_count]
Массив интерфейсов. Каждый элемент массива является индексом таблицы
пула констант, где указывается имя интерфейса.
Кафедра защищенных систем связи

18.

Структура class файла
field_info fields[fields_count]
Массив полей.
u2 fields_count
Размер массива полей.
field_info u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes [attributes_count];
>
Кафедра защищенных систем связи

19.

Структура class файла
method_info methods[methods_count]
Массив методов.
u2 methods_count
Размер массива методов.
method_info u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
>
Кафедра защищенных систем связи

20.

Структура class файла
attribute_info attributes[attributes_count]
Массив атрибутов.
u2 attributes_count
Размер массива атрибутов.
attribute_info u2 attribute_name_index;
u4 attribute_length;
u1 info [attribute_length];
>
Нас интересует атрибут Code, так как именно в нем содержится
необходимая нам информация. Если у вас нет исходных кодов программы,
есть только class файл и вам необходимо внести некоторые изменения в работе
программы, то начинать следует именно с этого атрибута.
Кафедра защищенных систем связи

21.

Структура class файла
Список атрибутов (attribute)
ConstantValue
LineNumberTable
Code
LocalVariabletable
StackMapTable
LocalVariableTypeTable
Exceptions
Deprecated
InnerClasses
RuntimeVisibleAnnotations
EnclosingMethod
RuntimeInvisibleAnnotations
Synthetic
RuntaimeVisibleParameterAnnotations
Signature
RuntaimeInvisibleParameterAnnotations
SourceFile
AnnotationDefault
SourceDebufExtension
BootstrapMethods
Кафедра защищенных систем связи

22.

Структура class файла
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];
>
Кафедра защищенных систем связи

23.

Средства работы с class файлами
JBE — Java Bytecode Editor – программа, позволяющая просматривать и
редактировать class файлы.
javap – декомпилятор class файлов.
javap команда дизассемблирует один или более файлов класса. Его вывод
зависит
от
используемых
опций.
Если
никакие
опции
не
используются, javap распечатывает пакет, защищенные, и общедоступные
поля и методы классов, которые передают к этому. javap печатает свой вывод
к stdout.
Кафедра защищенных систем связи

24.

Средства работы с class файлами
Некоторые опции javap:
-?
Распечатывает сообщение справки для javap.
-p
Показать все классы и элементы.
-s
Печатает внутренние подписи типа.-sysinfo
-c
Распечатывает дизассемблированный код, то есть, инструкции,
которые включают Байт-коды Java для каждого из методов в классе. Они
документируются в Спецификацию виртуальной машины Java.
Кафедра защищенных систем связи

25.

Средства работы с class файлами
Файл Main.java
public class Main public static void main (String [] args ) int a = 4;
int b = 5;
int c = a+b;
>
>
Скомпилируем данный код с помощью команды javac Main.java
Затем в командной строке или если в используемой вами IDE есть своя командная
строка, то в ней выполним команду:
javap -c Main.class
Кафедра защищенных систем связи

26.

Средства работы с class файлами
Команда вывела на экран инструкции, которые включают Байт-коды
Java для каждого из методов в классе.
Кафедра защищенных систем связи

27.

Средства работы с class файлами
JBE — Java Bytecode Editor – программа, позволяющая просматривать и
редактировать class файлы.
Файл Main.java
public class Main public static void main (String [] args ) int a = 4;
int b = 5;
int c = a+b;
>
>
Скомпилируем данный код с помощью команды javac Main.java.
Полученный class файл загрузим в ByteCode Editor.
Кафедра защищенных систем связи

28.

Средства работы с class файлами
Как видно на данном скриншоте, программа позволяет посмотреть пул констант
Кафедра защищенных систем связи

29.

Средства работы с class файлами
А также, возможен просмотр списка инструкций, в атрибуте Code
Результат идентичен выполнению команды javap -c
Кафедра защищенных систем связи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *