Gentoo Logo

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.



Stampa

Aggiornato il 16 giugno 2010

Oggetto: Questa guida è pensata per dare agli utenti una semplice spiegazione del perché una installazione di base di Gentoo non fornisca backtrace sensati e come fare in modo che ciò invece avvenga.

Diego E. Pettenò
Autore

Ned Ludd
Informazioni su hardened toolchain

Kevin Quinn
Informazioni su hardened toolchain e architettura x86

Donnie Berkholz
Revisione

Michele Caini
Traduzione

Donate to support our development efforts.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.