Accueil Nos publications Blog JVM Hardcore – Part 2 – Bytecode – Plume Java Bytecode Assembler

JVM Hardcore – Part 2 – Bytecode – Plume Java Bytecode Assembler

academic_duke Ajourd’hui nous allons 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é à 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)

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

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.

.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. Et 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 :

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. A 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 :

ant assemble

Le test unitaire correspondant est le suivant :

@Test
public void add0() {
  final int sum = Adder.add(2, 3);

  Assert.assertEquals(5, sum);
}

Source

Pour l’exécuter depuis Ant :

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

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)

Commentaires

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

  • un signe dièse (#)
  • un arobase (@)
  • un point virgule (;)

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

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

/* Ceci est
    un commentaire
    de plusieurs lignes */

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é.

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

Exemple :

.class org/isk/jvmhardcore/bytecode/parttwo/Adder
    # ...
.endclass

Equivalent Java :

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)

Méthodes

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

.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)

Noms

Le nom 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.

Encodage

Le projet jvm_hardcore doit être en UTF-8.

Créer un fichier classe en Java

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

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

@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 :

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

Source du test

Utilisation du code source

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

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é)

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.

Scripts de construction Ant

Une target assemble – déjà présentée au cours de cet article – a été rajoutée.

ant assemble

De plus, pour que les scripts de construction restent simple 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 ou 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.

What’s next

Au cours de cet article, 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 l’article suivant. Nous étudierons nos premières instructions.