UML (USER-MODE LINUX)
Czym jest UML?

UML to narzędzie służące to wirtualizacji systemu Linux. Program zwraca abstrakcję systemu operacyjnego działającą jako oddzielny proces użytkownika. Udostępnia tym samym platformę do testowania oprogramowania oraz tworzenia serwerów sieciowych o wysokim poziomie bezpieczeństwa.

Z naszej perspektywy najbardziej pożyteczna jest możliwość modyfikacji struktur jądra Linuxa bez ryzyka uszkodzenia systemu macierzystego. W rezultacie zyskujemy drogę do "bezstresowego" odpluskwienia jądra, a co bardzo istotne, potrzebujemy do tego tylko jednej maszyny (a nie, jak w przypadku innych narzędzi (np. kgdb), dwóch systemów działających w sieci).

Zasady działania

UML jest zintegrowany wewnątrz drzewa jądra systemu (od wersji 2.6.0).

W wyniku kompilacji źródeł powstaje wykonywalny plik ELF, który tworzy na macierzystej maszynie (host) instancję wirtualnego systemu (guest). Od tej pory na maszynie działają dwa kernele.

(uwaga: z punktu widzenia nowopowstałego systemu host nie jest wirtualizowany).

Architektury SKAS i TT

TT (TRACING THREAD))

Pierwotne, każdy proces jądra UML posiadał odpowiadający mu proces na maszynie hosta. Rozpoznawianiem i obsługą wywałań systemowych z poziomu guesta zajmował się specjalny wątek śledzący. Kod kernela UML zagnieżdżony był w przestrzeni adresowej każdego powstałego za jego pośrednictwem procesu.

SKAS (SEPARATE KERNEL ADDRESS SPACE)

Architektura TT posiada pewne wady, które dotyczą bezpieczeństwa i wydajności. M. in. Każdy proces utworzony przez UML ma dostęp (z prawem pisania) do kodu jądra guesta, w wyniku czego może uzyskać dostęp do przestrzeni maszyny macierzystej - sytuacja, która przeczy samej istocie UMLa.

Idea SKAS polega na rozdzieleniu przestrzeni adresowej jądra UML oraz odpowiadających mu procesów na maszynie hosta. Choć z punktu widzenia wirtualnej maszyny pozostają one logicznie połączone, kod jądra UML jest faktycznie niedostępny dla żadnego z jego procesów.

Ujmując sprawę abstrakcyjnie, UML z nałożoną łatą SKAS upraszcza widok jądra guesta z poziomu hosta. UML jest lżejszy i pozwala się prościej debuggować. Instalacja SKAS wymaga nałożenia określonej łaty na jądro hosta i jego rekompilację.

W poniższym tutorialu będziemy się zajmować przede wszystkim architektrą TT.

Instalacja UML

Aby zbudować jądro działające w trybie user-mode potrzebujemy dwóch rzeczy:

  • wybraną wersję źródeł jądra systemu Linux (najlepiej wersja surowa, pozbawiona dodatkowych patchy np. vanilla-sources)
  • odpowiadający temu jądru patch UML (tylko dla wersji kernela < 2.6.x)

Kolejne kroki instalacji:

1. Rozpakowujemy plik .tar ze źródłem jądra do odpowiedniego katalogu:

host% mkdir ~/uml
host% cd ~/uml
host% tar -xjvf linux-2.6.0-prerelease.tar.bz2

[ < 2.6.x ] 2. Aplikujemy łatę UML do źródeł kernela:

host% cd ~/uml/linux
host% bzcat uml-patch-2.6.0-prerelease.bz2 | patch - p1

3. Konfigurujemy jądro poleceniem:

make menuconfig ARCH=um

ustawienia kluczowe:

  • należy uaktywnić CONFIG_DEBUGSYM oraz CONFIG_PT_PROXY, aby umożliwić debuggowanie jądra
  • kernel powinien być skonfigurowany tak, by nie montował automatycznie urządzeń w /dev oraz dostarczał obsługę wirtulanych systemów plików (tmpfs - Virtual Memory Filesystem)

ustawienia opcjonalne:

  • lista symboli w zakładce Kernel Hacking pozwala ustalić zakres, w jakim jądro będzie poddawało się debuggowaniu, np. możliwość debuggowania wewnątrz spinlocka itp.
  • wyznaczenie potrzebnych Character Devices

4. Kompilujemy jądro:

make linux ARCH=um

W rezultacie otrzymamy plik wykonywalny o prostej nazwie linux, który dla wygody możemy skopiować do /usr/local/bin (konieczna może okazać się edycja pliku /etc/env.d/00basic).

5. Tworzymy instancję systemu operacyjnego na potrzeby UMLa:

mkdir /mnt/tempsys
cd /mnt/tempsys
tar xvjpf ścieżka_do_stage'a.tar.bz2
chroot /mnt/tempsys

Konfigurujemy system zgodnie z zasadami dla danej dystrybucji, lecz przede wszystkim należy dokonać edycji pliku /etc/fstab:

  • dev/ROOT zmieniamy na dev/ubda
  • dev/SWAP na dev/ubdb
  • dev/BOOT otaczamy komentarzem

Opuszczamy chroota, odmontowujemy niepotrzebne urządzenia i pakujemy pliki instancji systemu do jednego archiwum:

tar cvjpf katalog_roboczy/system.tbz2 *

6. Tworzymy system plików jądra UML - root_fs oraz swap_fs (po 500 MB):

dd if=/dev/zero of=root_fs seek=500 count=1 bs=1M
mke2fs -F root_fs
mount -o loop root_fs /mnt/loop
tar xvjpf system.tbz2 -C /mnt/loop
umount /mnt/loop

dd if=/dev/zero of=swap_fs seek=500 count=1 bs=1M
mkswap -f swap_fs

7. Odpalamy UMLa, podczepiając utworzone przed chwilą systemu plików:

linux ubd0=root_fs ubd1=swap_fs

Instalacja modułów w UML

Wszelkie źródła modułów muszą zostać skompilowane z poziomu jądra UML (moduły zbudowane na hoscie nie zadziałają w guescie, jeśli wersje kerneli są różne). Jeśli zdecydujemy się skopiować źródła jądra UML na system plików root_fs, instalację modułów przeprowadzamy w sposób klasyczny z poziomu guesta. Możemy zrobić to samo z poziomu hosta (maszyny szybszej):

1. Neleży wejść do katalogu ze źródłami jądra UML i wykonać:

(host) mount -o loop root_fs /mnt/loop
(host) make modules ARCH=um
(host) make modules_install INSTALL_MOD_PATH=/mnt/loop/ ARCH=um

Należy pamiętać o zainstalowaniu pakietów module-init-tools oraz glibc 2.3 dla kernela w wersji 2.6.

2. Przy własnych modułach - kompilujemy kod modulu modul.c do bliku binarnego modul.o. Potem:

(host) make SUBDIRS="sćieżka/do/modul.o" modules ARCH=um

Plik modules.ko musi zostać zainstalowany w /mnt/loop/lib/modules/2.xx.xx/kernel/...

3. Bootujemy UMLa i wywołujemy wiersz:

(uml) modprobe modul

Przy odrobinie szczęścia moduł dodał się do kernela. Sprawdźmy to:

(uml) lsmod

Debuggowanie kernela przy pomocy UML: wstęp

Informacje Pomocnicze

ptrace() to wywołanie systemowe pozwalające jednemu procesowi monitorować działanie drugiego procesu. Proces monitorowany, w wypadku otrzymania sygnału w trakcie wykonywania kodu, informuje o tym proces monitorujący i przechodzi w stan wstrzymania. Proces śledzony może także zatrzymać się w wyniku wystąpienia zdarzenia określonego przez proces-monitor. Każde wywołanie funkcji blokuje dostęp do jądra na czas swojego działania. ptrace() jest bardzo często używany do zadań związanych z debuggowaniem.

#include <sys/ptrace.h>
long int ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data)


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


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.


Jądro Sprawy, czyli debuggowanie kernela

Ponieważ jądro UML jest w gruncie rzeczy zwykłym procesem, pozwala się ono debuggować pod kontrolą gdb jak każdy inny proces utworzony przez użytkownika w danym systemie.

Proces debuggowania rozpoczynamy od uruchomienia gdb:

gdb

Następnie każemy debuggerowi ignorować wewnętrzne sygnały UMLa - SIGSEGV i SIGUSR1 - w przeciwynm wypadku będą one cyklicznie przerywać proces odpluskwiania:

(gdb) handle SIGSEGV pass nostop noprint
(gdb) handle SIGUSR1 pass nostop noprint

ładujemy tablicę symboli jądra UML:

(gdb) file linux

Ustalamy breakpointy, np:

(gdb) b start_kernel()

W końcu, uruchamiamy instancję wirtualnego linuxa z podczepionymi systemami plików:

(gdb) run ubd0=root_fs ubd1=swap_fs
(UML gdb) ...


* Możemy też zaaplikować do jądra instancję debuggera o określonym pid:

./linux debug gdb-pid=pid_debuggera


** Aby uruchomić debuggowanie jądra pod ddd:

host% ddd ./linux
./linux debug=parent gdb-pid=pid_utworzonego_wyżej_procesu
(gdb) attach 1

(podanie jako parametru attach pid, który nie odnosi się do procesu UML, podłącza gdb do wątku bieżącego)


*** Aby podłączyć gdb do już działającego jądra UML

Chodzi nam o to by wykonać instrukcję attach na aktywnym procesie jądra. Robimy to za pośrednictwem sztuczki polegającej na wysłaniu procesowi guesta sygnału SIGUSR1 (lub SIGINT):

host% kill -USR1 pid_UMLa

W wyniku otrzymamy okno xterm z gdb działającym na kernelu.

Debuggowanie kernela przy pomocy UML: przykłady użycia

Analiza śpiącego procesu

Jednym z typowych problemów, z którymi spotkać się można podczas debuggowania struktur jądra to zastój jednego lub więcej procesów korzystających z jego usług. Proces taki może ulec zakleszczeniu lub zagłodzeniu w efekcie użycia interfejsu IPC (semafory, kolejki komunikatów) lub niewykrytego błędu w kodzie kernela.

Jeśli już dowiemy się o jaki proces chodzi (np. przy użyciu ps) oraz zdobędziemy jego pid, możemy wykorzystać gdb do prześledzenia wywołań kolejnych usług jądra dokonanych przez proces przed niechcianym zawieszeniem.

Opuszczamy bieżący wątek i podłączamy gdb do naszego procesu:

(UML gdb) detach
(UML gdb) attach pid

Pobieramy stos wywołań funkcji, których wykonanie doprowadziło proces do zawieszenia:

(UML gdb) backtrace

Po analizie uzyskanych informacji możemy powrócić do aktualnego wątku i wznowić jego działanie:

(UML gdb) attach 1
(UML gdb) continue


Odpluskwainie modułów jądra

Debuggowanie modułów jest zadaniem o tyle trudnym, że wymaga od debuggera wsparcia dla analizy kodu ładowanego w sposób dynamiczny do przestrzeni adresowej kernela.

1. Uruchamiamy gdb. Ustawiamy obsługę sygnałów. Następnie pobieramy tablicę symboli UMLa i ustawiamy odpowiedni breakpoint:

(gdb) file linux
(gdb) b sys_init_module (albo link_module w zależności od potrzeby)
(gdb) r ubd0=root_fs ubd1=swap_fs

Bootujemy UML.

2. Teraz podczepiamy interesujący nas moduł pod jądro:

(UML gdb) modprobe nazwa_modułu

3. Gdb łapie breaka na procedurze ładującej moduły do jądra. Pobieramy wartość symbolu reprezentującego w przestrzeni kernela adres ostatnio dodanego modułu:

print ((struct module *) mod)->module_core

4. Kolejnym krokiem jest wgranie do debuggera tablicy symboli opisującej rozważany moduł i jego kod. Potrzebujemy do tego pobranego powyżej adresu obiektu w pamięci oraz jego lokalizacji na platformie hosta (plik .ko):

(UML gdb)add-symbol-file ścieżka/do/modułu/na/hoscie adres_modułu

np. u nas

(UML gdb)add-symbol-file
/home/ap219551/linux/2.6/um/arch/um/fs/ hostfs/ hostfs.ko 0xa9021054

Od tego momentu jesteśmy w stanie debuggować moduł pod gdb (ustawiać breakpointy itp.).

4. Jeśli zajdzie potrzeba wgrania nowej wersji debuggowanego modułu do kernela, wszystkie wyliczone powyżej czyności powinny być (niestety) powtórzone. Należy jednak najpierw wyczyścić zbiór symboli zapamiętany dotychczas przez gdb i załadować "na świeżo" symbole z bibliotek kernela:

(UML gdb) symbol-file
(UML gdb) symbol-file ścieżka/do/kernela

(uwaga: Przesunięcie adresu startu modułu w przestrzeni adresowej jądra nie jest wykluczone. Należy zatem od czasu do czasu zaglądać do struktury mod i w razie potrzeby aktualizować breakpointy. info address symbol)


Odpluskwianie modułów jądra (przy pomocy umlgdb)

uwaga: skrypt działa poprawnie tylko dla wersji kernela 2.4

Skrypt umlgdb to program automatyzujący w znacznym zakresie czynności przedstawione w punkcie wyżej. Wystarczy, by czytelnik, który przeanalizował i zrozumiał mechanizmy umożliwiające debuggowanie modułów kernela, zdał sobie sprawę z tego, że umlgdb to przede wszystkim skrypt zaoszczędzający nam zbędnego pisania.

1. Najpierw modyfikujemy skrypt umlgdb, zaznaczając jakimi modułami jesteśmy zainteresowani. Służy do tego zestaw MODULE_PATHS:

set MODULE_PATHS {
 nazwa_modułu_1 ścieżka_do_modułu_1_na_hoscie
 nazwa_modułu_2 ścieżka_do_modułu_2_na_hoscie
 ...
}

np.

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. Odpalamy skrypt z poziomu hosta. Teraz każda próba załadowania modułu do naszego kernela (poprzez insmod lub modprobe) spowoduje automatyczne ustawienie debuggera na tym module. Umlgdb ustawia flagę na fukcji jądra sys_init_module, dzięki czemu wykrywa każdą akcję wgrania nowego obiektu do listy modułów, a następnie wykonuje kroki przedstawione szczegółowo w punkcie poprzednim.

Debuggowanie kernela przy pomocy UML: uwagi końcowe

Do debuggowania jądra pod UML nie musimy używać wyłącznie gdb. Np. poniższa porcja kodu pozwoli na debuggowanie pod nadzorem strace'a:

host% sh -c 'echo pid=$$; echo -n hit return; read x; exec strace -p 1 - o strace.out'

Wydrukowany zostaje pid nowego procesu strace. Przechodzimy do katalogu UMLa:

./linux debug gdb-pid=otrzymany_pid

jądro UML zostaje uruchomione pod kontrolą tego debuggera, a wyjście zostaje przekierowane na plik strace.out.

Linki i zasoby