Odpluskwianie jądra
Odpluskwianiem nazywamy wyszukiwanie błędów (błąd to z ang. bug, czyli pluskwa) w danym programie,
określenie miejsca ich wystąpienia oraz, w najczęstszym przypadku, ich usunięcie.
W tej prezentacji skupimy się na odpluskwianiu jądra.
Każdorazowo po dodaniu jakiegoś nowego kawałka kodu do jądra istnieje spora szansa na to, że zawiera on błędy lub źle współgra z resztą kodu.
Wraz z rozwojem jądra, pracuje nad nim coraz więcej osób, co może powodować coraz większe trudności w jego utrzymaniu. Aby ułatwić pozbycie się błędów zostało stworzone specjalne oprogramowanie mające na celu wspomóc wyszukiwanie i usuwanie błędów w jądrze.
Podczas ładowania systemu mogą się zdarzyć różne błędy, przykładowo:
- Oops - dziwo, o którym będzie za chwilę
- kernel panic - nazwa komunikatu wyświetlanego przez system operacyjny po
wykryciu wewnętrznego błędu jądra systemu, którego same nie jest w stanie obsłużyć; często jest następstwem pojawienia
się wielu komunikatów "Oops"
Informują one nas o zajściu błędu, czasem też o jego miejscu wystąpienia. Nie jest to jednak wystarczające. Najwygodniej byłoby zastosować debugger, tak jak dla zwykłego programu. Niestety, ze względu na specyfikę jądra, nie jest to takie proste.
Z pomocą przychodzą nam takie narzędzia jak:
- KDB - debugger systemowy
- KGDB - łata na jądro umożliwiająca debugowanie go za pomocą gdb uruchomionego w innym systemie
- UML - User Mode Linux - system wirtualizacji, umożliwiający uruchomienie jądra systemu jako zwykłego procesu w działającym systemie.
Żeby odpluskwianie jądra było możliwe warto włączyć w konfiguracji jądra następującą opcję:
CONFIG_DEBUG_KERNEL = y
Można ją znaleźć w menu konfiguracyjnym w dziale Kernel Hacking.
W pierwszej części prezentacji zostaną omówione komunikaty Oops oraz KDB, natomiast w drugiej KGDB oraz UML.
Oops (i co teraz?)
Co to jest ten Oops?
Oops - raport jądra systemu świadczący o jego odstępstwie od poprawnego zachowania.
Gdy jądro znajdzie problem, wypisuje komunikat Oops oraz zabija każdy proces, który "zawinił".
Wiadomość ta jest wykorzystywana przez inżynierów jądra Linuksa do znalezienia przyczyn błędu i naprawy problemu.
Po wystąpieniu Oopsa niektóre zasoby mogą zostać utracone. Gdy system będzie chciał ich użyć, może to doprowadzić do stanu kernel panic.
Oops może się zdarzyć, gdy wystąpi:
- błąd programistyczny (np. gdy jądro chciało odwołać się do nieprawidłowego miejsca w pamięci)
- błąd sprzętowy
Przykłady:
- dla architektury x86
BUG: unable to handle kernel paging request at virtual address eaa34b3c
printing eip:
b0161cdd
*pde = 0048a067
*pte = 3aa34000
Oops: 0000 [#1]
PREEMPT SMP DEBUG_PAGEALLOC
last sysfs file: /devices/pci0000:00/0000:00:1d.1/usb3/idVendor
Modules linked in: snd_intel8x0 snd_ac97_codec snd_ac97_bus
snd_pcm_oss snd_mixer_oss snd_pcm snd_timer ide_cd cdrom intel_agp
agpgart snd i2c_i801 hw_random soundcore snd_page_alloc unix
CPU: 0
EIP: 0060:[<b0161cdd>] Not tainted VLI
EFLAGS: 00010282 (2.6.16-rc1-mm3 #4)
EIP is at do_path_lookup+0x22b/0x259
eax: eaa34b20 ebx: eb328000 ecx: 00000000 edx: eb328f4c
esi: ffffff9c edi: fffffffe ebp: eb328f24 esp: eb328f0c
ds: 007b es: 007b ss: 0068
Process udevd (pid: 731, threadinfo=eb328000 task=eb30ca80)
Stack: <0>00000000 eb5cb000 b015fab1 eb5cb000 eb5cb000 00000000
eb328f40 b01621f3
eb328f4c ffffff9c afbf6dec afbf6dec 00000100 eb328f9c b015bf69 eb328f4c
eaa34b20 b23d5f28 00000000 eb329003 b015f8ce 00000000 00000001 00000000
Call Trace:
[<b0103917>] show_stack_log_lvl+0xaa/0xb5
[<b0103a54>] show_registers+0x132/0x19d
[<b0103d91>] die+0x171/0x1fb
[<b02ab110>] do_page_fault+0x3be/0x568
[<b010343f>] error_code+0x4f/0x54
[<b01621f3>] __user_walk_fd+0x2d/0x41
[<b015bf69>] sys_readlinkat+0x26/0x93
[<b015bfe9>] sys_readlink+0x13/0x15
[<b01028bf>] sysenter_past_esp+0x54/0x75
Code: 00 83 c0 04 e8 9a 82 14 00 8b 03 c7 80 e4 01 00 00 00 00 00 00
8b 55 08 8b 45 ec e8 55 fa ff ff 89 c7 8b 55 08 8b 02 85 c0 74 24 <8b>
50 1c 85 d2 74 1d b8 00 f0 ff ff 21 e0 8b 00 83 b8 d4 04 00
- dla innej architektury
Zazwyczaj raport Oops składa się z (dla architektury x86):
- opisu (dzięki któremu wiadomo jaki to typ błędu):
BUG: unable to handle kernel paging request at virtual address eaa34b3c
- numeru Oops (który to już błąd tego typu zaczynając od 0000):
Oops: 0000 [#1]
- numeru procesora (0 jeśli mamy jeden procesor):
CPU: 0
- EIP, czyli wskaźnika instrukcji (podany jest adres instrukcji, która zawiodła):
EIP: 0060:[<b0161cdd>]
- zawartości rejestrów procesora:
eax: eaa34b20 ebx: eb328000 ecx: 00000000 edx: eb328f4c
esi: ffffff9c edi: fffffffe ebp: eb328f24 esp: eb328f0c
ds: 007b es: 007b ss: 0068
- stosu:
Stack: <0>00000000 eb5cb000 b015fab1 eb5cb000 eb5cb000 00000000
eb328f40 b01621f3
eb328f4c ffffff9c afbf6dec afbf6dec 00000100 eb328f9c b015bf69 eb328f4c
eaa34b20 b23d5f28 00000000 eb329003 b015f8ce 00000000 00000001 00000000
- śladu wywołań, który zasadniczo jest ciągiem instrukcji, które CPU właśnie przetwarzał w momencie wykonania Oops
Call Trace:
[<b0103917>] show_stack_log_lvl+0xaa/0xb5
[<b0103a54>] show_registers+0x132/0x19d
[<b0103d91>] die+0x171/0x1fb
[<b02ab110>] do_page_fault+0x3be/0x568
[<b010343f>] error_code+0x4f/0x54
[<b01621f3>] __user_walk_fd+0x2d/0x41
[<b015bf69>] sys_readlinkat+0x26/0x93
[<b015bfe9>] sys_readlink+0x13/0x15
[<b01028bf>] sysenter_past_esp+0x54/0x75
- kodu, który CPU właśnie wykonywał:
Code: 00 83 c0 04 e8 9a 82 14 00 8b 03 c7 80 e4 01 00 00 00 00 00 00
8b 55 08 8b 45 ec e8 55 fa ff ff 89 c7 8b 55 08 8b 02 85 c0 74 24 <8b>
50 1c 85 d2 74 1d b8 00 f0 ff ff 21 e0 8b 00 83 b8 d4 04 00
Gdzie ten Oops się teraz znajduje?
- Zazwyczaj komunikat Oops jest wczytywany z buforów jądra przez demona klogd i przekazywany dalej
do demona syslogd. Ten z kolei umieszcza komunikat w pliku /var/log/messages.
- Gdy klogd nie działa można samemu spróbować wczytać zawartość bufora jądra jednym ze sposobów:
- dmesg > plik
- cat /proc/kmsg > plik (należy tu pamiętać, by przerwać to, gdyż kmsg jest "niekończącym się" plikiem
- Jest jeszcze możliwość, że system zupełnie nie rozumie komend, które chcemy mu wprowadzić, więc zanim zmienimy dysk, wyląduje za oknem, itp. można spróbować jeszcze zrobić tak:
- przepisać ręcznie zawartość ekranu lub zrobić mu zdjęcie i wpisać zapisany tekst po zrestartowaniu systemu (można też ewentutalnie zmienić rozdzielczość ekranu, by dostać to co trzeba)
- uruchomić komputer ponownie zdalnie z drugiej maszyny używając kabla szeregowego (null-modemu)
- wykorzystać Kdump (który to pozwala na tworzenie tzw. zrzutów podczas padu system; taki zrzut pozwala na łatwe odpluskwianie)
Możliwe sposoby radzenia sobie z Oopsem:
- Metoda "zrób to sam":
(Żeby jej użyć, warto włączyć w konfiguracji następujące opcje:
CONFIG_DEBUG_SLAB = y
CONFIG_FRAME_POINTER = y)
- sprawdzamy co jest napisane (nazwa funkcji) przy "EIP is at ..." (np. w przykładzie powyżej jest to funkcja do_path_lookup)
- wyszukujemy nazwy tej funkcji w drzewie jądra
- tworzymy zdeassemblowany plik modułu, w którym się znajduje dana funkcja
- w pliku z rozszerzeniem .s szukamy numeru podanego po "+" i przed "\" w linii z "EIP is at ..."
- za pomocą funkcji printf "0x%x\n" $((0xCOS+0x22b)), wyszukujemy numer linii, który mówi nam, która to linjka kodu, itd. itd. ...
- Metoda "dla inżyniera jądra":
- przesłać informacje o Oops wraz z plikiem konfiguracyjnego jądra, z którym było kompilowane oraz ze sposobem odtworzenia, jeśli jest znany, na LKML(Linux Kernel Mailing List)
- inżynier jądra sprawdzi o co chodzi wydając komendy:
- gdb vmlinux
- list *0xb0161cdd, gdzie "b0161cdd" jest adresem napisanym po "EIP: ..."
- następnie używając swojej znajomości jądra danej dystrybucji systemu i używając przeróżnych sztuczek inżynier naprawia usterkę :)
- Metody Linusa Torvaldsa (który to jest przeciwnikiem odpluskwiaczy dla jądra Linuksa):
- gdb /usr/src/linux/vmlinux
- gdb> disassemble <offending_function> (gdzie offending_function
to nazwa funkcji sprawiająca problem (czyli ta przy napisie "EIP is at ..."))
- napisać program w C:
char str[] = "\xXX\xXX\xXX...";
main(){}
,gdzie spację należało zamienić na "\x" z kodu (napis po "Code: ...") oznaczonego jako XX XX ...
- następnie skompilować ów program z gcc -g oraz zdeassemblować łańcuch "str"
- Linus Torvalds twierdzi, że mając te informacje wraz z raportem Oopsa można często znaleźć usterkę metodą "znajdź i przypasuj", a jak nie to szukając, co nie pasuje.
Jednakże, zamiast się trudzić, lepiej czasem skorzystać z gotowego oprogramowania do odpluskwiania. np.
KDB
Do czego służy KDB?
KDB - jest to łata (z ang. patch) na jądro, która po dodaniu do niego daje możliwość
dokładnego zbadania "co się stało, że nie działa?"
Cechy KDB:
- sprawdzanie zawartości pamięci i struktura danych jądra
oraz śledzenie jego wykonywania
- do użytku na jednym komputerze, czyli lokalne odpluskwianie (inaczej jest w KGDB, gdzie potrzeba drugiej maszyny)
- użytkownicy KDB powinni umieć m. in. skompilować jądro
Instalacja
KDB jest projektem Silicon Graphics i można go ściągnąć z ich strony internetowej: Silicon Graphics - KDB.
Na stronie w dziale Download/Sources należy znaleźć odpowiednie pliki, np. dla jądra w wersji 2.6.17 i architektury i386 trzeba załadować 2 pliki:
kdb-v4.4-2.6.17-common-1.bz2
kdb-v4.4-2.6.17-i386-1.bz2
Obecnie najnowszą wersją KDB jest 4.4.
Teraz pliki te kopiujemy do katalogu /usr/src/linux i tam rozpakowujemy programem bzip2:
#bzip2 -d kdb-v4.4-2.6.17-common-1.bz2
#bzip2 -d kdb-v4.4-2.6.17-i386-1.bz2
Zastosowujemy patche:
#patch -p1 <kdb-v4.4-2.6.17-common-1
#patch -p1 <kdb-v4.4-2.6.17-i386-1
Należy teraz skonfigurować jądro (np. przez użycie make menuconfig, gdzie można ustawić następujące opcje:
- CONFIG_KDB (trzeba!), która znajduje się w dziale Kernel Hacking pod nazwą Built-in Kernel Debugger support
- CONFIG_FRAME_POINTER (można), w dziale jw. jako Compile the kernel with frame pointers, opcja ta wspiera analizę stosu, jednakże używa dodatkowego rejestru i tworzy nieco wolniejszy kod jądra
- CONFIG_KDB_OFF (warto), bu ustawić KDB domyślnie jako wyłączone przy starcie systemu
Po konfiguracji jądra należy zapisać zmiany, skompilować jądro, skopiować do katalogu /boot i uruchomić ponownie komputer.
Można też przed ponownym uruchomieniem wydać polecenie make clean.
Dodatkowe przygotowania
Można samemu zdefiniować komendy dla KDB. Powinny one być umieszczone w pliku kdb_cmds, który znajduje się w katalogu KDB w drzewie żródłowym Linuksa.
Plik ten służy także do ustawiania zmiennych środowiskowych, które mają wpływ na wyświetlanie komunikatów przy działaniu KDB.
Jednakże, po jego zmodyfikowaniu należy przekompilować i przeinstalować jądro.
Aktywacja KDB
Po ustawieniu CONFIG_KDB_OFF KDB nie będzie uruchamiane domyślnie po starcie systemu, więc należy je teraz włączyć, by móc z niego korzystać.
Można to zrobić tak:
#echo "1" >/proc/sys/kernel/kdb
Jeśli chcemy wyłączyć go to należy napisać:
#echo "0" >/proc/sys/kernel/kdb
Istnieją trzy sposoby, KDB zostało uruchomione:
- automatyczny - podczas paniki jądra KDB zostaje uruchomione od razu
- ręczny - po wciśnięciu klawisza PAUSE przez użytkownika
- ręczny - po wciśnięciu Ctrl + A z szeregowej konsoli
Obsługa KDB
Obsługę można podzielić na kilka rodzajów:
- WYŚWIETLANIE PAMIĘCI I JEJ MODYFIKACJA
- md - wyświetla zawartość pamięci zaczynając od podanego jako argumenty adresu dla tylu linii ile podano jako drugi argument
- mdr - jw. tyle, że zamiast ilości linii jest podawana ilość bajtów
- mm - zmienia zawartość pamięci (wielkości słowa maszynowego)
- mmW - jw. tylko, że wielkość pamięci jest podawana jako argument W (ilość bajtów)
np.:
wyświetlenie zawartości 15 linii zaczynając od adresu 0xc000000:
[0]kdb> md 0xc000000 15
zmiana zawartości pamięci dla adresu 0xc000000 na 0x10:
[0]kdb> mm 0xc000000 0x10
- WYŚWIETLANIE REJESTRÓW I ICH MODYFIKACJA
- rd - wyświetla zawartość rejestrów procesora
- rm - modyfikuje zawartość rejestrów procesora
- ef - wyświetla ramkę wyjątków dla podanego adresu
np.
ustawienie zawartości rejestru ebx na 0x25:
[0]kdb> rm %ebx 0x25
- PUNKTY ZATRZYMANIA
("BREAKPOINTS")
- bp - ustawia punkt zatrzymania na podanym adresie
- bd - wyłącza podany punkt zatrzymania (podany numer tego punktu)
- be - włącza punkt zatrzymania
- bl - wylistowuje wszystkie punkty zatrzymania
- bc - usuwa punkt zatrzymania z tablicy punktów zatrzymania
np.
ustawienie punktu zatrzymania na instrukcji sys_read:
[0]kdb> bp sys_write
- ŚLEDZENIE STOSU
- bt - próbuje zdobyć informacje o stosie dla bieżącego wątku
- btp - śledzi stos dla podanego procesu (podany PID)
- btc - śledzi stos dla działającego procesu na każdym działającym procesorze
- bta - śledzi stos dla wszystkich procesów w danym stanie:
- D - stan "nieprzerywalny"
- R - stan "działający"
- S - stan "przerywalny-śpiący"
- T - stan "śledzony lub zatrzymany"
- Z - stan "zombie"
- U - stan "niewykonywalny"
np.
śledzenie procesu o nr PID równym 575:
[0]kdb> btp 575
- INNE KOMENDY
- id - deassembluje instrukcje zaczynając od podanego adresu
- ss - wykonuje instrukcję "krok po kroku"
- go - kontynuuje pracę systemu (zatrzymując się na najbliższym punkcie zatrzymania - jeśli taki istnieje)
- reboot - uruchamia ponownie komputer
np.
deassemblacja instrukcji począwszy od "schedule", liczba linii wyświetlanych zależy od IDCOUNT:
[0]kdb> id schedule
wykonywanie instrukcji do napotkania rozgałęzienia (warunku) (w tym przypadku "jne"):
[0]kdb> ssb
0xc0105355 default_idle+0x25: cli
0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax
0xc0105359 default_idle+0x29: test %eax, %eax
0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
Podsumowanie
Jak widać, istnieją różne sposoby na odpluskwianie jądra: od metod "dla wytrwałych" w przypadku rozwiązywaniu problemów z Oopsem
do wygodnego użytkowania odpluskwiacza KDB. Od użytkownika systemu zależy, które z nich uzna za lepsze.
Przydatne linki