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.
Materiał udostępniany na podstawie licencji Creative Commons -
Attribution / Share Alike.
|