PODSTAWY DEBUGOWANIA JĄDRA
Co sprawia problemy?

Kod jądra nie może być w łatwy sposób uruchomiony ani śledzony pod kontrolą debugera z jednego powodu: jest to zestaw funkcji nie związanych z żadnym określonym procesem. Trudno jest powtórzyć błąd, który zauważyliśmy. Ponadto, może on spowodować awarię całego systemu i zniszczyć swoje świadectwo. Możemy spróbować zaklasyfikować różne rodzaje błędów: - wszystko działa, ale inaczej niż się tego spodziewaliśmy, - komputer się zawiesil (lockups); - zgłaszany jest błąd oops (normalny sposób informowania użytkonkia o nieprawidłowościach w działaniu jądra) - kernel panic -błąd paniczny powodujący załamanie się systemu

Konfiguracja jądra.

W testowaniu kodu jądra i debugowaniu przydatne mogą się okazać dostępne liczne opcje konfiguracyjne kompilacji jądra. Opcje te są zebrane w dziale Kernel hacking.

  • CONFIG_DEBUG_KERNEL - aktywność wszystkich tych opcji uzależniona jest od włączenia ogólnej opcji.
  • CONFIG_DEBUG_SLAB - włącza różne rodzaje sprawdzania alokacji pamięci, pozwala wykryć nadpisywanie buforów albo korzystanie z niezainicjanilowanej pamięci.
  • CONFIG_DEBUG_SPINLOCK - jądro wyłapuje działania na niezainicjalizowanym spinlocku oraz inne błędy (np. podwójne zwolnienia blokad)
  • CONFIG_DEBUG_INFO - jeżeli chcemy odpluskwiać jądro używając gdb musimy zaznaczyc tę opcję.
  • CONFIG_MAGIC_SYSRQ - włącza klawisz "magic SysRq". Opcja jest domyślnie nieaktywna.
  • CONFIG_DEBUG_STACKOVERFLOW i CONFIG_DEBUG_STACK_USAGE - opcje służące do wykrywania przepełnienia stosu.
  • CONFIG_KALLSYMS - domyślna. Powoduje, że tablica symboli jądra jest wbudowana w jądro. Odznaczenie tej opcji spowoduje, że wszystkie informacje wypisywane przez oops będą w systemie szesanstkowym.
  • CONFIG_IKCOFIG i CONFIG_IKCONFIG_PROC - dzięki temu konfiguracja jądra będzie dostępna przez /proc

Jeżeli zamierzamy modyfikować jądro warto włączyć je wszystkie

printk() - debugowanie przez wypisywanie.

Najważniejsze informacje.

Możliwość klasyfikowania komunikatów i nadawania im priorytetów.

Stałe zdefiniowane są w pliku linux/kernel.h:

W zależności od priorytetu jądro wypisuje informacje na konsolę, do portu szeregowego albo równoległego. Jeżeli w systemie działają zarówno klogd i syslogd to wszystkie komunikaty trafiają do pliku /var/log/messages. Jeżeli wyłączymy demona klogd, komunikaty będzie można obejrzeć tylko w pliku /proc/kmsg (na przykład poleceniem dmesg).

Aby dowiedzieć się nieco na temat ustawień naszej konsoli mozna wykonać następujące polecenia:

cat /proc/sys/kernel/printk

Cztery wartości w pliku to:

  • console_loglevel
  • default_message_loglevel
  • minimum_console_level
  • default_console_loglevel

Wartości te wpływają na zachowanie printk() podczas wypisywania lub logowania komunikatów błędów. Komunikaty o priorytecie wyższym niż console_loglevel będą wypisywane na konsoli. Komunikaty bez jawnego priorytetu będą wypisywane z priorytetem default_message_level. minimum_console_loglevel jest najmniejszą (najwyższą) wartością, którą można ustawić jako console_loglevel. default_console_loglevel jest domyślną wartością dla console_loglevel.

Sprawdźmy, czy printk() rzeczywiście działa:

echo 4 > /proc/sys/kernel/printk

Zmieniamy wartość console_loglevel na liczbę pomiędzy KERN_ALERT i KERN_NOTICE. Przy załadowaniu modułu komunikat pojawia się na konsoli. Przy odłączeniu natomiast nie, znajdziemy go w pliku /var/mlog/messages.

cat /var/log/messages

printk() a klogd i syslogd

Jak to działa? Dlaczego np. jeżeli wyłączymy klogd'a, to komunikaty obejrzeć będzie można wyłącznie w pliku /proc/mesg?

Komunikaty jądra umieszczane są w cyklicznym buforze o rozmiarze LOG_BUF_LEN. Po całkowitym zapełnieniu nadpisywane są najstarsze komunikaty. Z reguły jest to 16KB dla systemów jednoprocesorowych (ogólenie: od 4KB do 1 MB). Po umieszczeniu komunikatu w buforze, zostają obudzone procesy, które na ten komunikat czekają - takie, które czytają plik /proc/kmsg lub odwołują się do syslogd'a. Za pobieranie komunikatów jądra z bufora odpowiedzialny jest demon przestrzeni użytkownika klogd. Demon ten zapisuje komunikaty do systemowego pliku dziennika, korzystając przy tym z pomocy demona syslogd. Klogd w celu odczytywania komunikatów korzysta z pliku /proc/kmsg (bądź też z systemowego wywołania syslog()). Gdy przychodzi nowy komunikat, przekazuje go do demona syslogd. Jeżeli klogd nie działa, komunikaty pozostają w buforze, dopóki się on nie przepełni, lub ktoś je odczyta. Syslogd natomiast zależnie od swojej konfiguracji dostarcza wywołane komunikaty do pliku (domyślnie jest to plik/var/log/messages). Działanie demona można konfigurować w pliku /etc/syslog.conf.

Inne sposoby zarządzania komunikatami.

  • uruchomienie klogd z opcją -c [n] powoduje zmianę poziomu logowania konsoli na n.
  • uruchomienie klogd z opcją -f [plik] powoduje, że demon wypisuje komunikaty do oddzielnego pliku.
  • modyfikacja pliku /etc/syslog.conf
printk() po raz trzeci. Podsumowanie.

Zalety:

  • bardzo podobne do printf() - łatwe w użyciu,
  • funkcja ta może być użyta wszędzie w jądrze.

Wady:

  • problematyczne przy debugowaniu w procesie bootowania - czyli bardzo wcześnie, przed uruchomieniem konsoli. Alternatywa: użycie early_printk(). (plik kernel/early_printk.c)
  • częste wywołanie tej funkcji obciąża system. Dzieje się tak ponieważ wypisanie każdej linii wymaga kosztownej operacji dyskowej. syslogd próbuje zapisać wszystko na dysk na wypadek awarii systemu zaraz po wypisaniu komunikatu. Problem ten można rozwiązać na kilka sposobów, na przykład wypisywać komunikaty co zadany przedział czasu, albo użyć funkcji int printk_ratelimit(void).

strace i ltrace.

strace

  • śledzi wywołania systemowe programu przestrzeni użytkownika,
  • wyświetla nazwy wywołań,
  • wyświetla argumenty w postaci symbolicznej,
  • wyświetla symboliczną nazwę błędu oraz odpowiadający jej napis (jeżeli któreś z wywołan zakończy się błędem),
  • uzyskuje te dane z jądra - program może być śledzony bez wsparcia dla debuggowania
  • ma wiele istotnych opcji:
    • -t kiedy nastąpiło wywołanie systemowe,
    • -T czas spędzony w wywołaniu systemowym,
    • -e ogranieczenie typu śledzonych wywołań. Bardzo rozwinięta opcja, np. -eopen (oznacza -e trace=open) pozwala śledzić tylko wywołania systemowe open,
    • -o przekieruje wynik działania programu do pliku,
    • -f śledzenie nie tylko procesu macierzystego ale i dzieci,
    • -p możliwość "podłączenia" się do działającego w systemie procesu (strace -p pid )

ltrace

    Bardzo podobny do strace. Sledzi wywołania funkcji bibliotecznych (zwykle standardowych funkcji bibliotecznych C). Istnieje możliwość połączenia mozliwości obu programów uzywając lstrace -S, które wyświetli nam informacje o wywołaniach zarówno funkcji systemowych jak i bibliotecznych.

Zawieszenie się systemu.

np. program wpada w nieskończoną pętlę-> jądro nie może go mogło wywłaszczyć -> system nie reaguje na żadne akcje -> nic nie zostanie wypisane. Zapobiec takim sytuacjom może wstawianie wywyołania schedule() w newralgicznych miejscach kodu.

Przydatnym narzędziem jest klawisz "magic SysRq" uruchamiany przez naciśnięcie klawiszy Alt+SysRq i trzeciego klawisza spośród:

  • h - help,
  • k - zabicie wszystkich procesów na danym terminalu,
  • s - synchronizacja dysków,
  • u - odmontowanie i ponowne zamontowanie dysków w trybie tylko do odczytu,
  • b - natychmiastowy restart systemu (przedtem należ wykonać kombinację z s i u),
  • p - informacje o rejestrach procesora,
  • m - informacja o pamięci

By korzystać z tych opcji, konieczne jest zaznaczenie jej przy konfiguracji jądra. Naciśnięcie kolejno klawiszy Alt+SysRq i s, u oraz b jest w miarę bezpiecznym zakończeniem pracy systemu (bezpieczniejszym niż zwykły Reset).

Wprowadzenie do gdb.

gdb (gnu project debugger) to aplikacja pozwalająca analizować proces w trakcie działania. Służy do wychwytywania pluskiew w kodzie za pomocą kilku podstawowych technik:

  • ustalenie zbioru zdarzeń (breakpointów, watchpointów), których zajęcie ma wstrzymać działanie programu
  • możliwość odczytu stanu programu w momencie przerwania działania
  • stała modyfikacja kodu i analiza zmieniających się wyników debuggera

Najważniejsze instrukcje:

debug plik - uruchamia gdb na danym pliku

run parametry - wykonuje bieżacy program z zadanymi parametrami

attach pid - ładuje debugger dla działającego procesu (o zadanym id)

detach - zwalnia proces spod kontroli gdb

break nagłówek_funkcji - ustawia breakpoint (flagę) na danej funkcji

break numer_linii - ustawia breakpoint na określonej linii pliku źródłowego

info break - drukuje informację o ustawionych breakpointach

delete numer - usuwa breakpoint o numerze numer

step - wykonuje bieżącą linię kodu (po czym zatrzymuje się)

next - podobnie jak step, lecz jeśli w w następnej linii jest wywołanie funkcji to zostaje ona wykonana

continue - wykonuje program w normalnym trybie aż ten się zatrzyma lub natrafi na breakpoint

finish - wykonuje kod aż do momentu, gdy funkcja z bieżącej ramki stosu powróci

backtrace - wyświetla łańcuch wywołań funkcji, który doprowdził program do bieżącego stanu

watch wyrażenie - wstrzymuje program jeśli wartość wyrażenia ulegnie zmianie (watchpoint)

print wyrażenie - pokazuje aktualną wartość wyrażenia w programie

condition numer warunek - wstrzymuje program jeśli warunek = true

quit - opuszcza gdb

Ctrl^C - wstrzymuje działanie procesu i czeka na instrukcję użytkownika


Aby korzystac z gdb kompilujemy nasze programy z opcją -g.

ddd (data display debugger) to aplikacja dostarczająca interfejs graficzny dla debuggerów tekstowych działających pod Linuksem (np. gdb, dbx, python debugger). Program wzbogaca zestaw opcji debuggera o szereg narzędzi, m.in. możliwość wizualizacji stanu struktur danych bieżącego procesu w postaci grafów i diagramów.