Tutoriel sur la compréhension de la machine virtuelle Java

Image non disponible


précédentsommairesuivant

XII. ClassFile

Après plusieurs chapitres de détours, nous allons enfin reprendre notre périple dans l'univers du bytecode. Si l'ensemble des articles précédents ont bien été compris, créer un assembleur de bytecode ne sera qu'une simple formalité. Aujourd'hui, nous allons nous intéresser au format d'un fichier .class ce qui nous permettra de créer un assembleur simplifié (tel que celui que nous avons utilisé jusqu'à présent). Bien que simplifié, pour implémenter un tel assembleur nous avons besoin de comprendre 70 % (au doigt mouillé) du format d'un fichier .class décrit dans la JVMS et c'est à quoi nous allons nous atteler au cours de cet article.

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

XII-A. ClassFile

La JVMS définit un format binaire appelé le fichier class, qui représente une classe en tant qu'un flux d'octets. Mais le terme fichier class est trompeur puisqu'il n'y a pas d'obligation à ce que les données au format fichier class soient stockées dans un fichier. Elles peuvent être stockées dans une base de données, en mémoire, etc.

Un fichier .class peut être représenté par la classe Java suivante :

 
Sélectionnez
public class ClassFile {
  int magic;
  short minorVersion;
  short majorVersion;
  short constantPoolCount;
  ConstantPoolEntry[] constantPool;
  short accessFlags;
  short thisClass;
  short superClass;
  short interfacesCount;
  short[] interfaces;
  short fieldsCount;
  Field[] fields;
  short methodsCount;
  Method[] methods;
  short attributesCount;
  Attribute[] attributes;
}

Source (À noter que toutes les sources de cet article sont légèrement différentes de manière à suivre les principes de base de la programmation objet.)

où les types byte, short et int doivent être considérés comme des nombres non signés. Dans la JVMS ces trois types sont représentés respectivement sous les types u1, u2 et u4, où u signifie unsigned (non signé) et le chiffre indique le nombre d'octets.

Dans cet article nous allons faire l'impasse sur les champs suivants :

 
Sélectionnez
short[] interfaces;
Field[] fields;
Attribute[] attributes;

On considérera que leur champ associé - indiquant la taille des tableaux - aura pour valeur 0.

 
Sélectionnez
short interfacesCount;
short fieldsCount;
short attributesCount;

De plus, les types ConstantPoolEntry et Method ne seront étudiés que partiellement. Nous compléterons le détail d'un fichier .class au fil des articles suivants.

Avant de commencer à décrire l'utilité et l'utilisation des champs, il est important de noter que la génération d'un fichier .class consiste à ajouter tous les champs de la classe ClassFile et de toutes les classes du graphe d'objets, dans l'ordre, sous la forme d'octets, d'où le nom bytecode. Dans l'API Java, ce format est supporté par les interfaces java.io.DataInput et java.io.DataOutput, et des classes telles que java.io.DataInputStream et java.io.DataOutputStream.

En nous limitant aux quatre premiers champs, nous pouvons générer un fichier .class de la manière suivante :

 
Sélectionnez
DataOutputStream dos = new DataOutputStream(/*...*/);
dos.writeInt(this.magic);
dos.writeShort(this.minorVersion);
dos.writeShort(this.majorVersion);
dos.writeShort(this.constantPoolCount);

XII-A-1. magic

Le champ magic permet d'identifier un fichier comme étant un fichier .class. Il a toujours pour valeur 0xCAFEBABE. Nous pouvons donc écrire :

 
Sélectionnez
final int magic = 0xCAFEBABE;

XII-A-2. minorVersion et majorVersion

Les champs minorVersion et majorVersion indiquent le numéro de version du fichier .class. La JDK 1.0.2 supporte les versions comprises dans l'intervalle [45.0; 45.3], la JDK 1.1.* dans l'intervalle [45.0; 45.65536] et les JDK 1.k supportent les versions dans l'intervalle [45.0; 44+k.0] où k >= 2.

Par exemple, la JDK7 supporte les versions de fichiers .class comprises dans l'intervalle [45.0; 51.0].

Or comme mentionné dans la partie 3 (Part 3 - Bytecode - Constantes), nous allons nous concentrer sur Java 1.4 (version 48.0), nous pouvons rassembler les deux champs par un seul (version) :

 
Sélectionnez
// 48 = 0x00 (version mineure) | 0x30 (version majeure)
final private short version = 0x30;

0x30 étant une valeur littérale inférieure à 32 767 nous n'avons pas besoin de la caster pour l'assigner à un short. Néanmoins, comme nous l'avons déjà vu, la JVM préfère manipuler des int. Par conséquent, rien ne nous empêche d'écrire :

 
Sélectionnez
final private int version = 0x30;

Il suffira de ne pas oublier, lors de la génération du fichier classe, d'appeler la méthode writeShort() de la classe DataOutputStream, et non writeInt().

XII-A-3. constantPoolCount et constantPool

La partie suivante est assez particulière. Elle représente un pool de constantes et n'a aucun équivalent en Java. Elle a pour but de rassembler toutes les constantes présentes dans le code, comme le nom d'une classe ou d'une méthode, des littérales et des valeurs de constantes définies dans le code.

Le tableau constantPool est indexé de 1 à constantPoolCount - 1. L'index 0 est réservé pour l'utilisation de la JVM.

Chaque classe peut avoir jusqu'à 65 535 entrées dans son pool de constantes.

La classe ConstantPoolEntry a le format suivant :

 
Sélectionnez
ConstantPoolEntry {
  byte tag;
  byte[] info;
}

Le champ tag correspond au type de constante. Pour la JDK 1.4, il existe 11 tags différents, numérotés de 1 à 12 (2 n'étant pas utilisé).

Type de constante

Valeur

ConstantUtf8

1

ConstantInteger

3

ConstantFloat

4

ConstantLong

5

ConstantDouble

6

ConstantClass

7

ConstantString

8

ConstantFieldref

9

ConstantMethodref

10

ConstantInterfaceMethodref

11

ConstantNameAndType

12

Ces tags peuvent être représentés en Java sous la forme d'une énumération :

 
Sélectionnez
public static enum ConstantPoolTag {
  UTF8(1),
  INTEGER(3),
  FLOAT(4),
  LONG(5),
  DOUBLE(6),
  CLASS(7),
  STRING(8),
  FIELDREF(9),
  METHODREF(10),
  INTERFACE_METHODREF(11),
  NAME_AND_TYPE(12);
 
  private int value;
 
  private ConstantPoolTag(int value) {
    this.value = value;
  }
 
  public int getValue() {
    return this.value;
  }
}

Source

Le contenu du tableau d'octets info varie en fonction du type de tag. Nous allons donc créer une classe par type de constantes, avec une classe parente contenant le tag.

 
Sélectionnez
public static abstract class ConstantPoolEntry {
  final private int tag;
 
  public ConstantPoolEntry(ConstantPoolTag tag) {
    this.tag = tag.getValue();
  }
}

Source

XII-A-3-a. ConstantUtf8

Le type de constante le plus commun est le type UTF-8. La classe ConstantUtf8 est utilisée pour stocker le nom des packages, des classes, des champs, des méthodes et les littérales, mais aussi du texte ajouté lors de la génération des fichiers .class et utilisé par la JVM.

Les deux premiers octets correspondent à la taille de la chaîne de caractères (le nombre d'octets en UTF-8 modifié), les autres, la chaîne de caractères codée en UTF-8.

 
Sélectionnez
public class ConstantUTF8 {
  int tag = 0x01;
  int length;
  byte[] string;
}

La méthode writeUTF() de la classe DataOutputStream permet de transformer des chaînes de caractères codées en UTF-16 en UTF-8 modifié, en la faisant précéder par la taille de la chaîne.

Nous pouvons donc modifier la classe ConstantUTF8 de la manière suivante :

 
Sélectionnez
public static class UTF8 extends ConstantPoolEntry {
  final private java.lang.String value;
 
  public UTF8(final java.lang.String value) {
    super(ConstantPoolTag.UTF8);
    this.value = value;
  }
}

Source

XII-A-3-b. ConstantInt et ConstantFloat

Les classes ConstantInt et ConstantFloat permettent de stocker des constantes de types int et float.

 
Sélectionnez
public class ConstantInt {
  int tag = 0x03;
  int value;
}
 
public class ConstantFloat {
  int tag = 0x04;
  int value;
}

En suivant notre modélisation, les classes sont définies de la manière suivante :

 
Sélectionnez
public static class Integer extends ConstantPoolEntry {
  final private int integer;
 
  public Integer(int integer) {
    super(ConstantPoolTag.INTEGER);
    this.integer = integer;
  }
}
 
public static class Float extends ConstantPoolEntry {
  final private float floatValue;
 
  public Float(float floatValue) {
    super(ConstantPoolTag.FLOAT);
    this.floatValue = floatValue;
  }
}

Source

XII-A-3-c. ConstantLong et ConstantDouble

Les classes ConstantLong et ConstantDouble permettent de stocker des constantes de types long et double.

 
Sélectionnez
public class ConstantLong {
  int tag = 0x05;
  long value;
}
 
public class ConstantDouble {
  int tag = 0x06;
  long value;
}

Toutes les constantes dont les valeurs ont pour taille 8 octets prennent deux entrées du tableau constantPool d'un fichier .class. Si les constantes ConstantLong ou ConstantDouble sont présentes dans le tableau constantPool à l'index n, alors le prochain élément utilisable se trouve à l'index n+2. L'index n+1 est considéré comme inutilisable.

 
Sélectionnez
public static class Long extends ConstantPoolEntry {
  final private long longValue;
 
  public Long(long longValue) {
    super(ConstantPoolTag.LONG);
    this.longValue = longValue;
  }
}
 
public static class Double extends ConstantPoolEntry {
  final private double doubleValue;
 
  public Double(double doubleValue) {
    super(ConstantPoolTag.DOUBLE);
    this.doubleValue = doubleValue;
  }
}

Source

XII-A-3-d. ConstantString

La classe ConstantString permet d'indiquer qu'une chaîne de caractères est une constante. Elle contient l'index d'un objet de type ConstantUTF8 dans le tableau de ConstantPoolEntry.

Il ne faut donc pas confondre la classe ConstantString qui pointe vers une instance de la classe ConstantUTF8, avec la classe ConstantUTF8 qui contient la chaîne de caractères.

Sans rentrer dans le détail, lorsque nous utilisons l'instruction ldc suivie d'une chaîne de caractères :

 
Sélectionnez
ldc "Hello World"

nous procédons de la manière suivante :

  • nous créons un objet de type ConstantUTF8 contenant la valeur "Hello World" ;
  • nous ajoutons cet objet au tableau de type ConstantPoolEntry et récupérons l'index associé ;
  • nous créons un objet de type ConstantString contenant l'index l'objet de type ConstantUTF8 précédemment créé ;
  • nous ajoutons cet objet au tableau de type ConstantPoolEntry et récupérons l'index associé ;
  • nous associons l'instruction ldc à l'index précédent.

La structure de la classe ConstantString est la suivante :

 
Sélectionnez
public class ConstantString {
  int tag = 0x08;
  short utf8Index;
}

ou selon notre modèle :

 
Sélectionnez
public static class String extends ConstantPoolEntry {
  final private int utf8Index;
 
  public String(int stringIndex) {
    super(ConstantPoolTag.STRING);
    this.utf8Index = stringIndex;
  }
}

Source

XII-A-3-e. ConstantClass

La classe ConstantClass s'utilise de la même manière que la classe ConstantString. Elle stocke l'index d'un objet de type

 
Sélectionnez
ConstantUTF8

dans le tableau de ConstantPoolEntry qui lui contient le nom complètement qualifié d'une classe, tel que java/lang/Object, ou puisqu'un tableau est un objet la valeur [[I.

La structure de la classe est la suivante :

 
Sélectionnez
public class ConstantClass {
  int tag = 0x07;
  short utf8Index;
}

ou selon notre modèle :

 
Sélectionnez
public static class Class extends ConstantPoolEntry {
  final private int nameIndex;
 
  public Class(final int nameIndex) {
    super(ConstantPoolTag.CLASS);
    this.nameIndex = nameIndex;
  }
}

Source

XII-A-3-f. Les quatre derniers tags

Pour l'instant nous n'avons pas besoin de nous soucier des quatre autres tags. Nous les étudierons le moment venu.

XII-A-4. accessFlags

Le champ accessFlags permet d'indiquer l'ensemble des modificateurs d'une classe à l'aide de masques. En d'autres termes chaque bit correspond à un type de modificateur sachant que plusieurs peuvent être utilisés en même temps, mais que certaines combinaisons sont impossibles.

Nom du flag

Valeur

Mot clé Java

ACC_PUBLIC

0x0001

public

ACC_FINAL

0x0010

final

ACC_SUPER

0x0020

-

ACC_INTERFACE

0x0200

interface

ACC_ABSTRACT

0x0400

abstract

La représentation sur 2 octets est la suivante :

 
Sélectionnez
0000 a0b0 00cd 000e

où :

 
Sélectionnez
a = 0x0400 = 0000 1000 0000 0000 (ACC_ABSTRACT)
b = 0x0200 = 0000 0010 0000 0000 (ACC_INTERFACE)
c = 0x0020 = 0000 0000 0010 0000 (ACC_SUPER)
d = 0x0010 = 0000 0000 0001 0000 (ACC_FINAL)
e = 0x0001 = 0000 0000 0000 0001 (ACC_PUBLIC)

Quelques règles :

  • une interface est distinguée d'une classe à l'aide du flag ACC_INTERFACE ;
  • si le flag ACC_INTERFACE est à 1, il doit obligatoirement être accompagné des flags ACC_PUBLIC et ACC_ABSTRACT et les autres ne doivent pas être renseignés ;
  • si le flag ACC_INTERFACE est à 0, tous les autres peuvent être utilisés. Néanmoins, les flags ACC_ABSTRACT et ACC_FINAL ne peuvent pas être utilisés en même temps ;
  • le flag ACC_SUPER est un artefact du passé lié à l'instruction invokespecial. Tous les nouveaux compilateurs/assembleurs doivent le renseigner.

Dans la version de pjba que nous utilisons actuellement, il est impossible de préciser les modificateurs d'une classe. Le champ accessFlags a pour valeur 0x0021 :

 
Sélectionnez
0x0001 | 0x0020 // public super

XII-A-5. thisClass

Le champ thisClass contient l'index de l'objet de type ConstantClass dans le tableau de PoolConstantEntry pointant (par un index dans le tableau de PoolConstantEntry) vers l'objet de type ConstantUtf8 contenant le nom complètement qualifié de la classe.

XII-A-6. superClass

Le champ superClass contient l'index de l'objet de type ConstantClass dans le tableau de PoolConstantEntry pointant (par un index dans le tableau de PoolConstantEntry) vers l'objet de type ConstantUtf8 contenant le nom complètement qualifié de la classe parente de la classe courante.

En Java, une classe n'héritant pas explicitement (à l'aide du mot clé extends) d'une autre, hérite implicitement de la classe java.lang.Object. Dans un fichier .class tout étant indiqué explicitement, nous aurons toujours une super classe. La seule classe pouvant avoir le champ superClass égal à zéro est la classe Object.

XII-A-7. methodsCount et methods

Le champ methodsCount indique le nombre de méthodes définies dans le fichier .class (tout simplement la taille du tableau methods) et le champ methods est un tableau de méthodes de type Method dont la structure peut être représentée de la manière suivante :

 
Sélectionnez
public class Method {
  short accessFlags;
  short nameIndex;
  short descriptorIndex;
  short attributesCount;
  Attribute[] attributes;
}

Source

XII-B. Method

XII-B-1. accessFlags

Tout comme le champ accessFlags dans la classe ClassFile, celui-ci permet d'indiquer les modificateurs d'une méthode à l'aide de masques.

Nom du flag

Valeur

Mot clé Java

ACC_PUBLIC

0x0001

public

ACC_PRIVATE

0x0002

private

ACC_PROTECTED

0x0004

protected

ACC_STATIC

0x0008

static

ACC_FINAL

0x0010

final

ACC_SYNCHRONIZED

0x0020

synchronized

ACC_NATIVE

0x0100

native

ACC_ABSTRACT

0x0400

abstract

ACC_STRICT

0x0800

strictfp

Quelques règles :

  • une méthode ne peut avoir que l'un des flags suivants à 1 à la fois : ACC_PRIVATE, ACC_PROTECTED ou ACC_PUBLIC ;
  • une méthode ayant le flag ACC_ABSTRACT à 1, ne peut avoir aucun des flags suivants : ACC_FINAL, ACC_NATIVE, ACC_PRIVATE, ACC_STATIC, ACC_STRICT, ou ACC_SYNCHRONIZED ;
  • une méthode d'interface doit uniquement avoir les flags suivants à 1 : ACC_ABSTRACT et ACC_PUBLIC.

XII-B-2. nameIndex

Le champ nameIndex contient l'index de l'objet de type ConstantUtf8 dans le tableau ConstantPoolEntry contenant le nom de la méthode. Par exemple add.

XII-B-3. descriptorIndex

Le champ descriptorIndex contient l'index de l'objet de type ConstantUtf8 dans le tableau ConstantPoolEntry contenant le nom du descripteur de la méthode. Par exemple (I[[Lorg/isk/pjb/Test;Z)V.

XII-B-4. attributesCount et attributes

Le champ attributesCount indique la taille du tableau attributes et le champ attributes est un tableau de type Attribute dont la structure peut être représentée de la manière suivante :

 
Sélectionnez
public class Attribute {
  short nameIndex;
  int attributeLength;
  byte[] info;
}

Source (Dans le code, la classe Attribute est abstraite et n'a pas de champ info.)

XII-C. Attribute

Nous retrouvons la structure Attribute dans de nombreuses autres :

  • ClassFile  ;
  • Field  ;
  • Method  ;
  • Code .

Tout comme les ConstantXxx, il existe de nombreux attributs (9 pour la JDK 1.4), mais tous ne peuvent pas se retrouver dans les quatre structures mentionnées précédemment :

  • SourceFile : permet d'indiquer le nom du fichier source ;
  • ConstantValue : permet de définir la valeur d'une constante ;
  • Code : permet d'indiquer l'ensemble des instructions d'une méthode ;
  • Exceptions : permet de définir l'ensemble des exceptions - vérifiées - potentiellement levées par une méthode ;
  • InnerClasses : permet de définir une classe interne ;
  • Synthetic : permet d'indiquer qu'une classe, un champ ou une méthode, n'existant pas dans le fichier source, a été rajouté par le compilateur ;
  • LineNumberTable : permet d'indiquer à quelle ligne du fichier source correspondent un ensemble d'instructions ;
  • LocalVariableTable : permet d'attribuer un nom à la valeur présente à un index donné des variables locales ;
  • Deprecated : permet d'indiquer qu'une classe, un champ ou une méthode est déprécié.

Aujourd'hui, nous nous intéresserons uniquement à l'attribut Code.

XII-C-1. Code

L'attribut Code ne peut être présent que dans la structure Method.

L'attribut Code est probablement l'élément le plus important d'un fichier .class puisqu'il contient tous les détails d'implémentation d'une méthode.

Si une méthode est native ou abstraite, l'attribut Code ne doit pas être présent.

 
Sélectionnez
public class Code {
  short attributeNameIndex;
  int attributeLength;
  short maxStack;
  short maxLocals;
  int codeLength;
  byte[] code;
  short exceptionsCount;
  Exception[] exceptions;
  short attributesCount;
  Attribute[] attributes;
}

Source (Dans le code, la classe Code hérite de la classe Attribute.)

Les champs suivants ne seront pas décrits dans cet article :

 
Sélectionnez
short exceptionsCount;
Exception[] exceptions;
short attributesCount;
Attribute[] attributes;

On considérera que leur champ associé - indiquant la taille des tableaux - aura pour valeur 0.

 
Sélectionnez
short exceptionsCount;
short attributesCount;

XII-C-1-a. attributeNameIndex

Le champ attributeNameIndex contient l'index de l'objet de type ConstantUtf8 dans le tableau ConstantPoolEntry contenant le nom du type de l'attribut. Pour l'attribut Code, le nom est « Code ».

XII-C-1-b. attributeLength

Le champ attributeLength indique le nombre d'octets constituant l'attribut, moins les six premiers octets.

Nous pouvons calculer cette valeur de la manière suivante :

 
Sélectionnez
2 + 2 + 4 + code.length + 2 + 8 * exceptions.length + 2 + attributes.length

Note : La structure Exception prend 8 octets.

XII-C-1-c. maxStack

Le champ maxStack a pour valeur la taille maximum de la pile du cadre (frame) de la méthode (pour plus d'informations cf. Part 1 - Introduction à la JVM). Nous pourrions utiliser la valeur maximale pouvant être contenue dans un short non signé (0xFFFFFFFF), mais nous ne souhaitons pas consommer de la mémoire inutilement. Par conséquent, il est nécessaire de calculer précisément l'impact de chaque instruction sur la pile. Nous verrons comment faire dans l'article suivant.

XII-C-1-d. maxLocals

Le champ maxLocals a pour valeur la taille maximum des variables locales du cadre de la méthode. Nous verrons aussi comment la calculer dans l'article suivant.

XII-C-1-e. codeLength et code

Le champ codeLength indique la taille du tableau code et le champ code est un tableau de bytes contenant les instructions et leurs arguments si elles en ont.

Chaque opcode d'une instruction tient sur un octet. La taille et le nombre des arguments - des instructions qui en possèdent - sont variables. Sur les 200 instructions, seulement un quart ont des arguments (nous en avons déjà vu une dizaine ldc, bipush, xload, xstore, etc.)

XII-D. What's next

Nous avons à présent un ensemble de classes permettant de modéliser le format d'un fichier .class. Dans l'article suivant, nous verrons comment générer un fichier .class et survolerons l'implémentation d'un analyseur syntaxique de fichiers .pjb.


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.