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.

Spis treści:

  1. Podstawy
  2. Najprostszy moduł jądra 2.4
  3. Rejestracja urządzenia znakowego
  4. Rejestracja portów wejścia-wyjścia oraz obszaru pamięci
  5. Rejestracja zasobu IRQ
  6. Przykład modułu jądra 2.4
  7. Najprostszy moduł jądra 2.6
  8. Rezerwacja zasobów w jądrze 2.6
  9. Przykład modułu jądra 2.6
  10. Inne jądra i inne sztuczki

Podstawy

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.


Inne jądra i inne sztuczki

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:

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:

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:


Spis treści off-line (klawisz dostępu 1)
Spis treści on-line (klawisz dostępu 2)
Ułatwienia dla niepełnosprawnych (klawisz dostępu 0)