01 La guida
Con questo articolo impari a usare il debugger con breakpoint, watchpoint e analisi passo passo per risalire alla causa di un errore. La pratica funziona quando prima riproduci il problema, poi semplifichi il caso e infine osservi stato e flusso fino alla correzione senza creare regressioni.
Cosa significa davvero fare debugging
Nell’ingegneria il debugging indica il processo con cui trovi la causa originaria di un difetto, valuti soluzioni alternative e arrivi a possibili correzioni. Nel software questa attività combina analisi del flusso di controllo, analisi dei file di log, monitoraggio a livello di applicazione o di sistema, dump di memoria e profilazione. Molti linguaggi e ambienti mettono a disposizione programmi di supporto chiamati debugger, pensati per seguire l’esecuzione invece di indovinarla.
La difficoltà cambia molto con la complessità del sistema, con i linguaggi usati e con gli strumenti disponibili, e conta anche l’abilità di chi analizza il problema. I linguaggi di alto livello come Java rendono il lavoro più agevole grazie alla gestione delle eccezioni e al controllo dei tipi, che aiutano a individuare l’origine di un comportamento anomalo. In linguaggi come il C o l’assembly un errore può produrre problemi silenziosi come corruzione di memoria, e in quei casi servono strumenti dedicati alla memoria perché il punto iniziale del guasto resta nascosto.
Perché riprodurre il problema viene prima di tutto?
Il processo inizia normalmente con l’identificazione dei passi che permettono di riprodurre il problema, e questa fase può essere tutt’altro che banale. Con i processi paralleli e con alcuni difetti elusivi la sequenza non si ripete sempre allo stesso modo, e anche l’ambiente specifico dell’utente e la sua cronologia di utilizzo può rendere difficile la riproduzione. Se salti questo passaggio rischi di correggere un sintomo visto una sola volta mentre la causa resta attiva nel codice.
Quando lavori con un’interfaccia grafica puoi provare a saltare alcune interazioni descritte nella segnalazione e verificare se le azioni rimaste bastano ancora a far comparire il difetto. Questo modo di ridurre lo scenario ti dice quali passi contano davvero e quali aggiungono solo rumore. Solo dopo aver ottenuto una riproduzione stabile ha senso passare all’osservazione interna con gli strumenti, perché ogni prova diventa confrontabile con la precedente.
Come semplifichi un caso di test senza perdere il bug?
Dopo la riproduzione conviene semplificare i dati in ingresso del programma per rendere il debug più agevole. Un compilatore può bloccarsi mentre analizza un file sorgente molto grande, ma dopo la semplificazione del caso di test possono bastare poche righe del file originale per ottenere lo stesso arresto anomalo. La semplificazione può avvenire a mano con un approccio dividi e conquista, in cui togli alcune parti del caso originale e controlli se il problema si presenta ancora.
La stessa idea è automatizzata dal delta debugging, che riduce il caso di test in modo sistematico, mentre la tecnica chiamata Saff Squeeze isola il guasto dentro il test con inserimento progressivo delle parti del test che fallisce. Esistono anche tecniche di tracciamento delle catene di causa ed effetto, adattabili a difetti specifici come il dereferenziamento di un puntatore nullo. Se curi tabelle e selezioni nei tuoi programmi, lo stesso ragionamento vale quando studi il ciclo e lettura delle tabelle interne con pochi record di prova prima di rieseguire l’intera selezione.
Breakpoint, watchpoint e passo passo: come li usi?
Il debugging interattivo usa strumenti che permettono di avanzare un passo alla volta e di mettere in pausa l’esecuzione per ispezionare o modificare lo stato. I sottoprogrammi o le chiamate di funzione possono essere eseguiti a piena velocità e messi in pausa al ritorno al chiamante, oppure eseguiti passo passo, oppure trattati con una combinazione delle due modalità. I breakpoint permettono di eseguire a piena velocità il codice non sospetto e di fermarti solo nel punto utile, e la panoramica sul debugging colloca queste pratiche in un quadro più ampio di tattiche e strumenti. Un breakpoint messo subito dopo la fine di un ciclo aiuta a valutare il codice ripetitivo senza fermarti a ogni iterazione.
I debugger permettono di monitorare l’esecuzione, fermarla, riavviarla, impostare breakpoint e cambiare valori in memoria, e il termine indica sia lo strumento sia la persona che svolge l’analisi. I watchpoint lasciano proseguire l’esecuzione fino al cambiamento di una variabile precisa, mentre i catchpoint fermano il debugger per alcuni eventi come le eccezioni o il caricamento di una libreria condivisa. Dopo aver semplificato il caso, puoi esaminare i valori delle variabili e la pila delle chiamate per risalire all’origine, oppure usare il tracciamento con poche istruzioni di stampa che mostrano i valori in punti precisi dell’esecuzione.
Quando bastano stampe e log e quando serve altro?
Il print debugging si basa sull’osservazione di istruzioni di traccia, in tempo reale o registrate, che mostrano il flusso di esecuzione e la progressione dei dati. Puoi usare strumenti specializzati come la traccia di GDB oppure inserire istruzioni di traccia nel codice, pratica detta talvolta printf debugging per l’uso della funzione printf in C. Nelle prime versioni del BASIC per principianti il comando TRON, cioè Trace On, stampava i numeri di riga di ogni istruzione durante l’esecuzione. Il tracciamento delle attività rileva invece quanto tempo di processore viene impiegato in determinati segmenti di codice, spesso espresso come frazione del tempo totale per indirizzi di memoria o moduli, e una quota anomala può indicare una logica errata o allocazione inefficiente da ottimizzare.
Il debugging post mortem analizza il programma dopo l’arresto anomalo con i file di registro, la pila delle chiamate al momento dell’arresto e dump di memoria del processo. Il dump può essere generato automaticamente dal sistema per un’eccezione non gestita, da un’istruzione inserita dal programmatore oppure da un’azione manuale. Per il codice sintatticamente corretto ma sospetto esistono strumenti di analisi statica che cercano insiemi noti di problemi analizzando la semantica e il flusso dei dati più che la sintassi, utili su basi di codice sorgente molto grandi dove la revisione manuale riga per riga non è praticabile. Un esempio tipico è un dereferenziamento che avviene prima dell’assegnazione, e il vecchio programma Unix lint rappresenta un antenato di questa famiglia, soggetta però a falsi positivi. Lo stesso controllo paziente serve quando scorri liste interattive e griglie generate da selezioni ampie e vuoi capire se l’errore nasce nei dati o nella logica di formattazione.
Quali tecniche aiutano con i bug difficili da riprodurre?
Il debugging remoto riguarda un programma in esecuzione su un sistema diverso da quello del debugger, collegato tramite un canale di comunicazione come una rete locale per controllare l’esecuzione e leggerne lo stato. La tecnica record and replay crea una registrazione dell’esecuzione, per esempio con lo strumento libero rr di Mozilla che consente l’esecuzione reversibile, utile per difetti intermittenti, non deterministici e difficili da riprodurre anche da remoto. Il time travel debugging permette di tornare indietro nel codice, per esempio con Undo LiveRecorder, per capire cosa accade durante l’esecuzione, interagire con il programma e osservare come risponde se cambia la sequenza degli eventi.
L’algoritmo detto wolf fence, descritto da Edward Gauss nel 1982, propone di dividere il territorio a metà e di restringere la ricerca dal lato dove si sente ululare il lupo fino a vederlo, e Git lo applica con il comando bisect per trovare quale commit ha introdotto un difetto. La localizzazione dei guasti identifica automaticamente le istruzioni, i metodi o i componenti che possono contenere errori, e gli approcci moderni usano l’apprendimento automatico per migliorare la precisione. La correzione automatica dei bug genera patch senza intervento umano con l’obiettivo di eliminare i difetti senza causare regressioni, mentre il cosiddetto shotgun debugging modifica il codice in modo poco mirato e talvolta crea altri problemi.
Perché non tutti i difetti si correggono subito?
Con i sistemi complessi crescono i metodi per rilevare anomalie, valutarne l’impatto e pianificare correzioni o aggiornamenti completi. Le parole anomalia e discrepanza possono essere usate come termini neutri per evitare l’idea che ogni errore debba essere eliminato a ogni costo. Una valutazione dell’impatto può stabilire se rimuovere un’anomalia è conveniente oppure se una nuova versione pianificata renderà superflua la modifica, perché non tutti i problemi sono critici per la sicurezza o per la missione e una modifica può disturbare gli utenti più del problema noto.
Il termine bug nel senso di difetto risale almeno al 1878, quando Thomas Edison parlava di piccole difficoltà nelle sue invenzioni, e una storia nota degli anni quaranta lega Grace Hopper a una falena rimasta in un relè del Mark II di Harvard. Il termine debugging era usato in aeronautica prima di entrare nel mondo dei computer, con una lettera di J. Robert Oppenheimer del 27 ottobre 1944, un articolo del 1945 sul Journal of the Royal Aeronautical Society sui test dei motori e un articolo di Airforce del giugno 1945 sulle camere per aerei. L’articolo di Gill del 1951 discute gli errori di programmazione senza usare bug o debugging, il termine compare in tre atti degli incontri ACM del 1952 e nel 1963 è abbastanza comune da apparire senza spiegazioni a pagina 1 del manuale CTSS. Nei sistemi embedded la varietà di architetture, fornitori e sistemi operativi rende più difficili il debug e i test, con moduli di debug su chip accessibili tramite JTAG e soluzioni commerciali e prototipi di ricerca, mentre l’anti debugging ostacola l’analisi e l’ingegneria inversa con controlli su API, eccezioni, processi, codice modificato, registri hardware e tempi di esecuzione.
La pagina Wikipedia sul debugging presenta il debugging come processo di ricerca della causa originaria, delle soluzioni alternative e delle possibili correzioni. Vi trovi la storia del termine, tattiche interattive e di analisi, strumenti come debugger e analisi statica e tecniche dal post mortem al bisect. Puoi usarla per confrontare il tuo metodo passo passo con questo quadro e decidere la prossima prova sul tuo codice.
02 Prosegui la lettura
Altre guide della stessa rubrica e dei percorsi vicini.
