Odpluskwianie jądra Linuksa

Piotr Achinger, Jacek Dąbrowski, Dariusz Leniowski
Debugowanie jest obrzydliwe.

Spis treści

  1. Wstęp
  2. Wsparcie ze strony jądra (Piotr Achinger)
    1. Komunikacja - funkcja printk i logi systemowe
    2. Dostęp do logów systemowych - dmesg, klogd, syslogd
    3. Symbole jądra
    4. Komunikat Oops
    5. Makra BUG i BUG_ON
    6. Kernel panic
    7. Opcje kompilacji jądra
    8. Klawisz Magic SysRq
    9. Śledzenie - ptrace, strace i ltrace
  3. KDB (Piotr Achinger)
    1. Wstęp oraz instalacja
    2. Komendy KDB
    3. Przykład
  4. KGDB (Dariusz Leniowski)
    1. Motywacja
    2. Informacje ogólne
    3. Działanie
    4. Instalacja
    5. Użycie
    6. Środowisko graficzne
  5. User Mode Linux (Jacek Dąbrowski)
    1. Ogólnie o UML
    2. Instalacja i konfiguracja
    3. Debugowanie UML w GDB
  6. Podsumowanie

Wstęp

Metody radzenia sobie z błędami w kodzie:

  1. Czytanie kodu
  2. "Nadziewanie" kodu printfami
  3. Korzystanie z debuggera

DEBUGGER

Środowisko uruchomieniowe musi udostępniać mechanizmy śledzenia (podgląd zmiennych, śledzenie wywołań, breakpointy, wykonywanie krok po kroku).

  1. W przypadku języków skryptowych (oraz np. Javy) środowisko = "normalny program".
  2. W przypadku "normalnych programów", środowisko = system operacyjny + maszyna. Mechanizmy śledzenia powinny być udostępniane zarówno przez jądro (w Linuksie: syscall ptrace) jak i przez maszynę (w i386: INT3).
  3. W przypadku SO (jądra), ono samo jest swoim środowiskiem uruchomieniowym.

Wniosek. Debugowanie jądra jest trudniejsze niż debugowanie czego innego, m.in. dlatego, że powszechne metody debugowania działają w trybie użytkownika.

Mechanizmy odpluskwiania jądra stają się potrzebne, gdy:

  1. system napotkał błąd krytyczny i musiał przerwać działanie,
  2. system nie może się uruchomić,
  3. system się zawiesił i nie reaguje (tzw. lockup),
  4. coś w jądrze nie działa, a my nie wiemy, dlaczego
  5. coś działa, ale niezbyt dobrze (np. bardzo wolno)
  6. piszemy np. moduł i chcemy przeanalizować jego działanie

Poniżej przeanalizujemy metody przydatne w tych przypadkach.

Jak sobie radzić z odpluskwianiem jądra?

2. Wsparcie ze strony jądra

1. Komunikacja - funkcja printk i logi systemowe

Kiedy jądro chce coś zakomunikować, nie może oczywiście ot tak sobie wypisać komunikatu diagnostycznego na ekran/stdout.

Komunikaty jądra są przechowywane, a w dostępie do nich pośredniczą elementy jądra:

Jeśli w kodzie jądra chcemy wypisać komunikat, używamy funkcji printk.

Jej działanie polega na dodaniu do bufora komunikatów (log_buf) sformatowanego uprzednio komunikatu.

Komunikaty rożnią się stopniem istotności:

W tym celu, komunikat systemowy może być opatrzony informacją o istotności (loglevel), która może przyjmować następujące wartości:

loglevel nazwa stałej opis
0 KERN_EMERG Awaria (np. kernel panic)
1 KERN_ALERT Alarm
2 KERN_CRIT Błąd krytyczny
3 KERN_ERR Błąd
4 KERN_WARNING Ostrzeżenie
5 KERN_NOTICE Uwaga
6 KERN_INFO Informacja
7 KERN_DEBUG Komunikat diagnostyczny

loglevel daje nam możliwość filtrowania logów systemowych.

Możemy określić, które komunikaty chcemy widzieć (tzn. maksymalny loglevel dla komunikatów), za pomocą pliku /proc/sys/kernel/printk, który zawiera cztery liczby:

Dotyczy to tylko trafiania komunikatów na konsolę.

Jak programy trybu użytkownika mogą się dostać do listy komunikatów? Jądro udostępnia dwie możliwości:

BHP. Nie używać ze spinlockiem!

2. Dostęp do logów systemowych - dmesg, klogd, syslogd.

Dowolny program może czytać komunikaty jądra.

W systemach Linuksowych obsługą komunikatów zajmują się demony systemowe klogd i syslogd:

Oprócz klogd i syslogd, jest też program dmesg, który po prostu wypisuje komunikaty jądra.

Wszystko powyżej o obiegu komunikatów w Linuksie streszcza poniższy rysunek:

SHOW A oto przykład praktyczny w postaci modułu:

#include <linux/module.h>
	
MODULE_LICENSE("GPL");

static int init() {
	printk(KERN_DEBUG "komunikat diagnostyczny\n");
	printk(KERN_INFO "informacja\n");
	printk(KERN_NOTICE "uwaga\n");
	printk(KERN_WARNING "ostrzezenie\n");
	printk(KERN_ERR "blad\n");
	printk(KERN_ALERT "alarm\n");
	printk(KERN_EMERG "awaria\n");
	return 0;
}

static void exit() {}

module_init(init);
module_exit(exit);

Przenosimy się do konsoli (w zwykłym terminalu komunikaty się nie pojawią):

Widzimy, jak wpływa zawartość /proc/sys/kernel/printk na wypisywanie komunikatów. A oto, co wypisuje dmesg, a co trafia do /var/log/messages:

Zaobserwowaliśmy brak wpływu stałej console_loglevel na zachowanie obu.

3. Symbole jądra

W przypadku błędu często nie dostajemy od jądra informacji, które są "human-readable", np. :

Sytuację pogarsza różnorodność jąder - różnią się nie tylko wersją, ale też opcjami kompilacji itp., a co za tym idzie, ta sama funkcja jest często pod różnymi adresami.

Dlatego w Linuksie istnieje szereg sposobów na wydobycie potrzebnych informacji, np. przetworzenia adresów na symbole:

Część informacji przetwarza za nas klogd. Można się też posłużyć programem ksymoops, który do wersji 2.6 wchodził w skład źródeł jądra.

Mając zdekompresowane jądro vmlinux oraz dostęp do pamięci, możemy debugować jądro zwykłym debuggerem gdb, jednak jego działanie będzie ograniczone do dezasemblowania oraz podglądania zmiennych.

SHOW Oto przykład użycia debuggera gdb do zdezasemblowania funkcji printk:

4. Komunikat Oops

Komunikat Oops jest złożonym komunikatem wypisywanym w momencie poważnego błędu. Taki błąd nie musi spowodować załamania się systemu. Komunikat zawiera rozliczne dane, takie jak:

Jak już pisałem, klogd samoczynnie zamienia magiczne adresy funkcji na symbole i numery instrukcji.

SHOW Dla przykładu, testowy moduł o treści:

#include <linux/module.h>
	
MODULE_LICENSE("GPL");

static int init() {
	printk(KERN_INFO "Jestem\n");
	return 0;
}

static void exit() {
	char * ptr = NULL;
	printk(KERN_INFO "Pa pa\n");
	ptr[0] = 'a'; // null pointer
}

module_init(init);
module_exit(exit);

w przypadku próby jego usunięcia poleceniem rmmod może spowodować wyrzucenie takiego oto komunikatu:

5. Makra BUG i BUG_ON

Działają jak asercje, tzn. dają możliwość zapewnienia, że jeżeli zajdzie coś, co nie powinno nigdy zajść, zgłoszony zostanie błąd.

Składnia jest prosta:

Dodanie opcji kompilacji CONFIG_DEBUG_BUGVERBOSE spowoduje, że będą one wypisywać jeszcze więcej informacji niż zwykle.

Dla przykładu, moduł jak wyżej z BUG() zamiast null pointera:

#include <linux/module.h>
	
MODULE_LICENSE("GPL");

static int init() {
	printk(KERN_INFO "Jestem\n");
	return 0;
}

static void exit() {
	printk(KERN_INFO "Pa pa\n");
	BUG();
}

module_init(init);
module_exit(exit);

w przypadku próby jego usunięcia poleceniem rmmod może spowodować wyrzucenie komunikatu:

6. Kernel panic

Kiedy jądro napotyka na naprawdę poważny błąd, podejmuje taktyczną decyzję o odwrocie, tj. panikuje. Kernel panic polega na:

  1. wypisaniu komunikatu o błędzie,
  2. zrobieniu core dump'a, tj. zapisaniu stanu pamięci na potrzeby pośmiertnego debugowania
  3. zrebootowaniu maszyny / wpadnięciu w nieskończoną pętlę.

Jak widzimy, nawet w takiej sytuacji jądro wspiera chcących je debugować, zrzucając pamięć, aby można było dojść przyczyn błędu.

A oto przykładowy kernel panic:

7. Opcje kompilacji jądra

W konfiguracji jądra, w części "Kernel Hacking" można znaleźć następujące opcje:

Poniżej wyjaśniam, do czego służy większość z nich (ostatnia z nich pochodzi z częsci "Modules":
CONFIG_PRINTK_TIME Powoduje, że funkcja printk oprócz komunikatu wypisuje czas systemowy w momencie jego nadania; przydatne do oceny różnicy czasowej pomiędzy komunikatami jądra.
CONFIG_MAGIC_SYSRQ Włącza obsługę kombinacji Magic SysRq, której działanie opiszę poniżej.
CONFIG_LOG_BUF_SHIFT Logarytm dwójkowy z rozmiaru bufora komunikatów. Domyślna wartość to 16 odpowiadająca 64K, wartość 17 odpowiada 128K etc.
CONFIG_DETECT_SOFT_LOCKUPS Włącza wykrywanie "miękkich lockupów", reagując na przestoje dłuższe niż 10 sekund wypisaniem stack trace.
CONFIG_SCHEDSTATS Powoduje generowanie statystyk nt działania schedulera i udostępnia je w /proc/schedstat
CONFIG_DEBUG_MUTEXES Wykrywa deadlocki sekcji krytycznych
CONFIG_DEBUG_SPINLOCK Wykrywa deadlocki na spinlockach i inne błędy związane ze spinlockami
CONFIG_DEBUG_SPINLOCK_SLEEP System buntuje się, kiedy coś usiłuje zasnąć mając włączony spinlock.
CONFIG_DEBUG_KOBJECT Dodatkowe informacje o kobjectach.
CONFIG_DEBUG_BUGVERBOSE Powoduje wypisywanie większej ilości informacji przez makra BUG i BUG_ON
CONFIG_DEBUG_INFO Kompilacja z dodaniem informacji potzrbnej do odpluskwiania przez kompilator (opcja -g gcc)
CONFIG_DEBUG_FS Włącza debug_fs, pomocniczy system plików, podobny do proc-a
CONFIG_DEBUG_VM Włącza debugowanie obsługi pamięci wirtualnej
CONFIG_EARLY_PRINTK Włącza funkcję early_printk, działająca jak printk, kiedy printk nie może zostać jeszcze użyta (np. przed zainicjalizowaniem konsoli). Powoduje ona wypisywanie komunikatów do bufora VGA lub portu szeregowego.
CONFIG_DEBUG_STACKOVERFLOW Powoduje wykrywanie błędu przepełnienia stosu.
CONFIG_STACK_BACKTRACE_COLS Określa liczbę pozycji stosu wywołań w jednej linijce komunikatów takich jak oops
CONFIG_DEBUG_RODATA Chroni struktury jądra "tylko do odczytu" przed zapisem. Spowalnia pracę systemu, gdyż uniemożliwia użycie TLB dla stron na których są te dane.
CONFIG_MODULE_FORCE_UNLOAD Powoduje "siłowe" usuwanie modułów w przypadku błędu.

8. Klawisz Magic SysRq

Magic SysRq jest kombinacją klawiszy pozwalającą na zareagowanie na destabilizację pracy systemu (np. lockupy z reagowaniem na przerwania). Zwykle przywołujemy go, wciskając kombinację prawy alt + print screen + X, gdzie X może być jedną z poniższych opcji:

Action X
Set the console log level, which controls the types of kernel messages that are output to the console 0 - 9
Immediately reboot the system, without unmounting partitions or syncing b
Reboot kexec and output a crashdump c
Send the SIGTERM signal to all processes except init (PID 1) e
Call oom_kill, which will kill a process that is consuming all available memory. f
Output a terse help document to the console
Any key which is not bound to a command should also do the trick
h
Send the SIGKILL signal to all processes except init i
Kill all processes on the current virtual console (Can be used to kill X and svgalib programs, see below)
This was originally designed to imitate a Secure Access Key
k
Send the SIGKILL signal to all processes, including init l
Output current memory information to the console m
Shut off the system o
Output the current registers and flags to the console p
Switch the keyboard from raw mode, the mode used by programs such as X11 andsvgalib, to XLATE mode r
Sync all mounted filesystems s
Output a list of current tasks and their information to the console t
Remount all mounted filesystems in read-only mode u
Output Voyager SMP processor information v

(z http://en.wikipedia.org/wiki/Magic_sysrq)

Aby pamiętać sekwencję opcji prowadzących do stabilnego rebootu, wymyślono następujący mnemonik:

"Raising Skinny Elephants Is Utterly Boring" is often useful. It stands for Raw (take control of keyboard back from X), Sync (flush data to disk), tErminate (kill -15 programs, allowing them to terminate gracefully), kIll (kill -9 unterminated programs), Unmount (remount everything read-only), reBoot.

SHOW A oto obrazek z Magic SysRq + m:

9. Śledzenie - ptrace, strace i ltrace

Chyba najważniejszą funkcją związaną z tematem debuggowania w Linuksie jest ptrace.

#include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); 

Wywołanie to udostępnia procesowi-rodzicowi możliwość śledzenia jego dziecka, a także pozwala na obserwowanie i kontrolowanie go. Jest to podstawowy mechanizm służący do implementacji pułapek (breakpoint) i śledzenia wywołań systemowych.

Programy strace i ltrace służą do śledzenia wywołań odpowiednio funkcji systemowych oraz bibliotecznych. Wypisują one na stderr informację o kolejnych wywołaniach.

SHOW Przykład: poniżej fragmenty wyników wywołania strace i ltrace dla links www.google.com:



KDB - debugger działający w trybie jądra

1. Wstęp oraz instalacja

KDB jest debuggerem działającym w trybie jądra. Jest dystrybuowany jako patch na jądro, zatem aby go używać musimy go w nie wkompilować. Projekt KDB jest zarządzany przez Silicon Graphics i można go sciągnąć ze strony http://oss.sgi.com/projects/kdb/.

Opiszę, jak u mnie przebiegała instalacja. Po sciągnięciu patchy kdb-v4.4-2.6.17-i386-1.bz2 oraz kdb-v4.4-2.6.17-common-1.bz2 i odbzip2owaniu ich, zainstalowałem je poleceniem:

patch -p1 < kdb-v4.4-2.6.17-common-1
patch -p1 < kdb-v4.4-2.6.17-common-1

Potem jeszcze konfiguracja (make menuconfig), w której należy włączyć "Built-in kernel debugger support" (opcja dodana przez patch) oraz "Debug info" (przydają się także inne flagi np. debug frame pointer):

Następnie kompilujemy jądro poleceniem make bzImage i utworzony obraz jądra kopiujemy do /boot:

cp arch/i386/boot/bzImage /boot/bzImageKDB

Aby odpalić teraz jądro z KDB, podczas startu systemu wchodzimy do gruba, wybieramy 'c' (command line) i wpisujemy:

2. Komendy KDB

Po odpaleniu Linuksa z włączonym kdb, możemy wejść do debuggera wciskając klawisz Pause. Praca z debuggerem przypomina pracę z gdb. Poniżej opiszę podstawowe komendy.

id [symbol|addr] dezasembluje funkcję o podanej nazwie / adresie
md addr l wypisuje l linii pamięci, poczynając od adresu addr
mm addr val Umieszcza wartość val pod adresem addr
rd Wypisanie zawartości rejestrów procesora
rm %reg val Umieszcza wartość val w rejetrze reg
bp [symbol|addr] Ustawia breakpoint na funkcji o podanym symbolu / adresie
bl Wypisuje wszystkie breakpointy
[be/bd] nr Włącza/wyłącza breakpoint o podanym numerze
bc nr Usuwa breakpoint o podanym numerze
btp pid Wypisuje ślad stosu procesu o numerze pid
ss "Single step" - wykonuje pojedynczą instrukcję

3. Przykład

SHOW Na koniec -- przykład użycia KDB. Będąc w konsoli, naciskamy Pause, aby znaleźć się w KDB. Mogą wystąpić problemy z klawiaturą!

Na początek wypiszemy stan rejestrów komendą rd:

Następnie obejrzymy fragment kodu funkcji scheduler_tick:

Na koniec, obejrzymy sobie stos wywołań wybranego procesu o pidzie 6, po czym ustawimy breakpoint na scheduler_tick. Wracamy do systemu poleceniem go. Oczywiście breakpoint od razu zadziała i znajdziemy się znowu w debuggerze.

KGDB

[vmware-conf] [nullmodem]

1. Motywacja

Jak wcześniej wspomniano, jest wiele problemów związanych z debugowaniem jądra, takich jak zależnością działania debbugera od działania jądra, brak możliwości skorzystania z interfejsów graficznych czy choćby nawet braku oficjalnego wsparcia dla debbugerów od strony jądra. Jednym z rozwiązań, które rozwiązuje przynajmniej część z nich jest KGDB.

Strona domowa: http://kgdb.linsyssoft.com/index.html
Wikipedia: http://en.wikipedia.org/wiki/Kgdb
FreeBSD Developers' Handbook: http://www.freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/kerneldebug.html

2. Informacje ogólne

Zastosowana technika jest znana od względnie wielu lat i polega na debbugowaniu maszyny z innego komputera, stosowana głównie do pracy na bardzo niskim poziomie, który uniemożliwia normalną pracę na komputerze. Jest to pomocne np. przy pisaniu sterowników, ale znajduje również inne rozmaite zastosowania, jak np. crackowanie gier (SoftICE).

Zależnie od debbugera, komputery mogą połączone w różny sposób, w przypadku KGDB jest to łącze szeregowe. Nie jest to jednak kłopot, ze względu na to, że emulacja (na wirtualnej maszynie) portu szeregowego nie jest trudna (na linuxie tak, a na windowsie tak), a w dodatku w nowszych wersjach KGDB istnieje również (podobno) możliwość komunikacji przez Ethernet.

3. Działanie

Zacznijmy od tego, czym jest KGDB. Właściwie KGDB nie jest tak naprawdę debuggerem, a niezależnym zestawem łat na jądro, które umożliwiają zdalne połączenie się z komputerem na niskim, zależnym od bardzo niewielu rzeczy poziomie.


4. Instalacja

Po dodaniu poprawek do jądra (w prawdzie tutaj używałem jądra 2.6.22, ale dla 2.6.17.13 patche też są dostępne)


Włączamy odpowiednie opcje w jądrze




I rekompilujemy jądro

# make && make install_modules
.
.
.
# cp arch/i386/boot/bzImage /boot/vmlinuz_kgdb

Teraz trzeba przekazać do jądra przy uruchomieniu odpowiednie parametry:

kgdbwait kgdb8250=numer_portu,predkosc_port
Edytujemy Gruba
title=Gentoo Linux 2.6.22-r9 KGDB
root (hd0,1)
kernel /boot/vmlinuz_kgdb root=/dev/hda2 kgdbwait kgdb8250=0,112500
lub LILO
image=/boot/vmlinuz_kgdb
  label=gentoo_kgdb
  root=/dev/hda2
  append="kgdbwait kgdb8250=0,112500"

5. Użycie

Uruchamiamy maszynę, ktora będzie debugowana.

Potrzebujemy podobne jądro dla maszyny, z której będziemy debugować, a później tylko odpalamy gdb i mamy standardowe środowisko:

stty ispeed 115200 ospeed 115200 < /dev/ttyS0
gdb vmlinux
(gdb) target remote /dev/ttyS1 
.
.
.
(gdb) c
.
.



(używamy narzędzia vmwaregateway, które przekierowuje COM1 na port TCP:567)







6. Środowisko graficzne

GDB na komputerze, z którego debugujemy działa tak jak się spodziewamy i można do niego podłączyć bez problemów podłączyć środowisko graficzne, np. ddd (http://www.gnu.org/software/ddd/):






User Mode Linux

Ogólnie o UML

User Mode Linux, to narzędzie pozwalające uruchomić jądro Linuxa, jako proces. Pod kontrolą działającego już wcześniej jądra Linuxa.

Korzyści:

Dlaczego UML nadaje się do debugowania jądra?

Można użyć standardowych narzędzi do debugowania procesów, które są dostępne w linuxie. Nie trzeba prosić o pomoc KDB.

Instalacja i konfiguracja

Ściągamy Kernela z www.kernel.org



Rozpakowujemy plik tar.bz2



Wchodzimy do rozpakowanego katalogu i budujemy konfigurację jądra, używając parametru ARCH=um. To bardzo ważne, bez tego parametru nie uda nam się zbudować jądra dla UML.



Są także alternatywy dla MENUCONFIG, można użyć OLDCONFIG, XCONFIG lub CONFIG.

Oto MENUCONFIG:



Musimy ustawić opcje, które przydadzą nam się do pracy z UMLem. Najważniejsze pozycje w głównym Menu, to:





Zapisujemy konfigurację.

Domyślnie jest ona zapisywana do pliku .config.





Kompilujemy jądro poleceniem make, pamiętając o parametrze: ARCH=um.



Jądro skompilowało się, jeśli o wszystkim pamiętaliśmy.



Jeśli chcemy debugować moduły, dobrze jest je także skompilować. Tu też trzeba pamiętać o parametrze ARCH=um.



Moduły trzeba zainstalować, pamiętając o podmianie katalogu, na katalog inny niż katalog z modułami hosta.



Następnie należy ściągnąć binarne środowisko dla jądra, czyli tzw. filesystem.



Filesystem, celem wprowadzenia uprzednich modyfikacji należy podmontować lokalnie.



Gdyby ktoś korzystał z image Slackware-8.1, należy zmienić nazwę montowanego urządzenia w /etc/fstab:



Debugowanie UML w GDB

Gdy wszystko gotowe, można już odpalić UMLa. Naszym celem jest debugowanie, uruchamiamy go więc poprzez GDB.



Obsługujemy sygnały i ustawiamy breakpoint na start_kernel.



Uruchamiamy program, czyli w naszym wypadku jądro Linuxa.



Breakpoint został przechwycony.



Dla modułów jest troszkę inaczej:
(gdb) break kernel/module.c:1775
(gdb) continue

(gdb) print ((struct module *) _mod)->module_core
(gdb) add-symbol-file HOST_PATH_TO_KO [wynik_poprzedniej_linijki]
(gdb) break debugowana_funkcja
(gdb) continue

Podsumowanie

Teraz, gdy już wiemy jak odpluskwiać kod systemowy, warto na koniec zacytować osobę, która jest odpowiedzialna, za to całe zamieszanie.

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.

--Linus

Jak widać nie wszyscy chwalą wsparcie debbugerów w jądrze. Czy to dobrze? Nie będziemy tu spekulować nad znaczeniem tych słów, kwestie, którymi kierował się cytowany podczas swojej wypowiedzi pozostawimy słuchaczom jako zadanie do rozważenia.

I jeszcze coś ekstra

RR0D RASTA DEBUGGER

Strona domowa tego czegoś

Cytaty z FAQ:

Q: Hey man, is rr0d at least has a rasta mode? 
A: yea man, Of course it has. 

Q: Hey man, why rr0d and not KGDB? 
A: man, KGDB is *not* a rasta debugger 

Q: Hey man, how many functionalities rr0d has? 
A: man, it has plenty rasta functionalities