MokaByte Numero 14 - Dicembre 1997

Java: i file binari e la JVM 

 

di  
Paolo Perrucci

 La caratteristica più interessante del linguaggio Java è sicuramente la portabilità del codice su piattaforme diverse. Vedremo quindi la struttura di questo codice e analizzeremo la macchina virtuale e le fasi dell’esecuzione di un file binario

 

 
 

J

Il linguaggio Java deve il suo rapido sviluppo sul mercato al fatto che ben si adatta alla programmazione di applicazioni multipiattaforma destinate all’utilizzo su Internet. La compilazione di un programma Java genera un file binario, contenente il cosidetto bytecode, indipendente dalla piattaforma su cui poi esso sarà eseguito. Questi file hanno un nome con estensione CLASS e devono essere eseguiti su una piattaforma virtuale chiamata JVM (Java Virtual Machine).

Il particolare che rende questo linguaggio portabile si trova nel fatto che esiste una JVM specifica per ogni piattaforma. Questo chiaramente rende tutto il processo di esecuzione più lento e complesso dell’esecuzione di un "normale" eseguibile. Per ovviare a questo la Sun sta lavorando alla realizzazione di un processore che interpreta ed esegue direttamente un file binario Java.

In questo articolo analizzeremo la struttura interna dei file binari prodotti dal compilatore compreso nel JDK 1.1. Successivamente esamineremo l’architettura della macchina virtuale.

Il formato dei file CLASS

Ogni file binario Java contiene la definizione di esattamente una classe e la quantità più piccola di informazione accedibile è il byte. Le informazioni costituite da 16, 32 o 64 bit vengono costruite leggendo rispettivamente 2,4 o 8 byte consecutivi organizzati nel cosiddeto ordine big-endian, ovvero i primi byte letti saranno i byte più significativi.

La struttura dei file CLASS può essere rappresentata dalla pseudo-struttura, scritta usando la notazione del C, mostrata nel riquadro 1. Per rendere più chiara la descrizione di ogni singolo campo definiamo i tre tipi u1, u2 e u3 che rappresentano dei tipi senza segno formati rispettivamente da 1, 2 e 3 byte.

ClassFile {

        u4 magic;

        u2 minor_version;

        u2 major_version;

        u2 constant_pool_count;

        cp_info constant_pool[constant_pool_count - 1];

        u2 access_flags;

        u2 this_class;

        u2 super_class;

        u2 interfaces_count;

        u2 interfaces[interfaces_count];

        u2 fields_count;

        field_info fields[fields_count];

        u2 methods_count;

        method_info methods[methods_count];

        u2 attributes_count;

        attribute_info attributes[attribute_count];

}
 I nomi dei campi usati nell’articolo sono gli stessi adottati della documentazione ufficiale della Sun in modo da rendere più facile, per chi volesse, un approfondimento dell’argomento.

I primi tre campi della struttura hanno valori sempre uguali per ogni file. Il campo magic identifica il formato del file ed il suo valore deve essere 0xCAFEBABE espresso in esadecimale. Le due informazioni che seguono il campo magic rappresentano la versione maggiore e minore del compilatore che ha creato il file CLASS. Nella versione 1.02 del compilatore della Sun il valore di major_version era 45 e quello di minor_version era 3. Solo la Sun può definire nuovi valori per questi due campi.

Inizieremo l’analisi dai due campi constant_pool_count e constant_pool in quanto tutto il resto del file dipende pesantemente da questa parte.

 

La constant pool

La constant pool è una tabella i cui elementi hanno lunghezza variabile. Il contenuto di questa tabella rappresenta tutto ciò che è costante nel file compilato, ovvero stringhe, nomi di classi, nomi di campi, nomi di metodi, e tutte le altre costanti che sono riferite all’interno delle strutture e sottostrutture del file CLASS.

Il campo constant_pool_count del file CLASS definisce il numero di elementi della constant pool e il primo elemento della tabella (constant_pool[0]) è riservato per utilizzi interni alla macchina virtuale. Ogni elemento contenuto in questa struttura ha una dimensione variabile ed il relativo formato è indicato dal primo byte, detto tag, che specifica il tipo di informazione contenuta nell’elemento stesso. I possibili valori per questo byte sono mostrati in tabella 1.

Nome del TAG Valore
CONSTANT_Utf8 1
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_Class 7
CONSTANT_String 8
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_NameAndType 12
 

Non descriviamo il significato di ogni valore in quanto questo comporterebbe un approfondimento di molti altri aspetti della macchina virtuale che non verranno affrontati in questa sede. Diamo soltanto per ogni costante una descrizione sommaria:

CONSTANT_Utf8 è usata per rappresentare stringhe costanti. I caratteri vengono codificati seguendo il formato standard UTF-8 e quindi si possono rappresentare caratteri fino a 16 bit

CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long, CONSTANT_Double vengono usati per rappresentare i rispettivi valori numerici costanti. I valori float e double sono rappresentati usando la specifica IEEE 754, ed esattamente il float utilizza la singola precisione mentre il double usa la doppia precisione

CONSTANT_Class descrive una classe o una interfaccia. Per indicare il nome di una classe viene usato il nome completo, incluso il package, in cui si sostituisce il carattere ‘.’ con ‘/’. Ad esempio la classe System del package lang viene indicata con il nome java/lang/System

CONSTANT_String rappresenta oggetti costanti della classe java.io.String

CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_InterfaceMethodref descrivono rispettivamente dei riferimenti a campi e metodi di una classe, e metodi di una interfaccia

CONSTANT_NameAndType viene usata per descrivere i campi e i metodi di una classe

Nella parte restante del file CLASS si fa riferimento a questi elementi specificandone l’indice.

 

I campi Access_flag, this_class e super_class

Il campo access_flag fornisce le informazioni sui modificatori usati nella definizione della classe ed i valori possibili sono elencati nella tabella 2. Se ad esempio la classe compilata fosse stata definita con i modificatori public e static allora questo campo avrebbe il valore 0x0011, ovvero il risultato dell’operazione di OR aritmetico tra ACC_PUBLIC e ACC_FINAL.

Nome del flag Valore Significato
ACC_PUBLIC 0x0001 La classe è public
ACC_FINAL 0x0010 La classe è final
ACC_SUPER 0x0020 Flag speciale. Deve essere attivo
ACC_INTERFACE 0x0200 La classe definisce una interfaccia
ACC_ABSTRACT 0x0400 La classe è abstract
 

Nella documentazione sui file CLASS generati dal compilatore del JDK 1.1 viene descritto il flag speciale ACCESS_SUPER, ed è indicato che esso deve essere sempre impostato. Lo scopo di questo flag è quello di assicurare la compatibilità, sulla macchina virtuale del JDK 1.1, di codice generato da compilatori precedenti a quello incluso nel JDK 1.1. Tutto ciò è stato fatto perché la nuova macchina virtuale è in grado di effettuare chiamate a metodi di classi padre anche se il metodo interessato è stato ridefinito nella classe figlia.

Più avanti vedremo che anche per ognuno dei metodi e dei campi della classe compilata esiste un’attributo che specifica i modificatori usati nella dichiarazione.

I due campi this_class e super_class sono indici nella constant pool ad elementi di tipo CONSTANT_Class che specificano il nome della classe stessa e di quella del padre. Solo nel caso della classe java.lang.Object il campo super_class assume il valore zero.

 

Le interfacce

I due campi interfaces_count e interfaces memorizzano le informazioni sulle interfacce che la classe compilata implementa. Il primo di essi fornisce il numero di queste interfacce mentre il secondo campo è un vettore i cui elementi sono interfaces[i] con 0 £ i < interfaces_count. Il vettore contiene degli indici ad elementi di tipo CONSTANT_Class della constant pool che descrivono le interfacce vere e proprie.

 

I campi attributo

Prima di parlare di come i metodi e le variabili della classe vengono codificati nel file CLASS introduciamo il concetto di attributo. Gli ultimi due campi della struttura ClassFile specificano gli attributi del file compilato. Il primo di questi indica il numero di attributi presenti, mentre il secondo rappresenta gli attributi. Altri campi di questo tipo sono usati con significati diversi anche nelle sottostrutture field_info e method_info. La struttura generica di un campo attributo è mostrata nel riquadro 2.

attribute_info {

        u2 attribute_name_index;

        u4 attribute_length;

        u1 info[attribute_length];

}
 

Ogni attributo deve avere un nome dato dal campo attribute_name_index che rappresenta un indice nella constant pool a un elemento di tipo CONSTANT_Utf8, che come abbiamo visto rappresenta una stringa costante. Il campo attribute_length fornisce la lunghezza dell’attributo esclusi i primi 6 byte, mentre l’ultimo campo descrive il contenuto vero e proprio dell’attributo. Vi sono degli attributi predefiniti che possono far parte di un file CLASS e i loro nomi sono:

SourceFile

ConstantValue

Code

Exception

LineNumberTable

LocalVariableTable

La macchina virtuale riconosce questi attributi ma ignora tutti quelli di cui non conosce la struttura. Di conseguenza un compilatore può definirne di nuovi senza compromettere la portabilità del file binario.

Gli ultimi due attributi elencati in precedenza sono opzionali e vengono usati in fase di debug per determinare informazioni supplementari sul sorgente Java compilato.

L’unico attributo predefinito riferito alla struttura ClassFile è SourceFile che fornisce informazioni sul file sorgente compilato. Esso contiene il nome del file sorgente che è stato compilato. Nel nome del file non è compreso il percorso, quindi ad esempio esso conterrà "foo.java" e non "/home/perrucci/foo.java".

I descrittori

Prima di analizzare il contenuto dei campi fields e methods bisogna introdurre il concetto di descrittore. All’interno del file CLASS vengono usate delle stringhe per la decrizione del prototipo dei metodi e del tipo dei campi. Questi descrittori sono definiti da una grammatica molto semplice la quale determina una corrispondenza biunivoca tra informazione descritta e stringa generata. Questa grammatica è un insieme di regole che descrivono il modo per formare delle sequenze di caratteri che descrivono descrittori sintatticamente validi. Nel riquadro 3 è riportata la grammatica per la creazione dei decrittori. I simboli terminali della grammatica sono indicati in grassetto mentre per quelli non terminali non sono usati caratteri particolari. Per quanto riguarda il descrittore di una variabile bisogna osservare il non terminale BaseType che associa ai tipi base del linguaggio la codifica che poi apparirà nel descrittore. Questa codifica è:

B per byte

C per char

D per double

F per float

I per int

J per long

S per short

Z per boolean

 
FieldDescriptor : FieldType

FieldType : BaseType | ObjectType | ArrayType

BaseType : B | C | D | F | I | J | S | Z 

ObjectType : L <classname> ;

ArrayType : [ FieldType
MethodDescriptor : ( ParameterDescriptors ) ReturnDescriptor

ParameterDescriptor :   FieldType

ParameterDescriptors :  ParameterDescriptors FieldType 

ReturnDescriptor :      FieldType | V
Quindi, ad esempio, il descrittore di una variabile intera sarà semplicemente "I" mentre quella di una variabile di tipo String sarà "Ljava/lang/String;".

Per quanto riguarda il descrittore di un metodo osserviamo che il non terminale ReturnDescriptor può restituire il carattere "V" se il metodo di cui si sta costruendo il descrittore ritorna void.

Ad esempio per il metodo:

void MioMetodo(int i,double d) {…}

verrà creato il descrittore "(ID)V", mentre il descrittore associato al metodo:

Object MioMetodo(int i[], double d, String s) {…}

sarà "([IDLjava/lang/String;)Ljava/lang/Object;".

 

Il campo Fields

Questo campo descrive le variabili della classe compilata. Esso è un vettore di fields_count elementi, ognuno dei quali rappresenta una variabile e fornisce le seguenti informazioni:

Un valore a 16 bit che identifica i modificatori usati nella dichiarazione della variabile. I possibili valori per questo campo sono indicati nella tabella 3

Il nome della variabile

Il descrittore della variabile

 
Nome del flag Valore Significato
ACC_PUBLIC 0x0001 La variabile è public
ACC_PRIVATE 0x0002 La variabile è private
ACC_PROTECTED 0x0004 La variabile è protected
ACC_STATIC 0x0008 La variabile è static
ACC_FINAL 0x0010 La variabile è final
ACC_VOLATILE 0x0040 La variabile è valatilel
ACC_TRANSIENT 0x0080 La variabile è transient
 Oltre a questi tre campi vi possono essere degli attributi associati ad una variabile. Attualmente l’unico attributo definito è ConstantValue ed è usato per indicare il valore costante di una variabile statica.

 

Il campo Methods

L’ultimo campo che rimane da descrivere riguarda i metodi della classe compilata. Come nel caso delle variabili, anche per i metodi viene usato un vettore. Questo vettore ha methods_count elementi ed ognuno di essi descrive un metodo dando il valore dei modificatori, il nome e il descrittore del metodo. I valori possibili per i modificatori sono mostrati in tabella 4.

Nome del flag Valore Significato
ACC_PUBLIC 0x0001 Il metodo è public
ACC_PRIVATE 0x0002 Il metodo è private
ACC_PROTECTED 0x0004 Il metodo è protected
ACC_STATIC 0x0008 Il metodo è static
ACC_FINAL 0x0010 Il metodo è final
ACC_SYNCHRONIZED 0x0020 Il metodo è synchronized
ACC_NATIVE 0x0100 Il metodo è native
ACC_ABSTRACT 0x0400 Il metodo è abstract
 

Dopo queste informazioni seguono gli attributi, e gli unici definiti per i metodi sono:

Code che descrive il codice e le informazioni per l’esecuzione del metodo

Exceptions che fornisce le informazioni sulle eccezioni che il metodo può sollevare

Analizziamo prima di tutto Code, la cui struttura è indicata nel riquadro 4. Il campo attribute_name_index è un indice nella constant pool alla stringa "Code" mentre attribute_length è la lunghezza totale dell’attributo esclusi i primi sei byte. Il campo max_stack è il numero massimo di posizioni sullo stack della macchina virtuale di cui l’esecuzione del metodo ha bisogno. Segue poi il numero massimo di variabili locali di cui il metodo necessita, rappresentato da max_locals. Questo numero include i parametri passati al metodo e l’indice della prima variabile locale è 0. Il significato di questi due ultimi campi sarà più chiaro quando analizzeremo la struttura della macchina virtuale. Nell’attributo Code viene anche salvato il codice vero e proprio associato al metodo. Infatti il campo code è un vettore di byte di lunghezza code_length che memorizza la sequenza di byte che specificano il codice.

Code_attribute {

        u2 attribute_name_index;

        u4 attribute_length;

        u2 max_stack;

        u2 max_locals;

        u4 code_length;

        u1 code[code_length];

        u2 exception_table_length;

        { 

          u2 start_pc;

          u2 end_pc;

          u2 handler_pc;

          u2 catch_type;

        } exception_table[exception_table_length];

        u2 attributes_count;

        attribute_info attributes[attributes_count];

}
 

 Il campo exception_table fornisce le informazioni sui blocchi try {…} catch {…} finally {…} presenti nel corpo del metodo.

Esattamente questa sottostruttura memorizza per ogni istruzione try:

L’indirizzo inziale (start_pc) e finale (end_pc), all’interno del vettore code, del blocco di codice in cui è attivo il gestore dell’eccezione

L’indirizzo iniziale (handler_pc) della porzione di codice che implementa il gestore dell’eccezione

Il valore del campo catch_type che, se diverso da zero, deve essere un indice nella constant pool ad un elemento con tag CONSTANT_Class. La classe decritta da questo elemento rappresenta la classe dell’eccezione gestita dal gestore precedentemente descritto. La macchina virtuale invocherà il gestore solo se l’eccezione sollevata sarà un’istanza della classe indicata. Se il valore di catch_type è zero allora il gestore verrà chiamato per ogni eccezione sollevata. Questo viene usato per l’implementazione del costrutto finally del linguaggio Java.

L’attributo Code può avere, a sua volta, altri attributi associati. Fino ad ora quelli definiti sono LineNumberTable e LocalVariableTable, che come abbiamo detto sono entrambi opzionali e sono usati in fase di debug.

Il secondo attributo associato agli elementi del vettore methods è Exceptions, e rappresenta semplicemente un vettore di indici a elementi della constant pool di tipo CONSTANT_Class. Questi elementi descrivono le classi delle eccezioni che il metodo in questione può sollevare.

 

I metodi <init> e <clinit>

Il nome del costruttore della classe compilata non viene memorizzato nel file CLASS. Esso viene sostituito dalla stringa "<init>" che non è un nome valido per il linguaggio e quindi non ci sono problemi di sovrapposizione con i nomi dei metodi che possono essere usati. Quando si compila un’istruzione new il compilatore genera anche il codice per la chiamata del metodo "<init>" interessato.

C’è inoltre da osservare che il codice generato per le inizializzazioni dei campi non statici della classe è contenuto nei costruttori. Questo vuol dire che se si compila la seguente classe:

public class X

{

int a=1;

}

il risultato sarà che nel codice del costruttore di default, che viene inserito dal compilatore, ci sarà anche il codice per assegnare il valore 1 alla variabile intera a.

La situazione è un po’ diversa nel caso dei campi statici della classe. Il codice generato per l’inizializzazione di questi campi non può essere incluso nel costruttore altrimenti esso non sarebbe eseguito se non come effetto di una istruzione new. Questo è chiaramente inaccettabile in quanto i campi statici possono essere acceduti anche senza usare un’istanza della classe, ma utilizzando direttamente il nome della classe. Per ovviare a questo problema il compilatore include tutto il codice per l’inizializzazione dei campi statici della classe in un metodo di nome <clinit>. La macchina virtuale poi chiamerà questo metodo nei casi opportuni.

 

Struttura interna della macchina virtuale

La macchina virtuale consiste di cinque componenti distinte che sono:

Un insieme di istruzioni

Un insieme di registri

Uno stack

Un heap

Un’area per i metodi

Analizziamo ora una ad una queste parti.

L’insieme delle istruzioni è il linguaggio assembly con cui i programmi Java vengono compilati. Un’istruzione della macchina virtuale è formata da un codice operativo e da una serie di operandi che specificano i parametri su cui l’istruzione deve agire. Molte istruzioni non hanno operandi e consistono del solo codice operativo. Quando gli operandi sono più di uno essi vengono memorizzati secondo l’ordine big-endian di cui abbiamo parlato in precedenza. Sia il codice operativo che gli operandi hanno un’ampiezza di un byte. Più avanti approfondiremo meglio le istruzioni più importanti.

I registri della macchina virtuale sono analoghi ai registri di un qualunque microprocessore. Essi hanno un’ampiezza di 32 bit e comprendono:

Un program counter

Un puntatore alla cima dello stack degli operandi

Un puntatore all’ambiente di esecuzione del metodo corrente

Un puntatore alla prima variabile locale del metodo corrente

Proprio come un normale processore la macchina virtuale possiede un registro program counter che indica in ogni istante la prossima istruzione da eseguire. I puntatori allo stack e all’ambiente di esecuzione vengono usati per ottenere dei riferimenti alle aree che contengono dati, che più avanti descriveremo, che riguardano il metodo che la macchina virtuale sta eseguendo. Le variabili locali di un metodo vengono usati come zone di memoria temporanee da usare durante l’esecuzione del metodo.

La macchina virtuale è basata sull’uso di uno stack. Esso è usato per fornire gli operandi alle istruzioni, per ricevere i valori ritornati dai metodi, per passare i parametri ai metodi, etc. Ogni elemento dello stack memorizza lo stato associato alla chiamata di un metodo ed è composto di tre parti:

Le variabili locali

L’ambiente di esecuzione

Lo stack degli operandi

Le variabili locali sono riferite mediante uno spiazzamento, che consiste praticamente di un indice intero, da aggiungere al puntarore alla prima delle variabili locali. I tipi gestiti dalla macchina virtuale, e quindi dalle sue istruzioni, sono quelli base del linguaggio più un tipo oggetto generico in grado di memorizzare un riferimento a qualunque oggetto. Per memorizzare un valore long o double vengono usate due variabili locali mentre negli altri casi se ne utilizza soltanto una. La macchina virtuale mette a disposizione istruzioni per poter caricare il valore contenuto in una variabile locale sullo stesso e viceversa.

L’ambiente di esecuzione è usato per gestire le azioni sullo stack della macchina virtuale. Tra le altre cose esso contiene il puntatore all’elemento precedente dello stack, il puntatore alle variabili locali e quello alla base e alla cima dello stack.

La terza componente dello stack della macchina virtuale è essa stessa uno stack ed è usata per memorizzare gli operatori e i risultati delle istruzioni. Gli elementi di questo stack hanno un’ampiezza di 32 bit e come nel caso delle variabili locali i tipi long e double occupano due elementi al suo interno.

L’heap della macchina virtuale è l’area dove durante l’esecuzione vengono allocati gli oggetti. Dato che il linguaggio Java non da la possibilità all’utente di liberare memoria, viene usata la tecnica del garbage collection per rilasciare la memoria non più utilizzabile.

L’ultima componente della macchina virtuale è l’area dei metodi che è equivalente al segmento text di un programma eseguibile UNIX. Esso contiene il codice del metodo ed altre informazioni.

 

Verifica dei file CLASS

Prima dell’esecuzione di un file binario la macchina virtuale esegue una fase di verifica e controllo del file stesso. Questo viene fatto per evitare che un file CLASS corrotto venga eseguito mettendo così a rischio l’integrità del sistema.

 

Paolo Perrucci è iscritto al terzo anno del corso di laurea in Informatica dell’Università di Roma "La Sapienza".
 
 

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it