• Non ci sono risultati.

Java e il verificatore 1

N/A
N/A
Protected

Academic year: 2021

Condividi "Java e il verificatore 1"

Copied!
11
0
0

Testo completo

(1)

Java e il verificatore

Java è nato dal linguaggio di programmazione C++ , il quale a sua volta discende dal linguaggio C. Molte delle caratteristiche peculiari di Java derivano dai suoi predecessori o ne sono un affinamento. Sarebbe comunque un errore pensare che Java sia solo una versione migliorata di C++ in quanto presenta differenze sia pratiche che filosofiche.

Java nasce come un linguaggio rivolto allo sviluppo di software per sistemi embedded, si presentava nel 1991 come risposta al bisogno di un linguaggio di programmazione indipendente dalla piattaforma, da impiegarsi in dispositivi elettronici commerciali.

Con l’emergere di Internet, che aveva l’esigenza di programmi portatili, Java divenne un linguaggio d’avanguardia.

Java è un linguaggio interpretato: i sorgenti (.java) vengono compilati in file binari (.class) , chiamati appunto file class, che contengono istruzioni codificate in un formato binario indipendente, il bytecode, ed interpretate dalla Java Virtual Machine ( JVM).

Per questo Java è un linguaggio ‘portabile’; diverse piattaforme richiedono diverse implementazioni di JVM, non diverse versioni dei file class. Con Java si raggiunge quindi l’obiettivo di “scrivere una volta per tutte ed eseguire ovunque, in ogni

(2)

momento e per sempre” (“Write Once and Run Anywhere”) ottenendo dalla compilazione del file sorgente un codice a byte non eseguibile.

1.1 Il

bytecode

Quando un programma sorgente Java viene compilato, da esso si produce un codice intermedio chiamato bytecode che risiede in un file class. Per rendere più chiaro come sia strutturato un bytecode lo si può pensare come un 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, traducendo il

bytecode in un linguaggio macchina adeguato alla piattaforma su cui è installata la

JVM

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.

− Il set delle istruzioni è compatto: l’insieme di opcodes infatti è sufficientemente limitato da garantirne la codifica su un solo byte. Tutte le istruzioni hanno grandezza fissa, eccetto due istruzioni di salto condizionato. Queste caratteristiche aiutano a rendere il file class di dimensioni ridotte. − Il codice operativo è tipato: ogni istruzione opera su un determinato tipo

(int, float..) specifico. Questa informazione è codificata insieme all’opcode: Ad esempio la istruzione iconst_0 lavora su interi ( i sta per int ).

− La maggior parte delle istruzione operano su uno stack, gli operandi vengono sempre caricati e salvati sullo stack. La JVM è quindi una macchina ‘stack-based’: tale scelta è stata fatta per facilitarne l’implementazione su architetture con pochi registri, come ad esempio le periferiche embedded.

(3)

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

Il set d’istruzioni può essere diviso in otto gruppi:

− Istruzioni di load/store. Trasferiscono valori tra lo stack e i vari registri. Ad esempio dconst_0 mette in cima allo stack un double che vale 0.

− Istruzioni aritmetiche. Tutti gli opcode sono tipati e utilizzano lo stack per prelevare e salvare gli operandi.

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

− Istruzioni per la creazione degli oggetti: ad esempio l’istruzione new crea un muovo oggetto della classe specificata.

− Istruzioni per il controllo del flusso del programma: si dividono in salti condizionati ( if, tableswitch..) e salti incondizionati ( goto, jsr..).

− Istruzioni per l’invocazione dei metodi e per il loro return. Ad esempio invokevirtual invoca il metodo di un oggetto.

− Istruzioni per accedere agli attributi di un oggetto: ad esempio getfield salva sullo stack il campo di un oggetto specificato come operando.

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

1.2 La Java Virtual Machine

La JVM è l’interfaccia tra i file class (il codice compilato dal file sorgente Java) e le varie piattaforme hardware/software. Generalmente implementata in software (per questo ‘virtual’), la JVM rappresenta 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. Con l’avvento di Internet è nata la possibilità di eseguire codice non necessariamente certificato , per esempio

(4)

all’interno delle pagine web sono spesso presenti delle applets Java. La sicurezza diventa ancora più importante se si pensa che il bytecode stesso potrebbe essere modificato o scritto senza l’utilizzo di un compilatore: di conseguenza gli utenti devono essere protetti da un codice malizioso, corrotto o semplicemente errato che potrebbe rendere vulnerabile ad attacchi di vario genere il loro host.

Il paradigma utilizzato da Java per 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 migliore gestione della memoria rispetto ai suoi predecessori. Il secondo a livello di esecuzione, con il verificatore, che garantisce la correttezza del codice a run-time, e con il modello sandbox [SandBox], che garantisce la limitatezza delle applets con il sistema. Le applets infatti non utilizzano direttamente le risorse del sistema ma un determinato numero di risorse e di API’s messe a disposizione dall’ambiente Java: questa è l’idea alla base del modello sandbox.

1.3 La sicurezza in Java: il modello Sandbox

Come accennato Java confina i programmi dentro l’ambiente di esecuzione Java e non permette loro di accedere ad parti dell’host. L’insieme di misure che permettono tale sicurezza è denominato modello sandbox.

La sicurezza del modello sandbox si basa su tre componenti:

− Le applets non sono compilate fino al codice eseguibile, ma fino a un codice intermedio ( il bytecode appunto).

− Le applets non hanno accesso diretto alle risorse hardware, ma possono accedere solo ad un insieme attentamente designato di classi API (Application Program Interface) e metodi che effettuano un adatto controllo degli accessi prima di eseguirne eventuali interazioni con il mondo esterno per mezzo delle applets.

(5)

− Subito dopo il download il bytecode è soggetto ad un’analisi statica denominata ‘verifica del bytecode’, il cui scopo è quello di assicurarsi che il codice dell’applets sia corretto.

1.4 La verifica del bytecode.

La verifica del bytecode è una tappa cruciale per la sicurezza nel modello sandbox di Java; un baco nel verificatore potrebbe fare in modo da prendere per corretta un

applet maliziosa il che provocherebbe un possibile attacco alla sicurezza del host

dell’utente.

Il componente della JVM che si occupa di tale controllo è denominato verificatore. 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 quelli che assicurano che un file class sia scritto correttamente, per esempio che la sua lunghezza non superi quella massima consentita, che il target delle istruzioni di salto sia corretto e cosi via.

I vincoli strutturali [vmspecv2 4.8.2] sono dei vincoli più complessi da verificare perché per garantirli bisogna calcolare lo stato con cui vengono eseguite le istruzioni ed accettarne la validità; ad esempio bisogna controllare che le istruzioni abbiano argomenti del tipo appropriato che non provochino overflow o underflow nello stack, che l’esecuzione non vada oltre l’ultima istruzione del metodo. Quest’ultimi devono essere verificati dinamicamente attraverso una esecuzione astratta che sia in grado di seguire il flusso del programma.

La Sun propone un algoritmo organizzato in quattro passi:

− Passo 1: il file class deve avere la forma di un file class.

o Deve contenere il ‘numero magico ’ (0xCAFEBABE) nei primi 4 bytes. o Tutti gli attributi devono essere della lunghezza corretta.

(6)

o Le informazioni nel costant pool devono essere ‘potenzialmente’ corrette.

In definitiva il passo 1 deve verificare l’integrità del file che sta per essere caricato.

− Passo 2: vengono verificati tutti i vincoli strutturali che è possibile controllare senza analizzare il codice.

o Non devono esistere classi derivate da classi dichiarate final e non devono esistere metodi che fanno l’override di metodi dichiarati. o Ogni classe, eccetto Object deve avere una superclasse.

o Il costant pool deve soddisfare tutti i vincoli statici.

o Tutti i riferimenti degli attributi e dei metodi del costant pool devono essere validi.

Questo passo che le gerarchie ed il costant pool siano ben strutturati.

− Passo 3: viene analizzato il codice dei metodi. Ogni metodo viene analizzato in modo indipendente dagli altri effettuando i controlli statici restanti ed i controlli strutturali attraverso una ‘data-flow analysis’, è il passo più complesso della verifica e come vedremo l’utilizzo delle subroutines lo rende ancora più complesso.

o Lo stack ha sempre la stessa dimensione quando si giunge ad un’istruzione, contiene sempre gli stessi tipi per i valori contenuti nello stack che occorrono per eseguire tale istruzione, indipendentemente dal flusso di programma seguito.

o Solo le variabili locali che contengono valori con il tipo appropriato possono essere accedute.

o I metodi devono essere invocati con gli argomenti appropriati.

o Le istruzioni devono essere invocate con tipi appropriati nelle variabili locali e nello stack.

(7)

− Passo 4: alcuni controlli che concettualmente potrebbero appartenere al passo 3, per motivi di efficienza e sicurezza vengono rimandati al passo 4, in particolare quei controlli che possono esser fatti solo dopo il caricamento del file class.

I vincoli da verificare in questo passo sono:

o Quando viene referenziato un oggetto i tipi devono essere compatibili.

o I metodi e gli attributi referenziati devono essere compatibili. o Il descrittore dei metodi e degli attributi deve essere appropriato. o I metodi devono avere i diritti di accesso appropriato per l’esecuzione

dell’istruzione.

1.5 La Data-Flow Analysis

Il passo3 è il passo più complesso nella verifica, prima di cominciare la data-flow

analysis vengono controllati gli ultimi vincoli statici:

− Le istruzioni di salto devono saltare ad istruzioni all’interno del metodo. − I targets dei salti devono essere l’inizio di un’istruzione, non si può saltare

nel mezzo di un’istruzione.

− Le istruzioni devono accedere e modificare solo le variabili locali valide, le variabili locali sono numerate da 0 e il loro numero massimo è specificato all’inizio di ogni metodo con la direttiva .max locals.

− Il codice di un metodo non può terminare nel mezzo di un’istruzione. − L’esecuzione di un metodo non può andare oltre la fine del proprio codice. − 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 un’istruzione, ed il byte d’inizio deve precedere

(8)

sempre il byte di fine codice protetto e l’ultima istruzione del codice del metodo.

Controllati questi vincoli inizia la data-flow analysis. Per ogni istruzione del metodo viene salvato un frame, cioè l’insieme delle variabili locali e dello stack prima dell’esecuzione simbolica dell’istruzione (quindi in questo caso si parlerà di

in-frame).

L’esecuzione è detta simbolica perché fatta a livello di tipi, per esempio se deve essere eseguita l’istruzione iload_0 ( push del contenuto della variabile locale r0 ) le precondizioni da verificare sono che la variabile r0 contenga un valore di tipo int e che nello stack esista almeno un posto libero per poterla memorizzare (in caso contrario si avrebbe uno stack-overflow).

Come si vedrà meglio in seguito in realtà non è necessario salvare il frame per tutte le istruzioni ma solo per i targets delle istruzioni di salto e naturalmente per l’istruzione corrente.

Ogni istruzione ha un changed bit che se vale true indica che tale istruzione deve essere visitata dall’interprete astratto. Inizialmente lo stack è vuoto e solo la prima istruzione del metodo ha il changed bit settato a true.

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.

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

(9)

appropriato: se queste condizioni non sono soddisfatte la verifica fallisce

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

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

o se l’istruzione immette un valore in una variabile locale, si deve memorizzare il tipo immesso come nuovo tipo contenuto dalla variabile locale

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

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

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

(10)

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

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.

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

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.

(11)

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è 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, nel capitolo 4 si affronteranno in dettaglio le problematiche e la soluzione sviluppata in questo lavoro sulle subroutines.

Riferimenti

Documenti correlati

in quanto la relazione tra l’angolo di diffusione del fotone e l’energia ceduta all’elettrone (o, equivalentemente, lo spostamento in lunghezza d’onda) non è

Ciascuno dei due elettrodi metallici rilascia ioni positivi nella soluzione portandosi ad un potenziale negativo rispetto alla stessa; la diversa elettronegatività

“Figli” associati viene impostata a NULL(ovviamente se non ho specificato il vincolo che debba essere NOT NULL). Esempio: se cancello un cliente, l' id_cliente degli

Cioè se pensiamo di attraversare una zona soggetta ai vincoli idrogeologico, paesaggistico o geologico, in questa zona dovremo prevedere opportuni dispositivi di protezione

La violazione di un vincolo di integrità di chiave esterna può causare, come conseguenza delle politiche di gestione di tali vincoli, ulteriori modifiche alla base di

La violazione di un vincolo di integrità di chiave esterna può causare, come conseguenza delle politiche di gestione di tali vincoli, ulteriori modifiche alla

Con il presente Avviso l’Ambito Territoriale N23 – Comune di Nola Capofila, bandisce un avviso per la presentazione di manifestazioni di interesse per la

• Vendita di 49 strutture ospedaliere mettendo a carico della regioni affitto e manutenzione ( senza nessun miglioramento del rating delle varie agenzie