MokaByteNumero 16 - Febbraio 1998
 
Dentro la 
Java Virtual Machine
di
Alberto Bellina
Come creare un tool completamente personalizzato per esplorare e decompilare un file .class. Come usare la reflection API di Sun per ottenere lo stesso scopo


 


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.

Il formato dei file .class

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:

0x001 public
0x010 final
0x200 interface
0x400 abstract
I campi this_class e super_class sono indici relativi alla tabella constant_pool e puntano al nome della classe corrente (a ogni file .class corrisponde una sola classe) e al nome della classe super da cui deriva.

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.

La struttura constant_pool

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:
 

Il campo tag varrà CONSTANT_Class, mentre name_index è l'indice alla struttura constant_pool ove viene indicato il nome della classe. Se le costanti fossero di tipo CONSTANT_Fieldref oppure CONSTANT_Methodref o CONSTANT_InterfaceMethodref le relative strutture utilizzate sarebbero del tipo: Il campo name_index di CONSTANT_Class_info corrisponde anch'esso a un indice di constant_pool dove è contenuto il nome della classe. Il nome della classe è quello composto dal nome stesso più la signature.

Le costanti stringa sono rappresentate da

Il campo string_index è un indice ad un elemento di tipo CONSTANT_Utf8.
Interi e float, invece, hanno una struttura del genere:
  dove il campo bytes sarà ovviamente letto in due modi diversi secondo il tag, nel primo caso sarà in formato intero (il primo byte è quello più significativo), nel secondo nel formato IEEE 754 per la rappresentazione dei valori in virgola mobile singola precisione.

I long e i double occupano 8 byte:
 

La rappresentazione del valore per CONSTANT_Long è: (high_bytes << 32) + low_bytes.

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.
 

Il campo constant_pool[name_index] è una stringa CONSTANT_Utf8 che contiene il nome del field o del metodo, mentre constant_pool[signature_index] è una stringa CONSTANT_Utf8 che contiene la signature del field o del metodo.

Le costanti stringa

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".

La struttura fields

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.

La struttura methods

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 access_flag determina il tipo di accesso assegnato al metodo( public, private, protected, static, final, synchronized, native, abstract). Come abbiamo già visto per i field i due campi name_index e signature_index, puntano alle relative stringhe descrittive nella tabella delle costanti.

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
 

Il campo attribute_name è un indice al constant_pool che contiene una stringa di descrizione dell'attributo. La JVM associa una struttura particolare ad ogni tipo di attribute_name. Di solito il nome è dato dall'attributo seguito dalla parola attribute.

Per esempio: SourceFile_attribute per SourceFile, Code_attribute per Code, eccetera.

Se attribute_name vale "SourceFile" la struttura da leggere è:
 

In questo caso si può dedurre che il campo sourcefile_index sarà il solito indice alla constant_pool che contiene il nome del file sorgente. Se invece attribute_name vale "Code" si usa la struttura:
  Come si può notare generalmente i due primi campi puntano al nome e la sua lunghezza, mentre i campi che seguono variano e sono relativi al tipo di attributo.

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 le informazioni che abbiamo visto finora relativamente alla struttura del file .class, possiamo creare un programma che le decodifica e ne stampa alcune parti in un file.

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
 
 

Classi per leggere il .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:

jcdConstPool: per la lettura del pool delle costanti;
jcdField: per la lettura dei field;
jcdMethod: per la lettura dei metodi;
jcdAttr: per la lettura degli attributi;
jcdDefines: per gestione delle costanti utilizzate.
Ogni classe dispone del metodo read() con cui effettua le operazioni di lettura dal file .class per la parte che le compete.

Le varie classi

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:

che formano una definizione ricorsiva. In sostanza, si legge il byte tag e a seconda del suo valore si leggono i campi successivi nel modo più appropriato usando arg1 e arg2. Per esempio, si consideri:

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();
 

Ovviamente servirà una classe Code per le necessarie operazioni.

Java Reflection API

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:

getConstructors, getConstructor

getMethods, getMethod

getFields, getField

getSuperclass

getInterfaces

Le classi sono parte del package java.lang.reflect. Le classi che compongono le API di Reflection consentono di fare molto di più, esse infatti "conoscono" la rappresentazione delle varie parti di un file .class e forniscono dei metodi sicuri per estrarre tali informazioni.

Reflection e JavaBeans

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).

Conclusioni

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 Web  1998 - www.mokabyte.it

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