Pisanie modułów jądra Linuksa

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.

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.


Najprostszy moduł jądra 2.4


(przeskocz najprostszy moduł)

Zgodnie z tym, co powiedziałem wyżej, najprostszy moduł wygląda tak:


(przeskocz kod najprostszego modułu)
	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.", 0
Zauważcie kilka spraw:
  1. Wyświetlanie napisów odbywa się wewnętrzną funkcją jądra - printk. Działa ona podobnie do funkcji printf z języka C, która na etapie ładowania jądra jest oczywiście niedostępna.

    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.

  2. Składnia jest dla kompilatora FASM.

    Moduły kompilowane NASMem z niewiadomych przyczyn nie chciały mi wchodzić do jądra.

  3. Każda funkcja jądra uruchamiana jest w konwencji C, czyli my sprzątamy argumenty ze stosu.

  4. Nowa sekcja - 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.


Rejestracja urządzenia znakowego


(przeskocz rejestrację urządzenia znakowego)

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:

  1. Numer główny urządzenia, który sobie wybraliśmy.

    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).

  2. Adres nazwy urządzenia w postaci ciągu znaków zakończonego bajtem zerowym.

  3. Adres struktury file_operations, do której wpiszemy adresy odpowiednich funkcji do operacji na pliku.

    Najważniejsze są: otwieranie, zamykanie, zapis i czytanie z urządzenia. Sama struktura wygląda tak dla jądra 2.4:


    (przeskocz strukturę file_operations)
    	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.


Rejestracja portów wejścia-wyjścia oraz obszaru pamięci


(przeskocz rejestrację portów i pamięci)

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:

  1. Typ zasobu. Jeśli chcemy zarezerwować porty, podajemy tu adres zmiennej 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.
  2. Początkowy numer portu lub początkowy adres pamięci.
  3. Długość zakresu portów lub pamięci
  4. Adres nazwy urządzenia.

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).


Rejestracja zasobu IRQ


(przeskocz rejestrację IRQ)

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:

  1. Numer przerwania IRQ, które chcemy zająć.
  2. Adres naszej funkcji, która będzie obsługiwać przerwania. Prototyp takiej funkcji wygląda tak:
    	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.
  3. Wartość SA_INTERRUPT = 0x20000000
  4. Adres nazwy urządzenia.
  5. Adres struktury file_operations, uzupełnionej adresami funkcji

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.


Przykład modułu jądra 2.4


(przeskocz do jądra 2.6)

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.


(przeskocz kod modułu)
; 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:


(przeskocz skrypt instalacji)
#!/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


(przeskocz najprostszy moduł jądra 2.6)

Najprostszy moduł jądra 2.6 wygląda tak:


(przeskocz kod najprostszego modułu jądra 2.6)
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:

  1. .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.

  2. .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.

  3. 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:


    (przeskocz definicję vermagic)
    	#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.

  4. __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


(przeskocz rezerwację zasobów w jądrze 2.6)

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:

  1. Struktura file_operations

    W jądrze 2.6 wygląda tak:


    (przeskocz strukturę file_operations w jądrze 2.6)
    	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 *);
    	};
  2. Sposób przekazywania parametrów

    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.


Przykład modułu jądra 2.6


(przeskocz przykład modułu jądra 2.6)

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.


Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)