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.