MaCocoa 074

Capitolo 074 - Debugging

Introduco brevemente il meraviglioso mondo del Debugger.

Sorgenti: documentazione Apple e non solo.

Prima stesura: 3 settembre 2006.

GDB e XCode

Un debugger è una applicazione di ausilio a chi sviluppa del software; in effetti, una dell occupazioni che impiega maggiormente il tempo di uno sviluppatore è debuggare (verbo bruttissimo che utilizzo in mancanza di un equivalente e significativo verbo italiano), ovvero trovare gli errori logici di esecuzione dell'applicazione. Un debugger serve a mostrare cosa sta succedendo all'interno di un'altra applicazione mentre è in esecuzione, oppure per diagnosticare cosa sia successo negli istanti precedenti ad un crash. Un debugger è quindi una applicazione insostituibile per ogni sviluppatore, che può semplificare il lavoro esaminando una istruzione alla volta. Molti sono usi infarcire il proprio codice con delle scritture su console (che so, attraverso printf o NSLog) per capire cosa sta succedendo in un dato momento dell'esecuzione. Tutto ciò è svolto in maniera egregia e meno invasiva dal debugger, senza problemi.

I primi debugger erano semplicemente un metodo per eseguire le istruzioni dell'applicazione passo-passo, istruzioni che all'inizio erano in linguaggio macchina. In pratica, una sorta di interprete del linguaggio macchina. Il salto successivo, e drastico, è stato quando i debugger hanno cominciato a lavorare direttamente sul codice sorgente, in formato simbolico.

A cosa serve un debugger

Le operazioni principali svolte da un debugger sono: il lancio in esecuzione di una applicazione e l'esecuzione un passo alla volta delle istruzioni che lo compongono; il controllo di esecuzione dell'applicazione, con la possibilità di arrestare l'esecuzione della stessa in occasione di particolari condizioni; l'esame dei valori delle variabili, della memoria e di quant'altro appartenente all'applicazione; la modifica (!) al volo del valore di variabili, del contenuto della memoria e, in tali casi, perfino l'esecuzione di codice diverso da quello dell'applicazione.

All'interno di XCode è presente il debugger GDB (il debugger open-source del progetto GNU). Normalmente, GDB è un debugger che funziona da linea di comando; tuttavia, Apple è così gentile da averlo dotato di una comoda interfaccia grafica (come spesso accade agli strumenti di sviluppo di XCode). Inoltre, lo integra all'interno dell'ambiente di sviluppo in modo da poter tranquillamente dimenticare molte delle peculiarità della linea di comando. Da notare che GDB è in grado di debuggare più o meno tutti i principali linguaggi, con opportune estensioni per ciascuno di essi. Ad esempio, per Objective C, c'è il supporto per gli oggetti e i messaggi verso gli oggetti.

Per poter utilizzare pienamente GDB, occorre in primo luogo che il progetto si compili compiutamente senza errori. Il debugger non serve a trovare gli errori di compilazione, ma quelli di esecuzione. In secondo luogo, occorre che il compilatore produca, durante il processo di compilazione, una serie di informazioni aggiuntive. Questo avviene automaticamente quando il progetto è compilato in modalità Debug o Development. Non avviene (e quindi una sessione di debugging non può aver luogo) se il progetto è compilato in Release o Deployment. La scelta tra questi due modi avviene tramite il menu Project, con la Active Build Configuration. C'è poi un intero menu, Debug appunto, dedicato alle funzioni che si possono svolgere all'interno di GDB. La prima e fondamentale è appunto di attivare il debugger e contestualmente lanciare in esecuzione l'applicazione. A parte il menu, è possibile inserire nella toolbar di XCode una icona apposita, cosa che ho subito fatto, in quanto il Debugger è il secondo strumento più utilizzato dal processo di sviluppo (il primo, ovviamente, il compilatore).

Lanciando l'applicazione, non sembra succedere nulla di diverso da quanto accade lanciando in normale esecuzione l'applicazione, a parte l'apertura di una nuova finestra propria del debugger. Tuttavia, la finestra, divisa in tre sezioni, non presenta informazioni utili. Questo perché non è presente alcun breakpoint.

Non mi rompete

figura 01

figura 01

Un breakpoint (letteralmente: punto di rottura) è il concetto principe del debugger. Si tratta di un punto dove si vuole che il programma in esecuzione si blocchi e sospenda ogni attività; normalmente, un breakpoint coincide con una linea del programma sorgente, opportunamente indicata. L'indicazione di un breakpoint avviene portandosi sulla riga che si intende evidenziare, e utilizzare l'apposita voce di menu (Debug, e poi Add breakpoint...). Dovrebbe comparire, sulla barra sinistra della finestra del codice, una specie di linguetta scura, indicante appunto la presenza di un breakpoint attivo. Alternativamente, il breakpoint si può assegnare facendo clic direttamente un quella zona sulla sinistra della finestra (chiamata Gutter; se non è presente, bisogna attivarla esplicitamente nelle preferenze dell'editor di XCode). Facendo nuovamente clic, la linguetta diventa grigia: significa che il breakpoint è presente, ma non è attivo. L'assegnazione di un breakpoint può avvenire prima di lanciare il debugger, ma anche durante. L'operazione è la stessa, ed il breakpoint ha subito effetto.

GDB esegue l'applicazione normalmente, tranne quando incontra un breakpoint. In tal caso, sospende l'esecuzione dell'applicazione, giusto prima dell'esecuzione dell'istruzione indicata dal breakpoint, e ferma tutto (dove per tutto si intende anche eventuali altri thread o task o quant'altro, anche se questo è un comportamento che dipende da debugger a debugger).

Ed è proprio quando siamo in questo momento sospeso nel tempo di esecuzione dell'applicazione che la potenza del debugger si esplica nella sua pienezza. In primo luogo, si popolano i tre pannelli della finestra del debugger. A meno di configurazione esotiche, nel pannello in basso si trova il codice dell'applicazione, con la riga sospesa bene evidenziata. In alto a destra, sono presentate la variabili in quel momento accessibili all'applicazione (e quindi, le variabili locali, gli argomenti del metodo, e le globali ed altro che non mi interessa). Tra gli argomenti del metodo c'è anche la variabile self, che punta precisamente all'oggetto il cui metodo è attualmente sospeso. Il pannello in alto a sinistra mostra, per i vari thread presenti, qual è la situazione a livello di chiamate, ovvero la catena di metodi e funzioni che hanno portato alla situazione corrente.

Cambiare le variabili

In questa situazione sospesa, si può controllare il valore delle variabili, e verificare se sono consistenti con quello che ci si aspetta a qual punto dell'esecuzione. Ora, molte variabili sono normali variabili (interi, float, eccetera), ma molte altre sono oggetti, o meglio, puntatori ad oggetti. è per questo che nella seconda colonna del pannello delle variabili ci sono valori molto strani (che cominciano con 0x): si tratta degli indirizzi di memoria dove si trovano effettivamente gli oggetti. Queste variabili hanno però un triangolino davanti al nome, che permette di espandere il contenuto dell'oggetto. Ad esempio, espandendo la variabile self, si trovano (oltre a una serie di altre cose sconosciute, evidentemente ereditate dalle varie superclassi) le variabili d'istanza dell'oggetto. Inoltre, per alcuni degli oggetti, quelli di tipo più "semplice" (come NSString) la colonna Summary ne presenta il valore effettivo o comunque una indicazione un po' più interessante del contenuto. Il primo elemento di ogni classe espansa è il campo isa, che identifica il tipo di oggetto (la sua classe). è così possibile verificare durante l'esecuzione la classe di un oggetto altrimenti identificato col termine generico id.

Selezionando una variabile, si può attivare un menu contestuale (che poi è il menu Variabile View, sempre all'interno di Debug), dove ci sono un paio di voci interessanti di cui voglio parlare.

La prima è la Print Description to Console, che scrive sulla console (che è un'altra finestra che si può aprire, e corrisponde in pratica al terminale dove è partito GDB) una descrizione dell'oggetto. In pratica, è l'applicazione del metodo

        -(NSString*) debugDescription

all'oggetto selezionato. Ora, tutti gli oggetti ereditano da NSObject questo metodo, che aggiunge qualche informazione in più sull'oggetto e che i programmatori accorti possono sovrascrivere per le proprie classi, in modo che all'interno del debugger si abbia una rappresentazione più leggibile del loro oggetto. Ancora per esempio, per gli array, i dictionary e le collezioni in genere, il metodo description presenta il contenuto della collezione. Se si intende sovrascrivere debugDescription, è bene farlo a partire dal metodo

        - (NSString) description

dall'uso molto simile. Molti oggetti realizzano debugDescription semplicemente chiamando description. Il metodo debugDescription dovrebbe essere quindi una versione più ricca di description, a cui si dovrebbe rifare per le informazioni di base.

La seconda voce è Edit Value, che permette appunto di modificare il valore della variabile stessa (a dire il vero questa caratteristica si ottiene anche facendo doppio clic sopra il valore). Sebbene cambiare l'indirizzo di memoria dell'oggetto non sia un'operazione consigliata all'inesperto, modificare il valore di una variabile (intera, o stringa) è comodo per recuperare situazioni compromesse da un errore, per scatenare un errore e vedere come si comporta l'applicazione, cose del genere.

Passo dopo passo

A questo punto, conviene esaminare alcuni elementi della toolbar (che corrispondono a svariate voci del menu Debug). Mi interessano i tre pulsanti verdi ed i tre pulsanti il cui nome comincia per Step.

I pulsanti verdi regolano l'esecuzione generale dell'applicazione. Se l'applicazione è ferma (ad esempio, perché è appena scattato un breakpoint), il pulsante Pause è disabilitato, mentre sono attivi Restart e Continue. Come dovrebbe essere ovvio, il pulsante Restart riavvia completamente da zero l'applicazione; il pulsante Continue invece fa proseguire l'applicazione, che potrà fermarsi al prossimo breakpoint (sempre se ne esiste uno). Se invece l'applicazione sta funzionando normalmente, è attivo il solo pulsante Pause, che causa l'interruzione immediata dell'applicazione. Generalmente, la fermata avviene in mezzo al codice, per cui non si vedrà (a meno di fortune sfacciate) il codice Objective C, ma dell'incomprensibile linguaggio macchina.

Torno ora ai tre pulsanti Step: Step Over, Step Into e Step Out. Il loro effetto è di eseguire una (o più istruzioni) e fermarsi prima di continuare. In particolare, Step Over (salta sopra) esegue una istruzione, esattamente quella indicata dalla riga corrente. Se l'istruzione è la chiamata di un metodo, di una funzione, o anche più di una, insomma, si tratta di una istruzione complessa, sono eseguite tutte le operazioni indicate e l'esecuzione si arresta alla successiva riga di codice nel metodo/funzione corrente. Diverso è il comportamento del pulsante Step Into (salta dentro), che esegue una ed una sola istruzione: se quindi la riga contiene una chiamata ad una funzione o metodo, si salta dentro la funzione o metodo chiamato. Infine, Step Out (salta fuori) esegue tutte le istruzioni del metodo o funzione corrente, e si ferma all'istruzione successiva, ovvero all'interno della funzione o metodo che ha chiamato il metodo corrente.

Una sessione di debug

La combinazione di questi tre meccanismi di step, assieme ai breakpoint, fornisce uno strumento fondamentale per indagare il comportamento del codice. Faccio un esempio di una procedura di debug, piuttosto generica, che mostra come utilizzare questi strumenti.

Supponiamo di essere riusciti a circoscrivere un problema all'interno di un metodo mioMetodo. Allora, per debuggare mioMetodo, si mette un breakpoint alla prima istruzione. Si lancia in esecuzione l'applicazione, si fa qualcosa e ad un certo punto il debugger interrompe le operazioni in corrispondenza del breakpoint. La prima cosa da fare è verificare se i dati sono consistenti, ovvero se le variabili di istanza hanno dei valori ragionevoli, se la variabile self è quella che ci aspettiamo, se gli eventuali parametri della chiamata del metodo sono quelli previsti. Se qualcosa non torna, significa che non è tanto mioMetodo ad essere errato, quanto il fatto che le istruzioni precedenti hanno provocato confusione. Si deve quindi individuare il metodo chiamante (c'è il pannello in alto a sinistra che mostra appunto la catena delle chiamate) e debuggare quello. Se invece tutto ci pare normale, cominciamo ad eseguire le istruzioni una alla volta. La prima volta, faccio sempre Step Over (l'idea è di fidarsi in prima battuta di tutte le funzioni o metodi chiamati da mioMetodo). Ad ogni passo, controllo variabili e quant'altro, per vedere se succede qualcosa di strano. Se arrivo alla fine del metodo, e tutto va bene, forse ci siamo sbagliati ad imputare a mioMetodo la colpa dell'errore. Tuttavia, prima di assolverlo del tutto, faccio continuare l'applicazione (lasciando attivo il breakpoint) per vedere se eventuali chiamate successive di mioMetodo causano problemi. Se neppure questo succede, ma il problema rimane, allora assolvo mioMetodo e cerco altri colpevoli. Se invece mioMetodo mostra segni di commettere errori, la colpa può essere direttamente sua (perché alcune istruzioni sono palesemente sbagliate, perché una variabile ha un valore sbagliato, eccetera), oppure la colpa è di uno dei metodi/funzioni chiamate, diciamo metodoInterno. In questo caso, me ne dovrei accorgere perché l'operazione di Step Over sopra metodoInterno provoca qualche effetto collaterale indesiderato (valore restituito, modifica delle variabili, eccetera): la colpa è allora di metodoInterno. Tolgo il breakpoint su mioMetodo e ne metto un altro su metodoInterno. Faccio ripartire l'applicazione e passo a debuggare metodoInterno.

L'esempio sembra un po' noioso, ma dopo un po' ci si fa la mano, e si va via veloci.

è solo l'inizio

Quanto scritto in questo capitolo è solo la punta di un iceberg. GDB è un debugger molto potente, e l'interfaccia fornita da XCode non rende piena giustizia. Conviene in primo luogo esplorare un po' tutte le possibilità fornite dall'interfaccia, e poi cominciare a studiare la linea di comando. Si scopriranno così moltissime cose, preziose ed anche indispensabili per trovare gli errori più infidi. La documentazione (sia Apple che altra) non manca. La seguente arruffata lista dovrebbe solamente darvi un'idea.

I breakpoint, che ho semplicemente presentato come un meccanismo per fermare l'esecuzione di una applicazione in un punto prefissato, sono molto di più. E' possibile associare ad ogni breakpoint una condizione, di modo che si attivi solo in particolari occasioni. Ad esempio, il breakpoint potrebbe attivarsi solamente se il valore di una variabile è nullo. Se è diverso da zero, nessun breakpoint. è possibile associare ad ogni breakpoint un'azione (che so, scrivere il valore di una variabile all'interno di un file); una cosa piuttosto comoda (ma solo in occasioni speciali) è di permettere il proseguimento automatico: l'applicazione si ferma sul breakpoint, attende diciamo tre secondi, poi continua. Se si ha l'occhio veloce, si riesce a vedere come cambiano i valori delle variabili, e si velocizza il riconoscimento die problemi.

Si può definire un watchpoint, ovvero arrestare l'applicazione ogni volta che una variabile viene letta o scritta.

Con l'applicazione sospesa, è possibile vedere il contenuto della memoria, vedere il contenuto delle variabili, eseguire delle istruzioni per modificare il comportamento del programma, invocare metodi e funzioni presenti, saltare istruzioni o comunque saltare in altre parti dell'applicazione.

Si può inserire un breakpoint direttamente da programma, con le istruzioni Debugger() oppure DebugStr( stringa ), che fermano l'esecuzione dell'applicazione quando si incontrano nel codice.

Si può (provare a) modificare il codice, compilarlo e caricarlo nel debugger senza dover uscire dalla sessione di debug, e verificare l'effetto che fa (non ho mai provato, a dire il vero).

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).