V. Manipulation des variables locales▲
Au cours de cette cinquième partie, 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 grands 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).
V-A. 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 utilisé par la JVMS et qui est le suivant :
…, valeur1, valeur2 → …, résultat, où les valeurs le plus à droite sont au sommet de la pile. « valeur1 » et « valeur »2 é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 le 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.
V-B. Initialisation des variables locales▲
Comme nous l'avons vu dans la partie Part II - 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 1.
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.
À 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.
V-C. 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 disponibles 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:
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Aload
.method getThird(BILjava/lang/String;J)Ljava/lang/String;
aload 2
areturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@Test
public
void
aload
(
) {
final
String result =
Aload.getThird
((
byte
)1
, 2
, "Hello"
, 4
l);
Assert.assertEquals
(
"Hello"
, result);
}
Empile un long avec lload :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Lload
.method getFourth(BILjava/lang/String;J)J
lload 3
lreturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@Test
public
void
lload
(
) {
final
long
result =
Lload.getFourth
((
byte
)1
, 2
, "Hello"
, 4
l);
Assert.assertEquals
(
4
l, result);
}
Empile l'int dans les variables à l'index 0 avec iload_0 :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_0
.method getFirst(III)I
iload_0
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@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 :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Byte
.method getFirst(BII)B
iload_0
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@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 :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Char
.method getFirst(CII)C
iload_0
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@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 :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_Short
.method getFirst(SII)S
iload_0
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@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.
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_0_NoArgs
.method getFirst()I
iload_0
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
@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 :
2.
3.
4.
5.
6.
.class org/isk/jvmhardcore/bytecode/partfour/Iload_1
.method getSecond(III)I
iload_1
ireturn
.methodend
.classend
Test unitaire :
2.
3.
4.
5.
6.
@Test
public
void
iload_1
(
) {
final
int
result =
Iload_1.getSecond
(
1
, 2
, 3
);
Assert.assertEquals
(
2
, result);
}
V-D. Store▲
Les instructions suivantes permettent de stocker la variable au sommet de la pile 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
.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() :
2.
3.
4.
5.
6.
@Test
public
void
reassign
(
) {
final
double
result =
Reassign.reassign
(
);
Assert.assertEquals
(
1.0
, result, 0.00001
);
}
Tout comme de nombreuses instructions que nous avons vues dans la partie précédente, 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.
2.
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.
2.
3.
4.
5.
ldc "ma chaîne"
astore_2
ldc2_w 3.14
d
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.
2.
3.
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.
V-E. What's next ?▲
Dans la partie suivante, nous verrons des instructions permettant d'effectuer des opérations sur des nombres, mais aussi de changer le type des nombres.