Klawisz SysRq (Alt + PrintScreen) pozwala na utrzymanie kontroli nad systemem nawet wtedy, gdy się zawiesi i nie reaguje na żadne komendy (jednak nadal działa jądro). Kontrola ta polega na naciskaniu określonych klawiszy mając jednocześnie wciśnięty SysRq. Możemy w ten sposób:
Udostępnia dalsze opcje, ale sama nic nie włącza.
Włącza opcję wykrywania w jądrze zastojów - gdy system działa w pętli w trybie jądra więcej niż 10 sekund nie oddając procesora innym zadaniom. Po wykryciu blokady wypisywany jest stos wywołań i nic więcej się nie stanie - system pozostanie zablokowany. Pomijalny wpływ na szybkość działania jądra.
Jeśli wyrazimy zgodę, do schedulera zostanie dopisany dodatkowy kod, co spowoduje, że będą zbierane informacje o jego zachowaniu. Zostaną one zapisane w /proc/schedstat. Raczej nie potrzebujemy tej opcji jeżeli nie debugujemy samego schedulera. Nieduży narzut czasowy przy pracy jądra.
Spowoduje, że jądro będzie wykonywało dodatkowe sprawdzenia przy alokacji pamięci - każdy bajt jest ustawiany na 0xa5 przed przekazaniem go procesowi, a przy zwalnianiu ustawiany jest na 0x6b. Takie “zatruwanie” (”poisoning”) pamięci pozwala łatwiej wykrywać błędy, np. podczas analizy informacji wygenerowanych przez oops. Dodatkowo każdy blok przydzielanej pamięci jest oznaczany na początku i na końcu kontrolnymi wartościami. Jeśli zmienią się one, jądro natychmiast reaguje na przekroczenie przydzielonego obszaru.
Znacznie spowalnia działanie jądra, jeśli często są wykonywane operacje przydzielania i zwalniania dużych ilości pamięci.
Wykrywanie i raportowanie prób wyłamania mutex’ów, oraz związanych z semaforami blokad.
Wykrywanie prób użycia niezainicjalizowanych spinlocków, oraz innych często popełnianych błędów.
Jądro zacznie zgłaszać uwagi, gdy proces będąc wewnątrz spinlocka wywoła funkcję, która potencjalnie może spowodować zaśnięcie.
Obraz jądra po kompilacji będzie zawierał informacje dla debuggera (w rezultacie obraz będzie znacznie większy). Opcja ta bardzo nas interesuje, gdyż zamierzamy uruchomić jądro pod debuggerem. Warto też włączyć opcję CONFIG_FRAME_POINTER.
Opcja ta spowoduje wypisywanie komunikatów jeżeli stos wywołań funkcji przekroczy określony limit.
Udostępnia statystyki zużycia pamięci przez jądro. Do obejrzenia przy użyciu SysRq-T, SysRq-P.
Umożliwia debugowanie jądra standardową metodą - poprzez wypisywanie w dogodnych momentach informacji. Funkcja printk() różni się od printf() dwiema cechami:
Przykład wywołania:
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);
Makra opisujące priorytet są rozwijane do stringa i doklejane do komunikatu - stąd brak przecinka między pierwszymi dwoma parametrami.
KERN_EMERG | “<0>” | /* system is unusable */ |
---|---|---|
KERN_ALERT | “<1>” | /* action must be taken immediately */ |
KERN_CRIT | “<2>” | /* critical conditions */ |
KERN_ERR | “<3>” | /* error conditions */ |
KERN_WARNING | “<4>” | /* warning conditions */ |
KERN_NOTICE | “<5>” | /* normal but significant condition */ |
KERN_INFO | “<6>” | /* informational */ |
KERN_DEBUG | “<7>” | /* debug-level messages */ |
Jeżeli nie podamy parametru loglevel, priorytet zostanie ustawiony na DEFAULT_MESSAGE_LOGLEVEL, w jądrze 2.6.17 równe KERN_WARNING.
Funkcja printk() zapisuje komunikaty w buforze cyklicznym o długości __LOG_BUF_LEN, definiowanej przy kompilacji jądra. Jest to wartość pomiędzy 4KB a 1MB. Oznacza to, że starsze wiadomości nieodczytane z bufora zostają nadpisane. Następnie budzi któryś z procesów oczekujących na wiadomości - czytających z pliku /proc/kmsg lub uśpionych po wywołaniu funkcji systemowej syslog. Takim procesem jest np. demon klogd opisany poniżej.
Poniżej przedstawiamy schemat przepływu komunikatów poprzez klogd i syslogd:
Oops wyzwalany jest, gdy w jądrze zaistnieje jakaś wyjątkowa sytuacja - np. próba odczytu spod nieprawidłowego adresu w pamięci. Jest to zrzut zawartości wszystkich rejestrów procesora oraz stosu jądra z momentu wystąpienia błędu. Wypisywany jest na konsolę oraz do cyklicznego bufora jądra i ma być pomocą dla programisty próbującego zdiagnozować przyczynę błędu. Drugą jego funkcją jest powstrzymanie jądra przed zupełną destabilizacją lub zniszczeniem danych - dlatego natychmiast są zabijane procesy oraz komponenty jądra odpowiedzialne za sytuację.
Unable to handle kernel NULL pointer dereference at virtual address 00000014 *pde = 00000000 Oops: 0000 CPU: 0 EIP: 0010:[<c017d558>] EFLAGS: 00210213 eax: 00000000 ebx: c6155c6c ecx: 00000038 edx: 00000000 esi: c672f000 edi: c672f07c ebp: 00000004 esp: c6155b0c ds: 0018 es: 0018 ss: 0018 Process tar (pid: 2293, stackpage=c6155000) Stack: c672f000 c672f07c 00000000 00000038 00000060 00000000 c6d7d2a0 c6c79018 00000001 c6155c6c 00000000 c6d7d2a0 c017eb4f c6155c6c 00000000 00000098 c017fc44 c672f000 00000084 00001020 00001000 c7129028 00000038 00000069 Call Trace: [<c017eb4f>] [<c017fc44>] [<c0180115>] [<c018a1c8>] [<c017bb3a>] [<c018738f>] [<c0177a13>] [<d0871044>] [<c0178274>] [<c0142e36>] [<c013c75f>] [<c013c7f8>] [<c0108f77>] [<c010002b>] Code: 8b 40 14 ff d0 89 c2 8b 06 83 c4 10 01 c2 89 16 8b 83 8c 01
Surowe dane wypisane w oopsie są bardzo trudne do zanalizowania, dlatego używa się narzędzia ksymoops, które potrafi przetworzyć je na zrozumiałe symbole i adresy. Programowi temu należy przekazać następujące dane:
Jak widać, pojawiają się tu nawet ludzkie nazwy funkcji ze stosu wywołań:
... Process tar (pid: 2293, stackpage=c6155000) ... >>EIP; c017d558 <create_virtual_node+298/490> <===== Trace; c017eb4f <ip_check_balance+34f/ae0> Trace; c017fc44 <reiserfs_kfree+14/50> Trace; c0180115 <fix_nodes+115/450> Trace; c018a1c8 <reiserfs_insert_item+88/110> Trace; c017bb3a <reiserfs_new_inode+3da/500> Trace; c018738f <pathrelse+1f/30> Trace; c0177a13 <reiserfs_lookup+73/d0> Trace; d0871044 <END_OF_CODE+9a77/??? Trace; c0178274 <reiserfs_mkdir+d4/1d0> Trace; c0142e36 <d_alloc+16/160> Trace; c013c75f <vfs_mkdir+7f/b0> Trace; c013c7f8 <sys_mkdir+68/b0> Trace; c0108f77 <system_call+33/38> Trace; c010002b <startup_32+2b/139> Code; c017d558 <create_virtual_node+298/490> 00000000 <_EIP>: Code; c017d558 <create_virtual_node+298/490> <===== 0: 8b 40 14 mov 0x14(%eax),%eax <===== Code; c017d55b <create_virtual_node+29b/490> 3: ff d0 call *%eax
strace i ltrace są programami, które śledzą odwołania danego procesu odpowiednio - do funkcji systemowych (strace) oraz funkcji bibliotecznych (ltrace). Zarejestrowane jest każde wywołanie funkcji systemowej, a także sygnały jakie proces otrzymuje od systemu. Program, który będzie śledzony przez strace/ltrace nie musi być skompilowany z informacjami dla debuggera (-g). Wynik działania strace może sotać wypisany na stderr lub zapisany do pliku którego adres podajemy wraz z opcją -o.
Możemy również przypisać zmiennym wartości (print <zmienna>=<wartosc>
).
kgdb jest łatką na jądro Linuksa pozwalającą uruchomić je zdalnie pod debuggerem gdb.
Debugowanie odbywa się przy użyciu dwóch maszyn. Na jednej uruchamiany jest debugger, a na drugiej uruchamiany jest system operacyjny ze spatchowanym jądrem. Łatka zawiera komponent gdb, który pozwala na komunikację z debuggerem na drugiej maszynie poprzez port szeregowy lub ethernet. Modyfikuje również reakcję na błędy - jądro zamiast panikować, przekazuje kontrolę debuggerowi, co pozwala na zanalizowanie problemu.
Wersję kgdb przeznaczoną dla jądra 2.6.17.13 na razie możemy zdobyć pobierając najświeższe pliki z repozytorium CVS:
cvs -z3 -d:pserver:anonymous@kgdb.cvs.sourceforge.net:/cvsroot/kgdb co -P kgdb-2
Musimy nałożyć łatkę na jądro Linuksa. W tym celu pobieramy źródła jądra:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.17.13.tar.bz2 tar -xjf linux-2.6.17.13.tar.bz2 cd linux-2.6.17.13/
Następnie wykonujemy
patch -b -p1 <dla każdego pliku .patch znajdującego się w katalogu <gdzieśtam>/kgdb-2>
Lub używamy programu quilt zgodnie z instrukcją podaną w README kgdb-2/.
Wykonujemy make menuconfig
i wybieramy opcje, które nas interesują (kernel debug info!), a przede wszystkim opcje zalecone w README:
Kernel hacking -> KGDB: kernel debugging with remote gdb -> KGDB: Thread analysis KGDB: Console messages through gdb Device drivers -> Character devices -> Serial drivers -> KGDB: On generic serial port (8250)
Następnie wykonujemy make bzImage
, a uzyskany obraz
jądra kopiujemy do katalogu /boot na maszynie testowej. Dodajemy
odpowiedni wpis do menu.lst dla GRUBa, dopisując kgdbwait do polecenia
ładującego nasz obraz. To sprawi, że jądro po załadowaniu będzie czekać
na połączenie od gdb z drugej maszyny. Szczegóły we wspomnianym README.
KGDB można używać także na VMware, tutaj znajdują się odpowiednie wskazówki dla osób używających VMWare Server pod Linuksem i pod Windows.
Po rozpakowaniu obrazu jądra maszyna testowa zawiesza się w oczekiwaniu na połączenie ze zdalnego gdb. Na maszynie development
, w katalogu ze zrodlami jądra włączamy gdb vmlinux
, a następnie wpisujemy target remote <adres portu szeregowego, z ktorym polaczana jest druga maszyba>
. Gdb ma teraz dostep do kodu wykonywanego na maszynie testowej.
KDB jest debuggerem wbudowanym w jądro i pozwala monitorować struktury danych oraz pamięć systemu w czasie jego działania.
Możliwości:
KDB jest łatką na jądro - a więc instalacja polega na nałożeniu tej łatki, a następnie wybraniu odpowiednich opcji związanych z działaniem KDB w dziale Kernel hacking konfiguratora [ CONFIG_KDB ].
Debugger domyślnie jest włączony i uaktywnia się przy wystąpieniu kernel panic, lub po dojściu do wcześniej ustalonych breakpointów. Opcja kdb dodana w lilo spowoduje zatrzymanie jądra na początku inicjalizacji systemu, co pozwala na ustalenie breakpointów.
+-----------+-----------+----+ | Process 1 | Process 2 | ...| +-----------+-----------+----+ | Linux Kernel | +----------------------------+ | Hardware | +----------------------------+
Różnica pomiędzy jądrem UML-a a jąrdem Linuksa jest takaż że jest on traktowany jak zwykły proces, a wiec nie zarządza sprzętem, tylko o niego prosi jądro hosta.
+----------------+ | Process 2 | ...| +-----------+----------------+ | Process 1 | User-Mode Linux| +----------------------------+ | Linux Kernel | +----------------------------+ | Hardware | +----------------------------+
Ponieważ UML jest traktowany przez gdb jak zwykły proces, więc możemy używać tych samych poleceń i narzędzi co zostały przedstawione w rozdziale Podstawy gdb. Poniżej został przdstawiony przykład debugowania UML przy pomocy gdb.
Istnieją dwie podstawowe metody debudowania modułów.
Metoda ta polega na zlokalizowaniu danego modułu i podanie debugerowi jego adresu, który zastał przydzielony przez UML.
Kiedy mamy zbudowany UML, nalezy zainstalować moduły, oraz umieścić je w odpowiednik katalodu, by były widoczne dla gdb. Cała czynność sprowadz się do zaledwie kilku poleceń, które zostały przedstawione poniżej:
1 make modules_install INSTALL_MOD_PATH=mods ARCH=um 2 UML# mkdir -p /lib/modules/`uname -r` 3 host% scp -r mods/lib/modules/2.6.18-rc3-mm2/kernel/ root@UML:/lib/modules/2.6.18-rc3-mm2
W lini trzeciej powyższego kodu, nie powinniśmy wpisywać uname -r zamiast 2.6.18-rc3-mm2, gdyż grozi to rozszerzeniem modułów hosta.
Po wykonaniu poniższego polecenia, możemy sprawdzić czy udało nam się załadować moduły w odpowiednie miejsce, z którego będą pobierane prze gdb.
UML# lsmod Module Size Used by loop 12328 0
Gdyz mamy już zbudowane moduły, które się mieszczą w katalogu root@UML:/lib/modules/2.6.18-rc3-mm2, możemy przystąpić do debudowania, gdzie poprzez pierwsze polecenie p modules wczytujemy dostępne moduły. Cała trudność debugowania modułów przy pomocy gdb, poleda na tym, że jeżeli chcemy wybrać odpowiedzni moduł, musimy podać jego adres w pamieci, który został przydzielony dynamicznie przez UML-a. Przykład debugowania modułów UML-a przy pomocy gdb.
(gdb) p modules $1 = {next = 0x3502cea4, prev = 0x3502cea4} (gdb) p *((struct module *)0x3502cea0) $3 = {state = MODULE_STATE_LIVE, list = {next = 0x81e0dec, prev = 0x81e0dec}, name = "loop", '\0' <repeats 55 times>, mkobj = {kobj = { k_name = 0x3502ceec "loop", name = "loop", '\0' <repeats 15 times>, kref = {refcount = {counter = 2}}, entry = {next = 0x81e0ac8, prev = 0x34a486c8}, parent = 0x81e0ad0, kset = 0x81e0ac0, ktype = 0x0, dentry = 0x3297277c, poll = {lock = {raw_lock = {<No data fields>}}, task_list = {next = 0x3502cf1c, prev = 0x3502cf1c}}}, mod = 0x3502cea0}, param_attrs = 0x0, modinfo_attrs = 0x9598620, version = 0x0, srcversion = 0x0, syms = 0x3502bc20, num_syms = 2, crcs = 0x0, gpl_syms = 0x0, num_gpl_syms = 0, gpl_crcs = 0x0, unused_syms = 0x0, num_unused_syms = 0, unused_crcs = 0x0, unused_gpl_syms = 0x0, num_unused_gpl_syms = 0, unused_gpl_crcs = 0x0, gpl_future_syms = 0x0, num_gpl_future_syms = 0, gpl_future_crcs = 0x0, num_exentries = 0, extable = 0x0, init = 0x3502f000, module_init = 0x0, module_core = 0x3502a000, init_size = 0, core_size = 12328, init_text_size = 0, core_text_size = 6468, unwind_info = 0x0, arch = {<No data fields>}, unsafe = 0, license_gplok = 1, ref = {{count = { counter = 0}}}, modules_which_use_me = {next = 0x3502cfe0, prev = 0x3502cfe0}, waiter = 0x94cf7c0, exit = 0x3502b8b6, symtab = 0x3502bc30, num_symtab = 166, strtab = 0x3502c690 "", sect_attrs = 0x346e14d8, percpu = 0x0, args = 0x94c3698 ""} (gdb) p &((struct module *)0).list $4 = (struct list_head *) 0x4
Pierwsz metoda jest dość uciążliwa, gdyż musimy podać debugerowi adresy modułów jakie przydzielił im dynamicznie UML. Jednak istnieje wygodniejsze narzędzie napisane przez Chandan Kudige, które jest skryptem umlgdb.