Odpluskwianie w jądrze

Autor: Michał Korch

  • 1. Gadka wstępna
  • 2. Wyprowadzanie informacji
  •  2.1 Funkcja printk(), demony klogd i syslogd
  •  2.2 Makro BUG()
  •  2.3 Wywołanie panic()
  •  2.4 Wydruk śladu stosu
  • 3. Analiza błędów oops
  •  3.1 Reakcja systemu, oops vs. kernel panic
  •  3.2 Co nam mówi oops, analiza ksymoops i kallsyms
  • 4. Radzenie sobie z lockupami
  •  4.1 Rodzaje lockupów
  •  4.2 Lockupy z umożliwionymi przerwaniami, klawisze sysinfo, Magic sysrq key
  •  4.3 Lockupy z zablokowanymi przerwaniami, NMI Watchdog i inne
  • 5. Debugger do jądra
  •  5.1 Brak ,,oficjalnego'' debuggera jądra
  •  5.2 Co można zdziałać zwykłym debuggerem gdb
  •  5.3 kgdb
  •  5.4 Krótko o kdb
  • 6. User Mode Linux i jego użycie do debuggowania jądra
  •  6.1 Ogólnie o UMLu
  •  6.2 Instalacja
  •  6.3 Jak to działa?
  •  6.4 Odpluskwianie przy użyciu UMLa
  • 1. Gadka wstępna

    Use the Source, Luke, use the Source. Be one with the code.'. Think of Luke Skywalker discarding the automatic firing system when closing on the deathstar, and firing the proton torpedo (or whatever) manually. _Then_ do you have the right mindset for fixing kernel bugs.
    Linus

    Jak już pewnie wszyscy wiecie, z przygód związanych z labami do tego przedmiotu, jedną z ważniejszych cech różniącą programowanie w jądrze od programowania przestrzeni użytkownika jest stopień trudności w diagnostyce błędów. Problem komplikuje choćby to, że błąd przy programowaniu w jądrze może załamać cały system. Ta część prezentacji została pomyślana po to, żeby choć trochę pomóc w trudnym zadaniu odpluskwiania kodu w jądrze.

    W kwestii samych błędów (a raczej tego, co widzi użytkownik systemu, gdy następi błąd), to należy rozróżnić cztery sytuacje:

    W testowaniu kodu jądra i debuggowaniu przydatne mogą się okazać dostępne liczne opcje konfiguracyjne kompilacji jądra. Opcje te są zebrane w dziale Kernel hacking. Aktywność wszystkich tych opcji uzależniona jest od włączenia ogólnej opcji CONFIG_DEBUG_KERNEL. Jeżeli zamierzamy modyfikować jądro warto włączyć je wszystkie. W niniejszej prezentacji opisane są niektóre (częściej używane) z tych opcji.

    2. Wyprowadzanie informacji

    Najmniej skomplikowanym sposobem próby diagnozy tego, co tak na prawdę się złego dzieje i dlaczego jest wyprowadzenie informacji.

    2.1 Funkcja printk(), demony klogd i syslogd

    Funkcja printk() działa niemal identycznie, jak funkcja printf() z biblioteki dostępnej przy programowaniu przestrzeni użytkownika. Tak więc jest to funkcja wypisująca sformatowany ciąg znakowy. Nie jest jednak do końca taka zwyczajna.

    Jej przydatność polega właśnie na tym, że jest niezawodna i wszechstronna. Daje się wywołać niemal z dowolnego miejsca jądra, w dowolnym momencie wykonania, z dowolnego kontekstu. Można ją wywołać współbieżnie na wilu procesorach, bez konieczności pozyskiwania jakiejkolwiek blokady.

    Z jednym wyjątkiem: nie da się jej wywołać we wczesnej fazie rozruchu jądra, przed momentem zainicjowania konsoli. Niektórzy programiści wyprowadzają wtedy komunikaty na sprzęt, który działa zawsze, np. na port szeregowy. Istnieje jednak jeszcze słabo przenośna i nie na każdej architekturze zaimplementowana fukcja early_printk(), niczym się nie różniąca od printk(), poza tym, że można ją wywołać wcześniej.

    Inna główna różnica między wywołaniami printk() i printf() tkwi w zdolności tej pierwszej do określania poziomu rejestrowania (ang. loglevel). Wygląda to tak:

    printk(KERN_WARNING "Minister zdrowia ostrzega: palenie tytoniu powoduje choroby płuc.\n");
    printk(KERN_INFO "To jest prezentacja o odpluskwianiu.\n");
    printk("Nie został określony poziom rejestrowania.\n");
    W poniższej tabeli wypisano dostępne poziomy rejestrowania:
    KERN_EMERG < 0> Sytuacja awaryjna
    KERN_ALERT < 1> Problem wymagający natychmiastowej interwencji
    KERN_CRIT < 2> Sytuacja krytyczna
    KERN_ERR < 3> Błąd
    KERN_WARNING < 4> Ostrzeżenie
    KERN_NOTICE < 5> Sytuacja normalna, warta odnotowania
    KERN_INFO < 6> Informacja
    KERN_DEBUG < 7> Komunikat diagnostyczny
    Powyższe stałe zdefiniowane są w pliku linux/kernel.h. Wartości dołączane są po prostu na początek komunikatu. Jądro orównuje poziom z bieżącym poziomem rejestrowania (console_loglevel) i na tej podstawie decyduje, czy komunikat powinien być wyświetlony w konsoli, czy przekierowany gdzie indziej. W przykadku nieokreślenia poziomu, za jego wartość przyjmowany jest DEFAULT_MESSAGE_LOGLEVEL.

    Komunikaty jądra umieszczane są w cyklicznym buforze o rozmiarze LOG_BUF_LEN. Można go konfigurować przy kompilacji bodajże za pomocą opcji CONFIG_LOG_BUF_SHIFT. Z reguły jest to 16kB.

    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()). klog budzi się, gdy przychodzi nowy komunikat i przekazuje go do demona syslogd.

    syslogd natomiast dostarcza wywołane komunikaty do pliku (domyślnie jest to plik /var/log/messages). Dziłanie demona można konfigurować w pliku /etc/syslog.conf, który wygląda mniej więcej tak:

    # Log all kernel messages to the console.
    # Logging much else clutters up the screen.
    #kern.*							/dev/console
    
    # Log anything (except mail) of level info or higher.
    # Don't log private authentication messages!
    *.info;mail.none;authpriv.none;cron.none		/var/log/messages
    
    # The authpriv file has restricted access.
    authpriv.*						/var/log/secure
    
    # Log all the mail messages in one place.
    mail.*							/var/log/maillog
    
    
    # Log cron stuff
    cron.*							/var/log/cron
    
    # Everybody gets emergency messages
    *.emerg							*
    
    # Save news errors of level crit and higher in a special file.
    uucp,news.crit						/var/log/spooler
    
    # Save boot messages also to boot.log
    local7.*						/var/log/boot.log
    

    2.2 Makro BUG()

    Najczęściej używanymi makrami do wyprowadzania informacji i zgłaszania błędu to BUG() i BUG_ON(). Ich wywołanie powoduje zgłoszenie błędu oops wraz z wszelkimi związanymi informacjami. W większości architektur BUG() i BUG_ON() są po prostu rozwijane do jakiejś niedozwolonej operacji i w ten sposób generowany jest błąd oops. Z makrodefinicji tych korzysta się przede wszystkim do oznaczania sytuacji, które nie powinny mieć miejsca (takie asercje). Użycie jest następujące:
    if (zle_sie_dzieje) BUG();
    
    co jest równoważne:
    BUG_ON(zle_sie_dzieje);
    

    2.3 Wywołanie panic()

    Błędy bardziej krytyczne mogą być zgłaszane wywołaniem panic(), które powoduje wyświetlenie komunikatu o błędzie i zatrzymanie systemu. Np.:
    if (straszny_i_okropny_blad) panic("Maksymalna kicha!\n");
    Panic jako argumenty bierze formatowany tekst.

    2.4 Wydruk śladu stosu

    Niekiedy testowanie i odpluskwianie ułatwia wdrukowanie na konsolę śladu stosu. Stłuży do tego dump_stack(). Powoduje wypisanie na konsolę zawartości rejestrów i śladu wywołania funkcji. Przykład użycia:
    if (trzeba_analizowac_slad_stosu) dump_stack(); 
    

    3. Analiza błędów oops

    3.1 Reakcja systemu, oops vs. kernel panic

    Blad oops to standardowy sposób informowania użytkownika o nieprawidłowościach w działaniu jądra. Zgłoszenie błędu oops polega na wyświetleniu komunikatu o błędzie wraz z zawartością rejestrów i śladem wykonania (ang. backtrace). Niekiedy po zakończeniu obsługi pojawia się niespójność jądra. Konieczne jest wtedy ostrożne wycofanie się do poprzedniego kontekstu i przywrócenie kontroli nad systemem.

    Czasem jest to niemożliwe. Jeśli bład wystąpi w kontekście przerwania, jądro nie może nic zrobić i ,,panikuje'' dając w efeckie bład paniczny (kernel panic). Pojawia się on też przy błedzie podczas wykonywania procesu jałowego lub procesu init. Jedynie wystąpienie oopsa w kontekście jednego ze zwykłych procesów daje możliwość unicestwienia tego procesu i kontynuowania działania reszty systemu.

    3.2 Co nam mówi oops, analiza ksymoops i kallsyms

    Oto przykładowy oops:

    Zauważmy, że EIP = adres bazowy funkcji + offset instrukcji. Ślad wykonania to łańcuch wywołania funkcji prowadzący do błędu. Aby dokładnie go przeanalizować możemy zdekodować naszego oopsa za pomocą polecenia ksymoops.

    ksymoops zapis_oops.txt
    Oto przykładowy oops:

    Wtedy adresy wymienione w śladzie wykonania zostaną przetłumaczone na nazwy symboliczne funkcji. ksymoops korzysta w tym celu z pliku System.map generowanego w czasie kompilacji jądra oraz informacji o załadowanych dynamicznie modułach. Oto przykładowy fragment pliku System.map:

    Z wywołania ksymoops dostaniemy więc między innymi następujący napis:

    dzięki któremu możemy prześledzićwykonanie i określić funkcję, która spowodowała błąd. Więcej informacji o ksymoops można znaleźć na stronach man.

    Od wersji rozwojowej jądra 2.5 zostła wprowadzona funkcja kallsyms, aktywowana przy użyciu opcji konfiguracyjnej CONFIG_KALLSYMS. Uaktywnienie tej opcji powoduje umieszczenie w obrazie jądra odwzorowań nazw symbolicznych na adresy pamięci, dzięki czemu jądro może wyprowadzać zdekodowane ślady wykonania. I wtedy ksymoops nie jest już potrzebny.

    Gdy już wiemy, jaka funkcja spowodowała błąd możemy użyć informcji o offsecie (u nas 298) do odnalezienia konkretnej instrukcji, która błąd spowodowała. W tym celu potrzebujemy kopii problematycznej funkcji w postaci kodu assemlerowego. Możemy ją uzyskać za pomocą narzędzia objdump:

    objdump -d plik.o
    Wtedy możemy znaleźć instrukcję związaną ze znanym nam offsetem. Oto fragment pliku:

    Przydatna może być także zawartość rejestrów. Dysponując kopią problematycznej funkcji w postaci kodu assemlerowego można na podstawie zawartości rejestrów odtworzyć stan jej wykonania w momencie wystąpienia błędu.

    Jeśli już wiemy, co w kodzie assemblerowym powoduje błąd warto spróbować odnaleźć to miejsce w kodzie w C. Nie ma jakiejś prostej metody, by to zrobić. Należy zwrócić uwagę, że z reguły:

    Dość charakterystyczne jest także korzystanie ze spinlocków, użycie wskaźników, operacje na pamięci i operacje arytmetyczne.

    4. Radzenie sobie z lockupami

    Lockupem nazywamy błędy, w których system po prostu nie reaguje.

    4.1 Rodzaje lockupów

    Lockupy można podzielić na:

    4.2 Lockupy z umożliwionymi przerwaniami, klawisze sysinfo, Magic sysrq key

    Mamy z nimi do czynienia, gdy utknęliśmy w nieskończonej pętli lub wisimy na jakimś locku.

    Bardzo łatwo sprawdzić, czy przerwania nie są zablokowane. Choćby świadczy o tym reakcja światełek na klawiaturze (np. na klawisz Caps Lock).

    Pierwszym mechanizmem, który możemy wtedy wykorzystać, aby zdobyć jakiekolwiek informacje o stanie systemu to klawisze sysinfo. Odpowiednie kombinacje klaswiszy powodują wypisanie informacji. Szczegóły w poniższej tabelce:
    shift + scroll lock informacje o pamięci
    ctrl + scroll lock informacje o stanie procesora
    prawy alt + scroll lock informacje o rejestrach i ślad wykonania

    Sporo pracy może zaoszczędzić funkcja Magic SysRq Key, którą można aktywować za pośrednicrtwem opcji konfiguracyjnej CONFIG_MAGIC_SYSRQ. Można ją też włączyć za pomocą odpowiedniego pliku procowego:

    echo 1 > /proc/sys/kernel/sysrq
    
    Magic SysRq Key umożliwia komunikację z jądrem za pomocą specjalnych klawiszy. Na większości klawiatur znajduje się klawisz SysRq (jest to z reguły alt + print screen). To, co się dzieje po naciśnięciu klawisza SysRq wraz z innym klawiszem obrazuje następująca tabela:
    hpomoc dla sysrq key
    ssync
    uprzemontowuje wszystkie partycje tylko do odczytu
    brebootuje system
    owyłącza komputer
    rzmienia tryb klawiatury na XLATE (wyłącza raw)
    pinformacje o rejestrach i ślad wykonania
    twypisuje procesy i informacje o nich
    kkilluje proces na bieżącej konsoli
    ewysyła wszystkim oprócz init SIGTERM
    iwysyła wszystkim oprócz init SIGKILL
    lwysyła wszystkim SIGKILL
    mwypisuje informacje o pamięci
    0-8zmienia console_loglevel
    W szczególności Magic sysRq key daje możliwość zrebootowania systemu, co jest dużo lepsze od wciśnięcia przycisku reset. Stosując natomiat dane pozyskane w p można przeprowadzić podobną analizę jak przy oopsach. Małe zmiany w EIP mogą świadczyć o pracy w nieskończonej pętli. Nie należy się jednak spodziewać, że fukcjonalność Magic SysRq key, dająca użytkownikowi konsoli dość spore możliwości, będzie dostępna w bezpieczniejszych systemach.

    4.3 Lockupy z zablokowanymi przerwaniami, NMI Watchdog i inne

    NMI Watchdog potrafi wykrywać sytuacje, w których mamy do czynienia z takim lockupem (sprawdza czy w przeciągu kilka sekund było jakieś przerwanie) i w razie stwierdzenia takiej sytuacji automatycznie generuje oopsa.

    Jedną z opcji konfiguracyjnych (począwszy od wersji rozwojowej jądra 2.5) jest opcja określana w menu konfiguracyjnym sleep-inside-spinlock-checking -- wtedy też dostaniemy automatycznego oopsa w przypadku zawieszenia podczas przetrzymywainia spin_locka.

    5. Debugger do jądra

    5.1 Brak ,,oficjalnego'' debuggera jądra

    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

    Wielu programistów jądra zawsze chciało mieć debugger działający w jądrze. Niestety Linus nie chciał i blokował i blokuje próby wprowadzenia debuggera do jądra. Twierdził, że obecność debuggera powoduje wprowadzanie błędnych poprawek przeez kiepskich programistów. Jednak wyrodukowano szereg łat uzupełniających jądro o obsługę debuggera (ale są to łaty ,,nieoficjalne'').

    5.2. Co można zdziałać zwykłym debuggerem gdb

    Można uruchomić debugger wobec jądra (tak samo, jak wobec zwykłych procesów):

    gdb vmlinux /proc/kcore
    gdzie plik vmlinux to nieskompresowany obraz jkądra. Opcjonalny argument /proc/kcore pełni funkcję pliku obrazu pamięci.

    Po uruchomieniu debuggera można korzystać z dowolnych implementowanych w nim poleceń podglądu danych, np.:
    p zmienna_globalna
    Do deasemblacji funkcji należy użyć polecenia dissassemble:
    dissassemble moja_funkcja
    Jeżeli skompilujemy jądro z opcją -g, debugger może wydobyć oczywiście znacznie więcej informacji. Możliwe jest choćby podglądanie zawartości struktur i wyłuskiwanie wskaźników. Oczywiście jednak opcja -g powoduje znaczny rozrost jądra, więc absolutnie nie należy jej stosować przy kompilacji do celów innych niż debuggowanie.

    Ale na tym wyczerpuje się lista możliwości gdb jeżeli chodzi o jądro. Nie da się za jego pomocą modyfikować danych działającego jądra. Nie można wykonać pojedyńczej instrukcji kodu, ani też ustawić punktów wstrzymania wykonania. To poważne wady.

    5.3. kgdb

    Idea jest taka, że na jednej maszynie odpalamy testowane jądro z nałożoną łatą kgdb i odpluskwiamy je za pomocą gdb z drugiej maszyny połączonej z pierwszą łączem szeregowym (tak zwanym kablem null-modem, choć ostanie wersje wspierają też komunikację przez Ethernet). Potrzebna jest druga maszyna, żeby było gdzie postawić gdb do obsługi kodu źródłowego i informacji dla debuggera pozostawionych przez gcc.

    Gdb udostępnia polecenia target, dzięki któremu można debuggować zdalnie różne programy. Aby wszystkie informacje były dostępne dla gdb, na debuggowane jądro trzeba nałożyć łatę kgdb.

    Łatę kgdb i inne informacje można znaleźć na tej stronie.

    To jak się za to wszystko wziąć? Po kolei:

    Jądro przekaże kontrolę gdb w momencie wystąpienia błędu.

    Debuggowanie jest już takie same, jak debuggowanie zwykłych aplikacji, w szczególności można używać: ctrl + C, komend continue, break, step, next, backtrace, delete, info, info threads, thread, ps. Są też instrukcje: hwebrk, hwwbrk, hwabrk, hwrbrk (odpowiednio ustawiające breakpointy związane z pamięcią: wykonania, pisania, dostępu i usuwające te breakpointy).

    Problem jest z debuggowaniem modułów. Jak powiedziałem standardowe gdb nie wystarczy. Trzeba ściągnąć specjalne ze strony kgdb. W dodatku nowe wersje kgdb nie są wyposarzone w fubkcjonalność debuggowania modułów. Wręcz jezęli chce się debuggować moduły jądra 2.6 trzeba używać rozwojowej wersji kgdb

    Jak w takim razie debuggować moduły? Załóżmy, że mamy kgdb w wersji 1.8 lub wcześniejszej i debuggujemy jądro 2.4. Potrzebujemy jeszcze narzędzia modutils i specjalnego gdb (do ściągnięcia ze strony kgdb). Moduły należy skompilować z opcją -g. Można je załadować do gdb za pomocą polecenia load-symbol-file. Istnieje skrypt loadmodule.sh, który ładuje moduł do jądra i generuje odpowiednie komendy dla gdb. Wygląda to tak dla przykładowego modułu trfs: na maszynie testowej:

    $ cd /mnt/work/build/old-pc/trfs/modules/trfs 
    [amit@askii-pc trfs]$ loadmodule.sh trfs 
     Copying /mnt/work/build/old-pc/trfs/modules/trfs/trfs to old-pc 
     Loading module /mnt/work/build/old-pc/trfs/modules/trfs/trfs 
     Warning: modutils is reading from /etc/conf.modules because 
              /etc/modules.conf does not exist.  The use of /etc/conf.modules is 
              deprecated, please rename /etc/conf.modules to /etc/modules.conf 
              as soon as possible.  Command 
              mv /etc/conf.modules /etc/modules.conf 
     Generating script /mnt/work/gdbscripts/loadtrfs 
     $ 
    
    Zostanie wygenerowany następujący skrypt gdb do odpalenia w gdb:
    add-symbol-file /mnt/work/build/old-pc/trfs/modules/trfs/trfs 
    0xc1808060 -s .text.lock 0xc180968c -s .rodata 0xc18097a0 -s 
    __ksymtab 0xc1809a44 -s .data 0xc1809bc0 
    
    Skrypt wykonujemy na maszynie, z której testujemy:
    (gdb) source /mnt/work/gdbscripts/loadtrfs 
     add symbol table from file "/mnt/work/build/old-pc/trfs/modules/trfs/trfs" at 
             .text_addr = 0xc1808060 
             .text.lock_addr = 0xc180968c 
             .rodata_addr = 0xc18097a0 
             __ksymtab_addr = 0xc1809a44 
             .data_addr = 0xc1809bc0 
    (gdb) 
    
    Odładować moduł można normalnie, poprzez rmmod, ale jeżeli chcemy go ponownie załadować musimy coś zrobić z symbolami wczytanymi przez gdb. Możemy je odświeżyć pisząc:
    (gdb) symbol-file vmlinux
    
    i dopiero potem raz jeszcze wykonać skrypt loadtrfs.

    W późniejszych wersjach kgdb obsługa modułów jest robiona bardziej automatycznie.

    5.4. Krótko o kdb

    Zmieniamy podejście do problemu. Będziemy debuggować lokalnie a nie zdalnie. Nie potrzebujemy dwóch maszyn, ale za to nasze możliwości będą bardziej ograniczone.

    kdb (Build-in Kernel Debugger) to łata na jądro, która pozawala badać pamięć i struktury system podczas gdy system chodzi. Zbiór komend umożliwia mięedzy innymi:

    Ale nie umożliwia np. debuggowania na poziomie kodu.

    Więcej informacji na tej stronie.

    6. User Mode Linux i jego użycie do debuggowania jądra

    6.1 Ogólnie o UMLu

    Najkrócej można powiedzieć, że UML to tak zmodyfikowane jądro Linuxa, że można je uruchomiś jako zwykły proces w przestrzeni użytkownika. Powstaje zamknięte środowisko, które "emuluje" Linuksa.

    Procesy uruchomione pod kontrolą emulatora nie mogą oczywiście zdawać sobie sprawy, że są "oszukiwane" tzn. z punktu widzenia procesu nie da się rozróżnić że komunikacja przez wywołania systemowe odbywa się z mniejszym systemem. Jądro UML-a nie daje pełnej funkcjonalności zwykłego jądra jeśli chodzi o dostęp do sprzętu. Jest jednak kompatybilne pod względem działania systemu, obsługi procesów, pamięci, plików, wywołań systemowych itp. Daje ten sam interfejs programistyczny. UML może (a nawet powinien) być uruchamiany na systemie plików odwzorowanym w fizycznym pliku na dysku. Zaletą jest, że UML-a może uruchomić każdy, nie są do tego potrzebne uprawnienia root-a.

    Podstawowe zastosowanie UML to oczywiście testowanie samego jądra, modułów, aplikacji. Dokonywanie zmian w prawdziwym środowisku zawsze wiąże się z niebezpieczeństwem destabilizacji pracy systemu, zawieszenia go, utraty danych itp. W przypadku UML-a nie ma tego niebezpieczeństwa. Możemy jedynie spowodować zawieszenie procesu emulatora lub zepsuć obraz systemu plików. W każdym wypadku jest to o wiele mniej bolesne, niż spowodowanie paniki jądra, restartowanie systemu, czekanie na naprawienie systemu plików itp.

    6.2 Instalacja

    UML jest dostarczany jako łatka na jądro, lub jako binarny pakiet RPM. Użytkownicy Debiana otrzymują go w dystrybucji. Ponieważ z reguły interesuje nas nie tylko samo uruchomienie UML-a ale także jego modyfikowanie i kompilowanie modułów, chcemy użyć źródeł.

    Ściągamy teraz łatkę, której użyjemy do zapatchowania źródeł jądra. Łatki pobieramy ze strony user-mode-linux.sourceforge.net/dl-sf.html. Znajdujemy tam najnowszą łatkę odpowiadającą dokładnie wersji jądra, które będziemy kompilować. Następnie patchujemy nią źródła jądra. Większa część łatki obejmuje utworzenie w drzewie źródeł dodatkowego katalogu arch/um.

    W skrócie, zakładając, że spakowane źródła jądra i łatka są bieżącym katalogu wykonujemy:

    tar xjf linux-2.4.xx.tar.bz2
    cd linux-2.4.xx
    bzcat ../uml-patch-2.4.xx.bz2 | patch -p1
    

    Kolejny etap przypomina typową kompilację jądra. Robimy to jednak tak, jakbyśmy kompilowali jądro na szczególną architekturę um, tzn. przy każdym uruchomieniu make dodajemy ARCH=um. Nie można zapomnieć o tym przełączniku.

    Najpierw konfigurujemy jądro swoim ulubionym narzędziem np.

    make menuconfig ARCH=um
    Mamy do wyboru o wiele mniej opcji, niż w standardowym jądrze. Niektóre z nich na które warto zwrócić uwagę, to:

    Uruchamiamy teraz

    make linux ARCH=um
    aby skompilować UML-owe jądro. Po jej zakończeniu otrzymamy plik wykonywalny linux. Jego rozmiar może wahać się od kilku do kilkudziesięciu MB w zależności od tego czy wybraliśmy debugowanie w konfiguracji jądra. Jest to plik, którym będziemy uruchamiać UML-a.

    Jeżeli postanowiliśmy skompilować część jądra jako moduły, kompilujemy je dalej standardowo, tzn.

     
    make modules ARCH=um
    make modules_install ARCH=um
    
    Moduły zostaną zainstalowane w standardowym katalogu /lib/modules/2.4.xx-um. Po utworzeniu systemu plików którego będziemy używać z UML-em będziemy mogli je tam skopiować. Jeżeli nie mamy prawa zapisu do katalogu /lib możemy podać inny katalog w którym mają być zainstalowane moduły pisząc:
    make modules_install INSTALL_MOD_PATH=inny_katalog ARCH=um
    

    Możemy skompilować własne moduły tak, aby dało się je załadować w UML-u (moduły skompilowane pod standardowe jądra nie zadziałają). Najważniejsze jest poinformowanie gcc, że chcemy używać plików nagłówkowych z drzewa jądra UML. W tym celu najprościej wydobyć z pliku Makefile z katalogu ze źródłami zmienną CFLAGS:

    make script='@echo $(CFLAGS)' ARCH=um > uml_flags
    
    a następnie używać tych flag przy każdym użyciu gcc do kompilowania własnego modułu pod architekturę um, na przykład:
    gcc -DMODULE `cat uml_flags` -c nasze_moduly.c
    
    Otrzymamy moduł w wersji zgodnej z wersją jądra um.

    Przy instalowaniu możemy napotkać kilka problemów. Typowe, to:
    BłądPrzyczynaRozwiązanie
    różnorodne błędy w czasie kompilacji jądrabrak flagi ARCH=um wyczyścić wszystko (make clean / mrproper) i dodać
    błąd w ostatniej fazie kompilacji, przy linkowaniu plików vmlinuz.o, main.o brak statycznej biblioteki glibcdoinstalować np. pakiet glibc-static-devel
    błąd w jednym ze skryptów Makefile shell sh ma problem z arytmetyka zmienić na bash dopisując w arch/um/Makefile zmienną SHELL=/bin/bash
    moduły kompilują się, ale nie można ich załadować: undefined symbol to_virt typowy problem UML-a z jądrem 2.4.21 upgrade jądra, lub dodanie do ksyms.c: EXPORT_SYMBOL(to_virt)

    Następną rzeczą, jaka jest nam potrzebna, gdy mamy już działające jądro UML, jest system plików, na którym będzie mogło ono wystartować (bootable root filesystem).

    Ponieważ jądro UML jest uruchamiane jako zwykły program pod kontrolą systemu macierzystego (host system), więc dotyczą go wszystkie ograniczenia nałożone na użytkownika. W szczególności jądro nie posiada swobodnego dostępu do całego dysku, jak i bezpośredniego dostępu do sprzętu. Aby stworzyć iluzję pełnej kontroli nad komputerem, UML symuluje działanie na wirtualnym komputerze, udostępniając wirtualne urządzenia. System plików jest odzwierciedlany jako fizyczny plik na dysku gospodarza, co gwarantuje (o ile tylko użytkownik ma odpowiednie prawa do tego pliku), że UML może wykonywać na nim dowolne operacje, jak na normalnym systemie plików.

    Skąd zatem zdobyć odpowiedni uruchamialny system plików?
    Najprostszym chyba wyjściem jest ściągnięcie z internetu gotowego obrazu systemu plików (filesystem image). Na stronie domowej User Mode Linux-a (http://user-mode-linux.sourceforge.net/dl-sf.html) jest udostępnionych wiele systemów plików, zawierających instalacje większości głównych dystrybucji Linuksa. Ich rozmiary wahają się od 15 do 150 MB (oczywiście w postaci skompresowanej), a po rozpakowaniu zajmują na dysku zazwyczaj kilkaset MB.

    I możemy już normalnie korzystać z UMLa. Uruchamiamy go, wywołując po prostu polecenie

    linux
    wraz z parametrami służącymi do konfiguracji wirtualnego komputera oraz jego związków z macierzystym systemem. Nie będziemy tu opisywać dokładnie wszystkich możliwych parametrów wywołania, ale tylko te najważniejsze. Pełną ich listę wraz z opisem działania można znaleźć na stronie UML-a (http://user-mode-linux.sourceforge.net/switches.html).

    6.3 Jak to działa?

    UML może działać w dwóch trybach: trybie tt (od ang. tracing thread) i w trybie skas (od. ang. seperate kernel adress space). Tryb tt jest starszym trybem, ale to on, mimo pewnych wad będzie nas interesował, bo wtedy debuggowanie jest możliwe.

    Ogolnie powstaje pytanie w jaki sposób uruchamiać procesy tak, aby działały one pod kontrolą naszego jądra UML i nie miały kontaktu ze światem zewnętrznym, tj. z jądrem hosta. W trybie tt podstawową metodą stosowaną w UML jest użycie funkcji systemowej ptrace. Przypomnijmy najpierw jej działanie (patrz man 2 ptrace). Funkcja ta dostarcza procesom możliwość kontroli pracy swoich potomków, w szczególności pozwala na przechwytywanie ich wywołań systemowych i otrzymywanie informacji o sygnałach dostarczanych do nich. W każdej chwili kiedy proces - dziecko wywołuje funkcję systemową może zostać wstrzymany a informacja o tym fakcie zostać przekazana śledzącemu procesowi - rodzicowi.

    Każdemu procesowi, który "uruchamiamy" pod UML odpowiada rzeczywisty proces uruchomiony pod jądrem hosta, jednak kontrolowany całkowicie przez UML. Aby się o tym przekonać można np. wykonać polecenie ps. W wyniku otrzymamy mnóstwo procesów o nazwie linux. Ponadto przy starcie UML tworzy specjalny proces - wątek śledzący, którego zadaniem jest wirtualizacja wywołań systemowych innych procesów. W tym celu korzysta on oczywiście z funkcji ptrace.

    Dokładniej przebiega to następująco:

    Wątki

    Tryb tt ma swoje wady:

    W celu rozwiązanie tych problemów wprowadzono tryb skas, polegający na rozdzieleniu jądra UML od procesów użytkownika UML w kwestii przestrzeni adresowej. W tym trybie UML składa się z punktu widzenia tylko z czterech procesów:

    Aby uruchomić UMLa w trybie skas należy ściągnąć odpowiednią łatę. Nie mam ambicji tutaj opisać tego trybu dokładnie, bo to tylko dygresja od tematu tej prezentacji. Po szczegóły odsyłan na stronę temu poświęconą.

    6.4 Odpluskwianie przy użyciu UMLa

    Po dłuższej dygresji, jak już wiemy, czym się je UML, to przejdźmy do rzeczy.

    Widać, że aż się prosi, aby użyć UMLa do debuggowania jądra. Nie tylko dlatego, że uruchamianie rozwojowych wersji bezpośrednio na naszym komputerze może być niebezpieczne dla naszych danych. Także dla tego, że uruchomiając jądro w UMLu możemy do jego odpluskwiania użyć zwykłego gdb. Przedstawię najpierw przykładową sesję.

    Aby odpluswiać jądro UML w gdb należy przede wszystkim pamiętać o włączeniu opcji CONFIG_DEBUGSYM i CONFIG_PT_PROXY podczas konfiguracji. Spowoduje to skompilowanie jąda z opcją -g i włączenie ptrace proxy.

    Gdy mamy już takie jądro możdemy je uruchomić pod kontrolą gdb w następujący sposób

    linux debug

    Jądro njpierw wyśle kilka komend i zatrzyna się na start_kernel, co wygląda mniej więcej tak:

    GNU gdb 4.17.0.11 with Linux support
    Copyright 1998 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux"...
    (gdb)  att 1
    Attaching to program `/home/dike/linux/2.3.26/um/linux', Pid 1
    0x1009f791 in __kill ()
    (gdb)  b start_kernel
    Breakpoint 1 at 0x100ddf83: file init/main.c, line 515.
    (gdb)  c
    Continuing.
    
    Breakpoint 1, start_kernel () at init/main.c:515
    515             printk(linux_banner);
    (gdb)
    
    Od tego momentu możemy już sterować gdb, np. tak:
    (gdb)  n
    516             setup_arch(&command_line);
    (gdb) 
    517             printk("Kernel command line: %s\n", saved_command_line);
    (gdb) 
    518             parse_options(command_line);
    (gdb) 
    519             trap_init();
    (gdb) 
    520             init_IRQ();
    (gdb) 
    521             sched_init();
    (gdb) 
    522             time_init();
    (gdb) 
    523             softirq_init();
    (gdb) 
    530             console_init();    
    (gdb)  c
    Continuing.
    
    -- jak już się znudziliśmy kazaliśmy kontynuować. Gdy skończy się bootowanie możemy sprawdzić, co się dzieje przerywając poprzez ctr+C
    Program received signal SIGINT, Interrupt.
    0x100a4bc1 in __libc_nanosleep ()
    (gdb)  bt
    #0  0x100a4bc1 in __libc_nanosleep ()
    #1  0x100a4b7d in __sleep (seconds=10) at ../sysdeps/unix/sysv/linux/sleep.c:78
    #2  0x10095fbf in do_idle () at process_kern.c:424
    #3  0x10096052 in cpu_idle () at process_kern.c:450
    #4  0x100de0a4 in start_kernel () at init/main.c:593
    #5  0x10098df2 in start_kernel_proc (unused=0x0) at um_arch.c:72
    #6  0x1009858f in signal_tramp (arg=0x10098db8) at trap_user.c:50
    (gdb) 
    
    Jak widać system był zajęty ,,nic nie robieniem''. To może teraz postwimy breakpointa w scheluler i w ten sposób złapiemy sobie najbliższą zmienę kontekstu:
    (gdb)  b schedule
    Breakpoint 2 at 0x10004acd: file sched.c, line 496.
    (gdb)  c
    Continuing.
    
    Breakpoint 2, schedule () at sched.c:496
    496             if (!current->active_mm) BUG();
    (gdb)  bt
    #0  schedule () at sched.c:496
    #1  0x10095fb3 in do_idle () at process_kern.c:421
    #2  0x10096052 in cpu_idle () at process_kern.c:450
    #3  0x100de0a4 in start_kernel () at init/main.c:593
    #4  0x10098df2 in start_kernel_proc (unused=0x0) at um_arch.c:72
    #5  0x1009858f in signal_tramp (arg=0x10098db8) at trap_user.c:50
    
    No i jest.

    Możne też deguggować moduły, też za pomocą wsparcia gdb dla debuggowania kodu dynamicznie ładowanego. Ale jest to dość skomplikowane, więc jakiś dobry człowiek napisał skrypt umlgdb do ściągnięcia ze strony umla. W tym skrypcie trzeba wstawić swój moduł do listy ścieżek. Należy go po prostu uruchomić, a on mówi, co zrobić:

    ******** GDB pid is 21903 ********
    Start UML as: ./linux  debug gdb-pid=21903
    
    
    
    GNU gdb 5.0rh-5 Red Hat Linux 7.1
    Copyright 2001 Free Software Foundation, Inc.
    GDB is free software, covered by the GNU General Public License, and you are
    welcome to change it and/or distribute copies of it under certain conditions.
    Type "show copying" to see the conditions.
    There is absolutely no warranty for GDB.  Type "show warranty" for details.
    This GDB was configured as "i386-redhat-linux"...
    (gdb) b sys_init_module
    Breakpoint 1 at 0xa0011923: file module.c, line 349.
    (gdb) att 1
    Attaching to program: /home/jdike/linux/2.4/um/./linux, process 1
    0xa00f4221 in __kill ()
    (UML gdb)  c
    Continuing.
    (UML gdb)
    
    I od tego momentu debuggujemy normalnie. Gdy zrobimy insmoda, zostanie to dostrzeżone i zobaczymy coś w tym stylu:
    *** Module hostfs loaded *** 
    Breakpoint 1, sys_init_module (name_user=0x805abb0 "hostfs", 
        mod_user=0x8070e00) at module.c:349
    349             char *name, *n_name, *name_tmp = NULL;
    (UML gdb)  finish
    Run till exit from #0  sys_init_module (name_user=0x805abb0 "hostfs", 
        mod_user=0x8070e00) at module.c:349
    0xa00e2e23 in execute_syscall (r=0xa8140284) at syscall_kern.c:411
    411             else res = EXECUTE_SYSCALL(syscall, regs);
    Value returned is $1 = 0
    (UML gdb)  
    p/x (int)module_list + module_list->size_of_struct
    
    $2 = 0xa9021054
    (UML gdb)  symbol-file ./linux
    Load new symbol table from "./linux"? (y or n) y
    Reading symbols from ./linux...
    done.
    (UML gdb)  
    add-symbol-file /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o 0xa9021054
    
    add symbol table from file "/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o" at
            .text_addr = 0xa9021054
     (y or n) y
    
    Reading symbols from /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o...
    done.
    (UML gdb)  p *module_list
    $1 = {size_of_struct = 84, next = 0xa0178720, name = 0xa9022de0 "hostfs", 
      size = 9016, uc = {usecount = {counter = 0}, pad = 0}, flags = 1, 
      nsyms = 57, ndeps = 0, syms = 0xa9023170, deps = 0x0, refs = 0x0, 
      init = 0xa90221f0 , cleanup = 0xa902222c , 
      ex_table_start = 0x0, ex_table_end = 0x0, persist_start = 0x0, 
      persist_end = 0x0, can_unload = 0, runsize = 0, kallsyms_start = 0x0, 
      kallsyms_end = 0x0, 
      archdata_start = 0x1b855 
    , archdata_end = 0xe5890000
    , kernel_data = 0xf689c35d
    } >> Finished loading symbols for hostfs ...