Привет! Счастливого Пришествия: D Я Саймон Мэйпл ( @sjmaple) , Технический Евангелист для ZeroTurnaround. Вы знаете, ребята JRebel ! В результате мы напишем такой продукт, как JRebel, который взаимодействует с байт-кодом, чаще, чем вы себе представляете. Мы узнали о нем много вещей, которыми хотели бы поделиться.
Давайте начнем с самого начала … Java был языком, предназначенным для работы на виртуальной машине, поэтому его нужно было скомпилировать только один раз, чтобы он везде работал (да, да, один раз напиши, повсюду тестируй). В результате JVM, который вы устанавливаете на свою систему, будет встроенным, что позволит коду, работающему на нем, быть независимым от платформы. Java-байт-код является промежуточным представлением Java-кода, который вы пишете как источник, и является результатом компиляции вашего кода. Таким образом, ваши файлы классов являются байт-кодом.
Чтобы быть более кратким, байт-код Java — это кодовый набор, используемый виртуальной машиной Java, который JIT-компилируется в собственный код во время выполнения.
Вы когда-нибудь играли с ассемблером или машинным кодом? Байт-код в некотором роде похож, но многие люди в отрасли на самом деле не играют с ним больше, больше из-за необходимости отсутствия. Тем не менее, важно понимать, что происходит, и полезно, если вы хотите кого-то обмануть в пабе.
Во-первых, давайте взглянем на некоторые основы байт-кода. Сначала мы возьмем выражение «1 + 2» и посмотрим, как оно выполняется как байт-код Java. 1 + 2 можно записать в обратной польской записи как 1 2 +. Почему? Хорошо, когда мы помещаем это в стек, все становится ясно …
Хорошо, в байт-коде мы фактически увидели бы коды операций (iconst_1 и iconst_2) и инструкцию (iadd), а не push и add, но последовательность действий та же. Фактические инструкции имеют длину один байт, следовательно, байт-код. В результате получается 256 возможных кодов операций, но только 200 или около того используются. К кодам операций добавляется префикс с типом, за которым следует имя операции. Итак, то, что мы видели ранее с iconst и iadd, это константы целочисленного типа и инструкция добавления для целочисленных типов.
Это все очень хорошо, но как насчет чтения файлов классов. Как правило, все, что вы обычно видите в файле класса при открытии в выбранном вами редакторе, это набор смайликов и несколько квадратов, точек и других странных символов, верно? Ответ в javap, утилите кода, которую вы фактически получаете с вашим JDK. Давайте посмотрим на пример кода, чтобы увидеть javap в действии.
|
1
2
3
4
5
6
7
8
9
|
public class Main { public static void main(String[] args){ MovingAverage app = new MovingAverage(); }} |
Как только этот класс скомпилирован в файл Main.class, мы можем использовать следующую команду для извлечения байт-кода: javap -c Main
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
Compiled from "Main.java"public class algo.Main { public algo.Main(); Code: 0: aload_0 1: invokespecial #1 4: return// Method java/lang/Object."<init>":()Vpublic static void main(java.lang.String[]); Code: 0: new #2 3: dup 4: invokespecial #3 7: astore_1 8: return} |
Мы можем видеть, что у нас есть конструктор по умолчанию и метод main в байтовом коде сразу. Кстати, именно так Java дает вам конструктор по умолчанию для классов без конструктора! Байт-код в конструкторе — это просто вызов super (), в то время как наш метод main создает новый экземпляр MovingAverage и возвращает его. Символы #n на самом деле ссылаются на константы, которые мы можем просматривать с помощью аргумента -verbose следующим образом: javap -c -verbose Main. Интересная часть того, что возвращается, показана ниже:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
public class algo.Main SourceFile: "Main.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #5.#21 // java/lang/Object."<init>":()V #2 = Class #22 // algo/MovingAverage #3 = Methodref #2.#21 // algo/MovingAverage."<init>":()V #4 = Class #23 // algo/Main #5 = Class #24 // java/lang/Object |
Теперь мы можем сопоставить наши инструкции с нашими константами и собрать воедино то, что на самом деле происходит гораздо проще. Что-то еще беспокоит вас по поводу примера выше? Нет? Как насчет номеров перед каждой инструкцией …
|
1
2
3
4
5
6
7
8
9
|
0: new #2 3: dup 4: invokespecial #3 7: astore_1 8: return |
Теперь это действительно беспокоит тебя, верно? 🙂 Вот что мы получим, если представим тело этого метода в виде массива:
Обратите внимание, что каждая инструкция имеет представление HEX, поэтому, используя это, мы действительно видим это:
На самом деле мы можем увидеть это в файле класса, если откроем его в редакторе HEX:
Мы могли бы на самом деле изменить байт-код здесь, в нашем HEX-редакторе, но, честно говоря, это не то, что вы действительно хотели бы сделать, особенно в пятницу днем после обязательной поездки в паб. Лучшими способами сделать это было бы использование ASM или javassist.
Давайте перейдем от нашего основного примера и добавим некоторые локальные переменные, которые хранят состояние и взаимодействуют напрямую с нашим стеком. Проверьте следующий код:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
public static void main(String[] args) { MovingAverage ma = new MovingAverage(); int num1 = 1; int num2 = 2; ma.submit(num1); ma.submit(num2); double avg = ma.getAvg();} |
Давайте посмотрим, что мы получим в этот раз в нашем байт-коде:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
Code:
0: new #2 // class algo/MovingAverage3: dup4: invokespecial #3 // Method algo/MovingAverage."<init>":()V7: astore_18: iconst_19: istore_210: iconst_211: istore_312: aload_113: iload_214: i2d15: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V18: aload_119: iload_320: i2d21: invokevirtual #4 // Method algo/MovingAverage.submit:(D)V24: aload_125: invokevirtual #5 // Method algo/MovingAverage.getAvg:()D28: dstore 4LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 ma Lalgo/MovingAverage;10 21 2 num1 I12 19 3 num2 I30 1 4 avg D |
Это выглядит намного интереснее … Мы видим, что мы создаем объект типа MovingAverage, который хранится в локальной переменной ma с помощью инструкции astore_1 (1 — это номер слота в LocalVariableTable). Инструкции iconst_1 и iconst_2 предназначены для загрузки констант 1 и 2 в стек и сохранения их в слотах LocalVariableTable 2 и 3 соответственно с помощью инструкций istore_2 и istore_3. Инструкция загрузки помещает локальную переменную в стек, а инструкция сохранения извлекает следующий элемент из стека и сохраняет его в LocalVariableTable. Важно понимать, что когда используется инструкция сохранения, элемент извлекается из стека, и если вы хотите использовать его снова, вам необходимо загрузить его.
Как насчет потока выполнения? Все, что мы видели, — это простое продвижение от одной строки к другой. Я хочу увидеть немного GOTO 10 в базовом стиле! Давайте возьмем другой пример:
|
1
2
3
4
5
6
7
|
MovingAverage ma = new MovingAverage();for (int number : numbers) { ma.submit(number);} |
В этом случае поток выполнения будет многократно перескакивать при прохождении цикла for. Этот байт-код, предполагая, что переменная numbers является статическим полем в том же классе, показан следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
0: new #2 // class algo/MovingAverage3: dup4: invokespecial #3 // Method algo/MovingAverage."<init>":()V7: astore_18: getstatic #4 // Field numbers:[I11: astore_212: aload_213: arraylength14: istore_315: iconst_016: istore 418: iload 420: iload_321: if_icmpge 4324: aload_225: iload 427: iaload28: istore 530: aload_131: iload 533: i2d34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V37: iinc 4, 140: goto 1843: returnLocalVariableTable:Start Length Slot Name Signature30 7 5 number I 12 31 2 arr$ [I15 28 3 len $I 18 25 4 i$ I0 49 0 args [Ljava/lang/String;8 41 1 ma Lalgo/MovingAverage; 48 1 2 avg D |
Инструкции с позиций с 8 по 17 используются для настройки цикла. В таблице LocalVariable есть три переменные, которые на самом деле не упомянуты в источнике: arr $, len $ и i $. Это переменные цикла. arr $ хранит эталонное значение поля чисел, из которого определяется длина цикла len $. i $ — счетчик цикла, который увеличивается с помощью инструкции iinc.
Сначала нам нужно проверить наше выражение цикла, которое выполняется инструкцией сравнения:
|
1
2
3
4
5
|
18: iload 420: iload_321: if_icmpge 43 |
Мы загружаем 4 и 4 в стек, которые являются счетчиком цикла и длиной цикла. Мы проверяем, что id i $ больше или равно len $. Если это так, мы переходим к утверждению 43, в противном случае мы продолжаем. Затем мы можем выполнить нашу логику в цикле и в конце мы увеличиваем наш счетчик и возвращаемся к нашему коду, который проверяет условие цикла в операторе 18.
|
1
2
3
|
37: iinc 4, 1 // increment i$40: goto 18 // jump back to the beginning of the loop |
Существует несколько арифметических кодов операций и комбинаций команд типа, которые можно использовать в байт-коде, включая следующие:
А также ряд кодов операций преобразования типов, которые важны при назначении, скажем, целого числа переменной типа long.
В нашем драгоценном примере мы передаем целое число в метод submit, который принимает удвоение. Синтаксис Java делает это для нас, но в байт-коде вы увидите, что используется код операции i2d:
|
1
2
3
4
5
|
31: iload 5
33: i2d
34: invokevirtual #5 // Method algo/MovingAverage.submit:(D)V |
Итак, вы сделали это так далеко. Молодец, ты заработал кофе! Что-нибудь из этого на самом деле полезно знать, или это просто чудак? Ну, это оба! Во-первых, теперь вы можете сказать своим друзьям, что вы JVM, который может обрабатывать байт-код, а во-вторых, вы можете лучше понять, что вы делаете, когда пишете байт-код. Например, при использовании ObjectWeb ASM, который является одним из наиболее широко используемых инструментов манипулирования байт-кодом, вы обнаружите, что создаете инструкции, и эти знания окажутся бесценными!
Если вы нашли это интересным и хотите узнать больше, то ознакомьтесь с нашим бесплатным отчетом по освоению байт-кода Java от Антон Архипова, руководителя по продукту JRebel на ZeroTurnaround. (JRebel использует javassist, и нам было очень интересно учиться и взаимодействовать с байт-кодом Java!) Этот отчет углубляется и затрагивает, как использовать ASM.
Спасибо за чтение! Дайте мне знать, что вы думали! ( @sjmaple )





