7.6. Kernel Build System

Weil der Linux-Kernel so außergewöhnlich skalierbar ist, benötigt man zum Erstellen eines Treibers die Kernelquellen und insbesondere die zugehörige Kernelkonfiguration. Der Compiler muss mit unterschiedlichen Flags beziehungsweise Definitionen aufgerufen werden, abhängig davon,

Diese Information befindet sich in der Kernelkonfiguration und wird vom Kernel Build System ausgewertet. Das Kernel Build System ist die Summe der Skripte und Makefiles, mit denen der Kernel und seine Module generiert werden (siehe auch [kbuild2003] und [Ravnborg2003]).

Wer einen Treiber erstellt, muss sich ins Kernel Build System einklinken. Hier lassen sich zwei Szenarien unterscheiden:

Den eigenen Treibercode direkt in die Kernelquellen zu integrieren, ist der einfacherer Weg. Allerdings sollte man diesen Weg nur dann beschreiten, wenn

In allen anderen Fällen sollte die Variante gewählt werden, bei der der Treiber außerhalb der Kernelquellen verwaltet wird. Auf diese Weise ist ein Wechsel auf eine neue Kernelversion wesentlich leichter. Dann nämlich ist im Regelfall nur make aufzurufen, während ansonsten die offiziellen Kernelquellen erst erneut gepatcht werden müssen.

Built-in-Treiber sind insbesondere im Bereich der eingebetteten Systeme sinnvoll. Hier lassen sich die Codeteile und Werkzeuge zum Laden von Modulen einsparen, wenn alle Treiber einkompiliert sind. Built-in-Treiber sind ebenfalls notwendig, sobald ein Gerät zum Booten benötigt wird (das gilt beispielsweise für Festplattentreiber).

7.6.1. Treiberquellen als integrative Erweiterung der Kernelquellen

7.6.1.1. So funktioniert das Kernel Build System

Das Kernel Build System und die Kernelkonfiguration spielen direkt zusammen. Das Kernel Build System hat im Wesentlichen zwei Aufgaben (Targets): die Erzeugung des Kernels selbst (make bzImage) und die Erzeugung der Module (make modules). Für jedes Target gibt es eine Variable; für die Erzeugung des Kernels lautet die Variable »obj-y« und für die Erzeugung der Module »obj-m«. Alle Objektdateien, die der Variablen »obj-y« zugewiesen werden, werden direkt in den Kernel eingebunden. Die Objektdateien, die der Variablen »obj-m« zugewiesen werden, werden als Module generiert.

In den Makefiles wird der Kernelcode, der unbedingt benötigt wird, der Variablen »obj-y« direkt zugewiesen:

obj-y    += mem.o tty_io.o n_tty.o tty_ioctl.o pty.o misc.o random.o

Anders sieht es mit den Modulen aus. Viele Kernelkomponenten, insbesondere die Treiber, lassen sich entweder als Modultreiber oder als Built-in-Treiber (Kerneltreiber) kompilieren. Im ersten Fall muss der Modulname der Variablen »obj-m«, im zweiten Fall der Variablen »obj-y« zugewiesen werden. Ob die Variable mit »y« oder die mit »m« verwendet wird, wird über eine Konfigurationsvariable entschieden. Die Namen dieser Variablen werden aus dem Wortstamm CONFIG_ und dem Namen der Komponente gebildet. Damit ergibt sich der Eintrag für das Modul mit dem Namen stallion zu

obj-$(CONFIG_STALLION) += stallion.o

Die Variable CONFIG_STALLION wiederum wird über die Kernelkonfiguration gar nicht gesetzt oder aber auf den Wert »y« beziehungsweise auf den Wert »m« (Erzeugung als Modul). Entsprechend der Konfiguration wird damit die Variable »obj-y« oder »obj-m« erweitert und der Treiber in dem einen Fall als Built-in-Treiber und in dem anderen Fall als Modultreiber kompiliert.

7.6.1.2. In die Kernel-Konfiguration integrieren

Damit die Kernelkonfiguration die Variable setzen kann, muss diese bekannt gemacht werden. In jedem Verzeichnis des Kernels existiert eine Datei namens Kconfig, die wiederum für jede Konfigurationsoption einen Eintrag hat. Jeder Eintrag beginnt mit dem Schlüsselwort config und dem Namen der Konfigurationsvariablen. Danach folgen die Attribute der Konfigurationsvariablen, die die Auswahlmöglichkeiten darstellen.

Es können der Typ, Abhängigkeiten, der Hilfetext und Default-Werte eingestellt werden:

Typ

Die Typangabe besteht aus dem eigentlichen Typ und einer Beschreibung der Auswahlmöglichkeit. Fünf Typen stehen zur Auswahl: bool, tristate, string, hex und int (integer).

Die Variable vom Typ bool hat entweder den Wert »y« oder ist nicht ausgewählt. Die Variable vom Typ tristate kann zusätzlich noch den Wert »m« haben. Der Bool-Typ wird im Wesentlichen gesetzt, um weitere Konfigurationsoptionen (Freischaltung einer Auswahlgruppe, wie zum Beispiel der Gruppe der Netzwerkgeräte) zu ermöglichen, während der Tristate-Typ für die Auswahl einer konkreten Komponente, eines konkreten Treibers verwendet wird. Die drei übrigen Typen dienen der Eingabe bzw. Zuweisung einer Variablen mit freien Werten. Hierbei kann die Eingabe auf Hexadezimalziffern und normale Zahleneingaben eingeschränkt werden. Zusätzlich kann sogar der jeweils gültige Zahlenbereich eingeschränkt werden.

Abhängigkeiten

Abhängigkeiten werden über das Schlüsselwort depends oder requires definiert. Nach dem Schlüsselwort wird der Name der Option bzw. Variablen angegeben, von der das Modul abhängt (depends VT) oder (depends VT=m). Die Konfiguration wird nur dann zur Auswahl gestellt, falls die abhängige Komponente ausgewählt wurde bzw. den definierten Wert hat. Es können durchaus auch mehrere Abhängigkeiten angegeben werden. Diese werden dann mit den bekannten Operatoren logisch miteinander verknüpft (z.B. depends on X86_PC9800 && !PARPORT).

Hilfetext

Der Hilfetext wird durch das Schlüsselwort help eingeleitet. Er beginnt in der Zeile nach dem Schlüsselwort und muss eins weiter als das Schlüsselwort eingerückt werden. Das Ende des Hilfetextes wird nämlich an dieser Einrückung erkannt. Sobald wieder ein Text bzw. Schlüsselwort folgt, welches weiter links beginnt, gilt der Hilfetext als beendet.

Default-Werte

Über das Schlüsselwort default wird der Default-Wert eingestellt. Ist die Variable vom Typ bool oder tristate, kann der Default-Wert »y« oder »m« sein. Bei Integer- bzw. Hexwerten kann hier ein entsprechender Default-Wert eingestellt werden (default "256").

Beispiel Eintrag für ein neues Modul in der Kernelkonfiguration »kconfig« zeigt die Kconfig-Konfiguration für einen Treiber.

Beispiel 7-15. Eintrag für ein neues Modul in der Kernelkonfiguration »kconfig«

config STALLION
	tristate "Stallion EasyIO or EC8/32 support"
	depends on STALDRV
	help
	  If you have an EasyIO or EasyConnection 8/32 multiport Stallion
	  card, then this is for you; say Y.  Make sure to read
	  <file:Documentation/stallion.txt>.

	  If you want to compile this as a module ( = code which can be
	  inserted in and removed from the running kernel whenever you want),
	  say M here and read <file:Documentation/modules.txt>.  The module
	  will be called stallion.

7.6.1.3. Built-in-Treiber

Ob ein Treiber fest in den Kernel einkompiliert oder als Modul nach dem Hochfahren geladen wird, ist für die Programmierung des Treibers irrelevant. In beiden Fällen werden genau die gleichen Funktionen kodiert, nur wird im ersten Fall die Treiber-Initialisierungsfunktion beim Hochfahren, im zweiten Fall beim Laden des Moduls aufgerufen. Das Gleiche gilt für die Deinitialisierung. Allerdings muss vor dem Kompilieren festgelegt werden, ob der Treiber als Modul oder als Kernel-Bestandteil generiert werden soll. Aufgrund der Compiler-Option MODULE werden die beiden Makros module_init und module_exit nämlich unterschiedlich expandiert.

Im Fall eines Built-in-Treibers wird durch das Makro module_init die Adresse der Initialisierungsfunktion in einer Tabelle abgelegt. Beim Systemstart wird jede dieser Funktionen durch den Kernel abgearbeitet.

Darüber hinaus muss der Built-in-Treiber zu den übrigen Kernelfunktionen gelinkt werden. Ein Treiber-Modul wird jedoch unabhängig vom Kernel entwickelt. Dazu wiederum muss der Quellcode des Treibers in den Quellcode des Linux-Kernels integriert werden.

Ein Built-in-Treiber wird erstellt, indem

  1. die Treiberquellen in ein entsprechendes Verzeichnis in den Kernelquellen kopiert wird. Für einen zeichenorientierten Treiber ist dies beispielsweise <drivers/char/>. Die Quellen eines USB-Treibers werden abhängig von der Art des Gerätes in das entsprechende USB-Unterverzeichnis (<drivers/usb/>) kopiert: media/, net/, serial/, input/ oder storage/. Geräte, die sich in keiner der angegebenen Klassen einordnen lassen, werden in das Verzeichnis misc/ kopiert.

    Der Quellcode für PCI-Geräte wird allerdings nicht in das Verzeichnis <drivers/pci/> kopiert, sondern in ein dem Gerätetyp entsprechendes Verzeichnis (zum Beispiel <drivers/media/>).

    Die genaue Stelle, an der die Quellen kopiert werden, kann auch über die Kernelkonfiguration bestimmt werden. Die Auswahl zum Treiber taucht nämlich entsprechend der Ablage der Quellen innerhalb des Verzeichnisbaumes auf.

  2. Die Datei Kconfig des Verzeichnisses, in dem sich die Treiberquellen jetzt befinden, wird um die neue Option erweitert, mit der der Treiber ausgewählt werden kann. Handelt es sich um einen reinen Built-in-Treiber, wird eine Konfigurationsvariable vom Typ bool gewählt. Kann der Treiber auch als Modul realisiert werden, wird der Typ tristate ausgewählt.

  3. Das Makefile des Verzeichnisses, in dem sich die Treiberquellen jetzt befinden, wird erweitert. Es wird eine neue Zeile hinzugefügt, mit der der Name des Treibers an eine der beiden Variablen (abhängig von der Konfigurationsvariablen) wie oben beschrieben angehängt wird.

  4. Der Kernel wird neu konfiguriert, indem in dem Kernel-Quellverzeichnis (z.B. /usr/src/linux/) das Kommando make menuconfig oder das Kommando make xconfig aufgerufen wird.

7.6.2. Modultreiber außerhalb der Kernelquellen

Sämtliche Compiler-Optionen sind prinzipiell bereits durch die Kernelkonfiguration festgelegt. Daraus ergibt sich: Nur wenn Kernelquellen inklusive der Konfiguration vorliegen, kann der Treiber erstellt werden. Der Treiberentwickler muss also im Regelfall zuvor einmal im Verzeichnis der Kernelquellen (meist /usr/src/linux/) make menuconfig oder make xconfig aufgerufen haben.

Ansonsten wird noch benötigt:

Das Kernel Build System erwartet diese Informationen selbst in einem Makefile. Der Name des bzw. die Namen der zu übersetzenden Module müssen in die Variable »obj-m« abgelget werden. Die treiberspezifischen Flags werden in der Variablen EXTRA_CFLAGS für den C-Compiler, in der Variablen EXTRA_AFLAGS für den Assembler, in der Variablen EXTRA_LDFLAGS für den Linker und in der Variablen EXTRA_ARFLAGS für den Archiver (Bibliothekenbildung) spezifiziert:

EXTRA_CFLAGS+= -DISR_SUBSYSTEM
EXTRA_AFLAGS+=
EXTRA_LDFLAGS+=
EXTRA_ARFLAGS+=

obj-m	:= ModulName.o

Im Beispiel wird beim Generierungsvorgang (Compile-Vorgang) zusätzlich zu den normalen Optionen noch das Define »ISR_SUBSYSTEM« gesetzt. Der Name »ModulName.o« ist der Name des eigenen Moduls. Da in vielen Fällen die Extraflags nicht benötigt werden, können die ersten drei Zeilen entfallen, das Makefile reduziert sich auf eine einzige Zeile.

Das Kernel Build System wird durch Aufruf von make im Verzeichnis /lib/modules/<Kernelversion>/build gestartet. Damit nur der neue Treiber übersetzt wird, muss dem Kernel Build System noch als Parameter (SUBDIRS) der Pfad mitgegeben werden, in dem sich der Treibercode befindet. Wird das Kernel Build System aus dem Verzeichnis, in dem sich unsere Quellen befinden, aufgerufen, ergibt sich der folgende Befehl:

make -C /lib/modules/2.6.5/build SUBDIRS=`pwd` modules

Hier ist noch der Pfad zu den Kernelquellen, sprich die Versionsnummer des Kernels, anzupassen. Das Verzeichnis /lib/modules/<Kernelversion>/build ist ein Link auf die eigentlichen Kernelquellen, die normalerweise unter /usr/src/linux-<Kernelversion> stehen. Das Kernel Build System wird hier mit dem Kommando modules gestartet. Es erzeugt nur die Module, und zwar aufgrund des SUBDIRS-Kommandos nur die Module unterhalb des angegebenen Verzeichnisses (im Beispiel durch `pwd` das aktuelle Verzeichnis).

Das Makefile, in dem die zusätzlichen Flags und die zu übersetzenden Module eingetragen sind, muss übrigens genau Makefile heißen. Die Groß- und Kleinschreibung ist von Bedeutung!

Findige Programmierer geben sich mit einem derart langen Kommando nicht zufrieden – es jedes Mal eingeben zu müssen, nur um ein Modul zu generieren, ist unnötig umständlich. Durch einen Trick kann die Generierung durch einen einzigen, parameterlosen Aufruf von make realisiert werden:

ifneq ($(KERNELRELEASE),) 
obj-m   := mod1.o

else
KDIR    := /lib/modules/$(shell uname -r)/build
PWD     := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
endif

Dieses Makefile bemerkt, ob es durch das Kernel Build System aufgerufen wurde oder direkt vom User. In letzterem Fall ist die Variable KERNELRELEASE nämlich nicht gesetzt, so dass der zweite Teil des Makefiles aktiviert ist. Es feuert die Default-Regel mit Namen all, die mit den notwendigen Parametern das Build-System startet (Zeile 9). Dieses lädt wiederum das Makefile – nur ist jetzt die Variable KERNELRELEASE nicht mehr leer, sondern initialisiert. Folglich setzt das Makefile die Variable »obj-m« mit den Namen der zu generierenden Module.

Das dargestellte Makefile geht davon aus, dass der aktuelle Kernel auch der Kernel ist, für den es das Modul erstellen soll. Es verwendet nämlich das Kommando uname-r, um die Kernelversion zu ermitteln, die es in den Pfad zu den Kernel-Quellen einsetzt. Sollten die Kernel-Quellen nicht unter /lib/modules/<Kernelversion>build zu finden sein, dann ist Zeile 5 (KDIR := ...) entsprechend anzupassen.

Das Kernel Build System ruft die notwendigen Werkzeuge auf, insbesondere den Compiler. Damit erzeugt es die Objektdatei mod1.o und linkt sie mit der automatisch generierten (mod1.mod.c) und kompilierten (mod1.mod.o) Datei, die die Versions-Information enthält, zum Modul mod1.ko.

Soll nicht nur eines, sondern gleich mehrere Module erzeugt werden, sind die Namen der Module nacheinander der Variablen »obj-m« zuzuweisen:

obj-m  := mod1.o mod2.o mod3.o

Werden mehrere Module übersetzt, lassen sich für den Compiler auch modulspezifische Optionen angeben. Dazu wird eine Variable erzeugt, die sich aus dem Namen CFLAGS_ und dem Namen des Moduls zusammensetzt:

CFLAGS_mod1.o = -DAUTOCONF

Oftmals soll ein Treiber für ein anderes System erstellt werden, als für das, welches für die Treibererstellung genutzt wird. In diesem Fall wird die Variable KDIR mit dem Pfad zu den Kernelquellen und der Kernelkonfiguration belegt, für die der Treiber erstellt werden soll. Im Folgenden wird – unabhängig vom gerade aktiven Kernel – ein Treiber für einen Kernel erstellt, dessen Konfiguration sich im Verzeichnis /usr/src/linux-2.6.5 befindet:

TARGET=devfs

ifneq ($(KERNELRELEASE),)
obj-y   := ${TARGET}.o

else
KDIR    := /usr/src/linux-2.6.5
PWD	    := $(shell pwd)

default:
    $(MAKE)	-C $(KDIR)	SUBDIRS=$(PWD) modules
endif


Lizenz