Aus Linux-Magazin 09/2025

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 141

© Olena Sushitska / 123RF.com

Einfacher geht es kaum: Mit einer Zeile Code kann der Kernel Applikationen Zugriff auf ausgewählte Daten gewähren. Dabei hilft das weitgehend unbekannte virtuelle Dateisystem Debugfs.

Linux kann auch verwirren. Neben dem Kommando »debugfs« gibt es ein gleichnamiges Dateisystem, das außer dem Namen rein gar nichts mit dem Tool Debugfs aus der letzten Kern-Technik verbindet. Während das Kommando Debugfs Zugriff auf die Interna des Ext4-Dateisystems gibt, handelt es sich beim hier thematisierten Debugfs um ein eigenes, virtuelles Dateisystem. Es wurde entwickelt, um Gerätetreibern und sonstigen Kernel-Komponenten einen schnellen Weg freizumachen, um Daten zwischen dem Kernel und dem Userland hin und her zu schleusen [1].

Der Connaisseur runzelt jetzt die Stirn: Gab und gibt es dafür nicht das Proc- und das Sys-Filesystem? Wozu braucht man einen weiteren Vertreter virtueller Dateisysteme? Proc ist, wie schon der Name andeutet, für Informationen bezüglich der Tasks zuständig. Sys repräsentiert das System, insbesondere die Hardware. Debugfs dagegen soll primär Debug-Informationen liefern. Es ist hundertprozentig optional und sonstige Systemteile dürfen sich auf irgendwelche Einträge dort nicht verlassen. Etwas professioneller formuliert: Die Schnittstellen (Application Binary Interfaces, ABIs) sind nicht festgelegt und dürfen sich jederzeit ändern. Freiheit für die Entwicklerinnen und Entwickler!

Das Fehlen einer festgezurrten Schnittstelle ist aber nicht gleichbedeutend mit gänzlich fehlender Dokumentation. Tatsächlich finden sich im Dokumentenordner des Linux-Quellcodes unterhalb von »Documentation/ABI/testing/« einige Dateien, deren Namen mit dem Präfix »debugfs_« beginnen. Sie beschreiben die über das Debug-Filesystem ausgetauschten Daten.

Detaillierte Infos

Ein Blick auf Debugfs lohnt sich für Neugierige. Auf den meisten Systemen ist es standardmäßig unterhalb von »/sys/kernel/debug/« eingehängt. Findet es sich dort nicht, geben Sie am besten im Terminal das Kommando »sudo mount -t debugfs none /sys/kernel/debug/« ein. Unterhalb von »/sys/kernel/debug/« finden sich dann eine Reihe von Unterverzeichnissen. Den Dateien unterhalb von »block/« können Sie beispielsweise umfangreiche I/O-Statistiken entnehmen.

Spannend ist auch die Datei »debug« unterhalb von »sched/«. Darin finden Sie für jede CPU sehr detaillierte Informationen zum Zeitverhalten, inklusive der vom Scheduler für die CPU zugewiesenen Tasks. Die Datei »wakeup_sources« listet auf, welche Quellen wie häufig Rechenprozesse aufgeweckt haben. Unterhalb von »tracing/« finden Sie ein vorbildliches »README«, das die Möglichkeiten des Subsystems und dessen Abbildung auf die Dateien erläutert.

So einfach

Wesentlich interessanter ist es aber, Debugfs für die eigenen Zwecke einzuspannen. Es gibt wohl kaum eine einfachere Möglichkeit, dem Userland Zugriff auf interne Daten und Zustände zu gewähren. Ein Funktionsaufruf genügt, und schon findet sich die gewünschte interne Variable im Debug-Filesystem als Datei repräsentiert (Listing 1). Die lässt sich dann per Cat inspizieren beziehungsweise via Echo modifizieren (Abbildung 1).

Abbildung 1: Applikationen greifen per Cat und Echo auf exportierte Debugfs-Daten zu.

Abbildung 1: Applikationen greifen per Cat und Echo auf exportierte Debugfs-Daten zu.

Die dabei notwendige Umsetzung von der ASCII-Eingabe zum Integer erfolgt transparent innerhalb von Debugfs. Sie können sich allenfalls noch entscheiden, ob Sie Integer verwenden oder doch lieber auf die passenderen Hex-Repräsentationen zurückgreifen möchten. In Listing 1 beispielsweise taucht die Variable »internal« einmal als Dezimal- und einmal als Sedezimalwert auf. Das ermöglicht, wie Abbildung 2 zeigt, einen einfachen, Kernel-basierten Konverter zwischen dezimaler und hexadezimaler Darstellung. Neben den diversen Integer-Datentypen offeriert der Kernel noch Strings, Arrays sowie die Typen »size_t«, »bool« und »atomic_t«. Letzterer stellt sicher, dass nicht zwei Entities gleichzeitig die Variable verwenden und schützt damit vor Race Conditions.

Abbildung 2: Ein Dez-Hex-Konverter, produktiv implementiert mit vier Zeilen Code.

Abbildung 2: Ein Dez-Hex-Konverter, produktiv implementiert mit vier Zeilen Code.

Listing 1

miniex.c

#include <linux/module.h>
#include <linux/debugfs.h>
static struct dentry *dir_entry;
static u64 internal = 0;
static int __init debugfs_example_init(void) {
  dir_entry = debugfs_create_dir("linux-magazin", NULL);
  if (!dir_entry)
    return -ENOMEM;
  debugfs_create_u64("decimal", 0666, dir_entry, &internal);
  debugfs_create_x64("hexa", 0666, dir_entry, &internal);
  return 0;
}
static void __exit debugfs_example_exit(void) {
  debugfs_remove(dir_entry);
}
module_init(debugfs_example_init);
module_exit(debugfs_example_exit);
MODULE_LICENSE("GPL");

Die Tabelle “Zentrale Debugfs-Funktionen (Auswahl)” zeigt eine Auswahl der Debugfs-Funktionen und Listing 2 deren beispielhafte Anwendung. Bei den meisten Funktionen ist der erste Parameter der Name der Datei im Debug-Filesystem, danach folgen (oktal angegeben) der Zugriffsmodus und der Verweis auf das Verzeichnis, unterhalb dessen die Datei auftauchen soll. Als Letztes erscheint der Verweis auf die Daten selbst, typischerweise also auf die Adresse einer Variablen.

Funktion

Kurzbeschreibung

»debugfs_create_dir()«

Erstellt ein neues Verzeichnis im Debugfs.

»debugfs_remove()«

Entfernt ein Debugfs-Verzeichnis und dessen Inhalte.

»debugfs_create_file()«

Erstellt eine Datei mit benutzerdefinierten Dateioperationen.

»debugfs_create_u8()«

Exportiert eine 8-Bit-Variable als les- und schreibbare Datei.

»debugfs_create_u64()«

Exportiert eine 64-Bit-Variable als Datei.

»debugfs_create_ulong()«

Exportiert eine Variable des Typs »unsigned long«.

»debugfs_create_x16()«

Exportiert eine 16-Bit-Variable im hexadezimalen Format.

»debugfs_create_x32()«

Exportiert eine 32-Bit-Variable im hexadezimalen Format.

»debugfs_create_size_t()«

Exportiert eine Variable des Typs »size_t«.

»debugfs_create_atomic_t()«

Exportiert einen Zähler des Typs »atomic_t«.

»debugfs_create_bool()«

Exportiert eine boolesche Variable als Datei.

»debugfs_create_str()«

Exportiert einen String (»char[]«) als Datei.

»debugfs_create_blob()«

Exportiert einen beliebigen Speicherbereich (Blob, read-only).

»debugfs_create_regset32()«

Exportiert einen Satz von Hardwareregistern als Tabelle.

»debugfs_create_array()«

Exportiert ein Array von Werten identischen Typs.

»debugfs_print_regs32()«

Gibt Registerwerte formatiert in ein »seq_file« aus.

Listing 2

debugfs.c

#include <linux/module.h>
#include <linux/debugfs.h>
#include <linux/uaccess.h>
#include <linux/atomic.h>
#include <linux/slab.h>
#include <linux/io.h>
#define BUF_SIZE 128
#define MMIO_REGION_SIZE 0x10
static struct dentry *dir_entry;
static u32 counter = 0;
static bool my_flag = false;
static atomic_t atomic_counter = ATOMIC_INIT(0);
static char kernel_buffer[BUF_SIZE] = "Initial content\n";
static struct debugfs_blob_wrapper blob;
static void __iomem *hw_mmio_base;
static const struct debugfs_reg32 pseudo_regs[] = {
  { "CONTROL", 0x00 },
  { "STATUS",  0x04 },
  { "DATA",    0x08 },
  { "VERSION", 0x0c },
};
static struct debugfs_regset32 my_regset = {
  .regs = pseudo_regs,
  .nregs = ARRAY_SIZE(pseudo_regs),
  //.base = hw_mmio_base,  // can't be initialized here
};
/* callbacks for debugfs-files */
static ssize_t debugfs_read(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) {
  return simple_read_from_buffer(user_buf, count, ppos, kernel_buffer, strlen(kernel_buffer));
}
static ssize_t debugfs_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) {
  ssize_t len = min(count, (size_t)(BUF_SIZE - 1));
  if (copy_from_user(kernel_buffer, user_buf, len))
    return -EFAULT;
  kernel_buffer[len] = '\0';
  return len;
}
static const struct file_operations fops = {
  .owner = THIS_MODULE,
  .read  = debugfs_read,
  .write = debugfs_write,
};
static int __init debugfs_example_init(void) {
  dir_entry = debugfs_create_dir("linux-magazin", NULL);
  if (!dir_entry) return -ENOMEM;
  debugfs_create_u32("counter", 0666, dir_entry, &counter);
  debugfs_create_x32("xvalue", 0666, dir_entry, &counter);
  debugfs_create_bool("my_flag", 0666, dir_entry, &my_flag);
  debugfs_create_atomic_t("atomic_counter", 0666, dir_entry, &atomic_counter);
  debugfs_create_file("text_buffer", 0666, dir_entry, NULL, &fops);
  blob.data = kmalloc( GFP_KERNEL, 128 );
  strncpy( blob.data, "This is a blob.\n", 128);
  blob.size = strlen(blob.data);
  debugfs_create_blob("blob_file", 0444, dir_entry, &blob);
  /* simulated memory mapped region */
  hw_mmio_base = kzalloc(MMIO_REGION_SIZE, GFP_KERNEL);
  if (!hw_mmio_base)
    return -ENOMEM;
  my_regset.base = hw_mmio_base;
  /* just some sample values */
  iowrite32(0xDEADBEEF, hw_mmio_base + 0x00);
  iowrite32(0x0000BEEF, hw_mmio_base + 0x04);
  iowrite32(0x12345678, hw_mmio_base + 0x08);
  iowrite32(0x00000001, hw_mmio_base + 0x0C);
  debugfs_create_regset32("hw-regs", 0444, dir_entry, &my_regset);
  return 0;
}
static void __exit debugfs_example_exit(void) {
  debugfs_remove(dir_entry);
}
module_init(debugfs_example_init);
module_exit(debugfs_example_exit);
MODULE_LICENSE("GPL");

Knigge

Zu guter Letzt spendieren Sie dem Einzeiler noch eine Aufräumfunktion – schließlich wollen Sie beim Entladen des Kernel-Moduls keine unschönen Reste hinterlassen. Dabei könnten Sie beispielsweise die Funktion »debugfs_lookup_and_remove()« einsetzen. Ihr übergeben Sie den Namen der zu löschenden Datei und den Verweis auf das Verzeichnis, in dem sie sich befindet.

Wie bei den meisten Funktionen von Debugfs erhalten Sie hier übrigens keine Rückmeldung, ob die Aktion erfolgreich war. Eine solche Meldung ist aber auch gar nicht notwendig, da die Funktionen fail safe implementiert sind. Die bessere Variante stellt jedoch der Einsatz der Funktion »debugfs_remove(struct dentry *dentry)« dar. Der Debugfs-Knigge gibt vor, dass jede Komponente ein eigenes Verzeichnis erstellt, in dem sich die zugehörigen Dateien tummeln. Der Aufwand dafür ist so gering, dass es keinen Grund gibt, das zu unterlassen.

Die Funktion »debugfs_create_dir(char *name, struct dentry *parent)« ist dann auch eine der wenigen Funktionen, die einen Rückgabewert liefern, nämlich den Verweis auf das erzeugte Verzeichnis. Der Aufruf von »debugfs_remove()« hat den Vorteil, dass mit einem Funktionsaufruf sämtliche vorher im fraglichen Verzeichnis angelegten Dateien zusammen mit dem Verzeichnis gelöscht werden. Chapeau!

Komplexe Datentypen

Falls es mehr als nur eine Zahl zu publizieren gilt, wird es etwas komplizierter. Zunächst bietet Debugfs einen Blob-Datentyp an, repräsentiert durch die Adresse des zugehörigen Speichers und dessen Länge. Der Blob stellt also einen beliebig langen Speicherbereich zur Verfügung. Welche Daten dort stehen, spielt dabei keine Rolle.

Der Vorteil für die Entwicklung: Auch hier genügt eine einzige Funktion, um dem Userland Zugriff auf umfangreichere Daten zu geben. Aber Vorsicht: Beim achtlosen Einsatz eines Blobs fliegt einem schon einmal das System um die Ohren. Falls nämlich der Kernel die Daten ändert, während das Userland darauf zugreift, erhält das Userland einen inkonsistenten Datensatz (Abbildung 3).

Abbildung 3: Bei Datenstrukturen droht die Gefahr von Race Conditions.

Abbildung 3: Bei Datenstrukturen droht die Gefahr von Race Conditions.

Aber es kommt noch schlimmer: Wurde der Speicherbereich im Kernel dynamisch alloziert oder liegt er gar auf dem Stack und während des Zugriffs wird der Speicher freigegeben oder der Stack-Frame verlassen, dann knallt es richtig (Use-after-Free). Immerhin gewährt Linux Applikationen beim Blob nur lesenden Zugriff – das räumt schon einmal eine Falle beiseite.

Die Quintessenz: Wenn es um statische Daten wie eine Versionsnummer oder Ähnliches geht, also um Daten, die sich naturgemäß allenfalls selten ändern, kann der Debugfs-Blob die passende Methode sein.

Parameterliste

Ansonsten greifen Sie zum Makro »debugfs_create_file(name, mode, parent, data, fops)«. Bei den ersten drei Parametern handelt es sich um den Namen der Datei, die oktalen Zugriffsrechte und einen Verweis auf das Unterverzeichnis. Der vierte Parameter dient als frei definierbarer Parameter, im Regelfall werden Sie ihn wohl mit null belegen. Sie können damit aber im Prinzip machen, was immer Ihnen sinnvoll erscheint.

Der fünfte Parameter schließlich ist die Adresse einer Struktur »struct file_operations«, wie man sie generell in der Treiberprogrammierung nutzt. Darin hängt man die Adressen einer Lese- oder Schreibfunktion ein, die es dann noch zu implementieren gilt. Da man sich in diesem Einsatzfeld in der Regel nicht mit dem Kontrollfluss beschäftigen muss, also dem “Schlafenlegen”, ist das nicht allzu schwierig. Tatsächlich bietet der Kernel mit »simple_read_from_buffer()« und »simple_write_to_buffer()« sogar generische Funktionen an, die die in diesem Fall notwendige Logik realisieren.

Insofern müssen Sie sich nicht mehr um den Transfer als solchen kümmern, dass also nicht zu viele Daten kopiert werden und der Stand des Offset-Zählers stimmt. Andererseits versteckt sich auch hier der Teufel im Detail, der in Verkleidung einer Instanz hinterhältig parallel oder gestückelt zugreift. Lassen Sie die Schreibfunktion weg, ist dieser Beelzebub schon einmal auf den lesenden Zugriff beschränkt.

Den parallelen Zugriff bekommen Sie durch schlichtes Sequenzialisieren in den Griff, beispielsweise per Spinlock. Letzten Endes haben Sie die Kontrolle, da Sie die Lese- und die Schreibfunktion selbst implementieren. Das Problem des gestückelten Zugriffs, bei dem eine Applikation die Daten immer nur häppchenweise liest, haben wir bereits beim Proc- und Sys-Filesystem kennengelernt. Hier unterstützt Sie der Kernel mit dem Single- oder Sequenz-File [2].

Für Gerätetreiber

Bleibt noch eine letzte Rarität übrig, die Ausgabe von Register-Dumps, wie sie häufig in Gerätetreibern zu Debug-Zwecken vorkommt. Dazu beschreiben Sie in einem Array die Hardwareregister (Memory-mapped-Speicherzellen) per Namen und Offset zum Start des Speicherbereichs. Die Adresse dieses Arrays zusammen mit der Anzahl der Array-Elemente, der Basisadresse und einem optionalen Verweis auf das Device-Objekt legen Sie in einer Struktur »struct debugfs_regset32« ab. Der Verweis auf das Device-Objekt nutzt der Kernel – falls vorhanden – beispielsweise für das Power-Management.

Sie übergeben diese Datenstruktur einfach der Funktion »debugfs_create_regset32()«, und das Debug-Filesystem kümmert sich um den Rest. Greift eine Applikation auf die zugehörige Datei zu, bekommt sie den Inhalt der Speicherstellen zusammen mit dem Namen sauber aufgelistet. Die Funktion selbst verwendet intern »debugfs_create_file()« in Kombination mit einem Sequenz-File, nur dass Debugfs die eigentliche Ausgabefunktion von 32 Bit breiten Speicherzellen verwaltet. Dazu dient die Funktion »debugfs_print_reg32()«, die Ihnen auch für das Finetuning zur Verfügung steht.

Für Einsteiger

Um das Debugfs mit eigenen Einträgen zu testen, installieren Sie vorab auf Ihrem Ubuntu-System per »sudo apt install build-essential flex bison« die notwendigen Werkzeuge. Dann speichern Sie den Code aus Listing 2 als Datei »debugfs.c«, die Sie mithilfe des Makefiles aus Listing 3 zum Kernel-Modul »debugfs.ko« kompilieren.

Listing 3

Makefile

obj-m += debugfs.o
KDIR = /lib/modules/$(shell uname -r)/build
all:
  make -C $(KDIR) M=$(shell pwd) modules
clean:
  make -C $(KDIR) M=$(shell pwd) clean

Das erfolgreich generierte Modul laden Sie anschließend per »insmod debugfs.ko« (Abbildung 4). Der Beispielcode erzeugt das neue Verzeichnis »/sys/kernel/debug/linux-magazin/«, in das Sie nun wechseln. Sie können dort auf die neuen Dateien sowohl lesend (per Cat) als auch schreibend (per Echo und Ausgabeumlenkung) zugreifen.

Abbildung 4: F&uuml;r Einsteiger geeignet: Debugfs im eigenen Kernel-Modul.

Abbildung 4: Für Einsteiger geeignet: Debugfs im eigenen Kernel-Modul.

Der Code in »debugfs.c« zeigt Ihnen, wie Sie alle hier vorgestellten Möglichkeiten von Debugfs beispielhaft einsetzen und inwiefern Debugfs ein mächtiges Hilfsmittel ist, um Kernel-Module bequem zu testen und zu analysieren. Gerade bei der Entwicklung neuer Treiber oder bei der Fehlersuche im Kernel erweist es sich als tolles Hilfsmittel. Außerdem kann man kaum einfacher erste Erfahrungen in der Kernel-Programmierung sammeln. Probieren Sie es doch einfach einmal auf einem (nicht im Produktiveinsatz befindlichen) System aus! (jlu)

Der Autor

Jürgen Quade, Professor an der Hochschule Niederrhein, führt für Unternehmen Schulungen zu den Themen Treiberprogrammierung und Embedded Linux durch.

Infos

  1. Linux-Kernel-Dokumentation: https://docs.kernel.org/filesystems/debugfs.html
  2. Kern-Technik: Jürgen Quade, “Leckerer Zaubertrank”, LM 01/2024, S. 66, https://www.lm-online.de/48908
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben