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