Accueil Nos publications Blog JVM Hardcore – Part 6 – Bytecode – Manipulation de la pile

JVM Hardcore – Part 6 – Bytecode – Manipulation de la pile

academic_duke Cette semaine, nous allons étudier les instructions permettant de manipuler la pile. Comme nous le verrons, elles sont légèrement plus difficiles à appréhender que les précédentes.

Le code est disponible sur Github (tag et branche)

Tous les articles déjà publiés de la série portent le tag jvmhardcore.

Catégories des types

La correspondance entre les types utilisés par la JVM et les types Java est présentée dans le tableau ci-dessous.

Certaines instructions de la JVM, tels que pop et swap opèrent sur la pile sans prendre en compte le type des valeurs manipulées, contrairement à toutes les instructions que nous avons vu jusqu’à présent, telles que iadd, dload ou lshr. Cependant, ces instructions ne peuvent être utilisées que sur ​​des valeurs d’une certaine catégorie.

Il existe deux catégories permettant de distinguer les types nécessitant une entrée dans les variables locales ou la pile, de ceux en nécessitant deux.

Bien qu’une référence puisse être représentée sur 32 ou 64 bits en fonction de la JVM, elle sera toujours de catégorie 1, et par conséquent, prendra toujours une entrée dans les variables locales ou la pile.

Type Java Type JVM Catégorie
byte int 1
short int 1
char int 1
int int 1
float float 1
référence référence 1
adresse de retour * adresse de retour * 1
long long 2
double double 2
  • utilisée par l’instruction ret.

Représentation de la pile

La JVM étant basée sur le modèle de la pile, il est essentiel de connaître quel est l’impact des instructions. Pour représenter l’état avant/après l’exécution d’une instruction, nous allons reprendre le format utiliser par la JVMS et qui est le suivant :

..., valeur1, valeur2 → ..., résultat, où les valeurs les plus à droite sont au sommet de la pile. valeur1 et valeur2 étant les deux valeurs utilisées pour le calcul et résultat le résultat.

Il est important de noter que dans cette représentation les long et le double sont considérés comme une seule valeur. Par conséquent, lorsque nécessaire nous présenterons les différents cas d’utilisation d’une instruction en utilisant plusieurs formes.

Opérations sur la Pile

Il est parfois utile de manipuler les données au sommet de la pile sans avoir à faire intervenir les variables locales. C’est ce que permettent les instructions suivantes :

Hex Mnémonique Description
0x57 pop Dépile le premier élément
0x58 pop2 Dépile les deux premiers éléments
0x59 dup Duplique le premier élément et l’empile
0x5a dup_x1 Duplique le premier élément et l’ajoute sous le deuxième
0x5b dup_x2 Duplique le premier élément et l’ajoute sous le troisième
0x5c dup2 Duplique les deux premiers éléments et les empile (en gardant l’ordre)
0x5d dup2_x1 Duplique les deux premiers éléments et les ajoute sous le troisième (en gardant l’ordre)
0x5e dup2_x2 Duplique les deux premiers éléments et les ajoute sous le quatrième élément (en gardant l’ordre)
0x5f swap Échange les deux premiers éléments

Une chose importante à ne pas oublier est que les long et double prennent 2 cases dans la pile et doivent être considérés comme 2 éléments.

swap

L’instruction swap permet d’inverser les 2 premiers éléments au sommet de la pile quelque soit leur type à condition qu’il soit de catégorie 1.

Note: Elle ne peut pas être utilisée avec des long et des double.

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur2, valeur1, où valeur1 et valeur2 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Swap
  .method swap()I
    iconst_1
    iconst_2
    swap
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void swap() {
  final int result = Swap.swap();

  Assert.assertEquals(1, result);
}

Source

pop

L’instruction pop dépile l’élément au sommet de la pile.

Note: Elle ne peut pas être utilisée avec des long et des double.

État de la pile avant → après exécution : ..., valeur1 → ..., où valeur1 est de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Pop
  .method pop()D
    dconst_0
    iconst_1
    pop
    dreturn
  .methodend
.classend

Source

Test unitaire :


public void pop() {
  final double result = Pop.pop();

  Assert.assertEquals(0.0, result, 0.0001);
}

Source

pop2

L’instruction pop2 dépile les deux premiers éléments au sommet de la pile.

pop2 – Forme 1

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., où valeur1 et valeur2 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Pop2_Form1
  .method pop2()I
    iconst_0
    iconst_1
    iconst_1
    pop2
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void pop2_form1() {
  final int result = Pop2_Form1.pop2();

  Assert.assertEquals(0, result);
}

Source

pop2 – Forme 2

État de la pile avant → après exécution : ..., valeur1 → ..., où valeur1 est de catégorie 2.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Pop2_Form2
  .method pop2()I
    iconst_2
    dconst_1
    pop2
    ireturn
  .methodend
.classend

Source

Test unitaire :

Test
public void pop2_form2() {
  final int result = Pop2_Form2.pop2();

  Assert.assertEquals(2, result);
}

Source

dup

L’instruction dup duplique le premier élément et l’empile.

Note: Elle ne peut pas être utilisée avec des long et des double.

État de la pile avant → après exécution : ..., valeur1 → ..., valeur1, valeur1, où valeur1 est de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup
  .method dup()I
    iconst_2
    iconst_1
    dup
    pop2
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void dup() {
  final int result = Dup.dup();

  Assert.assertEquals(2, result);
}

Source

dup_x1

L’instruction dup_x1 duplique le premier élément et l’ajoute sous le deuxième.

Note: Elle ne peut pas être utilisée avec des long et des double.

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur1, valeur2, valeur1, où valeur1 et valeur2 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup_X1
  .method dup_x1()I
    iconst_2
    iconst_1
    dup_x1
    pop2
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void dup_x1() {
  final int result = Dup_X1.dup_x1();

  Assert.assertEquals(1, result);
}

Source

dup_x2

L’instruction dup_x2 duplique le premier élément et l’ajoute sous le troisième.

Note: Elle ne peut pas être utilisée avec des long et des double.

dup_x2 – Forme 1

État de la pile avant → après exécution : ..., valeur1, valeur2, valeur3 → ..., valeur3, valeur1, valeur2, valeur3, où valeur1, valeur2 et valeur3 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup_X2_Form1
  .method dup_x2()I
    iconst_3
    iconst_2
    iconst_1
    dup_x2
    pop2
    pop
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void dup_x2_form1() {
  final int result = Dup_X2_Form1.dup_x2();

  Assert.assertEquals(1, result);
}

Source

dup_x2 – Forme 2

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur2, valeur1, valeur2, où valeur1 est de catégorie 2 et valeur2 est de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup_X2_Form2
  .method dup_x2()I
    dconst_1
    iconst_2
    dup_x2
    pop
    pop2
    ireturn
  .methodend
.classend

Source

Test unitaire :


public void dup_x2_form2() {
  final int result = Dup_X2_Form2.dup_x2();

  Assert.assertEquals(2, result);
}

Source

dup2

L’instruction dup2 duplique les deux premiers éléments et les empile (en gardant l’ordre).

dup2 – Forme 1

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur1, valeur2, valeur1, valeur2, où valeur1, valeur2 et valeur3 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_Form1
  .method dup2()I
    iconst_2  # [empty] -> 2
    iconst_1  # 2 -> 2, 1
    dup2      # 2, 1 -> 2, 1, 2, 1
    @--------- Start packing
    bipush 8  # 2, 1, 2, 1 -> 2, 1, 2, 1, 8
    ishl      # 2, 1, 2, 1, 8 -> 2, 1, 2, 1 << 8
    ior       # 2, 1, 2, 1 << 8 -> 2, 1, 2 | 1 << 8
    bipush 8  # 2, 1, 2 | 1 << 8 -> 2, 1, 2 | 1 << 8, 8
    ishl      # 2, 1, 2 | 1 << 8, 8 -> 2, 1, (2 | 1 << 8) << 8
    ior       # 2, 1, (2 | 1 << 8) << 8 -> 2, 1 | (2 | 1 << 8) << 8
    bipush 8  # etc.
    ishl
    ior
    ireturn   # 0000 0001 0000 0010 0000 0001 0000 0010
  .methodend
.classend

Source

Nous souhaitons connaître l’état de la pile – les valeurs présentes et leur ordre – après l’exécution des trois premières instructions (celles se trouvant avant le commentaire Start packing) pour vérifier le fonctionnement de l’instruction dup2. Or avec les instructions que nous avons étudiées jusqu’à présent, la seule solution est d’empaqueter les quatres valeurs présentes dans la pile (2, 1, 2, 1). En prenant le type int, l’empaquetage consistera à découper la valeur retournée en quatre blocs de 8 bits (4 * 8 = 32 bits). Le bloc le plus à gauche contiendra la valeur au sommet de la pile, le suivant la valeur juste au dessous, et ainsi de suite jusqu’au bas la pile, pour obtenir la valeur 0x1020102. Pour ce faire nous utilisons les instructions d’opération bit à bit que nous avons vu dans l’article précédent.

Test unitaire :


public void dup2_form1() {
  final int result = Dup2_Form1.dup2();

  Assert.assertEquals(0x01020102, result);
}

Source

dup2 – Forme 2

État de la pile avant → après exécution : ..., valeur1 → ..., valeur1, valeur1, où valeur1 est de catégorie 2.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_Form2
  .method dup2()D
    dconst_1  # [empty] -> 1.0
    dup2      # 1.0 -> 1.0, 1.0
    dadd      # 1.0, 1.0 -> 2.0
    dreturn
  .methodend
.classend

Source

Test unitaire :


public void dup2_form2() {
  final double result = Dup2_Form2.dup2();

  Assert.assertEquals(2.0, result, 0.0001);
}

Source

dup2_x1

L’instruction dup2_x1 duplique les deux premiers éléments et les ajoute sous le troisième (en gardant l’ordre).

dup2_x1 – Forme 1

État de la pile avant → après exécution : ..., valeur1, valeur2, valeur3 → ..., valeur2, valeur3, valeur1, valeur2, valeur3, où valeur1, valeur2 et valeur3 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X1_Form1
  .method dup2_x1()I
    iconst_3  # [empty] -> 3
    iconst_2  # 3 -> 3, 2
    iconst_1  # 3, 2 -> 3, 2, 1
    dup2_x1   # 3, 2, 1 -> 2, 1, 3, 2, 1
    @--------- Start packing
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    ireturn  # 0001 0010 0011 0001 0010
  .methodend
.classend

Source

Test unitaire :


public void dup2_x1_form1() {
  final int result = Dup2_X1_Form1.dup2_x1();

  Assert.assertEquals(0x12312, result);
}

Source

dup2_x1 – Forme 2

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur2, valeur1, valeur2, où valeur1 est de catégorie 1 et valeur2 est de catégorie 2.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X1_Form2
  .method dup2_x1()D
    iconst_3  # [empty] -> 3
    dconst_1  # 3 -> 3, 1.0
    dup2_x1   # 3, 1.0 -> 1.0, 3, 1.0
    pop2      # 1.0, 3, 1.0 -> 1.0, 3
    pop       # 1.0, 3 -> 1.0
    dreturn
  .methodend
.classend

Source

Test unitaire :


public void dup2_x1_form2() {
  final double result = Dup2_X1_Form2.dup2_x1();

  Assert.assertEquals(1.0, result, 0.0001);
}

Source

dup2_x2

L’instruction dup2_x2 duplique les deux premiers éléments et les ajoute sous le quatrième élément (en gardant l’ordre).

dup2_x2 – Forme 1

État de la pile avant → après exécution : ..., valeur1, valeur2, valeur3, valeur4 → ..., valeur3, valeur4, valeur1, valeur2, valeur3, valeur4, où toutes les valeurs sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X2_Form1
  .method dup2_x2()I
    iconst_4  # [empty] -> 4
    iconst_3  # 4 -> 4, 3
    iconst_2  # 4, 3 -> 4, 3, 2
    iconst_1  # 4, 3, 2 -> 4, 3, 2, 1
    dup2_x2   # 3, 2, 1 -> 2, 1, 3, 2, 1
    @--------- Start packing
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    bipush 4
    ishl
    ior
    ireturn   # 0001 0010 0011 0100 0001 0010
  .methodend
.classend

Source

Test unitaire :


public void dup2_x2_form1() {
  final int result = Dup2_X2_Form1.dup2_x2();

  Assert.assertEquals(0x123412, result);
}

Source

dup2_x2 – Forme 2

État de la pile avant → après exécution : ..., valeur1, valeur2, valeur3 → ..., valeur3, valeur1, valeur2, valeur3, où valeur1 et valeur2 sont de catégorie 1 et valeur3 est de catégorie 2.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X2_Form2
  .method dup2_x2()D
    iconst_2  # [empty] -> 2
    iconst_0  # 2 -> 2, 0
    dconst_1  # 2, 0 -> 2, 0, 1.0
    dup2_x2   # 2, 0, 1.0 -> 1.0, 2, 0, 1.0
    pop2      # 1.0, 2, 0, 1.0 -> 1.0, 2, 0
    pop2      # 1.0, 2, 0 -> 1.0
    dreturn
  .methodend
.classend

Source

Test unitaire :


public void dup2_x2_form2() {
  final double result = Dup2_X2_Form2.dup2_x2();

  Assert.assertEquals(1.0, result, 0.0001);
}

Source

dup2_x2 – Forme 3

État de la pile avant → après exécution : ..., valeur1, valeur2, valeur3 → ..., valeur2, valeur3, valeur1, valeur2, valeur3, où valeur1 est de catégorie 2 et, valeur2 et valeur3 sont de catégorie 1.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X2_Form3
  .method dup2_x2()I
    dconst_1  # [empty] -> 1.0
    iconst_1  # 1.0 -> 1.0, 1
    iconst_2  # 1.0, 1 -> 1.0, 1, 2
    dup2_x2   # 1.0, 1, 2 -> 1, 2, 1.0, 1, 2
    pop2      # 1, 2, 1.0, 1, 2 -> 1, 2, 1.0
    pop2      # 1, 2, 1.0 -> 1, 2
    @--------- Start packing
    bipush 4
    ishl
    ior
    ireturn   # 0010 0001
  .methodend
.classend

Source

Test unitaire :


public void dup2_x2_form3() {
  final int result = Dup2_X2_Form3.dup2_x2();

  Assert.assertEquals(0x21, result);
}

Source

dup2_x2 – Forme 4

État de la pile avant → après exécution : ..., valeur1, valeur2 → ..., valeur2, valeur1, valeur2, où valeur1 et valeur2 sont de catégorie 2.

Code PJB :

.class org/isk/jvmhardcore/bytecode/partsix/Dup2_X2_Form4
  .method dup2_x2()D
    dconst_0
    dconst_1
    dup2_x2
    pop2
    pop2
    dreturn
  .methodend
.classend

Source

Test unitaire :


public void dup2_x2_form4() {
  final double result = Dup2_X2_Form4.dup2_x2();

  Assert.assertEquals(1.0, result, 0.0001);
}

Source

What’s next ?

Au cours des trois articles précédents et de celui-ci, nous avons vu 136 instructions. Néanmoins, l’objectif étant aussi de comprendre le contenu d’un fichier .class, nous allons mettre de côté les 69 restantes pendant quelques articles pour nous concentrer sur l’étude de différents éléments dont la connaissance est nécessaire à la construction d’un assembleur de bytecode.
Le prochain article sera dédié à la première partie de la création d’un analyseur syntaxique.