Na początek, skrótowo, mamy do zrobienia kilka rzeczy:
1. Zainstalowanie nowego, Linuxowego systemu operacyjnego, na którym będziemy eksperymentować,
2. Skomponowanie własnej konfiguracji jądra tego nowego systemu, któremu wkompilujemy rozszerzenia umożliwiające zdalny dostęp przez debugger gdb,
3. Skonfigurowanie własnego środowiska programowania modułów przy użyciu Eclipse i kompilatora gcc,
4. Wymyślenie jakiejś sensownej polityki cyklu programowania i testowania, aby nie była ona aż tak bardzo niewygodna, że aż niemożliwa.
Rozumiem, że punkt 4 zazębia się ze znaną łacińską sentencją: de gustibus non est disputandum. Programowanie, jak każda dziedzina, którą robi się ostatecznie głównie samemu, w dużej mierze wymaga samozadowolenia ze środowiska pracy. Gustów jest jednak tak wiele, jak wiele jest charakterów na świecie, dlatego nie jest możliwe podanie jednego przepisu, który zadowoli wszystkich. Podana przeze mnie metoda jest zapewne jedną z miliona, która jednak dla mnie działa, przeciwnie do wielu konfiguracji na które sam obiłem się podczas moich wcześniejszych poszukiwań w czeluściach internetu, dlatego postanowiłem opisać ją nieco szerzej i nieco dostępniej, niż w postaci abstrakcyjnych skojarzeń i wyładowań elektromagnetycznych neuronów w moim mózgu ;). Dodam też jeszcze, że zanim zaczniesz się zapierać rękami i nogami przed nowym doświadczeniem konfiguracji środowiska, w którym możesz pracować, możesz wziąć pod uwagę też inną łacińską sentencję, która jest niejako patronką bloga, którego czytasz: per aspera ad astra ;).
Zabierając się do trybu jądra, wiele rzeczy powinieneś/powinnaś potrafić robić samemu/samej. To znaczy, instalacja nowego systemu Linuxowego nie powinna przysparzać Ci kłopotów. Powinieneś/Powinnaś wiedzieć, czym jest Eclipse, jak też wiedzieć, że potrzebujesz plugina CDT, który umożliwia pracę z kodem C/C++ z poziomu środowiska Eclipse. Co prawda korzystanie z tego IDE jest całkowicie opcjonalne, jednak nawet zatwardziały programista Emacs'owego Lisp'a nie może zakwestionować wygody programowania przy użyciu takich dodatków jak automatyczna analiza składni kodu, wyświetlanie semantycznych problemów w czasie rzeczywistym, wyświetlanie składowych struktur, argumentów funkcji, etc.
Powinieneś/Powinnaś mieć co nieco wiedzy na temat systemów wirtualizacji, przy czym podstawowa wiedza powinna w zupełności wystarczyć; temat jest na tyle obszerny, że wymaga swojej własnej dedykacji czasu i zasobów, aby zacząć się w niego zgłębiać, na co przyjdzie zapewne czas w przyszłości ;). Do wirtualizacji systemu wykorzystam program VirtualBox, ponieważ jest dostępny za darmo na większości dystrybucji Linuxowych.
Przydatną umiejętnością i wiedzą jest samodzielność podczas kompilacji własnych wersji kernela. Nie jest wymagana do tego znajomość programowania, i umiejętność tą posiada każdy szanujący się administrator sieci Linuxowych, dlatego – jeśli jeszcze tego nie robiłeś/robiłaś – idź, i zainstaluj Gentoo :).
Konfiguracja systemu do eksperymentów
Jak wspomniałem wcześniej, przyjmę, że posiadasz już zainstalowany wirtualny bazowy system Linuxowy w VirtualBox, którego nazwa właściwie nie gra roli, dopóki posiada on kernel z rodziny co najmniej 2.6. W swoich przykładach będę posługiwał się 32-bitową dystrybucją Fedora w wersji 15. Przy okazji, sam posiadam skonfigurowaną też 64-bitową dystrybucję Linux Mint Debian Edition pod kernel development; metody konfiguracji były takie same.
Po pierwsze, należałoby ściągnąć interesującą nas wersję kernela z oficjalnej kernelowej strony, kernel.org. Jest to kernel nazywany przez dystrybucje mianem „vanilla”, czyli podstawowy, bez żadnych patchów innych podmiotów (tutaj: bez żadnych patchów Fedory), usprawniających interoperacyjność z innymi składnikami systemu. To, co ściągniesz, będziesz posiadał/posiadała w całości w taki sposób, w jaki sobie to skonfigurujesz, dlatego musisz upewnić się, że wiesz, co należy zrobić, aby Twój nowy kernel wiedział jak się utrzymać po otrzymaniu kontroli przez MBR ;). Ale po kolei.
Dla eksperymentu można ściągnąć najnowszą wersję kernela, którą jest wersja 3.0.2 (w momencie pisania tego posta) i zapisać go do katalogu np. ~/dev/custom-kernel. Wersja ściągnięta w ten sposób po części jest już skonfigurowana pod development, więc pozostaną nam do zaznaczenia tylko w sumie same szczegóły. Po ściągnięciu pliku i jego rozpakowaniu, można wejść do katalogu i zweryfikować, że pliki są na swoim miejscu (np. istnieje plik arch/x86/kernel/kgdb.c), po czym uruchomić make menuconfig (być może trzeba będzie doinstalować pakiet ncurses-devel).
Opcje do koniecznego zaznaczenia (w kernelach w wersjach innych niż 3.0.2 niektórych opcji może nie być; należy wziąć to raczej na inteligencję, a nie na proste kopiowanie):
Kernel hacking → [*] Enable __deprecated logic,
Kernel hacking → [ ] Strip assembler-generated symbols during link,
Kernel hacking → [ ] Enable unused/obsolete exported symbols,
Kernel hacking → [*] Kernel debugging (i wszystkie dzieci),
Kernel hacking → [*] Debug object operations,
Kernel hacking → [*] Kernel memory leak detector,
Tych kilka opcji ułatwi debugowanie mechanizmów synchronizacji międzywątkowej:
Kernel hacking → [*] RT Mutex debugging, deadlock detection,
Kernel hacking → [*] Spinlock and rw-lock debugging: basic checks,
Kernel hacking → [*] Mutex debugging: basic checks,
Kernel hacking → [*] Lock debugging: detect incorrect freeing of live locks,
Kernel hacking → [*] Lock debugging: prove locking correctness,
Kernel hacking → [*] RCU debugging: prove RCU correctness,
Kernel hacking → [*] KGDB: kernel debugger → [*] KGDB: use kgdb over the serial console oraz [*] „KGDB: Allow debugging with traps in notifiers” – natomiast reszta powinna być odznaczona,
Kernel hacking → [*] Enable verbose x86 bootup info messages – więcej informacji od kernela, gdy coś pójdzie niezgodnie z planem,
Kernel hacking → [ ] Write protect kernel read-only data structures – zaznaczona opcja nie pozwoli na zakładanie pułapek – gdb zwracałby błąd „error accessing memory address (...)”.
Znajduje się tutaj o wiele więcej opcji, które mogą być wykorzystywane w różnych aspektach pracy nad sterownikami, jednak my nie będziemy z nich korzystać. Właściwie, to nie będziemy korzystać z większości tego, co znajduje się w tabelce powyżej, ale wydaje mi się, że taki setup stanowi dość dobry bootstrap do samodzielnego sprawdzania, co, jak, gdzie, z kim i dlaczego :)
Po wybraniu wszystkich opcji, jak też i tych, które wiesz, że są Ci potrzebne, czy to do prawidłowego działania kernela na Twojej maszynie wirtualnej, czy z jakiegokolwiek innego powodu, warto zapisać aktualną konfigurację do pliku o nazwie .config przy pomocy opcji „Save an Alternate Configuration file”, po czym można już wyjść z menuconfig'a.
Kompilacja zostanie rozpoczęta po wykonaniu polecenia make -j 2, gdzie zamiast 2 wstaw liczbę core'ów w twojej maszynie wirtualnej, podwojonej przez 2. Ja wybrałem maszynę z jednym rdzeniem, choć polecam stworzyć maszynę z dwoma lub więcej rdzeniami; ich większa ilość w znaczący sposób może przyspieszyć problemy z synchronizacją algorytmów, problemy z poprawnym blokowaniem odpowiednich mutex'ów, poprawne wstawianie sekcji krytycznych o poprawnej długości w poprawne miejsca w kodzie, słowem: programowanie algorytmów przy użyciu wielozadaniowości stanie się bardziej naturalne.
Jak wiadomo, proces budowy kernela trwa dość długo nawet na szybkich procesorach. Lepiej mieć tutaj możliwie jak największą liczbę core'ów, np. 4 lub więcej; wtedy większa liczba argumentu -j polecenia make potrafi przyspieszyć build nawet kilkukrotnie, oszczędzając nam sporo czasu, a – jak wiadomo – czas, to pieniądz.
Jeśli chce ci się kombinować, możesz skompilować kernel na swojej natywnej maszynie, jednak musisz upewnić się, że maszyna natywna i maszyna wirtualna posiadają dokładnie taką samą wersję kompilatora gcc, który jest dość strategicznym punktem jeśli chodzi o ogólny temat programowania na system Linux.
Nawet jeśli jednak posiadasz tylko dwa core'y (lub jeden; choć statystyki mówią, że najczęstrzym przypadkiem są jednak dwa core'y), nie powinieneś/powinnaś narzekać, ponieważ jeszcze nie tak dawno temu, na maszynach z kilku lat wstecz (np. 8 lat) kompilacja kernela trwała całą noc... podejrzewam też, że nie posiadasz sprzętu klasy Pentium 133, ale jeśli tak – skocz na rower :)
Po zakończeniu procesu kompilacji, należy zainstalować nowy kernel w katalogu /boot (razem z plikiem System.map):
- virtual $ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-3.0.2
- virtual $ sudo cp System.map /boot/System.map-3.0.2
i zainstalować dla niego wszystkie skompilowane sterowniki:
- virtual $ sudo make modules_install
Zauważ też w aktualnym katalogu plik vmlinux, który też będzie potrzebny, ale na razie nie trzeba go nigdzie kopiować:
- virtual $ ls -la vmlinux
- -rwxr-xr-x. 1 root root 146667489 Aug 18 17:28 vmlinux
Teraz należałoby skonfigurować domyślny bootloader, by odpalał nasz kernel, a nie ten z domyślnej dystrybucji. W Fedorze znajduje się grub w pierwszej wersji, to znaczy, że należy zmodyfikować plik /boot/grub/menu.lst, zmieniając dyrektywę timeout na np. 10, wstawiając hash (#) przed hiddenmenu, aby dyrektywę tą wyłączyć, oraz dopisując nowy deskryptor naszego eksperymentalnego jądra, która, najlepiej gdy będzie kopią deskryptora innego kernela, ale nieco zmodyfikowaną, tzn. oczywiście zmieni się ścieżka do pliku z naszym kernelem, jak też i zniknie argument quiet z końca jego linii poleceń. Oprócz tego należy dodać argument inicjujący debugger kgdb – kgdboc=ttyS0,115200, co znaczy, że kgdb będzie nasłuchiwał na porcie szeregowym numer zero, czyli pierwszym ;), jak również opcję kgdbwait, która mówi kernelowi, aby poczekał na początkowe podłączenie się debuggera gdb na maszynie natywnej – ale tą opcję wstawimy trochę później, więc jeszcze jej nie dodawaj ;).
Oprócz tego, jeśli też korzystasz z Fedory, zwróć uwagę na to, że domyślna instalacja rozkłada partycje przy użyciu LVM; aby poprawnie wystartować kernel przy LVM, należy zbudować initrd dla nowej wersji kernela. Na szczęście ogranicza się to tylko do wywołania programu dracut:
- virtual $ sudo dracut --strip -L 5 /boot/initramfs-3.0.2.img 3.0.2
(gdzie 3.0.2 to wersja twojego kernela). Następnie zaktualizuj konfigurację gruba, wstawiając opisywany wcześniej deskryptor (u Ciebie może się trochę różnić, ale pamiętaj o argumencie kgdboc na końcu linii „kernel”):
- Title Experimental 3.0.2
- root (hd0,0)
- kernel /custom-kernel-3.0.2 ro root=/dev/mapper/vg_federal-lv_root rd_LVM_LV=vg_federal/lv_root rd_LVM_LV=vg_federal/lv_swap rd_NO_LUKS rd_NO_MD rd_NO_DM LANG=en_US.UTF-8 SYSFONT=latarcyrheb-sun16 KEYTABLE=pl2 rhgb kgdboc=ttyS0,115200
- initrd /initramfs-3.0.2.img
Grub w wersji 2 łatwiej skonfigurować za pomocą polecenia grub-mkconfig. Odsyłam do Google po nieco więcej szczegółów :).
Pora na test polowy, czyli restart wirtualnego systemu i sprawdzenie, czy cały system wstaje poprawnie i startuje do graficznego interfejsu – jeśli tak jest, można chyba założyć, że kernel został skonfigurowany poprawnie i można przystąpić do kroku następnego. W tym kroku polecam wykorzystać funkcję robienia snapshotów w VirtualBox – znacznie przyspieszy kombinowanie w przypadku, gdy twój kernel nie będzie chciał się podnieść. W razie gdy tak się stanie, musisz przeanalizować to, co kernel wypisał na ekranie zanim przestał odpowiadać i w jakiś sposób dojść do tego, co powoduje problem. Być może nie wkompilowałeś/wkompilowałaś w jądro obsługi systemu plików, tylko zrobiłeś/zrobiłaś sam moduł? Być może brakuje obsługi chipsetu, jakiegoś kontrolera dysków? VMware znane jest ze swoich niestandardowych wymagań jeśli chodzi o własną kompilację linuxowego kernela, proponuję więc skorzystać z Google w celu odnalezienia prawidłowych ustawień.

Zapomniałem zrobić initrd, aby kernel supportował LVM przy starcie. Skutek – czytanie na Google w jaki sposób zrobić poprawny initrd :)
Krokiem następnym (zakładając optymistycznie, że kernel wstał i załadował się w poprawny sposób) będzie stworzenie pewnego swojego rodzaju tunelu komunikacyjnego między debuggerem kgdb na systemie wirtualnym, a debuggerem gdb uruchamianym na systemie natywnym. W skrócie, polega to na tym, że kernel wirtualny z rozszerzeniami kgdb będzie sterowany przez te rozszerzenia; wszystkie rozkazy zatrzymania się, inspekcji obszarów pamięci, kontynuacji czy odczytywania instrukcji będą wprowadzane w życie przez kgdb, który jest uruchomiony na systemie wirtualnym. Jednak rozkazy dla kgdb będą wysyłane portem szeregowym przez natywnie uruchomiony debugger gdb, który po prostu będzie korzystał ze swojego standardowego protokołu debugowania zdalnego, który jest „przypadkowo” ;) rozumiany przez kgdb. W skrócie - kgdb, który działa w trybie jądra, jest po prostu stubem remote debugging dla gdb.
Niżej opisane zmiany opcji w VirtualBox należy przeprowadzić na wyłączonym systemie wirtualnym – co wynika z ograniczenia samego VirtualBox'a, który w chwili obecnej nie pozwala na dokonywanie tak „inwazyjnych” zmian maszyn wirtualnych. VMware z kolei raczej nie powinien mieć z tym problemu.
Po wejściu w opcję „Serial Ports”, zaznacz opcję „Enable Serial Port”, zaznaczając, że chodzi o (wirtualny) port COM1. Tryb portu powinien zawierać wartość „Host pipe”, opcja „Create Pipe” powinna być zaznaczona, oraz pole tekstowe „Port/File path” powinna zawierać ścieżkę do pliku na dysku, która powinna zawierać informacje o wybranym pipe'ie. Jak widać na screenshocie, u mnie widnieje ścieżka /home/antek/serial.
Pora teraz zainstalować program socat. Jego nazwa jest skrótem od socket cat (gdzie cat jest z kolei skrótem od concatenate) i służy do tłumaczeń różnych technik przesyłania informacji na inne techniki. Jest nam potrzebny, ponieważ konfigurujemy VirtualBox'a tak, aby port szeregowy systemu wirtualnego przekierowywał zapisane do niego dane do pipe'a stworzonego w wybranym pliku (w moim przypadku: /home/antek/serial). Gdb jednak nie posiada umiejętności zdalnego debugowania poprzez zwykłe pipe'y, dlatego trzeba tego pipe'a przetłumaczyć na coś, z czego można czytać jak ze zwykłego portu szeregowego. Takim czymś może być podsystem pseudoterminali („pty”), który zwykle „mieszka” na systemie natywnym w katalogu /dev/pts. Zdecydowana większość dystrybucji jest skonfigurowana w ten sposób, że pod ten katalog montowany jest system plików devpts, który podsystem ten implementuje. Socat potrafi zaalokować nowy pseudoterminal, umieszczając w nim dane, które odczyta z pipe'a o nazwie /home/antek/serial. Jeśli to mu się uda, wtedy gdb może podłączyć się do nowo-zaalokowanego pseudoterminala, hostowanego przez socat'a, który będzie forwardował wszystkie instrukcje z gdb do podłączonego pipe'a, który z kolei będzie wpychał ten ciąg danych do VirtualBox'a, który z kolei będzie oszukiwał system wirtualny, że owe dane nadchodzą z portu szeregowego, który zostanie odczytany przez kgdb i wykona on odpowiednią akcję. Dane zwrotne, z wynikami, będą podążać tą samą drogą, ale w przeciwną stronę :).

Być może brzmi to w dość przewrotny sposób, ale wszystko sprowadza się do stwierdzenia: za pomocą socat'a należy przetłumaczyć /home/antek/serial na wolny pseudoterminal w katalogu /dev/pts. Proste polecenie załatwi sprawę:
- native $ socat -d -d /home/antek/serial pty
W wyjściu programu będzie widniał numer pseudoterminala, pod który podpięty jest pipe serial. Ten numer należy zapamiętać, ponieważ trzeba go będzie wpisać potem jako argument jednego polecenia gdb. Oto przykład wyjścia, który mówi o zainicjowanym pseudoterminalu numer 3, czyli /dev/pts/3:
- native $ socat -d -d /home/antek/serial pty
- 2011/08/19 19:50:17 socat[14653] N opening connection to AF=1 "/home/antek/serial"
- 2011/08/19 19:50:17 socat[14653] N successfully connected from local address AF=1 "\0\0\0\0\0\0\0\0\0\x02\0\0\0\0:\0`\t"
- 2011/08/19 19:50:17 socat[14653] N successfully connected via \xCC\xD7}\x7E
- 2011/08/19 19:50:17 socat[14653] N PTY is /dev/pts/3
- 2011/08/19 19:50:17 socat[14653] N starting data transfer loop with FDs [3,3] and [4,4]
Skoro już wiesz jak z niego korzystać, możesz chwilowo go opuścić przez naciśnięcie ^C.
Druga rzecz to poprawne skonfigurowanie sieci na maszynie wirtualnej. Najlepiej posłuży do tego tryb mostka (bridged adapter), o nazwie takiej, jak twój interfejs sieciowy na maszynie natywnej, ponieważ wtedy maszyna wirtualna dostanie numer IP, który będzie „widziany” z maszyny natywnej (i wszystkich maszyn w sieci) przy użyciu standardowych poleceń sieciowych (np. lftp, rsync, ping). Być może będziesz musiał/musiała zmienić ustawienia DHCP swojego routera, by uwzględniał adres MAC wirtualnej karty sieciowej (który widnieje w polu „Mac Address”) i zaopatrywał go w wybrany numer IP – jeśli takowy serwer DHCP posiadasz. Jeśli nie, standardowa metoda konfiguracji statycznego adresu IP interfejsu sieciowego na maszynie wirtualnej (ifconfig) powinna załatwić sprawę. Właściwie ten krok nie jest niezbędny, ponieważ sama komunikacja debuggera będzie realizowana przez wirtualny port szeregowy – ale jeśli tego nie zrobisz, synchronizacja kodu źródłowego przez rsync (co będzie opisane w dalszej części posta), nie będzie działać. Może wpadniesz na inny pomysł, jak to wygodnie zrobić; dla mnie wygodnie jest tak, jak opisałem wyżej ;).
Pora przetestować połączenie z gdb. Włącz maszynę wirtualną i poczekaj na pełne uruchomienie systemu. Uruchom terminal i z poziomu użytkownika root wykonaj polecenie:
- virtual # echo g > /proc/sysrq-trigger
które zawiesi system ;). Jest to jednak zawieszenie kontrolowane! Podczas gdy system wirtualny jest zawieszony, na systemie natywnym uruchom socat'a, zgodnie z opisem kilka paragrafów powyżej. Po uzyskaniu numeru pseudoterminala, na maszynie natywnej uruchom gdb i powiedz mu, aby połączył się z uzyskanym numerem pty. Przykładowo, jeśli będzie to /dev/pts/3, polecenie powinno wyglądać tak:
- (gdb) target remote /dev/pts/3
- (2:1002)$ gdb
- (gdb) target remote /dev/pts/3
- Remote debugging using /dev/pts/3
- 0xc0485592 in ?? ()
(jeśli dostaniesz tutaj, lub w późniejszych krokach, błędy typu warning: Invalid remote reply, spróbuj po prostu naciskać ^C aż do uzyskania znaku zachęty, po czym ponownie wpisz rozkaz target remote (...) - desynchronizacja powinna zostać naprawiona)
Gdb uzyskał połączenie zdalne z wirtualnym kernelem. Kgdb po stronie wirtualnej wysłał raport na temat swojego stanu; aktualnie stoi w kernelu na adresie 0xc0485592 i oczekuje na dalsze rozkazy, które możemy wysyłać normalnie za pomocą gdb w standardowy sposób. Przykładowo, aby pobrać wartość rejestru EIP, można wysłać polecenie p $eip:
- (gdb) p $eip
- $2 = (void (*)()) 0xc0485592
Zwróci ono tą samą wartość którą uzyskaliśmy przed chwilą w innym miejscu, co znaczy, że wyniki mają logiczny sens, więc teoretycznie wszystko działa jak powinno. Lub też np. chcąc zrobić dump 20 bajtów spod adresu, na który wskazuje rejestr ESI, można wykonać to polecenie:
- (gdb) x/20b $esi
- 0xc0a784a4: 0x79 0x54 0x48 0xc0 0x37 0x35 0x95 0xc0
- 0xc0a784ac: 0x40 0x35 0x95 0xc0 0x00 0x00 0x00 0x00
- 0xc0a784b4: 0x6b 0x67 0x64 0x62
Podobnie, jeśli chcesz wyświetlić kod asemblerowy w miejscu, gdzie aktualnie jesteś, możesz wykonać np.:
- (gdb) x/10i $eip-3
- 0xc0485373: mov %esi,%esi
- 0xc0485375: int3
- => 0xc0485376: sfence
- 0xc0485379: mov %esi,%esi
- 0xc048537b: mov $0xc1088a60,%eax
- 0xc0485380: call 0xc0485351
- 0xc0485385: pop %ebp
- 0xc0485386: ret
- 0xc0485387: push %ebp
- 0xc0485388: mov %esp,%ebp
Wszystko pięknie, oprócz tego, że nie wiemy gdzie jesteśmy! Gdzie jest kod? Jak go zobaczyć? Jak postawić pułapkę na jakiejś funkcji? Jeszcze nie możemy tego zrobić, bo brakuje nam symboli – czyli nazw funkcji i zmiennych, które zmapowane są pod konkretne adresy.
Pamiętasz plik vmlinux? Ten plik zawiera mapowanie symboli do adresów. Teraz jest ten moment, gdy go potrzebujemy, razem ze źródłami wersji kernela, którą posiadasz na systemie wirtualnym. Najpierw odwieś wirtualny system za pomocą polecenia continue:
- (gdb) c
I przekopiuj plik vmlinux z głównego katalogu źródeł kernela (czyli z tego katalogu, z którego robiłeś/robiłaś make menuconfig) na system natywny, przy użyciu wybranej przez siebie metody (np. scp, sshfs, ftp, virtualbox shared folders, wyślij mailem, etc ;) - przy kopiowaniu możesz chcieć spakować plik np. tar i bzip2 przed wysyłaniem przez sieć).
- virtual $ scp vmlinux antek@192.168.1.2:/home/antek/dev/kernel/fedora-3.0.2
- vmlinux 100% 152MB 15.2MB/s 00:10
- virtual $ scp ../linux-3.0.2.tar.bz2 antek@192.168.1.2:/home/antek/dev/kernel/fedora-3.0.2
- linux-3.0.2.tar.bz2 100% 73MB 14.6MB/s 00:05
- native ~/dev/kernel/fedora-3.0.2 $ tar xfj *bz2
- native ~/dev/kernel/fedora-3.0.2 $ ls
- linux-3.0.2/ linux-3.0.2.tar.bz2 vmlinux*
- native ~/dev/kernel/fedora-3.0.2 $ cd linux-3.0.2/
- native ~/dev/kernel/fedora-3.0.2/linux-3.0.2 $
Po uzyskaniu wszystkiego na systemie natywnym, wyjdź z sesji gdb (^C do skutku) i uruchom go jeszcze raz, z katalogu źródeł kernela z systemu natywnego, i tym razem jako parametr podając plik vmlinux. Aktualne przebywanie w katalogu źródeł kernela jest ważne, ponieważ gdb będzie szukał źródeł kernela relatywnie do swojego aktualnego katalogu pracy (working directory):
- native ~/dev/kernel/fedora-3.0.1/linux-3.0.2 $ gdb ../vmlinux
- Reading symbols from /home/antek/dev/kernel/fedora-3.0.2/vmlinux...done.
- (gdb) target remote /dev/pts/3
Gdb tutaj powinien zawisnąć w oczekiwaniu na pułapkę. Na systemie wirtualnym ponownie wyślij sygnał „g” do /proc/sysrq-trigger. Gdb powinien wtedy zatrzymać się w:
- kgdb_breakpoint () at kernel/debug/debug_core.c:960
- 960 wmb(); /* Sync point after breakpoint */
- (gdb)
Sukces ;) Można wyświetlać kod:
- (gdb) list
- 955 void kgdb_breakpoint(void)
- 956 {
- 957 atomic_inc(&kgdb_setting_breakpoint);
- 958 wmb(); /* Sync point before breakpoint */
- 959 arch_kgdb_breakpoint();
- 960 wmb(); /* Sync point after breakpoint */
- 961 atomic_dec(&kgdb_setting_breakpoint);
- 962 }
- 963 EXPORT_SYMBOL_GPL(kgdb_breakpoint);
- 964
aktualną historię ramek:
- (gdb) bt
- #0 kgdb_breakpoint () at kernel/debug/debug_core.c:960
- #1 0xc04856cf in sysrq_handle_dbg (key=<value optimized out>) at kernel/debug/debug_core.c:750
- #2 0xc0668751 in __handle_sysrq (key=103, check_mask=<value optimized out>) at drivers/tty/sysrq.c:522
- #3 0xc06687ff in write_sysrq_trigger (file=<value optimized out>, buf=0xb78d2000 "g\n0;root@federal:/usr/src/custom-kernel/linux-3.0.2\a", count=2, ppos=0xe123df98) at drivers/tty/sysrq.c:870
- #4 0xc052b3c2 in proc_reg_write (file=0xe11e9900, buf=0xb78d2000 "g\n0;root@federal:/usr/src/custom-kernel/linux-3.0.2\a", count=<value optimized out>, ppos=0xe123df98) at fs/proc/inode.c:200
- #5 0xc04ec7c1 in vfs_write (file=0xe11e9900, buf=0xb78d2000 "g\n0;root@federal:/usr/src/custom-kernel/linux-3.0.2\a", count=2, pos=0xe123df98) at fs/read_write.c:377
- #6 0xc04ec983 in sys_write (fd=1, buf=0xb78d2000 "g\n0;root@federal:/usr/src/custom-kernel/linux-3.0.2\a", count=2) at fs/read_write.c:429
- #7 <signal handler called>
stawiać pułapki:
- (gdb) break search_binary_handler
- Breakpoint 1 at 0xc04f0ec9: file fs/exec.c, line 1352.
i wyświetlać źródła asm, posiadając wszystkie symbole pokazane na swoich miejscach:
- (gdb) x/16i search_binary_handler
- 0xc04f0ebb <search_binary_handler>: push %ebp
- 0xc04f0ebc <search_binary_handler+1>: mov %esp,%ebp
- 0xc04f0ebe <search_binary_handler+3>: push %edi
- 0xc04f0ebf <search_binary_handler+4>: push %esi
- 0xc04f0ec0 <search_binary_handler+5>: push %ebx
- 0xc04f0ec1 <search_binary_handler+6>: sub $0x1c,%esp
- 0xc04f0ec4 <search_binary_handler+9>: lea %ds:0x0(%esi,%eiz,1),%esi
- 0xc04f0ec9 <search_binary_handler+14>: mov %eax,%ebx
- 0xc04f0ecb <search_binary_handler+16>: mov 0x94(%eax),%eax
- 0xc04f0ed1 <search_binary_handler+22>: mov %edx,-0x1c(%ebp)
- 0xc04f0ed4 <search_binary_handler+25>: mov %eax,-0x14(%ebp)
- 0xc04f0ed7 <search_binary_handler+28>: mov %ebx,%eax
- 0xc04f0ed9 <search_binary_handler+30>: call 0xc05a3e5e <security_bprm_check>
- 0xc04f0ede <search_binary_handler+35>: test %eax,%eax
- 0xc04f0ee0 <search_binary_handler+37>: mov %eax,%edi
- 0xc04f0ee2 <search_binary_handler+39>: jne 0xc04f10b1 <search_binary_handler+502>
Wygląda na to, że wszystko działa jak należy. Kontynuujmy więc nasz kernel:
- (gdb) c
- Continuing.
I przejdźmy do dalszej części tutoriala ;).

Nie wszystko zawsze idzie różowo – takie błędy pojawiają się, gdy opcja „Write protect kernel read-only data structures” będzie uaktywniona.
Konfigurowanie Eclipse
Należałoby w końcu skonfigurować swoje środowisko programowania.
Przyjmuję, że masz już zainstalowane i skonfigurowane Eclipse wraz z pluginem CDT. Stwórz nowy, pusty projekt C, który nie jest zarządzany przez żadne Eclipse'owe mechanizmy automatycznego generowania plików Makefile – będzie trzeba napisać (i utrzymywać) własny Makefile. Nazwą projektu w oknie wizarda nowego projektu może być np. adx-tutorial.
Należy pamiętać o wyłączeniu zarządzania Makefile'ami:

Pierwszy krok to dodanie nowego pustego pliku Makefile o treści:
- # ustawienia projektu
- MODULENAME=adxtutorial
- OBJECTLIST=adxmain.o debug.o
- # ustawienia rsync
- TARGETNAME=adx-tutorial
- VIRTUALSYSTEM=
- ifneq ($(KERNELRELEASE),)
- obj-m := $(MODULENAME).o
- $(MODULENAME)-objs := $(OBJECTLIST)
- else
- KERNELDIR ?= /lib/modules/$(shell uname -r)/build
- PWD := $(shell pwd)
- default:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- clean:
- rm *.o
- rm *.ko
- rm *.mod.c
- rm *.order
- rm *.symvers
- rsync: default
- @([ ! "$(VIRTUALSYSTEM)" == "" ] && (echo -n "rsync: "; rsync -a ../$(TARGETNAME) $(VIRTUALSYSTEM)::workspacepp)) || true
- @echo done
- all: rsync
- @echo all done
- endif
Plik możesz skopiować w całości i wkleić do swojego, zmieniając w przyszłości tylko cztery zmienne w początkowych liniach pliku, upewniając się jednocześnie, że spacje zostaną zamienione na tab'y, a konkretnie seria spacji zostanie zamieniona na jeden tabulator ;). Znaczenie zmiennych jest takie:
MODULENAME – jest to nazwa pliku, jaki będzie nosił plik sterownika. Do tego ciągu znaków zostanie doklejone rozszerzenie '.ko' – oczywiście, będzie je można potem zmienić.
OBJECTLIST – jest to lista plików obiektowych, z których linker będzie musiał skonsolidować sterownik. Pliki obiektowe muszą kończyć się rozszerzeniem '.o', oraz ich nazwa musi być taka sama jak plików '.c' – czyli, mając w projekcie pliki źródłowe: main.c oraz secondary.c, twój OBJECTLIST wyglądałby tak: main.o secondary.o.
TARGETNAME – jest to nazwa katalogu, który zawiera projekt. Innymi słowy, jest to też nazwa projektu w Eclipse. Będzie potrzebna do konfiguracji rsync poniżej,
VIRTUALSYSTEM – numer IP, pod który można robić rsync, czyli numer IP systemu wirtualnego. Na początku niech będzie tutaj pusto; powinno zostać też puste w przypadku, gdy nie udało ci się zrobić konfiguracji sieciowej opartej na połączeniu mostkowanym, lub też nie chciałeś/chciałaś tego robić.
Reszty części Makefile nie trzeba teraz zmieniać. Dla kompletu, stwórz w projekcie dwa pliki z kodem źródłowym C: adxmain.c orac debug.c. Przepisz do nich taką treść:
adxmain.c:
- #include <linux/module.h>
- static __init int init_adxtut() {
- return 0;
- }
- module_init(init_adxtut);
dummy.c:
- int dummy;
Pora teraz przetestować, czy reguły kompilacji rzeczywiście działają. Tak powinien wyglądać mniej więcej poprawny log build'a Eclipse, po wybraniu opcji kompilacji:
- **** Build of configuration Debug for project adx-tutorial ****
- make all
- make -C /lib/modules/2.6.37.6-0.7-desktop/build M=/home/antek/workspacepp/adx-tutorial modules
- make[1]: Wejście do katalogu `/usr/src/linux-2.6.37.6-0.7-obj/x86_64/desktop'
- make -C ../../../linux-2.6.37.6-0.7 O=/usr/src/linux-2.6.37.6-0.7-obj/x86_64/desktop/. modules
- CC [M] /home/antek/workspacepp/adx-tutorial/debug.o
- LD [M] /home/antek/workspacepp/adx-tutorial/adxtutorial.o
- Building modules, stage 2.
- MODPOST 1 modules
- CC /home/antek/workspacepp/adx-tutorial/adxtutorial.mod.o
- LD [M] /home/antek/workspacepp/adx-tutorial/adxtutorial.ko
- make[1]: Opuszczenie katalogu `/usr/src/linux-2.6.37.6-0.7-obj/x86_64/desktop'
- rsync: failed to connect to 192.168.1.101: No route to host (113)
- rsync error: error in socket IO (code 10) at clientserver.c(122) [sender=3.0.7]
- rsync: done
- all done
(błędem rsync na chwilę obecną się nie przejmuj)
Jeśli w katalogu nie pojawił się żaden plik z rozszerzeniem .ko, sprawdź czy masz zainstalowane poprawne nagłówki swojego aktualnie uruchomionego kernela (aktualną wersję można odczytać przy pomocy polecenia uname -r). Jeśli jednak plik się pojawił, gratulacje!, jest to sterownik, gotowy do załadowania poleceniem insmod, ale tylko pod twój aktualnie uruchomiony kernel na systemie natywnym. Aby sterownik działał poprawnie na systemie wirtualnym, na którym ma działać (ponieważ tylko system wirtualny będzie można wygodnie debugować i nikt nie ucierpi z powodu nieplanowanych kernel paniców), trzeba źródła przekopiować na system wirtualny i tam skompilować. Tutaj do gry wkracza rsync, który w łatwy sposób pozwoli na synchronizację drzewa kodów źródłowych sterownika na wirtualną maszynę. Przed jego użyciem jednak, należy poprawnie skonfigurować demon'a rsync na systemie wirtualnym. Nie jest to na szczęście trudne i ogranicza się do edytowania pliku /etc/rsyncd.conf (jeśli go nie ma, stwórz go):
- virtual # vim /etc/rsyncd.conf
Teraz trzeba wybrać sobie katalog, do którego rsync z systemu natywnego będzie kopiował pliki na system wirtualny. Ten katalog w moim przykładzie nosi nazwę workspacepp. Jeśli chcesz go zmienić, musisz zrobić też to w pliku Makefile, który znajduje się kilka paragrafów wyżej, w regule o nazwie rsync.
Składnia pliku rsyncd.conf jest szerzej opisana w manualach rsync, dlatego tutaj wkleję tylko przykład, jak powinna wyglądać deklaracja katalogu workspacepp:
- [workspacepp]
- path = /home/antek/workspacepp
- read only = no
- uid = 500
- git = 500
I tyle ;), ścieżkę path oczywiście zmodyfikuj, aby pasowała do Twojego środowiska. Demon rsync jest uruchamiany przez:
- virtual $ sudo rsync --daemon
- virtual $ ps aux | grep rsync
- root 26005 0.0 0.0 4608 284 ? Ss 19:50 0:00 rsync –daemon
Nie zapomnij też stworzyć katalogu workspacepp na systemie wirtualnym:
- virtual $ mkdir ~/workspacepp
Demon rsync jest gotowy i czeka na swoim porcie na requesty nowych połączeń. Pamiętaj jednak o tym, że wiele dystrybucji posiada domyślnie konfigurowany firewall, który nie pozwala na połączenia przychodzące; Fedora jest jedną z tych dystrybucji, które ten firewall mają aktywny. Można go całkowicie wyłączyć, albo skorzystać z polecenia wstawienia „pustej” reguły akceptacji pakietów na sam początek łańcucha wejściowego, czyli łańcucha odpowiedzialnego za odbieranie pakietów do lokalnych gniazd (socketów):
- # iptables -I INPUT -s 0/0 -j ACCEPT
Reguła ta na pierwszym miejscu spowoduje akceptowanie wszystkich połączeń (praktycznie równoważne jest to usunięciu wszystkich reguł z łańcucha INPUT i ustawieniu domyślnej polityki akceptacji pakietów na ACCEPT). Lub też, zamiast 0/0 wstaw numer IP swojego systemu natywnego, aby tylko jemu pozwolić na połączenie się z rsync. Podejrzewam jednak, że w przypadku zacisza domowej sieci LAN, wyłączenie firewalla będzie skuteczniejsze, szybsze i łatwiejsze (no, chyba, że korzystasz z WIFI „zabezpieczonej” kluczem WEP – wtedy przerwij czytanie tego tutoriala i zainteresuj się uaktywnianiem WPA2 na swoim hotspocie, najlepiej już teraz).
Po poprawnym skonfigurowaniu, lub wyłączeniu firewalla, czas przetestować skrypt build i synchronizację źródeł. Wywołaj kompilację projektu w Eclipse raz jeszcze – tym razem błąd rsync nie powinien mieć miejsca i źródła powinny zostać skopiowane na system wirtualny w poprawny sposób. Na systemie wirtualnym powinny pojawić się pliki z hosta:
- .
- ├── adx-tutorial
- │ ├── adxmain.c
- │ ├── adxmain.o
- │ ├── adxtutorial.ko
- │ ├── adxtutorial.mod.c
- │ ├── adxtutorial.mod.o
- │ ├── adxtutorial.o
- │ ├── Debug
- │ │ ├── makefile
- │ │ ├── objects.mk
- │ │ ├── sources.mk
- │ │ └── subdir.mk
- │ ├── debug.c
- │ ├── debug.o
- │ ├── Makefile
- │ ├── modules.order
- │ └── Module.symvers
- └── dump.txt
- 2 directories, 16 files
Teraz, na systemie wirtualnym pozostaje przejść do katalogu adx-tutorial, wykonać make clean (ponieważ wszystkie obiekty i metadane pochodzą z systemu natywnego), i ponownie wykonać make (bez „all”), aby skompilować sterownik pod nowy kernel. Jeśli pojawią się błędy, to znaczy, że nowa wersja kernela zawiera nieco inne struktury lub zasady użycia pewnych mechanizmów. Najlepiej wziąć pod uwagę to w swoim sterowniku, aby kompilował się na starszych, jak i nowszych kernelach.
Spinanie wszystkiego w całość
Czas na krótkie podsumowanie.
Posiadasz maszynę wirtualną, która będzie odgrywała „linii frontu”, czyli systemu który będzie uruchamiał sterownik. W przypadku, gdy sterownik zawiera błąd powodujący kernel panic, zawieszeniu ulegnie tylko system wirtualny, chroniąc system natywny przed śmiercią przez restart.
Sterownik programować możesz przy użyciu Eclipse, które zawiera mnóstwo opcji i rozszerzeń wspomagających pracę. Przy każdorazowej kompilacji, aktualne źródła będą kopiowane również na system wirtualny, by w razie potrzeby tam skompilować sterownik i od razu spróbować go załadować, wraz z przeprowadzaniem odpowiednich testów jego działania. Jednym z głównych argumentów przemawiających za użyciem Eclipse jest jednak auto-uzupełnianie składni przez plugin CDT. W Visual Studio owo auto-uzupełnianie nosi nazwę intellisense i jest czymś, bez czego niektórzy nie wyobrażają sobie programowania ;). Auto-uzupełnianie Eclipse jest ulepszane z wersji na wersje, a plugin, który je realizuje nosi prostą nazwę Indexer'a.
Nie jest to jednak idealne rozwiązanie, zawiera kilka błędów, które niekoniecznie są intuicyjne, ale zaopatruje w pewne mechanizmy pozwalające na radzenie sobie z problemami. Jednym z takich mechanizmów jest włączenie opcji debug Indexer'a, które powinny rzucić nieco wyraźniejsze światło na jakiekolwiek problemy, które mogą spowodować brak takiego zachowania, jakie chcielibyśmy, aby istniało ;). Tryb debug włącza się poprzez dodanie kilku linijek do pliku np. debug.ini, w katalogu Eclipse, oraz uruchomienie eclipse z argumentem -debug debug.ini.
Plik debug.ini:
- 1 org.eclipse.cdt.core/debug=true
- 2 org.eclipse.cdt.core/debug/indexer/statistics=true
- 3 org.eclipse.cdt.core/debug/indexer/activity=true
- 4 org.eclipse.cdt.core/debug/indexer/problems=true
Uruchomienie Eclipse z argumentem -debug debug.ini (z konsoli) spowoduje wypisywanie informacji diagnostycznych indexer'a; błędy syntaktyczne, które raportuje indexer, uniemożliwiające poprawne zbieranie wyglądu struktur, będą wyświetlone między tymi informacjami, więc warto nauczyć się to wyjście interpretować.
Poprawna konfiguracja indexera polega na poprawnym wpisaniu ścieżek include do opcji projektu, we Właściwościach projektu → C/C++ General → Paths and Symbols. Należy tylko zainteresować się ścieżkami GNU C, ponieważ nie będziemy używać C++. Jako, że na systemie natywnym będziesz kompilować sterownik dla kernela natywnego (w wersji natywnej), ścieżki include powinny zostać pobrane właśnie z niego: /usr/src/linux/include oraz /usr/src/linux/arch/x86/include powinny załatwić sprawę.
Oprócz tego, w zakładce Symbols dodaj symbol o nazwie __KERNEL__, której przyporządkuj liczbę 1. Pytanie o przebudowę indeksu potwierdź przyciskiem YES i zaobserwuj wyjście diagnostyczne w konsoli, z której uruchomiłeś/uruchomiłaś Eclipse.
Można teraz przetestować indexer ;), czyli np. napisz definicję prostej struktury zaraz za #include'm:
- typedef struct {
- int a;
- } AA;
oraz w funkcji main napisz:
- AA a;
Być może zdarzyć się taka sytuacja, gdzie po napisaniu „a.” i kliknięciu Ctrl+spacja nie wyskoczy okienko z podpowiedzią składowych struktury o typie AA. Winą jest indexer; nie zebrał informacji o składowych struktury, zadeklarowanej kilka linijek wyżej. Spróbujmy dowiedzieć się, jaki jest tego powód.
Ten przykładowy problem indexer'a zawiera kilka informacji w wyjściu diagnostycznym:
- Indexer: start TriggerNotificationTask
- Indexer: completed TriggerNotificationTask[5ms]
- Indexer: start TriggerNotificationTask
- Indexer: completed TriggerNotificationTask[4ms]
- Indexer: start TriggerNotificationTask
- Indexer: completed TriggerNotificationTask[1ms]
- Indexer: start PDOMFastIndexerTask
- Indexer: parsing /adx-tutorial/adxmain.c
- Indexer: Syntax error in file: /home/antek/workspacepp/adx-tutorial/adxmain.c:7
- Indexer: adding file:/home/antek/workspacepp/adx-tutorial/adxmain.c
- C/C++ Indexer: Project 'adx-tutorial' (1 sources, 0 headers)
- Options: indexer='PDOMFastIndexer', parseAllFiles=true, unusedHeaders=useDefaultLanguage, skipReferences=false, skipImplicitReferences=false, skipTypeReferences=false, skipMacroReferences=false.
- Database: 815104 bytes
- Timings: 95 total, 50 parser, 0 resolution, 18 index update.
- Errors: 0 internal, 0 include, 0 scanner, 1 syntax errors.
- Names: 5 declarations, 3 references, 0(0,00%) unresolved.
- Cache[910MB]: 25640 hits, 14(0,05%) misses.
- Indexer: completed PDOMFastIndexerTask[96ms]
Błąd znajduje się w linii, która zawiera string Syntax error ;) i dotyczy linii 7 pliku adxmain.c. Znajduje się w niej deklaracja funkcji inicjującej moduł, razem ze specyfikatorem __init – zapewne to powoduje błąd, ponieważ indexer nie zna tego symbolu. Kliknięcie F3 na tym specyfikatorze pokazuje nam, że jego definicja znajduje się w pliku linux/init.h, którego nie includujemy w swoim pliku źródłowym. Dodaj więc odpowiedni #include
Jak już zapewnie zauważyłeś/zauważyłaś, setup opisany przeze mnie powyżej posiada pewną charakterystyczną cechę programowania pod dwie wersje kernela; jedna wersja znajduje się na systemie natywnym, druga wersja na systemie wirtualnym. Posiada to pozytywny skutek uboczny, którym jest zwracanie uwagi na różnicę w strukturach między różnymi wersjami kernela. Sam kernel zmienia się bardzo szybko (jeśli nie jest to aktualnie najbardziej zmieniający się produkt dostępny na rynku), dlatego zwracanie uwagi na poprawną interpretację API, które np. oznaczone są jako przestarzałe, jest bardzo ważne. Samo programowanie sterownika nie jest jedynym aspektem na który trzeba zwracać uwagę – równie ważne jest utrzymanie kodu, które im starszy jest projekt, tym więcej czasu zajmuje. Kod nieutrzymywany z czasem traci swoją ważność – czasami wcześniej, czasami później, jednak nie należy się do niego przywiązywać ;). Jak pisałem też wcześniej, konfiguracja opisana powyżej służy w sumie tylko mi i nie mam dowodów na to, że będzie wygodna dla kogokolwiek innego. Zawiera też ogromną ilość rzeczy, które można usprawnić, przykładowo: usunąć manager'a X'ów i bootować tylko do linii poleceń; skrócić i wyrzucić niepotrzebne usługi ładowane przy starcie, aby jeszcze bardziej zmniejszyć czas ładowania systemu po kernel panic'u. Można napisać też serię skryptów które będą ułatwiać pracę z Twoim sterownikiem, ale treść tych skryptów zależy w sumie od tego, co ten sterownik miałby robić.
Tutaj dochodzimy do sensu programowania w trybie jądra; mianowicie, co i w jaki sposób można czegoś dokonać ;). W kolejnym poście opiszę szybko krótki sterownik, który można zaprogramować; będzie on pobierał pewne dane z kernela, które przydać się mogą w trybie user-mode, a nie są eksportowane przez system plików /proc. Sterownik korzystać będzie ze standardowych mechanizmów stosowanych w tym wirtualnym systemie plików i będzie tworzył nowy „plik”, który można odczytać cat'em. Zupełnie jak np. /proc/cpuinfo. Do następnego więc ;)


