Tutoriel sur la compréhension de la machine virtuelle Java

Image non disponible


précédentsommairesuivant

III. Bytecode avec Plume Java Bytecode Assembler (PJBA)

Nous allons maintenant voir comment utiliser Plume Java Bytecode Assembler (PJBA) qui nous permettra d'avoir l'impression d'écrire du bytecode sans passer par des 0 et des 1. Nous l'utiliserons de manière intensive au cours des prochaines semaines. Sans lui, il aurait été nécessaire :

  • soit d'utiliser plusieurs outils qui peuvent s'avérer compliqués à prendre en main ;
  • soit d'écrire des fichiers binaires à la main, ce qui est, vous me l'accorderez, aussi utile que BrainFuck.

De plus, en ayant notre propre outil, nous allons pouvoir l'étudier en profondeur, puisqu'il a été conçu tout aussi bien pour être simple à utiliser qu'à comprendre.

Par convention, nous utiliserons l'extension .pjb pour identifier les fichiers devant être assemblés par PJBA, mais toutes les extensions sont possibles.

Le code est disponible sur Github (tag et branche).

III-A. Getting Started

Pour commencer, nous allons voir un exemple complet d'une classe avec la méthode statique nommée add() de la partie précédente qui additionne deux entiers.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
.class org/isk/jvmhardcore/bytecode/parttwo/Adder
 
  .method add(II)I
    iload_0
    iload_
    iadd
    ireturn
  .methodend
 
.classend

Source

Toutes les lignes commençant par un point (« . ») sont des directives et ce sont les choses qui nous intéresseront dans cette partie.

Pour l'instant, nous nous limiterons au strict minimum nous permettant d'étudier nos premières instructions. Dans quelques semaines, nous recréerons cet assembleur extrêmement simple. Nous pourrons ensuite y ajouter des fonctionnalités au fur et à mesure.

La version équivalente en Java est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
package org.isk.jvmhardcore.bytecode.parttwo;
 
public class Adder {
  public static int add (int i1, int i2) {
    return i1 + i2;
  }
}

Pour tester la méthode add() de la version Plume, il nous faut tout d'abord assembler le fichier Adder.pjb en un fichier .class. À partir de là, en ajoutant le fichier .class au classpath, nous pouvons appeler la méthode depuis un test unitaire écrit en Java.

Pour assembler les fichiers .pjb en .class :

 
Sélectionnez
ant assemble

Le test unitaire correspondant est le suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
@Test
public void add0() {
  final int sum = Adder.add(2, 3);
 
  Assert.assertEquals(5, sum);
}

Source

Pour l'exécuter depuis Ant :

 
Sélectionnez
ant test -DtestClass=org.isk.jvmhardcore.bytecode.parttwo.AdderTest -DtestMethod=add0

III-B. Descripteurs

Un descripteur est un moyen d'exprimer un type à l'aide de lettres et de symboles :

  • la plupart des types sont représentés par un seul caractère (I pour int, F pour float, ou de manière moins évidente Z pour boolean, J pour long, etc.) ;
  • le symbole [ indique un tableau du type indiqué ensuite. Par exemple [I signifie un tableau d'entiers. Il est possible d'avoir plusieurs crochets à la suite pour indiquer des tableaux à plusieurs dimensions ([[Z pour un tableau de tableaux de booléens) ;
  • la lettre L signifie un objet du type qui suit (Ljava/lang/String;) ;
  • seuls les types d'objets se terminent par un point-virgule.

Tous les descripteurs possibles sont présentés dans le tableau suivant :

Descripteur

Type

Z

boolean

B

byte

S

short

C

char

I

int

J

long

F

float

D

double

V

void

[<type>

tableau de type <type>

L<type>;

Objet de type <type>

Note : tout comme en Java, en bytecode (et de fait dans un fichier .pjb), V (void) ne peut être utilisé qu'en type de retour d'une méthode.

Exemples :

Bytecode

Java

add(II)I

int add(int i1, int i2)

concat(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

String concat(String s1, String s2)

merge([Z[Z)[Z

boolean[] merge(boolean[] a1, boolean[] a2)

III-C. Commentaires

Les commentaires d'une seule ligne sont identifiés soit par :

  • un signe croisillon (#) ;
  • une arobase (@);
  • un point-virgule (;).

Tout ce qui se trouve à droite de ces signes est un commentaire.

Il est aussi possible d'avoir des commentaires de plusieurs lignes (/* … */) :

 
Sélectionnez
1.
2.
3.
/* Ceci est
    un commentaire
    de plusieurs lignes */

III-D. Classes

La première directive que nous allons voir est .class. Elle indique à l'assembleur le nom complètement qualifié de la classe. De plus, ce nom indique le nom des répertoires contenant le fichier et celui du fichier .class généré.

 
Sélectionnez
1.
2.
3.
.class <nom complètement qualifié de la classe>
    # contenu de la classe
.endclass # Indique la fin d'une classe

Exemple :

 
Sélectionnez
1.
2.
3.
.class org/isk/jvmhardcore/bytecode/parttwo/Adder
    # ...
.endclass

Équivalent Java :

 
Sélectionnez
1.
2.
3.
4.
5.
package org.isk.jvmhardcore.bytecode.parttwo;
 
public class Adder {
 
}

Le nom du package et de la classe sont concaténés et les points inclus dans le nom du package sont remplacés par des slashes (/).

Le nom du package suivi de celui de la classe constituent le nom complètement qualifié d'une classe.

Limitations actuelles que nous lèverons ultérieurement :

  • une seule directive .class par fichier est autorisée ;
  • une classe est obligatoirement publique (public).

III-E. Méthodes

Une méthode est définie de la manière suivante :

 
Sélectionnez
1.
2.
3.
.method <nom de la méthode>(<descripteur des paramètres>)<descripteur du retour>
   # Instructions
.endmethod

Notons que contrairement à Java, le type de retour est complètement à la fin, comme nous l'avons vu dans le tableau précédent.

Notes :

  • pour indiquer plusieurs paramètres, il suffit de les écrire les uns à la suite des autres sans espace ;
  • le descripteur de la méthode (paramètres et type de retour) doit être unique. En revanche, contrairement à Java, deux descripteurs peuvent différer uniquement par le type de retour.

Limitations actuelles :

  • une méthode est obligatoirement publique et statique (public static).

III-F. Noms

Les noms des packages, classes et méthodes doivent commencer par une lettre ASCII, le caractère underscore « _ » ou dollar « $ », puis des lettres ASCII, des chiffres ou les caractères underscore « _ » ou dollar ‘$'.

Par exemple : helloworld, $hello_world1, _hello$world2, _my/$package/DoSomething1, etc.

III-G. Encodage

Le projet jvm_hardcore doit être en UTF-8.

III-H. Créer un fichier classe en Java

PJBA permet de créer un fichier .class à partir d'un fichier texte comme nous venons de le voir. Mais il est aussi possible d'utiliser des objets Java. Néanmoins en l'état, pour qui ne sait pas de quoi est constitué le format .class, son utilisation est plutôt compliquée.

En reprenant encore une fois notre exemple de méthode add() :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
@Test
public void assembleJava() throws Exception {
  // ConstantPoolEntries
  final String className = "org/isk/jvmhardcore/bytecode/parttwo/AdderJava";
  final String methodName = "add";
  final String methodDescriptor = "(II)I";
 
  // ClassFile
  final ClassFile classFile = new ClassFile(className);
  final int classNameIndex = classFile.addConstantUTF8(className);
  final int thisClassIndex = classFile.addConstantClass(classNameIndex);
  classFile.setThisClassIndex(thisClassIndex);
 
  // Method
  final int codeAttributeIndex = classFile.addConstantUTF8(Code.ATTRIBUTE_NAME);
  final int methodIndex = classFile.addConstantUTF8(methodName);
  final int descriptorIndex = classFile.addConstantUTF8(methodDescriptor);
  final Method method = new Method(codeAttributeIndex);
  method.setNameIndex(methodIndex);
  final int parameterCount = Method.getParameterCount(methodDescriptor);
  method.setDescriptorIndex(descriptorIndex, parameterCount);
  classFile.addMethod(method);
 
  // Instructions
  method.addInstruction(Instructions.iload_0());
  method.addInstruction(Instructions.iload_1());
  method.addInstruction(Instructions.iadd());
  method.addInstruction(Instructions.ireturn());
 
  this.createFile(classFile);
}

Source

Je ne m'attarderai pas sur cet exemple, nous y reviendrons un peu plus tard. De plus, une de nos actions sera de simplifier l'utilisation de PJBA en mode Java puisque même si l'on sait à quoi tout ceci correspond, 30 lignes pour créer une classe et une méthode qui ne fait qu'une addition, cela fait beaucoup.

Pour tester cette classe :

 
Sélectionnez
ant assemble
ant test -DtestClass=org.isk.jvmhardcore.bytecode.parttwo.AdderTest -DtestMethod=add1

Source du test

III-I. Utilisation du code source

Pour pouvoir assembler les fichiers .pjb en .class, la structure des projets a dû évoluer :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
project
|  +- 01_src (code source)
|  |  +- main
|  |  |  +- assembler (Assembleur)
|  |  |  +- java
|  |  |  +- pjb (Plume Java Bytecode)
|  |  +- test
|  |  |  +- java
|  |  |  +- resources
|  +- 02_build (généré)
|  |  |  +- assembler
|  |  |  |  +- classes (Assembleur compilé)
|  |  |  |  +- reports (Rapport d'assemblage)
|  |  |  +- classes
|  |  |  +- junit-data
|  |  |  +- junit-reports
|  |  |  +- pjb-classes (.pjb assemblés en .class)
|  |  |  +- test-classes
|  +- 03_dist (généré)

III-I-1. Assembleur

Le répertoire 01_src/main/assembler/ contient les classes permettant de générer des fichiers .class à partir d'un fichier .pjb ou du code Java. Par simplicité, le choix a été fait d'utiliser JUnit au lieu d'une méthode main() pour exécuter l'assembleur.

Le répertoire 02_build/assembler/ contient les composants compilés de l'assembleur et les rapports des tests unitaires exécutés pour pouvoir générer des fichiers .class.

La méthode assemblePjb() prend tous les fichiers du répertoire pjb/ et de ses enfants quelle que soit leur extension et appelle le parser PJBA (disponible dans le répertoire 02_libs sous le nom pjba.jar).

Les fichiers générés sont créés dans le répertoire 02_build/pjb-classes. Il doit être par conséquent rajouté au classpath pour que les tests unitaires testant ces classes et méthodes puissent y accéder.

III-I-2. Scripts de construction Ant

Une target assemble - déjà présentée au cours de cette partie - a été rajoutée.

 
Sélectionnez
ant assemble

De plus, pour que les scripts de construction restent simples et qu'il soit possible de compiler/assembler tous les projets à partir du script principal (celui à la racine du projet), toutes les commandes (targets) Ant sont accessibles depuis le répertoire root de n'importe quel projet. Par conséquent, il est possible d'utiliser la target compile sur le projet bytecode/ alors que le répertoire 01_src/main/java n'existe pas. Son exécution se terminera en succès, mais ne fera rien, puisque nous testons si le répertoire 01_src/main/java existe avant de poursuivre l'exécution.

III-J. What's next ?

Au cours de cette partie, nous nous sommes contenté de représenter une classe sous une forme proche de sa représentation en bytecode. Les choses sérieuses commenceront dès la partie suivante. Nous étudierons nos premières instructions.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2016 SOAT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.