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.


Techniki odpluskwiania jądra: ksymoops, kgdb, UML.

Spis treści


Bibliografia

Powrót do góry strony


Wstęp

Z programowaniem w jądrze wiążą sie jedyne w swoim rodzaju wyzwania dla odpluskwiania.

Nad debugowaniem jądra warto się zastanowić nie tylko jeśli otrzymujemy komunikat oops (czasem w zupełnie niespodziewanym momencie). Błędy (począwszy od najzwyklejszych pomyłek w kodzie, a skończywszy na błędach w synchronizacji) mogą się bowiem objawiać również poprzez:

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.

Powrót do góry strony


Wsparcie dla odpluskwiania w jądrze

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).

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

Powrót do góry strony


Odpluskwianie przez wypisywanie - printk

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:

W zależności od priorytetu jądro może wypisywać te informacje na konsolę, do portu szeregowego albo równoległego. Jeśli korzystamy z demonów klogdsyslogd informacje te trafiają do pliku /var/log/messages (można to zmienić odpowiednio konfigurując syslogd'a - patrz niżej). Jeśli wyłączymy klogd'a, to będziemy mogli je obejrzeć jedynie w pliku /proc/kmsg (najłatwiej go przeczytać poleceniem dmesg).

Powrót do góry strony


W jaki sposób przechowywane są informacje?

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).

Powrót do góry strony


Zady i Walety

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[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/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("..."); 

Powrót do góry strony


Odpluskwianie przez wypytywanie - /proc

Nadmierne używanie printk bardzo zwalnia system, ponieważ działa ona w ten sposób, że wypisanie każdej linii wymaga operacji dyskowej. Wynika to z tego, że syslogd próbuje zapisać możliwie dużo komunikatów w obawie, że system może ulec awarii w dowolnym momencie. W wielu przypadkach skuteczniejsze jest stworzenie pliku w  /proc, który zapewniałby nam dostęp do niezbędnych informacji. Czasem wystarczy też wykorzystać narzędzia dostępne w systemie, takie jak np. ps.

Powrót do góry strony


Odpluskwianie przez obserwowanie

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 straceltrace.

Powrót do góry strony


strace

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 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)                           = ?

Istotna jest również możliwość podłączenia się do działającego w systemie procesu. Należy w tym celu podać PID procesu (ps powinno wystarczyć, aby do niego dotrzeć) po opcji -p:

	strace -p <pid>

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.

Powrót do góry strony


ltrace

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 lstrace -S - w ten sposób dostaniemy informacje o wywołaniach zarówno funkcji systemowych jak i funkcji bibliotecznych. Opcja -estrace jest jednak znacznie bardziej rozwinięta, przez co dla ograniczania typu śledzonych wywołań systemowych lepiej użyć właśnie tego programu.

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).

Powrót do góry strony


Odpluskwianie błędów w systemie

Oops (ksymoops)

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):

	Unable to handle kernel NULL pointer dereference at virtual address 00000000
 	printing eip:
	d083a064
	Oops: 0002 [#1]
	SMP 
	CPU:    0
	EIP:    0060:[]    Not tainted
	EFLAGS: 00010246   (2.6.6) 
	EIP is at faulty_write+0x4/0x10 [faulty]
	eax: 00000000   ebx: 00000000   ecx: 00000000   edx: 00000000
	esi: cf8b2460   edi: cf8b2480   ebp: 00000005   esp: c31c5f74
	ds: 007b   es: 007b   ss: 0068
	Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
	Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460 
	       fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480 
	       00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005 
	Call Trace:
	 [] vfs_write+0xb8/0x130
	 [] sys_write+0x42/0x70
	 [] syscall_call+0x7/0xb
	Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0

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:

	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;
	}

Błąd możemy otrzymać też za pomocą takiej funkcji:

	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;
	}

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:

	EIP:    0010:[<00000000>]
	Unable to handle kernel paging request at virtual address ffffffff
	 printing eip:
	ffffffff
	Oops: 0000 [#5]
	SMP 
	CPU:    0
	EIP:    0060:[]    Not tainted
	EFLAGS: 00010296   (2.6.6) 
	EIP is at 0xffffffff
	eax: 0000000c   ebx: ffffffff   ecx: 00000000   edx: bfffda7c
	esi: cf434f00   edi: ffffffff   ebp: 00002000   esp: c27fff78
	ds: 007b   es: 007b   ss: 0068
	Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)
	Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 
	       bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 
	       00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 
	Call Trace:
	 [] sys_read+0x42/0x70
	 [] syscall_call+0x7/0xb
	Code:  Bad EIP value.

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.

Jak to wywołać w kodzie?

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()BUG_ON() (ich implementacja jest zależna od architektury), przy czym 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

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:

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 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"

Powrót do góry strony


Zawieszenie się systemu ("magic SysRq")

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ć:

Powyższa lista nie jest kompletna. Szczegółowe informacje można znaleźć w  Documentation/sysrq.txt. Wykonanie kolejno kombinacji z s, ub 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.

Powrót do góry strony


Odpluskwiacze i powiązane z nimi narzędzia

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.

Powrót do góry strony


gdb (lokalnie)

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 kgdbkdb, o których mowa dalej.

Powrót do góry strony


Zdalne debugowanie - kgdb

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.

Sam skomplikowany proces ustawiania kgdb i połączenia szeregowego jest dokładnie opisany na stronie domowej programu i w katalogu Documentation po nałożeniu łaty (patrz również: Bibliografia). Późniejsze debugowanie jest już proste.

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 rcprsh).

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.

Dodatkowo nowsze wersje kgdb potrafią wykrywać ładowanie/odładowywanie modułó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).

Powrót do góry strony


Lokalne debugowanie - kdb

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.

Niemożliwe jest jednak debugowanie z poziomu kodu źródłowego (tak jak to było w kgdb).

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.

Powrót do góry strony


UML

Co to jest UML?

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:


Powrót do góry strony


UML - instalacja

  1. 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
    
  2. Aplikujemy do nich łaty UML:

    	host% cd ~/uml/linux
    	host% bzcat uml-patch-2.4.27-1.bz2 | patch -p1
    
  3. Konfigurujemy i kompilujemy dla architektury 'um'

    	host% make menuconfig ARCH=um
    	host% make linux ARCH=um
    
  4. W katalogu z UML umieszczamy obraz systemu plików

    	host% bunzip2 root_fs_slack8.1.bz2
    	host% mv root_fs_slack8.1 root_fs
    
  5. Uruchamiamy poleceniem

    	host% ./linux
    

Powrót do góry strony


UML - własne moduły

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:

  1. Z poziomu systemu-gospodarza montujemy obraz systemu plików

    	host# mount root_fs katalog-z-uml/mnt -o loop
  2. Instalujemy w nim nasze moduły:

    	host# make install INSTALL_MOD_PATH=katalog-z-uml/mnt ARCH=um
  3. Odmontowujemy root_fs

    	host# umount katalog-z-uml/mnt
  4. ... i uruchamiamy UML

    	host% cd katalog-z-uml; ./linux


Powrót do góry strony


Przykładowy Makefile

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

Powrót do góry strony


UML i GNU debugger

Uruchamianie UML-a pod kontrolą gdb jest proste: wystarczy wywołać go z parametrem debug:

	host% ./linux debug
(dostaniemy gdb w nowym oknie terminala, z pułapką ustawioną na wejściu do start_kernel)

Możemy też przełączyć się na jakiś inny proces, który akurat jest w trybie jądra UML:

(UML gdb) detach
(UML gdb) attach <host pid>
(UML gdb) ...
gdzie <host pid> to numer interesującego nas procesu zwrócony przez polecenie ps z systemu-gospodarza.

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 w skład UML-a, np:

(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>
(numer procesu widać przy starcie UML-a: tracing thread pid = <uml-pid>)

Żeby to wszystko działało, UML musi być skompilowany z opcjami CONFIG_DEBUGSYM i CONFIG_PT_PROXY


Powrót do góry strony


Debugowanie modułów

... 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

  1. Odnaleźć w nim definicję zmiennej MODULE_PATHS
    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"
    }
  2. Wstawić tam nazwę naszego modułu i jego lokalizację
    "kerncalc" "/home/akmac/uml/devel/kerncalc.o"
  3. Przejść do katalogu z UML-em i stamtąd uruchomić zmodyfikowany skrypt
  4. I gotowe — w chwili załadowania naszego modułu gdb automatycznie rozpozna nowe symbole


Powrót do góry strony


Inne odpluskwiacze a UML

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:

  1. W oknie terminala wydajemy polecenie
    host% sh -c 'echo pid=$$; read x; exec strace -p 1 -o strace.out'
    pid=<strace-pid>
  2. W innym terminalu uruchamiamy UML-a...
    host% ./linux debug gdb-pid=<strace-pid>
  3. ... a w pierwszym wciskamy Enter, uruchamiając strace

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.

Powrót do góry strony


© Piotr Buczek (podstawy, ksymoops, kgdb itp.);
© Magdalena Dukielska (podstawy);
© Adam Maciejewski (UML);