È necessario un debugger:
- per fare riferimento a nomi di variabili e funzioni, non ai loro indirizzi (ad esempio
i
non 0xdeadbeef
e main()
non 0x4B1D
).
- per associare l'indirizzo del codice macchina con una riga nel codice sorgente.
A questo scopo il compilatore crea ( -g
) una tabella dei simboli (aumentata) (può essere incorporata nel programma o memorizzata come un file separato). A volte vengono generate altre informazioni secondarie.
Ci sono diversi formati per memorizzare queste informazioni (pugnalate, coff, nani ...).
Contrariamente alle credenze popolari, molti compilatori (sicuramente GCC) scrivono alcuni simboli in un file oggetto anche in modalità di rilascio (con l'opzione -O3).
Ecco perché anche con un binario di rilascio, puoi fare:
$ gcc -O3 programName.c
$ gdb a.out
[...]
Reading symbols from a.out...(no debugging symbols found)...done.
Ovviamente non è sufficiente per una buona esperienza di debug .
I simboli possono essere rimossi dal binario usando il comando strip:
$ strip -s a.out
$ nm a.out
nm: a.out: no symbols
e ora:
$ gdb a.out
[...]
(gdb) b main
Function "main" not defined.
Considerare anche che esiste un'interazione tra l'ottimizzazione del codice e il debug:
- alcune variabili che hai dichiarato potrebbero non esistere affatto;
- il flusso del controllo può spostarsi brevemente dove non te lo aspettavi;
- alcune istruzioni potrebbero non essere eseguite perché calcolano risultati costanti o i loro valori erano già disponibili
- alcune istruzioni possono essere eseguite in posizioni diverse perché sono state spostate dai cicli
- ...
Quindi, con molti compilatori, non può attivare sia l'ottimizzazione che il debug (GCC ha -Og switch per il debug del codice ottimizzato).
Ulteriori dettagli: