Debugging with GDB - STB1019/SkullOfSummer GitHub Wiki

Introduzione

A volte valgrind non è sufficiente per risolvere un problema. Vi serve bloccare il programma in una particolare linea di codice ed analizzare il valore di alcune variabili. Oppure vi serve capire se il programma entra veramente in un in if od esegue veramente il vostro ciclo while quel numero di volte che vi aspettate. I sistemi Linux hanno solitamente uno strumento molto utile per risolvere queste situazioni: il GDB, ossia un debugger.

Esempio

E' possibile visionare un esempio in cui si può smanettare con il gdb in questo link:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

struct list_cell_t {
	int int_payload;
	struct list_cell_t* next;
};

struct list_t {
	struct list_cell_t* head;
	int size;
};

struct list_cell_t* add_element(struct list_t* l, int el) {
	struct list_cell_t* cell = malloc(sizeof(struct list_cell_t));
	if (cell == NULL) {
		exit(2);
	}
	cell->int_payload = el,
	cell->next = NULL;

	if (l->head != NULL) {
		struct list_cell_t* tmp = l->head;
		while (tmp->next != NULL) {
			tmp = tmp->next;
		}
		tmp->next = cell;
	} else {
		//empty list
		l->head = cell;
	}

	l->size += 1;

	return cell;
}

void printList(const struct list_t* l) {
	printf("[");
	struct list_cell_t* cell = l->head;
	
	while (cell != NULL) {
		printf("%d", cell->int_payload);
		if (cell->next != NULL) {
			printf(" ");
		}
		cell = cell->next;
	}
	printf("]");
}

int main() {
	struct list_t* l = malloc(sizeof(struct list_t));
	if (l == NULL) {
		exit(1);
	}
	l->size = 0;
	l->head = NULL;
	
	add_element(l, 1);
	add_element(l, 1);
	add_element(l, 2);
	add_element(l, 3);
	add_element(l, 5);
	add_element(l, 8);

	printf("Fibonacci is:\n");
	printList(l);
	printf("\n");

	//no malloc deallocation!

	return 0;
}

Usare il GDB

Richiamare il GDB

Per usare il gdb, è necessario compilare il programma usando il flag di compilazione -g, ossia quello che permette di aggiungere informazioni di debug nel compilato creato dal gcc. usare la seguente linea di comando per attivare il gdb sul programma da debuggare:

gdb <nome_eseguibile_da_debuggare>

Quando si vedrà la scritta:

(gdb)

Allora saremo pronti a debuggare il programma. Notare che il gdb è un terminale con una history, quindi sarà possibile navigare su e giù con le ultime istruzioni da te eseguite. Inoltre il comando:

help

vi permetterà di vedere tutte le funzionalità del debugger. Per far girare il programma:

(gdb) run

Se non avete inserito breakpoints o altre cose, il programma girerà come al solito. Per uscire dal gdb, esegui:

(gdb) quit

Aggiungere un breakpoint

Un breakpoint è una istruzione che prima di essere eseguita, farà sospendere l'esecuzione del programma in modo tale che tu programmatore possa osservare lo stato del programma ottenuto a quel particolare punto. Per aggiungere un break point:

break <nome_file>:<line_number>

Per esempio:

break gdb_example.c:58 run

Il gdb (se avrai inserito il breakpoint nel posto giusto) si fermerà. A questo punto potrai immettere nuovi comandi, come ad esempio:

  • continue: riprendi l'esecuzione del programma;
  • step: vai avanti di un'istruzione nel programma;
  • run: rifai partire il programma da 0;
  • next: esegui l'intera riga all'interno della funzione in cui ti trovi (esegue anche tutte le sottofunzioni in quella riga);
  • step: esegui la riga all'interno della funzione in cui ti trovi (se la riga richiama una sottofunzione, entra in quella sottofunzione);
  • print <espressione>: stampa a video il risultato dell'espressione C. Potrebbe essere print l, print l->head o add_element(l,10) (quest'ultima ha dei side effects sull'intera esecuzione del programma dato che aggiunge un nuovo elemento!);
  • where: stampa lo stack trace dell'istruzione in cui ti trovi. Utile per capire che giro il programma ha fatto per arrivare fino a quella riga.

Dato il programma con breakpoint in linea 61:

add_element(l, 1); //linea 61
add_element(l, 1); //linea 62
add_element(l, 2); //linea 63

Usando next andremo in linea 62. Usando poi step entreremo nella funzione add_element:

//sono fermo in linea 61
Breakpoint 3, main () at gdb_example.c:61
61		add_element(l, 1);
(gdb) next //eseguo il comando next
62		add_element(l, 1);
(gdb) step //eseguo quindi il comando step
add_element (l=0x602010, el=1) at gdb_example.c:16
16		struct list_cell_t* cell = malloc(sizeof(struct list_cell_t)); //error! wrong size_t //prima riga della subroutine add_element
(gdb)

Puoi visionare i breakpoint inseriti con info breakpoints ed eliminare i breakpoint che non vuoi più con delete <numero_breakpoint> dove <numero_breakpoint> è il numero del breakpoint visionabile con info.

Aggiungere un watch

I watch sospendono il programma non appena la variabile osservata dal watch muta di valore. Per esempio:

watch l

Il watch vale solo se la modifica viene eseguita nella stessa funzione in cui hai aggiunto il watch.

Esplorare lo stack di chiamate

Supponi che stai debuggando una funzione foo e ti rendi conto che vuoi visitare la funzione che ha chiamato foo. Usando:

up 1

puoi risalire alla riga (e al contesto) della chiamata a funzione. Puoi usare down 1 per scendere nello stacktrace.

GDB cheat sheet

Una lista di tutte le funzionalità è disponibile in questo GDB cheat sheet.

Usare valgrind insieme a GDB

A volte quando esegui un programma potresti ottenere una segmentation fault talmente criptica che persino l'output di valgrind non ti aiuta così tanto. Per debuggare al massimo puoi:

  • istruire valgrind a fermarsi appena nota un errore;
  • attivare il gdb e collegarlo a valgrind in modo da analizzare la situazione in cui valgrind ha incontrato un errore;

Per farlo attiva valgrind con la flag vgdb-error:

valgrind --vgdb-error=1 ./awesomeProgram
# il tuo programma verrà eseguito fino a che valgrind non trova un errore. dopo di che si sospenderà

Ora apri un altro terminale, apri il gdb ed esegui l'istruzione:

gdb ./awesomeProgram
target remote | vgdb

Così facendo entrerai nel gdb nell'esatta linea in cui valgrind ha riscontrato l'errore!

Interfaccia grafica

Il GDB ha un'interfaccia grafica built-in chiamata tui. E' primitiva, ma per basse esigenze funziona benissimo. Per attivarla esegui:

gdb -tui <nome_eseguibile_da_debuggare>

Per esempio:

gcc -g gdb_example.c -o gdb_example && gdb -tui ./gdb_example

Immagine da Beej's Quick Guide to GDB

L'interfaccia grafica in sé non è facile da navigare, ma comunque è un buon modo per capire cosa si sta facendo. Alcuni comandi sono:

  • Page up: vai su di una schermata nel file visualizzato dalla tui;
  • Page down: vai giù di una schermata nel file visualizzato dalla tui;
  • Freccia su: vai su di una riga nel file visualizzato dalla tui;
  • Freccia giù: vai giù di una riga nel file visualizzato dalla tui;
  • Ctrl+x+a attiva o disattiva la schermata tui. Utile se vuoi scorrere la history dei tuoi comandi;

Referenze

⚠️ **GitHub.com Fallback** ⚠️