MokaByteNumero
16 - Febbraio 1998
|
|||
|
Java Virtual Machine |
||
Alberto Bellina |
|
||
Di Java mi ha sempre incuriosito il meccanismo che permette la portabilità del codice virtualmente su tutte le piattaforme hardware/software esistenti. Questa è la ragione fondamentale che mi ha spinto a indagare sui file .class prodotti dai compilatori Java.
Scopo ultimo è la costruzione di un piccolo tool (chiamato JCD, Java Class Decoder) che sia in grado di decompilare i file .class estraendone i dati.
Tra gli argomenti trattati nel seguito vi è anche la Java Reflection API, ovvero uno strumento per interrogare a runtime una classe e conoscerne così le caratteristiche.
Cominciamo con uno sguardo accurato al formato dei file.
Nei file .class sono memorizzate tutte le informazioni necessarie alla Virtual Machine di Java per far girare una determinata classe. La documentazione ufficiale a cui far riferimento è disponibile su Internet presso http://java.sun.com/docs/books/vmspec/.
I campi contraddistinti dai tipi u1, u2, e u4 sono formati da interi senza segno di 1, 2 e 4 byte rispettivamente.
Il primo campo è un intero che deve sempre contenere la cifra 0xCAFEBABE. Qualsiasi implementazione di Virtual Machine esamina questo valore e prosegue solo se il confronto è positivo. I campi minor_version e major_version contengono la versione del compilatore usato per il file in questione. Di solito il major vale 45 e il minor 3. Essi servono a impedire che una JVM attivi un .class generato per una versione superiore e incompatibile. Il campo successivo constant_pool_count indica il numero di valori contenuti nella tabella successiva denominata constant_pool, che contiene stringhe e dati costanti utilizzati all'interno del codice.
access_flagscontiene un numero che indica il tipo di accesso definito per questa classe. Possibili valori sono:
Se nel sorgente non è stata usata la keyword extends il campo super_class farà riferimento a java.lang.Object. Il campo interfaces_count indica il numero delle interfacce implementate, indicate in interfaces. Quest'ultimo è un array di indici a constant_pool dove sono i nomi delle interfacce.
I campi successivi descrivono rispettivamente le variabili, i metodi e gli attributi della classe.
Nell' array constant_pool
sono contenute tutte le parti costanti presenti nel file .class.
Per esempio il nome della classe, della superclasse e delle classi richiamate,
dei metodi interni e tutte le stringhe utilizzate.
L'unico dato
costante del sorgente originario che manca è il nome delle variabili
interne alle classi e il nome degli argomenti dei metodi.
L'indice 0 dell'array
non è utilizzato. Il primo byte detto tag è un numero
che indica il tipo di costante seguente.
I byte seguenti
formano una struttura diversa a seconda del tag. Nel caso di CONSTANT_Class
ci troviamo di fronte a:
Le costanti stringa sono rappresentate da
I long e i double
occupano 8 byte:
Per CONSTANT_Double i due campi high_bytes e low_bytes sono utilizzati secondo lo standard IEEE 754 per i valori in virgola mobile e doppia precisione.
Si noti che questi due tipi sono gli unici che occupano due posizioni della tabella constant_pool.
Se tag
vale CONSTANT_NameAndType rappresenta un field o un metodo senza
indicazione della classe a cui appartiene.
Per i tag che valgono CONSTANT_Utf8 and CONSTANT_Unicode ci troviamo di fronte alla rappresentazione di costanti stringhe.
Le stringhe CONSTANT_Utf8 sono "encoded" per contenere solamente i valori Ascii diversi da null.
La rappresentazione utilizzata consente di rappresentare una stringa un byte per carattere, ma si può anche rappresentare un carattere con due byte (per l'internazionalizzazione dei caratteri).
Di seguito le due strutture utlizzate:
CONSTANT_Utf8_info
{
u1 tag;
u2 length;
u1 bytes[length];
}
CONSTANT_Unicode_info
{
u1 tag;
u2 length;
u2 bytes[length];
}
Ricordo ancora che length indica il numero di byte nella stringa, memorizzata nel campo bytes, e che la stringa non è "null-terminated".
Il campo fields della struttura ClassFile è un array di strutture field_info di dimensione indicata nel campo fields_count, che descrivono le variabili presenti nella classe. Ogni variabile viene descritta da:
field_info {
u2 name_index;
u2 signature_index;
u2 attribute_count;
attribute_info attributes[attribute_count];
}
Il campo access_flag è un intero che determina il tipo di accesso assegnato alla variabile. Assume i valori: public o private o protected (uno solo tra questi), static, final, volatile, transient.
Il campo name_index e signature_index contengono gli indici alla tabella constant_pool dove sono contenute le stringhe con i nomi e la signature delle variabili. Il campo attribute_count contiene il numero di attributi opzionali associati con la variabile.
Allo stato attuale l'unico attributo utilizzato è ConstantValue il quale indica che questo campo è una costante numerica.
methods_count
indica quanti metodi sono utilizzati nella classe. Essi sono descritti
nel successivo campo methods, che è un array di strutture
method_info:
Il campo attribute_count contiene il numero di attributi opzionali associati con il metodo.
Allo stato attuale
gli attributi utilizzati sono Code_attribute e Exception_attribute
che sono rispettivamente il codice che viene eseguito per il metodo e le
exception che sono dichiarate come risultato dell'esecuzione del metodo.
La struttura
attributes
Gli attributi
possono essere presenti in vari punti del file .class. Infatti li
abbiamo già visti nella descrizione delle classe e dei metodi. La
descrizione degli attributi è formata dai campi attributes_count
e attributes, che indicano rispettivamente il numero di attributi
e l'array di strutture che li descrivono.
Un attributo
è descritto da
Per esempio: SourceFile_attribute per SourceFile, Code_attribute per Code, eccetera.
Se attribute_name
vale "SourceFile" la struttura da leggere è:
Si può
notare come i campi presenti identificano perfettamente dati necessari
alla Virtual Machine per far "girare" la classe. In particolare il campo
code
(di lunghezza indicata da code_length) contiene il codice relativo
alla classe.
Con il programma jcd è possibile decodificare tutti i costruttori ed i metodi con le relative tipologie di argomenti da un file .class, anche se non ne possediamo alcuna documentazione.
Per esempio trovate nel vostro archivio personale il file spread.class che probabilmente implementa proprio lo spreadsheet di cui avete bisogno, ma non ne avete la documentazione. Con jcd potete estrarre la definizione di tutti i costruttori ed i metodi che dovrete richiamare per utilizzare la classe.
Ecco un esempio di output prodotto da jcd
// thisclass:
java/lang/Object
// superclass:
public class java/lang/Object extends {
// methods found: 6
public native int hashCode( )
protected native java.lang.Object clone( )
public java.lang.String toString( )
public final void wait( )
protected void finalize( )
public void <init>( )
} // end of
class
Un Java Decoder deve:
1. Aprire in lettura il file .class
2. Leggere e decodificare le informazioni
3. Scrivere l'output su un file .dj
L'apertura del file da leggere e la creazione di quello di output avviene attraverso i metodi openInput() e openOutput().
Il file .class viene letto mediante i metodi della classe DataInputStream, che consente di leggere agevolemente variabili intere o buffer di byte. Il file .dj viene scritto mediante i metodi della classe PrintWriter che fornisce l'unico metodo che utilizzeremo, cioè println().
La lettura reale del file classe viene effettuata da read() della classe jcdClass.
La classe jcdClass legge il file .class richiamando altre classi per la lettura di parti specifiche:
La classe jcdClass contiene due soli metodi read() e write(). Il primo legge tutti i campi interi della struttura ClassFile, utilizzando le altre classi per la lettura dei rimanenti tipi. È qui che interviene il test sul valore del campo magic che, come detto, deve essere 0xCAFEBABE.
Per la lettura degli elementi della tabella constant_pool viene richiamato il metodo read() della classe jcdConstPool. Essa include tra le sue variabili anche:
public class
prova {
public boolean prova( int i ){
return( true );
}
}
Vediamo adesso
la struttura methods_info nella quale il primo metodo punta all'indice
15 che è una stringa contenente "prova" (il nome) e per la signature
fa riferimento a 6 che descrive un metodo con argomento intero che ritorna
un boolean. Questo tipo di ragionamento è necessario per tutte le
strutture che hanno puntatori alla tabella delle costanti. Si noti, infine,
la presenza di un metodo nascosto <init> in ogni classe.
JcdFields legge
le variabili locali della classe, ma esterne ai metodi. JcdMethods, invece,
riempie la struttura method_info. Il suo metodo write() scrive
i prototipi trovati nel file .dj. Due metodi interni, GetRet()
e GetArgs(), sono usati per ottenere le informazioni sul tipo restituito
e sugli argomenti.
La classe jcdAttr,
infine, viene richiamata per leggere la struttura attribute_info
mediante l'unico metodo read().
Per la lettura
sarà utilizzata la struttura GenericAttribute_info che descrive
in modo generale tutti i tipi di attributi e permette di effettuarne la
lettura senza decodificare il contenuto.
Nell'esempio
gli attributi non sono decodificati, ma si tratta di una caratteristica
che può essere facilmente aggiunta, magari esplorando la struttura
Code_attribute.
Come suggerimento, si può pensare di modificare il metodo read(...)
nella classe jcdAttr:
name = pool[di.readShort()];
len = di.readInt();
Nel JDK 1.1 è
stata aggiunta una nuova parte definita reflection con la quale
è possibile interrogare a run-time una classe per conoscerne le
caratteristiche. Maggiori informazioni sull'URL le trovate al sito:
http://java.sun.com/products/jdk/1.1/docs/guide/reflection/index.html
Utilizzando
gli altri metodi presenti in java.lang.Class è possibile
estrarre tutte le altre informazioni dalla classe desiderata, alcuni dei
metodi più utili sono:
getMethods, getMethod
getFields, getField
getSuperclass
getInterfaces
Le Java Reflection API sono state create soprattutto per definire un modello per lo sviluppo di componenti e delle regole per gestirli. Questo modello non è altro che la specifica dei JavaBeans.
Le Reflection sono quindi la parte "operativa" dei JavaBeans e sono alla base di un tool come il BeanBox fornito nel BDK (Beans Development Kit) che permette di gestire la personalizzazione di un bean consentendo l'identificazione e la modifica delle proprietà in modo semplice e mediante regole codificate. L'unica parte che le API Reflection non consentono di esaminare, a differenza di jcd, sono le parti contenenti gli attributi (come il codice).
Perché creare un tool come jcd? Soprattutto per la curiosità e per il piacere di capire cosa c'è dentro la Java Virtual Machine. Perché usare i metodi Reflection?
Perché
con essi, a differenza di quello che può fare un tool personale,
avrete sempre la compatibilità garantita da SUN anche per eventuali
modifiche del formato dei .class in versioni successive di JVM.
|
||
MokaByte ricerca
nuovi collaboratori.
|
||
|