In questo articolo illustreremo a livello introduttivo come è possibile effettuare il debugging di programmi C in Linux.
gdb: il debugger C sotto Linux
di Lorenzo Bettini e Antonio Gallo
Un debugger éè un particolare software utilizzato per analizzare il comportamento di un altro programma allo scopo di individuare ed eliminare eventuali “bug”. Già dall’etimologia del nome (bug significa errore, ed il de ha un significato privativo), si può capire a cosa possa servire un tale programma: con un debugger, tipicamente si possono effettuare le seguenti operazioni:
eseguire un programma un’istruzione alla volta ;
ispezionare e modificare il valore delle variabili ;
far bloccare il programma quando si arriva in determinati punti ;
controllare il flusso del programma (l’ordine con cui sono state chiamate le funzioni) ;
analizzare un file “core”
Il debugger è uno strumento indispensabile, pPurtroppo il programmatore perfetto non esiste e,: nonostante l’esperienza o l’attenzione che si è posta in fase di programmazionedesign, non si può mai essere sicurio che un programma funzioni “al primo colpo” come se questo non bastasse non si può nemmeno essere sicuri che se, nella fase di testing, tutto va bene, tutto andrà bene sempre (di solito gli errori, come ben sanno i programmatori, vengono fuori durante una demo).
Gli I vari errori possono essere raggruppati in due principali categorie:
quelli per cui il programma non viene eseguito come ci si aspetta ;
quelli che bloccano il programma, mandandolo in crash
Invece di inserire Un programmatore alle prime armi potrebbe cercare di solito di individuare tali errori inserendo varie istruzioni di stampa in punti strategici del programma (ad esempio prima della chiamata di un funzione, o subito dopo il suo ritorno), oppure nei punti in cui si si pensa che si possa trovare la causae del l’errore/malfunzionamento, risulta più produttivo utilizzare un debugger. .
Tuttavia, questo metodo, quando si ha a che fare con un programma abbastanza complesso, questo metodo può non essere sufficiente, oltre ad essere molto poco elegante risulta scomodo e poco efficace, ed oltre a riempire il codice di istruzioni che poi dovranno essere rimosse prima della release del programma. In questi casi l’uso di un torna comodo un programma ausiliario, spesso fornito insieme al compilatore, chiamato debugger è la scelta migliore. Già dall’etimologia del nome (bug significa errore, ed il de ha un significato privativo), si può capire a cosa possa servire un tale programma: con un debugger, tipicamente si possono effettuare le seguenti operazioni:
eseguire un programma un’istruzione alla volta
ispezionare il valore delle variabili (ed anche modificarlo)
far bloccare il programma quando si arriva in determinati punti
controllare il flusso del programma (l’ordine con cui sono state chiamate le funzioni)
Con un tool del genere, non è detto che si riesca a trovare l’errore o la sua causa immediatamente, ma senz’altro il compito è molto facilitato.
In questo articolo Nell’articolo introdurremo il gdb, cioè il debugger C\C++ della GNU, in quanto quest’ultimo è disponibile per quasi tutti i principali sistemi Unix e non solo per Linux.distribuito insieme al compilatore gcc. Un particolare da notare è che l’autore originale di gdb è stato Richard Stallman [GNU] in persona.
Avvio di gdbI primi comandi
Per illustrare l’utilizzo di unil funzionamento del debugger è comodo utilizzare un programma di esempio, con alcuni errori, e mostrare i vari comandi del debugger tramite i quali si può riuscire a capire l’errore. Per il nostrouseremo, come primo esempio, consideriamo un programma che, date due stringhe per argomento, controlla se queste sono uguali.
Prima di tutto è bene far notare che pPerché il debugger possa funzionare, è necessario che nel programma eseguibile siano contenute certe informazioni utili, che saranno utilizzate d al debugger per avere accesso ai vari dati ed al codice del programma. Queste informazioni tipicamente vengono inserite dal compilatore, che dovrà essere lanciato con una particolare opzione di compilazione. Nel caso del gcc, si dovrà compilare con l’opzione –g:
gcc –g uguali.c –o uguali
questa opzione istruisce il compilatore ad inserire all’interno dell’eseguibile una serie di informazioni addizionali necessari al debugger per maneggiare che creerà il programma uguali, che potrà essere “debuggato”.
Proviamo a testare il nostro programma, e lanciamoloTestando il programma con due stringhe uguali, ad esempio:
uguali ciao ciao
DIVERSI
subito ci accorgiamo che qualcosa non va: il programma ci comunica che le due stringhe sono diverse! Allora iniziamo ad effettuare il debugging; è bene specificare che gdb è un programma a linea di comando, e questo può scoraggiare chi è abituato ai vari IDE visuali. Comunque con l'uso si noterà che non è poi così difficile da utilizzare e che ha tutte le funzionalità degli altri debuggerma non ha niente da invidiare ai vari IDE visuali.
Il debugger viene lanciato specificando il nome dell'eseguibile come argomento:
gdb uguali
A questo punto si entra nella shell del debugger che viene comandato inserendo direttamente i comandi; la (numerosa) lista dei comandi può essere ottenuta tramite il comando help (specificando successivamente un sotto argomento).
lorenzo@lorenzo:~/articoli > gdb uguali
GNU gdb 4.17.0.11 with Linux support
Copyright 1998 Free Software Foundation, Inc.
...
(gdb)
Una volta lanciato il gdb mostra il suo prompt , all’interno di questoAl prompt è possibile utilizzare i tasti cursore per scorrere la history di tutti i comandi inseriti, il tasto tab per avere a disposizione la funzione di completamento automatico quando si digita il nome di un comando, variabile o di una funzione. Inoltre una volta digitato un comando, la successiva pressione del tasto Invio sulla riga vuota causerà la ripetizione dell’ultimo comando inserito.
Comandi di base
Il programma viene eseguito col comando run, seguito dagli argomenti che intendiamo passare; in questo modo però il programma viene eseguito senza interruzioni, non dandoci la possibilità di vedere ciò che accade, mentre noi vogliamo seguirlo passo passo. Occorre quindi inserire un Allora inseriamo un breakpoint ,, ovvero un particolare punto del programma che, quando incontrato dal gdb, mette in pausa l’esecuzione del programma consentendo di inserire nuovamente comandi dalla command linesulla funzione main. Un breakpoint è un modo di segnalare al debugger di fermare l'esecuzione quando il programma arriva in quel punto (è possibile inserire un breakpoint anche su un determinato numero di riga).: Un breakpoint può essere specificato mediante il comando breakpoint, break o semplicemente usando la lettera “b”, seguito dal numero di riga del programma o dal nome di una funzione. Quindi per arrestare un programma appena viene eseguito occorre iImpostareiamo quindi un breakpoint sulla funzione main.
(gdb) break main
Breakpoint 1 at 0x8048643: file uguali.c, line 19.
ed a questo punto possiamo lanciare il programma
(gdb) run ciao ciao
Starting program: /home/lorenzo/articoli/uguali ciao ciao
Breakpoint 1, main (argc=3, argv=0xbffff834) at uguali.c:19
19 if ( uguali( argv[0], argv[1] ) )
il debugger si blocca arrivato alla funzione main, mostrando i suoi argomenti, e la prossima riga di programma che sarà eseguita. A questo punto il programma è fermo, ed il debugger è in attesa di un comando. I principali comandi che si hanno a disposizione in questa fase sono riportati nel Riquadro 1 :
n, next - per avanzare alla successiva linea di programma senza entrare in sotto-funzioni
s, step - per avanzare alla successiva linea di programma entrando nelle sotto-funzioni
p, print - per visualizzare o assegnare il contenuto di una variabile
l, list - per visualizzare il codice sorgente del programma sotto esame
finish - per completare l’esecuzione del programma sino alla fine della funzione corrente
c, continue - per continuare l’esecuzione del programma fino al prossimo breakpoint
quit - per uscire dal gdb.
. Per passare all’istruzione successiva eseguire un comando passo passo si hanno a disposizione i comandi next (abbreviato con n) e step (abbreviato con s). Se specifichiamoCon next, il debugger eseguirà la prossima istruzione (la valutazione della condizione dell'if) e si fermerà sull'istruzione presente sulla riga successiva del codice sorgente senza entrare all’interno della funzione uguali; noi invece saremmo siamo interessati ad entrare all'interno della funzione uguali, e per questo usiamo step:
(gdb) s
uguali (s1=0xbffff98b "/home/lorenzo/articoli/uguali", s2=0xbffff9a9 "ciao")
at uguali.c:8
8 l1 = strlen( s1 ) ; l2 = strlen( s2 ) ;
eseguendo nuovamente step entreremmo nel codice della funzione strlen (funzione di libreria), che però probabilmente, oltre a non essere compilata per il debugging, non rappresenta senz'altro la fonte del malfunzionamento. L'errore invece dovrebbe essere già chiaro: guardando i valori dei parametri s1 e s2 ci rendiamo conto che la prima stringa passata è il percorso del comando con cui è stato lanciato il programma: in effetti gli argomenti veri e propri passati ad un programma iniziano dall'indice 1 dell'array argv; quindi abbiamo capito l'errore e quindi possiamo modificare il programma (magari da un altro terminale)
if ( uguali( argv[1], argv[2] ) )
ricompilare e riprovare a lanciare nuovamente il programma
lorenzo@lorenzo:~/articoli > uguali ciao ciao
UGUALI
lorenzo@lorenzo:~/articoli > uguali ciao miao
UGUALI
sStavolta però il programma sembra troppo permissivo. Torniamo allora al debugger (se non si era usciti dal debugger si può riprendere dalla sessione precedente: il programma verrà riletto). Adesso però ci sembra di aver capito che il problema è nella funzione uguali, quindi togliamo il breakpoint su main (che era il numero 1) e ne mettiamo uno sulla funzione uguali.
Ad ogni breakpoint èviene assegnato un numero intero progressivo, per visualizzare la lista di tutti i breakpoint s inseriti si usa il comando info break, mentre per cancellarli si usa il comando delete seguito dal numero del breakpoint che vogliamo cancellare.
, e rilanciamo il programma:
(gdb) delete 1
(gdb) break uguali
Breakpoint 2 at 0x80485a6: file uguali.c, line 8.
(gdb) run ciao miao
The program being debugged has been started already.
Start it from the beginning? (y or n) y
...`/home/lorenzo/articoli/uguali' has changed; re-reading symbols.
Starting program: /home/lorenzo/articoli/uguali ciao miao
Breakpoint 2, uguali (s1=0xbffff9a9 "ciao", s2=0xbffff9ae "miao") at uguali.c:8
8 l1 = strlen( s1 ) ; l2 = strlen( s2 ) ;
eseguendo il programma passo passo (con next), arriviamo a
(gdb) n
9 if ( l1 != l2 ) return 0 ;
(gdb) n
11 for ( i = 1 ; i < l1 ; ++i )
(gdb) n
12 if ( s1[i] != s2[i] )
(gdb) n
11 for ( i = 1 ; i < l1 ; ++i )
il test sulla prima lettera ('c' e 'm') sarebbe dovuto fallire ed invece ha avuto successo; possiamo visualizzare il contenuto di s1[i] col comando print
(gdb) print s1[i]
$1 = 105 'i
a questo punto ci ricordiamo che il primo carattere in una stringa (vettore di caratteri) ha indice 0, e non 1: quindi dobbiamo far partire il ciclo for da 0 e non da 1. Un altro comando per la visualizzazione di variabili molto utile è display che ad ogni istruzione rivisualizza il contenuto delladi una variabile utilizzata come parametro:
(gdb) display s2[i]
1: s2[i] = 105 'i'
(gdb) n
12 if ( s1[i] != s2[i] )
1: s2[i] = 97 'a'
A questo punto il nostro programma funziona a dovere e possiamo uscire dal debugger con quit.
Analizzare un file di core
Quando un programma crash di solito termina con l’indicazione
"Segmentation Fault (core dumped)"
e viene generato il famosissimo file core. Questi non è altro che un dump della memoria usata dal programma al momento in cui è stato terminato, una vera è propria fotografia di quell’attimo. Utilizzando gdb possiamo analizzare il file di core per localizzare l’errore, per far cio’ si avvia gdb con il comando
gdb crash core
dove “crash” è il programma da debuggare e “core” è il file di core da utilizzare.
Lanciato il gdb appare subito l’indirizzo di memoria dove è avvenuto l’errore
#0 0x40054b03 in ?? () from /lib/libc.so.6
per capire quale funzione l’ha causato viene usato il comando bt (backtrace) che stampa lo stack delle chiamate per raggiungere quel punto
(gdb) bt
#0 0x40054b03 in ?? () from /lib/libc.so.6
#1 0x80484fc in copia (s=0xbffffc87 "crash") at crash.c:6
#2 0x804852e in main (argc=1, argv=0xbffffba0) at crash.c:14
In questo modo è facile capire che l’errore è avvenuto al momento in cui è stata eseguita l’istruzione alla linea numero 6 del programma crash.c
gdb con emacs
Abbiamo detto che gdb è un debugger a linea di comando, tuttavia è possibile utilizzarlo in modo più confortevole tramite Emacs (si presuppone che il lettore abbia una certa familiarità con Emacs), col comando
M-x gdb
(cioè Alt+x gdb) e poi specificando il nome dell'eseguibile. In questo modo si ha anche il vantaggio, eseguendo il programma passo passo, di avere in una finestra il debugger e nell'altra il codice sorgente con un indicatore della posizione corrente (in modo molto simile ai debugger degli IDE). Se poi si utilizza XEmacs si ha a disposizione anche una tool bar. Nella Figura 1 si può vedere il gdb all'interno di XEmacs dove stiamo debuggando il programma crash, che provoca un segmentation fault. eseguendo il comando bt (back trace) gdb mostra l'istruzione che ha provocato il crash, e anche lo stack delle chiamate che ha portato a tale istruzione; in questo caso ci rendiamo conto che il crash è avvenuto all'interno di strcpy a cui viene passato un puntatore nullo: la variabile globale stringa non inizializzata (errore classico).
Quando un programma va in crash (come il nostro crash.c) di solito genera un file core cioè un dump della memoria usata dal programma al momento in cui è stato terminato. Utilizzando gdb possiamo analizzare il file di core per localizzare l’errore, per far ciò si avvia gdb con il comando
gdb crash core
Lanciato il gdb appare subito l’indirizzo di memoria dove è avvenuto l’errore ed anche in questo caso, si può usare bt per vedere lo stack delle chiamate.
Ddd - un front-end grafico alper il gdb
Un interessante prodotto basato su gdb è DDD (Data Display Debugger) [DDD]. Si tratta di un front-end grafico aper gdb che gira sotto X-Window e che semplifica di molto il lavoro con gdb. Infatti fornisce la possibilità di visualizzare su finestre diverse il prompt di gdb, i sorgenti del programma e le variabili che si sta tenendo sotto controllo. La caratteristica principale di DDD, oltre a fornire un ambiente semplice e di facile utilizzo, è quella di mettere a disposizione una visualizzazione “grafica” dei dati. In DDD ogni variabile o struttura viene rappresentata da un rettangolo al cui interno vengono visualizzati i valori. Nel caso in cui ci sia un campo di tipo puntatore, DDD visualizza una freccia che parte dal dato contenete il puntatore verso il campo che è puntato (Figura 2). Questa feature rende DDD uno strumento davvero efficace nel debugging di strutture dati complesse quali alberi o grafi.
Sul sito ftp troverete i listati dei vari programmi utilizzati come esempio in questo articolo.
Riferimenti
[BeGa1] L.Bettini, A.Gallo, Compilare i programmi sotto Linux, Login N. 17???
[BeGa2] L.Bettini, A.Gallo, CVS: sistema per il controllo di versione del software, Login N. 16???
[J2H] L.Bettini, java2html, http://www.gnu.org/software/java2html/java2html.html.
[Cygnus] www.cygnus.com home page del compilatore GNU Win32
[GNU] http://www.gnu.org
[DDD] http ://www.cs.tu-bs.de/softech/ddd/
Riquadro 1
I principali comandi di “gdb”
· n, next - per avanzare alla successiva linea di programma senza entrare in sotto-funzioni;
· s, step - come next, ma entrando nelle sotto-funzioni;
· p, print - per visualizzare o assegnare il contenuto di una variabile;
· l, list - per visualizzare il codice sorgente del programma sotto esame;
· finish - per completare l'esecuzione del programma sino alla fine della funzione corrente;
· c, continue - per continuare l'esecuzione del programma fino al prossimo breakpoint;
· quit - per uscire dal gdb.
Figura 1

Figura 2

