User Mode Linux - UML

User Mode Linux to bardzo ciekawe podejście do wirtualizacji, jak wszyscy wiemy polega na uruchamianiu "linuxa w linuxie". Takie podejście niesie wiele możliwości, a co dla nas interesujące - rozsądną i rozbudowaną możliwość debugowania. Skoro UML to zwyły proces w nadrzędnym linuxie, możemy go debugować jak zwykłe programy! (prawie)

Instalacja i konfiguracja

  1. Tryb TT (Tracing Thread) czy SKAS (Separate Kernel Address Space)?

    Zaleca się aby debugowanie przeprowadzać na UML'u działającym w trybie SKAS (niesie to większe możliwości, głównie w kwesti podłączania się z gdb do działającego UML'a). Jak wiadomo, aby to było możliwe, jądro hosta (systemu na którym odpalamy UML'a) musi na to pozwalać. Należy zatem spatchować jądro hosta odpowienim patchem skas, oraz przekompilować to jądro.

  2. Ściągamy i rozpakowujemy źródła linuxa (żadnych patchy):
    wget http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.17.13.tar.gz
    tar zxvf linux-2.6.17.13.tar.gz
    cd linux-2.6.17.13
    Wszędzie poniżej będę zakładał, że jestem w tym folderze (linux-2.6.17.13)
  3. Uruchamaimy konfigurację
    make menuconfig ARCH=um
    
  4. Zaznaczamy przede wszystkim następujące opcje:
    UML-specific options
    	-> Tracing thread support 				yes 
    		->  Separate Kernel Address Space support	yes
    	-> Host filesystem					yes
    
    Character Devices
    	-> stderr console		yes
    	-> Virtual serial line		yes
    	-> null channel support		yes
     	-> port channel support		yes
     	-> pty channel support		yes
    	-> tty channel support		yes
      	-> xterm channel support	yes
    
    Block Devices
    	-> Virtual block devices 	yes
    
    Kernel Hacking 
    	-> Show timing information on printks 			yes
    	-> Kernel debugging					yes
    		-> Compile the kernel with debug info 		yes
    	-> Compile the kernel with frame pointers 		yes
    	-> Enable ptrace proxy					yes
  5. Ściągamy root file system (slackware 8.1 - bo mały):
    wget http://heanet.dl.sourceforge.net/sourceforge/user-mode-linux/root_fs_slack8.1.bz2
    bunzip2 root_fs_slack8.1.bz2
    
  6. Poprawiamy root_fs_slack8.1 (odwoływanie do urządzeń):
    mkdir mnt
    sudo mount root_fs_slack8.1 mnt/ -o loop
    sudo vim mnt/etc/fstab
    # zamieniamy odwolanie do "/dev/ubd/0" na "/dev/ubd0", zapisujemy i wychodzimy
    sudo umount mnt
    
  7. Odpalamy UML'a pod kontrolą gdb!:
    gdb --args ./linux ubd0=root_fs_slack8.1
    gdb# run
    

Debugowanie "zwykłe"

  1. W tym momencie jądro bardzo szybko zatrzyma swoje wykonywanie. Dzieje się tak dlatego, że podczas uruchamiania obficie wysyłane są rózne sygnały. Musimu ustawić maskowanie tych sygnałów w gdb:
    gdb# handle SIGSEGV pass nostop noprint
    gdb# handle SIGUSR1 pass nostop noprint
    
    Teraz możemy już kontynuować uruchamianie gdb, ale zanim to zrobimy postawmy jakiegoś breakpointa. Gdy jądro dojdzie do kodu na którym chcemy się zatrzymać - przeskoczymy do gdb:
    gdb# break start_kernel
    gdb# continue
    
  1. Zatrzymujemy sie na breakpoincie, wykonujemy kod jądra krok po kroku, śledzimy zmienne itp. Analogicznie do debugowania zwykłych programów.

Debugowanie poprzez "podłączanie"

Dodatkowo możemy w dowolnym momencie wykonywania kodu jądra "przeskoczyć" do gdb zatrzymująć się dokładnie w na tej instrukcji, którą wykonywał nasz UML!

  1. Do działającego UML'a podłączamy się z innej konsoli poleceniem:
    kill -INT <pid_umla>
    
    gdzie <pid_umla> to najmniejszy z pid'ów procesów o nazwie zawierającej linux w naszym sytemie "hoście".

Debugowanie modułów jądra

Instalacja modułów w UML

Najpierw potrzebujemy skompilować moduły i zainstalować je na root filesystemie UML'a.

mount root_fs_slack8.1 mnt -o loop
make modules ARCH=um
make modules_install INSTALL_MOD_PATH=mnt ARCH=um

Instalacja module-init-tools

Aby działały moduły pod UML'em z jądrem 2.6 potrzebujemy paczki module-init-tools oraz glibc-2.3.6. Instalujemy je oczywiście z poziomu UML'a:

installpkg glibc-2.3.6-i486-6.tgz
installpkg module-init-tools-3.2.tar.gz

Debugowanie...

Debugowanie modułów jest troszkę bardziej skomplikowane, z poziomu gdb z odpalonym UML'em musimy postawić breaka na funkcji link_module
gdb# break kernel/module.c:1775
gdb# continue
Teraz gdy zaladujemy jakis modul (np. poleceniem modprobe) zatrzymamy się na naszym breakpoincie. Dalej wpisujemy co nastepuje:
gdb# print ((struct module *) _mod)->module_core
gdb# add-symbol-file HOST_PATH_TO_KO <to_co_zwrocil_print>
gdb# break jakas_funkcja_z_modulu
gdb# continue
Powyzsze operacje powoduja dodanie do gdb informacji o nowo zaladowanym module (o jego kodzie zrodlowym, miejscu gdzie zostal zaladowany do pamieci - czyli niezbedne rzeczy). Napis HOST_PATH_TO_KO zamieniamy na ścieżkę do pliku *.ko odpowiadającego ładowanemu modułowi (ścieżka w systemie plików hosta). Zatrzymamy się jak dojdzie do wykonywania funkcji z modułu na której postawiliśmy break'a.

Problemy

Kiedy break nie działa...

Niestety UML' nie jest doskonały i czasem ni stąd ni z owąd potrafi nie zatrzymać się na zadanym breakpointcie. Wtedy pozostaje tylko sztuczka z nieskończonym while'em. Gdzieś w kodzie deklarujemy globalna zmienna stop_here ustawioną początkowo na 1, oraz w miejscu w którym chcemy się zatrzymać wstawiamy nieskończonego while jak poniżej:
int stop_here = 1;
...
while (stop_here) sleep(1);
Gdy uruchomimy UML'a (oczywiście wszystko odbywa się z poziomu gdb) i dojdziemy do "naszego" miejsca w kodzie, system oczywiście wejdzie w pętle nieskończoną (tą którą zdefiniowaliśmy powyżej). Pozostaje teraz tylko "podłączyć" się do tego UML'a wysyłając sygnał INT w standardowy sposób (tak jak to było opisane powyżej) Następnie już z poziomu gdb, do którego przeskoczyliśmy robimy:
gdb# set stop_here = 1
I już. Jesteśmy teraz w takiej samej sytuacji jakbyśmy byli po zatrzymaniu się na breakpoincie w danym miejscu (miejscu w którym był nasz nieskończony while).

Przykładowa sesja z debugowania

Pod poniższym linkiem, znajduje się krótki screencast z przykładowej sesji z gdb + UML. Odpowiada to mniej więcej krokom opisanym na tej stronie. Miłego oglądania.

Linki