3.1. Auf der Kommandoebene entwickeln

Wer einen Treiber zusammenklicken will, tut sich unter Linux schwer. Hierfür gibt es kaum Unterstützung. Das ist auch nicht notwendig, gab es doch unter Unix bereits vor dem Durchbruch grafischer Benutzeroberflächen ausgereifte Entwicklungswerkzeuge, mit denen Entwickler ihre Programme effizient realisieren. Auch die Entwicklung und Generierung des Linux-Kernels selbst kommt ohne eine so genannte »Integrierte Entwicklungsumgebung« aus (siehe Anhang Kernel generieren und installieren).

Zur Treiberentwicklung werden die folgenden Werkzeuge eingesetzt:

Diese Programme werden über die Shell gestartet. Die Shell ist ein Kommandointerpreter – ein Programm, das Eingaben über die Tastatur entgegennimmt, auswertet, zugehörige Systemcalls startet und Ausgaben auf dem Bildschirm anzeigt.

Der Profi, der die Kommandos der Shell kennt und die Werkzeuge zu bedienen weiß, ist mit dieser Methode besonders effektiv. So ist er mit nur einem Kommando in der Lage, seinen Treiber zu generieren, zu testen und beispielsweise Sicherungskopien anzulegen. Treten darüber hinaus Fehler bei der Generierung des Treibers auf, wird der Treiberentwickler die Ursache in kürzerer Zeit aufspüren können – schließlich kennt er die Abläufe im Detail.

Im Folgenden sollen zunächst Shell und Werkzeuge kurz vorgestellt werden.

Shell

Abbildung 3-2. Datenflüsse in der Shell (stark vereinfacht)

Die Shell ist der Rechenprozess im System, der die Benutzereingaben entgegennimmt, auswertet und entsprechende Dienste (Systemcalls) des Betriebssystems aufruft. Die Standard-Shell bei Linux heißt bash (Bourne Again Shell). Sie wird automatisch gestartet, sobald man sich an einer Textkonsole anmeldet oder auf einer grafischen Oberfläche (KDE) ein Konsolenfenster öffent. Die Standardkommandos der Shell und die wichtigsten Systemprogramme sind:

Tabelle 3-1. Die wichtigsten Kommandos in der Kurzübersicht

KommandoBeschreibungBeispiel
man <Kommando> Anzeigen der Manualpage Kommando. man ls
cd <DirName> Wechselt in das Verzeichnis DirName. Wird kein Verzeichnis angegeben, wird in das Home-Verzeichnis des Benutzers gewechselt. Wird anstelle des Verzeichnisnamens ein Strich (»-«) angegeben, wird in das zuletzt verwendete Verzeichnis gewechselt. cd /usr/src/linux
ls Das Kommando zeigt den Inhalt (sprich die Dateien und Unterverzeichnisse) eines Ordners an. Ohne Pfadangabe wird der Inhalt des aktuellen Verzeichnisses ausgegeben. ls /usr/src/linux/
mkdir <DirName> Legt in dem aktuellen Verzeichnis ein neues Verzeichnis mit dem Namen DirName an. mkdir TreiberTest
rm <Dateiname> Löscht die Datei Dateiname. rm treiber.o
tail <Dateiname> Gibt das Ende einer Datei auf den (aktiven) Bildschirm aus. In Verbindung mit der Option »-f« wird eine Datei daraufhin überprüft, ob an ihr etwas angehängt wurde. Ist das der Fall, werden die angehängten Daten ausgegeben. tail -f /var/log/messages
ps -awxu Zeigt alle im System vorhandenen Prozesse an. Mit Hilfe dieses Kommandos lassen sich die PIDs gesuchter Prozesse identifizieren. ps -awxu
kill <pid> Schickt der Task mit der PID pid ein Signal. Welches Signal geschickt werden soll, kann per Option angegeben werden. In der Variante killall kann anstelle der PID auch der Programmname verwendet werden. kill -HUP 1927
> Das »>«-Zeichen wird anderen Kommandos angehängt, um die Ausgabe des Kommandos umzulenken. Mit Hilfe des >-Zeichens lassen sich so sehr leicht Ausgaben in eine Datei umlenken. make >CompilerAusgabe
< Das »<«-Zeichen dient dazu, die Eingabe nicht von der Tastatur, sondern aus einer Datei zu nehmen. cat <treiber.c

Über das Kommando man lassen sich Syntax und Funktion der Kommandos anzeigen. Unter Oberflächen wie beispielsweise KDE lässt sich diese Information auch über das Hilfesystem abrufen.

Rechenprozesse können während der Abarbeitung kurzfristig unterbrochen werden. Dazu schickt man ihnen mit Hilfe des Kommandos bzw. Systemcalls kill ein Signal. Eine Applikation kann Signale akzeptieren oder blocken. Wenn sie diese nicht blockt, muss sie einen Signal-Handler zur Verfügung stellen, der aufgerufen wird, sobald ein Signal eintrifft. Geschieht dies nicht, bricht das Betriebssystem die Task ab. Insbesondere für Treiberentwickler ist es wichtig zu wissen, dass Signale laufende Systemcalls unterbrechen und beenden. Schließlich ist der Treibercode Teil eines solchen Systemcalls und implementiert als dieser den Abbruch und die Rückgabe eines geeigneten Fehlerwertes.

Zusätzliche, wichtige Systeminformationen finden sich im /proc-Verzeichnis. Die dortigen Dateien und Verzeichnisse werden dynamisch vom Betriebssystem erzeugt, sind also nicht auf irgendeiner Festplatte oder einem anderen Hintergrundspeicher existent. Belegte Systemressourcen lassen sich über die folgenden Kommandos anzeigen:

cat /proc/interrupts
cat /proc/cpuinfo
cat /proc/ioports

In der dynamisch erzeugten Datei cpuinfo finden sich beispielsweise alle Informationen, die über die CPU bekannt sind, wie beispielsweise CPU-Typ, Taktfrequenz, Stepping, Cache-Größe oder auch bekannte CPU-Fehler.

Editor

Der Treiberentwickler hat unter Linux diverse Editoren zur Auswahl. Welcher Editor gewählt wird, ist sicherlich Geschmackssache. Prinzipiell stehen ausgesprochen leistungsfähige, professionelle Editoren leicht erlernbaren und intuitiv bedienbaren Editoren gegenüber.

Der Standardeditor unter Unix schlechthin ist der vi. Die ursprüngliche vi-Version wird allerdings längst übertroffen durch diverse Kopien, die seine Leistungsfähigkeit nochmals erweitert haben. Hier ist insbesondere der vim (Vi IMproved) zu nennen. Der wesentliche Unterschied (und zugleich der Grund für die unerschöpfliche Leistungsfähigkeit) zu den übrigen Editoren ist, dass der vi bzw. der vim über einen Eingabe- und einen Kommando-Modus verfügt. Jedes Mal, wenn eine Eingabe vollständig ist, und der Entwickler an eine andere Stelle der Datei möchte, sollte er in den Kommandomodus umschalten. Der für viele auf Anhieb nicht einsichtige Vorteil liegt darin, dass der Editor das letzte Kommando inklusive der Eingabe kennt und einfach (über das Punktkommando ».«) wiederholen kann. Da die Kommandos sehr mächtig sind, erleichtert dieses Feature die Editieraufgaben erheblich.

Im vim ist es möglich, ohne komplizierte Verwendung von Umschalttasten (und selbstverständlich ohne Maus) sämtliche Editierfunktionen aufzurufen. Eine Eigenschaft, die insbesondere Entwickler von eingebetteten Systemen zu schätzen wissen, da dieser Editor keine grafische Window-Umgebung benötigt. Damit lässt sich der Editor zuweilen sogar auf dem eingebetteten System selbst einsetzen. Doch auch für den normalen Treiberentwickler überzeugt der vim mit einem ganzen Bündel an Leistungsmerkmalen: Aus dem vim lassen sich direkt Kommandos absetzen zur Generierung des Treibers (Befehl :make). Das Einrücken von Programmteilen erfolgt automatisch und ist individuell konfigurierbar. Syntax-Highlighting wird ebenso angeboten wie das Suchen und Ersetzen über reguläre Ausdrücke. Entwickler, die keine Versionsverwaltung nutzen, können über den Diff-Modus Dateien vergleichen.

Von Nachteil ist der ausgesprochen hohe Einarbeitungsaufwand. Die Tastaturkürzel erscheinen gerade Neueinsteigern ziemlich kryptisch. Ein halbstündiges Einarbeitungsprogramm (vimtutor) erleichtert dem Neuling den Einstieg.

Der zweite, sehr verbreitete Editor für den Profi ist der Emacs (Editor MACroS). In puncto Popularität und Leistungsfähigkeit steht er dem vim nicht nach. Der Emacs ist bekannt für seine Flexibilität und seine Fähigkeit, leicht angepasst werden zu können – benötigt aber mehr Hauptspeicherressourcen.

Der Emacs selbst ist in der Programmiersprache Lisp geschrieben. So hat der Emacs, in gewissem Sinne, eine ganze Programmiersprache »in sich eingebaut«, um ihn anzupassen, zu erweitern und sein Verhalten verändern zu können. Infolgedessen stellt er mehr eine integrierende Umgebung als nur einen Editor dar. Von Haus aus wird eine Vielzahl von Macros mitgeliefert (C, C++, Java, make, Latex etc.), um den Emacs als komplette Entwicklungsumgebung einsetzen zu können. Treiber-Entwicklern wird neben Syntax-Highlighting auch eine Anbindung an die Versionsverwaltung CVS geboten. Der Compiler lässt sich aus dem Editor heraus aufrufen.

Ein Derivat des Emacs mit einem deutlichen Schwerpunkt auf die Unterstützung von grafischen Oberflächen ist der XEmacs.

Klick-gewohnte Ein- oder Umsteiger wählen sicherlich den unter KDE verfügbaren Editor kate oder den unter GNOME verfügbaren Editor gedit. Beide Editoren sind intuitiv zu bedienen und besitzen alles, was ein moderner Editor benötigt. Bei kate ist dies beispielsweise Syntax-Highlighting oder sogar ein integrierter Terminalemulator (Shell), über die der Treiber direkt kompiliert und getestet werden kann.

Compiler

Der zur Treibererzeugung vorwiegend eingesetzte Compiler ist der GNU-Compiler GCC. In der Datei /usr/src/linux/Documentation/Changes schreibt Linus Torvalds die für die Kernel- und Treibergenerierung zu verwendende Compiler-Version vor. Gegenwärtig wäre dies Version 2.95.3. Allerdings ist diese Angabe wohl veraltet, verwenden doch die meisten Kernel-Programmierer eine Compilerversion, die eine 3 vor dem ersten Punkt stehen hat.

Wer einen Blick auf den Kernelcode riskiert, stellt an einigen Stellen – insbesondere auch bei den Treibern – fest, dass in der C-Programmiersprache unbekannte, zusätzliche Schlüsselworte (z.B. __init) verwendet werden. Hierbei handelt es sich um Makros oder Defines, die proprietäre Erweiterungen des Compilers verstecken und die Codegenerierung beeinflussen. Genaueres dazu findet sich in den Abschnitten Management implizit zugeteilter Ressourcen und Datentypen und Datenablage.

Für den Programmierer von praktischer Bedeutung sind die Möglichkeiten, die der Präprozessor des Compilers bietet. Seine Kommandos sind innerhalb der Programmiersprache C anhand der vorgestellten Raute »#« erkennbar. Der Präprozessor hat die bekannten Aufgaben:

Der Präprozessor wird – wie auch der Linker – automatisch durch den Compiler aufgerufen.

Für die Treiberentwicklung sind insbesondere die Möglichkeiten des Präprozessors zur bedingten Kompilierung interessant. Über Variablen lassen sich Passagen des Quellcodes mit in den Übersetzungsvorgang einschließen bzw. ausschließen. Geradezu klassisch ist die Verwendung der bedingten Kompilierung für die Generierung einer Produktivversion und einer Debugversion:

#ifdef DEBUG
    fprintf(stderr,"Zwischenergebnis = %d\n", returnvalue );
#endif

Wird entweder im Quellcode die Variable DEBUG gesetzt

#define DEBUG
oder aber wird die Variable beim Aufruf des Compilers über die Option -D gesetzt
$ gcc -DDEBUG quellcode.c
wird die Zeile mit der Funktion fprintf übersetzt. Ansonsten würde die Zeile fehlen.

Das Beispiel verdeutlicht, dass sich Präprozessorvariablen beim Aufruf des Compilers definieren lassen bzw. dass ihnen beim Aufruf Werte zugewiesen werden können.

Soll für einen Treiber eine Codebasis verwendet werden, um daraus für unterschiedliche Kernelversionen (beispielsweise eine Version für Kernel 2.4 und eine für 2.6) Treiber zu generieren, wird ebenfalls auf den Mechanismus der bedingten Kompilierung zurückgegriffen. In den Header-Dateien des Linux-Kernels sind die Makros LINUX_VERSION_CODE und KERNEL_VERSION definiert. Ersteres ergibt die Versionsnummer des Linux-Kernels, Letzteres lässt die Definition einer spezifischen Versionsnummer zu.

Eine versionsbedingte Kompilierung der Treiberquellen kann anhand des folgenden Codes erreicht werden:

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
    Old-Version here
#else
    New-Version here
#endif

Ein nicht unbedingt allgemein bekanntes Feature moderner Compiler betrifft die Definition und Initialisierung einzelner Datenstruktur-Elemente. Mit Nennung der Elementnamen lassen sich einzelnen Elementen gezielt Werte zuweisen. Die Struktur struct file_operations (in <linux/fs.h>) enthält beispielsweise rund 18 Elemente. Ohne die Anzahl und die Reihenfolge der Elemente kennen zu müssen, lassen sich nach folgendem Schema die Elemente owner, read, write, open und release belegen:

static struct file_operations fops = {
    .owner=   THIS_MODULE,
    .read=    &driver_read,  /* read */
    .write=   &driver_write, /* write */
    .open=    &driver_open,  /* open */
    .release= &driver_close, /* release */
};

Make

Eines der wichtigsten Programmentwicklungswerkzeuge unter Linux ist make. Es automatisiert viele Aufgaben der Entwicklung, so zum Beispiel die Generierung von Kernel und Treiber durch Aufruf des Compilers oder auch das Anfertigen eines Backups. Von der Idee her ist make relativ simpel: Make arbeitet Regeln ab. Die Regeln bestehen dabei aus zwei Teilen, einer Bedingung und einem Ausführungsteil. Der Bedingungsteil wiederum besteht aus der unabhängigen und der abhängigen Komponente.

# Regel
AbhängigerTeil: UnabhängigerTeil   # Bedingung
    Kommando                       # Ausführungsteil

Die abhängige Komponente, z.B. das zu erzeugende Testprogramm mit dem Namen treibertest, nach der die Regel benannt ist, steht am Zeilenanfang und wird von der unabhängigen Komponente (in diesem Fall der Quelltext treibertest.o) durch Doppelpunkt getrennt.

treibertest: treibertest.o
    gcc -o treibertest treibertest.o

Bedingungen sind zunächst immer zeitlicher Natur. Make überprüft die Regeln daraufhin, ob die abhängige Seite (»treibertest«) existiert – und wenn sie existiert, ob diese älteren oder jüngeren Datums als die unabhängigen Teile (»treibertest.o«) ist. Ist die abhängige Seite älter, wird der Ausführungsteil bearbeitet. Gibt bei der Bearbeitung ein von make aufgerufenes Programm einen Fehlercode zurück, wird die Bearbeitung durch make abgebrochen.

Bevor make eine Regel abarbeitet, überprüft es die unabhängigen Komponenten daraufhin, ob sich auf diese eine der ansonsten vorhandenen Regeln anwenden lässt. Ist dies der Fall, wird zunächst diese Regel ausgeführt.

Existiert beispielsweise die Regel

treibertest.o: treibertest.c
    gcc -O -c treibertest.c
dann wird diese noch vor der zuvor beschriebenen Regel abgearbeitet.

Die Regeln selbst werden in einer Datei abgelegt, die in den meisten Fällen Makefile oder makefile heißt. Zu Beginn des Makefiles – vor den Regeln – können Variablen definiert werden. Diese Variablen können innerhalb der Regeln verwendet werden. So lassen sich beispielsweise die Compiler-Optionen über eine Variable festlegen. In diesem Fall muss bei geänderten Optionen nicht gleich jede Regel geändert werden. Vielmehr reicht es aus, die Optionen in der Variablen anzupassen.

Die Mächtigkeit von make liegt zum einen darin begründet, dass bereits viele Regeln und Variablen in make eingebaut sind und zum anderen darin, dass sich die Regeln nicht nur auf Quelltexte anwenden lassen, sondern auf alle Dateien, die zu einem Projekt gehören. Außerdem können im Ausführungsteil Shell- bzw. Systemkommandos verwendet werden.

Beispiel 3-1. Einfaches Makefile

CFLAGS=-g -O -Wall                                         (1)

all:	treibertest                                           (2)
                                                           (3)
treibertest.o: treibertest.c                               (4)
    $(CC) $(CFLAGS) -c treibertest.o                       (5)

treibertest: treibertest.o
    $(CC) -o treibertest treibertest.o

devices:                                                   (6)
    rm -f MeineGeraeteDatei                                (7)
    mknod MeineGeraeteDatei c 240 0
    chmod a+rw MeineGeraeteDatei

clean:
    rm -f *~ treibertest *.o

backup: clean
    (cd ..; tar cvfz /tmp/driver`date -u +%Y%j`.tgz driver)
(1)
Ein Makefile besteht aus zwei Teilen, einem Definitions- und einem Regelteil. In diesem Beispiel besteht der Definitionsteil aus einer einzigen Definition. Es wird die Variable CFLAGS belegt.
(2)
Eine Regel ist an dem Doppelpunkt erkennbar. Die erste Regel eines Makefiles wird als Default-Regel bezeichnet. Regeln bestehen aus zwei Teilen, einem Bedingungsteil und einem Ausführungsteil. Der Bedingungsteil selbst ist wiederum aus zwei Komponenten zusammengesetzt, die durch Doppelpunkt voneinander getrennt sind: der abhängigen und der unabhängigen Komponente. Diese Komponenten sind normalerweise die Namen von Dateien. Die Datei all hängt demnach von der Datei treibertest ab. Ist die Datei treibertest jünger (editiert) als die Datei all, oder existiert all überhaupt nicht, wird der Ausführungsteil abgearbeitet. Zuvor jedoch wird überprüft, ob die Datei treibertest »up to date« ist. Dazu sucht make eine entsprechende Regel im Makefile, die auf treibertest angewendet werden kann.
(3)
Der Ausführungsteil der Regel ist optional. Im Fall dieser Regel bedeutet das, dass die Datei all nie generiert wird. Die Default-Regel wird auf diese Weise dazu genutzt, die eigentlichen sonstigen Regeln anzustoßen (zu »triggern«).
(4)
Dies ist eine typische Regel. Der unabhängige Teil darf auch aus mehreren Dateien bestehen.
(5)
In diesem Ausführungsteil wird die abhängige Komponente erzeugt. Dazu ist der Aufruf des Compilers notwendig. Anstatt den Compiler direkt per Kommando aufzurufen, wird eine Variable (CC) verwendet. Durch diese lässt sich im Definitionsteil ein Compiler sehr einfach auswählen, falls mehrere zur Verfügung stehen. Der Ausführungsteil ist für make daran erkennbar, dass die auszuführenden Befehle eingerückt sind. Zum Einrücken muss unbedingt ein TABULATOR verwendet werden! Leerzeichen sind nicht erlaubt.
(6)
Dies ist eine Regel ohne unabhängige Komponente. Auch existiert die Datei devices nicht bzw. wird nicht als solche generiert. Wird make daher mit dem Namen dieser Regel (make devices) aufgerufen, wird der zugehörige Ausführungsteil immer abgearbeitet.
(7)
Der Ausführungsteil darf auch durchaus aus mehreren Kommandos bestehen. Diese werden der Reihe nach abgearbeitet.

Im Beispiel Einfaches Makefile sind noch die beiden Regeln clean und backup hervorzuheben. Die Regel clean räumt auf, indem alle nicht unbedingt benötigten Dateien bzw. generierbare Dateien gelöscht werden. Die Regel backup erleichtert das Anlegen eines Backups, welches – so die Regel hier – im Verzeichnis /tmp abgelegt wird. Der Aufruf des Kommandos date mit den Optionen »%Y%j« sorgt dafür, dass der Name der Backup-Datei eine Nummer enthält, die den Tag im Jahr (von 1 bis 365) repräsentiert (beispielsweise wird der 10. Januar 2004 zu »04010«).

Versionsverwaltung

Treiberentwickler sollten, wie jeder andere Entwickler auch, ihren Quellcode managen. Versionsverwaltungssysteme helfen dabei, die Entwicklungsschritte zu protokollieren und neuen Fehlern auf die Spur zu kommen. Welches Tool verwendet wird, hängt vor allem von der Komplexität des Projektes ab. Bei größeren Projekten wird gern auf das mächtige CVS (Concurrent Versions System) zurückgegriffen. Dies unter anderem deshalb, weil CVS die Möglichkeit offeriert, dass verschiedene Entwickler parallel an derselben Programmdatei arbeiten können.

Mit RCS (Revision Control System) bietet Linux ein zwar nicht netzwerkfähiges, jedoch einfach handhabbares Werkzeug zur Versionsverwaltung an. Bei kleineren Treiberprojekten überzeugt der vergleichsweise geringe Einarbeitungsaufwand – weswegen die wesentlichen Gründzüge von RCS an dieser Stelle kurz aufgeführt seien:

Die beiden Kommandos ci und co reichen aus, Versionen »einzuchecken« bzw. »auszuchecken«. Dabei wird die Datei in der Versionsverwaltung abgelegt und durch eine Versionsnummer gekennzeichnet. Alte Versionen lassen sich jederzeit wieder herausholen.

Zur Verwendung von RCS legt man am einfachsten ein Verzeichnis mit dem Namen RCS an. Möchte der Entwickler eine Datei, z.B. den Quellcode treiber.c, einchecken, reicht das Kommando

# ci -l treiber.c
aus. Die Option -l bedeutet dabei, dass die Datei für die Versionsverwaltung im Zustand »ausgecheckt« bleiben soll, dass also die Datei weiter editiert werden darf (andernfalls setzt die Versionsverwaltung die Schreibrechte auf die Datei zurück).

Beim Einchecken verlangt die Versionsverwaltung eine Beschreibung der durchgeführten Änderungen. Die Änderung wird abgeschlossen, indem in einer Zeile nur ein Punkt eingegeben wird.

Verwendet man beim Einchecken die Option -l, wird das Kommando zum Auschecken (co) nicht benötigt.

Soll die aktuelle Version ausgecheckt (und editiert) werden, ist einfach nur co mit dem Namen der Datei aufzurufen:

# co -l treiber.c

Die Option -r checkt eine alte Version aus.

# co -r1.1 treiber.c  # checkt die Version 1.1 aus.

Verzichtet man auf die Kommentierung der Änderungen, lässt sich die Versionsverwaltung auch ohne interaktive Abfrage nutzen. Dann kann der Treiberentwickler die Versionen automatisiert (beispielsweise im Makefile) verwenden. Die Regel, die die Datei vor jedem Kompilier-Vorgang eincheckt, zeigt Beispiel Makefile mit Versionsverwaltung.

Beispiel 3-2. Makefile mit Versionsverwaltung

CFLAGS=-O -Wall
CC=gcc

treibertest.o: treibertest.c
    ci -l -m"Automatischer Checkin" treibertest.c
    $(CC) $(CFLAGS) -c treibertest.c

Die Informationen, die die Versionsverwaltung generiert – beispielsweise die Versionsnummer – lassen sich auch in den Quelltext übernehmen. Dazu sind im Quelltext die Schlüsselworte

$Author: $
$Id: $
$Date:$
einzubinden. Wird der Quelltext eingecheckt, geht RCS durch die Datei und sucht nach den Schlüsselworten. Werden sie gefunden, fügt RCS zwischen dem Doppelpunkt und dem hinteren Dollarzeichen die gewünschte Information ein.

Hiermit kann man im Programmcode beispielsweise die RCS-ID als Versionsnummer übernehmen:

static char *version = "$Id:$";
...

Treiber testen

Jeder Treiber muss ausgiebig und strukturiert getestet werden. Dazu ist im Regelfall ein geeigneter Satz von Testprogrammen zu erstellen. Da unter Unix jedoch Geräte auf Dateien abgebildet werden, lassen sich erste Treibertests mit vorhandenen Systemprogrammen (beispielsweise cat, echo, dd und cp) durchführen.

Das funktioniert natürlich nur, wenn der Treiberentwickler die eingeführten Schnittstellen auch systemkonform implementiert hat.

Wird beispielsweise das Kommando cat in Verbindung mit der zum Treiber gehörigen Gerätedatei ausgeführt, werden innerhalb des Treibers die Funktionen open, read und release aufgerufen. Nutzt man darüber hinaus die Möglichkeit, die Ausgabe des Kommandos umzulenken, wird auch die Funktion write innerhalb des Treibers aktiviert.

# cat GeraeteDatei            # triggert open, read und close
# cat /etc/motd >GeraeteDatei # schreibt die Datei /etc/motd in den Treiber

Für die meisten innerhalb des Buches verwendeten Beispiele reichen Systemkommandos aus, um die Funktionalität des Treibers zu testen. Eigene Testprogramme sind hier nur in seltenen Fällen zu erstellen.

Fehler finden

Der Standard-Linux-Kernel unterstützt Kernel-Debugging nicht.[1] Allerdings gibt es in Form von Patches einen »Built-in-Debugger« (http://oss.sgi.com/projects/kdb/) und einen so genannten »Kerneldebugger« (http://kgdb.sourceforge.net/).

Der Built-in-Debugger von SGI ermöglicht die Fehlersuche auf Assemblerebene. Der Debugger ist Teil des Kernels (daher der Name »Built-in«), so dass kein zweiter Rechner zum Debuggen benötigt wird. Er ermöglicht das Debuggen sowohl des Kernels als auch von Built-in-Treibern und von Modultreibern. Nach dem Patchen des Kernels findet sich im Kernelverzeichnis Documentation/kdb/ die notwendige Dokumentation. Abgesehen von der fehlenden Hochsprachenunterstützung sind sämtliche Standard-Funktionen eines Debuggers vorhanden. Der Zugriff auf Adressen über Symbole (also Variablen- oder Funktionsnamen) ist möglich. Allerdings kann der Debugger nicht unter X-Window aufgerufen werden. Sobald nämlich der Linux-Kernel angehalten wird, steht auch das X-Window-System.

Es ist zu erwarten, dass der Kerneldebugger bald zum Standard-Kernel gehören wird. Er ist zumindest bereits im Linux-Quellcode von Andrew Morton – dem Maintainer von Kernel 2.6 – integriert (zum Beispiel in Kernel »2.6.3-mm4«, siehe http://www.kernel.org). Der Kerneldebugger ermöglicht zwar Debugging auf Hochsprachenniveau, benötigt dazu aber einen zweiten Rechner. Auf dem so genannten Target läuft der zu debuggende Linux-Kernel, auf dem so genannten Hostrechner der Debugger. Der Hostrechner muss Zugriff auf den Quellcode haben.

Host und Target sind üblicherweise über eine serielle Schnittstelle verbunden. In vielen Fällen kann zum Debuggen auch Ethernet verwendet werden, dann nämlich, wenn der Debugger die Ethernet-Hardware des Targets unterstützt. Der in Linux integrierte tcp/ip-Stack kann zum Debuggen nicht, respektive nur sehr eingeschränkt verwendet werden – denn schließlich wird beim Debuggen der Kernel angehalten. Die Dokumentation zum Kerneldebugger findet sich im Quellcodeverzeichnis Documentation/i386/kgdb/.

In früheren Kernelversionen hat der Kerneldebugger auch die Fehlersuche in Modulen unterstützt. In der im Kernel 2.6.3-mm4 vorhandenen Version ist das momentan nicht der Fall. Soll hier ein Treiber untersucht werden, muss er als Kerneltreiber übersetzt werden. Darüber hinaus ist das Debuggen auf Hochsprachenniveau teilweise sehr zäh, wenn der Debugger lange Symbollisten durchsucht. Und noch ein weiterer Nachteil: Der Kerneldebugger steht nicht für alle Hardware-Plattformen zur Verfügung.

Aus diesen Gründen sollte jeder Treiber die Fehlersuche wie im Folgenden beschrieben aktiv unterstützen.

In der Linux-Welt wird eine Methode favorisiert, bei der an markanten Stellen im Code Ausgaben eingebaut werden. Dazu bietet Linux die Funktion printk an, die der in Applikationen gebräuchlichen Funktion printf bezüglich Aufruf und Funktionalität ähnelt. Da jedoch innerhalb des Kernels kein Bildschirm zur Ausgabe zur Verfügung steht, schreibt printk die Ausgaben in einen Kernel-internen Speicherbereich, auf den ein User-Prozess (der klogd) zugreifen kann. Achtung: Nur wenn am Ende des printk-Strings ein »'\n'« auftaucht, wird der String sofort ausgegeben.

Innerhalb eines Unix-Systems ist ein zentraler Prozess für die Protokollierung von Ereignissen und Systemzuständen zuständig: der syslogd. Der syslogd ist bezüglich der Ereignisse, die er protokolliert und bezüglich der Art, wie die Protokollierung vonstatten geht, frei konfigurierbar. Im einfachsten Fall werden alle Informationen in eine Datei geschrieben. Kompliziertere Konfigurationen erlauben die Protokollierung von Daten aufgeschlüsselt nach Bereichen (z.B. Ereignisse des Mailsystems, Ereignisse des Kernels) bzw. Prioritäten und auf unterschiedliche Rechner.

Abbildung 3-3. Printk-Debugging

Der klogd übergibt die Ereignisse bzw. Informationen aus dem Betriebssystemkern dem syslogd. Dieser sollte für die Treiberprogrammierung so konfiguriert werden, dass Ereignisse in einer Datei abgelegt werden, die über entsprechende Systemkommandos (tail -f) verfolgt werden kann.

Steht beispielsweise die Zeile

*.*	-/var/log/messages
in der Datei /etc/syslog.conf, werden sämtliche Ereignisse des Betriebssystems in der Datei /var/log/messages abgelegt.

Sie sollten überprüfen, ob auch auf Ihrem System ein solcher Eintrag existiert. Andernfalls sollten Sie diesen Eintrag anlegen und den syslogd über das Kommando »killall -HUP syslogd« dazu auffordern, die Konfiguration neu einzulesen.

Für die Treiberentwicklung ist es sinnvoll, die Ausgaben des Kommandos »tail -f /var/log/messages« ständig im Blickfeld zu haben.

Auf einigen Rechnern ist der zwischenzeitlich in die Jahre gekommene syslogd durch neuere Varianten, wie beispielsweise dem syslog-ng, ersetzt worden. Diese verwenden andere Konfigurationsformate. Die Konfigurationsdatei des syslog-ng nennt sich beispielsweise /etc/syslog-ng/syslog-ng.conf.

Tabelle 3-2. Priorisierung von Kernelnachrichten

Symbolische Bezeichnung Wert Bedeutung
KERN_EMERG <0> Das System ist nicht mehr zu gebrauchen.
KERN_ALERT <1> Das System ist in einem Zustand, der sofortige Maßnahmen erfordert.
KERN_CRIT <2> Der Systemzustand ist kritisch.
KERN_ERR <3> Fehlerzustände sind aufgetreten.
KERN_WARNING <4> Warnung.
KERN_NOTICE <5> Wichtige Nachricht, aber kein Fehlerzustand.
KERN_INFO <6> Information.
KERN_DEBUG <7> Debug-Informationen.

Die Funktion printk gibt die auszugebenden Informationen an den klogd aus: Sie bekommt einen Formatstring und eine Reihe von Parametern übergeben, die gemäß dem Formatstring zu einem Ausgabestring zusammengesetzt werden.

Steht zu Beginn dieses Ausgabestrings eine der in Tabelle Priorisierung von Kernelnachrichten dargestellten Zeichenketten (zum Beispiel »<4>«), interpretiert der klogd dies als eine Priorität. Die Priorität entspricht exakt der im syslogd gängigen Priorisierung. Demnach müssen Debug-Informationen mit der Priorität 7 versehen werden:

    printk( KERN_DEBUG "Lesefifo ist auf %d gesetzt\n", fifoindex );

Der Compiler setzt hieraus beispielsweise den String »"<7>Lesefifo ist auf 12 gesetzt\n"« zusammen. Der klogd wiederum entfernt die ersten drei Zeichen (<7>) und übergibt den ursprünglichen String zusammen mit der gewünschten Priorität dem syslogd. Falls dieser konfiguriert ist, Nachrichten der übergebenen Priorität auszugeben, erscheinen sie in der Datei /var/log/messages.

Die für die Fehlersuche interessanten Ausgaben des Treibers sind für eine Produktivversion möglicherweise sehr störend. Deswegen könnte auf die Möglichkeiten der bedingten Kompilierung zurückgegriffen werden, mit der sich die Produktivversion von der Debugversion unterscheiden lässt. Die Methode der bedingten Programmierung hat jedoch den Nachteil, dass sie den Code unübersichtlich macht. Schließlich kommen zu einer Zeile Informationsausgabe (mit printk) noch zwei Zeilen für den Präprozessor hinzu. In der Header-Datei <linux/kernel.h> sind zwei Makros definiert, die die Übersichtlichkeit wiederherstellen: pr_debug und pr_info.

Wird der Treiber mit dem Define DEBUG (Compileroption -DDEBUG) übersetzt, expandiert das Makro pr_debug in eine entsprechende printk-Anweisung. Diese enthält auch die korrekte Priorisierung (KERN_DEBUG). Wird das Define weggelassen, so wird auch der Code nicht übersetzt. Das Setzen dieses Flags wird im Makefile über die Variable EXTRA_CFLAGS gesteuert (siehe hierzu Kapitel Modultreiber außerhalb der Kernelquellen).

	pr_debug( "Lesefifo ist auf %d gesetzt\n", fifoindex );

Das Makro pr_info wird unabhängig davon übersetzt, ob das Define DEBUG verwendet wird oder nicht. Doch fügt es die richtige Priorisierung (KERN_INFO) vor dem auszugebenden String an.

Ein Treiber kann aber nicht nur über printk und pr_info die Fehlersuche aktiv unterstützen. Debug- und Zustandsinformationen können vom Treiber in eine virtuelle Datei im Verzeichnis /proc abgelegt werden. Näheres dazu findet sich im Kapitel Proc-Filesystem.

Eine letzte Möglichkeit der aktiven Fehlersuche wird schließlich über die ebenfalls in der Datei <linux/kernel.h> definierten Makros WARN_ON und BUG_ON angeboten.

    WARN_ON( index==limit );

Ist die im Makro WARN_ON spezifizierte Bedingung erfüllt, werden sowohl der Funktionsname als auch der Name der Datei und die Zeile innerhalb der Datei, aus der heraus das Makro aufgerufen wird, ausgegeben. Darüber hinaus bekommt der klogd noch den so genannten Call Trace mitgeteilt, sprich die Aufrufreihenfolge der Funktionen.

Dec 30 12:33:35 obil kernel: Badness in secure_add_timer at ktimer.c:45
Dec 30 12:33:35 obil kernel: Call Trace:
Dec 30 12:33:35 obil kernel:  [<cc84e0d5>] secure_add_timer+0x39/0x70 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e45c>] .rodata+0x17c/0x5e0 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e44d>] .rodata+0x16d/0x5e0 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e444>] .rodata+0x164/0x5e0 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e938>] my_timer+0x0/0x18 [ktimer]
Dec 30 12:33:35 obil kernel:  [<c011783f>] printk+0x127/0x150
Dec 30 12:33:35 obil kernel:  [<cc84e938>] my_timer+0x0/0x18 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e15f>] inc_count+0x53/0x58 [ktimer]
Dec 30 12:33:35 obil kernel:  [<cc84e938>] my_timer+0x0/0x18 [ktimer]
Dec 30 12:33:35 obil kernel:  [<c011ee66>] update_wall_time+0x12/0x3c
Dec 30 12:33:35 obil kernel:  [<c011f13f>] do_timer+0x4b/0xcc
Dec 30 12:33:35 obil kernel:  [<c011f08f>] run_timer_tasklet+0xe7/0x130
Dec 30 12:33:35 obil kernel:  [<cc84e10c>] inc_count+0x0/0x58 [ktimer]
Dec 30 12:33:35 obil kernel:  [<c011c029>] tasklet_hi_action+0x3d/0x60
Dec 30 12:33:35 obil kernel:  [<c011be4a>] do_softirq+0x5a/0xac
Dec 30 12:33:35 obil kernel:  [<c0109e60>] do_IRQ+0xfc/0x118
Dec 30 12:33:35 obil kernel:  [<c0108a30>] common_interrupt+0x18/0x20
    BUG_ON( index==limit );

Ist die innerhalb des Makros BUG_ON spezifizierte Bedingung erfüllt, wird die dargestellte Information ebenfalls ausgegeben. Im Unterschied zu WARN_ON wird die Bearbeitung des Codes (Treibers) jedoch sofort abgebrochen.

Obwohl innerhalb des Kernels sämtliche Ressourcen zugänglich sind, muss nicht jeder Programmierfehler zum Absturz des gesamten Systems führen. Vielmehr versucht der Kernel Fehler, wie beispielsweise den Zugriff auf nichtexistente Speicherbereiche, abzufangen. In einem solchen Fall stürzt die zuständige Komponente – der Treiber – ab, das System als solches »lebt« aber weiter.

Dabei erzeugt der Kernel eine so genannte »Oops«-Message, bei der die Register-Inhalte des Prozessors in die Datei /var/log/messages bzw. auf der Konsole ausgegeben werden.

So führt beispielsweise die Zeile Code im Treiber

static void that_is_a_faulty_function( void )
{
     *(int *)NULL = 0;
}

static ssize_t driver_write( struct file *instanz, const char *user, size_t count,
    loff_t *off)
{
    ...
    printk("Now say goodby ...\n");
    that_is_a_faulty_function();
    copy_from_user( NULL, user, to_copy );
    printk("Never will see this ...\n");
    ...

in Zusammenhang mit dem Kommando »cat /etc/motd > /dev/mydevice« zu folgender Oops-Message:

Dec 30 10:06:40 cia kernel: driver_open called
Dec 30 10:06:40 cia kernel: Now say goodby ...
Dec 30 10:06:40 cia kernel: Unable to handle kernel NULL pointer dereference at virtual address 00000000
Dec 30 10:06:40 cia kernel:  printing eip:
Dec 30 10:06:40 cia kernel: cc84c093
Dec 30 10:06:40 cia kernel: *pde = 00000000
Dec 30 10:06:40 cia kernel: Oops: 0002
Dec 30 10:06:40 cia kernel: oops ppp_generic slhc 3c59x ehci-hcd vfat  
Dec 30 10:06:40 cia kernel: CPU:    0
Dec 30 10:06:40 cia kernel: EIP:    0060:[<cc84c093>]    Not tainted
Dec 30 10:06:40 cia kernel: EFLAGS: 00010282
Dec 30 10:06:40 cia kernel: EIP is at that_is_a_faulty_function+0x3/0x10 [oops]
Dec 30 10:06:40 cia kernel: eax: 00000013   ebx: 00000193   ecx: cbb9d000   edx: c2cde000
Dec 30 10:06:40 cia kernel: esi: 00000193   edi: c847697c   ebp: c2cdff5c   esp: c2cdff5c
Dec 30 10:06:40 cia kernel: ds: 0068   es: 0068   ss: 0068
Dec 30 10:06:40 cia kernel: Process cat (pid: 833, threadinfo=c2cde000 task=c7557340)
Dec 30 10:06:40 cia kernel: Stack: c2cdff7c cc84c0cf 00000000 00000000 c89a5920 0804d000 0804e000 00100077 
Dec 30 10:06:40 cia kernel:        c847699c c013b4a1 c847697c 0804c038 00000193 c847699c c847697c fffffff7 
Dec 30 10:06:40 cia kernel:        00000000 bffff8b8 c013b59e c847697c 0804c038 00000193 c847699c c2cde000 
Dec 30 10:06:40 cia kernel: Call Trace:
Dec 30 10:06:40 cia kernel:  [<cc84c0cf>] driver_write+0x2f/0x58 [oops]
Dec 30 10:06:40 cia kernel:  [<c013b4a1>] vfs_write+0xc5/0x15c
Dec 30 10:06:40 cia kernel:  [<c013b59e>] sys_write+0x2a/0x3c
Dec 30 10:06:40 cia kernel:  [<c01088eb>] syscall_call+0x7/0xb
Dec 30 10:06:40 cia kernel: 
Dec 30 10:06:40 cia kernel: Code: c7 05 00 00 00 00 00 00 00 00 c9 c3 90 55 89 e5 83 ec 10 56 

Aus dieser Nachricht lässt sich ablesen, dass die fehlerhafte Codezeile in der Funktion that_is_a_faulty_function steht. Allerdings wird eine Auflösung von Adressen zu Symbolen nur durchgeführt, wenn der Kernel mit der Option »Lade alle Symbole für Debugging« übersetzt wurde (Menüpunkt General Setup/Remove Kernel Features (for embedded systems)/Load all symbols for debugging/kksymoops der Kernelkonfiguration).

Fußnoten

[1]

Linus Torvalds meint zum Thema Debugging: »I'm afraid that I've seen too many people fix bugs by looking at debugger output, and that almost inevitably leads to fixing the symptoms rather than the underlying problem.«


Lizenz