JVM Hardcore – Part 11 – Bytecode – Format d’un fichier .class
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;
}
}
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();
}
}
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;
}
}
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;
}
}
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;
}
}
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 typeConstantUTF8
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;
}
}
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;
}
}
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 flagsACC_PUBLIC
etACC_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 flagsACC_ABSTRACT
etACC_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;
}
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
ouACC_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
, ouACC_SYNCHRONIZED
. - une méthode d’interface doit uniquement avoir les flags suivants à 1 :
ACC_ABSTRACT
etACC_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 sourceConstantValue
: permet de définir la valeur d’une constanteCode
: permet d’indiquer l’ensemble des instructions d’une méthodeExceptions
: permet de définir l’ensemble des exceptions – vérifiées – potentiellement levées par une méthodeInnerClasses
: permet de définir une classe interneSynthetic
: permet d’indiquer qu’une classe, un champ ou une méthode, n’existant pas dans le fichier source, a été rajouté par le compilateurLineNumberTable
: permet d’indiquer à quelle ligne du fichier source correspondent un ensemble d’instructionsLocalVariableTable
: permet d’attribuer un nom à la valeur présent à un index donné des variables localesDeprecated
: 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 byte
s 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.