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 :
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 :
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);
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 :
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) :
//
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 :
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 :
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
(
);
}
}
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.
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;
}
}
XII-A-3-b. 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;
}
}
XII-A-3-c. 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 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.
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;
}
}
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 :
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;
}
}
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
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;
}
}
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 |
|
|
ACC_FINAL |
|
|
ACC_SUPER |
|
|
ACC_INTERFACE |
|
|
ACC_ABSTRACT |
|
|
La représentation sur 2 octets est la suivante :
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
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 :
public
class
Method {
short
accessFlags;
short
nameIndex;
short
descriptorIndex;
short
attributesCount;
Attribute[] attributes;
}
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 |
|
|
ACC_PRIVATE |
|
|
ACC_PROTECTED |
|
|
ACC_STATIC |
|
|
ACC_FINAL |
|
|
ACC_SYNCHRONIZED |
|
|
ACC_NATIVE |
|
|
ACC_ABSTRACT |
|
|
ACC_STRICT |
|
|
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 :
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.
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;
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 :
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.