• Non ci sono risultati.

1.1 Il bytecode Java 1

N/A
N/A
Protected

Academic year: 2021

Condividi "1.1 Il bytecode Java 1"

Copied!
17
0
0

Testo completo

(1)

C A P I T O L O

1

Java

Java è un linguaggio di programmazione ad alto livello, orientato agli oggetti e molto simile al linguaggio C/C++ nella sintassi. Chiamato inizialmente Oak, Java era stato pensato come linguaggio di programmazione per lo sviluppo di software avanzato per sistemi embedded connessi in rete [Star7]; è stato sviluppato a partire dagli inizi degli anni ’90 da James Gosling, Patrick Naughton, Mike Sheridan e un gruppo di ingegneri come parte del progetto Green [GreenProject], ed è stato ufficialmente presentato il 23 marzo 1995 alla conferenza SunWorld insieme a HotJava, il primo browser in grado di supportare tale tecnologia.

Java è un linguaggio interpretato: i sorgenti (.java) vengono compilati in file binari (.class), chiamati file class, che contengono istruzioni codificate in un formato binario indipendente, il bytecode, ed interpretate dalla Java Virtual Machine (JVM). Java è per questo un linguaggio ‘portabile’, cioè piattaforme diverse richiedono solo diverse implementazioni della JVM, non diverse versioni dei file class.

1.1 Il

bytecode

Il bytecode è la sequenza di byte con cui vengono codificate le istruzioni dei metodi di ogni file class. Potremmo dire, in sostanza, che è il linguaggio macchina della Java Virtual Machine: quando un file class viene caricato, la JVM, dopo i dovuti controlli di correttezza del codice, interpreta il bytecode o esegue una compilazione just-in-time, a seconda della particolare implementazione della JVM, traducendo il bytecode in un linguaggio macchina adeguato alla piattaforma su cui è installata la JVM.

(2)

CAPITOLO 1:JAVA

Compilatore

...

Method public static void main(String[]) >> max_stack=2, max_locals=1 << 0 getstatic #2 <Field System.out:PrintStream> 3 ldc #3 <String "Hello World!">

5 invokevirtual #4 <Method PrintStream.println(String):void> 8 return

...

HelloWorld.class

Java Virtual Machine(s)

class HelloWorldApp{ public static void main( String args[]){ System.out.println("Hello World!"); }

}

HelloWorld.java

fig. 1.1: La piattaforma Java

Uno stream di bytecode è ad esempio: 03 3b 84 00 01. Ovviamente per comodità noi non faremo riferimento allo stream di byte , ma ad istruzioni mnemoniche cioè al bytecode disassemblato. Ad esempio lo stream precedente corrisponde a tre istruzioni:

03

iconst_0

3b

istore_0

84 00 01

iinc 0, 1

(3)

CAPITOLO 1:JAVA

Le principali caratteristiche del bytecode sono:

- ogni istruzione è nel formato <opcode> <operand(s)>: il codice operativo (opcode) ha un codice mnemonico molto simile all’assembler e il numero di operandi varia a seconda del codice operativo;

- il set di istruzioni è compatto: l’insieme di opcodes infatti è sufficientemente limitato da garantirne la codifica su un solo byte. In più, tutte le istruzioni, eccetto due istruzioni di salto condizionato, hanno una grandezza fissa. Queste caratteristiche aiutano a minimizzare la dimensione del file class che deve viaggiare attraverso la rete ed a rendere più facile l’implementazione della JVM; - il codice operativo è tipato1: ogni istruzione opera su un determinato tipo

(integer, float, double,..) specifico. Questa informazione è codificata insieme all’opcode: ad esempio l’istruzione iconst_0 lavora su interi (la ‘i’ sta per ‘integer’ )

- la maggior parte delle istruzioni operano su uno stack: la JVM non ha registri interni in cui poter memorizzare i risultati, quindi gli operandi vengono sempre caricati e salvati sullo stack. Ad esempio l’istruzione iadd effettua il pop di due operandi interi dallo stack ed il push della loro somma. La JVM è quindi una macchina “stack-based”: questa scelta è stata fatta per facilitarne l’implementazione su architetture con pochi registri, come ad esempio le periferiche embedded.

La JVM supporta sette tipi primitivi: byte, short, int, long, float, double, char. Questi tipi primitivi vengono utilizzati come operandi nel bytecode e, se occupano più di un byte, vengono salvati in ordine big-endian: l’istruzione sipush

256, ad esempio, viene codificata come 17 01 00, dove 17 è l’opcode della sipush

(‘si’ sta per short integer) e 01 00 è la codifica big-endian di 256 (hex 0100). Il set di istruzioni può esser pensato diviso in otto gruppi:

- istruzioni di load/store: trasferiscono i valori tra lo stack e le variabili locali della JVM. Ad esempio dconst_0 mette in cima allo stack un double che vale 0,

(4)

CAPITOLO 1:JAVA

mentre fstore_3 effettua il pop del float che si trova in cima allo stack e lo salva nel registro r3.

- istruzioni aritmetiche. Tutti gli opcode sono tipati ed utilizzano lo stack per prelevare gli operandi e salvare i risultati. Ad esempio imul preleva due interi dallo stack e salva in cima allo stack il risultato della loro moltiplicazione.

- istruzioni per la conversione ed il controllo di tipo: f2l ad esempio converte un float in un long.

- istruzioni per la gestione dello stack: sono istruzioni che permettono una “manipolazione diretta” dello stack. Ad esempio dup duplica il valore in cima allo stack.

- istruzioni per la creazione di oggetti: ad esempio new crea una nuova istanza di una classe; newarray crea un nuovo array.

- istruzioni per il controllo del flusso del programma: salti incondizionati (goto,

jsr, ret, ...) e salti condizionati (if, tableswitch, ...).

- istruzioni per l’invocazione di metodi e per il loro return: invokevirtual ad esempio invoca il medoto di un oggetto;

- istruzioni per accedere agli attributi di un oggetto: getfield ad esempio salva sullo stack il valore del campo che gli viene specificato come operando.

Per maggiori dettagli fare riferimento al paragrafo 3.11.1 delle specifiche Java 2 [vmspecv2].

(5)

CAPITOLO 1:JAVA

1.2 La

Java

Virtual

Machine

La JVM è l’interfaccia tra i programmi Java compilati e le varie piattaforme hardware/software. Generalmente implementata in software (per questo è detta “virtuale”), la JVM costituisce l’elemento centrale della tecnologia Java perché rende il bytecode portabile e perché consente di controllare la correttezza e la sicurezza del codice prima della reale esecuzione. In seguito al fenomeno Internet è nata la possibilità, per chiunque, di eseguire codice non necessariamente “certificato”, basti pensare ad esempio alle pagine web, molte delle quali contengono Java applets per arricchirle nell’aspetto e nei contenuti. Bisogna tener presente che il bytecode di una applet potrebbe anche esser stato modificato oppure scritto senza l’utilizzo di un compilatore: gli utenti devono quindi esser protetti da eventuale codice malizioso, corrotto o semplicemente errato che potrebbe rendere vulnerabile ad attacchi di vario genere il loro host.

La necessità di affrontare in modo adeguato il problema della correttezza e della sicurezza del bytecode è diventata evidente da quando la tecnologia Java si sta espandendo anche nel settore delle periferiche embedded: le Java Cards ad esempio verranno utilizzate per contenere informazioni riservate e per questo la modifica, la cancellazione di dati, o un semplice crash di sistema, sono assolutamente da evitare.

Il paradigma utilizzato da Java per quanto riguarda la sicurezza si basa su due meccanismi. Il primo a livello di linguaggio di programmazione: Java offre infatti un garbage collector ed altri strumenti utili per una gestione migliore della memoria rispetto ad altri linguaggi (C/C++ in particolare). Il secondo a livello di esecuzione, con il verificatore, che garantisce la correttezza del codice al run-time, e con il modello sandbox [SandBox], che garantisce la limitatezza dell’interazione delle applets con il sistema. Le applets infatti non utilizzano direttamente le risorse del sistema ma un determinato numero di risorse e di APIs messe a disposizione dall’ambiente Java: questa è l’idea alla base del modello sandbox.

(6)

CAPITOLO 1:JAVA

1.3 Il

sandbox

Il modello sandbox [SandBox] prevede l’utilizzo di un ambiente con un numero limitato di APIs, il sandbox appunto, in cui poter eseguire il codice delle applets scaricate dalla rete, in modo da consentire solo determinate interazioni con il sistema. Quindi se le APIs della JVM, in particolare quelle del sandbox, sono ben programmate, l’applet non riuscirà in alcun modo a corrompere l’integrità del sistema e dei dati.

Il primo modello di sandbox utilizzato da Java nelle JDK 1.0 prevedeva l’utilizzo di due ambienti: uno in cui poter eseguire le applets “locali”, cioè quelle create e salvate direttamente sull’host che le deve eseguire, ed un altro ambiente, con un numero limitato di variabili e di APIs, in cui eseguire il codice delle applets “remote” in modo da consentire solo determinate tipologie di interazioni con le risorse di sistema.

JDK 1.0 Security Model

Sandbox

System resources (files,...) JVM APIs

remote code local code

JVM Environment

fig. 1.3: Modello della sicurezza offerto dalle JDK 1.0

Abbastanza presto però è nata l’esigenza di poter diversificare il comportamento della JVM nei confronti delle applets scaricate dalla rete: a partire dalle JDK 1.1 viene infatti introdotto il concetto di crittografia (JCA, Java Cryptography Architecture) e applet firmata (signed applet); se un’applet, distribuita in un file JAR (Java ARchive) insieme ad una firma, è firmata correttamente da un produttore “trusted”, allora viene eseguita come un’applet

(7)

CAPITOLO 1:JAVA

locale, cioè non vengono fatte limitazioni sulle variabili d’ambiente e sul numero e tipo di APIs che possono essere utilizzare.

Nelle JDK 1.2 questa idea viene trattata in modo più accurato attraverso l’introduzione del controllo di accesso alle APIs e di politiche di sicurezza configurabili con tre nuovi strumenti:

- estensione dell’architettura di sicurezza delle JDK 1.1

- estensione dell’architettura della crittografia introdotta nelle JDK 1.1 - strumenti per il controllo della sicurezza

JDK 1.2 Security Model

JVM Environment

Sandbox

System resources (files,...) JVM APIs local or remote code, signed or not class loader, security policies

fig. 1.4: Modello della sicurezza delle JDK 1.2

Per maggiori dettagli si rimanda alla documentazione fornita dalla Sun [JavaSecurity].

Per evitare un eventuale by-pass delle APIs della JVM (le applets non devono mai poter accedere direttamente alle risorse di sistema) viene effettuata una analisi statica, chiamata data flow analysis, del codice delle applets prima della loro reale esecuzione. La data flow analysis viene fatta attraverso un interprete astratto (il verificatore) che controlla la “correttezza” (stack overflow/underflow, cast illegali,…) del codice da eseguire.

(8)

CAPITOLO 1:JAVA

1.4 Il

verificatore

Il verificatore è il componente della JVM che controlla i vincoli statici e strutturali dei file class prima della loro esecuzione: anche se il compilatore della Sun (javac) è stato implementato per produrre solo codice “valido”, la JVM non ha la garanzia che ogni file che carica sia stato generato dal javac o comunque che sia “ben strutturato” [vmspecv2 4.9]. Ad esempio i browsers non scaricano il codice sorgente delle applets per poi compilarlo, ma scaricano file già compilati: questo codice potrebbe essere errato o malizioso e per questo potrebbe potenzialmente provocare problemi all’integrità del sistema. Un altro problema legato alla validità del codice è anche dovuto alla compatibilità tra le versioni compilate dei vari file class [JavaSpecs]: ad esempio un programmatore potrebbe aver correttamente compilato il proprio codice java che fa riferimento ad alcuni metodi di un altro file class (perchè magari quel file class contiene una classe derivata da una classe dell’altro), ma la definizione di tali metodi potrebbe un giorno cambiare in modo incompatibile rispetto a prima (ad esempio per un upgrade in cui alcuni metodi che prima erano pubblici diventano privati).

I vincoli da controllare per determinare la validità di un file class possono essere raggruppati in due insiemi: vincoli statici e vincoli strutturali.

I vincoli statici [vmspecv2 4.8.1] sono quei vincoli che assicurano che un file class sia stato “scritto correttamente”: si controlla ad esempio che la lunghezza massima consentita di un file class non venga mai superata, che il target delle istruzioni di salto sia una istruzione all’interno dello stesso metodo, che non vengano create istanze di interfacce o di classi astratte o che non vengano utilizzati registri che non esistono, in definitiva tutti quei vincoli che costituiscono i requisiti minimi per avere del codice potenzialmente corretto.

I vincoli strutturali [vmspecv2 4.8.2] sono invece dei vincoli più complessi da verificare perché per garantirli bisogna calcolare lo stato con cui vengono eseguite le istruzioni ed accertarne la validità: ad esempio si deve controllare che le istruzioni abbiano argomenti del tipo appropriato, che non provochino stack overflow o underflow, che l’esecuzione non vada oltre l’ultima istruzione del metodo. Questi vincoli non possono essere verificati analizzando staticamente il

(9)

CAPITOLO 1:JAVA

codice: occorre una analisi “dinamica” attraverso una esecuzione astratta che sia in grado di seguire il flusso di dati del programma.

La Sun propone come riferimento un algoritmo organizzato in 4 passi. Questo algoritmo è da considerarsi come un insieme di linee guida per una buona ed efficiente implementazione dell’algoritmo di verifica: i controlli effettuati nei vari passi sono stati infatti studiati per ottimizzare il verificatore implementato nelle JVM. Per completezza, vista l’importanza dell’algoritmo di verifica, nell’introdurre i vari passi verrà anche riportato il testo originale delle specifiche Java 2 [vmpecv2]. I 4 passi proposti dalla Sun nelle specifiche Java 2 [vmpecv2 4.9.1] sono:

- passo 1: il file class deve avere la “forma di un file class”:

o deve contenere 0xCAFEBABE (il “numero magico”) nei primi 4 bytes (the

first four bytes must contain the right magic number)

o tutti gli attributi “conosciuti” devono essere della lunghezza corretta (all

recognized attributes must be of the proper length)

o il file class non deve essere troncato o avere “extra bytes” alla fine (the

class file must not be truncated or have any extra bytes at the end)

o le informazioni contenute nel constant pool devono essere “potenzialmente” corrette (the constant pool must not contain any

superficially unrecognizable information).

In definitiva il passo 1 deve controllare l’integrità del file class che sta per essere caricato dalla JVM.

- passo 2: vengono verificati tutti i vincoli strutturali che è possibile controllare senza analizzare il codice dei metodi:

o non devono esistere classi derivate da classi dichiarate final e non devono esistere metodi che fanno l’ovverride di metodi dichiarati final (ensuring that final classes are not subclassed and that final methods are

not overidden)

o ogni classe, eccetto Object, deve avere una superclasse (checking that

(10)

CAPITOLO 1:JAVA

o il constant pool deve soddisfare tutti i vincoli statici (ensuring that the

constant pool satisfies the documented constraints)

o tutti i riferimenti degli attributi e dei metodi del constant pool devono essere validi (checking that all field references and method references in

the constant pool have valid names, valid classes, and a valid type descriptor).

Da notare che questo passo non verifica ancora l’effettiva reale esistenza dei metodi, degli attributi o delle classi, controlla solamente che le gerarchie ed il constant pool siano ben strutturati: i controlli rimanenti vengono effettuati nei successivi passi 3 e 4.

- passo 3: viene analizzato il codice dei metodi. Ogni metodo viene verificato in modo indipendente dagli altri effettuando i controlli statici restanti ed i controlli strutturali attraverso una “data-flow analysis”. E’ il passo più complesso della verifica e vengono accertate le seguenti condizioni per ogni istruzione:

o lo stack ha sempre la stessa dimensione quando si giunge ad una determinata istruzione e contiene gli stessi tipi per i valori contenuti nello stack che occorrono per l’esecuzione di tale istruzione, indipendentemente dal particolare flusso di programma seguito (no

matter what code path is taken to reach that point the operand stack is always of the same size and contains the same types of values);

o solo le variabili locali che contengono valori del tipo appropriato possono essere accedute (no local variable is accesed unless it is known to

contain a value of an appropriate type);

o i metodi devono essere invocati con gli argomenti appropriati (methods

are invoked with the appropriate arguments)

o le istruzioni devono essere invocate con tipi appropriati nelle variabili locali e nello stack (all opcodes have appropriate type arguments on the

operand stack and in the local variable array)

Questo passo viene analizzato più in dettaglio nel paragrafo seguente: data-flow analysis.

(11)

CAPITOLO 1:JAVA

- passo 4: per ragioni di efficienza e di sicurezza alcuni controlli che concettualmente potevano esser fatti durante il passo 3 vengono ritardati, in particolare vengono ritardati quei controlli che si possono effettuare solo dopo il caricamento del file class (Pass3 of the verifier avoids loading class files unless

it has to). I vincoli da verificare sono:

o quando viene referenziato un oggetto i tipi devono essere compatibili (checks that the currently executing type is allowed to reference the

type)

o i metodi e gli attributi referenziati devono esistere (ensure that the

referenced method or field exists in the given class)

o il descrittore dei metodi e degli attributi referenziati deve essere quello appropriato (checks that the referenced method or field has the indicated

descriptor)

o i metodi devono avere i diritti di accesso adeguati per l’esecuzione dell’istruzione (checks that the currently executing method has access to

the referenced method or field)

Durante questo passo non sono necessari i controlli di tipo per le variabili locali e per lo stack perché sono già stati effettuati durante il passo 3.

1.5 La

data-flow analysis

Il passo 3 è il passo più complesso del processo di verifica, in termini di tempo e di spazio in memoria per le strutture dati. I metodi vengono analizzati in modo indipendente: viene effettuatato il parsing del bytecode ed il codice di ogni metodo viene diviso in una sequenza di istruzioni e salvato in un array. Prima di cominciare la data-flow analysis vengono verificati gli ultimi vincoli statici:

(12)

CAPITOLO 1:JAVA

- le istruzioni di salto devono saltare ad istruzioni all’interno del codice del metodo (branches must be within the bounds of the code array of the

method)

- i targets dei salti devono essere l’inizio di una istruzione: le istruzioni possono occupare più byte e non bisogna saltare nel mezzo del loro codice (the targets of all control-flow instructions are each the start of an

instruction ... branches into the middle of an instruction are disallowed)

- le istruzioni devono accedere o modificare solo variabili locali valide: le variabili locali sono numerate a partire da 0 ed il loro numero massimo è specificato all’inizio di ogni metodo con la direttiva .max locals (no

instruction can access or modify a local variable at an index greater than or equal to the number of local variables that its method indicates it allocates)

- il codice di un metodo non può terminare nel mezzo di una istruzione (the

code does not end in the middle of an instruction)

- l’esecuzione di un metodo non può andare oltre la fine del proprio codice (execution cannot fall off the end of the code)

- per ogni gestore delle eccezioni, i byte del bytecode che delimitano l’inizio e la fine del codice protetto devono essere l’inizio (o la fine nel caso di ultima istruzione del metodo) di una istruzione, ed il byte di inizio deve precedere sempre il byte di fine codice protetto e l’ultima istruzione del codice del metodo (for each exception handler, the starting and the ending point of

code protected by the handler must be at the beginning of an instruction or, in the case of ending point, immediately past the end of the code. The starting point must be before the ending point. The exception handler code must start at a valid instruction ...).

Dopo aver accertato i vincoli su elencati inizia la data-flow analysis. Per ogni istruzione del metodo viene salvato un frame, in particolare un in-frame, cioè l’insieme delle variabili locali e dello stack prima dell’esecuzione simbolica dell’istruzione. L’esecuzione è detta simbolica perché viene fatta a livello di tipi, cioè l’interprete astratto Java controlla la validità delle pre-condizioni di ogni istruzione solo a livello di tipi: ad esempio se deve essere eseguita l’istruzione iload_0 (push del contenuto della variabile r0) le precondizioni da verificare sono che la variabile

(13)

CAPITOLO 1:JAVA

r0 contenga un valore int e che nello stack esista almeno 1 posto libero dove

poterla memorizzare (altrimenti avremmo uno stack overflow).

Come chiariremo in seguito, in realtà non occorre salvare queste informazioni per tutte le istruzioni, ma solo per i targets delle istruzioni di salto e, naturalmente, per l’istruzione corrente.

Le pre-condizioni per la prima istruzione del metodo sono salvate nel descrittore del metodo: i registri lì specificati saranno quindi inizializzati con un determinato tipo, mentre tutti gli altri non verranno inizializzati e conterranno (per ora) valori illegali; lo stack è inizialmente vuoto. Ogni istruzione ha un “changed bit” che indica, se vale true, che tale istruzione deve essere “visitata” dall’interprete astratto. Inizialmente viene messo a true solo il changed bit della prima istruzione.

La data-flow analysis compie il seguente ciclo:

1- viene selezionata (non necessariamente secondo un ordine prestabilito) una istruzione con il changed bit a true: il bit viene cambiato al valore false e si procede con il secondo passo dell’analisi. Se nessuna istruzione ha il

changed bit che valga true allora vuol dire che il metodo è stato verificato

con successo: in questo caso diremo che è stato raggiunto il fixed-point dell’iterazione.

(select a virtual machine instruction whose changed bit is set. If no

instructions remains whose changed bit is set, the method has successfully been verified. Otherwise, turn off the changed bit of the selected instruction)

2- si esegue simbolicamente l’istruzione, simulandone l’effetto sullo stack e sulle variabili locali:

o se l’istruzione preleva operandi dallo stack si deve controllare che siano in numero sufficiente (per evitare stack underflow) e del tipo appropriato: se queste condizioni non sono soddisfatte la verifica fallisce (if the instruction uses values from the operand stack, ensure

that there are a sufficient number of values on the stack and that the top values on the stack are of the appropriate value. Otherwise, verification fails)

(14)

CAPITOLO 1:JAVA

o se l’istruzione preleva operandi da variabili locali si deve controllare che contengano valori del tipo appropriato: se la condizione non è soddisfatta la verifica fallisce (if the instruction uses a local variable,

ensure that the specified local variable contains a value of the appropriate type. Otherwise, verification fails)

o se l’istruzione immette operandi nello stack, dopo aver controllato che esista sufficiente spazio (per non provocare stack overflow) si simula l’esecuzione mettendo sullo stack gli operandi del tipo specificato dall’istruzione (if the instruction pushes values onto the

stack, ensure that there is suffient room on the operand stack for the new values. Add the indicated types to the top of the modeled operand stack)

o se l’istruzione immette un valore in una variabile locale, si deve memorizzare il tipo immesso come nuovo tipo contenuto dalla variabile locale (if the instruction modifies a local variable, record

that the local variable now contains a new value)

3- si determinano i successori dell’istruzione appena eseguita. I possibili successori sono:

o l’istruzione successiva, che chiameremo in seguito anche “successore naturale”, se non siamo in presenza di salti incondizionati (goto,

return, athrow,...); la verifica fallisce se si va oltre l’ultima istruzione

del metodo (the next instruction, if the current instruction in not an

unconditional control transfer instruction (for instance goto, return, or athrow). Verification fails if it is possible to “fal off” the last instruction of the method)

o i targets di salti condizionati (if, switch) e incondizionati (the

target(s) of a conditional or unconditional branch or switch)

o i gestori delle eccezioni che proteggono l’istruzione (any exception

handler for this instruction)

4- si effettua il merge dell’out-frame generato in seguito all’esecuzione dell’istruzione corrente nell’in-frame di tutti i successori, cioè l’in-frame dei

(15)

CAPITOLO 1:JAVA

successori verrà modificato seguendo le regole che descriveremo fra poco. Se un successore è un gestore delle eccezioni, e solo in questo caso, lo stack conterrà un solo oggetto, del tipo specificato dal gestore. (Merge the state

of the operand stack and local variable array at the end of the execution of the current instruction into each of the successor instructions. In the special case of control transfer to an exception handler, the operand stack is set to contain a single object of the exception type indicated by the exception handler information).

Inoltre

o se il successore deve essere “visitato” per la prima volta bisogna cambiare il suo changed bit al valore true e salvare il frame generato dal merge come suo in-frame (if this is the first time the successor

instruction has been visited, record that the operand stack and the local variable values calculated in steps 2 and 3 are the state of the operand stack and local variable array prior to executing the successor instruction. Set the “changed bit” for the successor instruction)

o se il successore era già stato visitato precedentemente bisogna cambiare il changed bit al valore true se e solo se l’in-frame del successore è cambiato in seguito al merge con l’out-frame dell’istruzione corrente. (If the successor instruction has been seen

before, merge the operand stack and local variable array values calculated in steps 2 and 3 into the values already there. Set the “changed bit” if there is any modification to the values).

5- Continuare con il passo 1.

Per fare il merge tra due frames bisogna seguire le seguenti regole:

o per fare il merge tra due stack, l’altezza dei due stack deve essere identica, cioè i due stack devono contenere esattamente lo stesso numero di valori. Inoltre valori in posizioni corrispondenti dei due stack devono essere dello stesso tipo, eccetto che per i riferimenti ad oggetti: in questo caso il risultato

(16)

CAPITOLO 1:JAVA

del merge porterà in quella posizione un riferimento del tipo della prima superclasse comune ai due oggetti (una superclasse comune esiste sicuramente perché tutti le classi sono derivate da Object). Se queste condizioni non sono soddisfatte la verifica del metodo fallisce. (To merge two operand stacks, the number of values on each stack must be

identical. The types of values on the stack must also be identical, except that differently typed reference values may appear at corresponding places on the two stacks. In this case, the merged operand stack contains a reference to an instance of the first common superclass of the two types. Such a reference type always exists because the type Object is a superclass of all classes and interfaces types. If the operand stacks cannot be merged, verification of the method fails).

o Per fare il merge tra due insiemi di variabili locali bisogna confrontare il tipo contenuto nelle variabili locali corrispondenti dei due frames (l’out-frame dell’istruzione corrente e l’in-frame del successore esaminato): se i tipi di una coppia non sono identici, a meno di non essere in presenza di riferimenti ad oggetti, il tipo della variabile locale esaminata, dopo il merge, è da considerare “non valido” (cioè nessuna istruzione potrà utilizzare tale variabile come operando). Se invece si era in presenza di riferimenti ad oggetti, dopo il merge la variabile locale conterrà un riferimento del tipo della prima superclasse comune ai due oggetti. (To merge two local variable

array states, corresponding pairs of local variables are compared. If the two types are not identical, then unless both contain reference values, the verifier records that the local variable contains as unusable value. If both of the pair of local variables contain reference values, the merged states contains a reference to an instance of the first common superclass of the two types).

Come anticipato precedentemente non è necessario memorizzare l’in-frame per ogni istruzione, ma solo per i targets: questo perché la memorizzazione di un

in-frame serve solamente per effettuare il merge quando una istruzione viene visitata

più volte. E’ evidente che se il codice è sequenziale nessuna istruzione verrà mai incontrata nuovamente ed è quindi sufficiente conoscere il current-in-frame, cioè

(17)

CAPITOLO 1:JAVA

l’in-frame dell’istruzione esaminata, per procedere comunque correttamente con la data-flow analysis.

Bisogna inoltre notare che salvare l’in-frame dei targets porta due effetti collaterali benefici:

1- è sufficiente a prevenire loop infiniti durante la data-flow analysis: non stiamo infatti memorizzando alcuna informazione aggiuntiva per evitarli. 2- minimizza il numero di in-frames da salvare: infatti spesso i targets delle

istruzioni di salto sono identici. Questo è poi particolarmente evidente per le istruzioni di salto condizionato, basta pensare ad esempio a più istruzioni

if_L annidate, dove L è il label del target: i targets sono uguali, mentre i

successori naturali no.

L’insieme degli in-frames salvati durante la data-flow analysis viene chiamato

dizionario.

Alcune istruzioni e tipi di dati particolari complicano questo passo della verifica, in particolare le subroutines creano non pochi problemi: per analizzare infatti correttamente un metodo che contiene le subroutines si deve ricorrere ad una analisi detta polivariante. Per maggiori dettagli fare riferimento al capitolo 4 paragrafo 4.2.1 di questo lavoro, ed ai capitoli 4.8.2 e 4.9.6 delle specifiche Java 2 [vmpecv2].

Figura

fig. 1.1: La piattaforma Java
fig. 1.3: Modello della sicurezza offerto dalle JDK 1.0
fig. 1.4: Modello della sicurezza delle JDK 1.2

Riferimenti

Documenti correlati