Accueil Nos publications Blog JVM Hardcore – Part 11 – Bytecode – Format d’un fichier .class

JVM Hardcore – Part 11 – Bytecode – Format d’un fichier .class

academic_duke
Après plusieurs semaines 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)

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

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 (A noter que toutes les sources 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 :

short[] interfaces;
Field[] fields;
Attribute[] attributes;

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

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 :

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

magic

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

final int magic = 0xCAFEBABE;

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 JDKs 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) :

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

0x30 étant une valeur littérale inférieure à 32767 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 :

final private int version = 0x30;

Il suffira juste 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().

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 par exemple, 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’à 65535 entrées dans son pool de constantes.

La classe ConstantPoolEntry a le format suivant :

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 :

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.

public static abstract class ConstantPoolEntry {
  final private int tag;

  public ConstantPoolEntry(ConstantPoolTag tag) {
    this.tag = tag.getValue();
  }
}

Source

ConstantUtf8

Le type de constante le plus commun est le type UTF8. 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.

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 :

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

ConstantInt et ConstantFloat

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

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 :

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

ConstantLong et ConstantDouble

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

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 de le 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.

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

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 suivit d’une chaîne de caractères :

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 :

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

ou selon notre modèle :

public static class String extends ConstantPoolEntry {
  final private int utf8Index;

  public String(int stringIndex) {
    super(ConstantPoolTag.STRING);
    this.utf8Index = stringIndex;
  }
}

Source

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

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

ou selon notre modèle :

public static class Class extends ConstantPoolEntry {
  final private int nameIndex;

  public Class(final int nameIndex) {
    super(ConstantPoolTag.CLASS);
    this.nameIndex = nameIndex;
  }
}

Source

Les quatres derniers tags

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

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

0000 a0b0 00cd 000e

où :

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 :

0x0001 | 0x0020 // public super

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.

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.

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 :

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

Source

Method

accessFlags

Tout comme le champs 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.

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.

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.

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 :

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)

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 toutes 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ésent à 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.

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.

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 :

short exceptionsCount;
Exception[] exceptions;
short attributesCount;
Attribute[] attributes;

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

short exceptionsCount;
short attributesCount;

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

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 :

2 + 2 + 4 + code.length + 2 + 8 * exceptions.length + 2 + attributes.length

Note : La structure Exception prend 8 octets.

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.

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.

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 leur arguments s’ils en ont.

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

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.