======================= Zajęcia 7: Moduły jądra ======================= Data: 12.04.2022 .. contents:: .. toctree:: :hidden: zadanie Materiały dodatkowe =================== - :download:`examples.tar` -- przykładowe moduły - :ref:`06-zadanie` Co to jest moduł? ================= Moduł to relokowalny kod/dane, które mogą być wstawiane i usuwane z jądra w czasie działania systemu. Moduł może odwoływać się do (eksportowanych) symboli jądra tak, jakby był skompilowany jako cześć jądra oraz sam może udostepniać (eksportować) symbole, z ktorych mogą korzystać inne moduły. Moduł odpowiada za pewną określoną usługę w jądrze -- np. modułami są sterowniki urządzeń i systemów plików, filtry sieciowe, algorytmy kryptograficzne, itp. Moduły są kompilowane pod konkretną wersję i konfigurację jądra -- użycie modułów z innych wersji jądra (bądź tej samej wersji ze znacząco różniącą się konfiguracją) prawdopodobnie się nie uda. System ładowania modułów spróbuje wykryć i uniemożliwić taką sytuację. Programy i pliki związane z zarządzaniem modułami w Linuksie ============================================================ ``insmod nazwa_modulu.ko [parametry]`` Ładuje podany plik modułu do jądra. Jeśli są podane parametry, to przekazuje je modułowi. Należy podać pełną ścieżkę do modułu -- ``insmod`` nie próbuje samemu szukać potrzebnego pliku. Parametry maja postać zmienna=wartość np.:: insmod ne.ko io=0x300 irq=7 ``modprobe nazwa_modulu [parametry]`` Przyjazna użytkownikowi nakładka na ``insmod`` - ładuje moduł, samemu znajdując go w ``/lib/modules`` i ładując wszystkie potrzebne mu do dzialania zależności. W tym celu wykorzystywana jest baza danych o zależnosciach między modułami utworzona za pomocą ``depmod`` (patrz poniżej). Moduły szukane są standardowo w katalogu ``/lib/modules/``. ``depmod -a`` Tworzy bazę danych zależności pomiędzy modułami dla aktualnego jądra. Zależności zostaną wpisane do pliku ``/lib/modules//modules.dep``. ``/etc/modprobe.conf`` i/lub ``/etc/modprobe.d/*`` Pliki sterujące zachowaniem ``modprobe`` i ``depmod``. Tradycyjnie był jeden plik konfiguracyjny. Obecnie ze względu na łatwość modyfikacji używa się katalogu ``/etc/modprobe.d/``, w którym umieszcza się pliki zawierające opcje. W ten sposób można łatwo dodać opcje, np. gdy instalujemy jakieś urządzenie, bez konieczności modyfikacji pliku. Najważniejsze polecenia: ``alias nazwa nazwa_modulu`` Definiuje, że moduł nazwa_modulu ma byc załadowany, gdy zażąda się załadowania modułu nazwa, np. :: alias eth0 ne2k-pci powoduje załadowanie odpowiedniego modułu karty sieciowej gdy zażąda się załadowania modułu ``eth0``. ``options nazwa_modulu opcje`` Powoduje ustawienie podanych opcji przy każdym żądaniu załadowania danego modułu, np. :: options ne io=0x300 irq=10 spowoduje użycie opcji ``io=0x300 irq=10`` przy każdym ładowaniu moduło ``ne``. ``install nazwa_modulu polecenia...`` Powoduje wykonanie polecenia powłoki zamiast ładowania danego modułu. Możliwe jest również załadowanie modułu lub kilku modułów przez polecenie, np. :: install foo /sbin/modprobe bar; /sbin/modprobe --ignore-install foo $CMDLINE_OPTS Opcja ``--ignore-install`` jest konieczna, by zapobiec zapętleniu przy ładowaniu modułu foo, powoduje zignorowanie opcji install. Parametr ``$CMDLINE_OPTS`` zostanie zastąpiony opcjami podanymi w wywołaniu modprobe lub dołączonymi za pomocą poleceń options. Polecenie install przydaje się również do innych sztuczek, np. ładowania firmware po załadowaniu modułu. Możliwe jest też załadowanie pierwszego pasującego modułu za pomocą konstrukcji:: install probe-ethernet /sbin/modprobe e100 || /sbin/modprobe eepro100 Pierwszy moduł, który się pomyślnie załaduje powoduje zaprzestanie dalszego sprawdzania. W tym wypadku jest to pierwszy pasujący moduł do karty sieciowej. ``blacklist nazwa_modulu`` Powoduje, że moduł nie będzie automatycznie ładowany (np. przez udev), przydaje się w przypadku zabugowanych nieużywanych przez nas sterowników lub modułów do debugowania (np. ``evbug``) ``rmmod nazwa_modulu`` Usuwa podany moduł z jądra (``nazwa_modulu`` to nazwa modułu, a nie nazwa pliku ``.ko``). Jądro automatycznie śledzi, które moduły są obecnie aktywnie używane (np. są zależnościami innych modułów, kontrolują zamontowany system plików, obsługują urządzenie otwarte przez jakiś proces) i odmawia usunięcia ich. Jeżeli bardzo chcemy usunąć używany moduł, można użyć opcji ``-f``, ale to zazwyczaj bardzo źle się kończy. ``lsmod`` Wypisuje wszystkie załadowane moduły wraz z informacją o ich zależnościach (ten sam wynik daje ``cat /proc/modules``). ``modinfo nazwa_modulu_lub_nazwa_pliku`` Wypisuje opis modułu wraz z listą parametrów. Tworzenie modułów ================= Moduły jądra (jak i główny kod jądra) są pisane w języku C (użycie innych języków nie jest możliwe). Środowisko wewnątrz jądra jest jednak dość charakterystyczne i różni się poważnie od pisania zwykłego programu w przestrzeni użytkownika. W jądrze przyjęło się pisać zgodnie z oficjalnym stylem kodowania -- https://www.kernel.org/doc/html/v4.15/process/coding-style.html . Kompilacja modułów ------------------ Do komplacji modułów potrzebny jest katalog ze skonfigurowanymi i skompilowanymi źródłami jądra. W zasadzie wystarczą same pliki nagłówkowe i konfiguracja, ale oddzielenie odpowiednich plików od reszty jest bardzo skomplikowanym procesem i tylko dystrybucje Linuxa z dużą ilością własnych skryptów są w stanie to zrobić. Za kompilację modułów (jak i samego jądra) odpowiedzialny jest system Kbuild, będący dośc skomplikowaną nakładką na Makefile. Aby skompilować nasz moduł, musimy stworzyć plik ``Kbuild`` opisujący nasz kod, na przykład:: obj-m := modul.o inny_modul.o skompiluje plik ``modul.c`` do modułu ``modul.ko``, a plik ``inny_modul.c`` do pliku ``inny_modul.ko``. Jeśli chcemy połączyć kilka plików źródłowych w jeden moduł, możemy to zrobić następująco:: obj-m := modul.o modul-objs := modul_p1.o modul_p2.o Taki plik Kbuild skompiluje pliki ``modul_p1.c`` i ``modul_p2.c`` i połączy je w moduł ``modul.ko``. Aby wywołać kompilację modułu, należy wywołać ``make`` w katalogu ze źródłami jądra, wskazując mu nasz katalog z zewnętrznymi modułami:: make -C /usr/src/linux- M=/home//moje_moduly Dla ułatwienia, można napisać własny ``Makefile`` wywołujący odpowiednie polecenie (patrz przykład). Metadane modułu --------------- Każdy moduł może (ale nie musi) definiować metadane za pomocą makr (zdefiniowanych w ``linux/module.h``):: MODULE_LICENSE("GPL"); MODULE_AUTHOR("Koń Fred"); MODULE_DESCRIPTION("Sterownik do mojego urządzenia"); Tak zdefiniowane metadane przechowywane są (wraz z wieloma innymi danymi) w sekcji ``.modinfo`` gotowego modułu i można je wypisać poleceniem ``modinfo``. Wybór licencji ma ważny i nieoczywisty efekt -- użycie licencji zgodnej z GPL pozwoli nam używać symboli jądra oznaczonych jako dostępne tylko dla modułów na licencji GPL. Jako zgodne licencje rozpoznawane są: - ``"GPL"`` -- GNU Public License v2 lub późniejsza, - ``"GPL v2"`` -- GNU Public License v2, - ``"GPL and additional rights"`` -- prawa GNU Public License v2 + dodatkowe, - ``"Dual BSD/GPL"`` -- GNU Public License v2 lub licencja BSD do wyboru, - ``"Dual MPL/GPL"`` -- GNU Public License v2 lub Mozilla do wyboru, - ``"Dual MIT/GPL"`` -- GNU Public License v2 lub MIT do wyboru. Konstruktor i destruktor modułu ------------------------------- Moduły nie mają funkcji ``main`` ani własnego procesu/wątku (chyba, że sobie go stworzą, ale to dość rzadkie). Zamiast tego, kod modułu jest wywoływany przez różne podsystemy jądra, gdy jest dla niego coś do zrobienia. Każdy moduł może definiować funkcję inicjującą moduł (konstruktor) i zwalniająca moduł (destruktor). Standardowo funkcje te muszą być zdefiniowane w następujący sposób:: int funkcja_inicjujaca(void) { /* ... */ } void funkcja_zwalniajaca(void) { /* ... */ } module_init(funkcja_inicjujaca); module_exit(funkcja_zwalniajaca); Funkcja inicjująca jest wywoływana przy ładowaniu modułu. Jeśli wszystko się udało, powinna zwrócić 0. Jeśli nie udało się zainicjować modułu, powinna zwrócić kod błędu (zanegowany kod z ``errno*.h``) -- moduł zostanie wtedy natychmiast usunięty przez jądro. Funkcja zwalniająca jest wywoływana przy usuwaniu modułu (ale nie jest wywoływana, gdy funkcja inicjująca zwróciła błąd). Zadaniem funkcji inicjującej jest "wpięcie" funkcjonalności dostarczanej przez moduł w struktury jądra -- na przykłąd sterownik urządzenia PCI będzie w tej funkcji informował podsystem PCI o obsługiwanych urządzeniach i funkcjach, które powinien wywołać w razie wykrycia pasującego urządzenia. Bez takiej rejestracji, jądro nigdy nie wywoła kodu naszego modułu, więc moduły bez funkcji inicjującej są użyteczne w zasadzie jedynie jako biblioteczki funkcji dla innych modułów. Zadaniem funkcji zwalniającej jest odwrócenie wszystkiego, co zrobiła funkcja inicjująca i posprzątanie po całej działalności modułu. Jeżeli moduł ma funkcję inicjującą, zawsze należy dostarczyć też funkcję zwalniającą (choćby miała być pusta) -- w przeciwnym wypadku, jądro uzna, że nasz moduł nie obsługuje usuwania i nie pozwoli wykonać na nim ``rmmod``. Czasami można spotkać starsze moduły używające funkcji o domyślnych nazwach ``init_module()`` i ``cleanup_module()``, bez deklarowania ich przez ``module_init()`` i ``module_exit()``. Nie jest to zalecane w obecnych wersjach jądra. Moduł powinien mieć tylko jeden konstruktor i tylko jeden destruktor. Pierwszy przykładowy moduł pokazuje użycie ``printk`` oraz konstruktora i destruktora. Korzystanie z symboli zewnętrznych ---------------------------------- W modułach można dowolnie używać symboli zdefiniowanych i wyeksportowanych przez główny kod jądra oraz przez inne moduły (można je obejrzeć w pliku ``/proc/kallsyms``). Aby symbol naszego modułu był widoczny z zewnątrz, należy go wyeksprotować makrem ``EXPORT_SYMBOL``:: EXPORT_SYMBOL(moja_funkcja); int moja_funkcja(int x) { ... } Istnieje również analogiczne makro ``EXPORT_SYMBOL_GPL``, eksportujące symbol tylko dla modułów na licencji GPL (bądź kompatybilnej). Program ``depmod`` automatycznie zbiera informacje o zależnościach między modułami wynikających z użycia wyeksportowanych symboli i zapewni, żeby były ładowane w odpowiedniej kolejnośći. Drugi przykładowy moduł pokazuje eksportowanie symboli oraz użycie wyeksportowanych symboli. Parametryzacja modułów ---------------------- Można zadeklarować, że określona zmienna będzie zawierała parametr, ktory może zostać zmieniony przy ładowaniu modułu. Nazwa parametru jest taka sama jak nazwa zmiennej. W czasie ładowania modułu w miejsce podanych zmiennych zostaną wstawione wartości podane przez użytkownika (jeśli je poda), np. :: insmod modul.ko irq=5 podstawi w miejsce zmiennej ``irq`` wartosc 5. Do deklaracji, że pewna zmienna ma być wykorzysta jako parametr modułu służy makro:: module_param(zmienna, typ, uprawnienia); Typami moga być: ``byte``, ``short``, ``ushort``, ``int``, ``uint``, ``long``, ``ulong``, ``charp``, ``bool``, ``invbool``. Typ ``charp`` jest używany do przekazywania napisów (``char *``). Typ ``invbool`` oznacza parametr ``bool``, który jest zaprzeczeniem wartości. Można definiować własne typy parametrów, trzeba wówczas zdefiniować również funkcje ``param_get_XXX``, ``param_set_XXX`` i ``param_check_XXX``. Uprawnienia oznaczają uprawnienia, które zostaną nadane parametrowi w ``sysfs``. Każdy parametr powinien posiadać opis. Opis parametru można potem odczytać wraz z opisem całego modułu za pomocą programu ``modinfo``, dzięki czemu moduł niesie ze sobą opis użycia. Opis nadaje się za pomocą makra ``MODULE_PARM_DESC``:: MODULE_PARM_DESC(zmienna, opis) Przykłady:: int irq = 7; module_param(irq, int, 0); MODULE_PARM_DESC(irq, "Irq used for device"); char *path="/sbin/modprobe"; module_param(path, charp, 0); MODULE_PARM_DESC(path, "Path to modprobe"); Użycie:: printk(KERN_INFO "Using irq: %d", irq); printk(KERN_INFO "Will use path: %s", path); Aby zadeklarować tablicę parametrów trzeba użyć innego makra:: module_param_array(zmienna, typ, wskaznik_na_licznik, uprawnienia) Wszystkie pola poza ``wskaznik_na_licznik`` mają takie same znaczenie jak w ``module_param()``. ``wskaznik_na_licznik`` zawiera wskaźnik do zmiennej do której wpisana zostanie liczba elementów tablicy. Jeśli nie interesuje nas liczba argumentów, można podać ``NULL``, ale wtedy trzeba rozpoznawać, czy argument jest czy, nie na podstawie jego zawartości, co nie jest wskazane. Maksymalna liczba elementów tablicy jest określona przez deklarację tablicy, np. jeśli zadeklarujemy jej rozmiar na 4, to użytkownik będzie mógł przekazać maksymalnie 4 elementy. W opisie parametru tablicowego zwyczajowo umieszcza się w nawiasach kwadratowych maksymalną liczbę parametrów. Przykład:: int num_paths = 2; char *paths[4] = {"/bin", "/sbin", NULL , NULL}; module_param_array(paths, charp, &num_paths, 0); MODULE_PARM_DESC(paths, "Search paths [4]"); Użycie:: int i; for (i=0; i Doładowanie modułu jest możliwe dzięki funkcji:: int request_module(const char *module_name) Licznik odwołań --------------- Każdy moduł ma swój licznik odwołań -- dopóki jest on dodatni, jądro nie pozwoli na usunięcie modułu. Powinien być on zwiększany, gdy nasz moduł jest w aktywnym użyciu (np. obsługuje otwarte urządzenie czy zamontowany system plików). Zarządzaniem takim licznikiem zazwyczaj zajmują się inne podsystemy jądra, lecz trzeba im w tym pomóc, przekazując wskaźnik na nasz moduł (makro ``THIS_MODULE``). Na przykład w przypadku sterownika urządzenia znakowego, trzeba wypełnić pole ``owner`` struktury ``file_operations`` tym wskaźnikiem. Ćwiczenia wprawkowe =================== - Skompilować i uruchomić przykładowe moduły. - Zbadać doświadczalnie maksymalny rozmiar, który można zaalokować za pomocą ``kmalloc``. - Przerobić przykład 4 tak, by działał dla większych buforów (za pomocą ``vmalloc``). - Znaleźć i wyjaśnić dziurę bezpieczeństwa w jednym z kodów przykładowych. Zastanowić się nad konsekwencjami tego typu błędów w kodzie jądra. Literatura ========== 1. ``man insmod``, ``rmmod``, ``lsmod``, ``modprobe``, ``depmod``, ``modinfo`` 2. A. Rubini, J. Corbet "Linux Device Drivers" 2nd Edition, O'Reilly 2001, rozdział II i XI - http://www.xml.com/ldd/chapter/book 3. Peter Salzman, Ori Pomerantz "The Linux Kernel Module Programming Guide", 2001 - http://www.faqs.org/docs/kernel 4. http://tldp.org/HOWTO/Module-HOWTO/ 5. http://tldp.org/LDP/lkmpg/2.6/html/index.html 6. ``Documentation/kbuild/makefiles.txt``, ``modules.txt`` .. ========================================================================== Autor: Grzegorz Marczyński (g.marczynski@mimuw.edu.pl) Aktualizacja: 2004-10-19 ========================================================================== Maria Fronczak (marys@mimuw.edu.pl) Aktualizacja: 23.10.2005 ========================================================================== Krzysztof Lichota (lichota@mimuw.edu.pl) Aktualizacja do jądra 2.6.17.13: 8.11.2006 Poprawka /proc/ksyms na /proc/kallsyms: Jan Urbański ========================================================================== Krzysztof Lichota (lichota@mimuw.edu.pl) Aktualizacja do jądra 2.6.33.4, przeróbka na Slackware i dodanie p4_kmalloc: 21.03.2011 ========================================================================== Marcelina Kościelnicka (mwk@mimuw.edu.pl) Drobne poprawki, dziura, konstruktory/destruktory, podstawowe funkcje. 09.02.2012 ========================================================================== Marek Dopiera (dopiera@mimuw.edu.pl) Przepisanie makefile-i. 12.03.2012