5 strumenti per il debugging

Spunti, suggerimenti e storie opinionate di vita vissuta

Penso di trovare il consenso di tutti dicendo che il debugging è una necessità. Penso anche di non offendere nessuno dicendo che debuggare è più un’arte che una scienza. Un’arte molto volubile, considerando che debuggare dipende tantissimo dal contesto in cui si sta lavorando. Ad esempio:

Questi sono solo alcuni dei vari contesti che mi vengono in mente, forse i più comuni. Ma anche senza andare ad elencarli tutti, provate a pensare: cos’è un bug? Per me una definizione abbastanza generica potrebbe essere “un’anomalia nel comportamento atteso dal programma”… ossia il “capitolo bug” è grande tanto quanto il “capitolo programma”. E di rimando, il “capitolo debugging” non sarà molto più piccolo.

Ma quindi, perché provare a fare un elenco dei vari tipi di bug e poi uscirsene a dire che il capitolo (de)bug è immenso? Solo per avvalorare questi due punti:

  1. ogni sessione di debugging è un mondo a parte: le tecniche di debugging più efficaci dipendono esageratamente dallo scenario in questione
  2. e conseguentemente, l’amara realtà che non esiste una soluzione magica che vada bene per tutti: anche la tecnica più banale può avere risultati spettacolari se usata nel giusto contesto!

Ed eccoci quindi arrivati al cuore di questo post (spero di avervi convinti): quali sono le tecniche (e gli strumenti) che possiamo utilizzare per fare debugging? Quali sono i loro pregi e difetti? In quali contesti eccellono?

Dopo quest’ultimo paragrafo altisonante, metto subito le mani avanti e ridimensiono la faccenda: fortunatamente, di strumenti e tecniche per aiutare il debugging ce ne sono tantissimi. Ci sono strumenti molto comuni che diamo per scontati, come per esempio la classica “print” dei linguaggi di programmazione. Ci sono quelli meno comuni che sono estremamente specifici per un particolare tipo di problemi, come per esempio wireshark. E poi ci sono quelli che “non esistono”, o meglio, che esistono solo per un brevissimo lasso di tempo (normalmente il tempo di scovare il bug) e risiedono temporaneamente nell’intimità dell’hard disk di un programmatore, spesso nella forma di scripts/appunti/parsers o simili.

In questo articolo tratterò di una piccolissima parte di questi strumenti e tecniche, per lo più rimanendo nel capitolo “strumenti molto comuni”.

Strumento 1: print

Le print… Penso siano il primo strumento che ogni programmatore usa per avere un po’ di introspezione nel proprio codice. E forse perché sono legate ai primi passi che un programmatore muove nel vasto mondo dell’informatica, le “print” vengono spesso derise ed etichettate come approccio ingenuo e da dilettanti quando è necessario debuggare. Che sia vero o meno, che faccia male all’orgoglio nerd doversi “abbassare ad usare le print”, fatto sta che usarle per debuggare è una tecnica potenzialmente a basso costo ed immensi benefici!

È potenzialmente a basso costo perché aggiungere uno statement di print non è difficile. Però, come ci insegna la vita di tutti i giorni, non possiamo fare i conti senza l’oste! Ad esempio il linguaggio di programmazione utilizzato potrebbe rovinarci la festa: potrebbe esserci la necessità di “ricompilare tutto”…e se il progetto è grosso, la compilazione potrebbe richiedere diverso tempo. Oppure: aggiungere la “print giusta” al primo colpo è certamente veloce…ma quante sono le possibilità che la prima print sia quella giusta? Questo approccio è un processo inerentemente iterativo, e questa iteratività richiede il suo tempo.

Gli immensi benefici? Se si conosce bene la base di codice, si possono inserire delle print in punti strategici che permettono di evidenziare velocemente e palesemente il bug.

Infine, un’altra cosa che viene spesso sottovalutata: le print prese e salvate su file appiattiscono il tempo del programma: ci fanno vedere senza la necessità di ri-eseguire l’intero programma il passato, il presente e il futuro (lasciatemi l’espressione, futuro inteso come “le print successive a quella su cui mi sto focalizzando ora al tempo T e su cui penso si annidi il bug”). Questo appiattimento ci permette di fare ragionamenti su come sta eseguendo il programma vedendo la rappresentazione del suo stato come tante piccole istantanee in evoluzione. Inoltre, le print salvate su file si prestano benissimo ad un post processing, che può essere utilizzato per capire quando è successo qualcosa di male, o quante volte accade una certa situazione… insomma, creare script ad-hoc che aiutano la disinfestazione!

Pros:

Cons:

Strumento 2: log e trace

Abilitare il logging o il tracing di un’applicazione tramite variabili d’ambiente (o opzioni da linea di comando) costa zero… Le difficoltà maggiori sono sapere della loro esistenza e sapere come poi interpretare il risultato!

Log e trace: ma non sono la stessa cosa delle print? Effettivamente, il logging altro non è che “una selezione delle print più belle e utili che sono rimaste nel programma che è andato in produzione”. L’aspetto negativo (e che differenzia il log dalle print inserite manualmente alla bisogna) è che il log tende ad essere più generico e con una risoluzione più grossolana: in fondo, il log non sta andando attivamente a caccia di bug e le informazioni che ci darà difficilmente saranno tanto esaustive da permetterci di mettere all’angolo il bug. Però, i log e i trace hanno un vantaggio non indifferente: sono il distillato di esperienze e necessità passate, e come tali possono contenere tanti spunti utilissimi per indirizzarci sulla giusta strada. Quanto è bello vedere la stampa di log “WARNING: cannot connect to service XYZ, using stub instead” quando qualcosa non sta funzionando? Qualcuno ci sta già dicendo che qualcosa puzza in una precisa zona di codice… e tutto questo senza dover ricompilare nulla!

Un esempio di vita vissuta: logging/tracing di Qt. Esistono delle variabili d’ambiente e/o file di configurazione standard che permettono di modificare il livello di logging dell’applicazione (qualsiasi applicazione che contiene Qt!). Molto utile per scremare i problemi (senza dover ricompilare tutto Qt per aggiungere i simboli di debug oppure 2-3 print specifiche in un punto di codice), però come scrivevo poco sopra, ricordarsi che esistono e ricordarsi cosa fare esattamente per abilitarli è lo scoglio maggiore da superare!

Pros:

Cons:

Strumento 3: opzioni di compilazione

Nel caso ci si stia cimentando con un programma che richiede la compilazione, è probabile che il compilatore offra la possibilità di aggiungere automaticamente del codice che faciliti il contenimento di errori logici (come race conditions, array out of bound, etc), dove con contenimento solitamente si intende “il sollevamento di un errore che blocca l’avanzata del programma nel momento in cui si riscontra l’anomalia logica” (e dove molto spesso questa anomalia logica tende ad essere la causa primordiale di un bug che diventa palese più tardi, e qui la scommessa è che questo “generico bug” sia in realtà il bug in esame e non un altro che è ancora latente nel codice).

Provo a fare qualche nome che dovrebbe essere già noto agli sviluppatori C/C++ (come abilitare queste opzioni dipende dal compilatore specifico):

Nonostante la specificità di questi strumenti, penso sia utile menzionarli perché molto spesso vengono dimenticati in quanto “opzioni non standard” o “di nicchia”.

Pros:

Cons:

Strumento 4: IDE con debugger integrato

Codice, esecuzione e breakpoints in un solo ambiente unificato con l’ergonomia del proprio IDE di fiducia: una goduria.

Onestamente non saprei che altro dire, è la combo perfetta per la gran maggioranza dei casi in cui si vuole veramente indagare passo passo quello che sta succedendo all’interno del nostro programma. Avere una corrispondenza 1:1 del codice sorgente con l’esecuzione del programma (e magari avere anche un po’ di pop up che compaiono, soffermandosi sulle varie variabili scritte direttamente nell’editor per vedere qual è il loro valore attuale) è un aiuto impareggiabile.

Aspetti negativi o limitazioni? Per esperienza personale, ritengo che lo scoglio principale sia l’ergonomia dell’IDE. Penso che le 3 cose più care ad un programmatore siano la propria tastiera, la propria sedia e il proprio editor di fiducia… Imporre l’utilizzo di un IDE perchè ha un debugger integrato chiedendo di tralasciare il proprio editor di fiducia: sì, si può fare una o due volte, ma se l’IDE non è di proprio gradimento è difficile averlo pronto a portata di mano quando serve. Inoltre, può capitare che per progetti particolarmente “fantasiosi e articolati” sia complicato configurare correttamente l’intero ambiente di sviluppo ed esecuzione (penso ad esempio una rete di microservizi, server docker da avviare a runtime, un progetto che utilizza più linguaggi di programmazione differenti).

Forse l’aspetto più limitante nell’utilizzare un debugger integrato in un IDE è la fatica (se non impossibilità) nell’automatizzare gli step di debug… in fin dei conti gli IDE sono per lo più programmi GUI, dove il mouse regna sovrano, mentre l’automazione è dominio di scripts dove la tastiera la fa da padrona.

Pros:

Cons:

Strumento 5: GDB

E quando non si ha a propria disposizione il proprio IDE di fiducia (mai capitato di dover debuggare un programma in un server remoto il cui unico metodo d’accesso è fare 3 hops in ssh?), rimane sempre la possibilità di utilizzare i debugger stand-alone. In ambiente *nix il più famoso è GDB (GNU Project Debugger).

Possiamo descrivere GDB come uno spione invadente che mette a nudo lo stack, lo heap, i threads e più in generale il processo che si vuole osservare. Il suo raggio d’azione è veramente ampio: avendo pieno accesso al programma da debuggare è in grado di:

  1. attaccarsi a processi già in esecuzione
  2. osservare lo stato del programma
  3. modificare l’esecuzione del programma
  4. osservare cosa stava combinando il programma al momento del crash (assumendo di avere a disposizione il file di coredump)

Un’altra flessibilità offerta da GDB è quella di poter automatizzare (tramite scripting) la sessione di debug. Questa feature è impagabile nel caso di bug molto reticenti in cui si capisce perfettamente cosa sta andando storto, ma non si capisce come mai le modifiche apportate non fixino il problema: avere uno script per ri-eseguire con un solo click l’intera sessione di debug è un risparmio in termini di tempo e sanità mentale da non sottovalutare!

Come per la maggior parte dei debugger, GDB permette di interrompere il programma quando si raggiunge una certa istruzione, quando una variabile viene sovrascritta, oppure quando si entra per la settima volta all’interno di un ciclo while. E come per la maggior parte dei debugger, GDB offre diversi front-end grafici, anche se nella sua forma base si presenta come tool da linea di comando. Questa sua forma base è un’arma a doppio taglio: da una parte l’assenza di una GUI rende ostico l’utilizzo del programma per un neofita, dall’altra parte permette ad un utilizzatore esperto di concentrarsi al 100% sul bug invece di doversi preoccupare di dove sono nascoste le varie configurazioni che rendono GDB tanto potente e versatile (mai capitato di raggiungere l’opzione che si stava cercando nascosta dietro 3 menù a tendina diversi, 2 dialog modali e un combo box?).

Come fare per familiarizzare con GDB da linea di comando? Io consiglierei di utilizzarlo per eseguire il post-mortem di un programma crashato. Partendo dal file di coredump (che altro non è che l’istantanea del programma al momento del crash) è possibile curiosare per capire cosa è successo. Sapere dallo stack trace che siamo entrati in un if e poi abbiamo fatto una divisione per zero è utile, ma guardarsi attorno (tra memoria e variabili) e scoprire perché effettivamente siamo finiti in quel branch con il divisore a zero lo è anche di più.

Pros:

Cons:

Guarda anche il webinar si Luca su GDB:

Altri strumenti degni di nota

Mi permetto di menzionare una manciata di altri programmi che sanno rendersi molto utili in certi scenari

Conclusioni

Sono sicuro che ogni programmatore ha nel proprio intimo una storia da raccontare su come è riuscito ad ingegnarsi utilizzando uno strumento per scovare un bug. Con questo post ho cercato di dare qualche spunto e qualche accenno agli strumenti che mi sono capitati tra le mani durante i miei vagabondaggi nelle lande del codice sorgente. Se anche voi avete qualche storia da raccontare o un qualche strumento di debug a voi caro, non siate timidi: lì fuori c’è di sicuro un qualche programmatore ferito e smarrito che aspetta solamente di mettere a frutto la vostra esperienza passata!

Forse ti portrebbe interessare anche questo evento sul debugging del firmware.