Gentoo Logo

Poprawne korzystanie ze śledzenia wstecznego w Gentoo

1.  Śledzenie wsteczne w Gentoo

Co to jest śledzenie wsteczne?

Śledzenia wsteczne (ang. backtrace), czasem również zwane bt, trace (śledzenie) lub stack trace (śledzenie stosu) jest zrozumiałym dla człowieka zapisem stosu wywołań programu. Podaje, w którym miejscu programu się znajdujemy oraz poprzez wywołania jakich funkcji został on osiągnięty -- w teorii aż do funkcji main(). Zapisy śledzenia wstecznego są z reguły analizowane, gdy błędy ochrony pamięci (ang. segmentation faults) lub awarie kończące się przerwaniem działania programu są sprawdzane debugerem, np. gdb (debuger GNU), w celu znalezienia ich przyczyny.

Zrozumiały zapis zawiera nie tylko obiekty współdzielone (ang. shared objects), w których nastąpiło wywołanie, lecz również nazwę funkcji, pliku i linię kodu, gdzie program się zatrzymał. Niestety, śledzenie wsteczne jest bezużyteczne, jeśli system został zoptymalizowany dla wyższej wydajności i mniejszego zużycia miejsca na dysku. Podaje ono wtedy wyłącznie wskaźniki na stos oraz serię ?? zamiast nazw i lokalizacji funkcji.

Celem tego poradnika jest przedstawienie, w jaki sposób uzyskać przydatne, zrozumiałe pliki śledzenia wstecznego w Gentoo, korzystając z własności Portage.

Flagi kompilatora

Standardowo gcc nie wbudowuje informacji debugowania do budowanych plików obiektowych (bibliotek i programów), ponieważ to je powiększa. Również wiele opcji optymalizacji interferuje z zapisem informacji debugowania. Z tych powodów, w pierwszej kolejności zwrócimy uwagę na to, jak powinna być ustawiona zmienna CFLAGS, aby generować użyteczne informacje debugowania.

Bazową flagą, którą należy dodać w tym przypadku, jest -g. Nakazuje ona kompilatorowi załączenie dodatkowych informacji w plikach obiektowych, takich jak nazwy plików i numery linii kodu. Zazwyczaj wystarcza to do uzyskania podstawowego zapisu śledzenia wstecznego, ale flaga -ggdb dodaje więcej informacji. W rzeczywistości istnieje też kolejna flaga (-g3), ale korzystanie z niej nie jest zalecane. Zdaje się uszkadzać zgodność binarną i może prowadzić do dodatkowych awarii. Na przykład, glibc ulega uszkodzeniu, jeśli jest budowane z tą flagą. Jeśli zamierzasz dostarczyć jak najwięcej informacji, powinieneś użyć flagi -ggdb.

Listing 1.1: Przykład zmiennej CFLAGS z flagami debugowania

CFLAGS="-march=k8 -O2 -ggdb"
CXXFLAGS="${CFLAGS}"

Optymalizacje również muszą być stosowane z umiarem. Przykładowo, flaga -O służąca do włączania jednego ze standardowych poziomów optymalizacji musi być ustawiana rozważnie. Jeśli potrzeba naprawdę dokładnego zapisu, należy użyć flagi -O1 (poziom optymalizacji 0 nie jest zalecany, gdyż znane są przypadki, gdy gcc generuje nieprawidłowy kod). Ale dla zwyczajnego śledzenia wstecznego, -O2 i -Os również się nadają. Włączanie -O3 dla debugowanego programu jest niemądre, ponieważ włącza dodatkowe optymalizacje, które uszkodzą informacje potrzebne do debugowania.

Wiadomość dla użytkowników architektury x86: mają oni często flagę -fomit-frame-pointer w zmiennej CFLAGS. Architektura x86 dysponuje ograniczoną liczbą rejestrów ogólnego zastosowania, a ta flaga może uwolnić dodatkowy rejestr, poprawiając wydajność. Nic jednak za darmo: uniemożliwia ona gdb "przejście stosu" -- innymi słowy, wiarygodny zapis śledzenia wstecznego. Usuń tę flagę z CFLAGS, by zbudować kod łatwiejszy do zrozumienia przez gdb. Większość pozostałych platform nie ma się czego obawiać; albo nie ustawia się tam zazwyczaj -fomit-frame-pointer, albo kod generowany przez gcc nie przeszkadza gdb w pracy. (W tym przypadku flaga jest już włączona przez poziom optymalizacji -O2.)

Użytkownicy Gentoo Hardened mają inne powody do zmartwień. Często zadawane pytania projektu Hardened dostarczą im dodatkowych wskazówek.

Usuwanie symboli debugowania

Samo zmienienie CFLAGS i przeinstalowanie world i tak nie umożliwi przydatnego śledzenia wstecznego, ponieważ trzeba wyłączyć usuwanie symboli debugowania. Standardowo Portage usuwa je z plików wyjściowych. Innymi słowy, usuwa sekcje niepotrzebne do ich uruchamiania by zmniejszyć ich rozmiar. Jest to dobre dla przeciętnego użytkownika, który nie potrzebuje śledzenia wstecznego, ale usuwa wszystkie informacje debugowania generowane z pomocą flag -g*, jak również tablice symboli wykorzystywane do wyświetlania zapisu śledzenia wstecznego w postaci zrozumiałej dla człowieka.

Istnieją dwa sposoby, aby powstrzymać usuwanie symboli przed utrudnianiem debugowania i generowania przydatnych zapisów śledzenia wstecznego. Pierwszy polega na nakazaniu Portage, by tego nie robiło, dodając nostrip do zmiennej FEATURES. Pozostawi to instalowane pliki w takiej postaci, w jakiej je stworzyło gcc, wraz ze wszystkimi informacjami debugowania i tablicami symboli, co zwiększy ilość miejsca na dysku zajmowanego przez pliki wykonywalne i biblioteki. Aby tego uniknąć, w wersji 2.0.54-r1 i serii 2.1 programu Portage, można zamiast tego dodać splitdebug do zmiennej FEATURES.

Mając włączone splitdebug, Portage nadal będzie usuwało symbole z instalowanych plików wyjściowych. Ale zanim to zrobi, wszystkie przydatne informacje debugowania są kopiowane do pliku ".debug", który jest instalowany do katalogu /usr/lib/debug (pełna nazwa tego pliku jest tworzona przez dodanie tego rozszerzenia do nazwy instalowanego pliku wyjściowego). Położenie pliku ".debug" jest następnie zapisywane w oryginalnym pliku w sekcji ".gnu_debuglink" formatu ELF, aby gdb mogło odczytać, z którego pliku ładować symbole.

Ważne: Jeśli włączysz zarówno nostrip , jak też splitdebug, Portage nie będzie wcale usuwać symboli debugowania, o co nie do końca mogło się rozchodzić.

Kolejną zaletą opcji splitdebug jest brak potrzeby przebudowania pakietu, by się pozbyć informacji debugowania. Jest to przydatne, jeśli budujesz niektóre pakiety z flagami debugowania, aby uzyskać zapis śledzenia pojedynczego błędu. Kiedy zostanie on już naprawiony, wystarczy usunąć katalog /usr/lib/debug.

Aby upewnić się, że symbole debugowania nie zostaną usunięte, trzeba sprawdzić, czy flaga -s nie jest ustawiona w zmiennej LDFLAGS. Nakazuje ona linkerowi usunąć symbole podczas fazy łączenia. Warto zauważyć, że korzystanie z tej flagi może prowadzić do dalszych problemów. Nie podlega ona ograniczeniu usuwania symboli ustawianym przez niektóre pakiety, które po tym zabiegu przestają działać.

Uwaga: Niestety, niektóre pakiety same usuwają symbole, wewnątrz plików Makefile dołączonych przez autorów. Jest to błąd i powinien być zgłoszony. Wszystkie pakiety powinny pozostawiać w kwestii Portage usuwanie symboli lub całkowicie wyłączać tą funkcję. Głównym wyjątkiem są pakiety binarne. Z reguły są one pozbawione symboli przez autorów, całkowicie poza kontrolą Portage.

Wprowadzenie do obsługi gdb

Kiedy pakiety zostały już zbudowane z informacjami debugowania, wystarczy tylko zapisać dane ze śledzenia wstecznego. Aby temu sprostać, trzeba zainstalować pakiet sys-devel/gdb. Zawiera on debuger GNU (gdb). Po jego instalacji, można kontynuować śledzenie wsteczne. Najprostszy sposób to uruchomienie programu wewnątrz gdb. Aby tak zrobić, trzeba podać gdb ścieżkę programu do uruchomienia, parametry, których on wymaga, a następnie nakazać wykonanie:

Listing 1.2: Wykonywanie ls poprzez 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)

Wiadomość "Program exited normally" (Program zakończył się normalnie) oznacza, że program zakończył wykonanie z kodem błędu 0. Jest to oznaką braku błędów. Nie należy na tym jednak za bardzo polegać, ponieważ występują programy wychodzące z takim kodem nawet, gdy wystąpi błąd. Kolejnym powszechnie występującym komunikatem jest "Program exited with code nn" (Program zakończył się kodem błędu nn). Podaje on po prostu, jaki niezerowy kod błędu został zwrócony. Może to oznaczać obsłużony lub spodziewany błąd. W przypadku błędów ochrony pamięci lub nieprzewidzianego przerwania, wyświetlona zostanie wiadomość "Program received signal SIG*" (Program odebrał sygnał SIG*).

Program może odebrać sygnał z wielu różnych powodów. W przypadku SIGSEGV i SIGABRT (odpowiednio błędu ochrony pamięci i nieprzewidzianego przerwania), zazwyczaj oznacza, że program wykonuje niedozwoloną operację, np. nieprawidłowe wywołanie systemowe (ang. syscall) lub próbuje uzyskać dostęp do pamięci przez niepoprawny wskaźnik. Innymi powszechnymi sygnałami są SIGTERM, SIGQUIT oraz SIGINT (ten ostatni jest wynikiem naciśnięcia klawiszy CTRL-C, i zwykle jest przechwytywany przez gdb oraz ignorowany przez program).

W końcu mamy też całą gamę "zdarzeń czasu rzeczywistego" (Real-Time events). Noszą one nazwy SIGnn, gdzie nn to liczba większa od 31. Biblioteka wątków pthread zazwyczaj korzysta z nich w celu synchronizacji między różnymi wątkami wykonania programu, zatem nie przedstawiają one sobą żadnego błędu. Łatwo można dostarczyć nic nieznaczący zapis śledzenia wstecznego myląc sygnały czasu rzeczywistego z wystąpieniem błędu. Aby temu zapobiec, można rozkazać gdb, by nie zatrzymywało wykonania, kiedy odbierze jeden z nich, lecz przekazało je do bezpośrednio do programu. Opisuje to poniższy przykład.

Listing 1.3: Uruchamianie xine-ui poprzez gdb, ignorując sygnały czasu rzeczywistego.

$ 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? [Zatrzymać debugowany program?] (y or n) y
(gdb) run

Polecenie handle powiadamia gdb, co ma robić, gdy dany sygnał został wysłany do programu; dostępne opcje to nostop (nie zatrzymuj programu, przekazując kontrolę debugerowi), noprint (nie wypisuj informacji o odebraniu danego sygnału), noignore (nie ignoruj sygnału -- ignorowanie sygnałów jest niebezpieczne, ponieważ oznacza ich odrzucenie bez przekazania programowi), pass (przekaż sygnał debugowanemu programowi).

Kiedy już możliwe zdarzenia czasu rzeczywistego będą ignorowane przez gdb, trzeba spróbować odtworzyć awarię, którą chcemy zgłosić. Jeśli występuje ona systematycznie, jest to dosyć proste. Gdy gdb powiadamia o otrzymaniu sygnału SIGSEGV lub SIGABRT (lub dowolnego innego mogącego oznaczać błąd programu), należy uruchomić śledzenie wsteczne, zapisując jego wynik w pliku. Podstawowym poleceniem do tego stosowanym jest bt, skrót od backtrace, co wyświetli zapis śledzenia wstecznego aktywnego wątku (program może być oczywiście jednowątkowy).

Alternatywnym poleceniem, stosowanym w celu otrzymania bardziej szczegółowego zapisu, jest bt full. Dostarcza ono również informacji o parametrach i zmiennych lokalnych funkcji, które były wywołane przed awarią (jeśli te informacje są dostępne, a nie usunięte przez optymalizacje). Sprawia to, że zapis śledzenia wstecznego jest dłuższy, ale bardziej przydatny w celu wykrycia, np. dlaczego wskaźnik nie jest zainicjalizowany.

Wreszcie, nawet proste programy nierzadko są napisane z zastosowaniem wielu wątków wykonania, co sprawia, że proste wyjście polecenia bt, mimo dużej liczby informacji, jest bezużyteczne, ponieważ może przekazać stan innego wątku niż ten, z którego został wysłany sygnał, lub w którym wystąpił błąd (jeśli inny wątek odpowiada za wysyłanie sygnałów). Z tego powodu należy zastosować dłuższe polecenie thread apply all bt full, które nakazuje wypisanie pełnego zapisu śledzenia wszystkich aktualnie uruchomionych wątków.

Jeśli zapis śledzenia jest krótki, łatwo można go skopiować z terminala (chyba, że awaria występuje na komputerze bez X), ale czasami jest on zbyt długi, zajmując wiele stron. Aby uzyskać plik z zapisem śledzenia wstecznego, który następnie można dołączyć do raportu, można skorzystać z funkcji logging:

Listing 1.4: Zastosowanie polecenia logging w celu zapisania wyniku śledzenia wstecznego do pliku

$ 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

Teraz można znaleźć zapis w pliku backtrace.log i zwyczajnie wysłać go pocztą elektroniczną lub dołączyć do odpowiedniego raportu.

Zrzuty pamięci

Czasami trudno odtworzyć awarie, program składa się z wielu wątków, za wolno jest wykonywany przez gdb lub popsuty podczas uruchamiania za jego pomocą. (nie powinno nikogo zaskoczyć, że przy uruchamianiu przez debuger występuje więcej problemów, niż bez niego). W takich przypadkach, mamy do dyspozycji przydatne narzędzie: plik zrzutu pamięci (ang. core dump).

Plik zrzutu pamięci zawiera obraz całego obszaru pamięci dostępnego programowi w chwili awarii. Za pomocą tego pliku, można wydobyć zapis śledzenia stosu nawet jeśli program uległ defektowi poza gdb, zakładając, że zrzuty pamięci są włączone. Standardowo, nie są one aktywne w Gentoo Linux (lecz przeciwnie w Gentoo/FreeBSD), więc trzeba je włączyć.

Zapis zrzutów pamięci można włączyć dla całego systemu lub dla sesji powłoki. W pierwszym przypadku, cokolwiek w systemie ulegnie awarii i nie ma programu do ich obsługi (więcej informacji o programie obsługi awarii KDE dalej), dokona takiego zrzutu. Jeśli zostanie aktywowane dla sesji, tylko programy w niej uruchomione wygenerują plik zrzutu.

By włączyć zrzuty pamięci dla całego systemu, trzeba zmodyfikować albo plik /etc/security/limits.conf, jeśli stosowana jest biblioteka PAM, jak jest w standardzie, albo /etc/limits.conf. W pierwszym przypadku, trzeba podać ograniczenie (twarde, lub częściej miękkie) rozmiaru pliku zrzutu. Może się on zawierać pomiędzy 0 i brakiem ograniczenia (ang. unlimited). W drugim przypadku, należy ustawić zmienną C na ograniczenie rozmiaru pliku zrzutu -- nie ma opcji jego braku.

Listing 1.5: Przykład reguły PAM pozwalającej na tworzenie plików zrzutu dowolnej wielkości

# /etc/security/limits.conf
*             soft      core       0

Listing 1.6: Przykład reguły pozwalającej na tworzenie plików zrzutu do 20MB -- bez stosowania PAM

# /etc/limits.conf
*       C20480

Aby włączyć zapisywanie zrzutów pamięci w danej sesji powłoki, można skorzystać z polecenia ulimit z opcją -c. 0 oznacza wyłączenie generowania; dowolna liczba dodatnia to maksymalny rozmiar tworzonego pliku zrzutu w KB, unlimited oznacza brak ograniczenia. Od tego momentu, wszystkie programy kończące wykonanie z powodu sygnału typu SIGABRT lub SIGSEGV będą pozostawiać pliki zrzutu nazwane "core" lub "core.pid" (gdzie pid to identyfikator procesu programu, który uległ awarii).

Listing 1.7: Przykład zastosowania polecenia ulimit

$ ulimit -c unlimited
$ awaryjny-program
[...]
Abort (Core Dumped) [Przerwano (Utworzono plik zrzutu)]
$

Uwaga: Polecenie ulimit to wewnętrzna komenda powłok bash i zsh. W innych może nazywać się inaczej lub nawet wcale nie być dostępna.

Po otrzymaniu zrzutu pamięci, można uruchomić gdb, podając ścieżkę do programu, który wygenerował zrzut (musi to być dokładnie taki sam plik wykonywalny -- jeśli w międzyczasie go przebudujesz, zrzut jest bezużyteczny) oraz ścieżkę do pliku zrzutu. Kiedy już będzie otwarty w gdb, można postępować zgodnie z instrukcjami podanymi powyżej, jak z programem, który otrzymał sygnał zatrzymujący wykonanie.

Listing 1.8: Uruchamianie gdb z plikiem zrzutu pamięci

$ gdb $(which awaryjny-program) --core core

Alternatywnie, można zastosować opcje wiersza poleceń programu gdb, aby uzyskać zapis śledzenia wstecznego bez przejścia w tryb interaktywny. Ułatwia to zapisanie wyniku śledzenia do pliku lub przesłania go przez dowolny potok. Kluczowe są tu opcje --batch i -e akceptowane przez gdb. Można skorzystać z następujących funkcji bash, by otrzymać pełen zapis śledzenia wstecznego na bazie zrzutu pamięci (włącznie ze wszystkimi wątkami) na standardowym wyjściu.

Listing 1.9: Funkcja wypisująca pełen wynik śledzenia wstecznego na podstawie zrzutu pamięci

gdb_get_backtrace() {
    local exe=$1
    local core=$2

    gdb ${exe} \
        --core ${core} \
        --batch \
        --quiet \
        -ex "thread apply all bt full" \
        -ex "quit"
}

Informacje o programie obsługi awarii KDE

Aplikacje KDE są standardowo uruchamiane z własnym programem obsługi awarii, który przedstawia się użytkownikowi jako "Dr. Konqi", jeśli ten program jest zainstalowany (pakiet kde-base/kdebase lub kde-base/drkonqi -- zawarty w kdebase-meta). Ten program wyświetla okno informacyjne informujące o awarii programu. Można w nim znaleźć zakładkę "Śledzenie wsteczne", która wywołuje gdb przy otwarciu i automatycznie ładuje dane oraz generuje pełen zapis śledzenia wstecznego, wyświetlając go w głównym polu tekstowym i umożliwiając bezpośredni zapis do pliku. Jest on zwykle wystarczający do zgłoszenia problemu.

Jeśli drkonqi nie jest zainstalowany, awarie nie będą generować zrzutu pamięci, a użytkownik nie otrzyma żadnej informacji. Aby tego uniknąć, można przekazać każdej z aplikacji KDE opcję wiersza poleceń --nocrashhandler. Wyłącza ona całkowicie program obsługi awarii i pozostawia obsługę sygnałów systemowi operacyjnemu. Jest to przydatne do generowania plików zrzutu pamięci, gdy drkonqi nie jest dostępny, lub w celu ręcznego przejrzenia stosu wywołania.



Drukuj

Zaktualizowano 2 grudnia 2006

Podsumowanie: Ten poradnik ma na celu wyjaśnienie użytkownikom, dlaczego standardowa instalacja Gentoo nie dostarcza znaczących plików logowania śledzenia wstecznego oraz informuje, w jaki sposób można je uzyskać.

Diego Pettenò
Autor

Ned Ludd
Informacje dot. Hardened

Kevin Quinn
Informacje dot. Hardened i architektury x86

Donnie Berkholz
Redaktor

Radosław Szkodziński
Tłumacz

Donate to support our development efforts.

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