Debuggowanie jądra linuxa

Kornel Jakubczyk, Marcin Koziński, Adam Radziwończyk-Syta

Debugowanie jądra

Jasne jest, że debugowanie jądra systemu znacząco się różni od debugowanie zwykłego programu. Jądro nie jest uruchamiane pod kontrolą żadnego innego procesu, więc jego przerywanie, sprawdzanie wartości zmiennych czy wykonywanie instrukcja po instrukcji nie jest możliwe. Ponadto w przypadku błędów w jądrze nie możemy postąpić w zwykły sposób, nie możemy nawet zamknąć systemu, bo jądro może już nie reagować. Powtarzenie przeplotów prowadzących do błędów w przypadku jądra, zależnych od tak wielu czynników jest bardzo ciężkie, dlatego także należy starać się uzyskać maksimum informacji z każdego błędu napotkanego podczas testowanie jądra. W naszej prezentacji przedstawiamy różne techniki i narzędzie wspierające debugowanie jądra.

Istnieje wiele różnych narzędzi i technik umożliwiających odpluskwianie jądra. Omówimy następujące zagadnienia:

Przyjrzymy się najpierw rodzajom błędów, zwłaszcza, że nasze podstępowanie będzie różniło się w zależności od typu błędu. Podstawowe rodzaje błędów jądra to:

Narzędzia podstawowe

Opcje jądra

Część Kernel Hacking podczas konfigurowania jądra umożliwia wybranie opcji umożliwiających i wspomagających debugowanie jądra. Należy te opcje wybierać jednak tylko wtedy, gdy kompiluje się jądro przeznaczone do testowania, gdyż większość z wymienionych tu opcji spowolnia pracą jądra.

Uwaga: Opisywane poniżej opcje pochodzą z jądra 2.6.17.13, w nowszych jądrach opcji jest jeszcze więcej i część z poniższych nazw została zmieniona. Dla porównania screen z najnowszego jądra

Show timing information on printk (CONFIG_PRINTK_TIME) dodaje informacje dotyczące czasu wypisania do printk (umożliwia analizę czasu działania i znajdowanie kodu, którego wykonanie zajmuje dużo czasu)
Magic SysRq key (CONFIG_MAGIC_SYSRQ) włącza obsługę klawiszy SysRq (p. niżej)
Kernel debugging (CONFIG_DEBUG_KERNEL) umożliwia wybranie poniższych opcji
Kernel log buffer size (16 => 64 Kb, 17 => 128KB) (CONFIG_LOG_BUF_SHIFT) Rozmiar bufora komunikatów jądra
Detect Soft Lockups (CONFIG_DETECT_SOFTLOCKUP) umożliwia wykrywanie "soft lockups", jeżeli następuje praca w trybie jądra dłużej niż 10 sekund, i nie będzie możliwości oddania procesora innym procesom, to zostanie wypisany stos wywołań, ale system pozostanie w stanie lockup
Collect scheduler statistics (CONFIG_SCHEDSTATS) do schedulera dołączany jest kod zbierający statystyki dotyczące jego działania, dostępne są one w /proc/schedstat
Debug slab memory allocations (CONFIG_DEBUG_SLAB) Włącza sprawdzanie podczas alokacji pamięci - następuje zatruwanie (poisoning) każdy bajt jest ustawiany na 0xa5 przed przekazaniem go procesowi, a przy zwalnianiu ustawiany jest na 0x6b, ułatwie to np. analizę błędów oops
Mutex debugging, deadlock detection (CONFIG_DEBUG_MUTEXES) Wykrywanie błędów związanych z zakleszczeniami wywołanymi przez mutexy i operacjami na mutexach
Spinlock debugging (CONFIG_DEBUG_SPINLOCK) Umożliwia znajdowanie błędów niezainicjalizowanie spinlocków i innych powszechnych błędów popełnianych podczas użycia spinlocków
Sleep-inside-spinlock checking (CONFIG_DEBUG_SPINLOCK_SLEEP) Raportowane są wywołanie funkcji, które mogą powoduję zaśnięcie, podczas posiadania spinlocka
Compile the kernel with debug info (CONFIG_DEBUG_INFO) jądro będzie zawierało informacje potrzebne do debugowania. Zaznaczenie tej opcji jest konieczne jeżeli będzie się korzystać z crash, kgdb, LKCD, gdb itp.
Debug filesystem (CONFIG_DEBUG_FS) Udostępnia debugfs, wirtualny system plików, stosowany do debugowania
Debug VM (CONFIG_DEBUG_VM) Włącza debugowanie obsługi pamięci wirtualnej
Compile the kernel with frame pointers (CONFIG_FRAME_POINTER) Dodaje pożyteczne informacje podczas debugowania z użyciem zewnętrznych debuggerów
Compile the kernel with frame unwind information (CONFIG_UNWIND_INFO) Dodaje pożyteczne informacje podczas debugowanie z użyciem zewnętrznych debuggerów
Check for stack overflows (CONFIG_DEBUG_STACKOVERFLOW) Powoduje wypisywanie ostrzeżeń, gdy zostaje mało miejsca na stosie, umożliwia znajdowanie błędów przepełnienia stosu
Write protect kernel read-only data structures (CONFIG_DEBUG_RODATA) Zaznacza dane kernel tylko do odczytywania jako tylko do odczytywania w tablicach stron - umożliwia wykrywanie przypadkowych zapisań do tych części danych

Mamy jeszcze jedną przydatną opcję pomocną podczas debugowania modułów:

Forced module unloading (CONFIG_MODULE_FORCE_UNLOAD) umożliwia zmuszanie jądra do usuwania modułów

printk

Funkcja printk jest odpowiednikiem printf i umożliwia wypisywanie komunikatów. Można z niej korzystać w trybie jądra i ustalać priorytety komunikatów, który sterują ich zachowaniem. Należy zwrócić uwagę, że nie ma przecinka między priorytetem komunikatu a nim samym (tzn. priorytet jest dołączany na początku komunikatu)

Funckja printk różni się od printf w paru szczegółach - printk nie obsługuje liczb zmiennoprzecinkowych

KERN_EMERG 0 /* system is unusable */
KERN_ALERT 1 /* action must be taken immediately */
KERN_CRIT 2 /* critical conditions */
KERN_ERR 3 /* error conditions */
KERN_WARNING 4 /* warning conditions */
KERN_NOTICE 5 /* normal but significant condition */
KERN_INFO 6 /* informational */
KERN_DEBUG 7 /* debug-level messages */

W przypadku nie podania priorytetu komunikatu wartość zostanie ustawiona na default_message_level (p. poniżej)

Zachowaniem printk steruje plik /proc/sys/kernel/printk. Przyjrzyjmy się jego zawartości:

[ar237576@green06 ~]$ cat /proc/sys/kernel/printk
1       4       1       7

Cztery wartości w pliku printk to: console_loglevel, default_message_loglevel, minimum_console_level i 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ą (tj. najwyższą) wartością, którą można ustawić jako console_loglevel. default_console_loglevel jest domyślną wartością dla console_loglevel.

Uruchamiając klogd z opcją -c można ustawić wartość console_loglevel

Aby zapobiec przeciążeniu systemu przez zalew komunikatów można użyć funkcji printk_ratelimit. Kontroluje ona liczbę nadchodzących komunikatów i zwraca 0 w sytuacji, gdy jest ich zbyt wiele. Umieszcza się ją przed printk:

if (printk_ratelimit())
  printk(KERN_DEBUG "Niezbyt interesujący debug\n");

Jej zachowanie kontroluje się poprzez następujące pliki:
/proc/sys/kernel/printk_ratelimit - minimalny średni odstęp między komunikatami
/proc/sys/kernel/printk_ratelimit_burst - najdłuższa dopuszczalna seria komunikatów

Zobaczmy prosty moduł obrazujący działanie printk:

#include <linux/module.h>

MODULE_LICENSE("GPL");

int init_module(void) 
{

    printk(KERN_DEBUG "DEBUG\n");
    printk(KERN_INFO "INFO\n");
    printk(KERN_NOTICE "NOTICE\n");
    printk(KERN_WARNING "WARNING\n");
    printk(KERN_ERR "ERROR\n");
    printk(KERN_ALERT "ALERT\n");
    printk(KERN_EMERG "EMER\n");

    return 0;
}

void cleanup_module(void){}

Zobaczmy jak printk zareaguje na zmianę wartości w pliku /proc/sys/kernel/printk

daroon:~/SO/# insmod printk.ko
DEBUG
INFO
NOTICE
WARNING
ERROR
ALERT
EMER
daroon:~/SO# rmmod printk.ko
daroon:~/SO# echo "5 5 1 7" >/proc/sys/kernel/printk 
daroon:~/SO# insmod printk.ko
WARNING
ERROR
ALERT
EMER
Oczywiście wszystkie wiadomości zostały w zapisane w logach:
daroon:~/SO# cat /var/log/kern.log | tail -n 14
Nov 22 20:14:47 daroon kernel: DEBUG
Nov 22 20:14:47 daroon kernel: INFO
Nov 22 20:14:47 daroon kernel: NOTICE
Nov 22 20:14:47 daroon kernel: WARNING
Nov 22 20:14:47 daroon kernel: ERROR
Nov 22 20:14:47 daroon kernel: ALERT
Nov 22 20:14:47 daroon kernel: EMER
Nov 22 20:14:58 daroon kernel: DEBUG
Nov 22 20:14:58 daroon kernel: INFO
Nov 22 20:14:58 daroon kernel: NOTICE
Nov 22 20:14:58 daroon kernel: WARNING
Nov 22 20:14:58 daroon kernel: ERROR
Nov 22 20:14:58 daroon kernel: ALERT
Nov 22 20:14:58 daroon kernel: EMER

Sprawdźmy jeszcze, jak działa printk_ratelimit

#include <linux/module.h>

MODULE_LICENSE("GPL");

int init_module(void)
{
    int i=0;
    while (printk_ratelimit()){
        printk(KERN_DEBUG "%d\n",i);
        i++;
    }
    return 0;
}

void cleanup_module(void){}
i obejrzyjmy logi:
daroon:~/SO# cat /proc/sys/kernel/printk_ratelimit
5
daroon:~/SO# cat /proc/sys/kernel/printk_ratelimit_burst
10
daroon:~/SO# insmod printk2.ko
daroon:~/SO# cat /var/log/kern.log | tail -n 10
Nov 22 20:29:30 daroon kernel: 0
Nov 22 20:29:30 daroon kernel: 1
Nov 22 20:29:30 daroon kernel: 2
Nov 22 20:29:30 daroon kernel: 3
Nov 22 20:29:30 daroon kernel: 4
Nov 22 20:29:30 daroon kernel: 5
Nov 22 20:29:30 daroon kernel: 6
Nov 22 20:29:30 daroon kernel: 7
Nov 22 20:29:30 daroon kernel: 8
Nov 22 20:29:30 daroon kernel: 9

Demony klogd i syslogd

Printk zapisuje komunikaty do cyklicznego bufora, który ma długość LOG_BUF_LEN. Następnie budzone są procesy oczekujące na komunikaty - czytające z /proc/kmsg lub oczekujące na wywołaniu syslog. Obydwa sposoby dostępu są podobne - różnicą jest to, że odczytanie z /proc/kmsg powoduje pobranie danych z bufora, a syslog może zwrócić dane umożliwiając innym procesom odczytanie tego samego z bufora (uruchomienie klogd z opcją -s powoduje korzystanie z syslog, w przeciwnym wypadku klogd korzysta z /proc/kmsg). Gdy bufor zostanie zapełniony, zostaje wypełniany od początku - a stare komunikaty zostają nadpisane.

Jeśli klogd jest uruchomiony odzyskuje komunikaty z bufora i przekazuje je do syslogd, który je przetwarza zgodnie z ustawieniami w /etc/syslog.conf. Jeśli klogd nie jest uruchomiony - dane pozostają w buforze, dopóki ktoś inny ich nie odczyta lub nie zostaną nadpisane po zapętleniu bufora. Możemy rzecz jasna samemu odczytywać komunikaty z bufora korzystając z dmesg albo po prostu wykonując cat /proc/kmsg. Należy wówczas zamknąć demona klogd (lub go nie uruchamiać), by nie konkurował z naszym procesem o wiadomości.

logd można uruchomić z opcją -f plik i wówczas zapisywać będzie komunikaty do pliku. Jeśli klogd i syslogd są uruchomione to komunikaty wypisywane przez printk znajdą się w pliku wyspecyfikowanym w syslog.config (np. /var/log/kern.log ). Zobaczmy fragment przykładowego syslog.conf

*.=crit;kern.none            /var/adm/critical

kern.*                       /var/adm/kernel
kern.crit                    /dev/console
kern.info;kern.!err          /var/adm/kernel-info

Z tego co powiedzieliśmy wynika już, że duża ilość wywołań printk znacznie spowalnia pracę systemu - syslogd po każdym wypisaniu komunikatu wykonuje zapisanie go na dysk. Musi tak być, bo w przypadku błędu logi były kompletne. By zmienić to zachowanie należy wpis w syslog.conf poprzedzić znakiem -.

Oto schemat ilustrujący standardowy przepływ komunikatów:

Magic SysRq

W przypadku "soft lockup", tzn. zawieszeniu pracy działania przy reagowaniu na klawiaturę, jądra udostępnia mechanizm pozwalający na kontrolę pracy systemu. Po włączeniu odpowiednej opcji podczas kompilacji jądra kombinacja klawiszy Alt, SysRq i litera spowoduje wykonanie jednej z akcji przez system. (Uwaga: poniższe układy działają dla układu klawiatury qwerty)

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 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) 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 and svgalib, 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

Należy zauważyć, że wybranie samej opcji b - restart bez odmontowania i synchronizacji jest niebezpieczne. Zalecanym sposobem postępowania po utracie kontroli nad systemem jest:

Regulować tym czy Magic SysRq jest włączone może poprzez plik /proc/sys/kernel/sysrq, w którym znajduje się 1, gdy jest one aktywne, a 0 w przeciwnym przypadku. Tak więc:

echo 0 > /proc/sys/kernel/sysrq
wyłącza obsługę sysrq,
echo 1 > /proc/sys/kernel/sysrq

włącza obsługę sysrq.

Możliwe jest także skorzystanie z Magic SysRq poprzez trigger (/proc/sysrq-trigger)

echo t > /proc/sysrq-trigger 

Zobaczmy co wypisze SysRq, gdy poprosimy o informacje o pamięci. Pamiętajmy, że przy wypisywanie korzysta z printk, tak więc zobaczymy jedynie te komunikaty, które mają odpowiedni priorytet


SysRq : Show Memory
Mem-info:
DMA per-cpu:
CPU    0: Hot: hi:    0, btch:   1 usd:   0   Cold: hi:    0, btch:   1 usd:   0
Normal per-cpu:
CPU    0: Hot: hi:  186, btch:  31 usd: 159   Cold: hi:   62, btch:  15 usd:  11
Active:77913 inactive:40999 dirty:8 writeback:0 unstable:0
 free:1910 slab:6032 mapped:13772 pagetables:642 bounce:0
DMA free:2252kB min:88kB low:108kB high:132kB active:8312kB inactive:2404kB present:16256kB pages_scanned:0
all_unreclaimable? no
lowmem_reserve[]: 0 492 492
Normal free:5388kB min:2792kB low:3488kB high:4188kB active:303340kB inactive:161592kB present:503876kB pages_scanned:0
all_unreclaimable? no
lowmem_reserve[]: 0 0 0
DMA: 11*4kB 8*8kB 6*16kB 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 1*2048kB 0*4096kB = 2252kB
Normal: 1*4kB 39*8kB 21*16kB 22*32kB 7*64kB 2*128kB 3*256kB 1*512kB 0*1024kB 1*2048kB 0*4096kB = 5388kB
Swap cache: add 0, delete 0, find 0/0, race 0+0
Free swap  = 2096440kB
Total swap = 2096440kB
Free swap:       2096440kB
131056 pages of RAM
0 pages of HIGHMEM
1886 reserved pages
74114 pages shared
0 pages swap cached
8 pages dirty
0 pages writeback
13772 pages mapped
6032 pages slab
642 pages pagetables

strace i ltrace

W linuksie dostępne są strace, ptrace i ltrace wspomagające debugowanie programów. Umożliwiają one śledzenie wywołań funkcji systemowych i funkcji bibliotecznych - dzięki temu mamy dokładniejszy wgląd w to jak programy komunikują się z jądrem.

strace umożliwia śledzenie wywołań funkcji systemowych i sygnałów otrzymywanych przez program. Niektóre z opcji, które można podać to:

-o nazwa_pliku przekierowanie wyjścia do pliku (w przeciwnym wypadku informacje zostają wypisywane na standardowy strumień diagnostyczny)
-f śledzenie także procesów potomnych utworzonych poprzez fork (śledzenie jest rozpoczynane dopiero, gdy znane jest pid dziecka)
-ff gdy włączona jest opcja -o zapisuje raport z każdego procesów do oddzielniego pliku (nazwa.pid)
-F próbuje także śledzić vforka
-i wypisuje wskaźnik instrukcji
-tt wypisanie na początku czasu wywołania (z dokładnością do mikrosekund)
-T wypisanie na końcu czasu wykorzystanego przez wywołanie
-p pid przyłączenie się do działającego procesu (może zostać przerwane przez CTRL-C, wtedy strace odłączy się, a proces będzie kontynuować działanie)
-e trace=set śledzenie tylko zbioru wywołań np. trace=open,close,read,write
-e trace=file śledzenie wywołań, które jako argument przyjmują plik
-e trace=process śledzenie wywołań, które dotyczą zarządzania procesami (fork, wait, exec)

Można podać także inne nazwy grup: network, signal, ipc, desc..

ltrace jest bardzo podobny do strace, ale umożliwia śledzenie wywołań funkcji bibliotecznych

Zaletą obydwu programów jest fakt, że nie musimy posiadać źródeł programów, by móc śledzić wykonywane przez nie wywołania. Obydwa programy zazwyczaj generują dużą liczbę linii. Dlatego też generowane przez nie wyjścia nie powinny być czytane, a jedynie przeszukiwane.

W przypadku strace format w jakim wypisywane są wywołania funkcji systemowych to:

  access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)

Sygnały otrzymywane przez program są wypisywane w następującej postaci (sleep został przerwany przez SIGINT):

adam@daroon:~/SO$ strace -o plik sleep 100
adam@daroon:~/SO$ cat plik | tail -n 2
--- SIGINT (Interrupt) @ 0 (0) ---
+++ killed by SIGINT +++

[ar237576@students ~]$ strace -o plik echo "Witaj strace"
Witaj strace
[ar237576@students ~]$ cat plik
execve("/bin/echo", ["echo", "Witaj strace"], [/* 49 vars */]) = 0
brk(0)                                  = 0x804d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=216482, ...}) = 0
mmap2(NULL, 216482, PROT_READ, MAP_PRIVATE, 3, 0) = 0xf7f05000
close(3)                                = 0
open("/lib/libc.so.6", O_RDONLY)        = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\2e\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1266080, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7f04000
mmap2(NULL, 1275472, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xf7dcc000
mmap2(0xf7efe000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x131) = 0xf7efe000
mmap2(0xf7f01000, 9808, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf7f01000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7dcb000
set_thread_area({entry_number:-1 -> 12, base_addr:0xf7dcb6c0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0,
limit_in_pages:1, seg_not_present:0, useable:1}) = 0
open("/dev/urandom", O_RDONLY)          = 3
read(3, "\215D\23\32", 4)               = 4
close(3)                                = 0
mprotect(0xf7efe000, 4096, PROT_READ)   = 0
mprotect(0xf7f54000, 4096, PROT_READ)   = 0
munmap(0xf7f05000, 216482)              = 0
brk(0)                                  = 0x804d000
brk(0x806e000)                          = 0x806e000
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 30), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0x1000) = 0xf7f39000
write(1, "Witaj strace\n", 13)          = 13
close(1)                                = 0
munmap(0xf7f39000, 4096)                = 0
close(2)                                = 0
exit_group(0)                           = ?

oops

W jądrze dostępne są makra umożliwiające zgłoszenie błędu i sprawdzenie warunku (i w przypadku jego fałszywości zgłoszenie błędu). Są to odpowiednio BUG() i BUG_ON(warunek).

#define BUG() do { \
         printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __FUNCTION__); \
         panic("BUG!"); \
} while (0)

#define BUG_ON(condition) do { if (unlikely((condition)!=0)) BUG(); } while(0)

Zanim opowiemy o oopsie, przyjrzyjmy się najpierw ustawieniom regulującym jak zachowa się kernel po błędzie:

/proc/sys/kernel/panic

Plik panic umożliwia dostęp (odczyt i zapis) do zmiennej jądra panic_timeout. Jeśli jest to zero, jądro będzie się zapętlać podczas paniki; jeśli wartość niezerowa, to określa liczbę sekund, po której jądro powinno się automatycznie przeładować.

/proc/sys/kernel/panic_on_oops

Plik ten kontroluje zachowanie jądra, kiedy wystąpi oops. Jeśli ten plik zawiera 0, to system próbuje kontynuować operację. Jeśli zawiera 1, to system czeka parę sekund (aby dać demonowi klogd czas na zapisanie wyjęcia z oops), a następnie panikuje. Jeżeli wartość w pliku /proc/sys/kernel/panic również jest niezerowa, to nastąpi restart komputera.

Oto przykładowy oops i moduł, który go wygenerował

#include 

MODULE_LICENSE("GPL");

int init_module(void) 
{
    int* i=0;
    *i=5;

    return 0;
}

void cleanup_module(void){}
BUG: unable to handle kernel NULL pointer dereference at virtual address 00000000
 printing eip:
e0856002
*pdpt = 00000000165a0001
*pde = 0000000000000000
Oops: 0002 [#1]
Modules linked in: oops_mod ipv6 n_hdlc ppp_synctty ppp_generic slhc ppdev lp ac battery nls_iso8859_1 ntfs fuse dm_snapshot
dm_mirror dm_mod loop snd_mpu401 snd_mpu401_uart snd_seq_dummy snd_seq_oss snd_seq_midi snd_seq_midi_event snd_seq tsdev
snd_intel8x0 snd_ac97_codec ac97_bus snd_pcm_oss snd_mixer_oss snd_rawmidi snd_pcm snd_seq_device parport_pc parport snd_timer
analog gameport serio_raw snd i2c_sis630 button i2c_sis96x psmouse sis_agp firmware_class pcspkr evdev usbatm shpchp soundcore
snd_page_alloc i2c_core rtc agpgart pci_hotplug ext3 jbd ide_cd cdrom ide_disk ohci_hcd sis5513 generic floppy sis900 mii ide_core
usbcore thermal processor fan
CPU:    0
EIP:    0060:[]    Not tainted VLI
EFLAGS: 00010246   (2.6.23.1 #1)
EIP is at init_module+0x2/0xd [oops_mod]
eax: 00000000   ebx: d54a094c   ecx: 00000000   edx: ffffffff
esi: d54a0800   edi: d54a096c   ebp: e0856300   esp: d5447edc
ds: 007b   es: 007b   fs: 0000  gs: 0033  ss: 0068
Process insmod (pid: 3682, ti=d5446000 task=ddcb6f10 task.ti=d5446000)
Stack: c0130dcc 00000000 00000000 00000001 ffffffff 000031f5 000003e8 e0856214
       e0ba7204 e0ba7200 ddcb6f10 00000000 00000000 00000000 00000000 00000000
       00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Call Trace:
 [<c0130dcc>] sys_init_module+0x11b0/0x1286
 [<c0103c26>] sysenter_past_esp+0x5f/0x89
 =======================
Code: <c7> 05 00 00 00 00 05 00 00 00 c3 c3 90 90 04 00 00 00 14 00 00 00
EIP: [<e0856002>] init_module+0x2/0xd [oops_mod] SS:ESP 0068:d5447edc

Oops zawiera między innymi:

klogd będzie próbował przetłumaczyć adresy jądra na symbole z /proc/kallsyms i /boot/System.map. klogd jednak ich nie aktualizuje. Można temu zaradzić na dwa sposoby - uruchomić klogd z opcją -p powodującą ładowanie symboli przy załadowaniu modułu lub wysłać do klogd sygnał SIGUSR1 lub SIGUSR2. /proc/kallsyms jest uaktualniany dynamicznie w trakcie ładowania modułów, System.map zawiera stałe dla jądra. Możemy w nich ręcznie odszukać znaczenie adresów i znaleźć offset kolejnej instrukcji, która miała być wykonana

daroon:~/SO# cat /proc/kallsyms | less
e0ba7080 ? __mod_vermagic5      [oops_mod]
e0856300 d __this_module        [oops_mod]
e085600d t cleanup_module       [oops_mod]
e0856000 t init_module  [oops_mod]
00000000 a af_inet6.c   [ipv6]

Możemy teraz zdeasemblować nasze pliki i znaleźć instrukcję, która spowodowała błąd. Możemy to zrobić dzięki znalezionemu wcześniej offsetowi

daroon:~/SO# objdump -S -d oops_mod.o

oops_mod.o:     file format elf32-i386

Disassembly of section .text:

00000000 :
int init_module(void)
{
    int* i=0;
    *i=5;
    return 0;
}
   0:   31 c0                   xor    %eax,%eax
MODULE_LICENSE("GPL");

int init_module(void)
{
    int* i=0;
    *i=5;
   2:   c7 05 00 00 00 00 05    movl   $0x5,0x0
   9:   00 00 00
    return 0;
}
   c:   c3                      ret

0000000d :

void cleanup_module(void){}
   d:   c3                      ret

UML

Wstęp

Ogólnie User Mode Linux jest narzędziem do uruchamiania jądra linuxa jako zwykły proces użytkownika. Jest to jedno z narzędzi do wirtualizacji, jakie poznaliśmy podczas pierwszej prezentacji. Ma też możliwość korzystania z sieci i innych urządzeń, czego nie będziemy tu opisywać.
Może on mieć duże zastosowanie do odpluskwiania zarówno jądra jak i jego modułów, ze względu na to, że możemy wykorzystać praktycznie dowolny debugger dostępny do pracy z normalnymi aplikacjami z poziomu użytkownika.
Strona domowa projektu UML user-mode-linux.sourceforge.net aktualnie w przebudowie, stąd np linki znalezione w starych forach mogą być nieaktualne.

Tryby pracy UML

Przygotowania

Przydatne będą następujące narzędzia:

Kompilowanie jądra

Cała procedura teoretycznie jest dziecinnie prosta ( i nawet w miarę szybka )

Przygotowanie obrazu systemu plików

Kolejną rzeczą jaka jest potrzebna to obraz systemu plików na którym będzie operować nasz system. Najłatwiej i najlepiej jest ściągnąć gotowy np. z tąd http://uml.nagafix.co.uk/.
Jeśli ktoś (bardzo) chce, można też przygotować swój własny obraz. Najpierw trzeba mieć odpowiednio duży plik (obrazy mają od około 256MB w górę) dd if=/dev/zero of=plik bs=rozmiar_bloku count=ilosc_bloków potem wystarczy założyć na nim system plików odpowiedniego typu (oczywiście obsługiwanego przez jądro) dla ext2 mkfs.ext2 plik . Potem trzeba jeszcze umieścić tam odpowiednie pliki i katalogi. Trzeba dodać odpowiedni plik /etc/fstub w którym dodamy wpis typu /dev/ubda / auto defaults 1 1, opisujący urządzenie, gdzie widoczny będzie obraz dysku

Umieszczamy otrzymany wcześniej plik linux i obraz w jednym katalogu. Dobrym pomysłem jest teraz zmiana nazwy obrazu na root_fs, aby był on domyślnie uruchamiany.
Teraz możemy zainstalować moduły w obrazie. Montujemy obraz do drzewa katalogów hosta
(sudo) mount -o loop root_fs katalog/ , w ten sposób uzyskujemy dostęp do obrazu. Aby zainstalować moduły wykonujemy z głównego katalogu źródeł jądra
make modules_install INSTALL_MOD_PATH=ścieżka_do_katalogu_z_zainstalowanym_obrazem ARCH=um
UWAGA Trzeba pamiętać aby przed uruchomieniem UML-a odmontować ten katalog(umount katalog), jeśli dwa systemy używałyby go jednocześnie mógłby łatwo być uszkodzony wewnętrzny system plików obrazu.
W ten sposób otrzymujemy obraz gotowy do uruchomienia.

Teraz wystarczy już tylko "./linux " aby uruchomić nasze jądro. (i wpisać polecenie halt, żeby je wyłączyć)
UML akceptuje też różne opcje z linii poleceń, najważniejsza to udba=nazwa_pliku_root_fs, ustawia wykorzystywany obraz dysku.
Opcja mem pozwala ustawić limit pamięci np na 128M - megabajtów.

Dostęp do systemu plików hosta

Najprostsza metoda to podmontować katalog hosta, z UML wystarczy wpisać:
mount none /host -t hostfs zamontuje katalog hosta "/" w folderze /host w UML
jeśli chcemy podać konkretny katalog wystarczy dopisać mount none /host -t hostfs -o katalog
taka prostota ma swoją cenę - ograniczenia, a wśród nich: nie można nic zapisywać, chyba że uruchomiliśmy UML jako root, operacje na hoście są cache'owane wewnątrz UML, przez co zmiany mogą pozostawać niezauważone przez jakiś czas, problemy z numerami użytkowników i uprawnieniami,są też inne ograniczenia.
Część problemów (ale nie wszystkie) rozwiązuje korzystanie z humfs, ale wymaga przygotowania specjalnego katalogu
Po szczegóły odsyłam tutaj

Poza tym istnieje narzędzie UML_console pozwalające na dostęp do uruchomionego jądra i wykonywanie na nim pewnych operacji np. podłączanie urządzeń do plików.

Debugger GDB

Strona GDB
Zasadniczo chciałbym pokazać DDD, ale mocno wykorzystuje on gdb, w szczególności można normalnie wpisywać jego komendy. A wśród nich warto chyba znać: Żeby uruchomić UML-a pod kontrolą gdb wystarczy wpisać gdb linux argumenty przekazywane do UML-a nie należy wpisywać w linii poleceń, ale jako argumenty polecenia w gdb - run
UWAGA Żeby zatrzymać program pod kontrolą gdb wystarczy normalnie wcisnąć Ctrl+C, jednak w przypadku UMLa to nie działa. Należy w tym celu wysłać z innej konsoli SIGINT do pierwszego procesu UMLa, tj. zwykle procesu o najniższym pid'ie o nazwie linux, zakładając że uruchamiamy 1 kopię UMLa.
Jeśli chcemy debugować już uruchomione jądro można uruchomić gdb i podać mu polecenie att pid_UML
gdzie ten pid wyznaczamy podobnie jak wyżej.
Analogiczne polecenie det odłącza od aktualnego procesu. Dzięki nim możemy przełączać się między procesami.
Jeśli program będzie zbyt często zatrzymywał się na sygnałach o błędzie ochrony pamięci i USR1 możemy wyłączyć to poleceniem handle SIGSEGV pass nostop noprint. Sygnał USR1 był kiedyś używany przez UML do wewnętrznej komunikacji, stąd polecenie to było istotne. Przy ustawieniach z tej prezentacji pozostawiamy obsługę sygnałów niezmienioną.
Gdb daje również możliwość analizowania plików core, tj. zrzutów pamięci i informacji o stanie programu w chwili wystąpienia błędu. Należy w tym celu uruchomić gdb program plik_core . Aby włączyć zapisywanie plików core należy wpisać w bashu ulimit -c max_rom_w_MB . Poza tym pliki core zapisywane są w formacie ELF, można więc próbować je odczytać przy pomocy objdump i readelf.

DDD i debugowanie jądra

DDD to w zasadzie graficzna nakładka na GDB, o dużych możliwościach. Umożliwia wygodne wyświetlanie uruchomionego kodu aplikacji, wizualizację zawartości pamięci w tym wyświetlanie struktur i list i sporo innych rzeczy. Najlepiej będzie zobaczyć to na przykładzie.
Uruchamianie oczywiście ddd linux

Raczej nie chcemy śledzić całego startu UML, trzeba będzie ustawić pułapkę w jakimś ciekawym miejscu kodu, a nawet poczekać na start systemu i wtedy dopiero przerwać działanie sygnałem INT, w celu ustawienia pułapek. Jeśli chcemy mieć możliwość zacząć debugować jądro od momentu startu warto spróbować ustawić breakpointa na funkcji start_kernel
Do wad DDD można zaliczyć wyraźnie wolniejsze działanie oraz konieczność uważania na to co się robi(np. przycisk Interrupt nie robi tego co trzeba).

Debugowanie modułu

Najpierw potrzebny będzie nasz moduł do odpluskwiania, stworzyć go można na podstawie scenariuszy laboratoryjnych. Potem trzeba zamontować obraz dysku jak opisano wyżej. Teraz trzeba będzie tylko trochę przerobić wziętego stamtąd Makefile'a, aby korzystał z obrazu dysku i odpowiednich źródeł jądra. Można to zrobić np. tak:
TARGET = nowymodul
MAIN_OBJ = nowymodul.o
OBJS = $(MAIN_OBJ)
MDIR = drivers/misc
ROOTF = /home/kornel/Desktop/soUML/kat #ścieżka do zamontowanego obrazu pliku

EXTRA_CFLAGS = -DEXPORT_SYMTAB -g # na wszelki wypadek chcemy wkompilowac informacje dla debuggera
CURRENT = 2.6.22  #zamiast uname -r,
KDIR = $(ROOTF)/lib/modules/$(CURRENT)/build # dodamy tą ścieżkę na początek
PWD = $(shell pwd)
DEST = $(ROOTF)/lib/modules/$(CURRENT)/kernel/$(MDIR)

obj-m +=$(OBJS)

default:
	make -C $(KDIR) SUBDIRS=$(PWD) modules

$(MAIN_OBJ): $(OBJS)
	$(LD) $(LD_RFLAG) -r -o $@ $(OBJS)

install:
	sudo  cp -v $(TARGET).ko $(DEST) #do pisania po obrazie potrzeba uprawnień
clean:
	-rm -f *.o *.ko .*.cmd .*.flags *.mod.c

-include $(KDIR)/Rules.make
Można zauważyć, że po kompilacji i instalacji modułów jądra zostały utworzone linki do dobrych źródeł jądra w katalogu root_fs/lib/modules/wersja_jadra. Korzystamy z tego przy kompilacji.
Teraz wystarczy już tylko wykonać make ARCH=um potem make install
Ważne jest, żeby korzystać z tego samego kompilatora i opcji jakie zostały użyte przy budowie jądra.
Może pojawić się ostrzeżenie postaci WARNING: vmlinux(.got+0x8858084): Section mismatch: reference to .init.text: ..., jednak wydaje się ono być niegroźne.
Debugowanie modułu jest to trochę skomplikowane i niewygodne, z tego powodu powstał skrypt UMLgdb, jednak zdaje się, że poprostu nie działa. Dlatego pozostaje przejście całej procedury ręcznie.

Zakładając, że chcemy ręcznie załadować nasz moduł trzeba wykonać poniższą procedurę Czasami może się zdarzyć, że gdb nie zatrzymuje się w pułapce, można spróbować dać kilka breakpointów, autor UML-a poleca w ostateczności spróbować wpisać nieskończoną pętlę w potrzebnym miejscu, z warunkiem na swoją globalna zmienną którą można przestawić z debuggera aby wyjść z pętli. Dokładniej o tym tutaj.
A tutaj filmik demonstrujący debuggowanie modułu.

W sumie

kdb i kgdb

Wprowadzenie

Dlaczego kdb i kgdb? W czym są lepsze od umla? W czym są gorsze? Ale ogólnie są bardzo fajne, zwłaszcza kgdb. Szkoda, że strona internetowa projektu podaje, że ostatnie wspierane jądro to 2.6.15.5. Widać, że projekt nie zatrzymał się całkiem. Można na przykład skorzystać produktu kgdbPro, który kosztuje zaledwie "$1499.00" dla i386. Za darmo można też pobrać najnowszą wersję wprost z repozytorium cvs na sourceforge.net, ale jest to jakieś 2.6.21, czyli ani najnowsze, ani to, nad któym pracujemy na tych zajęciach.

kdb

kdb = ? built-in, czyli jest to element jądra: nie pakiet, nie aplikacja -- kawałek jądra. W związku z tym "instalacja" polega na ściągnięciu patchy na jądro, zaaplikowanie ich i kompilację jądra:
wget ftp://oss.sgi.com/projects/kdb/download/v4.4/kdb-v4.4-2.6.17-common-1.bz2
wget ftp://oss.sgi.com/projects/kdb/download/v4.4/kdb-v4.4-2.6.17-i386-1.bz2
bunzip2 kdb-v4.4-2.6.17-common-1
bunzip2 kdb-v4.4-2.6.17-i386-1
cd linux-2.6.17.13/
patch -p1 < ../kdb-v4.4-2.6.17-common-1
patch -p1 < ../kdb-v4.4-2.6.17-i386-1
make linux
Potem pozostaje dodać odpowiednie rzeczy do /boot i zrobić update-grub.

Ogromne zalety kdb: Możliwości kdb: To gdzie te wady?

kgdb

kgdb = ? Nie tylko w nazwie przypomina kdb i jest lepszy nie tylko o dodatkową literkę.

Jak to działa? No to instalujemy!

Wnioski

Ogółem

Linki