Do jądra systemu Linux na stałe wkompilowane są tylko najważniejsze sterowniki podstawowych urządzeń (na przykład dyski twarde), gdyż umieszczanie tam wszystkich to strata pamięci a przede wszystkim czasu na uruchomienie i wyłączenie się sterowników do urządzeń nieistniejących w danym komputerze. Dlatego sterowniki do urządzeń opcjonalnych umieszczono w modułach jądra, ładowanych przez system na żądanie.
Spis treści:
Moduł jądra to najzwyklejszy skompilowany plik w standardowym formacie ELF. Musi eksportować na zewnątrz dwie funkcje: init_module, służącą do inicjalizacji modułu (i uruchamianą w czasie jego ładowania) oraz cleanup_module, służącą do wykonania czynności koniecznych do prawidłowego zakończenia pracy (uruchamianą w czasie usuwania modułu z jądra).
Funkcja init_module musi być tak napisana, że w przypadku sukcesu zwraca zero, a w przypadku porażki - najlepiej jedną ze znanych ujemnych wartości błędu, która dobrze będzie opisywać problem.
Sporo informacji dotyczących jądra 2.4 przenosi się na jądro 2.6, więc w sekcji poświęconej jądru 2.6 powiem tylko, co się zmieniło w stosunku do 2.4.
Zgodnie z tym, co powiedziałem wyżej, najprostszy moduł wygląda tak:
format ELF section ".text" executable ; początek sekcji kodu ; eksportowanie dwóch wymaganych funkcji public init_module public cleanup_module ; deklaracja zewnętrznej funkcji, służącej do wyświetlania extrn printk init_module: push dword napis1 ; napis do wyświetlenia call printk pop eax ; zdejmujemy argumenty ze stosu xor eax, eax ; zero oznacza brak błędu ret cleanup_module: push dword napis2 call printk pop eax ret section ".data" writeable napis1 db "<1> Jestem w init_module." , 10, 0 napis2 db "<1> Jestem w cleanup_module.", 10, 0 section ".modinfo" __module_kernel_version db "kernel_version=2.4.26", 0 __module_license db "license=GPL", 0 __module_author db "author=Bogdan D.", 0 __module_description db "description=Pierwszy modul jadra.", 0Zauważcie kilka spraw:
W skrócie: adres napisu podajemy na stosie, poprzedzając dodatkowymi danymi w odwrotnej kolejności, jeśli funkcja w ogóle ma wyświetlić jakieś zmienne w napisie, na przykład %d (liczba całkowita). Będzie to dokładniej pokazane na przykładowym module.
Napis powinien się zaczynać wyrażeniem <N>, gdzie N to pewna liczba. Ma to pozwolić jądru rozróżnić powagę wiadomości. Nam wystarczy za N wstawiać 1.
Jeśli wyświetlanych napisów nie widać na ekranie, to na pewno pojawią się po
komendzie dmesg
(zwykle na końcu) oraz w pliku /var/log/messages.
Moduły kompilowane NASMem z niewiadomych przyczyn nie chciały mi wchodzić do jądra.
Każda funkcja jądra uruchamiana jest w konwencji C, czyli my sprzątamy argumenty ze stosu.
modinfo
.
Zawiera informacje, dla której wersji jądra moduł jest przeznaczony, kto jest jego autorem, na jakiej jest licencji, argumenty. Nazwy zmiennych muszą pozostać bez zmian, treść po znakach równości powinniście pozmieniać według potrzeb.
Moduł ten, po kompilacji (fasm modul_hello.asm
) instaluje się jako
root komendą
insmod ./modul_hello.o
a usuwa z jądra - komendą
rmmod modul_hello
(zauważcie brak rozszerzenia .o).
Listę modułów obecnych w jądrze można otrzymać komendą lsmod
.
Pokażę teraz, jak zarejestrować urządzenie znakowe, zająć dla niego zasoby IRQ oraz zakres portów i pamięci.
Do rejestracji urządzenia znakowego (czyli takiego, z którego można odczytywać po bajcie,
w przeciwieństwie do na przykład dysku twardego) służy eksportowana przez jądro funkcja
register_chrdev
. Przyjmuje ona 3 argumenty. Od lewej (ostatni
wkładany na stos) są to:
Można podać zero, wtedy jądro przydzieli nam jakiś wolny. Numer główny to pierwszy z dwóch numerów (drugi to poboczny), widoczny w szczegółowym widoku plików z katalogu /dev, na przykład
crw-rw-rw- 1 root root 1, 5 sie 16 15:28 /dev/zero
Urządzenie /dev/zero ma numer główny 1 i poboczny 5, litera C na początku oznacza właśnie urządzenie znakowe. Inne oznaczenia to D (katalog), S (gniazdo), B (urządzenie blokowe), P (FIFO), L (dowiązanie symboliczne).
Adres nazwy urządzenia w postaci ciągu znaków zakończonego bajtem zerowym.
Najważniejsze są: otwieranie, zamykanie, zapis i czytanie z urządzenia. Sama struktura wygląda tak dla jądra 2.4:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file*, char*, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t,
loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *,
struct poll_table_struct *);
int (*ioctl) (struct inode*, struct file*, unsigned int,
unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file*,struct dentry*, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *,
unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *,
unsigned long, loff_t *);
};
Każde pole tej struktury to DWORD. Do podstawowej funkcjonalności wystarczy wypełnić pola: trzecie, czwarte, dziewiąte i jedenaste (zamykanie pliku). Jeśli jakiejś funkcji nie planujemy pisać, należy na odpowiadające jej miejsce w tej strukturze wpisać zero.
Jeśli podaliśmy tej funkcji nasz własny numer główny urządzenia, to jeśli rejestracja się
udała, register_chrdev
zwróci zero w EAX. Jeśli poprosiliśmy o przydzielenie
nam numeru głównego, to jeśli rejestracja się powiedzie, register_chrdev
zwróci liczbę większą od zera, która to liczba będzie przeznaczonym dla naszego urządzenia
numerem głównym.
UWAGA: Funkcja register_chrdev
nie tworzy
pliku urządzenia w katalogu /dev. O to musimy zadbać sami, po załadowaniu modułu.
Wyrejestrowanie urządzenia znakowego następuje poprzez wywołanie funkcji
unregister_chrdev
. Pierwszy argument od lewej (ostatni na stos)
to przydzielony urządzeniu numer główny, a drugi - adres nazwy urządzenia.
Zarezerwowanie tych zasobów jest dość łatwe. Należy tylko uruchomić funkcję
__request_region
. Przyjmuje ona 4 argumenty. Od lewej (ostatni
wkładany na stos) są to:
ioport_resource
, jeśli pamięć - iomem_resource
.
Obie zmienne są eksportowane przez jądro, więc można je zadeklarować
jako zewnętrzne dla modułu.W przypadku niepowodzenia, funkcja zwraca zero (w EAX).
Oba te rodzaje zasobów zwalnia się funkcją __release_region
. Jako
swoje argumenty przyjmuje ona 3 pierwsze z powyższych (typ oraz początek i długość zakresu).
Zasoby żądania przerwania (IRQ)
rejestruje się funkcją request_irq
.
Przyjmuje ona aż 5 argumentów typu DWORD. Od lewej (ostatni wkładany na stos) są to:
void handler (int irq, void *dev_id, struct pt_regs *regs);
Jak widać, będzie można ze stosu otrzymać informacje, które przerwanie zostało wywołane
oraz przez jakie urządzenie. Ostatni argument podobno jest już rzadko używany.Jeśli zajęcie przerwania się nie powiedzie, funkcja zwróci wartość ujemną.
Zwolnienie przerwania odbywa się poprzez wywołanie funkcji free_irq
.
Jej pierwszy argument od lewej (ostatni na stos) to nasz numer IRQ, a drugi - adres
naszej struktury file_operations.
Pokazany niżej program zarejestruje programowe urządzenie znakowe (czyli takie, dla którego nie ma odpowiednika w sprzęcie, jak na przykład /dev/null) z IRQ 4, zakresem portów 600h-6FFh, zakresem pamięci 80000000h - 8000FFFFh oraz z podstawowymi operacjami: otwieranie, zamykanie, czytanie, zapis, zmiana pozycji. Dla uproszczenia kodu nie sprawdzam, czy dane zakresy są wolne. Jeśli okażą się zajęte, jądro zwróci błąd i moduł się nie załaduje.
; Przykładowy moduł jądra 2.4 ; ; Autor: Bogdan D., bogdandr (na) op . pl ; ; kompilacja: ; fasm modul_dev_fasm.asm format ELF section ".text" executable ; eksportowanie wymaganych funkcji public init_module public cleanup_module ; importowanie używanych funkcji i symboli extrn printk extrn register_chrdev extrn unregister_chrdev extrn request_irq extrn free_irq extrn __check_region extrn __request_region extrn __release_region extrn ioport_resource extrn iomem_resource ; zakresy zasobów, o które poprosimy jądro PORTY_START = 0x600 PORTY_ILE = 0x100 RAM_START = 0x80000000 RAM_ILE = 0x00010000 ; stałe potrzebne do rezerwacji przerwania IRQ. SA_INTERRUPT = 0x20000000 NUMER_IRQ = 4 ; funkcja inicjalizacji modułu init_module: pushfd ; rejestrowanie urządzenia znakowego: push dword file_oper push dword nazwa push dword 0 ; numer przydziel dynamicznie call register_chrdev add esp, 3*4 ; usuwamy argumenty ze stosu cmp eax, 0 ; sprawdzamy, czy błąd jg .dev_ok ; tu wiemy, że jest błąd. wyświetlmy to. push eax ; argument do informacji o błędzie push dword dev_blad ; adres informacji o błędzie call printk ; wyświetl informację o błędzie add esp, 1*4 ; specjalnie usuwam tylko 1*4 pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .dev_ok: mov [major], eax ; rezerwacja portów wejścia-wyjścia push dword nazwa push dword PORTY_ILE push dword PORTY_START push dword ioport_resource call __request_region add esp, 4*4 test eax, eax ; sprawdzamy, czy błąd jnz .iop_ok push eax ; argument do informacji o błędzie push dword porty_blad ; adres informacji o błędzie call printk ; wyświetl informację o błędzie add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia push dword nazwa push dword [major] call unregister_chrdev add esp, 2*4 pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .iop_ok: ; rezerwacja pamięci push dword nazwa push dword RAM_ILE push dword RAM_START push dword iomem_resource call __request_region add esp, 4*4 test eax, eax ; sprawdzamy, czy błąd jnz .iomem_ok push eax push dword ram_blad call printk ; wyświetl informację o błędzie add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia push dword nazwa push dword [major] call unregister_chrdev add esp, 2*4 ; zwolnienie zajętych przez nas portów push dword PORTY_ILE push dword PORTY_START push dword ioport_resource call __release_region add esp, 3*4 pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .iomem_ok: ; przydzielanie przerwania IRQ: push dword file_oper push dword nazwa push dword SA_INTERRUPT push dword obsluga_irq push dword NUMER_IRQ call request_irq add esp, 5*4 cmp eax, 0 jge .irq_ok push eax push dword irq_blad call printk ; wyświetl informację o błędzie add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia push dword nazwa push dword [major] call unregister_chrdev add esp, 2*4 ; zwolnienie zajętych przez nas portów push dword PORTY_ILE push dword PORTY_START push dword ioport_resource call __release_region add esp, 3*4 ; zwolnienie zajętej przez nas pamięci push dword RAM_ILE push dword RAM_START push dword iomem_resource call __release_region add esp, 3*4 pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .irq_ok: ; wyświetlenie informacji o poprawnym uruchomieniu modułu push dword NUMER_IRQ push dword [major] push dword uruch call printk add esp, 3*4 xor eax, eax ; zero - brak błędu .koniec: popfd ret ; funkcja uruchamiana przy usuwaniu modułu cleanup_module: pushfd push eax ; zwolnienie numeru IRQ: push dword file_oper push dword NUMER_IRQ call free_irq add esp, 2*4 ; wyrejestrowanie urządzenia: push dword nazwa push dword [major] call unregister_chrdev add esp, 2*4 ; zwolnienie zajętych przez nas portów push dword PORTY_ILE push dword PORTY_START push dword ioport_resource call __release_region add esp, 3*4 ; zwolnienie zajętej przez nas pamięci push dword RAM_ILE push dword RAM_START push dword iomem_resource call __release_region add esp, 3*4 ; wyświetlenie informacji o usunięciu modułu push dword usun call printk add esp, 1*4 pop eax popfd ret ; nasza funkcja obsługi przerwania. Ta tutaj nie robi nic, ale ; pokazuje rozmieszczenie argumentów na stosie obsluga_irq: push ebp mov ebp, esp ; [ebp] = stary EBP ; [ebp+4] = adres powrotny ; [ebp+8] = arg1 ; ... irq equ ebp+8 dev_id equ ebp+12 regs equ ebp+16 leave ret ; Zdefiniowane operacje na urządzeniu ; Czytanie z urządzenia - zwracamy żądanej długości ciąg bajtów 1Eh. ; To urządzenie staje się nieskończonym źródłem, podobnie jak /dev/zero czytanie: push ebp mov ebp, esp ; rozmieszczenie argumentów na stosie: s_file equ ebp+8 ; wskaźnik na strukturę file bufor equ ebp+12 ; adres bufora na dane l_jedn equ ebp+16 ; żądana liczba bajtów loff equ ebp+20 ; żądany offset czytania pushfd push edi push ecx mov ecx, [l_jedn] mov al, 0x1e cld mov edi, [bufor] rep stosb ; zapychamy bufor bajtami 1Eh pop ecx pop edi popfd mov eax, [l_jedn] ; zwracamy tyle, ile chciano leave ret zapis: push ebp mov ebp, esp ; nic fizycznie nie zapisujemy, zwracamy tylko liczbę bajtów, ; którą mieliśmy zapisać mov eax, [l_jedn] leave ret przejscie: zamykanie: otwieranie: xor eax, eax ret section ".data" writeable major dd 0 ; numer główny urządzenia przydzielany przez jądro ; adresy funkcji operacji na tym urządzeniu file_oper: dd 0, przejscie, czytanie, zapis, 0, 0, 0, 0, otwieranie, 0 dd zamykanie, 0, 0, 0, 0, 0 dev_blad db "<1>Blad rejestracji urzadzenia: %d.", 10, 0 irq_blad db "<1>Blad przydzielania IRQ: %d.", 10, 0 porty_blad db "<1>Blad przydzielania portow: EAX=%d", 10, 0 ram_blad db "<1>Blad przydzielania pamieci: EAX=%d", 10, 0 uruch db "<1>Modul zaladowany. Maj=%d, IRQ=%d", 10, 0 usun db "<1>Modul usuniety.", 10, 0 nazwa db "test00", 0 sciezka db "/dev/test00", 0 section ".modinfo" __module_kernel_version db "kernel_version=2.4.26", 0 __module_license db "license=GPL", 0 __module_author db "author=Bogdan D.", 0 __module_description db "description=Pierwszy modul jadra", 0 __module_device db "device=test00", 0
Powyższy moduł po kompilacji najprościej zainstalować w jądrze stosując taki oto skrypt:
#!/bin/bash PLIK="modul_dev_fasm.o" # Tu wstawiacie swoją nazwę NAZWA="test00" # umieszczenie modułu w jądrze. /sbin/insmod $PLIK $* || { echo "Problem insmod!" ; exit -1; } # wyszukanie naszej nazwy modułu wśród wszystkich /sbin/lsmod | grep `echo $PLIK | sed 's/[^a-z]/ /g' | awk '{print $1}' ` # wyświetlenie informacji o zajmowanych zasobach grep $NAZWA /proc/devices grep $NAZWA /proc/ioports grep $NAZWA /proc/iomem grep $NAZWA /proc/interrupts # znalezienie i wyświetlenie numeru głównego urządzenia NR=`grep $NAZWA /proc/devices | awk '{print $1}'` echo "Major = $NR" # ewentualne usunięcie starego pliku urządzenia rm -f /dev/$NAZWA # fizyczne utworzenie pliku urządzenia w katalogu /dev # wykonanie funkcji sys_mknod z modułu NIE działa mknod /dev/$NAZWA c $NR 0 ls -l /dev/$NAZWA # krótki test: czytanie 512 bajtów i sprawdzenie ich zawartości dd count=1 if=/dev/$NAZWA of=/x && hexdump /x && rm -f /x
Wystarczy ten skrypt zachować na przykład pod nazwą
instal.sh
, nadać prawo wykonywania komendą chmod u+x instal.sh
i uruchamiać poprzez
./instal.sh
, oczywiście jako root. Jeśli załadowanie
modułu się uda, skrypt wyświetli przydzielone modułowi zasoby - porty, IRQ, pamięć - poprzez
zajrzenie do odpowiednich plików katalogu /proc. Skrypt utworzy też plik urządzenia w katalogu
/dev z odpowiednim numerem głównym oraz wykona prosty test.
Odinstalować moduł można łatwo takim oto skryptem:
#!/bin/bash PLIK="modul_dev_fasm" # Tu wstawiacie swoją nazwę, bez rozszerzenia .o NAZWA="test00" /sbin/rmmod $PLIK && rm -f /dev/$NAZWA
Najprostszy moduł jądra 2.6 wygląda tak:
format ELF section ".init.text" executable align 1 section ".text" executable align 4 public init_module public cleanup_module extrn printk init_module: push dword str1 call printk pop eax xor eax, eax ret cleanup_module: push dword str2 call printk pop eax ret section ".modinfo" align 32 __kernel_version db "kernel_version=2.6.16", 0 __mod_vermagic db "vermagic=2.6.16 686 REGPARM 4KSTACKS gcc-4.0", 0 __module_license db "license=GPL", 0 __module_author db "author=Bogdan D.", 0 __module_description db "description=Pierwszy modul jadra 2.6", 0 section "__versions" align 32 dd 0xfa02c634 n1: db "struct_module" times 64-4-($-n1) db 0 dd 0x1b7d4074 n2: db "printk" times 64-4-($-n2) db 0 section ".data" writeable align 4 str1 db "<1> Jestem w init_module(). ", 10, 0 str2 db "<1> Jestem w cleanup_module(). ", 10, 0 section ".gnu.linkonce.this_module" writeable align 128 align 128 __this_module: ; łączna długość: 512 bajtów dd 0, 0, 0 .nazwa: db "modul", 0 times 64-4-($-.nazwa) db 0 times 100 db 0 dd init_module times 220 db 0 dd cleanup_module times 112 db 0
Od razu widać sporo różnic, prawda? Omówmy je po jednej sekcji na raz:
.init.text
W zasadzie powinny być co najmniej dwie: .init.text
, zawierająca procedurę
inicjalizacji oraz .exit.text
, zawierająca procedurę wyjścia.
Dodatkowo, może być oczywiście sekcja danych .data
i kodu .text
.
Jeśli podczas próby zainstalowania modułu dostajecie komunikat Accessing
a corrupted shared library
(Dostęp do uszkodzonej biblioteki współdzielonej
),
to pogrzebcie w sekcjach - doróbcie .text
, usuńcie .init.text
,
zamieńcie kolejność itp.
.gnu.linkonce.this_module
Ta jest najważniejsza. Bez niej próba
instalacji modułu w jadrze zakończy się komunikatem No module found in
object
(w pliku obiektowym nie znaleziono modułu
). Zawartość tej sekcji to struktura
typu module
o nazwie __this_module
. Najlepiej zrobicie, przepisując tę powyżej
do swoich modułów, zmieniając tylko nazwę modułu oraz funkcje wejścia i wyjścia.
Możecie też skorzystać z następującego makra:
macro gen_this_module name*, entry, exit { section '.gnu.linkonce.this_module' writeable align 128 align 128 __this_module: dd 0, 0, 0 .mod_nazwa: db name, 0 times 64-4-($-.mod_nazwa) db 0 times 100 db 0 if entry eq dd init_module else dd entry end if times 220 db 0 if exit eq dd cleanup_module else dd exit end if times 112 db 0 }
Korzysta się z niego dość łatwo: wystarczy podać nazwę modułu, która ma być wyświetlana
po komendzie lsmod
oraz nazwy procedur wejścia i wyjścia z modułu, na przykład
gen_this_module "nasz_modul", init_module, cleanup_module
To wywołanie makra należy umieścić tam, gdzie normalnie ta sekcja by się znalazła, czyli na przykład po ostatniej deklaracji czegokolwiek w sekcji danych. W każdym razie NIE tak, żeby było to w środku jakiejkolwiek sekcji.
modinfo
Sekcja ta wzbogaciła się w stosunku do tej z jądra 2.4 o tylko jeden, ale
za to bardzo ważny wpis - vermagic
. U większości z Was ten napis
będzie się różnił od mojego tylko wersją jądra. W oryginale wygląda on tak:
#define VERMAGIC_STRING \
UTS_RELEASE " " \
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_ARCH_VERMAGIC \
"gcc-" __stringify(__GNUC__) "." __stringify(__GNUC_MINOR__)
#define MODULE_ARCH_VERMAGIC MODULE_PROC_FAMILY \
MODULE_REGPARM MODULE_STACKSIZE
a można go znaleźć w podkatalogach asm* katalogu INCLUDE w źródłach jądra oraz w pliku VERMAGIC.H.
__versions
Ta sekcja zawiera informacje o wersjach procedur, z których nasz moduł korzysta. Struktura jest dość prosta: najpierw jako DWORD wpisujemy numerek odpowiadający danej funkcji jądra, a znaleziony w pliku MODULE.SYMVERS w katalogu głównym źródeł jadra. Zaraz za numerkiem wpisujemy nazwę naszej funkcji, dopełnioną zerami do 64 bajtów.
Ta sekcja nie jest wymagana do prawidłowej pracy modułu, ale powinna się w każdym znaleźć,
żeby nie pojawiały się komunikaty o zanieczyszczeniu
jądra (kernel tainted
).
Całą tę sekcję możecie wygenerować, korzystając z mojego skryptu symvers-fasm.txt. Wystarczy uruchomić perl symvers-fasm.pl wasz_modul.asm
.
Rezerwacja zasobów w jądrze 2.6 z zewnątrz (czyli z perspektywy języka C) nie różni się od tej z jądra 2.4. Ale tak naprawdę zaszły dwie istotne zmiany:
W jądrze 2.6 wygląda tak:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file*,char __user*,size_t,
loff_t*);
ssize_t (*aio_read) (struct kiocb *, char __user *,
size_t, loff_t);
ssize_t (*write) (struct file *, const char __user *,
size_t, loff_t *);
ssize_t (*aio_write) (struct kiocb *, const char __user*,
size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *,
struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *,
unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int,
unsigned long);
long (*compat_ioctl) (struct file *, unsigned int,
unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *,
int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *,
unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *,
unsigned long, loff_t *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t,
read_actor_t, void *);
ssize_t (*sendpage) (struct file *, struct page *, int,
size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *,
unsigned long, unsigned long, unsigned long,
unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
};
Moje jądro dystrybucyjne zostało skompilowane tak, żeby trzy pierwsze parametry do każdej procedury z wyjątkiem printk przekazywało w rejestrach: EAX, EDX, ECX, a resztę na stosie. Aby sprawdzić, czy u Was też tak jest, wykonajcie komendy:
grep -R regpar /lib/modules/`uname -r`/build/|grep Makefile grep -R REGPAR /lib/modules/`uname -r`/build/|grep config
Jeśli ich wyniki zawierają takie coś:
CONFIG_REGPARM=y #define CONFIG_REGPARM 1
to prawdopodobnie też tak macie. Możecie wtedy bez przeszkód używać makra URUCHOM, które umieszczę w module poniżej. Jeśli nie, możecie je zmodyfikować. Potrzeba modyfikacji może wynikać z zawieszania się całego systemu podczas próby zainstalowania modułu.
Podobnie, jak w jądrze 2.4, pokazany niżej program zarejestruje programowe urządzenie znakowe (czyli takie, dla którego nie ma odpowiednika w sprzęcie, jak na przykład /dev/null) z IRQ 4, zakresem portów 600h-6FFh, zakresem pamięci 80000000h - 8000FFFFh oraz z podstawowymi operacjami: otwieranie, zamykanie, czytanie, zapis, zmiana pozycji. Dla uproszczenia kodu nie sprawdzam, czy dane zakresy są wolne. Jeśli okażą się zajęte, jądro zwróci błąd i moduł się nie załaduje.
format ELF section ".text" executable align 4 public init_module public cleanup_module extrn printk extrn register_chrdev extrn unregister_chrdev extrn request_irq extrn free_irq extrn __request_region extrn __release_region extrn ioport_resource extrn iomem_resource PORTY_START = 0x600 PORTY_ILE = 0x100 RAM_START = 0x80000000 RAM_ILE = 0x00010000 SA_INTERRUPT = 0x20000000 NUMER_IRQ = 4 macro uruchom funkcja, par1, par2, par3, par4, par5 { if ~ par5 eq push dword par5 end if if ~ par4 eq push dword par4 end if if ~ par3 eq mov ecx, par3 end if if ~ par2 eq mov edx, par2 end if if ~ par1 eq mov eax, par1 end if call funkcja if ~ par5 eq add esp, 4 end if if ~ par4 eq add esp, 4 end if } init_module: pushfd ; rejestrowanie urządzenia znakowego: uruchom register_chrdev, 0, nazwa, file_oper cmp eax, 0 jg .dev_ok ; wyświetlenie informacji o błędzie push eax push dword dev_blad call printk add esp, 1*4 ; specjalnie tylko 1*4 pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .dev_ok: mov [major], eax ; rejestrowanie zakresu portów uruchom __request_region, ioport_resource, PORTY_START, PORTY_ILE, nazwa test eax, eax jnz .iop_ok push eax push dword porty_blad call printk add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia uruchom unregister_chrdev, [major], nazwa pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .iop_ok: ; rejestrowanie zakresu pamięci uruchom __request_region, iomem_resource, RAM_START, RAM_ILE, nazwa test eax, eax jnz .iomem_ok push eax push dword ram_blad call printk add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia uruchom unregister_chrdev, [major], nazwa ; wyrejestrowanie zakresu portów uruchom __release_region, ioport_resource, PORTY_START, PORTY_ILE pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .iomem_ok: ; przydzielanie przerwania IRQ: uruchom request_irq, NUMER_IRQ, obsluga_irq, SA_INTERRUPT, nazwa, file_oper cmp eax, 0 jge .irq_ok push eax push dword irq_blad call printk add esp, 1*4 ; potem pop eax ; wyrejestrowanie urządzenia uruchom unregister_chrdev, [major], nazwa ; wyrejestrowanie zakresu portów uruchom __release_region, ioport_resource, PORTY_START, PORTY_ILE ; wyrejestrowanie zakresu pamięci uruchom __release_region, iomem_resource, RAM_START, RAM_ILE pop eax ; wychodzimy z oryginalnym błędem jmp .koniec .irq_ok: ; wyświetlenie informacji o załadowaniu modułu push dword NUMER_IRQ push dword [major] push dword uruch call printk add esp, 3*4 xor eax, eax .koniec: popfd ret ; funkcja uruchamiana przy usuwaniu modułu cleanup_module: pushfd push eax ; zwolnienie numeru IRQ: uruchom free_irq, NUMER_IRQ, file_oper ; wyrejestrowanie urządzenia: uruchom unregister_chrdev, [major], nazwa ; wyrejestrowanie zakresu portów uruchom __release_region, ioport_resource, PORTY_START, PORTY_ILE ; wyrejestrowanie zakresu pamięci uruchom __release_region, iomem_resource, RAM_START, RAM_ILE push dword usun call printk add esp, 1*4 pop eax popfd ret ; deklaracja wygląda tak: ; void handler (int irq, void *dev_id, struct pt_regs *regs); ; ostatni argument zwykle nieużywany section ".text" executable align 4 obsluga_irq: push ebp mov ebp, esp ; tu Wasz kod leave ret ; Zdefiniowane operacje: czytanie: ; ssize_t (*read) (struct file *, char *, size_t, loff_t *); push ebp mov ebp, esp loff equ ebp+8 pushfd push edi push ecx mov al, 0x1e cld mov edi, edx rep stosb pop ecx pop edi popfd ; mówimy, że przeczytano tyle bajtów, ile żądano mov eax, ecx leave ret zapis: ; ssize_t (*write) (struct file *, const char *, size_t, loff_t *); push ebp mov ebp, esp ; nic fizycznie nie zapisujemy, zwracamy tylko liczbę bajtów, ; którą mieliśmy zapisać (trzeci parametr) mov eax, ecx leave ret przejscie: zamykanie: otwieranie: xor eax, eax ret section ".data" writeable align 4 major dd 0 ; numer główny urządzenia przydzielany przez jądro ; adresy funkcji operacji na tym urządzeniu file_oper: dd 0, przejscie, czytanie, 0, zapis, 0, 0, 0, 0, 0, 0, 0 dd otwieranie, 0, zamykanie, 0, 0, 0, 0, 0, 0, 0, 0, 0 dd 0, 0, 0 dd 0, 0, 0 dev_blad db "<1>Blad rejestracji urzadzenia: %d.", 10, 0 irq_blad db "<1>Blad przydzielania IRQ: %d.", 10, 0 porty_blad db "<1>Blad przydzielania portow: EAX=%d", 10, 0 ram_blad db "<1>Blad przydzielania pamieci: EAX=%d", 10, 0 uruch db "<1>Modul zaladowany. Maj=%d, IRQ=%d", 10, 0 usun db "<1>Modul usuniety.", 10, 0 nazwa db "test00", 0, 0 sciezka db "/dev/test00", 0 section ".modinfo" align 32 __kernel_version db "kernel_version=2.6.16", 0 __mod_vermagic db "vermagic=2.6.16 686 REGPARM 4KSTACKS gcc-4.0",0 __module_license db "license=GPL", 0 __module_author db "author=Bogdan D.", 0 __module_description db "description=Pierwszy modul jadra 2.6", 0 __module_device db "device=test00", 0 __module_depends db "depends=", 0 ; nieistotne, wzięte ze skompilowanego modułu C: __mod_srcversion db "srcversion=F5CE0CFFE0191EDB2F816D4", 0 section "__versions" align 32 ____versions: dd 0xfa02c634 ; Z MODULE.SYMVERS n1: db "struct_module", 0 times 64-4-($-n1) db 0 dd 0x1b7d4074 n2: db "printk", 0 times 64-4-($-n2) db 0 dd 0xb5145e00 n3: db "register_chrdev", 0 times 64-4-($-n3) db 0 dd 0xc192d491 n4: db "unregister_chrdev", 0 times 64-4-($-n4) db 0 dd 0x26e96637 n5: db "request_irq", 0 times 64-4-($-n5) db 0 dd 0xf20dabd8 n6: db "free_irq", 0 times 64-4-($-n6) db 0 dd 0x1a1a4f09 n7: db "__request_region", 0 times 64-4-($-n7) db 0 dd 0xd49501d4 n8: db "__release_region", 0 times 64-4-($-n8) db 0 dd 0x865ebccd n9: db "ioport_resource", 0 times 64-4-($-n9) db 0 dd 0x9efed5af n10: db "iomem_resource", 0 times 64-4-($-n10) db 0 section ".gnu.linkonce.this_module" writeable align 128 align 128 __this_module: ; łączna długość: 512 bajtów dd 0, 0, 0 .mod_nazwa: db "modul_dev_fasm", 0 times 64-4-($-.mod_nazwa) db 0 times 100 db 0 dd init_module times 220 db 0 dd cleanup_module times 112 db 0
Do instalacji i usuwania modułu z jądra można użyć tych samych skryptów, które były dla jądra 2.4, zmieniając ewentualnie nazwę pliku modułu.
W późniejszych wersjach jądra ogólny sposób pisania modułów nie uległ zmianie. Ale jądro systemu Linux, jak każdy większy program, jest rozbudowywane i ulega zmianom. Takim zmianom ulegają między innymi:
__versions
,__versions
,moduleumieszczonej w
__this_module
(a, co gorsza, część jej elementów istnieje tylko warunkowo, w zależności
od konfiguracji konkretnego jądra, co wpływa na adresy innych elementów),file_operations
,.modinfo
, w szczególności pojawienie się nowych parametrów, na przykład:
"retpoline=Y"
, oznaczający kompilację bezpieczną, bez skoków niebezpośrednich
(np. jmp [eax]
),"intree=Y"
, oznaczający moduł z drzewa jądra, co zapobiega zanieczyszczeniu,
gold
,
nie można nawet skonfigurować jądra, gdy ten linker jest używany,section '.note.GNU-stack'
section .note.GNU-stack noalloc noexec nowrite progbits
Sposób kompilacji modułów jądra można podpatrzeć
, wykonując make
(z odpowiednimi parametrami) z flagą V=1, na przykład make O=build/ V=1 modules
(do zbudowania wszystkich skonfigurowanych modułów).
Lokalizacja pliku z sumami kontrolnymi może się zmieniać w zależności od wersji jądra i od
konkretnej dystrybucji Linuksa - może to być plik Module.symvers
gdzieś w katalogu /lib/modules/wersja_jądra/
, może to być plik symvers-wersja_jądra
(być może spakowany) w katalogu /boot/
.
Zawartość struktury module
powinna znajdować się w pliku module.h
w katalogu rozpakowanych źródeł jądra w odpowiedniej wersji: linux-X.Y.Z/include/linux/
.
Plik /usr/include/linux/module.h
może być do tego celu nieodpowiedni.
Inne interesujące pliki to, w zależności od wersji jądra, na przykład:
linux-X.Y.Z/include/asm-i386/MODULE.H
,linux-X.Y.Z/include/linux/VERSION.H
,linux-X.Y.Z/scripts/mod/MODPOST
,linux-X.Y.Z/include/linux/init.h
,linux-X.Y.Z/include/linux/vermagic.h
,linux-X.Y.Z/include/linux/module.h
,linux-X.Y.Z/include/linux/moduleparam.h
,linux-X.Y.Z/Makefile
,linux-X.Y.Z/scripts/Makefile.modpost
,linux-X.Y.Z/scripts/Kbuild.include
,linux-X.Y.Z/scripts/Makefile.build
,linux-X.Y.Z/arch/x86/Makefile
,a samą obsługą modułów zajmują się na przykład linux-X.Y.Z/kernel/module.c
i
linux-X.Y.Z/arch/x86/module.c
.
Strukturę file_operations
znajdziemy w linux-X.Y.Z/include/linux/fs.h
,
a w jądrze 5.5.12 wygląda już tak:
(przeskocz nową strukturę file_operations)
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, bool spin); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); }
Zamiast funkcji register_chrdev
trzeba prawdopodobnie używać
register_chrdev_region
, zamiast request_irq
- prawdopodobnie pci_request_irq
(taki symbol na liście symboli jest
zaznaczony jako eksportowany przez vmlinux
, czyli przez samo jądro).
Mając kod jądra skonfigurowany zgodnie z jądrem docelowym, poprzez make XXXconfig
,
możecie skorzystać z poniższego generatora sekcji .modinfo
(jeśli kompilujemy na swój system, wystarczy skopiować plik konfiguracyjny, na przykład
/boot/config-wersja_jądra
jako plik .config
do katalogu źródeł jądra,
po czym wykonać make oldconfig
).
(przeskocz generator)
#include <stdio.h> #include <stddef.h> #include <linux/module.h> #define NAZWA_MODULU "modul1" static void wyswietl_wspolne (const char nazwa[], const char wskaznik[]) { struct module m; puts ("align 128"); puts ("__this_module:"); printf ("\t\t\ttimes %d db 0\n", offsetof (struct module, name)); printf ("\t.mod_nazwa:\tdb '%s', 0\n", nazwa); printf ("\t\t\ttimes %d - ($ - .mod_nazwa) db 0\n", sizeof (m.name)); printf ("\t\t\ttimes %d db 0\n", offsetof (struct module, init) - offsetof (struct module, name) - sizeof (m.name)); printf ("\t.mod_init:\t%s init_module\n", wskaznik); printf ("\t\t\ttimes %d db 0\n", offsetof (struct module, exit) - offsetof (struct module, init) - sizeof (m.init)); printf ("\t.mod_exit:\t%s cleanup_module\n", wskaznik); printf ("\t\t\ttimes %d db 0\n", sizeof (struct module) - offsetof (struct module, exit) - sizeof (m.exit)); puts ("--------------------------------"); } static void wyswietl_nasm (const char nazwa[], const char wskaznik[]) { puts ("--------------------------------\nsection .gnu.linkonce.this_module"); wyswietl_wspolne (nazwa, wskaznik); } static void wyswietl_fasm (const char nazwa[], const char wskaznik[]) { puts ("--------------------------------\nsection '.gnu.linkonce.this_module' writeable align 128"); wyswietl_wspolne (nazwa, wskaznik); } int main (void) { puts ("NASM, 32-bit:"); wyswietl_nasm (NAZWA_MODULU, "dd"); puts ("NASM, 64-bit:"); wyswietl_nasm (NAZWA_MODULU, "dq"); puts ("FASM, 32-bit:"); wyswietl_fasm (NAZWA_MODULU, "dd"); puts ("FASM, 64-bit:"); wyswietl_fasm (NAZWA_MODULU, "dq"); return 0; }
Kompiluje się go takim skryptem:
(przeskocz skrypt kompilujący)
#!/bin/bash lpath=/ścieżka/do/linux-X.Y.Z gcc \ -I /usr/include \ -I $lpath/arch/x86/include \ -I $lpath/arch/x86/include/generated \ -I $lpath/arch/x86/include/uapi \ -I $lpath/arch/x86/include/generated/uapi \ -I $lpath/include \ -I $lpath/include/uapi \ -I $lpath/include/generated \ -I $lpath/include/generated/uapi \ -I $lpath/build/include \ -I $lpath/build/arch/x86/include \ -I $lpath/build/arch/x86/include/generated \ -I $lpath/build/arch/x86/include/uapi \ -I $lpath/build/arch/x86/include/generated/uapi \ -include $lpath/include/linux/kconfig.h \ -D__KERNEL__ \ -DMODULE \ -o gen-modul-info \ gen-modul-info.c
wstawiając swoją ścieżkę do rozpakowanych źródeł jądra.
Po uruchomieniu programu wyświetli on zawartość, którą należy ewentualnie uzupełnić
(np. innymi polami struktury, bo ustawiane są tylko: nazwa, adres funkcji uruchamiającej i kończącej),
po czym wstawić do sekcji .modinfo
:
(przeskocz przykładowy wynik)
section .gnu.linkonce.this_module align 128 __this_module: times 24 db 0 .mod_nazwa: db 'modul1', 0 times 64 - ($ - .mod_nazwa) db 0 times 296 db 0 .mod_init: dq init_module times 432 db 0 .mod_exit: dq cleanup_module times 72 db 0
Jądro jest pisane w języku C, więc programiści piszący moduły w tym języku mają tę wygodę, że nie muszą do swoich modułów kopiować i dostosowywać struktur, bo już mają je w plikach nagłówkowych. Tak samo, do inicjalizacji struktur wystarczy zainicjalizować odpowiednie pola, a kompilator już umieści odpowiednie wartości w odpowiednich miejscach - nie trzeba liczyć, po ilu bajtach należy umieścić kolejne pole. Dla programistów innych języków staje się to coraz trudniejsze.
W związku z tym, można też rozważyć napisanie frontowej
części swojego
modułu (tej z deklaracją funkcji uruchamiającej, kończącej, z sekcją .modinfo
i ze wszystkimi strukturami) w języku C, a samych funkcjonalności - w asemblerze, po czym połączyć
te części w całość według zasad uruchamiania funkcji między C i asemblerem, opisanych w
dziesiątej części mojego kursu.
Problem może stwarzać też architektura: 32- lub 64-bitowa, gdyż inne są nazwy rejestrów oraz sposób przekazywania parametrów do wywoływanych funkcji.
W przypadku FASM-a, gdzie typ pliku wynikowego jest umieszczony w kodzie źródłowym, trzeba pisać osobne wersje na systemy 32- i 64-bitowe.
W przypadku NASM-a może być trochę łatwiej, gdyż w kodzie można sprawdzić format docelowy
(podawany na linii poleceń) i odpowiednio zmodyfikować nazwy rejestrów lub rozkazów.
Można wykorzystać na przykład takie makra:
(przeskocz makra architektury)
%ifidn __OUTPUT_FORMAT__, elf64 bits 64 %define ARCH 'x64' %define RET_REG rax %define wskaznik dq %define rozmiar_wsk 8 %define pushflags pushfq %define popflags popfq %else bits 32 %define ARCH 'x86' %define RET_REG eax %define wskaznik dd %define rozmiar_wsk 4 %define pushflags pushfd %define popflags popfd %endif %macro uruchom 1-7 ; funkcja, par1, par2, par3, par4, par5, par6 %if ARCH = 'x64' %ifnempty %7 mov r9, %7 %endif %ifnempty %6 mov r8, %6 %endif %ifnempty %5 mov r10, %5 %endif %ifnempty %4 mov rdx, %4 %endif %ifnempty %3 mov rsi, %3 %endif %ifnempty %2 mov rdi, %2 %endif call %1 %else %ifnempty %7 push dword %7 %endif %ifnempty %6 push dword %6 %endif %ifnempty %5 push dword %5 %endif %ifnempty %4 mov ecx, %4 %endif %ifnempty %3 mov edx, %3 %endif %ifnempty %2 mov eax, %2 %endif call %1 %ifnempty %7 add esp, 4 %endif %ifnempty %6 add esp, 4 %endif %ifnempty %5 add esp, 4 %endif %endif %endmacro
I potem wykorzystać je w kodzie:
init_module: uruchom printk, tekst_uruchomiono xor RET_REG, RET_REG ret
To powinno trochę ułatwić pracę i zmniejszyć duplikację kodu między wieloma plikami.
Jeśli chcecie się na poważnie zająć pisaniem modułów, możecie zacząć od przeczytania dokumentacji o tym, jak wszystko zrobić poprawnie oraz jakie funkcjonalności i mechanizmy jądro oferuje: