Il linguaggio Java deve il suo rapido sviluppo sul mercato al fatto che ben si adatta alla programmazione di applicazioni multipiattaforma destinate allutilizzo 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 dellesecuzione 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 larchitettura 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 nellarticolo sono gli stessi adottati della documentazione ufficiale della Sun in modo da rendere più facile, per chi volesse, un approfondimento dellargomento.
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 majorversion era 45 e quello di minorversion era 3. Solo la Sun può definire nuovi valori per questi due campi.
Inizieremo lanalisi dai due campi constantpoolcount 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 allinterno delle strutture e sottostrutture del file CLASS.
Il campo constantpoolcount 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 nellelemento stesso. I possibili valori per questo byte sono mostrati in tabella 1.
Nome del TAG Valore CONSTANTUtf8 1 CONSTANTInteger 3 CONSTANTFloat 4 CONSTANTLong 5 CONSTANTDouble 6 CONSTANTClass 7 CONSTANTString 8 CONSTANTFieldref 9 CONSTANTMethodref 10 CONSTANTInterfaceMethodref 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:
> CONSTANTUtf8 è 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 > > CONSTANTInteger, CONSTANTFloat, CONSTANTLong, CONSTANTDouble 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 > > CONSTANTClass 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 > > CONSTANTString rappresenta oggetti costanti della classe java.io.String > > CONSTANTFieldref, CONSTANTMethodref, CONSTANTInterfaceMethodref 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 lindice.
I campi Accessflag, thisclass e super_class
Il campo accessflag 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 delloperazione di OR aritmetico tra ACCPUBLIC e ACC_FINAL.
Nome del flag Valore Significato ACCPUBLIC 0x0001 La classe è public ACCFINAL 0x0010 La classe è final ACCSUPER 0x0020 Flag speciale. Deve essere attivo ACCINTERFACE 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 unattributo che specifica i modificatori usati nella dichiarazione.
I due campi thisclass e superclass sono indici nella constant pool ad elementi di tipo CONSTANTClass che specificano il nome della classe stessa e di quella del padre. Solo nel caso della classe java.lang.Object il campo superclass assume il valore zero.
Le interfacce
I due campi interfacescount 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 \< interfacescount. 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 fieldinfo e methodinfo. 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 attributenameindex che rappresenta un indice nella constant pool a un elemento di tipo CONSTANTUtf8, che come abbiamo visto rappresenta una stringa costante. Il campo attributelength fornisce la lunghezza dellattributo esclusi i primi 6 byte, mentre lultimo campo descrive il contenuto vero e proprio dellattributo. 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.
Lunico 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. Allinterno 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 ACCPUBLIC 0x0001 La variabile è public ACCPRIVATE 0x0002 La variabile è private ACCPROTECTED 0x0004 La variabile è protected ACCSTATIC 0x0008 La variabile è static ACCFINAL 0x0010 La variabile è final ACCVOLATILE 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 lunico attributo definito è ConstantValue ed è usato per indicare il valore costante di una variabile statica.
Il campo Methods
Lultimo 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 ACCPUBLIC 0x0001 Il metodo è public ACCPRIVATE 0x0002 Il metodo è private ACCPROTECTED 0x0004 Il metodo è protected ACCSTATIC 0x0008 Il metodo è static ACCFINAL 0x0010 Il metodo è final ACCSYNCHRONIZED 0x0020 Il metodo è synchronized ACCNATIVE 0x0100 Il metodo è native ACCABSTRACT 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 lesecuzione 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 attributenameindex è un indice nella constant pool alla stringa "Code" mentre attributelength è la lunghezza totale dellattributo esclusi i primi sei byte. Il campo maxstack è il numero massimo di posizioni sullo stack della macchina virtuale di cui lesecuzione del metodo ha bisogno. Segue poi il numero massimo di variabili locali di cui il metodo necessita, rappresentato da maxlocals. Questo numero include i parametri passati al metodo e lindice della prima variabile locale è 0. Il significato di questi due ultimi campi sarà più chiaro quando analizzeremo la struttura della macchina virtuale. Nellattributo Code viene anche salvato il codice vero e proprio associato al metodo. Infatti il campo code è un vettore di byte di lunghezza codelength 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:
> Lindirizzo inziale (startpc) e finale (endpc), allinterno del vettore code, del blocco di codice in cui è attivo il gestore delleccezione > > Lindirizzo iniziale (handlerpc) della porzione di codice che implementa il gestore delleccezione > > Il valore del campo catchtype che, se diverso da zero, deve essere un indice nella constant pool ad un elemento con tag CONSTANTClass. La classe decritta da questo elemento rappresenta la classe delleccezione gestita dal gestore precedentemente descritto. La macchina virtuale invocherà il gestore solo se leccezione sollevata sarà unistanza della classe indicata. Se il valore di catchtype è zero allora il gestore verrà chiamato per ogni eccezione sollevata. Questo viene usato per limplementazione del costrutto finally del linguaggio Java.
Lattributo 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 unistruzione 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 linizializzazione 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 unistanza della classe, ma utilizzando direttamente il nome della classe. Per ovviare a questo problema il compilatore include tutto il codice per linizializzazione 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 > > Unarea per i metodi
Analizziamo ora una ad una queste parti.
Linsieme delle istruzioni è il linguaggio assembly con cui i programmi Java vengono compilati. Unistruzione della macchina virtuale è formata da un codice operativo e da una serie di operandi che specificano i parametri su cui listruzione deve agire. Molte istruzioni non hanno operandi e consistono del solo codice operativo. Quando gli operandi sono più di uno essi vengono memorizzati secondo lordine big-endian di cui abbiamo parlato in precedenza. Sia il codice operativo che gli operandi hanno unampiezza 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 unampiezza di 32 bit e comprendono:
> Un program counter > > Un puntatore alla cima dello stack degli operandi > > Un puntatore allambiente 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 allambiente 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 lesecuzione del metodo.
La macchina virtuale è basata sulluso 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 > > Lambiente 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.
Lambiente di esecuzione è usato per gestire le azioni sullo stack della macchina virtuale. Tra le altre cose esso contiene il puntatore allelemento 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 unampiezza di 32 bit e come nel caso delle variabili locali i tipi long e double occupano due elementi al suo interno.
Lheap della macchina virtuale è larea dove durante lesecuzione vengono allocati gli oggetti. Dato che il linguaggio Java non da la possibilità allutente di liberare memoria, viene usata la tecnica del garbage collection per rilasciare la memoria non più utilizzabile.
Lultima componente della macchina virtuale è larea 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 dellesecuzione 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 lintegrità del sistema.
Paolo Perrucci è iscritto al terzo anno del corso di laurea in Informatica dellUniversità di Roma "La Sapienza".
