Come ottenere backtrace significativi in Gentoo
1.
Backtrace con Gentoo
Cosa sono i backtrace
Un backtrace (talvolta anche chiamato bt, trace, o stack trace) è un
resoconto in un formato leggibile dall'uomo della catena di chiamate (o stack,
pila, di chiamate) di un programma. Dà informazioni sul punto del programma in
cui ci si trova e come questo è stato raggiunto attraverso tutte le funzioni a
partire dal main() (almeno in teoria). I backtrace sono solitamente
analizzati quando utilizzando un debugger come gdb (GNU debugger) ci si
scontra con una situazione di errore come un segmentation fault o un abort, per
scovare la fonte del problema.
Un backtrace significativo non contiene solo gli oggetti condivisi dove la
chiamata è stata generata, ma anche il nome della funzione, il nome del file e
la linea dove il programma si è fermato. Sfortunatamente su un sistema
ottimizzato per avere maggiori prestazioni e conservare lo spazio su disco, i
backtrace sono inutili e mostrano solo il puntatore allo stack delle chiamate e
una serie di punti interrogativi (??) invece del nome della funzione e la
posizione.
Questa guida mostrerà come è possibile ottenere degli utili, significativi
backtrace in Gentoo, usando alcune caratteristiche di Portage.
Flag per il compilatore
In modo predefinito gcc non compila informazioni di debug all'interno
degli oggetti (librerie e programmi) che crea, in quanto questo porterebbe ad
oggetti di dimensioni maggiori. Inoltre, molte ottimizzazioni interferiscono sul
modo in cui le informazioni di debug vengono salvate. Per queste ragioni, la
prima cosa a cui fare attenzione è il fatto che la variabile CFLAGS venga
impostata in modo da da generare informazioni di debug utili.
L'opzione fondamentale da aggiungere in questo caso è -g. Questa informa
il compilatore sul fatto che esso deve aggiungere informazioni ulteriori negli
oggetti, come nome del file e numeri di riga. Ciò è di solito sufficiente per
avere un backtrace basilare, ma l'opzione -ggdb aggiunge ulteriori
informazioni. C'è al momento un'altra opzione (-g3), ma il suo uso non è
raccomandato. Sembra che questa corrompa le interfacce binarie e possa
comportare maggiori fallimenti (crash del programma). Per esempio,
glibc dà problemi quando viene compilato con questa opzione. Se si
desidera ottenere più informazioni possibile, andrebbe utilizzata l'opzione
-ggdb.
Codice 1.1: Esempio di CFLAGS per l'aggiunta di informazioni di debug |
CFLAGS="-march=k8 -O2 -ggdb"
CXXFLAGS="${CFLAGS}"
|
Elevati livelli di ottimizzazione, come -O3 possono rendere il backtrace
poco fedele, o incorretto. Generalmente parlando, -O2 e -Os
possono essere usate in sicurezza per ottenere un backtrace approsimativo,
relativamente alla funzione chiamata e all'aria del file sorgente dov'è
avvenuto il crash. Per backtrace più precisi, si dovrebbe invece usare
-O1.
Nota:
L'uso di -O0 viene suggerito spesso quando si sta provando a produrre un
backtrace completo. Sfortunatamente questo non sempre gioca a favore del
software stesso, in quando disabilitando tutte le ottimizzazioni cambiano le
implementazioni delle funzioni nella libreria GNU C (sys-libs/glibc), al punto
da considerarle quasi due librerie differenti, una per le compilazioni
ottimizzate e una per quelle non ottimizzate. Inoltre, alcuni software
falliranno interamente nella compilazione quando viene usato -O0 a causa
dei cambiamenti negli include delle intestazioni, e alla mancanza di
caratteristiche come la propagazione delle costanti nell'eliminazione del
codice superfluo.
|
Nota per gli utenti di architetture x86: tali utenti hanno frequentemente
l'opzione -fomit-frame-pointer nella loro variabile CFLAGS.
L'architettura x86 ha un insieme limitato di registri generali, e questa opzione
può rendere disponibile un registro ulteriore, che migliora le prestazioni. Ma
questo lo si paga: rende impossibile per gdb il "camminare sullo stack"
— in altre parole, gli impedisce di generare un backtrace in modo
affidabile. Rimuovendo questa opzione dalla variabile CFLAGS si ottiene qualcosa
di più facile comprensione per gdb. Gli utenti della maggior parte delle
altre piattaforme non hanno di che preoccuparsi; o in genere non viene impostata
comunque l'opzione -fomit-frame-pointer, o il codice generato da
gcc non confonderà gdb (in tal caso l'opzione è già abilitata dal
livello di ottimizzazione -O2).
Gli utenti di Gentoo Hardened dovrebbero preoccuparsi anche di altri aspetti. Il
documento riguardante le
domande frequenti su Hardened Gentoo fornisce suggerimenti aggiuntivi e
trucchi che sarebbe necessario conoscere.
Stripping
Cambiando solamente la propria variabile CFLAGS o rieffettuando l'emerge totale
di world non si otterranno comunque dei backtrace significativi, in quanto
bisogna risolvere il problema dello "stripping", o svuotamento. Di norma Portage
ripulisce i binari (ovvero effettua lo "strip" dei binari, termine usato da qui
in avanti). In altre parole, vengono rimosse le sezioni non necessarie
all'esecuzione così da ridurre le dimensioni dei file che vengono installati.
Questa è una cosa utile per l'utente medio che non necessita di backtrace utili,
ma rimuove tutte le informazioni di debug generate dalle opzioni -g*, e
anche le tabelle dei simboli che vengono usate per trovare le informazioni di
base per poter mostrare backtrace in un formato leggibile dall'uomo.
Ci sono due modi per impedire al processo di stripping di interferire con il
debug e i backtrace utili. Il primo è quello di indicare a Portage che non deve
effettuare assolutamente lo strip dei binari, aggiungendo nostrip alla
variabile FEATURES. Questo lascerà i file installati esattamente come gcc
li ha creati, con tutte le informazioni di debug e le tabelle di simboli, che
aumentano lo spazio su disco occupato da eseguibili e librerie. Per evitare
questo problema, in Portage versione 2.0.54-r1 e dalla serie 2.1, è possibile
usare invece la FEATURE splitdebug.
Con l'opzione splitdebug abilitata, Portage farà ancora lo strip dei
binari installati nel sistema. Ma prima di farlo, tutte le informazioni di debug
utili verranno copiate nel file ".debug", che successivamente verrà installato
in /usr/lib/debug (il nome completo del file potrebbe essere dato
aggiungendo a quest'ultimo il percorso in cui il file è attualmente installato).
Il percorso per raggiungere questo file è poi salvato nel file originale
all'interno di una sezione ELF chiamata ".gnu_debuglink", così che gdb
possa sapere da quale file caricare i simboli.
Importante:
Se vengono abilitate le opzioni nostrip e splitdebug, Portage non
effettuerà per niente lo strip dei binari, quindi bisogna fare attenzione a cosa
si vuole ottenere.
|
Un altro vantaggio dell'opzione splitdebug è il fatto che non richiede di
ricompilare il pacchetto per liberarsi delle informazioni di debug. Questo è
utile quando si compilano alcuni pacchetti con informazioni di debug per avere
un backtrace relativo ad un singolo errore. Una volta che questo è corretto,
sarà sufficiente rimuovere la cartella /usr/lib/debug.
Per essere sicuri di non effettuare lo strip dei binari, ci si deve anche
assicurare di non avere l'opzione -s impostata nella propria variabile
LDFLAGS. Questa indica al linker di effettuare lo strip dei binari risultanti
nella fase di link. Si noti anche che l'uso di questa opzione potrebbe portare
ad ulteriori problemi. Non verrebbero rispettate le restrizioni sullo stripping
imposte da qualche pacchetto che smette di funzionare quando viene sottoposto a
strip completo.
Nota:
Alcuni pacchetti sfortunatamente gestiscono lo stripping da soli, all'interno
dei makefile forniti dagli sviluppatori originali. Questo è un errore e dovrebbe
essere indicato. Tutti i pacchetti dovrebbero lasciare a Portage il processo di
stripping o semplicemente vietarlo completamente. La principale eccezione a
questo sono i pacchetti binari. Questi sono solitamente sottoposti a stripping
dagli sviluppatori originali, fuori dal controllo di Portage.
|
flag USE debug
Alcune ebuild forniscono una flag USE debug. Sebbebe alcuni erroneamente
la usino per fornire informazioni di debug e giocare con le flag di
compilazione quando essa è abilitata, il suo scopo non è quello.
Se si sta provando ad effettuare il debug di un crash riproducibile, è
consigliabile lasciare stare questa flag USE, in quanto essà farà compilare un
codice sorgente diverso da quello ottenuto in precedenza. È più efficiente
ottenere prima un backtrace senza modificare il codice, ma semplicemente
modificando le informazioni sui simboli, e solamente dopo abilitare le
funzionalità di debug per indagare più a fondo nel problema.
Le funzionalità di debug che vengono abilitate da questa flag USE includono
asserzioni, log di debug a video, file di debug, rilevazioni di falle e
operazioni extra-sicure (per esempio la pulizia della memoria prima dell'uso).
Alcune di esse potrebbero risultare gravose, specialmente per software
complesso o software dove le prestazioni sono un aspetto importante.
Per queste ragioni, è caldamente consigliato di agire con cautela quando si
abilita la flag USE debug, e considerarla solamente come ultima carta da
giocare.
Introduzione a gdb
Una volta che i pacchetti sono stati compilati con le informazioni di debug e
non sono sottoposti al processo di stripping, è necessario solamente recuperare
il backtrace. Per farlo c'è bisogno del pacchetto sys-devel/gdb.
Questo contiene il debugger GNU (gdb). Dopo averlo installato, è
possibile procedere al recupero dei backtrace. Il modo più semplice per
ottenerne uno lo si fa eseguendo il programma all'interno di gdb. Per
farlo, è necessario far puntare gdb al percorso del programma da
eseguire, passargli gli argomenti di cui necessita, e quindi eseguirlo:
Codice 1.2: Eseguire ls attraverso gdb |
$ gdb /bin/ls
GNU gdb 6.4
[...]
(gdb) set args /usr/share/fonts
(gdb) run
Starting program: /bin/ls /usr/share/fonts
[Thread debugging using libthread_db enabled]
[New Thread 47467411020832 (LWP 11100)]
100dpi aquafont baekmuk-fonts cyrillic dejavu fonts.cache-1 kochi-substitute misc xdtv
75dpi arphicfonts CID default encodings fonts.dir mikachan-font util
Program exited normally.
(gdb)
|
Il messaggio "Program exited normally" (programma terminato correttamente)
significa che il programma è terminato restituendo il codice 0. Questo indica
che non ci sono state situazioni di errore, ma non bisogna farci troppo
affidamento, in quanto ci sono programmi che escono restituendo il valore di
stato 0 quando raggiungono condizioni di errore. Un altro messaggio comune è
"Program exited with code nn" (programma terminato con codice nn).
questo semplicemente informa che è stato restituito un codice di stato diverso
da zero. Potrebbe comportare una situazione di errore gestita o prevista. Per
problemi di segmentation fault o abort, viene invece restituito il messaggio
"Program received signal SIGsomething" (il programma ha ricevuto il segnale
SIGqualcosa).
Quando un programma riceve un segnale, potrebbero esserci molte ragioni diverse.
In caso di SIGSEV e SIGABRT (rispettivamente segmentation fault e abort),
significa di solito che il codice ha fatto qualcosa di sbagliato, come
effettuare una syscall (chiamata di sistema) errata o tentare l'accesso in
memoria attraverso un puntatore gestito male. Altri segnali comuni sono SIGTERM,
SIGQUIT e SIGINT (l'ultimo è ricevuto quando viene inviato un CTRL-C al
programma, e normalmente viene intercettato da gdb e ignorato dal
programma stesso).
Infine c'è la serie degli "eventi Real-Time". Vengono indicati come SIGnn
dove nn rappresenta un numero maggiore di 31. L'implementazione pthread
di solito li usa per sincronizzare i differenti thread del programma, e quindi
non rappresentano condizione di errore di alcun tipo. È facile ottenere
backtrace senza senso quando si confondono i segnali Real-Time con le condizioni
di errore. Per prevenire questa situazione, è possibile indicare a gdb di
non fermare il programma quando vengono ricevuti tali segnali, ma piuttosto
passarli direttamente al programma, come nel seguente esempio.
Codice 1.3: Eseguire xine-ui attraverso gdb, ignorando i segnali real-time |
$ gdb /usr/bin/xine
GNU gdb 6.4
[...]
(gdb) run
Starting program: /usr/bin/xine
[...]
Program received signal SIG33, Real-time event 33.
[Switching to Thread 1182845264 (LWP 11543)]
0x00002b661d87d536 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib/libpthread.so.0
(gdb) handle SIG33 nostop noprint noignore pass
Signal Stop Print Pass to program Description
SIG33 No No Yes Real-time event 33
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) run
|
Il comando handle informa gdb su cosa dovrebbe fare quando i
segnali indicati vengono inviati al programma; in questo caso le opzioni sono
nostop (non fermare il programma restituendo il comando al debugger),
noprint (non preoccuparsi di informare alla ricezione dei dati segnali),
noignore (non ignorare il segnale — ignorare segnali è pericoloso,
in quanto significa scartarli senza passarli al programma), pass (passare
il segnale al programma di cui si sta effettuando il debug).
Dopo che gli eventuali eventi Real-Time sono stati ignorati da gdb, si
dovrebbe provare a riprodurre un crash (blocco o chiusura, terminazione inattesa
di un programma) che si desidera riportare. Se lo si può riprodurre
sistematicamente, risulta abbastanza semplice. Quando gdb informa che il
programma ha ricevuto un segnale SIGSEV o SIGABRT (o qualsiasi altro segnale che
potrebbe rappresentare la condizione di errore per il programma), si deve
effettivamente procedere per avere un backtrace, possibilmente salvandolo da
qualche parte. Il comando di base per fare questo è bt, che è una
scorciatoia per backtrace, il quale mostrerà il backtrace del thread
corrente (se il programma non è multi thread, esiste allora un solo thread).
Un comando alternativo per avere un backtrace più dettagliato è bt full.
Questo permette di avere anche informazioni riguardo a parametri e variabili
locali della funzione dove vengono effettuate le chiamate (quando questi sono
disponibili e non rimossi dalle ottimizzazioni). Ciò comporta una tracciatura
più lunga ma anche più utile quando si cerca di scoprire, per esempio, perchè un
puntatore non risulta inizializzato.
Ultimamente non è raro che perfino semplici programmi vengono scritti con thread
multipli, rendeno l'uso di un semplice output di bt, anche se
significativo, abbastanza inutile, in quanto potrebbe rappresentare lo stato di
un thread diverso da quello in cui il segnale è invocato, o da quello in cui si
manifesta la condizione di errore (nel caso in cui ci sia un altro thread
responsabile dell'invocazione di segnali). Per queste ragioni, si potrebbe
invece recuperare la traccia con il comando esteso thread apply all bt
full, che indica al debugger di riportare la tracciatura completa di tutti i
thread al momento in esecuzione.
Se il backtrace è corto, è facile fare copia e incolla fuori dal terminale (a
meno che il fallimento non avvenga in un terminale senza X), ma talvolta è
semplicemente troppo lungo per essere copiato con facilità, perchè si estende su
più pagine. Per poter indirizzare i backtrace su un file da affiancare al bug, è
possibile usare il sistema di logging:
Codice 1.4: Usare il sistema di logging per salvare il backtrace su file |
$ gdb /usr/bin/xine
GNU gdb 6.5
[...]
(gdb) run
[...]
(gdb) set logging file backtrace.log
(gdb) set logging on
Copying output to backtrace.log.
(gdb) bt
#0 0x0000003000eb7472 in __select_nocancel () from /lib/libc.so.6
...
(gdb) set logging off
Done logging to backtrace.log.
(gdb) quit
|
Ora è possibile prendere il backtrace dal file backtrace.log, e
inviarlo semplicemente tramite email o allegare il file al bug relativo.
Core dump
Talvolta non è facile riprodurre il fallimento (o meglio, la situazione di
crash), il programma sfrutta pesantemente i thread, la sua esecuzione in
gdb è troppo lenta o viene scombinata quando eseguito tramite di esso
(non dovrebbe sorprendere nessuno il fatto che eseguendo all'interno del
debugger ci sono più bug che risultano riproducibili senza il debugger stesso).
In questi casi, esiste uno strumento che viene in aiuto: il core dump.
Un core dump è un file contenente l'intera area di memoria di un programma
quando questo termina in malo modo. Usando questo file, è possibile estrarre il
backtrace dello stack anche se il programma è incappato in un crash fuori da
gdb, assumendo che i core dump siano abilitati. Di base i core dump non
sono abilitati su Gentoo Linux (lo sono, comunque, in modo predefinito su
Gentoo/FreeBSD),
quindi si ha la necessità di abilitarli.
I file core dump sono generati direttamente dal kernel; per questa ragione il
kernel ha bisogno che siano abilitate le relative opzioni in fase di
compilazione, per poter lavorare correttamente. Mentre tutte le configurazioni
di base abilitano i file core dump, nel caso in cui si stia eseguendo un kernel
embedded o si siano configurate per altre vie le caratteristiche elementari del
kernel, bisogna verificare le seguenti opzioni:
Nota:
Si può saltare questo passaggio nel caso in cui non si abbia abilitata l'opzione
"Configure standard kernel features", la quale non lo dovrebbe essere a meno
che non si sia certi di cosa si sta facendo.
|
Codice 1.5: Opzioni del kernel per abilitare i core dump |
General Setup --->
Configure standard kernel features --->
Enable ELF core dumps
|
I core dump possono essere abilitati a livello di sistema o a livello di
sessione di shell. Nel primo caso, tutto ciò che nel sistema termina in modo non
corretto e non ha già un gestore di tali situazioni (si veda più sotto per
maggiori note riguardo il gestore di crash di KDE) effettuerà il dump. Quando
abilitato a livello di sessione della shell, solo i programmi avviati nella
sessione si lasceranno dietro un dump.
Per abilitare i core dump a livello di sistema, bisogna modificare il file
/etc/security/limits.conf (se si utilizza PAM, come avviene
normalmente) oppure /etc/limits.conf. Nel primo caso, è necessario
definire un limite (sia stringente che, più comunemente, non vincolante; per i
file di core, questo può in ogni caso essere compreso fra 0 e no limit, nessun
limite). Nel secondo caso, basta impostare la variabile C alla dimensione limite
del file core (qua non c'è l'opzione "senza limite").
Codice 1.6: Esempio di regola per avere file core senza limite sulla dimensione usando PAM |
# /etc/security/limits.conf
* soft core unlimited
|
Codice 1.7: Esempio di regola per avere file core con dimensione massima pari a 20Mb quando non si usa PAM |
# /etc/limits.conf
* C20480
|
Per abilitare i file core in una singola sessione di shell è possibile usare il
comando ulimit con l'opzione -c. Il valore 0 significa
disabilitato; ogni altro numero positivo rappresenta la dimensione in KB del
file core generato, mentre unlimited semplicemente rimuove il limite
sulla dimensione del file core. Da questo punto in poi, tutti i programmi che
terminano a causa di un segnale come SIGABRT o SIGSEGV si lasceranno dietro un
file core che può essere chiamato sia "core" che "core.pid" (dove pid è
rimpiazzato con il pid attuale del programma che termina).
Codice 1.8: Esempio dell'uso di ulimit |
$ ulimit -c unlimited
$ crashing-program
[...]
Abort (Core Dumped)
$
|
Nota:
Il comando ulimit è un comando interno in bash e zsh. Su altre shell
potrebbe essere chiamato in modo diverso o potrebbe perfino non essere
disponibile per niente.
|
Dopo aver ottenuto un core dump, si può lanciare gdb su di esso,
specificando sia il percorso al file che ha generato il core dump (deve essere
lo stesso esatto binario, quindi se si ricompila, il core dump è inutilizzabile)
e il percorso al file core. Una volta che gdb è stato aperto su di esso,
si possono seguire le stesse istruzioni incontrate appena prima che queste
ricevessero il segnale di terminazione.
Codice 1.9: Eseguire gdb su un file core |
$ gdb $(which crashing-program) --core core
|
Come alternativa, si possono usare le potenzialità della riga di comando di
gdb per ottenere il backtrace senza entrare in modalità interattiva.
Questo rende anche più facile il salvataggio del backtrace in un file o il suo
invio ad una pipe di qualsiasi tipo. Il trucco risiede nelle opzioni
--batch e -ex che sono accettate da gdb. È possibile usare
la seguente funzione bash per avere l'intero backtrace di un core dump (inclusi
tutti i thread) sullo standard output stream.
Codice 1.10: Funzione per il recupero dell'intero backtrace da un core dump |
gdb_get_backtrace() {
local exe=$1
local core=$2
gdb ${exe} \
--core ${core} \
--batch \
--quiet \
-ex "thread apply all bt full" \
-ex "quit"
}
|
Note sul gestore di crash di KDE
Le applicazioni basate su KDE vengono eseguite in modo predefinito con il loro
personale gestore di crash, che viene presentato all'utente tramite il così
detto "Dr Konqi" se risulta installato (il pacchetto è o
kdebase-meta/kdebase o kde-base/drkonqi, incluso in
kdebase-meta). Questo gestore di crash mostra all'utente una
finestra informativa che lo informa sul fatto che il programma è terminato in
modo inatteso. In questa finestra c'è una linguetta denominata "Backtrace" che,
quando caricata, invoca gdb e fa si che questo carichi i dati e generi
l'intero backtrace per conto dell'utente, mostrando il tutto nella finestra
principale e dando la possibilità di salvarlo direttamente in un file. Questo
backtrace è normalmente sufficiente per riportare un problema.
Quando drkonqi non è installato, una terminazione inattesa non genererà in ogni
caso un core dump, e l'utente in modalità predefinita non riceverà nessuna
informazione. Per evitarlo, è possibile usare con ogni applicazione basata su
KDE l'opzione a riga di comando --nocrashhandler. Questo inibisce
totalmente il gestore di crash e lascia che i segnali vengano gestiti dal
sistema operativo come succede di solito. Ciò è utile per generare i file core
quando drkonqi non è disponibile o quando si vuole ispezionare le varie parti
dello stack a mano.
I contenuti di questo documento sono rilasciati sotto la licenza Creative
Commons - Attribution / Share Alike.
|