Prev   Top   Next 

Instruction Set

L'Instruction Set della Macchina Astratta viene utilizzato dall'interprete per la decodifica di ogni bytecode. Si tratta di una tabella che fornisce, per ogni istruzione, la sequenza di azioni da portare avanti acché lo stato della macchina cambi di modo da rispecchiare il significato dell'istruzione interpretata.

L'insieme delle componenti viste fin qui vengono quindi utilizzate e manipolate secondo quanto indicato in questa tabella; è dall'analisi dell'Instruction Set che si evince il supporto che la macchina fornisce per le caratteristiche di Object-Orientation accennate nell'introduzione.

Il complesso di istruzioni può essere ripartito in varie categorie, a seconda del diverso effetto che esse hanno sullo stato della macchina:

  • PUSH;
  • STORE;
  • INVOKE;
  • RETURN;
  • JUMP;
  • CHECK;
  • NEW;
  • PRIMITIVE OPERATIONS.

Ad alcune di queste categorie si è accennato nel trattare le parti che piú direttamente ne realizzano la semantica. Nel seguito si riassumono, brevemente, le caratteristiche dei vari gruppi di istruzioni.

JUMP
Si tratta delle istruzioni atte ad alterare il normale ordine di esecuzione della sequenza di bytecode. Esse sono necessarie per l'implementazione delle strutture di controllo primitive ( cfr. Interpreter ).
PRIMITIVE OPERATIONS
Sono le comuni operazioni aritmetico-logiche, presenti in qualunque macchina. L'unica caratteristica di rilievo è che gli operandi vengono presi dallo stack degli operandi e non dal flusso di bytecode; ciò perché la macchina in analisi è Stack-Based ( cfr. Interpreter ).
PUSH&STORE
Dal momento che tutte le operazioni hanno luogo sullo stack degli operandi, è indispensabile un gruppo di istruzioni per spostare i dati da/verso tale stack.
NEW
L'istruzione NEW è preposta alla creazione di un nuovo oggetto. La classe di cui si vuole creare un'istanza viene specificata tramite un indice, ottenuto a partire dagli operandi dell'istruzione, all'interno della Run-Time Costant Pool. Tale indice viene passato, insieme al puntatore alla Class Structure per il metodo corrente ( prelevato dall'Environment dello Stack Frame ), al Class Manager, che risolve la relativa entry, ottenendo un puntatore alla Class Structure della classe che si sta istanziando.
Questo puntatore viene poi utilizzato dall'Heap Manager, per determinare quanto spazio bisogna riservare per il nuovo oggetto e quest'ultima componente restituisce all'interprete il puntatore all'area di memoria allocata, che viene finalmente depositato sullo stack degli operandi.
RETURN
Si tratta dell'istruzione che provoca la rimozione dello Stack Frame corrente e il ripristino dell'interprete al suo stato prima dell'invocazione del metodo che questo bytecode conclude.
All'inizio, il registro FRAME dell'interprete viene settato al valore del campo Sender del Frame corrente, riguadagnando cosí accesso al Frame del metodo chiamante.
Quindi il top dello stack degli operandi viene salvato sulla prima locazione dell'area delle variabili locali, che coincide con il top dello stack del Frame sottostante, e il Frame corrente viene distrutto.
A questo punto, dall'Environment puntato da FRAME si recuperano gli altri registri e si passa al prossimo bytecode.
CHECK
Questa istruzione ha lo scopo di verificare se l'oggetto puntato dal riferimento al top dello stack è o meno un'istanza della classe o interfaccia specificata dagli operandi del bytecode tramite indicizzazione nella Run-Time Costant Pool corrente.
Dopo che il Class Manager è risalito ( in modo analogo a quanto visto per la NEW ) alla Class Structure della classe o interfaccia C in questione, si inizia un processo finalizzato a determinare se l'oggetto sul top dello stack si possa pensare ( magari per SUBSUMPTION ), come oggetto di tipo C.
Inizialmente viene richiamato l'Heap Manager per determinare il tipo dinamico dell'oggetto, ottenendo cosí un'altra Class Structure D.
Se C non è un'interfaccia, si verifica se essa è la superclasse di D: se cosí è, si mette true sullo stack e si termina. Altrimenti il Class Manager provvede ricorsivamente a controllare se qualche antenato di D coincide con C.
Se invece C è un'interfaccia, il Class Manager la cerca nell'array delle interfacce implementate da D. Se questa ricerca fallisce passa alle interfacce implementate dalla superclasse di D, e cosí via risalendo la gerarchia delle classi fino ad Object. Se anche tale verifica termina senza successo, si pone false sullo stack, e la macchina passa alla interpretazione del bytecode successivo.
INVOKE
L'istruzione INVOKE invoca un nuovo metodo. Essa è presente in due varianti: una per invocare i metodi di classe e l'altra per invocare i metodi istanza.
L'invocazione dei metodi di classe non presenta particolare interesse: si tratta sempre di risolvere un link simbolico per mezzo del Class Manager per poi creare un nuovo Frame nello Stack per l'esecuzione dei bytecode associati al dato metodo.
Più interessante è il caso dell'invocazione dei metodi istanza, poiché essa realizza il DISPATCHING DINAMICO. Per maggiore chiarezza, esaminiamo cosa succede con un esempio.

class Y {
      ...
      void n( ) {
           ...	
      }
}

class Y' extends Y {
      ...
      void n( ) {	// OVERRIDE
           ...
      }
}

class X {
      int m( ) {
          Y o;
          ...
          o = new Y';
          ...
          o.n( );
      }
}
Nel codice dell'esempio c'è una classe Y' che ridefinisce uno dei metodi della sua superclasse Y. Successivamente nel codice viene invocato il metodo istanza n( ) su un oggetto di tipo statico Y ma tipo dinamico Y'.
Cosa succederá? Il DISPATCHING DINAMICO vuole che il codice eseguito sia quello del metodo definito in Y': vediamo quali passi deve compiere la macchina per raggiungere tale scopo.
Il bytecode INVOKE si aspetta sullo stack l'oggetto su cui operare e gli argomenti ( nell'esempio nessun argomento ), mentre specifica negli operandi il solito indice a 16 bits che, una volta risolto dal Class Manager, punterà alla entry associata ad n( ) nella Method Table della Class Structure di Y.
A questo punto, però, anzicché creare il Frame per n( ) ed iniziarne l'esecuzione ( come sarebbe successo nel caso di metodi di classe ), si risale, attraverso il riferimento sul top dello stack, alla Class Structure della classe di cui o è istanza tramite l'Heap Manager.
Quindi si cerca un metodo con lo stesso nome e signature di n( ): poiché lo si trova, si passa alla creazione del Frame; se non lo si fosse trovato, si sarebbe risalita la gerarchia delle classi e nella peggiore delle ipotesi si sarebbe tornati al metodo n( ) di Y.
La creazione del Frame è compito dello Stack Manager. Essenzialmente esso salva lo stato dell'interprete nell'Environment del chiamante, legge le dimensioni delle due aree di grandezza variabile ( area delle variabili locali e stack degli operandi ) dalla entry della Method Table che il processo sopra esposto ha reperito e alloca un nuovo Stack Frame. Quindi, dopo aver salvato nel campo Sender il puntatore al Frame del chiamante, aggiorna i registri VARS, OPTOP, FRAME e PC ... e la vita continua.

 Prev   Top   Next