GDB (acronimo di GNU Debugger) è un software sviluppato come parte del progetto GNU che permette di eseguire il debugging di altri programmi, aiutando i programmatori a identificare e correggere errori nel loro codice. È progettato principalmente per linguaggi come C e C++, ma supporta anche altri linguaggi come Fortran, Assembly e Rust.

1 - Caratteristiche principali di GDB

  • Debugging interattivo: è possibile seguire passo-passo l’esecuzione di un programma per comprendere il flusso del codice e identificare gli errori.
  • Breakpoints: si possono impostare punti di interruzione nel codice detti breakpoints per fermare l’esecuzione in punti specifici.
  • Analisi e modifica delle variabili: con GDB si può esaminare il contenuto di variabili e strutture dati durante l’esecuzione, ma anche modificare il valore delle variabili mentre il programma è in esecuzione.
  • Backtrace e analisi dello stack: è possibile visualizzare la sequenza delle chiamate di funzione attive (detta call stack) per risalire facilmente all’origine di un errore nel programma.
  • Core dump: GDB permette di caricare un core dump per analizzare il motivo del crash del programma e fornisce informazioni come lo stato della memoria e i registri al momento del crash.

2 - Come usare GDB

2.1 - Compilazione di un programma con supporto al debugging

Per usare GDB, nella compilazione del codice sorgente di un programma è consigliato usare:

  • Un’opzione (solitamente -g) per l’inclusione di informazioni di debug nel file eseguibile generato durante la compilazione, in modo che GDB sia in grado di interpretare meglio il codice sorgente e di collegare le istruzioni eseguibili alle righe di codice originali.
  • Un’opzione (solitamente -O0) per disabilitare tutte le ottimizzazioni del compilatore durante la compilazione del codice: il codice viene compilato “così com’è” e ogni istruzione corrisponde direttamente a una riga del codice sorgente.

In particolare:

  • Con i programmi scritti in C (con compilatore gcc) e in Fortran (con compilatore gfortran), è consigliato usare l’opzione -g per includere informazioni di debug e l’opzione -O0 per disabilitare le ottimizzazioni.

Esempio

Per il C:

gcc -g -O0 nome-file-sorgente -o nome-eseguibile

Per il Fortran:

gfortran -g -O0 nome-file-sorgente -o nome-eseguibile

Attenzione

Alcuni costrutti avanzati di Fortran potrebbero non essere completamente supportati. In particolare:

  • Allocatable arrays: l’allocazione dinamica di array può rendere difficile ispezionare i valori direttamente in GDB.
  • Derived types (tipi derivati o struct-like types): le strutture dati complesse di Fortran non sempre vengono visualizzate correttamente.
  • Interfacce e procedure contenute: le funzioni interne (contained procedures) potrebbero non essere accessibili direttamente tramite breakpoint.
  • Ottimizzazioni del compilatore: se il codice è compilato con ottimizzazioni (-O2 o superiore), GDB potrebbe non riuscire a tracciare correttamente le variabili Fortran, spostandole o eliminandole.
  • Coarrays (Fortran moderno con parallelismo): il debugging di coarrays (usati per il parallelismo in Fortran) è limitato in GDB.
  • Con i programmi scritti in Rust (con compilatore rustc), è consigliato usare l’opzione -g per includere informazioni di debug. Per disabilitare le ottimizzazioni, usare l’opzione -C opt-level=0 oppure compilare direttamente con cargo build che, di default, compila in modalità debug che ha opt-level=0.

Esempio

rustc -C opt-level=0 -g nome-file-sorgente.rs

2.2 - Esecuzione del programma con GDB

Una volta compilato il programma, per eseguirlo usare il comando:

gdb <nome-eseguibile>

In questo comando non vanno specificati gli argomenti con cui eseguire il programma, perché verranno dichiarati successivamente durante l’esecuzione del programma all’interno del GDB.

A questo punto, verrà avviato il prompt interattivo di GDB in cui inserire i comandi.

2.3 - Principali comandi di GDB

  • help (alias: h): elenca i comandi a disposizione.
    • help all: elenca la lista completa di tutti i comandi a disposizione.
    • help <nome-comando>: illustra la documentazione completa di un singolo comando.
  • list (alias: l): mostra una porzione del codice sorgente del programma. Eseguendolo più volte, mostra porzioni successive.
    • list <numero-riga>: mostra il codice sorgente attorno alla riga specificata.
    • list <nome-funzione>: mostra il codice della funzione specificata.
    • list .: mostra le prime righe del codice sorgente.
  • info <sotto-comando> (alias: i <sotto-comando>): mostra informazioni sul programma che si sta debuggando, in particolare su ciò che viene specificato nel <sotto-comando>.
  • quit (alias: q): termina l’esecuzione di GDB.

2.3.1 - Avvio ed esecuzione di un programma

  • run (alias: r): avvia il programma e lo esegue al primo breakpoint (se ce ne sono).
    • run <argomenti>: avvia il programma e lo esegue con gli argomenti specificati.
  • start: avvia il programma, ma si ferma alla prima istruzione eseguibile.
  • continue (alias: c): prosegue l’esecuzione del programma fino al breakpoint successivo (se ce ne sono).
  • kill (alias: k): termina il programma in esecuzione.

2.3.2 - Navigazione nel codice

  • next (alias: n): esegue una linea di codice e, se si tratta di una chiamata di una funzione, la esegue interamente.
  • step (alias: s): esegue una linea di codice e, se si tratta di una chiamata di una funzione, entra nel codice della funzione senza eseguirla interamente.
  • finish: finisce di eseguire la funzione corrente.
  • jump <numero-riga> (alias: j <numero-riga>): salta a una specifica riga di codice e prosegue l’esecuzione del programma fino al breakpoint successivo (se ce ne sono).

2.3.3 - Breakpoints

  • break <numero-riga> (alias: b <numero-riga>): inserisce un breakpoint alla riga indicata.
    • break <nome-file-sorgente>:<numero-riga>: inserisce un breakpoint alla riga specificata all’interno del file <nome-file-sorgente> (utile nel caso in cui si stia debuggando un programma composto da più file sorgente).
  • info breakpoints (alias: i b): mostra la lista dei breakpoint definiti, a ognuno dei quali è stato associato un ID (visibile nella colonna Num).
  • clear <numero-riga> (alias: cl <numero-riga>): elimina il breakpoint alla riga specificata.
  • delete <id> (alias: d <id>): elimina il breakpoint con ID <id>.
  • disable <id> (alias: dis <id>) ed enable <id> (alias: en <id>): disattiva (senza eliminarlo) o riattiva il breakpoint con ID <id>.

2.3.4 - Variabili e stato del programma

  • print <espressione> (alias: p <espressione>): valuta l’espressione e ne stampa il valore.
  • set <variabile> = <valore>: modifica il valore di una variabile.
  • display <espressione>: valuta l’espressione e ne stampa il valore ad ogni step che verrà eseguito.
  • info display: mostra la lista dei display definiti, a ognuno dei quali è stato associato un ID (visibile nella colonna Num).
  • undisplay <id>: rimuove il display con ID <id>.
  • info locals: mostra tutte le variabili locali e i loro relativi valori.

2.3.5 - Call stack e funzioni

  • backtrace (alias: bt, where): mostra la sequenza delle chiamate attive (detta call stack).
  • frame <id> (alias: f <id>): passa al frame con ID <id> nel call stack.
  • up e down: per muoversi su o giù all’interno del call stack.

Fonti

  • 🏫 Lezioni e slide del Prof. Schifanella Claudio del corso di Laboratorio di Sistemi Operativi (canale B, turno T4), Corso di Laurea in Informatica presso l’Università di Torino, A.A. 2024-25: