Kompilieren und Laden

Im Rest dieses Kapitels werden wir ein vollständiges, wenn auch typenloses Modul schreiben, das heißt, das Modul wird nicht zu einer der in “the Section called Klassen von Geräten und Modulen in Kapitel 1” in Kapitel 1 genannten Klassen gehören. Der Beispiel-Treiber, den wir Ihnen in diesem Kapitel zeigen werden, heißt skull, was ein Akronym für “Simple Kernel Utility for Loading Localities” ist. Sie können den Source-Code von skull verwenden, um Ihren eigenen lokalen Code in den Kernel zu laden, wenn Sie die darin enthaltene Beispiel-Funktionalität entfernen.[1]

Bevor wir uns mit der Aufgabe von init_module und cleanup_module beschäftigen, werden wir ein makefile schreiben, mit dem Objektcode erzeugt wird, den der Kernel laden kann.

Zunächst müssen wir das Präprozessor-Symbol __KERNEL__ definieren, bevor wir irgendwelche Header-Dateien einbinden. Wie bereits erwähnt, ist ein großer Teil des Kernel-spezifischen Inhalts in den Kernel-Header-Dateien unerreichbar, wenn dieses Symbol nicht definiert ist.

Ein weiteres wichtiges Symbol ist MODULE, das definiert sein muß, bevor <linux/module> eingebunden wird. Dieses Symbol ist immer definiert, es sei denn, es wird ein Treiber kompiliert, der direkt zum Kernel hinzugelinkt wird. Da keiner der Treiber, die wir in diesem Buch behandeln werden, direkt zum Kernel hinzugelinkt wird, definieren wir dieses Symbol in diesem Buch immer.

Wenn Sie Code für einen SMP-Rechner kompilieren, dann müssen Sie auch __SMP__ definieren, bevor Sie Kernel-Header-Dateien einbinden. In der Version 2.2 wurde die Wahl zwischen Multiprozessor und Einzelprozessor zu einem richtigen Konfigurationselement, weswegen es ausreicht, diese Zeilen am Anfang Ihres Moduls zu verwenden:

 #include <linux/config.h>
 #ifdef CONFIG_SMP
 # define _ _SMP_ _
 #endif

Ein Modul-Programmierer muß darüber hinaus den Compiler-Schalter –O auf der Kommandozeile angeben, da viele Funktionen in den Header-Dateien als inline deklariert sind. gcc expandiert Inline-Funktionen nicht, wenn die Optimierung nicht eingeschaltet ist, kommt aber auch mit gleichzeitiger Angabe von –g und –O klar, so daß Sie auch Code, der Inline-Funktionen verwendet, debuggen können.[2] Weil der Kernel umfassenden Gebrauch von Inline-Funktionen macht, ist es wichtig, daß diese korrekt expandiert werden.

Sie müssen außerdem sicherstellen, daß der Compiler, den Sie verwenden, zum Kernel paßt, den Sie kompilieren; lesen Sie dazu die Datei Documentation/Changes in den Kernel-Quellen. Der Kernel und der Compiler werden zur gleichen Zeit von verschiedenen Gruppen entwickelt, so daß manchmal Änderungen an dem einen Fehler im anderen hervorrufen können. Manche Distributionen (besonders Red Hat ist dafür bekannt) liefern eine Version des Compilers aus, die zu neu ist, um den Kernel zuverlässig bauen zu können. In diesem Fall gibt es normalerweise ein separates Paket (das oft kgcc heißt) mit einem für die Kernel-Kompilierung geeigneten Compiler.

Schließlich möchten wir Ihnen raten, daß Sie den Compiler-Schalter –Wall (alle Warnungen) verwenden, um unliebsame Überraschungen vermeiden, und daß Sie Ihren Code so ändern, daß der Compiler keine Warnungen ausgibt, selbst wenn das bedeutet, daß Sie Ihren üblichen Programmierstil ändern müssen. Beim Schreiben von Kernel-Code ist der bevorzugte Code-Stil eindeutig der von Linus selbst. Documentation/CodingStyle liest sich recht amüsant und ist eine obligatorische Lerneinheit für alle, die sich für das Kernel-Hacken interessieren.

Es ist am besten, alle bisher eingeführten Definitionen und Schalter in der von make verwendeten Variable CFLAGS unterzubringen.

Außer einer passenden CFLAGS-Variable muß das Makefile eine Regel enthalten, um die einzelnen Objektdateien zu verbinden. Diese Regel wird nur benötigt, wenn das Modul über verschiedene Quelldateien verteilt ist, was aber bei Modulen nicht unüblich ist. Die Module werden mit dem Befehl ld -r zusammengelinkt. Genauer gesagt, ist das kein Link-Vorgang, auch wenn der Linker verwendet wird. Die Ausgabe ist eine neue Objektdatei, die den Code aller Eingabedateien enthält. Die Option –r steht für relocatable, die Ausgabedatei ist relozierbar, da sie noch keine absoluten Adressen enthält.

Das folgende makefile ist ein minimales Beispiel, das zeigt, wie man ein Modul aus zwei Quelldateien baut. Wenn das Modul nur aus einer einzigen Quelldatei besteht, können Sie den Eintrag mit ld -r einfach weglassen.


 
# Entweder hier aendern oder auf der Kommandozeile von "make" angeben
KERNELDIR = /usr/src/linux

include = $(KERNELDIR)/.config

CFLAGS = -D__KERNEL__ -DMODULE -I$(KERNELDIR)/include \
  -O -Wall

ifdef CONFIG_SMP
  CFLAGS += -D__SMP__ -DSMP
endif

all: skull.o

skull.o: skull_init.o skull_clean.o
        $(LD) -r $⁁ -o $@

clean:
        rm -f *.o *˜ core

Wenn make neu für Sie ist, dann wundern Sie sich vielleicht darüber, daß keine .c-Datei und keine Kompilationsregel im obigen makefile stehen. Diese Deklarationen sind nicht notwendig, da make schlau genug ist, ohne weitere Anweisungen eine .c-Datei zu einer .o-Datei zu machen, wobei der aktuelle Compiler (oder der Default), $(CC), und dessen Flags, $(CFLAGS), verwendet werden.

Nachdem das Modul gebaut worden ist, muß es in den Kernel geladen werden. Wie wir bereits erwähnt haben, wird das von insmod erledigt. Dieses Programm arbeitet ähnlich wie ld; es linkt alle im Modul nicht aufgelösten Symbole gegen die Symboltabelle des laufenden Kernels. Im Gegensatz zum Linker wird aber nicht die Datei auf der Festplatte, sondern das Abbild im Speicher modifiziert. insmod akzeptiert eine Reihe von Kommandozeilenoptionen (näheres finden Sie in der Man-Page), und kann Integer- und String-Variablen in Ihrem Modul Werte zuweisen, bevor das Modul in den laufenden Kernel gelinkt wird. Wenn ein Modul also korrekt entwickelt wurde, dann kann es zur Ladezeit konfiguriert werden. Diese Fähigkeit gibt dem Benutzer mehr Flexibilität als die Konfiguration zur Kompilierzeit, die leider noch manchmal verwendet wird. Die Konfiguration zur Ladezeit wird in “the Section called Automatische und manuelle Konfiguration” weiter hinten in diesem Kapitel beschrieben.

Interessierte Leser wollen sich vielleicht ansehen, wie insmod vom Kernel unterstützt wird. insmod verwendet einige wenige Systemaufrufe, die in kernel/module.c definiert sind. Die Funktion sys_create_module alloziert Kernel-Speicher, um das Modul aufzunehmen (dieser Speicher wird mit vmalloc alloziert, siehe dazu “the Section called vmalloc und Freunde in Kapitel 7” in Kapitel 7); der Systemaufruf get_kernel_syms gibt die Symboltabelle des Kernels zurück, um das Modul dagegen zu linken, und sys_init_module kopiert den relozierten Objekt-Code in den Kernel-Space und ruft die Initialisierungsfunktion des Moduls auf.

Wenn Sie sich die Kernel-Quellen ansehen, werden Sie feststellen, daß den Namen der Systemaufrufe sys_ vorangestellt ist. Das ist bei allen Systemaufrufen und bei keiner anderen Funktion der Fall. Das ist nützlich zu wissen, wenn Sie mit grep nach Systemaufrufen in den Quellen suchen.

Versionsabhängigkeit

Denken Sie daran, daß Ihr Modul für jede Version des Kernels, mit der es verwendet werden soll, neu kompiliert werden muß. Jedes Modul definiert ein Symbol namens __module_kernel_version, das von insmod mit der Versionsnummer des aktuellen Kernels verglichen wird. Dieses Symbol steht im .modinfo-Abschnitt des ELF-Formats (Executable Linking and Format; siehe Kapitel 11. Beachten Sie bitte, daß die Beschreibung der Interna nur auf die Versionen 2.2 und 2.4 zutrifft; Linux 2.0 tat das gleiche auf eine andere Weise.

Der Compiler definiert dieses Symbol für Sie, wenn Sie <linux/module.h> einbinden (deswegen hat hello.c das nicht deklariert). Das bedeutet auch, daß Sie <linux/module.h> nur einmal einbinden müssen, selbst wenn Ihr Modul aus mehreren Quelldateien besteht. (Es sei denn, Sie verwenden __NO_VERSION__, das in Kürze eingeführt werden wird.)

Wenn die Versionen nicht übereinstimmen, können Sie trotzdem noch versuchen, das Modul mit einer anderen Kernel-Version zu laden, in dem Sie den Schalter –f (“force”) beim Aufruf von insmod verwenden; aber das ist nicht besonders sicher und kann auch fehlschlagen. Außerdem ist es schwierig vorherzusagen, was passieren wird. Das Laden kann wegen nicht zusammenpassender Symbole fehlschlagen. In diesem Fall wird eine Fehlermeldung ausgegeben. Es kann aber auch wegen einer internen Änderung im Kernel fehlschlagen. Wenn das passiert, bekommen Sie üble Laufzeitfehler und möglicherweise eine Systempanik — ein guter Grund, um vorsichtig mit nicht zusammenpassenden Versionen zu sein. Nicht zusammenpassende Versionen können besser mit Versioning im Kernel behandelt werden, ein fortgeschrittenes Thema, das wir in “the Section called Versionskontrolle in Modulen in Kapitel 11” in Kapitel 11 behandeln werden.

Wenn Sie ein Modul für eine bestimmte Kernel-Version kompilieren wollen, dann müssen Sie die passenden Header-Dateien für diesen Kernel einbinden (beispielsweise, indem Sie ein anderes KERNELDIR im obigen makefile deklarieren). Diese Situation ist nicht ungewöhnlich, wenn man mit den Kernel-Quellen herumspielt, weil Sie am Ende oft mehrere Versionen des Quellbaumes haben werden. Alle Beispiel-Module in diesem Buch verwenden die Variable KERNELDIR, um auf die korrekten Kernel-Quellen zu verweisen; diese Variable kann entweder in Ihrer Umgebung oder in der Kommandozeile von make gesetzt werden.

Um Versionsabhängigkeiten beim Laden zu berücksichtigen, verwendet insmod einen speziellen Suchpfad und sucht zuerst in versionsabhängigen Verzeichnissen unterhalb von /lib/modules. Ältere Versionen suchten zunächst im aktuellen Verzeichnis, aber dieses Verhalten ist jetzt aus Sicherheitsgründen abgeschaltet (das Problem ist das gleiche wie bei der Umgebungsvariable PATH). Wenn Sie also ein Modul aus dem aktuellen Verzeichnis laden wollen, müssen Sie ./module.o verwenden, was mit allen insmod-Versionen funktioniert.

Manchmal werden Sie auf Kernel-Schnittstellen stoßen, die sich bei den Versionen 2.0.x und 2.4.x unterschiedlich verhalten. In diesem Fall müssen Sie die Makros verwenden, die die Versionsnummer des aktuellen Quellbaumes definieren. Diese sind in der Header-Datei<linux/version.h> definiert. Wir weisen Sie jeweils darauf hin, wenn sich Schnittstellen geändert haben; entweder direkt vor Ort oder in einem speziellen Abschnitt über Versionsabhängigkeiten am Ende des jeweiligen Kapitels, wenn wir eine 2.4-spezifische Diskussion nicht verkomplizieren wollen.

Die Header-Datei, die von linux/module.h automatisch eingebunden wird, definiert die folgenden Makros:

UTS_RELEASE

Dieses Makro expandiert zu einem String, der die Version des Kernel-Baumes beschreibt, etwa "2.3.48".

LINUX_VERSION_CODE

Dieses Makro expandiert zur binären Repräsentation der Kernel-Version; dabei wird ein Byte pro Teil der Versionsnummer verwendet. Beispielsweise ist der Code für 2.3.48 131888 (0x020330).[3] Mit dieser Information kann man ziemlich einfach bestimmen, mit welcher Kernel-Version man es zu tun hat.

KERNEL_VERSION(major,minor,release)

Dies ist das Makro, das dazu verwendet wird, einen "Kernel-Versions-Code" aus den einzelnen Zahlen der Versionsnummer zu bauen. Beispielsweise expandiert KERNEL_VERSION(2,3,48) zu 131888. Dieses Makro ist nützlich, wenn Sie die aktuelle Version und einen bekannten Referenzpunkt vergleichen müssen. Wir werden dieses Makro in diesem Buch mehrmals verwenden.

Die Datei version.h wird von module.h eingebunden, so daß Sie normalerweise version.h nicht explizit einbinden müssen. Andererseits können Sie module.h auch daran hindern, indem Sie __NO_VERSION__ deklarieren. Dies machen Sie, wenn Sie <linux/module.h> in mehreren, später zusammengelinkten Quelldateien einbinden müssen, zum Beispiel, wenn Sie in module.h deklarierte Präprozessor-Makros benötigen. Das Deklarieren von _ _NO_VERSION_ _ vor dem Einbinden von module.h verhindert die automatische Deklaration des Strings _ _module_kernel_version oder seiner Äquivalente in den Quelldateien, in denen Sie das nicht wünschen (ld -r würde sich sonst über die mehrfache Definition des Symbols beschweren). Die Beispiel-Module in diesem Buch verwenden aus diesem Grund ebenfalls _ _NO_VERSION_ _.

Die meisten Abhängigkeiten von der jeweiligen Kernel-Version können mit bedingten Präprozessor-Ausdrücken, die KERNEL_VERSION und LINUX_VERSION_CODE verwenden, umgangen werden. Versionsabhängigkeiten sollten den Treiber-Code aber nicht mit komplizierten #ifdef-Ausdrücken verschandeln; am besten ist es, wenn man die Inkompatibilitäten in einer einzigen Header-Datei versteckt. Deswegen enthält unser Beispiel-Code eine Header-Datei namens sysdep.h, die alle Inkompatibilitäten in passenden Makro-Definitionen verbirgt.

Die erste Versionsabhängigkeit, mit der wir es zu tun haben, ist die Definition einer make install-Regel für unsere Treiber. Wie Sie vielleicht erwarten, wird das Installationsverzeichnis, das sich von Kernel-Version zu Kernel-Version unterscheiden kann, durch Nachschlagen in version.h ermittelt. Das folgende Fragment stammt aus der Datei Rules.make, die in alle Makefiles eingebunden wird:


VERSIONFILE = $(INCLUDEDIR)/linux/version.h
VERSION  = $(shell awk -F\" '/REL/ {print $$2}' $(VERSIONFILE))
INSTALLDIR = /lib/modules/$(VERSION)/misc

Wir haben uns entschlossen, alle unsere Treiber in das misc-Verzeichnis zu installieren; das ist einerseits die richtige Wahl für diverse Zusatztreiber und andererseits eine gute Möglichkeit, um mit der Änderung in der Verzeichnisstruktur unterhalb von /lib/modules umzugehen, die kurz vor Einführung der 2.4-Version des Kernels vorgenommen wurde. Auch wenn die neue Verzeichnisstruktur komplizierter ist, wird das misc-Verzeichnis sowohl von alten als auch von neuen Versionen des modutils-Pakets verwendet.

> > Mit der oben gezeigten Definition von INSTALLDIR sieht die install-Regel aller Makefiles dann so aus:


install:
        install -d $(INSTALLDIR)
        install -c $(OBJS) $(INSTALLDIR)


Plattformabhängigkeiten

Jede Computer-Plattform hat ihre Eigenheiten, und Kernel-Designern steht es frei, alle diese Eigenheiten auszunutzen, um in der Objektdatei eine bessere Performance zu erreichen.

Im Gegensatz zu Applikationsentwicklern, die ihren Code gegen vorkompilierte Bibliotheken linken und sich an Konventionen zur Parameterübergabe halten müssen, können Kernel-Entwickler einige Prozessor-Register für bestimmte Aufgaben reservieren und haben das auch getan. Außerdem kann Kernel-Code für einen bestimmten Prozessor in einer CPU-Familie optimiert sein, um das Maximum aus der Zielplattform herauszuholen: Im Gegensatz zu Applikationen, die oft binär verteilt werden, kann eine spezialisierte Kernel-Kompilation für einen bestimmten Computer optimiert sein.

Modularisierter Code muß mit den gleichen Optionen wie der Kernel kompiliert werden, um mit diesem zusammenarbeiten zu können (es müssen also die gleichen Register zur besonderen Verwendung reserviert und die gleichen Optimierungen vorgenommen werden). Aus diesem Grund bindet unser Top-level-Rules.make eine plattformabhängige Datei ein, die die Makefiles durch zusätzliche Definitionen ergänzt. Alle diese Dateien heißen Makefile.plattform und weisen den make-Variablen für die aktuelle Kernel-Konfiguration passende Werte zu.

Ein weiteres interessantes Feature dermaßen aufgebauter Makefiles ist die Unterstützung der Cross-Kompilation in einem gesamten Baum von Beispieldateien. Wann immer Sie für eine bestimmte Zielplattform cross-kompilieren müssen, müssen Sie all Ihre Werkzeuge (gcc, ld usw.) durch andere Werkzeuge (zum Beispiel m68k-linux-gcc und m68k-linux-ld ) ersetzen. Das zu verwendende Präfix ist als $(CROSS_COMPILE) definiert, entweder auf der make-Kommandozeile oder in Ihrer Umgebung.

Die SPARC-Architektur ist ein Sonderfall, der ebenfalls von den Makefiles behandelt werden muß. User-Space-Programme, die auf der SPARC64-(SPARC V9-)Plattform laufen, sind die gleichen Binaries wie auf SPARC32 (SPARC V8). Der Default-Compiler auf SPARC64 (gcc) erzeugt daher SPARC32-Objekt-Code. Der Kernel benötigt aber SPARC V9-Objekt-Code, weswegen ein Cross-Compiler her muß. Alle Linux-Distributionen für SPARC64 enthalten einen passenden Cross-Compiler, den die Makefiles auswählen.

Auch wenn die vollständige Liste der Versions- und Plattformabhängigkeiten noch etwas komplizierter als hier beschrieben ist, sollten die vorangegangene Beschreibung und die von uns bereitgestellten Makefiles für den Anfang genug sein. Wenn Sie detailliertere Informationen wünschen, können Sie die Makefiles und die Kernel-Quellen durchlesen.

Fußnoten

[1]

Wir verwenden das Wort “lokal” hier, um damit unsere eigenen Änderungen am System zu bezeichnen, und befinden uns damit in guter Tradition von /usr/local.

[2]

Beachten Sie aber, daß jede Optimierung größer als –O2 riskant ist, da der Compiler eventuell Funktionen als inline behandeln könnte, die nicht im Quellcode als inline deklariert sind. Das kann bei Kernel-Code ein Problem sein, da manche Funktionen ein bestimmtes Stack-Layout bei ihrem Aufruf erwarten.

[3]

Damit sind bis zu 256 Entwicklungsversionen zwischen den stabilen Versionen möglich.