JVM Hardcore – Part 4 – Bytecode – Manipulation des variables locales
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
Test unitaire :
@Test
public void aload() {
final String result = Aload.getThird((byte)1, 2, "Hello", 4l);
Assert.assertEquals("Hello", result);
}
Empile un long
avec lload
:
.class org/isk/jvmhardcore/bytecode/partfour/Lload
.method getFourth(BILjava/lang/String;J)J
lload 3
lreturn
.methodend
.classend
Test unitaire :
@Test
public void lload() {
final long result = Lload.getFourth((byte)1, 2, "Hello", 4l);
Assert.assertEquals(4l, result);
}
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
Test unitaire :
@Test
public void iload_0() {
final int result = Iload_0.getFirst(1, 2, 3);
Assert.assertEquals(1, result);
}
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
Test unitaire :
@Test
public void iload_0_byte() {
final byte result = Iload_0_Byte.getFirst((byte) 1, 2, 3);
Assert.assertEquals(1, result);
}
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
Test unitaire :
@Test
public void iload_0_char() {
final char result = Iload_0_Char.getFirst((char) 1, 2, 3);
Assert.assertEquals(1, result);
}
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
Test unitaire :
@Test
public void iload_0_short() {
final short result = Iload_0_Short.getFirst((byte) 1, 2, 3);
Assert.assertEquals(1, result);
}
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
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"
}
}
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
Test unitaire :
@Test
public void iload_1() {
final int result = Iload_1.getSecond(1, 2, 3);
Assert.assertEquals(2, result);
}
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
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);
}
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.