Strumenti di Verifica e Validazione di Codice Java

annuncio pubblicitario
Strumenti di Verifica e Validazione di Codice Java
Florian Daniel, Federico M. Facca, Stefano Modafferi and Enrico Mussi
Dipartimento di Elettronica e Informazione
Via Ponzio 34/5, 20133
Milano, Italy
[daniel, facca, modafferi, mussi]@elet.polimi.it
ABSTRACT
Sistemi informatici sempre più complessi sono ormai costantemente parte della vita di tutti i giorni di milioni di persone.
Le applicazioni software che gestiscono questi sistemi informatici diventano man mano sempre più grosse e difficili da
gestire e per questo l’incidenza del numero di bug al loro
interno è diventata sempre più alta. La creazione di un software perfetto, ossia senza bug, è un obiettivo, che teoremi
e anni di esperienza, hanno dimostrato impossibile. Tuttavia, con le conoscenze attuali, è possibile fare molto di più
di quanto è stato fatto in passato per ricercare automaticamente bug all’interno del software. Java negli ultimi anni è
stato al centro delle attenzioni dei programmatori per vari
motivi tra cui spicca la portabilità, questo ha fatto sı̀ che
molti si siano interessati all’opportunità di studiare metodi
per la verifica e la validazione di programmi Java. Alcuni
dei tool più recenti consento di effettuare verifiche senza impattare notevolmente sui tempi di sviluppo, permettendone
quindi l’impiego per qualsiasi tipo di applicazione. Questo
articolo presenta un insieme di tool che, sfruttando tecniche
diverse, si propongono di verificare e validare applicazioni
sviluppate in Java.
1. INTRODUZIONE
La validazione e la verifica delle applicazioni software è una
tematica che sta suscitando molti interessi non solo nell’ambito della ricerca ma anche in ambito industriale. I cicli
di sviluppo e mantenimento di applicazioni software sempre
più complessi, non possono essere, come l’esperienza ha più
volte dimostrato, immuni ai bug e spesso questi bug possono
avere anche effetti disastrosi. Per questo motivo in molti
ambiti industriali la verifica e la validazione del software è
diventato uno degli step fondamentali del ciclo di sviluppo
del software, soprattutto nel caso di sistemi critici.
Oggi sono disponibili una serie di tool con un grande potenziale per aiutare gli sviluppatori nella ricerca degli errori di
programmazione, alcuni dei quali non sarebbero facilmente
individuabili altrimenti. Alcuni di questi tool sono anche
in grado di rilevare automaticamente sia deadlock che vio-
lazioni delle sezioni critiche, due tra le più critiche tipologie
di errori che spesso causano gravi problemi ai software in
esecuzione.
I tool che si occupano di analisi del software alla ricerca
di bug possono essere distinti in due categorie principali a
seconda di come avviene la verifica.
• Analisi dinamica: esegue il codice dell’applicazione
e durante l’esecuzione effettua una serie di analisi per
verificare la correttezza dell’output prodotto. È un
metodo rapido, ma fortemente legato alla capacità delle
test suite usate di coprire tutti gli aspetti critici del
codice analizzato.
• Analisi statica: verifica il software senza eseguirlo.
Fa uso di diverse tecniche tra cui i bug pattern e i
modelli per la rappresentazione del codice e delle sue
proprietà. L’analisi statica può essere più o meno dispendiosa in termini di tempo e di costo computazionale
a seconda delle tecniche usate.
In base al tool utilizzato e al tipo di bug ricercati, il risultato
può essere più o meno preciso. Comunque nessun tool è in
grado di dare la certezza dell’assenza di bug nel codice. La
ricerca di errori di programmazione soffre essenzialmente di
due problematiche:
• falsi positivi: alcuni dei problemi individuati in realtà non lo sono. Questo tipo di problema non è critico,
ma, se in quantità eccessiva, porta all’inutilizzabilità
del tool perché richiede un ulteriore tempo di analisi
dell’output per distinguere i bug reali dai falsi allarmi.
• falsi negativi: non tutti i bug presenti nel codice
vengono individuati. Questo tipo di problema è critico perché non rileva potenziali problemi. Per ridurre
le occorrenze di questo tipo di inconveniente piuttosto
che effettuare analisi general purpose è possibile effettuare delle analisi di codice mirate alla verifica di
specifiche proprietà e quindi più precise.
In questo articolo noi ci concentriamo su alcuni tool che si
occupano della validazione e della verifica di applicazioni
Java [16]. I tool presi in considerazione dalla nostra analisi
effettuano tutti un’analisi di tipo statico: FindBugs, PMD,
Esc/Java2, Java Pathfinder e Bandera. In Figura 1 vengono riassunti alcuni dettagli sui tool, quali le interfacce ed
i metodi di verifica utilizzati.
Figure 1: Riepilogo dei Tool
tura dei pattern in una maniera esplicita e dichiarativa direttamente in codice Java. FindBugs non analizza direttamente il codice sorgente Java, ma la sua versione compilata
file .class). Si tratta pertanto di uno strumento di analisi di bytecode, cioè del “linguaggio macchina” della Java
Virtual Machine. Per questo motivo, i detector adottati
da FindBugs, internamente, sfruttano la libreria BCEL [2],
una libreria open source di analisi di bytecode Java. Più
precisamente, FindBugs propone una struttura di detector
basata sul cosiddetto Visitor design pattern di BCEL: ogni
detector visita singolarmente ciascuna classe della libreria o
applicazione da analizzare ed esegue la sua analisi in maniera
atomica ed indipendente da eventuali altri detector operanti
sullo stesso codice.
L’articolo è organizzato come segue: le Sezioni 2 e 3 mostrano i tool FindBugs e PMD; in Sezione 4 viene presentato il
tool Esc/Java2; la Sezione 5, dopo un’introduzione alle tecniche di Model Checking, presenta il tool Java PathFinder;
nella Sezione 6 viene introdotto il tool Bandera; e infine,
la Sezione 7 presenta un confronto fra i tool e riporta le
conclusioni a cui siamo giunti analizzando i tool.
FindBugs è corredato da un numero elevato di rilevatori
di bug pattern ricorrenti (la versione 0.9.1 ne fornisce circa 130), altri possono essere progettati su misura e aggiunti a piacere alla configurazione standard del tool. Usando
tecniche di analisi statica relativamente semplici, possono
essere implementati tanti diversi rivelatori automatici, uno
per ciascun pattern nuovo da riconoscere.
Nome
FindBugs
Input
Bytecode
PMD
Esc/Java2
Source
Source
+
Annotation
Pathfinder Source
Bandera
Source
Interf.
CL,Gui
CL,Gui
CL,Gui
CL
CL,Gui
Tecnologia
Syntax,
Dataflow
Syntax
Theorem
Proving
Model Cecking
Model Cecking
Indipendentemente dal fatto che un particolare detector sia
stato progettato per soddisfare problemi di programmazione
FindBugs [19] è un prodotto open source originalmente svilupsu misura o che faccia parte dei detector generali forniti inpato da Bill Poth e correntemente mantenuto da Bill Pugh
sieme ad una versione particolare di FindBugs, questi pose David Hovemeyer. Codice sorgente Java e la documensono essere raggruppati in quattro classi di riconoscitori funtazione possono essere scaricati gratuitamente dal sito http://findzionalmente diversi. In particolare, le diverse strategie di
bugs.sourceforge.net.
implementazione dei detector pongono i seguenti approcci
analitici nel centro della loro attenzione:
All’interno della famiglia degli strumenti di analisi di codice
Java FindBugs è probabilmente uno dei tool più intuitivi,
• Struttura della classe e gerarchia di ereditarietà. Questi
perché si avvicina molto al nostro modo di interpretare e
detector guardano solamente alla struttura delle classi
leggere codice sorgente. Questo perché, fra le varie tecanalizzate e controllano se le gerarchie di ereditarietà
niche di analisi statica esistenti, FindBugs adotta un ape le implementazioni di interfacce siano coerenti, senproccio basato sulla scoperta di cosiddetti bug pattern alza tenere conto di eventuale codice sorgente all’interno
l’interno del codice da analizzare [15]. Tali pattern sono
delle classi.
frammenti o strutture di codice sorgente o combinazioni di
istruzioni che molto probabilmente rappresentano errori di
• Scansione lineare del codice. Questi detector analizprogrammazione, e la loro natura prettamente sintattica fazano più in dettaglio ogni singola classe ed “entrano”
cilita la comprensione dei rispettivi warnings da parte dei
nei rispettivi metodi. Si basano su una lettura linprogrammatori.
eare del bytecode dei metodi da analizzare, in base
alla quale aggiornano (se necessario) una macchina a
Prima di entrare meglio nei dettagli dell’analisi basata su
stati finiti interna, ogni qualvolta un detector richiebug pattern, però, è importante notare che FindBugs isda il mantenimento di un certo stato durante l’analisi.
peziona possibili errori di programmazione e non rappresenQuesti detector non usano eventuali informazioni dita un tool di verifica dello stile di programmazione. Verifisponibili sul flusso di controllo vero dell’esecuzione e
catori di stile esaminano il codice per determinare se questo
piuttosto si basano su euristiche che ne permettono
rispetta o meno determinate regole stilistiche, come quelle
un’approssimazione efficace.
adottate all’interno di un progetto software che coinvolga più
• flusso di controllo. I detector di questa categoria si
programmatori diversi. Tali regole, infatti, servono sopratbasano su un grafo accurato del flusso di controllo per
tutto per mantenere uno stile di codifica coerente in tutto
ciascun metodo da analizzare. Non usando più apil progetto, cosa che, di conseguenza, aumenta la leggibilità
prossimazioni del flusso di esecuzione, l’applicazione
e la manutenibilità del codice. In generale però, non mandi questi detector risulta più onerosa in termini di
tenere uno stile accordato, non deve per forza risultare in
velocità.
un errore.
• Flusso di dati. I detector più complicati si basano
su un’analisi approfondita del flusso di dati, oltre che
2.1 Pattern e Detector built-in
sull’analisi del flusso di controllo.
Per il riconoscimenti dei bug pattern sintattici, FindBugs
2. FINDBUGS
fornisce un’architettura modulare che permette di aggiungere opportuni detector, uno per ciascun pattern da riconoscere.
Tramite questi detector si specificano le proprietà e la strut-
Queste tecniche di analisi statica permettono di coprire un
gran numero di pattern di errori e determinano, insieme alla
scelta di limitare l’analisi al bytecode, il potere riconoscitivo dei detector. Conseguentemente, FindBugs si presta soprattutto per la scoperta delle seguenti classi di problemi o
errori:
• Problemi di correttezza per thread singoli.
• Problemi di correttezza nella sincronizzazione di più
thread cooperanti.
• Questioni di performance.
• Problemi di sicurezza o vulnerabilità del codice.
Generalmente, più è complesso il pattern da riconoscere, più
è complesso il suo detector. I detector basati sull’analisi del
flusso di dati sono i più complessi, mentre quelli per l’analisi
strutturale delle classi sono i più semplici. Comunque, i detector più complessi solo in pochi casi superano le 1000 righe
di codice Java, e quasi la metà dei detector totali occupa
meno di 100 righe.
Date queste premesse e per una migliore comprensione, di
seguito discuteremo qualche detector tipico, già presente in
FindBugs; per motivi di spazio non approfondiremo tutti i
pattern definiti.
Dropped Exception.
Iniziamo con un pattern molto ricorrente e di facile comprensione. Il detector Dropped Exception si concentra sulla gestione delle eccezioni in Java, e quindi sui blocchi try-catch.
In particolare, questo detector individua tutti quei costrutti try-catch con blocco catch vuoto, cioè tutti quelli che
scartano eventuali eccezioni.
Spesso durante la stesura del codice un programmatore è
convito che certe eccezioni non possano mai accadere e quindi ignora la gestione corretta delle relative eccezioni. Ma,
soprattutto in base a modifiche successive al codice che alterano anche le premesse stesse alla base di quel particolare frammento di codice, le eccezioni potrebbero sı̀ essere
lanciate. Questo molto probabilmente avrebbe come conseguenze comportamenti di runtime anomali da parte dell’applicazione. Trovare problemi connessi ad eccezioni non
propriamente gestite risulta essere molto oneroso in pratica
e spesso richiede molto tempo.
Null Pointer Dereference, Redundant Comparison
to Null.
Chiamare un metodo o accedere a variabili di istanza tramite
riferimenti al puntatore nullo (null) porta inevitabilmente
ad una NullPointerException durante l’esecuzione dell’applicazione. Solitamente un programmatore è molto attento a
questo tipo di errore, perché la NullPointerException spesso – se non gestita in maniera opportuna – causa il crash dell’intero programma, ma quando il riferimento del puntatore
è determinato per esempio dall’esecuzione precedente di un
qualche metodo, il puntatore può anche risultare nullo. Per
questo, il detector Null Pointer Dereference individua quelle
istruzioni che de-referenziano un puntatore potenzialmente
nullo.
Alla base dell’implementazione di questo detector c’è un’analisi del flusso di dati, ristretto all’interno del metodo sotto
analisi. Il pattern adottato non determina direttamente se
parametri passati a o ritornati da un metodo siano nulli, ma
sfrutta eventuali condizioni if presenti in righe precedenti al
potenziale eccezione. Nel seguente frammento, per esempio,
il puntatore foo è sicuramente nullo all’interno del blocco
della condizione:
if (foo == null) {
... //foo è null in tutto il corpo della if
}
Analogamente, il frammento seguente mostra un esempio di
de-referenziazione di un puntatore nullo tratto dal codice
sorgente di Eclipse e scoperto con FindBugs [15]:
Control c = getControl();
if (c == null && c.isDisposed()) {
return;
Oltre a dereferenziazioni di null, il pattern di questo detector permette anche di individuare confronti il cui risultato è prefissato, perché entrambi i valori sono sicuramente
nulli o uno è sicuramente nullo e l’altro sicuramente non
lo è (Redundant Comparison to Null). Data la natura del
problema, questo non deve risultare per forza in un errore a
runtime, ma spesso indica una certa incertezza da parte del
programmatore e può addirittura rivelare errori completamente diversi in maniera indiretta. Il seguente frammento,
preso dalla classe java.awt.MenuBar (Sun JDK 1.5, build
59 ), ne riporta un esempio tipico:
if (m.parent != this) { //m non nullo!
add(m);
}
helpMenu = m;
if (m != null){ //confronto fra m e null
...
Unconditional Wait.
Sincronizzare thread tramite le due istruzioni wait() e notify() nei programmi multi-threaded rappresenta una sorgente di errori frequenti. Questa tesi è nettamente confermata anche dalle esperienze raccolte con FindBugs da Hovemeyer e Pugh [15] e gli innumerevoli errori di sincronizzazione riscontrati.
Il detector Unconditional Wait, per esempio, indirizza un ricorrente problema collegato alla wait(). Cerca quelle parti
di codice dove la wait() è eseguita in maniera incondizionata all’ingresso in un blocco synchronized (un monitor).
Tipicamente questa configurazione indica che la condizione
associata alla wait() che valuta se sospendere o meno il
thread in oggetto viene valutata senza detenere il lock sul
monitor. Conseguentemente, eventuali messaggi di notifica
da parte di altri thread potrebbero andare persi. Questo tipo
di errore è particolarmente difficile da riprodurre, perché
intrinsecamente dipende anche dal tempo e dall’ordine di
esecuzione dei singoli thread.
Il detector per questo pattern esegue una scansione lineare
del bytecode del metodo e individua quelle chiamate a wait(),
immediatamente preceduti da un’istruzione monitorenter e
non da una condizione. Il codice seguente (JBoss 4.0.ORC1 )
ne dimostra un esempio reale:
if (!enabled) {
try {
synchronized (lock) {
lock.wait();
...
Questi tre esempi rappresentano solo una piccola frazione
dei detector complessivi disponibili in FindBugs, ma per il
momento ci interessa capire l’approccio di fondo. In questo
senso è importante notare, che il metodo basato su bug pattern risulta essere abbastanza accurato in generale, ma in
determinate situazioni produce comunque cosiddetti false
positives, cioè messaggi di warning sbagliati, e false negatives (fallimenti nel riconoscere errori esistenti). Inolte,
una parte importante dei messaggi prodotti da FindBugs
si riferisce più a “violazioni” di regole di buona programmazione e meno ad errori veri e propri. Comunque, secondo
gli autori di FindBugs [15] la percentuale di errori effettivi
sul totale dei messaggi prodotti durante l’analisi si aggira
intorno al 50%, che è una percentuale buona nel contesto
dell’analisi statica di codice Java [20].
2.2 Il tool in pratica
FingBugs si presenta con vari front end: una semplice applicazione batch (eseguita dalla riga dei comandi) che genera rapporti testuali in maniera online per ciascun pattern
riscontrato; un’applicazione batch che genera rapporti in
formato XML per una facile integrazione con altri tool di
sviluppo; e un tool interattivo con interfaccia grafica, che
permette di scorrere la lista di warnings generati durante
la fase di analisi, di confrontare tali messaggi con il codice
sorgente Java e di apportare eventuali annotazioni personali. Inoltre è stato sviluppato anche un plugin di FindBugs
per l’ambiente di sviluppo Java Eclipse. Figura 2 mostra
uno screenshot dell’interfaccia grafica di FindBugs al lavoro.
La lista nella parte superiore della finestra rappresenta i
problemi riscontrati, mentre nella parte inferiore è possibile
visualizzare una descrizione testuale del pattern.
2.3 Definizione di detector personalizzati
In questa sezione mostriamo con un piccolo esempio come
FindBugs può essere esteso con detector personalizzati per
soddisfare esigenze particolari, non previste dai detector di
default. In particolare, in questa sezione definiremo un detector per il seguente pattern di codice Java:
if (Logger.isLogging()) {
new Logger().log("bob");
...
Si tratta di assicurare che tutte le chiamate al metodo log()
della classe Logger siano precedute da una clausola if che
controlla se il componente di logging effettivamente è attivo
o meno. Infatti, un programmatore potrebbe essere tentato di eseguire operazioni di logging senza prima controllare
il corretto funzionamento del rispettivo componente. Per
Figure 2: Interfaccia grafica di FindBugs.
evitare eventuali problemi del genere, di seguito definiamo
un opportuno detector.
Come già accennato in precedenza, i detector di bug pattern di FindBugs si basano sulla Byte Code Engineering Library (BCEL) per la lettura di bytecode Java. Tutti i detector implementano il cosiddetto Visitor pattern, per il quale
FindBugs fornisce interfacce predefinite, che possono essere
facilmente estese per fronteggiare nuovi pattern. L’approccio generale a tale scopo può essere riassunto come segue:
Prima si scrive il frammento di codice Java che rappresenta il
pattern da controllare e lo si compila per ottenere la versione
bytecode del pattern. Applicando il comando javap -c (o
qualsiasi altro disassemblatore di bytecode) si ottiene una
versione “leggibile” del bytecode1 . Questa rappresentazione
permette di derivare la struttura generale del detector per
effettuare l’analisi sintattica del bytecode Java.
Applicando per esempio javap -c al pattern precedente, si
ottiene il bytecode disassemblato mostrato in figura 3 (per
motivi di presentazione, la formattazione è stata modificata leggermente). Questa rappresentazione è la base per
comprendere a fondo il flusso di controllo e le “istruzioni
macchina” a livello di bytecode che caratterizzano il particolare pattern sotto esame. La conoscenza delle istruzioni
bytecode del pattern permette di formulare le condizioni per
l’analisi sintattica, come riportate dalla figura 4. In particolare, la figura mostra solo il metodo principale del detector, il
metodo sawOpcode(), che viene chiamato iterativamente per
ciascuna istruzione bytecode del programma sotto analisi. A
questo punto non descriviamo tutti i dettagli implementativi
dell’esempio; basta concentrarsi sulle condizioni della figura
4 per vedere come queste si riferiscano alle singole istruzioni
1
Il comando javap disassembla file .class di Java; fa parte
di tutte le versioni JDK.
public void methodWithLogging_guarded();
Bytecode:
0:
invokestatic
#28;
//Method cbg/app/Logger.isLogging:()Z
3:
ifeq
18
6:
new
#16;
//class Logger
9:
dup
10: invokespecial
#17;
//Method cbg/app/Logger."<init>":()V
13: ldc
#19;
//String bob
15: invokevirtual
#23;
//Method cbg/app/Logger.log:(Ljava/lang/Object;)V
18: aload_0
19: invokespecial
#31;
//Method doWork:()V
22: return
Figure 3: Bytecode della Java Virtual Machine come prodotto da javap disassemblando file .class.
public void sawOpcode(int seen) {
if ("cbg/app/Logger".equals(classConstant) && seen == INVOKESTATIC &&
"isLogging".equals(nameConstant) && "()Z".equals(sigConstant)) {
seenGuardClauseAt = PC;
}
if (seen == IFEQ && (PC >= seenGuardClauseAt + 3 && PC < seenGuardClauseAt + 7)) {
logBlockStart = branchFallThrough;
logBlockEnd = branchTarget;
}
if (seen == INVOKEVIRTUAL && "log".equals(nameConstant)) {
if (PC < logBlockStart || PC >= logBlockEnd) {
bugReporter.reportBug(new BugInstance(
"CBG_UNPROTECTED_LOGGING",HIGH_PRIORITY).addClassAndMethod(this).addSourceLine(this));
}
}
}
Figure 4: Codice Java del detector per il controllo sull’uso corretto del logging.
bytecode di figura 3, per comprendere il funzionamento dei
detector di bug pattern in FindBugs.
3. PMD
PMD2 [22], come FindBugs o simili strumenti di analisi statica di codice Java, effettua un’analisi sintattica del programma o della libreria. Contrariamente a FindBugs, PMD analizza direttamente il codice sorgente in oggetto e non effettua
alcun tipo di controllo a livello di bytecode. Inoltre, PMD
non è dotato di meccanismi per l’analisi del flusso di controllo o del flusso di dati. Conseguentemente, molti degli
“errori” che si trovano con PMD si riferiscono soprattutto a
convenzioni stilistiche. In generale queste rappresentano più
sospetti sulla forma del codice e meno sulla sua correttezza.
Infatti, in letteratura PMD è trattato più come verificatore
di stile che come verificatore di codice sorgente [20].
Nel confronto diretto fra PMD e FindBugs, il primo nella
maggior parte dei casi rileva più problemi del secondo (usando i tool nelle loro configurazioni base) [15], ma questo non
vuol dire che l’uno sia “migliore” dell’altro. Infatti, basta
pensare che PMD lavora direttamente sul codice sorgente,
2
L’acronimo PMD non sembra avere una forma estesa; citiamo letteralmente gli autori: “We’ve been trying to find the
meaning of the letters PMD - because frankly, we don’t really know. We just think the letters sound good together.”
[22]
mentre FindBugs analizza bytecode. É proprio per questo
motivo che il primo strumento si presta meglio per verificare
proprietà stilistiche del codice, e il secondo si rivela più adatto per l’analisi di potenziali errori, trascurando la forma del
codice vero e proprio (infatti, a livello di bytecode la “forma”
non c’è più). Nonostante questa diversità nell’approccio, comunque si registra un certo livello di sovrapposizione fra i
messaggio di warning prodotti da PMD e FindBugs. Infatti,
basta pensare al detector Dropped Exception di FindBugs,
descritto nella sezione 2.1, che tratta più un problema stilistico e meno la scoperta di un errore vero e proprio. Conseguentemente, sia PMD che FindBugs producono messaggi
relativi alla gestione scorretta delle eccezioni.
In base alle considerazioni precedenti, si può quindi affermare che i due strumenti si concentrano su due aspetti diversi della qualità del software e si completano a vicenda,
pur con qualche sovrapposizione.
4.
ESC/JAVA2
ESC/Java2 [10] è un tool per il controllo statico esteso (Extended Static Checking) del codice, ossia si prefigge di verificare il codice a compile-time individuando possibili errori che vanno al di là dei semplici errori solitamente riportati dalla verifica statica eseguita dai compilatori (i.e., type
checking). La verifica statica avviene a partire dalle annotazioni JML inserite all’interno del codice sorgente usando
il theorem proving. Il tool è anche in grado di individuare
alcuni possibili errori anche senza l’ausilio di annotazione
(i.e. index out of bounds).
ESC/Java è nato nei laboratori Compaq Research come sviluppo del tool ESC/Modula-3 [17]. Dopo la fusione di Compaq
con HP il progetto era stato abbandonato in seguito alla
chiusura dei laboratori Compaq Research. Questo ha fatto
sı̀ che ESC/Java rimanesse indietro rispetto alle evoluzioni
di Java e di JML. Recentemente Cok e Kiniry, ottenuto il
codice sorgente di ESC/Java, hanno dato vita al progetto
ESC/Java2 [5] con lo scopo di: (i) rendere il tool completamente compatibile con la versione 1.4 di Java;3 (ii)
aggiornare le annotazione accettate dal tool in modo che
siano consistenti con la versione attuale di JML; (iii) aumentare la sintassi JML riconosciuta dal tool in modo da,
potenzialmente, coprirla interamente.
ESC/Java2, in linea con uno degli utilizzi per cui è stato pensato JML, si propone come un tool per supportare
la tecnica design-by-contract, che enfatizza l’importanza di
specificare in modo esplicito i vincoli che devono sussistere
prima e dopo l’esecuzione di un componente software.
Il progetto non ha ancora prodotto una versione definitiva del nuovo tool, tuttavia la release attuale – alpha-9 –
e quelle immediatamente precedenti hanno raggiunto una
stabilità sufficiente da permettere l’applicazione del tool a
problemi reali. ESC/Java2 funziona sia da riga di comando
che tramite interfaccia grafica. Allo stato attuale, l’interfaccia grafica è ancora molto limitata.
Il tool può essere scaricato gratuitamente dalla pagina web:
http://secure.ucd.ie/products/opensource/ESCJava2/.
Il progetto ESC/Java2 prevede tra i vari obiettivi, quello di aggiornare e rendere compatibili con la versione attuale tutta una serie di tool che erano stati sviluppati per
ESC/Java: Calvin un tool per la verifica di invarianti all’interno di programmi Java multithreaded; Race Condition
Checker per Java (RCC/Java) un tool che identifica staticamente le potenziali condizioni di concorrenza all’interno di
codice Java multithreaded; e infine Houdini un tool in grado di inferire automaticamente alcune annotazioni JML dal
codice Java per assistere i programmatori nell’inserimento
dell’annotazioni da testare con ESC/Java.
Accanto a questi, recentemente sono stati sviluppati alcuni
progetti che estendono ESC/Java2. CnC [7] è un tool che
usa i controesempi generati da ESC/Java2 e li compila in
test case per la verifica dinamica con JUnit. Inoltre esiste
un plugin per Eclipse che permette un integrazione parziale
di ESC/Java2 con l’ambiente di sviluppo di Eclipse [4].
4.1 Architettura e funzionamento del tool
Il funzionamento di ESC/Java2, come accennato in precedenza, si basa sulla tecnica di theorem proving. Ossia si
cerca di dimostrare la correttezza del codice trasformandolo
in un modello logico basato su predicati e verificando la loro
validità. Di seguito sono descritti i passi fondamentali del
funzionamento di ESC/Java2.
3
Gli autori non hanno ancora investigato le problematiche
legate a rendere compatibile ESC/Java2 con Java 1.5.
int Absolute(int x){
if(x>=0) return x;
else return -x;
}
VAR int x@pre IN
{
ASSUME integralGE(x, 0);
RES = x;
[]
ASSUME boolNot(integralGE(x, 0));
RES = integralNeg(x);
};
END;
...
(OR
(AND (>= |x| 0) (EQ |@true| |@true|)
(AND
(NOT (>= |x| 0)
(EQ |@true| |@true|)
)
(EQ |:RES| (- 0 |x|))
...
)
...
Figure 5: Un semplice frammento di codice Java per
il calcolo del valore assoluto di un numero, lo stesso
programma trasformato in Guarded Command, e infine la sua trasformazione nelle condizioni di verifica
passate al theorem prover.
1. A partire dal codice sorgente Java e dalle specifiche
JML incluse nel codice, viene generato un set di predicati da verificare.
2. I predicati, insieme ad un modello logico del funzionamento di Java, vengono elaborati da un theorem prover,
il quale verifica che le condizioni sussistano o in caso contrario genera un controesempio che indica un
potenziale bug.
3. I controesempi vengono elaborati e trasformati in avvisi
di possibili errori con riferimento ai punti del codice
interessati dal problema.
Il primo passo del processo di verifica, prevede più passaggi di semplificazione per arrivare alla generazione finale dei
predicati da passare al theorem prover. Inizialmente il codice
Java/JML viene trasformato in una forma semplificata del
linguaggio Guarded Command di Dijkstra, che viene ulteriormente ridotta passando alla versione core di Guarded
Command, in fine da questa forma viene generato l’insieme
di condizioni da passare al theorem prover. ESC/Java, come
mostrato in figura 5, permette di ottenere, tramite linea di
comando, sia la trasformazione in Guarded Command (escj
-pgc codice.java), che in condizioni di verifica (escj -v
-ppvc codice.java).
Il theorem prover usato, Simplify, accetta come input una
sequenza di formule del primo ordine e cerca di provare la
validità di ognuna di esse. Simplify non implementa un procedura di decisione per i suoi input: a volte non è in grado di
dimostrare che una formula valida sia effettivamente valida.
Tuttavia è conservativo, ossia non dice mai che una formula non valida sia valida. Simplify per provare una formula
F , procede per assurdo, ossia cerca di dimostrare che ¬F
non è valida. Se ¬F è valida, allora Simplify assume che F
non lo sia e produce un controesempio, ossia un insieme di
formule atomiche che ritiene rendano invalida la formula F .
Purtroppo non sempre il controesempio prodotto è corretto
ed è, data la sua verbosità, di difficile comprensione e quindi
di scarsa utilità per individuare facilmente una soluzione al
problema riscontrato.
while(e) c;
if(e){ c; while (e) c; }
if(e){ c; if (e) {assume false;} }
Figure 6: Un frammento semplificato di codice Java contenente un ciclo while, la sua trasformazione
mediante faithful unrolling e, infine, nell’effettivo
unrolling usato da ESC/Java2.
variante, ma sicuramente fa sı̀ che molti errori possano non
essere trovati. In alternativa è possibile specificare il numero di volte che si vuole “srotolare” il ciclo, questo permette di aumentare il numero di possibili errori trovati, ma
comunque non è in grado di garantire che tutti i possibili
bug possano essere trovati. In figura 6 è mostrato come
ESC/Java2 gestisce l’unrolling dei cicli, ossia in modo non
faithful (fedele): il ciclo più interno che si ottiene con il faithful unrolling viene sostituito con un assunzione di falsità. E
quindi su quel ramo del ciclo non avviene più alcuna verifica.
Questo dimostratore, già usato per nei tool ESC/Modula3 e ESC/Java, pur essendosi dimostrato un dimostratore
di logica del primo ordine sufficientemente robusto e pur
essendo disponibile per diverse piattaforme, soffre di parecchie limitazioni dovute al fatto che è basato su un vecchie
metodologie per i dimostratori e che il suo codice non è più
mantenuto da anni. Tra le limitazioni più evidenti di Simplify ci sono: la sua incapacità di gestire operazioni, se non
le più semplici, che non usino numeri interi e alcuni limiti
nell’uso dei quantificatori.4
Inoltre, come altri tool5 ESC/Java permette, tramite la notazione JML, di specificare le invarianti di ciclo e di verificare automaticamente che le invarianti sussistano per tutte
le possibili iterazioni del ciclo e quindi di gestire i cicli in
maniera sound, srotolando i cicli un numero di volte pari a
quello massimo possibile secondo le invarianti fornite. Questo
oltre a richiede l’interazione del programmatore, causa anche
un costo computazionale maggiore per la verifica da parte
del theorem prover.
L’architettura e i principi di funzionamento di ESC/Java, gli
consentono di agire modularmente su ogni metodo presente
nel codice anziché sull’intero programma. Ovviamente per
trarre i maggiori vantaggi da ciò è necessario che vengano
specificate le precondizioni e le postcondizioni JML per i
metodi chiamati da altri metodi.
La logica di verifica con cui sia ESC/Java che ESC/Java2
sono stati implementati non identifica ne tutti i potenziali errori (e.g. perché non tutti gli aspetti di Java sono modellati
dalla logica usata) ne evita tutti possibili falsi allarmi (e.g.
a causa delle limitazioni del dimostratore di teoremi usato).
Queste limitazioni sono giudicate nello stato dell’arte della
verifica statica di codice, un buon ed efficace compromesso
per mantenere comunque i tool efficienti ed usabili. Un modello logico completo che rappresenti fedelmente la logica di
comportamento di Java – senza considerare alcuni problemi
di indecidibilità nel suo comportamento – richiederebbe un
sforzo computazionale al theorem prover tale da rendere il
tool molto più lento e quindi meno usabile dello stato attuale
senza per altro aumentarne significativamente l’efficacia.
Le considerazioni espresse prima si possono riassumere dicendo che ESC/Java2 è unsound e incomplete: il tool potrebbe
non rilevare un errore effettivamente presente (unsoundness)
e inoltre potrebbero generare avvisi di errori che in realtà
non sono presenti (incompleteness).
4.2
Eccezioni a runtime (Cast, Null, NegSize, IndexTooBig,
IndexNegative, ZeroDiv, ArrayStore).
Il messaggio Cast viene generato quando ESC/Java2 non
può provare che una ClassCastException non verrà generata. Ad esempio, il seguente codice:
1
2
3
4
5
Una delle cause di unsoundness di ESC/Java 2 è la modalità
con cui vengono gestiti i cicli. Il comportamento predefinito
di ESC/Java per gestire i cicli semplicemente “srotola” il
ciclo una sola volta. Questo semplifica il compito del programmatore a cui non è richiesto di specificare alcuna in4
Uno degli sviluppi futuri del progetto è quello di trovare
un dimostratore alternativo a Simplify di concezione più
moderna e il cui codice sia ancora supportato.
Il tool in pratica
ESC/Java2 è in grado di rilevare diverse tipologie di errori:
possibili eccezioni a runtime, possibili violazioni nelle specifiche JML delle funzioni, violazioni delle condizioni “non
null”, errori nei cicli e nei flussi, possibili violazioni nelle
specifiche delle classi, problemi legati alle eccezioni e qualche
problema legato al multithreading.
public class CastWarning {
public void m(Object o) {
String s = (String)o;
}
}
genera il seguente warning:
CastWarning.java:3: Warning: Possible type
5
LOOP e JACK sono due tool semiautomatici per la verifica
statica che, nel caso dei cicli, richiedono sempre all’utente
di specificare le invarianti.
cast error (Cast)
String s = (String)o;
^
//@ ensures \result > 0;
public int mm() {
int j = m(-1);
}
//@ ensures \result > 0;
public int mmm() {
int j = m(0);
return j;
}
5
6
7
8
Mentre il seguente codice non produce alcun warning:
9
10
11
1
2
3
4
5
6
7
public class CastWarningOK {
public void m(Object o) {
if (o instanceof String) {
String s = (String)o;
}
}
}
12
13
14
genera i seguenti messaggi:
PrePost.java:7: Warning: Precondition possibly
not established (Pre)
int j = m(-1);
^
Associated declaration is "PrePost.java",
line 2, col 4:
//@ requires i >= 0;
^
PrePost.java:8: Warning: Postcondition possibly
not established (Post)
}
^
Associated declaration is "PrePost.java",
line 5, col 4:
//@ ensures \result > 0;
^
PrePost.java:13: Warning: Postcondition possibly
not established (Post)
}
^
Allo stesso modo, quando il tool non può provare che non
verrà lanciata una NullPointerException genera un Null
warning:
1
2
3
4
5
public class NullWarning {
public void m(Object o) {
int i = o.hashCode();
}
}
genera il seguenta warning:
NullWarning.java:3: Warning: Possible null
dereference (Null)
int i = o.hashCode();
^
Associated declaration is "PrePost.java",
line 9, col 4:
//@ ensures \result > 0;
Mentre il seguente non produce alcun segnale:
1
2
3
4
5
public class NullWarningOK {
public void m(/*@ non_null */ Object o) {
int i = o.hashCode();
}
}
Il primo warning indica come secondo le specifiche JML,
l’argomento passato alla funzione m() debba essere maggiore
o uguale a 0 mentre in questo caso è negativo. A causa di
questo errore anche le post condizioni della funzione mm()
non sono rispettate. Lo stesso di scorso vale per la funzione
mmm() dove la postcondizione //@ ensures \result > 0; è
violata dato che il valore ritornato dalla funzione è 0.
Negli altri casi: ZeroDiv viene sollevato quando un denominatore di una divisione può essere 0, NegSize quando la
dimensione di un array durante la sua allocazione potrebbe
essere negativa, IndexNegative quando l’indice di un array
potrebbe essere negativo IndexTooBig quando l’indice di un
array potrebbe più grande della dimensione effettiva dell’array.
Specifiche JML delle funzioni (Precondition, Postcondition, Modifies).
Questi warning vengono generati in risposta alle precondizioni (requires) e alle postcondizioni (ensures, signals)
annotate dagli utenti. Ad esempio il seguente codice:
Il warning Modifies indica un tentativo di assegnare un valore a un campo di un oggetto che viola la clausola modifies.
Condizioni JML “non null” (NonNull, NonNullInit).
I campi delle classi dichiarati non null devono essere inizializzati a valori non nulli in ogni costrutturo altrimenti viene
prodotto un messaggo NonNullInit.
1
2
3
1
2
3
4
public class PrePost {
//@ requires i >= 0;
//@ ensures \result == i;
public int m(int i);
}
4
public class NonNullInit {
/*@ non_null */ Object o;
public NonNullInit() { }
}
genera i seguenti messaggi:
NonNullInit.java:4: Warning: Field declared
non_null possibly not initialized (NonNullInit)
public NonNullInit() { }
^
Associated declaration is "NonNullInit.java",
line 2, col 6:
/*@ non_null */ Object o;
^
Le clausole invariant e constraint generano postcondzioni
aggiuntive per ogni funzione, mentre initially genera postcondzioni aggiuntive per ogni costruttore. Se queste condizioni non sussistono, i messaggi di errore appropriati vengono generati:
1
2
Un messaggio NonNull viene generato dal tool ogni volta
che viene vatto un assegnamento a un campo o una variabile
dichiarata non null ma ESC/Java2 non può determinare se
il valore assegnato è nullo o meno.
3
4
5
6
7
8
1
2
3
4
public class NonNull {
/*@ non_null */ Object o;
public void m(Object oo) { o = oo; }
}
genera:
NonNull.java:4: Warning: Possible assignment of null
to variable declared non_null (NonNull)
public void m(Object oo) { o = oo; }
^
Associated declaration is "NonNull.java",
line 2, col 6:
/*@ non_null */ Object o;
^
Cicli e flussi (Assert, Reachable, LoopInv, DecreasesBound).
Questi avvisi sono generati da violazioni delle specifiche contenute nei corpi delle routine (Assert, Reachable) o dei cicli
(LoopInv, DecreasesBound). Ad esempio il warning Assert
viene restituito quando una annotazione assert potrebbe
non essere soddisfatta.
1
2
3
4
5
6
7
8
public class AssertWarning {
//@ requires i >= 0;
public void m(int i) {
//@ assert i >= 0;
--i;
//@ assert i >= 0;
}
}
genera:
AssertWarning.java:6: Warning: Possible
assertion failure (Assert)
//@ assert i >= 0;
^
Dato che nel caso il valore di i sia 0, dopo l’istruzione --i
il nuovo valore sarebbe -1 in contraddizione con l’ultima
asserzione //@ assert i >= 0;.
Specifiche JML delle classi(Invariant, Constraint, Initially).
9
public class Invariant {
public int i,j;
//@ invariant i > 0;
//@ constraint j > \old(j);
public void m() {
i = -1;
j = j-1;
}
}
produce i seguenti messaggi:
Invariant.java:8: Warning: Possible violation
of object constraint at exit (Constraint)
}
^
Associated declaration is "Invariant.java",
line 4, col 6:
//@ constraint j > \old(j);
^
Invariant.java:1: Warning: Possible violation
of object invariant (Invariant)
public class Invariant {
^
Associated declaration is "Invariant.java",
line 3, col 6:
//@ invariant i > 0;
^
Eccezioni (Exception).
Non tutte le possibili eccezioni possono essere previste da
ESC/Java2, dato che alcune di esse sono da causate da
condizioni che non possono essere modellate. In particolare gli errori Java (i.e. OutOfMemoryError) possono essere
generati in qualsiasi momento e non possono essere previsti
da ESC/Java2.
ESC/Java2 è più rigoroso della semantica dei compilarori
Java e quindi genera un Exception warning se una eccezione
incontrollata può essere esplicitamente lanciata ma non è
dichiarata in nelle throws. Per quanto riguarda le eccezioni
controllate, il tool si comporta pressoché come un compilatore Java.
Multithreading (Race, RaceAllNull, Deadlock).
Questo tipo di messaggi sono causati da potenziali problemi
con i monitor.
Problemi di multithreading causati dall’assenza di alcuna
sincronizzazione non sono rilevati da ESC/Java2.
Il tool è in grado, per esempio, di generare un messaggio
di Deadlock quando ogni thread di un gruppo di thread
ha bisogno di accedere ad un monitor bloccato da un altro thread. La ricerca di deadlock è disabilitata di default,
ma può essere attivata usando l’opzione -warn Deadlock.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DeadlockWarning {
/*@ non_null */
final static Object o = new Object();
/*@ non_null */
final static Object oo = new Object();
//@ axiom o < oo;
//@ requires \max(\lockset) < o;
public void m() {
synchronized(o) { synchronized(oo) { }}
}
//@ requires \max(\lockset) < o;
public void mm() {
synchronized(oo) { synchronized(o) { }}
}
}
genera:
DeadlockWarning.java:11: Warning: Possible
deadlock (Deadlock)
synchronized(oo) { synchronized(o) { }}
^
Le funzionalità di ESC/Java2 descritte mostrano come il
tool trovo facilmente alcuni errori altrimenti rilevabili solo
a runtime (i.e., null pointer, division by zero, index out of
bounds), per gli altri warning molto di pende dalle annotazioni JML: più il codice è annotato più ESC/Java2 è in
grado di fare assunzioni su di esso. Questo però implica
anche che la possibilità di cercare problemi all’interno del
codice si basa fortemente sull’assunzione che le annotazioni
JML siano complete e corrette.
5. JAVA PATHFINDER
Java Path Finder (JPF) [1, 18, 24] è uno strumento per la
verifica, l’analisi e il testing di codice Java. Java Path Finder
è un model checker in grado di eseguire tutte le istruzioni
contenute all’interno del bytecode e dunque è in grado di
analizzare qualsiasi programma scritto in Java.
Prima di descrivere in dettaglio lo strumento Java PathFinder verrà fatta una breve introduzione alle tecniche utilizzate
per fare model checking sui linguaggi di programmazione.
5.1 Model Checking
In letteratura è possibile ritrovare diverse definizioni di model checking, ognuna delle quali pone l’accento su di un aspetto diverso del problema [8]. Qui di seguito sono riportate alcune definizioni di model checking che possono essere
ritrovate in letteratura:
• decidere in modo efficiente se una formula logica è
soddisfacibile da un modello di macchina a stati finiti;
• il compito di definire in modo automatico se un sistema
soddisfa una formula temporale [3];
• il grafo dello stato globale di un sistema può essere
visto come una struttura di Kripe finita e un algoritmo efficiente (model checker) può essere utilizzato per
determinare se la struttura è un modello di una formula particolare (i.e., per determinare se un programma
soddisfa le specifiche) [9];
• supponendo che M sia una strutture di Kripe M =
!S, I, R, L", dove S è un insieme di stati, I ⊆ S è l’insieme degli stati iniziali, R ⊆ S × S è la relazione
di transizione e L è una funzione che etichetta gli
stati con preposizioni atomiche prese da un opportuno linguaggio, supponendo che ϕ sia una formula di logica temporale, trovare l’insieme di tutti gli
stati di K che soddisfano ϕ, cioè l’insieme degli stati
{s ∈ S|K, s |= ϕ}.
Ciò che caratterizza tutte queste definizioni è un comune
denominatore: il model checking tradizionale riguarda la
verifica di proprietà temporali di sistemi a stati finiti.
Normalmente il model checking di un software viene effettuato analizzando le proprietà che riguardano le specifiche del software stesso [9]. Le specifiche vengono dapprima rappresentate come una macchina a stati finiti, e solo
successivamente, vengono analizzate con i sistemi di model
checking.
Recentemente è stata invece introdotta una nuova metodologia per l’analisi del software: il model checking non viene più
effettuato sulle specifiche, bensı̀ sul codice che implementa
il software. L’obiettivo di tale tecnica è dimostrare che l’implementazione del software mantiene proprietà importanti
e, in particolare, le stesse proprietà che erano state stabilite
nelle specifiche.
È comunque importante notare che contrariamente a quanto accadeva per il model checking tradizionale, nel caso dell’analisi del codice non è necessario costruire il modello del
software poiché questo è fornito gratuitamente dal codice
stesso che rappresenta un modello formale del sistema implementato. Il modello è formale poiché è formale il linguaggio
di programmazione utilizzato per descriverlo.
Esistono due approcci per effettuare il model checking a
partire dal codice [8]:
• l’approccio translation based : cerca di riutilizzare i
sistemi di model checking esistenti. Il codice sotto
analisi viene prima tradotto in una notazione interpretabile dai sistemi di model checking tradizionali e
quindi analizzato;
• l’approccio ad hoc: consiste nello sviluppare sistemi
di model checking che accettano come notazione di
specifica il particolare linguaggio di programmazione
utilizzato per implementare il software.
Il vantaggio più evidente dei sistemi translation based è la
possibilità di poter riutilizzare strumenti e algoritmi molto
efficienti, già studiati a lungo e messi a disposizione dai sistemi di model checking tradizionali. Il difetto più grave di
questo approccio risiede invece in una mancanza di flessibilità,
causata dal fatto che spesso vi sono notevoli differenze semantiche tra i linguaggi di programmazione e i linguaggi di modellazione utilizzati nei sistemi di model checking
tradizionali. Per esempio, i normali linguaggi di modellazione non supportano diverse funzionalità presenti invece
nei normali linguaggi di programmazione (Floating points,
allocazione dinamica della memoria, passaggio di parametri
mediante puntatori). Appare dunque chiaro che se si vuole
effettuare model checking su codice è preferibile un approccio ad hoc piuttosto che un approccio translation based che
non può grantire un’analisi completa ed esaustiva di tutto il
codice.
5.1.1
L’Esplosione degli Stati
Generalmente il model checking comporta l’analisi di un insieme di stati molto elevato. La capacità di elaborare e
gestire un numero elevato di stati ha un effetto diretto sull’efficienza dei sistemi di model checking e ne puó limitare
la capacità di trattare sistemi con un numero elevato di
stati. Il punto debole di tutti i sistemi di model checking,
tradizionali e non, è rappresentato dall’esplosione degli stati.
Difficilmente un sistema può essere modellato mediante un
insieme di stati finito e questo porta ad avere problemi durante la fase di gestione degli stati (memorizzazione, tempi di
ricerca, gestione della memoria). Per limitare questo problema molti sistemi di model checking utilizzano particolari
tecniche chiamate di astrazione o riduzione degli stati.
5.2 Java Path Finder
In Figura 7 è rappresentata la struttura di Java Path Finder. Il cuore della della struttura è rappresentato dalla Java
Path Finder Virtual Machine (JVMJPF ) che ha il compito di eseguire il bytecode. Durante l’esecuzione è possibile
configurare e guidare la JVMJPF attraverso un insieme di
componenti (property checcker e choice generator) e librerie
(library abstraction).
Per svolgere questi compiti, JPF combina tecniche di model
checking tradizionali con tecniche che permettono di trattare
spazi con un numero molto alto o infinito di stati. Queste
tecniche comprendono la symmetry reduction, per ridurre il
numero degli stati trattati, la static analysis, per supportare la riduzione delle transizioni, la state abstracttion, per
astrarre lo spazio degli stati e la runtime analysis utile per
mettere in evidenza blocchi di codice contenenti lock e corse
critiche e perciò potenzialmente dannosi.
5.2.1
Linguaggio e Proprietà Supportate
Come già accennato, la JVMJPF supporta tutto il bytecode
Java e ogni programma scritto utilizzando codice Java puro
può essere analizzato. Purtroppo non tutti i programmi
risultano composti da codice Java puro e spesso certi metodi
sono nativi rispetto al sistema operativo utilizzato. Esempi
di codice nativo sono rappresentati da operazioni sul filesystem, comunicazioni di rete oppure creazione di codice per
applicazioni GUI. Questo significa che ogni volta che all’interno di un programma Java vengono chiamati metodi che
non hanno un bytecode corrispondente, JPF non è in grado
di determinare lo stato di questo codice e di conseguenza
non riesce ad effettuarne l’analisi. A questo proposito JPF
fornisce MJI, un’interfaccia di astrazione in grado di trattare
questo problema (Sezione 5.2.3).
E’ necessario mettere in evidenza che JPF, come succede
anche per gli strumenti di testing, può trattare solamente
sistemi chiusi. Questo significa che JPF effettua il model
checking solamente sul sistema preso in esame e sull’ambiente all’interno del quale viene eseguito. Le analisi di sistemi
esterni non sono ammissibili.
5.2.2
Le Funzionalità di JPF
La funzionalità di base di JPF, funzionalità grazie alla quale
è possibile effettuare il model checking di codice Java, è la
capacità di simulare il non determinismo. Grazie a questa
capacità, JPF funziona come una VM che non solo è in grado
di eseguire un normale programma Java ma è in grado di eseguirlo in tutti i modi possibili. Ovviamente la simulazione
del non determinismo è qualcosa di più di una semplice generazione di stati e per gestirla sono necessarie due tecniche
particolari:
• Backtracking: permette a JPF di ritornare in stati
eseguiti precedentemente per verificare se esistono percorsi di esecuzione inesplorati. La stessa cosa potrebbe
essere effettuata eseguendo ogni volta il programma
dall’inizio. Con il backtracking si ha maggiore efficienza e la gestione degli stati può essere ottimizzata;
• State Matching: permette a JPF di evitare lavoro
inutile riducendo il numero di stati da controllare. Durante l’esecuzione di un programma, JPF controlla se
ogni nuovo stato generato è uguale ad uno stato precedentemente analizzato. Se questo accade significa che
non è necessario proseguire nuovamente lungo quel persorso di esecuzione e JPF può fare backtracking e proseguire lungo un percorso non ancora esplorato.
Poiché anche Java Path Finder incorre nei problemi di gestione degli stati propri di ogni sistema di model checking,
JPF dispone di un insieme di meccanismi che sono in grado di mantenere sotto controllo l’eslosione degli stati. JPF
affronta questo problema:
• Riducendo il costo di memorizzazione di ogni
stato. Pur non essendo la tecnica principale per affrontare l’esplosione degli stati è comunque necessario
disporre di meccanismi che permattono di utilizzare
in modo efficiente la memoria per il salvataggio degli
stati. Poiché una transizione di stato solitamente comporta piccoli combiamenti, JPF utilizza tecniche di
state collapsing che permettono di ridurre lo spazio
necessario alla memorizzazione degli stati;
• Utilizzando strategie di ricerca configurabili.
Normalmente non è possibile limitare il numero di risorse
utilizzate indirizzando la ricerca all’intero spazio degli
stati. JPF permette di risolvere il problema utilizzando il model checker non come un dimostratore ma,
bensı̀, come un debugger. In questo modo è possibile utilizzare euristiche che permettono di impostare
proprietà sugli stati in modo da poterli ordinare e filtrare durante l’analisi di un percorso di esecuzione. La
definizione delle euristiche e della proprietà non fanno
parte del core di JPF ma devono essere costruite dagli
utenti attraverso particolari classi di configurazione
descritte in Sezione 5.2.3;
data/scheduling
heuristics
VM observer
verification target
(Java bytecode program)









state
mgnt














verification report













-3)SGI
Figure 7: Struttura di Java Path Finder
tale analisi possono ritenersi validi indipendentemente dai valori di ingresso del programma. Al
fine di ridurre il numero degli stati, JPF utilizza tre tipi di analisi statica: i) la static slicing,
che dato un programma e un insieme di criteri di
slice genere un programma più piccolo che risulta
funzionalmente equivalente al programma originario rispetto ai criteri di slice (JPF utilizza il
tool di slicing di BANDERA); ii) la partial evaluation, che propaga i valori costanti e semplifica le
espressioni presenti nel codice; la iii) partial order
computation che si occupa di identificare statement che possono essere incrociati in modo sicuro
con statement presenti in altri thread;
– Runtime Analysis: l’analisi dinamica viene effettuata eseguendo un programma una sola volta
per poi osservarne le tracce di esecuzione generate in modo da riuscire ad estrarre da esse informazione riguardanti il programma sotto analisi.
Queste informazioni potranno poi essere utilizzate per predire se differenti esecuzioni dello stesso programma possono violare i vincoli imposti
al sistema. JPF utilizza due algoritmi di analisi dinamica: i) il data race detection [21], che
permette di rilevare quando si verificano accessi concorrenti ai dati che possono generare corse
critiche; ii) il deadlock detection [24] che invece
permette di rilevare le situazioni di deadlock nelle
quali possono incorrere thread differenti.
• Riducendo il numero degli stati che devono essere
memorizzati in modo da aumentare la scalabilità del
sistema. Questa riduzione è supportata da quattro
meccanismi [24]:
– Symmetry Reduction: l’idea su cui poggia questo
meccanismo è che la simmetria tra gli stati di un
sistema produce una relazione di equivalenza e
che durante l’analisi uno stato può essere scartato se uno stato equivalente è già stato analizzato.
JPF utilizza tecniche di partial order reduction
che attraverso l’analisi del bytecode sono in grado di valutare la simmetria tra gli stati. Tale
simmetria può essere calcolata sia per gli stati instanziati nella memoria statica che per quelli instanziati nella memoria dinamica. Oltre all’analisi del bytecode, la simmetry reduction si basa anche sul garbage collector che permette a JPF di
eliminare dalla memoria dinamica tutti gli stati
che sono generati da oggetti non più referenziati;
– State Abstraction: attraverso questo metodo,
un utente può specificare funzioni di astrazione
su una o più parti del sistema sotto osservazione.
Partendo da queste funzioni, un generico sistema
di model checking ha a disposizione due scelte: i)
generare on-the-fly, durante l’esecuzione del model checking, il grafo degli stati sui dati astratti;
ii) generare un sistema astratto che manipola i
dati astratti e che a sua volta deve essere modellato per poi essere sottoposto a model checking.
Poichè le astrazioni vengono solitamente fatte su
piccole parti del sistema sotto analisi, JPF utilizza il secondo approccio e genera un programma astratto il quale sarà poi sottoposto a model
checking;
– Static analysis: l’analisi statica viene effettuata analizzando i programmi senza eseguirli e senza fare particolare assunzioni sui dati di ingresso
del programma. Per questo motivo i risultati di
5.2.3
Estendibilità di JPF
Un’ulteriore caratteristica di JPF risiede nel fatto che esso è
stato progettato in modo tale da favorirne l’estendibilità da
parte degli utenti. Questo rende JPF un sistema di model
checking adattabile e scalabile. L’estendibilità viene fornita
attraverso due meccanismi:
• I Search-/VMListeners. Possono essere utilizzati per estendere il modello interno di JPF e danno
trazione del codice e che sia in grado di fornire modelli specializzati secondo le proprietà che vanno verificate. Un interessante funzionalità offerta da Bandera è quella di fornire
controesempi che mostrino la violazione di una determinata
proprietà.
I componenti principali di Bandera sono:
• Slicer che comprime i percorsi nei programmi rimuovendo punti di controllo, variabili e dati che sono irrilevanti per la verifica di una data proprietà.
• Abstraction-Based Specializer che consente all’utente
di ridurre la cardinalità dei dati associati con le variabili.
Figure 8: Struttura generale di Bandera
agli utenti la possibilità di controllare proprietà complesse degli stati, configurare le strategie di ricerca o
raccogliere statistiche sull’esecuzione dei programmi.
L’estensione avviene attraverso il pattern degli observer che permette di sottoscriversi ad eventi generati
all’interno di JPF. Questo meccanismo permette, ad
esempio, di ricevere notifiche quando vengono eseguite
particolari istruzioni nel bytecode o quando avviene il
passaggio tra due diversi stati del sistema sotto osservazione;
• La Model Java Interface (MJI). Questo è un meccanismo che permette di controllare la comunicazione
tra il codice eseguito all’interno dell JVMJPF e quello che invece è eseguito dalla virtual machine esterna (quella che esegue JPF). MJI può essere utilizzato
per realizzare astrazioni delle librerie Java in modo da
contenere la generazione degli stati e superare alcuni dei problemi di gestione del codice Java non puro
evidenziati nella Sezione 5.2.2.
• Back End che genera il BIR: un linguaggio intermedio di basso livello basato su comandi condizionali che
genera un’astrazione dei comuni linguaggi di input per
model checker. Il Back End contiene un traduttore per
ogni model checker supportato
• User Interface che facilita l’interazione con le varie
componenti e mostra i controesempi all’utente in termini di programma sorgente, in maniera simile ad un
debugger.
La fase di astrazione del codice è caratterizzata da:
• Eliminazione di componenti irrilevanti. Molte delle
componenti dei programmi possono essere non rilevanti per la proprietà che deve essere verificata.
• Astrazione dei dati. Dopo l’eliminazione delle componenti non rilevanti, alcune delle restanti variabili,
sebbene rilevanti, possono contenere più dettagli del
necessario per la proprietà da verificare. È spesso possibile generare una nuova variabile che è un’astrazione
della precedente e quindi più piccola. Per esempio un
applicazione potrebbe memorizzare un insieme di item
in un vettore, ma la proprietà da verificare dipende
solo dalla presenza o meno nel vettore di un particolare item, in questo caso è possibile atrarre l’insieme
dei possibili stati del vettore in un insieme più piccolo
{ItemInVector, ItemNotInVector}.
6. BANDERA
Bandera [6] è un tool per la verifica di codice Java attraverso tecniche di model checking. La Figura 8 mostra la struttura generale di Bandera. Molti tools per la verifica del
codice traducono un intero programma direttamente in un
linguaggio di analisi. L’approccio di Bandera è invece quello
tipico del model checking cioè quello di costruire una versione semplificata/astratta del codice su sui applicare le tecniche di model checking. In particolare Bandera effettua
una traduzione del codice in un linguaggio intermedio considerando le specifiche proprietà che si vogliono verificare per
il codice e quindi supporta una mappatura di questo codice
su diversi linguggi di input per model checker.
Bandera fornisce un supporto automatico per la costruzione
di questo modello. Inoltre il tool consente una mappatura automatica dall’“error trace” espresso nel linguaggio del
model checker sul codice sorgente java ed una visualizzazione
grafica di questa traccia.
Lo scopo di Bandera è quindi quello di fornire un tool per
la verifica di codice che sfrutti i modelli per model checking
già esistenti, che fornisca un supporto automatico per l’as-
L’architettura di Bandera è simile ad un compilatore ottimizzante. Utilizza diversi linguaggi intermedi per il passaggio dal codice Java a quello di input per model checker.
6.1
Descrizione dettagliata del tool
In questo paragrafo verrà mostrato maggiormente in dettaglio il funzionamento di Bandera. Figura 9 mostra la
struttura dettagliata di Bandera.
6.1.1
Linguaggi di rappresentazione intermedi
Figure 9: Struttura dettagliata di Bandera
I principali sono due: uno di alto livello chiamato Jimple
e sviluppato in [23] ed uno di basso livello chiamato BIR
(Bandera Intermediate representation).
È stato sviluppato un front-end chiamato JJJC (Java-toJimple-to-Java Compiler) che mantiene una corrispondenza
fra il codice sorgente Java e la sua rappresentazione in Jimple. Analoga corrispondenza è mantenuta fra il codice in
Jimple ed il codice in linguaggio BIR che si pone fra il linguaggio Jimple e quelli di input ai model checkers e risulta
particolarmente utile nella generazione dei controesempi.
6.1.2
Slicer
Le proprietà da verificare per un determinato codice possono
essere considerate i criteri in input alla fase di riduzione e
sono definite dall’utente attraverso ad esempio formule in
logica temporale. Bandera offre un menu di template da cui
è possibile scegliere.
di un’astrazione, l’abstraction engine trasformerà il codice
sorgente in una versione specializzata. Riprendendoo l’esempio dove un vettore è astratto in due stati ItemInVector
e ItemNotInVector, è chiaro che l’operazione di astrazione
può indurre dei problemi di non determinismo (ad. es. se è
coinvolta in una decisione la lunghezza del vettore). Le situazioni di non determinismo che vengono gestite sono legate
ad informazioni non interessanti (ad esempio una scelta condizionale sulla lunghezza del vettore) ed è lo specialization
engine che in automatico si occupa della loro gestione. In
questa fase di astrazione è richiesta la collaborazione dell’utente che può scegliere da una libreria di template di
astrazioni come effettuare l’astrazione di determinate variabili. La propagazione di questa scelta nel codice è compito
del sistema. Usando il Bandera Abstraction Specification
Language (BASL) [13] è possibile definire nuove astrazioni.
6.1.4
Back-end
Il compito del modulo Slicer è proprio quello di ridurre il
codice Java in base alle proprietà da verificare per consentire delle prestazioni accettabili ai model checker. L’effettiva
riduzione del codice dipende dalla struttura del programma.
In alcune situazione la riduzione è davvero notevole, in altri
casi in cui i compomenti sono strettamente legati e le proprietà da verificare interessano larga parte di codice la riduzione
è molto minore. In ogni caso il costo computazionale per la
riduzione è molto minore di quello per il model checking
e questa operazione è totalmente automatica, in Bandera
viene sempre effettuata questa operazione.
Questo modulo è simile ad un generatore di codice, in input
accetta il codice (ridotto ed astratto) e produce un output
in un linguaggio di input per model checker. I componenti
di questo modulo comunicano attraverso il BIR (Bandera
Intermediate representation). Come mostrato in Figura 9 il
modulo BIRC genera la rappresentazione BIR a partire dalla
rappresentazione in Jimple. Un traduttore ad-hoc poi a partire dal BIR genera l’input per il verificatore scelto. Lo scopo
del BIR è essenzialmente quello di garantie l’estendibilità di
Bandera verso nuovi strumenti di verifica in quanto fornisce
una semplice interfaccia per generare nuovi traduttori. L’uso del BIR facilita poi la generazione dei controesempi per
le proprietà violate.
Formalmente, dato un programma P e alcune espressioni
di interesse C = {s1 , ..., sk } che definiscono un criterio di
riduzione, un Program Slicer genererà una versione ridotta
di P rimuovendo tutte le parti di P che non sono coinvolte
nei criteri espressi da C. A seguito della riduzione è stato
dimostrato che una specifica Φ vale per P se e solo se Φ vale
per la versione ridotta di P [12]
7.
CONCLUSIONI
Bandera Abstraction Based Specializer
L’articolo ha presentato un insieme di tool per l’analisi e la
verifica di codice Java. La varietà degli approcci alla base
dei tool presentati mette in evidenza la complessità del problema affrontato: non esistono tecniche che garantiscano una
soluzione completa al problema dell’analisi e della verifica di
codice Java al fine di scoprire potenziali errori. Per questo
motivo è compito del programmatore interpretare il problema da risolvere e combinare le varie tecniche in modo da
trovarne una soluzione esaustiva.
Questo modulo consente la riduzione del codice attraverso
un’astrazione delle variabili. Data una appropriata definizione
Infatti, è sı̀ vero che tutte le strategie adottate puntano a
Costruire un tool automatico per la riduzione del codice Java è piuttosto costoso, ma Bandera sfrutta i risultati già
ottenuti in [11, 14].
6.1.3
risolvere il problema della rilevazione degli errori nel codice
ma non per questo portano tutte alla medesima soluzione
in termini di potere riconoscitivo. In parte i risultati di
strumenti diversi si sovrappongono, ma in generale questi
non sono sostituibili l’uno con l’altro. Gli strumenti si completano a vicenda ma purtroppo, anche applicandoli tutti
insieme, non sono in grado di garantire l’assenza totale di
errori.
La nostra analisi ha messo in evidenza pregi e difetti di
cinque tool per l’analisi e la verifica di codice Java. Partendo
da questa analisi possiamo affermare che:
• FindBugs & PMD analizzano il bytecode (FindBugs) e il codice sorgente (PMD). Entrambi si prestano
molto bene all’integrazione con il processo di compilazione del codice e permettono un’analisi veloce di
intere applicazioni e di intere librerie.
• ESC/Java2 permette di analizzare codice Java standard, anche di dimensioni elevate. Il vero potere del
tool viene però raggiunto solo annotando opportunamente il codice con JML. Esc/Java2 si presta soprattutto per l’analisi di porzioni limitate di codice e pertanto è consigliabile utilizzarlo per analizzare quelle
parti di codice ritenute critiche.
• Java PathFinder è uno strumento in grado di effettuare model checking su programmi Java. Java PathFinder ricade nella categoria dei model checker ad hoc, ed
è utilizzabile in modo efficiente sopratutto se ne vengono create delle apposite estensioni attraverso MJI e
listener. Data la complessità dello strumento, l’utilizzo di JPF è riservato a programmatori esperti. Come
ESC/Java2, anche Java PathFinder si presta per analisi limitate del codice (i.e., programmi con un numero
massimo di 10000 linee di codice).
• Bandera ricade nella categoria dei model checker translation based e utilizza meccanismi di riduzione e astrazione per preparare e ottimizzare codice Java in
modo da adattarlo a diversi model checker (es: SPIN e
NuSMV). Bandera interpreta gli error trace dei model
checker ed è in grado di fornire controesempi. Contrariamente a Java PathFinder, Bandera dispone di
un’ottima interfaccia grafica che ne facilita l’uso.
Concludendo, possiamo affermare che tutti gli strumenti
analizzati sono efficienti ed effettivamente sono in grado di
rilevare potenziali problemi nel codice. Ciò che li accomuna in negativo è la presenza di problemi nella generazione
dell’output.
Spesso producono una quantità di messaggi e indicazioni
(nei tools basati su model checkers in misura minore ed indotti dalla necessità di costruire un’astrazione del codice
per evitare che la complessità esploda). Questo comporta
che l’utente medio percepisca addirittura il tool stesso come
uno strumento di bassa qualità, poiché da un lato spesso
non è in grado di interpretare correttamente tutte le informazioni mostrate mentre dall’altro lato bisogna ammettere
che il problema della generazione di false positives è reale e
può compromettere la qualità del risultato.
La strada attualmente seguita per migliorare sia l’efficienza dell’analisi stessa che la generazione dell’output consiste
nell’inserire annotazioni all’interno del codice sotto analisi.
Ovviamente questo comporta un notevole sforzo da parte
dell’utente e addirittura introduce una nuova dimensione di
complessità nella manutenzione di codice e annotazioni.
Altre strade che potrebbero essere intraprese sono il miglioramento degli stessi algoritmi di analisi e l’introduzione di
tecniche di filtraggio dell’output. La prima dovrebbe puntare
alla radice del problema, ottimizzando le informazioni generate, mentre la seconda dovrebbe cercare di sintetizzare e
calibrare l’output in base alle esigenze degli utenti.
8.
REFERENCES
[1] The JPF Runtime Verification System.
http://javapathfinder.sourceforge.net/JPF.pdf.
[2] Apache Software Foundation. The byte code
engineering library. http://jakarta.apache.org/bcel/,
June 2005.
[3] R. Cleaveland and S. Smolka. Strategic directions in
concurrency research. ACM Computing Surveys,
28(4):607 – 625, 1996.
[4] David Cok. Esc/java2 eclipse plugin project.
http://sort.ucd.ie/projects/escjava-eclipse/.
[5] David R. Cok and Joseph Kiniry. Esc/java2: Uniting
esc/java and jml. In Gilles Barthe, Lilian Burdy,
Marieke Huisman, Jean-Louis Lanet, and Traian
Muntean, editors, Construction and Analysis of Safe,
Secure, and Interoperable Smart Devices, International
Workshop, CASSIS 2004, Marseille, France, March
10-14, 2004, Revised Selected Papers, volume 3362 of
Lecture Notes in Computer Science, pages 108–128.
Springer, 2004.
[6] J.C. Corbett, M.B. Dwyer, J. Hatcliff, S. Laubach,
C.S. Pasareanu, and R.H. Zheng. Bandera: extracting
finite-state models from java source code. In Proc. of
International Conference on on Software Engineering,
(ICSE), pages 439–448, Limerick, Ireland, 2000. ACM.
[7] Christoph Csallner and Yannis Smaragdakis. Check
’n’ Crash: Combining static checking and testing. In
Proceedings of the 27th international conference on
Software engineering, pages 422–431, May 2005.
[8] Giovanni Denaro. A comparison between symbolic
execution based verification and model checking. Technical report, Università degli Studi di Milano - Bicocca,
http://www.lta.disco.unimib.it/doc/ei/pdf/lta.2002.03.pdf,
2002. Internal Report.
[9] E. Emerson E. Clarke and A. Sistla. Automatic
verification of finite-state concurrent systems using
temporal logic specifications. ACM Transactions on
Programming Languages and Systems, 8(2):244 – 263,
April 1986.
[10] Cormac Flanagan, K. Rustan M. Leino, Mark
Lillibridge, Greg Nelson, James B. Saxe, and Raymie
Stata. Extended static checking for java. SIGPLAN
Not., 37(5):234–245, 2002.
[11] J. Hatcliff, J.C. Corbett, M.B.Dwyer, S. Sokolowski,
and H.Zheng. A formal study of slicing for
multi-threaded programs with jvm concurrency
primitives. In Proc. of Int. Static Analysis Symposium
(SAS), 1999.
[12] J. Hatcliff, M.B.Dwyer, and H.Zheng. Slicing software
for model construction. Higher-order and Symbolic
Computation, 2000.
[13] J. Hatcliff, M.B.Dwyer, and S.Laubach. Staging static
analysis using abstraction-based program
specialization. In Proc. of Int. Symposium on
Prociples of Declarative Programming (PLILP), 1998.
[14] S. Horwitz, T. Reps, and D. Binkley. Interprocedural
slicing using dependence graphs. ACM Transaction on
Programming Languages and systems, 12(1):26–60,
1990.
[15] D. Hovemeyer and W. Pugh. Finding bugs is easy. In
Proceedings of the Onward! Track of the ACM
Conference on Object-Oriented Programming,
Systems, Languages, and Applications (OOPSLA),
2004.
[16] JAVA. http://java.sun.com/.
[17] K. Rustan M. Leino and Greg Nelson. An extended
static checker for Modula-3. In Kai Koskimies, editor,
Proceedings of the 7th International Conference on
Compiler Construction, CC’98, volume 1383 of
Lecture Notes in Computer Science, pages 302–305.
Springer, April 1998.
[18] NASA. Java™path finder.
[19] Bill Pugh and David Hovemeyer. Findbugs - find bugs
in java programs.
http://findbugs.sourceforge.net/index.html, May 2005.
[20] Nick Rutar, Christian B. Almazan, and Jeffrey S.
Foster. A comparison of bug finding tools for java. In
The 15th IEEE International Symposium on Software
Reliability Engineering (ISSRE’04), Saint-Malo,
Bretagne, France, November 2004.
[21] Stefan Savage, Michael Burrows, Greg Nelson, Patrick
Sobalvarro, and Thomas Anderson. Eraser: A
dynamic data race detector for multithreaded
programs. ACM Transactions on Computer Systems,
15(4):391–411, 1997.
[22] Sourceforge.net. Pmd. http://pmd.sourceforge.net/,
June 2005.
[23] R. Valle-Rai, L. Hendren, V. Sundaresan, P. Lam,
E. Gagnon, and P. Co. Soot- a java optimization
framework. In Proc. of CASCON, 1999.
[24] Willem Visser, Klaus Havelund, Guillaume Brat, and
SeungJoon Park. Model checking programs. In ASE
’00: Proceedings of the The Fifteenth IEEE
International Conference on Automated Software
Engineering (ASE’00), page 3, Washington, DC, USA,
2000. IEEE Computer Society.
Scarica