Strona główna. |
Następny rozdział: Odpluskwianie w systemach opartych na Windows NT (KD, WinDbg). |
Poprzedni rozdział: Narzędzia do wykrywania wycieków pamięci w programie i do profilowania kodu. |
Z programowaniem w jądrze wiążą sie jedyne w swoim rodzaju wyzwania dla odpluskwiania.
Kod jądra nie może być bezpośrednio wykonywany w debugerze, ani śledzony w prosty sposób, bo cześć funkcjonalności nie jest związana z konkretnym procesem.
Należy brać pod uwagę wielowątkowość jądra.
Interakcje między jądrem, przestrzenią użytkownika i sprzętem są bardzo delikatną kwestią.
Czasem bardzo trudno jest powtórzyć błąd, który już raz zaobserwowaliśmy i na dodatek może on spowodować awarię całego systemu, w ten sposób niszcząc świadectwo swojego istnienia.
W przypadkach specyficznego kodu tzw. race conditions mogą wystąpić tylko raz na milion iteracji algorytmu.
Im więcej informacji zbierzemy, kiedy błąd wystąpi tym lepiej. A jeśli uda nam się powtórzyć ten sam błąd, to już połowa sukcesu.
Zazwyczaj dopiero długi łańcuch zdarzeń prowadzi od błędu w kodzie do błędu zauważonego przez użytkownika. Np. współdzielona struktura bez licznika użytkowników może doprowadzić do race conditions. Jeden proces może ją zwolnić, podczas gdy inny nadal chce z niej korzystać. Może to powodować takie błędy jak odwołanie się do miejsca w pamięci o adresie 0 (NULL pointer dereference (co da w wyniku komunikat oops), czy odczytanie śmieci zamiast prawidłowych danych (w najlepszym przypadku niepoprawne działanie systemu z uszkodzeniem danych włącznie, później może i tak doprowadzić do oops'a).
Niedokładnie przemyślany, czy nawet jedynie źle skompilowany program może poprawnie działać na jednym systemie, podczas gdy na innym powoduje krytyczny błąd (dostajemy wtedy komunikat oops). Wystarczy tu wspomnieć o architekturze SMP.
Mimo iż sam mistrz Linus stwierdził:
I'm afraid that I've seen too many people fix bugs by looking at debugger output,
and that almost inevitably leads to fixing the symptoms rather than the underlying problems.
szczególnie dla słabo doświadczonych programistów jądra debugowanie może być
dobrym rozwiązaniem. Poniżej opiszemy kilka technik,
które sprawdzają się podczas odpluskwiania jądra.
Kiedy zaczynamy myśleć o debugowaniu jądra, warto je najpierw przekompilować, ponieważ jego twórcy wbudowali w nie kilka cech wspomagających odpluskwianie (a zwalniających działanie samego systemu, dlatego niedostępnych w standardowo instalowanych dystrybucjach). Większość wymienionych poniżej opcji można znaleźć w menu "kernel hacking" dostępnym podczas konfiguracji jądra. Nie wszystkie z opcji są też wspierane przez każdą architekturę (wylistowane poniżej są dostępne dla jądra 2.6).
Zaznaczenie tej opcji udostępnie inne opcje, choć ona sama nic nie daje.
Jest to kluczowa opcja, która włącza różne rodzaje sprawdzania alokacji pamięci i pozwala wykrywać nadpisanie buforów czy korzystanie z niezainicjalizowanej pamięci. Każdy zaalokowany bajt jest ustawiany na 0xa5 przed przekazaniem do funkcji alokującej pamięć, a ustawiany na 0x6b podczas zwalniania. Jeśli zauważymy te wartości w tym co wypisuje nasz sterownik, albo oops, będziemy wiedzieli na czym polega problem. Wykonywane są też inne sprawdzenia, np. jądro alokuje kawałki pamięci przed i za każdym zaalokowanym przez nas fragmentem, żeby wykryć błędne odczyty i zapisy.
Przy zwalnianiu pamięci z przestrzeni jądra są usuwane całe ramki. To może wszystko znacznie zwolnić, ale pomaga wykryć niektóre problemy z pamięcią.
Jądro wyłapuje działania na niezainicjalizowanym spinlocku i inne błędy, np. podwójne zwolnienia blokad.
Wykrywanie przypadków, kiedy możemy spać ze spinlockiem. Dostaniemy ostrzeżenie nawet wtedy, kiedy tylko potencjalnie możemy zasnąć, nie musi to koniecznie nastąpić.
Służy do wykrywania nielegalnych prób dostępu do danych, które mogą być zmieniane jedynie w trakcie ładowania systemu/modułu.
Jądro będzie zbudowane z włączonymi danymi do debugowania. Musimy zaznaczyć tą opcję, jeśli chcemy odpluskwiać jądro z użyciem gdb.
Włącza klawisz "magic SysRq" opisany dalej.
Te opcje służą do wykrywania przepełnienia stosu. Pierwsza dodaje jawne sprawdzenia przepełnienia, a druga powoduje, że jądro zbiera statystyki odnośnie użycia stosu, które będą dostępne przy wykorzystaniu "magic SysRq".
Opcja z "General setup/Standard features". Jest domyślnie ustawiana i powoduje, że tablica symboli jądra jest wbudowana w jądro. Bez tego wszystkie informacje wypisane przez oops są w systemie szesnastkowym i niewiele nam powiedzą.
Opcje z "General setup" powodują, że konfiguracja jądra jest dostępna przez /proc. To jest użyteczne wtedy, gdy nie do końca wiemy w jakiej konfiguracji zbudowano jądro (np. ktoś inny buduje je dla nas).
Jest jeszcze kilka dostęnych opcji, ale nie są one tak istotne. W książce [2] poleca się następujące ustawienie opcji:
CONFIG_PREEMPT=y CONFIG_DEBUG_KERNEL=y CONFIG_KALLSYMS=y CONFIG_DEBUG_SPINLOCK_SLEEP=y
Funkcjonalność printk jest podobna do tej znanej z printf, z tym że jest dostępna w trybie jądra. Ale są też pewne różnice.
Jedną z różnic jest to, że printk pozwala klasyfikować komunikaty i nadawać im różne priorytety (tzw. loglevels). Np.:
printk(KERN_DEBUG "Here i am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
Dostępne jest 8 priorytetów, które są zdefiniowane w pliku <linux/kernel.h> i rozwijają się do napisu "<nr>". Są to:
Zazwyczaj używany do komunikatów poprzedzających awarię systemu. Najbardziej istotny komunikat - definiowany jako "<0>".
Sytuacje wymagające natychmiastowej uwagi.
Sytuacje krytyczne, najczęściej związane z poważnym błędem sprzętu lub oprogramowania.
Błąd; sterowniki urządzeń używają tego do powiadamiania o problemie ze sprzętem.
Ostrzeżenia o problemach, które nie powodują awarii całego systemu.
Sytuacje, które nie są błędami, ale warto je zasygnalizować. Często komunikaty o bezpieczeństwie wypisywane są na tym poziomie.
Komunikaty informacyjne. Często sterowniki wypisują w ten sposób dane o znaleznionym sprzęcie.
Używane do debugowania. Najmniej istotny komunikat - definiowany jako "<7>"
dmesg
).
printk umieszcza komunikaty w cyklicznym buforze (z nadpisywaniem najstarszych komunikatów po całkowitym zapełnieniu), który ma __LOG_BUF_LEN bajtów (od 4KB do 1MB, można to wybrać konfigurując jądro, dla systemów jednoprocesorowych jest to domyślnie 16KB). Po umieszczniu komunikatu funkcja budzi procesy czekające na ten komunikat, czyli każdy proces, który odwoływał się do syslogd'a albo czytał plik /proc/kmsg. Demon klogd wyjmuje komunikaty z tego bufora i dostarcza je do syslogd'a, który z kolei przekazuje je użytkownikowi w sposób zależny od swojej konfiguracji (opisanej w pliku /etc/syslog.conf).
Dla rozwiązania problemu nadmiernej ilości komunikatów można samemu ją ograniczyć. Dla funkcji, które wykonywane są bardzo często (nawet wiele razy na sekundę), warto wywoływać printk tylko raz na kilka sekund, wykorzystując np. (kod z [2]; wypisywanie komunikatu nie częściej niż co 2 sekundy):
static unsigned long prev_jiffy = jiffies; /* rate limiting*/ if (time_after(jiffies, prev_jiffy + 2*HZ)) { prev_jiffy = jiffies; printk(KERN_ERR "blah blah blah\n"); }
Czasem, gdy funkcja jest po prostu wielokrotnie wywoływana, a nie zależy
nam na komunikacie za każdym razem, można (podobnie jak wyżej) na statycznej
zmiennej trzymać licznik i wypisywać tylko kilka pierwszych komunikatów
(względnie wypisywać np. co setny, wykorzystując operację modulo:
if (licznik++ %= 100) ...
)
Rozwiązeniem podanym dla jądra 2.6 w [1] jest użycie
funkcji int printk_ratelimit(void)
, która zapewnia funkcjonalność
obu powyższych rozwiązań (poza operacją modulo), jednak dla kontroli
wartości (co ile sekund dopuszczany jest następny komunikat i
ile komunikatów ma być wypisanych) wymaga modyfikacji plików systemu
procfs (odpowiednio /proc/sys/kernel/printk_ratelimit
i /proc/sys/kernel/printk_ratelimit_burst). Później korzystanie
z funkcji sprowadza się jedynie do:
if (printk_ratelimit( )) // funkcja zwraca coś != 0 jeśli komunikat jest dopuszczalny printk("...");
Nadmierne używanie printk bardzo zwalnia system,
ponieważ Niektóre problemy można rozwiązać przez obserwowanie wykonania programu
w przestrzeni użytkownika. Możemy się też w ten sposób upewnić,
że np. nasz moduł robi to, co powinien. Do takiego śledzenia wykorzystuje
się programy strace i ltrace.
Jest to potężne narzędzie, które pokazuje nam,
jakie wywołania systemowe wystąpiły w programie przestrzeni użytkownika
(zazwyczaj otwieranie/zamykanie plików, dostęp do sieci, sprawdzanie
uprawnień do plików itp.).
Pokazuje nie tylko nazwy wywołań, ale także ich argumenty w symbolicznej formie.
Kiedy wywołanie zakończy się błędem, wypisana zostanie
symboliczna nazwa błędu (np. ENOMEM) i odpowiadający jej napis
("Out of memory").
Polecenie strace ma wiele
przydatnych opcji takich, jak (dla pełnego spisu odsyłam do man'a):
Strace uzyskuje te informacje bezpośrednio z jądra,
więc program może być testowany nawet, jeśli skompilowano go bez wsparcia
dla debugowania. Dane wygenerowane przez strace są zazwyczaj
dołączane do raportów o błędach wysyłanych do ludzi rozwijających daną aplikację.
Czasem wystarczy spojrzeć kilka wywołań funkcji wstecz, od wywołania funkcji,
która daje błędny wynik, aby znaleźć powód błędu.
Przykładowy wynik działania strace może być taki:
(przykład z [1],
wynik dla polecenia
Istotna jest również możliwość podłączenia się do działającego w systemie procesu.
Należy w tym celu podać PID procesu ( Ponadto warto zwrócić uwagę na możliwość zwiększenia ilości znaków
pokazywanych w łańcuchach znaków (standardowo 32 znaki, potem "..." -
patrz listing wyżej) przy użyciu opcji -s.
Przydatna, a czasem
wręcz niezbędna jest opcja -f, która powoduje, że strace
śledzi nie tylko proces macierzysty programu (tak jak to jest standardowo), ale
również dzieci tego procesu.
Bardzo podobny do strace. Nie śledzi on jednak wywołań systemowych
(system calls - stąd s w nazwie), a wywołania funkcji
bibliotecznych (library calls - stąd l w nazwie).
Większość opcji i możliwości pozostaje taka sama. W dodatku można połączyć
funkcjonalność obu programów, wywołując
Należy również pamiętać, że funkcje biblioteczne
(w przeciwieństwie do funkcji systemowych) odnoszą się bardziej
do trybu użytkownika, niż trybu jądra. Zwykle są to standardowe funkcje
biblioteczne C (jak np. glibc).
Nawet jeśli użyjemy wszystkie techniki do monitorowania kodu,
czasem błąd i tak powstanie, a wtedy najważniejsze jest zebranie o nim
jak najwięcej informacji. Na szczeście Linux radzi sobie dobrze z większością
błędów i zazwyczaj kończą się one zabiciem wykonującego się
procesu, ale sam system działa dalej. Możemy też dostać błąd "kernel panic",
który jest znacznie poważniejszy (zdarza się np. gdy mamy oops'a,
który powstał w trakcie obsługi przerwania i powoduje zawieszenie systemu;
może wystąpić też wtedy, gdy oops pojawi się w procesie
idle (PID=0) albo init (PID=1)), ale jeśli błąd jest
np. w sterowniku urządzenia, to kończy się najczęściej na śmierci procesu,
który akurat z urządzenia korzystał. Mimo że oops nie powoduje
awarii całego systemu, po jego wystąpieniu musimy go zrestartować.
Zły sterownik może pozostawić dane jądra w niespójnym stanie albo
popsuć pamięć w losowych miejscach.
Jeśli nie jesteśmy do końca pewni czy nic ważnego nie zostało zniszczone,
najlepiej zrestartować system.
Większość błędów występuje, kiedy wykonamy nieprawidłową operację na
wskaźnikach, np. będziemy próbować wyciągnąć dane spod wskaźnika równego
NULL. Zazwyczaj wynikiem takiego działania jest błąd oops.
Prawie każdy wirtualny adres, który używamy odnosi się do jakiegoś adresu
fizycznego, poprzez strukturę tablic stron. Kiedy korzystamy
z nieprawidłowego wskaźnika, systemowi stronnicowania nie udaje się zamapować
wskaźnika do adresu fizycznego i sygnalizowany jest błąd braku strony.
Jeśli brakującej strony nie ma, generowany jest błąd oops,
a odpowiednia wiadomość zostaje wypisana na konsolę.
oops zawiera informacje o stanie procesora w momencie,
kiedy wystąpił błąd, włącznie z zawartością rejestrów i innymi informacjami,
które dość trudno odczytać bez użycia jakiś dodatkowych narzędzi.
Przykładowy oops może wyglądać tak (odwołujemy się do adresu 0):
Najbardziej przydatna jest sekcja "Call trace", która wskazuje,
w jakiej funkcji wystąpił błąd i jakie funkcje ją wywołały.
Podobnie istotna jest wartość rejestru EIP, który zawiera adres
instrukcji, która spowodowała błąd.
Ten błąd możemy otrzymać w wyniku wywołania następującej funkcji:
Błąd możemy otrzymać też za pomocą takiej funkcji:
Ta funkcja kopiuje string do zmiennej lokalnej. Tablica, do której
kopiujemy jest za krótka, żeby zmieścić cały napis. W wyniku tego
mamy przepełnienie bufora i następujący komunikat:
Jądro twierdzi, że mamy nieprawidłową wartość rejestru EIP.
To i fakt, że ta wartość występuje na początku stosu świadczy o tym,
że stos jest popsuty.
Czasem programista może chcieć spowodować oops'a lub nawet
błąd krytyczny ("kernel panic"), np. jeśli sam wykryje nieprawidłową
sytuację w jakejś funkcji swojego sterownika, a chce uniknąć
zniszczenia danych jądra, czy nawet (w rzadkich przypadkach)
uszkodzenia sprzętu. Zazwyczaj zaznacza się
po prostu w ten sposób w kodzie sytuacje, które nie powinny się wydarzyć.
Jest to też swego rodzaju sposób na automatyczną dokumentację.
Dla powstania komunikatu oops wykorzystuje się makra
BUG() i BUG_ON() (ich implementacja jest zależna
od architektury), przy czym
Komunikat oops można przekazać do programu ksymoops,
który spróbuje przekonwertować niezbyt czytelne informacje m.in. na temat
ostatnio wywoływanej funkcji na wartości symboliczne.
W wielu przypadkach taka informacja już nam może wystarczyć. Potrzeba jednak
najpierw dostarczyć temu narzędziu odpowiednich danych:
Odpluskwianie przez obserwowanie
strace
strace ls /dev > /dev/scull0
)
open("/dev", O_RDONLY|O_NONBLOCK|O_LARGEFILE|O_DIRECTORY) = 3
fstat64(3, {st_mode=S_IFDIR|0755, st_size=24576, ...}) = 0
fcntl64(3, F_SETFD, FD_CLOEXEC) = 0
getdents64(3, /* 141 entries */, 4096) = 4088
[...]
getdents64(3, /* 0 entries */, 4096) = 0
close(3) = 0
[...]
fstat64(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(254, 0), ...}) = 0
write(1, "MAKEDEV\nadmmidi0\nadmmidi1\nadmmid"..., 4096) = 4000
write(1, "b\nptywc\nptywd\nptywe\nptywf\nptyx0\n"..., 96) = 96
write(1, "b\nptyxc\nptyxd\nptyxe\nptyxf\nptyy0\n"..., 4096) = 3904
write(1, "s17\nvcs18\nvcs19\nvcs2\nvcs20\nvcs21"..., 192) = 192
write(1, "\nvcs47\nvcs48\nvcs49\nvcs5\nvcs50\nvc"..., 673) = 673
close(1) = 0
exit_group(0) = ?
ps
powinno
wystarczyć, aby do niego dotrzeć) po opcji -p:
strace -p <pid>
ltrace
lstrace -S
- w ten
sposób dostaniemy informacje o wywołaniach zarówno funkcji systemowych
jak i funkcji bibliotecznych. Opcja -e w strace
jest jednak znacznie bardziej rozwinięta, przez co dla ograniczania
typu śledzonych wywołań systemowych lepiej użyć właśnie tego programu.
Odpluskwianie błędów w systemie
Oops (ksymoops)
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[
ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
int ret;
char stack_buf[4];
/* Let's try a buffer overflow */
memset(stack_buf, 0xff, 20);
if (count > 4)
count = 4; /* copy 4 bytes to the user */
ret = copy_to_user(buf, stack_buf, count);
if (!ret)
return count;
return ret;
}
EIP: 0010:[<00000000>]
Unable to handle kernel paging request at virtual address ffffffff
printing eip:
ffffffff
Oops: 0000 [#5]
SMP
CPU: 0
EIP: 0060:[
if (blad) BUG();
jest
równoważne BUG_ON(blad);
.
Dla błędów krytycznych
podaje się dodatkowo komunikat, który ma być wypisany przed
wstrzymaniem systemu:
if (blad_krytyczny) panic("...");
ksymoops uruchomiony bez dodatkowych opcji, ładuje wymienione wyżej
pliki ze standardowych miejsc
w systemie, na którym jest uruchamiany.
Nieeksportowane symbole modułów próbuje wyciągnąć
z plików objektowych modułów o ile znajdują się w standardowym katalogu
(/lib/modules/...) - w innym wypadku warto podać (opcja -o)
ścieżki samodzielnie. Do wyciągania symboli z plików używa programu nm
(do użycia
(pliki są wymienione w kolejności ważności,
czyli m.in. symbole z vmlinux przysłaniają te z /proc/ksyms):
nm <plik>
).
Często nie unikniemy ostrzeżeń, np. jeśli
informacje o symbolach w wymienionych wyżej źródłach się nie zgadzają
(zachodzi wtedy wspomniane przysłanianie),
lub używamy modułu skompilowanego nie pod naszą wersję jądra.
Dokładne instrukcje na temat użycia ksymoops'a są w źródle
jądra Documentation/oops-tracing.txt oraz na stronie man'a.
Analiza konkretnego komunikatu znajduje się w artykule http://www-128.ibm.com/developerworks/linux/library/l-debug/, w rozdziale "Oops analysis"
Jeśli system się zawiesi, żadna wiadomość nie jest wypisana. Przykładowo program wpadnie w nieskończoną pętlę, jądro nie będzie mogło go wywłaszczyć i system przestanie reagować na jakiekolwiek akcje, w tym kombinację Alt+Ctrl+Del. Są dwie metody radzenia sobie z takimi sytuacjami: można ich unikać (np. wstawiając wywołania schedule() w strategicznych miejscach kodu) albo próbować debugować je już po fakcie.
Bardzo przydatnym narzędziem jest klawisz "magic SysRq". Jest on uruchamiany przez kombinację klawiszy Alt+SysRq. Trzecim klawiszem, który naciśniemy razem z tymi dwoma może być:
Wyświetla informację pomocy (listę wszystkich możliwych klawiszy/opcji).
Przełącza klawiaturę w tryb surowy.
Powoduje zabicie wszystkich pocesów, które działają na danym terminalu.
Synchronizacja dysków.
Próba odmontowania i ponownego zamontowania dysków w trybie tylko do odczytu. Pozwala to uniknąć późniejszego sprawdzania systemu plików.
Natychmiast restartuje system (przed tym powinniśmy wykonać kombinację z s i u).
Wypisuje informację o rejestrach procesora.
Wypisuje informację o pamięci.
Powyższa lista nie jest kompletna. Szczegółowe informacje można znaleźć w Documentation/sysrq.txt. Wykonanie kolejno kombinacji z s, u i b jest bezpieczniejszym zakończeniem pracy systemu niż zwykłe wciśnięcie przycisku Reset. Opcja "magic SysRq" musi być jawnie zaznaczona podczas konfiguracji jądra, bo większość dystrybucji domyślnie ją wyłącza.
Użycie odpluskwiacza do przechodzenia kodu krok po kroku, obserwowania wartości siedzących w zmiennych i rejestrach jest zajęciem czasochłonnym i powinno się go unikać, o ile to możliwe. Jądro działa w swojej własnej przestrzeni, na rzecz wszystkich procesów systemu. Część funkcjonalności odpluskwiaczy trybu użytkownika, taka jak pułapki czy przechodzenie do kolejnych instrukcji danego procesu, nie daje się tak łatwo przenieść do debugowania jądra.
Wykorzystanie gdb w celu debugowania działającego systemu
(o debugowaniu plików zrzutu pamięci - core, była mowa w
pierwszym punkcie dokumentu) wymaga
sporej wiedzy o komendach tego odpluskwiacza
(poza standardową wiedzą z debugowania w trybie użytkownika),
a także umiejętności dopasowywania kodu źródłowego
do zoptymalizowanego kodu maszynowanego - dla większości śmiertelników
jest to chyba rzecz nie do przejścia (a przynajmniej perspektywa
wielogodzinnej pracy na takim poziomie nie jest najciekawsza).
Nie zaleca się używania środowisk graficznych do debugowania jądra -
można w ten sposób nie zauważyć istotnych danych wypisywanych na konsolę
czy nawet oops'a.
Standardowo (w konsoli) zaczyna się od komendy
gdb vmlinux /proc/kcore
, gdzie
vmlinux to nieskompresowany obraz jądra (żadem zImage czy bzImage).
Po tym można wykonywać różne komendy gdb do odczytu danych
(z deasemblacją funkcji włącznie).
Modyfikacja danych jądra jest niedostępna. Podobnie wspomniana wyżej
standardowa funkcjonalność (pułapki itd.). Do jądra 2.6.7 niemożliwe było
również debugowanie modułów (które nie są zawarte w przekazywanym
gdb obrazie). Jak widać są tu głównie ograniczenia, które
rozwiązują dopiero kgdb i kdb, o których mowa dalej.
kgdb to łata na jądro, która umożliwia jego debugowanie z wykorzystaniem pełni możliwości gdb. Istnieją nawet specjalne wersje rozszerzające tą funkcjonalność o wykonywanie funkcji. Jest tylko jedno "małe" ale: potrzebne są dwie maszyny! Debugowanie jest bowiem zdalne.
Dzięki zdalnemu debugowaniu możemy szukać błędów już podczas ładowania systemu (w szczególności podczas ładowania modułów). Można również debugować kilka systemów jednocześnie, podłączając hosta do kilku maszyn docelowych. Dla każdej takiej maszyny na hoście będzie uruchomiony oddzielny proces gdb, stąd komputer ten powinien mieć odpowiednią ilość RAM'u (conajmniej 128MB). Dla połączenia szeregowego jest wsparcie jedynie poprzez kable typu nullmodem (o innych nie ma mowy na stronie programu). Najnowsze wersje kgdb umożliwiają również pracę poprzez ethernet. Dla debugowania modułów wystarczy, jeśli komputery są w sieci (wykorzystanie rcp i rsh).
Na hoście, w uruchomionym terminalu gdb
uzyskamy dostęp do wszystkich komend i zatrzymamy debugowany system
kombinacją klawiszy Ctrl+C. W takim stanie wszystkie procesory
debugowanego systemu będą kontrolowane przez kgdb i żadne
inne części jądra nie będą wykonywane.
Wyjście z tego stanu uzyskujemy komendą (gdb) continue
.
Dalej opisze krótko użycie gdb w stosunku
do standardowego użycia dla debugowania trybu użytkownika.
(gdb) info thr
wyświetli wątki
ze specjalnymi identyfikatorami (czasem różnymi od PID'ów),
którymi można się dalej posługiwać w analizie konkretnych wątków.
Na stronie kgdb znajduje się też sporo dokumentacji odnośnie różnych architektur, na które odpluskwiacz jest dostępny (x86_64, PowerPC, Linux/s390).
Alternatywa do kgdb, w przeciwieństwie do którego nie jest odpluskwiaczem zdalnym. kgdb to podobnie łata na jądro, która rozszerza je umożliwiając debugowanie systemu.
Samo uruchomienie (po nałożeniu łaty) jest banalnie proste. Wystarczy wcisnąć klawisz break w konsoli. Dodatkowo odpluskwiacz uruchamia się automatycznie, jeśli wywołany zostanie oops. Więcej w dokumentacji: Documentation/kdb (po nałożeniu łaty) i na stronie domowej: http://oss.sgi.com.
User-Mode Linux to zbiór łat na jądro Linuxa, pozwalających na jego kompilację do postaci zwykłego pliku wykonywalnego, uruchamianego w trybie użytkownika (stąd nazwa).
UML ma oprócz debugowania jądra także inne zastosowania...
... ale nie będziemy się tu nimi zajmować.
Więcej informacji:
Rozpakowujemy źródła wybranej wersji jądra
(najlepiej gdzieś
poza katalogiem /usr/src/linux):
host% mkdir ~/uml host% cd ~/uml host% tar -xjvf linux-2.4.31-1.tar.bz2
Aplikujemy do nich łaty UML:
host% cd ~/uml/linux host% bzcat uml-patch-2.4.27-1.bz2 | patch -p1
Konfigurujemy i kompilujemy dla architektury 'um'
host% make menuconfig ARCH=um host% make linux ARCH=um
W katalogu z UML umieszczamy obraz systemu plików
host% bunzip2 root_fs_slack8.1.bz2 host% mv root_fs_slack8.1 root_fs
Uruchamiamy poleceniem
host% ./linux
Kompilujemy prawie tak jak zwykle, jedyne różnice to architektura docelowego systemu i opcje kompilatora. Najprościej "pożyczyć" je z pliku Makefile całego jądra:
CFLAGS = $(shell cd zrodla-uml/linux-2.4.31; make script \ 'SCRIPT=@echo $$(CFLAGS)' ARCH=um)
Instalacja jest trochę bardziej skomplikowana:
Z poziomu systemu-gospodarza montujemy obraz systemu plików
host# mount root_fs katalog-z-uml/mnt -o loop
Instalujemy w nim nasze moduły:
host# make install INSTALL_MOD_PATH=katalog-z-uml/mnt ARCH=um
Odmontowujemy root_fs
host# umount katalog-z-uml/mnt
... i uruchamiamy UML
host% cd katalog-z-uml; ./linux
CFLAGS = $(shell cd $(HOME)/uml/linux-2.4.31; make script \ 'SCRIPT=@echo $$(CFLAGS)' ARCH=um) -DMODULE VERSION = 2.4.31-1um ROOT_FS_IMG = $(HOME)/uml/mnt INSTALLDIR = $(ROOT_FS_IMG)/lib/modules/$(VERSION)/misc MODULES = kerncalc .PHONY: clean all: .depend $(MODULES).o $(MODULES).o: $(MODULES).c $(CC) -c $(CFLAGS) $< -o $@ install: install -d $(INSTALLDIR) install -c $(MODULES).o $(INSTALLDIR) clean: rm -f *.o *~ core .depend .depend: $(CC) $(CFLAGS) -M *.c > $@ ifeq (.depend,$(wildcard .depend)) include .depend endif
Uruchamianie UML-a pod kontrolą gdb jest proste: wystarczy wywołać go
host% ./linux debug
Możemy też przełączyć się na jakiś inny proces, który akurat jest
(UML gdb) detach (UML gdb) attach <host pid> (UML gdb) ...
By przełączyć się z powrotem do głównego procesu jądra UML, wystarczy
podać jakiś numer pid, który nie odnosi się do żadnego z procesów
wchodzących
(UML gdb) attach 1
Można też uruchomić gdb dla już działającego UML-a, wysyłając mu sygnał SIGUSR1:
host% kill -USR1 <uml-pid>
Żeby to wszystko działało, UML musi być skompilowany z opcjami CONFIG_DEBUGSYM i CONFIG_PT_PROXY
... nie jest aż tak skomplikowane, jak by się mogło wydawać. Całą pracę związaną z pobieraniem listy symboli wykona za nas skrypt umlgdb z zestawu narzędzi uml_utilities_xxxxx.tar.bz2 (podkatalog tools/umlgdb/umlgdb). Wystarczy
set MODULE_PATHS { "fat" "/usr/src/uml/linux-2.4.18/fs/fat/fat.o" "isofs" "/usr/src/uml/linux-2.4.18/fs/isofs/isofs.o" "minix" "/usr/src/uml/linux-2.4.18/fs/minix/minix.o" }
"kerncalc" "/home/akmac/uml/devel/kerncalc.o"
UML potrafi również współpracować z innymi debuggerami, nie tylko gdb. Trzeba mu jednak wtedy explicite przekazać numer procesu debuggera, którego chcemy używać: zamiast opcji debug podajemy debug gdb-pid=<pid-debuggera>
Przykładowo, dla strace:
host% sh -c 'echo pid=$$; read x; exec strace -p 1 -o strace.out' pid=<strace-pid>
host% ./linux debug gdb-pid=<strace-pid>
Gdybyśmy użyli po prostu host% strace ./linux
,
w pliku strace.out znalazłyby się tylko wyniki śledzenia
głównego procesu jądra — tak mamy komplet informacji.
© Piotr Buczek (podstawy, ksymoops, kgdb itp.); | |
© Magdalena Dukielska (podstawy); | |
© Adam Maciejewski (UML); |