Leitfaden zur Programmierung von Linux-Kernelmodulen
(sysprog21.github.io)- Linux Kernel Module Programming Guide ist ein kostenloser Leitfaden zum Erstellen ladbarer Kernelmodule für Linux v5.10 und neuer. Er behandelt den gesamten Ablauf von der Entwicklungsumgebung über Build, Laden und Debugging bis zu wichtigen Kernel-Schnittstellen.
- Die ersten Beispiele nutzen
hello-*.c, ummodule_init(),module_exit(),kbuild,insmod,rmmodunddmesgeinzuüben; QEMU-basierte devtools verringern dabei das Risiko, das Host-System zu beschädigen. - Kernelmodule laufen im Kernel-Adressraum. Fehlerhafte Pointer, eine falsche Unload-Reihenfolge, Nebenläufigkeit oder Fehler beim Kopieren von Userspace-Speicher können daher zu Beschädigung von Kernel-Speicher oder Systeminstabilität führen.
- Der Leitfaden erweitert den Umfang auf Character Devices,
/proc,seq_file, threaded IRQs, Input, PCI, USB, Block, Netzwerk, Device Model, Device Tree und static key und behandelt dabei wiederholt die Reihenfolge von Registrierung und Freigabe sowie Lifetime-Management. - Da sich interne Kernel-APIs je nach Version ändern, prüft der Leitfaden Bedingungen wie
LINUX_VERSION_CODE,KERNEL_VERSION,CONFIG_MODVERSIONS, SecureBoot-Signierung und version magic; auch die Beispiele enthalten bedingte Kompilierung.
Aufbau des Leitfadens und grundlegender Ablauf
- Dieser Leitfaden ist Lernmaterial zu Kernelmodulen mit einem GitHub-Repository und einem PDF-Dokument. Er darf unter den Bedingungen der Open Software License 3.0 kopiert, verändert und verbreitet werden.
- Der aktuelle Leitfaden setzt Linux v5.10 als Mindestversion an und zielt darauf ab, die Kompatibilität der Beispiele und Anleitungen über die Breite der Long-Term-Support-Kernel hinweg zu erhalten.
- Lernende benötigen Kenntnisse in C und Erfahrung mit der Programmierung für gewöhnliche Prozesse. Kernelmodule werden dynamisch geladen und entladen und erweitern Kernel-Funktionen ohne Neustart.
- Der grundlegende Entwicklungsablauf besteht aus dem Installieren der Kernel-Header, dem Bauen der
.ko-Datei mitmake, der Prüfung mitmodinfo, dem Laden perinsmod, der Kontrolle der Logs mitdmesgoderjournalctl -kund dem Entladen perrmmod. devtools/baut Kernel-Quellcode und ein BusyBox-Root-Dateisystem, bootet diese in QEMU, teiltexamples/per 9p virtfs und ermöglicht anschließend das Testen der Module im Gast.- Für Initialisierung und Aufräumen von Modulen werden
module_init()undmodule_exit()bevorzugt. Die ältere Methode mitinit_module()undcleanup_module()kann seit Kernel 6.15 unter bestimmten Bedingungen Build-Fehler auslösen, wenn x86 IBT aktiviert ist. - Kernelmodule verwenden weder
printf()noch libc und können nur vom Kernel exportierte Symbole nutzen; Ausgaben gehen nicht an das Terminal, sondern in den Kernel-Log-Ringpuffer. - Für den Datentransfer zwischen Userspace und Kernelspace sind spezielle Funktionen wie
put_user,get_user,copy_to_userundcopy_from_usererforderlich. - Das Character-Device-Beispiel zeigt
register_chrdev,file_operations, dynamische Major Numbers, das Erstellen von/dev-Nodes, exclusive open, read auf Basis vonput_userund die Behandlung nicht unterstützter write-Operationen. - Die
/proc-Beispiele behandelnproc_create,proc_ops, read/write-Callbacks und dieseq_file-API und spiegeln die Änderung wider, dass seit Linux v5.6 für/proc-Handlerproc_opsstattfile_operationseingeführt wurde. - Threaded IRQs teilen mit
request_threaded_irq()Top-Half und Bottom-Half. Die Top-Half erledigt im Interrupt Context nur minimale Arbeit und weckt mitIRQ_WAKE_THREADdie threadbasierte Bottom-Half. - Spätere Kapitel erweitern den Umfang auf reale Treiberbereiche wie Input, PCI, USB, Block, Netzwerk, Device Model und Device Tree und konzentrieren sich auf die Registrierungsmechanismen der jeweiligen Subsystems sowie die Wahl der Userspace-ABI.
- Die Abschnitte zu Optimierung und Sicherheit behandeln
likely/unlikely, static key, den kleinen Kernel-Stack, das Verbot der FPU-Nutzung, das Leaken nicht initialisierten Paddings und Vorsicht bei internen APIs mit doppeltem Unterstrich.
Einschränkungen, auf die man beim Bauen und Laden zuerst stößt
- Ein für einen Kernel kompiliertes Modul lässt sich unter Umständen nicht in einem anderen Kernel laden. Wenn version magic und
CONFIG_MODVERSIONSnicht passen, kommt es zuInvalid module formatoder Abweichungen bei Symbolversionen. - In den meisten gewöhnlichen Linux-Distributionskerneln kann modversioning aktiviert sein. Wenn Beispiele nicht sofort funktionieren, sollte man einen Kernel ohne modversioning oder eine QEMU-Umgebung in Betracht ziehen.
- Auf Systemen mit aktiviertem SecureBoot kann das Laden nicht signierter Module eingeschränkt sein. Wenn
Lockdown: insmod: unsigned module loading is restrictederscheint, ist das Deaktivieren von SecureBoot oder ein Verfahren zur Modulsignierung erforderlich.
QEMU-basierte Übungsumgebung
devtools/setup.shlädt den Kernel-Tarball und BusyBox herunter, baut sie und paketiert ein initramfs.devtools/build-modules.shbaut Module für den QEMU-Kernel,devtools/boot.shstellt eine Gast-Shell bereit, unddevtools/test-modules.shführt automatischeinsmod-/rmmod-Tests pro Modul aus.- GDB-Debugging erfolgt, indem mit
LKMPG_NO_PREBUILT=1 devtools/setup.shvmlinuxgebaut wird und anschließenddevtools/boot.sh --gdbsowie eine Remote-GDB-Verbindung genutzt werden.
Regeln für das Schreiben von Kernel-Code
- Da Registrierung und Allokation in init-Funktionen fehlschlagen können, müssen bereits erworbene Ressourcen in
goto-basierten Fehlerpfaden in umgekehrter Reihenfolge freigegeben werden. - Wenn eine Callback-Struktur im Kernel registriert wird, kann der Userspace Callbacks bereits vor der Rückkehr von init aufrufen. Deshalb ist die Regel register last, unregister first wichtig: interne Initialisierung zuerst abschließen, dann zuletzt registrieren und beim Aufräumen zuerst deregistrieren.
- Process Context, softirq/tasklet context und hardirq context unterscheiden sich darin, ob Sleep, Zugriff auf Userspace-Speicher,
GFP_KERNELund Mutex-Nutzung erlaubt sind. Ein falsches Verständnis dieser Unterscheidung führt zu häufigen Kernel-Bugs.
Hinweise zu Devices und einzelnen Subsystems
- Character Devices identifizieren den Treiber über die Major Number und unterscheiden mehrere Geräte innerhalb des Treibers über die Minor Number. In modernen Ansätzen wird statt
register_chrdev()dascdev-Interface empfohlen. - PCI-Treiber gehen nicht von festen Adressen aus, sondern mappen die vom PCI-Core enumerierten BAR resources. In Code für Linux 5.10 und neuer sind
pcim_enable_device()und device-managed resource APIs hilfreich, um Teardown-Bugs zu reduzieren. - USB-Treiber müssen hotplug und disconnect als normale Ereignisse behandeln und unter der Annahme entworfen werden, dass URB completion mit disconnect, timeout, suspend und Userspace-Shutdown konkurrieren kann.
- Block-Treiber arbeiten rund um
blk-mq,request,gendisk, Queue-Limits und flush/FUA semantics und nehmen nicht an einem einfachen read/write-Callback-Modell teil, sondern an einem Modell mit asynchroner request completion. - Netzwerktreiber kombinieren
struct net_device,net_device_ops,sk_buff, NAPI, Offload-Feature-Flags und Link-State-Berichte. Falsch deklarierte Offloads können zu beschädigtem Traffic führen.
Umgang mit Änderungen zwischen Kernel-Versionen
- Die Beispiele behandeln per bedingter Kompilierung die geänderte Signatur von
class_create()in Linux 6.4,proc_opsin Linux v5.6, den geänderten Rückgabetyp vonremove()in Linux 6.11 sowie Änderungen anblk-mq-Helpern zwischen Linux 5.15 und 6.9. - Interne Kernel-Schnittstellen ändern sich häufiger als Systemaufrufe. Module, die mehrere Kernel unterstützen, kommen daher kaum um Vergleiche mit
LINUX_VERSION_CODEundKERNEL_VERSIONherum.
Sicherheits-Checkpoints
- Der Kernel-Stack ist deutlich kleiner als der Userspace-Stack und kann auf vielen Systemen nur 8 KiB oder 16 KiB groß sein. Große Arrays sollten daher mit
kmalloc()oderkzalloc()allokiert werden. - Beim Senden von Daten in den Userspace mit
copy_to_user()müssen alle Bytes einschließlich Padding initialisiert sein; andernfalls kann es zu Informationslecks aus dem Kernel-Speicher kommen. - APIs, die wie
__kmalloc()oder__list_add()mit einem doppelten Unterstrich beginnen, können interne Vorbedingungen voraussetzen. Wenn die Dokumentation es nicht verlangt, sollte bevorzugt der öffentliche Wrapper verwendet werden.
Ausgelassener Umfang
- Da ausdrücklich angegeben ist, dass einige Original-Chunks bei der Eingabeverarbeitung aus Längen- und Kostengründen ausgelassen wurden, deckt diese Zusammenfassung nicht ausnahmslos alle Kapitel, Beispiele und Code-Pfade des Leitfadens ab.
1 Kommentare
Hacker-News-Kommentare
QEMU ist eine gute Möglichkeit, Erfahrungen mit Kernel-Hacking zu sammeln
Es wäre schön, wenn jemand LDD (Linux Device Drivers) und Linux-Kernel-Bücher aktualisieren würde, und da sich mit solchen Fachbüchern kaum Geld verdienen lässt, könnte auch die Linux Foundation das fördern.
Auch diese Woche habe ich mit QEMU + GDB ein Problem reproduziert, bei dem der Kernel unter v6.8 auf arm64 sofort still stehen bleibt, wenn der Kernel-Kommandozeilenparameter länger als 146 Zeichen ist, und auf einem Debian-12-Bookworm-amd64-Host einen arm64-Kernel-Build emuliert, um den problematischen Code Zeile für Zeile nachzuverfolgen und die Ursache zu finden.
Der Ablauf besteht darin, in einer Umgebung mit vorbereiteten Build-Abhängigkeiten und Cross-Compile-Werkzeugen ein arm64-Kernel-Image und Skripte für GDB zu bauen, auf dem Host
gdb, bei Bedarfgdb-multiarchundqemu-system-armzu installieren, dannqemu-system-aarch64mit-S -gdb tcp::1234im angehaltenen Zustand zu starten und sich in einem anderen Terminal mitgdb-multiarch ./vmlinuxzu verbinden.Danach kann man in GDB
target remote :1234,break __parse_cmdline,continueausführen und die üblichen GDB-Funktionen wie das Prüfen von Speicher, Variablen und Stack sowie Einzelschritt-Ausführung nutzen.Für Kernel-GDB-Debugging und die
lx-*-Skripte siehe https://www.kernel.org/doc/html/latest/dev-tools/gdb-kernel-...Damit GDB die
lx-*-Python-Skripte verwendet, muss meist auch der Pfad erlaubt werden, etwa mitecho "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinitVerwandte HN-Threads: https://news.ycombinator.com/item?id=35782630, https://news.ycombinator.com/item?id=28283030
The Linux Memory Manager ist ebenfalls sehenswert: https://linuxmemory.org/chapters
Laut dem letzten Update des Autors von Anfang Juli ist der erste Entwurf fertig, und jetzt ist man gemeinsam mit dem Verlag in die Redaktionsphase eingetreten.
Einige Beispiele scheinen sich nicht leicht direkt ausführen zu lassen
Zum Beispiel setzt „Detecting button presses“ voraus, dass man ein Modul für den RPi bauen kann, was seinerseits Arbeiten wie Cross-Compiling erfordert und daher nicht unbedingt einfach ist.
Ausgezeichnet, weil es ein detailliertes, praxisorientiertes Tutorial ist, in dem man direkt Kernel-Module baut.
Ebenfalls sehenswert: https://0xax.gitbooks.io/linux-insides/content/index.html
Ich frage mich, wo man etwas über Linux-Kernel-Programmierung allgemein wie Dateisysteme oder Speicherverwaltung nachlesen kann
Früher gab es Robert Loves „Linux Kernel Development“, aber das scheint inzwischen nicht mehr aktualisiert zu werden.
Ich habe das zum ersten Mal vor etwa 22 Jahren gelesen :)