Versionskontrolle in Modulen

Eines der Hauptprobleme beim Arbeiten mit Modulen ist ihre Versionsabhängigkeit, die wir bereits in “the Section called Versionsabhängigkeit in Kapitel 2” in Kapitel 2 erwähnt haben. Es kann mühsam werden, die Module gegen die Header-Datei jeder verwendeten Version kompilieren zu müssen, wenn Sie mehrere eigene Module verwenden; und wenn Sie ein in binärer Form ausgeliefertes kommerzielles Modul verwenden, ist das Neukompilieren überhaupt nicht möglich.

Glücklicherweise haben die Kernel-Entwickler eine flexible Möglichkeit gefunden, um mit Versionsproblemen umzugehen. Der Grundgedanke ist, daß ein Modul nur dann inkompatibel mit einer anderen Kernel-Version ist, wenn sich die Software-Schnittstelle des Kernels verändert hat. Die Software-Schnittstelle kann also durch einen Funktionsprototyp und die exakte Definition aller im Funktionsaufruf verwendeten Datenstrukturen repräsentiert werden. Ein CRC-Algorithmus[1] dient dazu, alle Informationen über die Software-Schnittstelle auf eine 32 Bit-Zahl abzubilden.

Das Problem der Versionsabhängigkeit wird also dadurch beseitigt, daß jedes vom Kernel exportierte Symbol mit einer Prüfsumme kombiniert wird. Diese Information wird durch das Parsen der Header-Dateien und das Extrahieren der in ihnen stehenden Informationen ermittelt. Dieses Verfahren ist optional und kann zur Kompilationszeit eingeschaltet werden. In modularen Kerneln, die von Linux-Distributoren geliefert werden, ist die Versionsunterstützung normalerweise eingeschaltet.

Beispielsweise wird das Symbol printk an Module als so etwas wie printk_R12345678 exportiert, wenn die Versionsunterstützung eingeschaltet ist, wobei 12345678 eine hexadezimale Repräsentation der Prüfsumme der von der Funktion verwendeten Software-Schnittstelle ist. Wenn ein Modul in den Kernel geladen wird, kann insmod (oder modprobe) seine Arbeit nur durchführen, wenn die zu jedem Symbol im Kernel hinzugefügte Prüfsumme zu dem jeweiligen Symbol im Modul paßt.

Dieses Verfahren hat einige Einschränkungen. Das Laden eines für SMP-Systeme kompilierten Moduls in einen Einzelprozessor-Kernel oder umgekehrt sorgt häufig für Überraschungen. Weil diverse Inline-Funktionen (wie etwa Spinlock-Operationen) und Symbole in SMP-Kerneln anders definiert sind, ist es wichtig, daß sich die Module und der Kernel darüber einig sind, ob sie für SMP gebaut worden sind oder nicht. Die Version 2.4 und neuere 2.2-Kernel ergänzen jedes Symbol um smp_, wenn für SMP kompiliert wird, um diesen Sonderfall abzudecken. Es gibt aber trotzdem noch Fallgruben. Module und der Kernel können sich in der verwendeten Compiler-Version unterscheiden, darin wie sie den Speicher organisieren, für welche Prozessorversion sie kompiliert worden sind und in vielem mehr. Die Versionsunterstützung kann die gängigsten Probleme abfangen, aber man sollte trotzdem vorsichtig sein.

Schauen wir uns an, was im Kernel und im Modul passiert, wenn die Versionsunterstützung eingeschaltet ist:

Beachten Sie, daß der Kernel und das Modul sich einig darüber sein müssen, ob die Versionsunterstützung verwendet werden soll oder nicht. Wenn einer von beiden Symbole mit Versionsinformationen verwendet und der andere nicht, lehnt insmod das Laden des Moduls ab.

Versionsunterstützung in Modulen verwenden

Treiber-Autoren müssen einiges an expliziter Unterstützung in ihre Module einbauen, damit diese mit Versionen funktionieren. Dies kann an einer von zwei Stellen geschehen: im Makefile oder in der Quelle selbst. Weil die Dokumentation des Pakets modutils beschreibt, wie man das im Makefile macht, zeigen wir Ihnen hier, wie es in den C-Quellen geschieht. Wir verwenden das master-Modul, um zu zeigen, wie kmod mit Versionsinformationen behaftete Symbole unterstützt. Diese Fähigkeit wird automatisch eingeschaltet, wenn der Kernel, mit dem die Module kompiliert wurden, Versionsunterstützung beinhaltet.

Der wichtigste Bestandteil zum Hinzufügen von Versionsinformationen zu Symbolnamen ist die Header-Datei <linux/modversions.h>, die Präprozessordefinitionen für alle öffentlichen Kernel-Symbole enthält. Diese Datei wird während der Kompilation des Kernels (genauer gesagt, im “make depend”-Schritt) erzeugt. Wenn Ihr Kernel noch nie oder bisher ohne Versionsinformationen gebaut worden ist, wird da nicht viel Interessantes drinstehen. <linux/modversions.h> muß vor allen anderen Header-Dateien eingebunden werden; stellen Sie sie also in Ihren Treiber-Quellen an den Anfang. Die übliche Technik ist aber, gcc mitzuteilen, die Datei direkt einzubinden:

gcc -DMODVERSIONS -include /usr/src/linux/include/linux/modversions.h...

Nach dem Einbinden des Headers sieht der Compiler immer die mit Versionsinformationen ergänzte Version, wenn das Modul ein Kernel-Symbol verwendet.

Um die Versionsinformationen im Modul einzuschalten, wenn sie bereits im Kernel aktiviert worden sind, müssen wir sichergehen, daß CONFIG_MODVERSIONS bereits in <linux/autoconf.h> definiert worden ist. Diese Header-Datei gibt an, welche Funktionalität im aktuellen Kernel aktiviert (also hineinkompiliert) worden ist. Jedes definierte CONFIG_-Makro gibt an, daß die zugehörige Option aktiviert ist.[2]

master.c beginnt daher mit den folgenden Zeilen:


 
#include <linux/config.h> /* CONFIG_*-Makros holen */
#if defined(CONFIG_MODVERSIONS) && !defined(MODVERSIONS)
#   define MODVERSIONS /* erzwingen */
#endif

#ifdef MODVERSIONS
#  include <linux/modversions.h>
#endif

Wenn diese Datei gegen einen Kernel mit Versionsinformationen kompiliert wird, bezieht sich die Symboltabelle im Objekt auf die Symbolnamen mit Versionsinformationen, die zu denen passen, die der Kernel selbst exportiert. Der folgende Auszug zeigt die Symbolnamen in master.o. In der Ausgabe von nm bedeutet “T” “Text”, “D” “Daten” und “U” “undefiniert”. “Undefiniert” kennzeichnet Symbole, die von der Objektdatei referenziert, aber nicht definiert werden.


00000034 T cleanup_module
00000000 t gcc2_compiled.
00000000 T init_module
00000034 T master_cleanup_module
00000000 T master_init_module
         U printk_Rsmp_1b7d4074
         U request_module_Rsmp_27e4dc04
morgana% fgrep 'printk' /proc/ksyms
c011b8b0 printk_Rsmp_1b7d4074

Weil die Prüfsummen, die den Symbolnamen in master.o hinzugefügt worden sind, die gesamte Schnittstelle von printk und request_module beinhalten, ist das Modul kompatibel mit einer großen Anzahl von Kernel-Versionen. Wenn aber die Datenstrukturen, die von einer der beiden Funktionen benutzt werden, verändert werden, wird insmod das Modul wegen der Inkompatibilität mit dem Kernel nicht laden.

Exportieren von Symbolen mit Versionsinformationen

Eine Situation wurde bisher noch nicht besprochen: Was passiert, wenn ein Modul Symbole exportiert, die von anderen Modulen verwendet werden sollen? Wenn wir uns auf die Versionsinformationen verlassen, um die Portabilität des Moduls sicherzustellen, dann würden wir auch unseren eigenen Symbolen gern einen CRC-Code hinzufügen. Dieser Bereich ist etwas kniffliger als das bloße Hinzulinken zum Kernel, weil wir die modifizierten Symbolnamen an die anderen Module exportieren müssen; wir müssen also einen Weg finden, die Prüfsummen zu erzeugen.

Das Programm genksyms ist dafür zuständig, die Header-Dateien zu parsen und die Prüfsummen zu erzeugen. Es stammt aus dem modules-Paket. Das Programm erwartet die Ausgabe des C-Präprozessors auf seiner Standardeingabe und gibt eine neue Header-Datei auf der Standardausgabe aus. Diese enthält Namen mit Prüfsummen für jedes Symbol, das von der ursprünglichen Quelldatei exportiert wird. Die Ausgabe von genksyms wird meistens mit dem Suffix .ver abgespeichert. Wir werden hier dieser Praxis folgen.

Um Ihnen zu zeigen, wie Symbole exportiert werden, haben wir zwei Testmodule namens export.c und import.c geschrieben. export exportiert eine einfache Funktion namens export_function, die vom zweiten Modul, import.c, importiert wird. Diese Funktion erwartet zwei Integer-Argumente und gibt deren Summe zurück. Wir sind hier aber nicht an der Funktion selbst, sondern am Link-Vorgang interessiert.

Das Makefile im Verzeichnis misc-modules enthält eine Regel, um die Datei export.ver aus export.c zu erzeugen, so daß im Modul import das mit einer Prüfsumme versehene Symbol für export_function verwendet werden kann:


 
ifdef CONFIG_MODVERSIONS
export.o import.o: export.ver
endif

export.ver: export.c
        $(CC) -I$(INCLUDEDIR) $(CFLAGS) -E -D_ _GENKSYMS_ _ $⁁ | \
                $(GENKSYMS) -k 2.4.0 > $@

Diese Zeilen führen vor, wie export.ver erzeugt wird, und fügen diese Datei zu den Abhängigkeiten der beiden Objektdateien hinzu; allerdings nur, wenn MODVERSIONS definiert ist. Einige wenige zusätzliche Zeilen in Makefile sorgen dafür, daß MODVERSIONS definiert ist, wenn die Versionsunterstützung im Kernel aktiviert ist, aber es lohnt sich nicht, diese hier zu zeigen. Die Option -k muß verwendet werden, um genksyms mitzuteilen, mit welcher Kernel-Version Sie arbeiten. Damit wird nur das Format der Ausgabedatei bestimmt; die verwendete Kernel-Version muß nicht hundertprozentig stimmen.

Die Definition des Symbols GKSMP ist jedoch interessant. Wie bereits erwähnt, wird jeder Prüfsumme ein Präfix (-p smp_) vorangestellt, wenn der Kernel für SMP-Systeme gebaut wird. Das Hilfsprogramm genksyms fügt dieses Präfix nicht von selbst hinzu, sondern muß darauf hingewiesen werden. Der folgende Makefile-Ausschnitt sorgt dafür, daß alles korrekt zugeht:


ifdef CONFIG_SMP
        GENKSYMS += -p smp_
endif

Die Quelldatei muß also die richtigen Präprozessor-Symbole für jeden denkbaren Präprozessor-Lauf deklarieren: die Eingabe von genksyms und die eigentliche Kompilierung, beide mit und ohne Versionsunterstützung. Außerdem sollte export.c in der Lage sein, eine Versionsunterstützung im Kernel automatisch zu erkennen, genau wie master.c. Das gelingt mit den folgenden Zeilen:


 
#include <linux/config.h> /* CONFIG_*-Makros holen*/
#if defined(CONFIG_MODVERSIONS) && !defined(MODVERSIONS)
#   define MODVERSIONS
#endif

/*
 * Die Definitionen mit Versionssymbolen fuer sowohl
 * die  Kernelsymbole als auch unser Symbol holen, *sofern*
 * wir  keine Pruefsummen erzeugen. (_ _GENKSYMS_ _ defined) */
#if defined(MODVERSIONS) && !defined(_ _GENKSYMS_ _)
#    include <linux/modversions.h>
#    include "export.ver" /* "export_function" umdefinieren, so  dass CRC-Pruefsummen gebildet werden */
#endif

Der Code ist zwar schwierig, hat aber den Vorteil, das Makefile in einem sauberen Zustand zu lassen. Wenn wir die richtigen Schalter von make aus übergeben wollten, müßten wir für die verschiedenen Fälle lange Kommandozeilen schreiben, wozu wir hier keine Lust haben.

Das einfache Modul import ruft export_function auf und übergibt dabei die Zahlen 2 und 2 als Argumente; das erwartete Resultat ist also 4. Das folgende Beispiel zeigt, daß import tatsächlich die Symbole mit Versionsinformationen von export verwendet und die Funktion aufruft. Die Symbole mit Versionsinformationen erscheinen auch in /proc/ksyms.


morgana.root# insmod ./export.o
morgana.root# grep export /proc/ksyms
c883605c export_function_Rsmp_888cb211  [export]
morgana.root# insmod ./import.o
import: my mate tells that 2+2 = 4
morgana.root# cat /proc/modules
import                   312   0  (unused)
export                   620   0  [import]

Fußnoten

[1]

CRC bedeutet “cyclic redundancy check”, ein Verfahren, um kurze, eindeutige Zahlen aus beliebigen Datenmengen zu erzeugen.

[2]

Die CONFIG_-Makros sind in <linux/autoconf.h> definiert. Sie sollten statt dessen aber <linux/config.h> einbinden, weil letztere Datei vor doppeltem Einbinden geschützt ist und <linux/autoconf.h> intern einbindet.