Accueil Nos publications Blog JVM Hardcore – Part 4 – Bytecode – Manipulation des variables locales

JVM Hardcore – Part 4 – Bytecode – Manipulation des variables locales

academic_duke En cours de ce cinquième article, nous allons continuer notre voyage dans le monde des instructions bytecode Java.

Chaque cadre (frame) a un tableau de variables locales pouvant accueillir jusqu’à 65 536 entrées. Ceci signifie que la somme du nombre de paramètres et des variables temporaires d’une méthode ne peut excéder 65 536. Sachant que l’utilisation de variables de type long ou double réduit ce nombre. Bien évidemment, pour des raisons de performance, il est préférable d’utiliser des cases contiguës à partir de l’index 0 pour éviter à la JVM de créer des tableaux excessivement grand pour seulement quelques valeurs.

Les instructions load et store permettent de déplacer des valeurs de la pile vers les variables locales et vice versa. Plus précisément, load récupére une valeur des variables locales, puis l’empile, et store stocke la valeur au sommet de la pile dans les variables locales.

Le code est disponible sur Github (tag et branche)

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

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.

Initialisation des variables locales

Comme nous l’avons vu dans l’article Part 1 – Introduction à la JVM, la JVM utilise les variables locales pour passer des paramètres à la méthode appelée. Lorsqu’une méthode de classe (statique) est appelée, tous les paramètres sont stockés dans les variables locales de manière consécutive à partir de l’index zéro. Lorsqu’une méthode d’instance est appelée, la variable à l’index zéro est toujours une référence de l’objet (this) sur laquelle la méthode a été appelée. Les paramètres sont quant à eux stockés de manière consécutive à partir de l’index un.

Néanmoins, les variables locales étant aussi utilisées pour stocker des résultats partiels, les paramètres d’une méthode ou this peuvent être remplacés.

A noter que pour l’instant, PJBA permettant uniquement de générer des méthodes statiques, leurs paramètres sont accessibles à partir de l’index 0 des variables locales.

Load

Les instructions suivantes permettent de récupérer des valeurs depuis les variables locales. Les valeurs sont ajoutées au sommet de la pile (mais sont toujours disponible dans les variables locales).

État de la pile avant → après exécution : ... → valeur

Hex Mnémonique Argument Description
0x15 iload n Empile la valeur de type int à l’index n
0x16 lload n Empile la valeur de type long à l’index n
0x17 fload n Empile la valeur de type float à l’index n
0x18 dload n Empile la valeur de type double à l’index n
0x19 aload n Empile la référence à l’index n
0x1a iload_0 Empile la valeur de type int à l’index 0
0x1b iload_1 Empile la valeur de type int à l’index 1
0x1c iload_2 Empile la valeur de type int à l’index 2
0x1d iload_3 Empile la valeur de type int à l’index 3
0x1e lload_0 Empile la valeur de type long à l’index 0
0x1f lload_1 Empile la valeur de type long à l’index 1
0x20 lload_2 Empile la valeur de type long à l’index 2
0x21 lload_3 Empile la valeur de type long à l’index 3
0x22 fload_0 Empile la valeur de type float à l’index 0
0x23 fload_1 Empile la valeur de type float à l’index 1
0x24 fload_2 Empile la valeur de type float à l’index 2
0x25 fload_3 Empile la valeur de type float à l’index 3
0x26 dload_0 Empile la valeur de type double à l’index 0
0x27 dload_1 Empile la valeur de type double à l’index 1
0x28 dload_2 Empile la valeur de type double à l’index 2
0x29 dload_3 Empile la valeur de type double à l’index 3
0x2a aload_0 Empile la référence à l’index 0
0x2b aload_1 Empile la référence à l’index 1
0x2c aload_2 Empile la référence à l’index 2
0x2d aload_3 Empile la référence à l’index 3

Voyons quelques exemples :

Empile une chaîne de caractères avec aload :

.class org/isk/jvmhardcore/bytecode/partfour/Aload
  .method getThird(BILjava/lang/String;J)Ljava/lang/String;
    aload 2
    areturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void aload() {
  final String result = Aload.getThird((byte)1, 2, "Hello", 4l);

  Assert.assertEquals("Hello", result);
}

Source

Empile un long avec lload :

.class org/isk/jvmhardcore/bytecode/partfour/Lload
  .method getFourth(BILjava/lang/String;J)J
    lload 3
    lreturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void lload() {
  final long result = Lload.getFourth((byte)1, 2, "Hello", 4l);

  Assert.assertEquals(4l, result);
}

Source

Empile l’int dans les variables à l’index 0 avec iload_0 :

.class org/isk/jvmhardcore/bytecode/partfour/Iload_0
  .method getFirst(III)I
    iload_0
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_0() {
  final int result = Iload_0.getFirst(1, 2, 3);

  Assert.assertEquals(1, result);
}

Source

Empile l’int dans les variables à l’index 0 avec iload_0 et retourne un byte :

.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Byte
  .method getFirst(BII)B
    iload_0
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_0_byte() {
  final byte result = Iload_0_Byte.getFirst((byte) 1, 2, 3);

  Assert.assertEquals(1, result);
}

Source

Empile l’int dans les variables à l’index 0 avec iload_0 et retourne un char :

.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Char
  .method getFirst(CII)C
    iload_0
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_0_char() {
  final char result = Iload_0_Char.getFirst((char) 1, 2, 3);

  Assert.assertEquals(1, result);
}

Source

Empile l’int dans les variables à l’index 0 avec iload_0 et retourne un short :

.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Short
  .method getFirst(SII)S
    iload_0
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_0_short() {
  final short result = Iload_0_Short.getFirst((byte) 1, 2, 3);

  Assert.assertEquals(1, result);
}

Source

Essaie de retourner la valeur à l’index 0 des variables locales.

.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_NoArgs
  .method getFirst()I
    iload_0
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_0_noargs() {
  try {
    Iload_0_NoArgs.getFirst();
    Assert.fail();
  } catch (VerifyError e) {
    // Assertion
    // Message retourné par la JVM:
    //   (class: org/isk/jvmhardcore/bytecode/partfour/Iload_0_NoArgs, 
    //   method: getFirst signature: ()I) Accessing value from uninitialized register 0"
  }
}

Source

La méthode n’ayant pas de paramètre et aucune instruction de stockage dans les variables locales n’ayant été effectuée, il n’y a rien à l’index 0. Par conséquent, une exception est levée.

Empile l’int dans les variables à l’index 1 avec iload_1 :

.class org/isk/jvmhardcore/bytecode/partfour/Iload_1
  .method getSecond(III)I
    iload_1
    ireturn
  .methodend
.classend

Source

Test unitaire :

@Test
public void iload_1() {
  final int result = Iload_1.getSecond(1, 2, 3);

  Assert.assertEquals(2, result);
}

Source

Store

Les instructions suivantes permettent de stocker la variable au sommet de la pile dans dans les variables locales. La valeur au sommet de la pile est supprimée.

État de la pile avant → après exécution : ..., valeur → ...

Hex Mnémonique Argument Description
0x36 istore n Stocke la valeur de type int au sommet de la pile à l’index n
0x37 lstore n Stocke la valeur de type long au sommet de la pile à l’index n
0x38 fstore n Stocke la valeur de type float au sommet de la pile à l’index n
0x39 dstore n Stocke la valeur de type double au sommet de la pile à l’index n
0x3a astore n Stocke la référence au sommet de la pile à l’index n
0x3b istore_0 Stocke la valeur de type int au sommet de la pile à l’index 0
0x3c istore_1 Stocke la valeur de type int au sommet de la pile à l’index 1
0x3d istore_2 Stocke la valeur de type int au sommet de la pile à l’index 2
0x3e istore_3 Stocke la valeur de type int au sommet de la pile à l’index 3
0x3f lstore_0 Stocke la valeur de type long au sommet de la pile à l’index 0
0x40 lstore_1 Stocke la valeur de type long au sommet de la pile à l’index 1
0x41 lstore_2 Stocke la valeur de type long au sommet de la pile à l’index 2
0x42 lstore_3 Stocke la valeur de type long au sommet de la pile à l’index 3
0x43 fstore_0 Stocke la valeur de type float au sommet de la pile à l’index 0
0x44 fstore_1 Stocke la valeur de type float au sommet de la pile à l’index 1
0x45 fstore_2 Stocke la valeur de type float au sommet de la pile à l’index 2
0x46 fstore_3 Stocke la valeur de type float au sommet de la pile à l’index 3
0x47 dstore_0 Stocke la valeur de type double au sommet de la pile à l’index 0
0x48 dstore_1 Stocke la valeur de type double au sommet de la pile à l’index 1
0x49 dstore_2 Stocke la valeur de type double au sommet de la pile à l’index 2
0x4a dstore_3 Stocke la valeur de type double au sommet de la pile à l’index 3
0x4b astore_0 Stocke la référence au sommet de la pile à l’index 0
0x4c astore_1 Stocke la référence au sommet de la pile à l’index 1
0x4d astore_2 Stocke la référence au sommet de la pile à l’index 2
0x4e astore_3 Stocke la référence au sommet de la pile à l’index 3

Contrairement à Java, les variables sont typées dynamiquement. Par conséquent, il est possible de stocker les cinq types connus de la JVM à n’importe quel index des variables locales. Une variable locale peut donc avoir des valeurs différentes à différents moments.

Prenons le code suivant :

.class org/isk/jvmhardcore/bytecode/partfour/Reassign
  .method reassign()D
    ldc "ma chaîne"
    astore_0
    aload_0
    dconst_1
    dstore_0
    dload_0
    dreturn
  .methodend
.classend

Source

Voyons ce qu’il se passe dans le cadre contenant la méthode reassign() :

Pour finir testons la méthode reassign() :

@Test
public void reassign() {
  final double result = Reassign.reassign();

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

Source

Tout comme de nombreuses instructions que nous avons vu dans l’article précédent, load et store sont liées à des types. Ces types sont identifiables – le plus souvent – grâce à la première lettre de la mnémonique de l’instruction. De fait, il est nécessaire de faire attention à utiliser l’instruction correspondant aux types des valeurs présentes dans la pile et qui seront utilisées comme opérandes.

ldc  "ma chaîne"
istore 0            # Erreur! astore doit être utilisé

Les variables de type long et double prennent deux entrées dans la pile et les variables locales.

ldc  "ma chaîne"
astore_2
ldc2_w 3.14d
dstore_1
aload_2         # Erreur! Une partie du double a remplacé "ma chaîne"

Pour finir notre tour des points d’attention, il est important de noter qu’une variable locale doit être initialisée avant d’être utilisée.

iconst_5    # Empile 5
istore_3    # Stocke 5 dans la variable locale à l'index 3
iload_3     # Récupère la valeur (5) stockée dans la variable locale à l'index 3

Pour rappel, le tableau des variables locales – d’un cadre – est automatiquement initialisé par la JVM avec les paramètres de la méthode en cours d’exécution, si elle en a.

What’s next ?

Dans l’article suivant nous verrons des instructions permettant d’effectuer des opérations sur des nombres, mais aussi de changer le type des nombres.