2 Punkte von GN⁺ 2024-07-28 | 1 Kommentare | Auf WhatsApp teilen
  • 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, um module_init(), module_exit(), kbuild, insmod, rmmod und dmesg einzuü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 mit make, der Prüfung mit modinfo, dem Laden per insmod, der Kontrolle der Logs mit dmesg oder journalctl -k und dem Entladen per rmmod.
  • devtools/ baut Kernel-Quellcode und ein BusyBox-Root-Dateisystem, bootet diese in QEMU, teilt examples/ 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() und module_exit() bevorzugt. Die ältere Methode mit init_module() und cleanup_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_user und copy_from_user erforderlich.
  • Das Character-Device-Beispiel zeigt register_chrdev, file_operations, dynamische Major Numbers, das Erstellen von /dev-Nodes, exclusive open, read auf Basis von put_user und die Behandlung nicht unterstützter write-Operationen.
  • Die /proc-Beispiele behandeln proc_create, proc_ops, read/write-Callbacks und die seq_file-API und spiegeln die Änderung wider, dass seit Linux v5.6 für /proc-Handler proc_ops statt file_operations eingefü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 mit IRQ_WAKE_THREAD die 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_MODVERSIONS nicht passen, kommt es zu Invalid module format oder 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 restricted erscheint, ist das Deaktivieren von SecureBoot oder ein Verfahren zur Modulsignierung erforderlich.

QEMU-basierte Übungsumgebung

  • devtools/setup.sh lädt den Kernel-Tarball und BusyBox herunter, baut sie und paketiert ein initramfs.
  • devtools/build-modules.sh baut Module für den QEMU-Kernel, devtools/boot.sh stellt eine Gast-Shell bereit, und devtools/test-modules.sh führt automatische insmod-/rmmod-Tests pro Modul aus.
  • GDB-Debugging erfolgt, indem mit LKMPG_NO_PREBUILT=1 devtools/setup.sh vmlinux gebaut wird und anschließend devtools/boot.sh --gdb sowie 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_KERNEL und 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() das cdev-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_ops in Linux v5.6, den geänderten Rückgabetyp von remove() in Linux 6.11 sowie Änderungen an blk-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_CODE und KERNEL_VERSION herum.

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() oder kzalloc() 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

 
GN⁺ 2024-07-28
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.

    • Es gibt einen Artikel, der ein wenig darüber schreibt, wie man Treiber erstellt und mit QEMU ein eigenes Gerät baut und anbindet: [0] https://blog.davidv.dev/posts/learning-pcie/, [1] https://blog.davidv.dev/posts/pcie-driver-dma/
    • Mit virtme-ng https://github.com/arighi/virtme-ng lässt sich ein in Entwicklung befindlicher Kernel in QEMU wirklich sehr einfach starten.
    • Für frühes Kernel-Debugging ohne Konsole wird QEMU häufig verwendet
      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 Bedarf gdb-multiarch und qemu-system-arm zu installieren, dann qemu-system-aarch64 mit -S -gdb tcp::1234 im angehaltenen Zustand zu starten und sich in einem anderen Terminal mit gdb-multiarch ./vmlinux zu verbinden.
      Danach kann man in GDB target remote :1234, break __parse_cmdline, continue ausfü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 mit echo "add-auto-load-safe-path ${SRC_DIR}/scripts/gdb/vmlinux-gdb.py" > ~/.gdbinit
    • Die jetzt im Kernel enthaltene WireGuard-Testsammlung ist ein gutes Beispiel dafür, mit QEMU Kernel-Module zu entwickeln und sogar automatisierte Tests auszuprobieren.
    • Greg KH hat ziemlich klar gesagt, dass es LDD, 4. Auflage nicht geben wird.
  • Verwandte 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.

    • Das Inhaltsverzeichnis sieht gut aus, schade ist nur, dass es keine Vorbestellung gibt, mit der man die Erstellung unterstützen könnte.
  • 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.

    • Etwas umständlich vielleicht, aber man könnte den Compiler einfach auf dem Raspberry Pi selbst laufen lassen.
  • 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 :)