Aus Linux-Magazin 06/2006

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 28

Mit jeder 64-Bit-Maschine, die über die Ladentheke geht, stehen die Kernelprogrammierer auf dem Prüfstand: Ist ihr eigener Code 64-Bit-tauglich oder nicht? Auch bei der Portierung auf exotische Prozessoren gibt es einiges zu beachten.

Bis vor kurzem war die Welt noch einfach: Alle gängigen PCs waren mit einem Prozessor der Pentium-Klasse bestückt, deren Registerbreite stets 32 Bit beträgt. Zwar unterstützt der Linux-Kernel schon seit Jahren auch andere Architekturen. Aber Hand aufs Herz: Auf der Mehrheit der Maschinen prangt ein Intel- oder ein AMD-Logo.

64 statt 32 Bit

Die Sache mit dem Logo hat sich seitdem nicht grundlegend geändert, doch bei der Registerbreite hat AMD mit der ursprünglich AMD64 genannten Technik einen Sprung getan. Intel folgte mit der kompatiblen Technik EMT64 nach, sodass neue Rechner heute problemlos mit 64 Bit rechnen.

Beim Datentyp »long« kommt der wesentliche Unterschied zwischen 32- und 64-Bit-Technik zum Tragen: Er umfasst auf einem 32-Bit-System tatsächlich 32 Bit, auf dem 64-Bit-System 64. Anders beim Datentyp »int«: Dass er auf einem 64-Bit-Rechner ebenfalls 64 Bit umfasst, ist ein weit verbreiteter Irrglaube. Auf beinahe allen Plattformen ist »int« 32 Bit breit.

Spürbar wird dieser Unterschied dann, wenn ein Programmierer sich unbedacht »unsigned int« als Datentyp für einen so genannte neutralen Parameter wählt, dessen wirklicher Typ sich erst beim Aufruf entscheidet. Die meisten Kernelprogrammierer setzen dafür entweder den Typ »unsigned long« oder »void *« ein. Der entscheidende Punkt ist, dass der Datentyp »unsigned int« mit seinen 32 Bit auf einem 64-Bit-System nicht der Breite des Adressbusses entspricht. Während auf einer 32-Bit-Maschine keine Probleme auftreten (siehe Abbildung 1), ist es fatal, unter einem 64-Bit-Linux die Warnung des Compilers zu übersehen (Abbildung 2).

Abbildung 1: Ausgabe des Programms von Listing 1 auf einer 32-Bit-Maschine. Beide Adressen sind wie erwartet identisch.

Abbildung 1: Ausgabe des Programms von Listing 1 auf einer 32-Bit-Maschine. Beide Adressen sind wie erwartet identisch.

Abbildung 2: Ausgabe des Programms von Listing 1 auf einer 64-Bit-Maschine. Der Compiler warnt bereits bei der Übersetzung vor Problemen, die beim Ablauf auch prompt auftreten.

Abbildung 2: Ausgabe des Programms von Listing 1 auf einer 64-Bit-Maschine. Der Compiler warnt bereits bei der Übersetzung vor Problemen, die beim Ablauf auch prompt auftreten.

Listing 1 zeigt beispielhaft, wie ein derartiger, nicht portierbarer Code aussehen kann. Auf einem 64-Bit-System gehen beim Casting einer 8 Byte belegenden Adresse (»&var«) auf einen »int« (Zeile 14) die oberen 4 Bytes verloren. Die Abbildungen 1 und 2 zeigen die zugehörigen Ausgaben auf einer 32- und auf einer 64-Bit-Maschine. Während bei einer Portierung von 32 Bit auf 64 Bit das geschilderte Problem virulent ist, kann es im umgekehrten Fall zu Bereichsüberschreitungen kommen. Ein Beispiel ist der Jiffies-Zähler vom Typ »unsigned long«: Während die knapp 200 Tage bis zum Wrap-Around auf dem 32-Bit-System innerhalb der gesetzlichen Gewährleistungspflicht liegen, brauchen Besitzer von 64-Bit-Maschinen sich erst nach gut 2000 Milliarden Jahren Sorgen zu machen.

Listing 1: Typecast mit
Datenverlust

01 #include <stdio.h>
02 
03 void print_para_adr( unsigned int neutraler_parameter )
04 {
05    printf("Adresse 2: %pn", (char *)neutraler_parameter);
06 }
07 
08 int main( int argc, char **argv )
09 {
10    int var;
11 
12    printf("Adresse 1: %pn", &var );
13    print_para_adr( (int)&var );
14    return 0;
15 }

Ein anderer Problemfall sind Signale: Auf der 32-Bit-Maschine braucht man zur Verarbeitung von 64 Signalen zwei Wörter, auf einem 64-Bit-System dagegen nur eines. Man sollte sich als Programmierer also lieber zweimal überlegen, ob der Wertebereich der definierten Variablen auch auf Maschinen unterschiedlicher Wortbreite ausreicht. Der geschickte Einsatz des Makros »BITS_PER_LONG« hilft im Übrigen – wie bei den Signalen – dabei, in der Länge begrenzte Datenfelder portabel zu beherrschen.

Portable Datentypen

Wer eine Speicherzelle mit definierter Wortbreite braucht, kann auf entsprechende Typedefs zurückgreifen. Die in der Headerdatei »asm/types.h« definierten Datentypen »u8«, »u16«, »u32«, »u64« sowie »s8«, »s16«, »s32« und »s64« stellen entsprechende vorzeichenfreie und vorzeichenbehaftete Speicherzellen zur Verfügung. Tauscht der Kernel solche Daten mit einer Applikation aus, zum Beispiel im Rahmen eines IO-Control, sollte man auch die im Userspace bekannten Versionen mit den zwei Unterstrichen verwenden: »__u8«, »__u16«, »__u32«, »__u64« und »__s8«, »__s16«, »__s32« und »__s64« (siehe Tabelle 1).

Tabelle 1:
Datentypen für bitweise definierte Wörter

 

Format

Datentypnamen

Host-Format

__u8, __u16, __u32, __u64

Little-Endian-Format

__le16, __le32, __le64

Big-Endian-Format (Netzformat)

__be16, __be32, __be64

Endianness

Doch selbst Prozessoren mit gleicher Wortbreite unterscheiden sich noch so grundlegend, dass es zu Portierungsproblemen kommt. Unterschiedliche Prozessoren legen Daten nämlich auch anders im Speicher ab. Denn unglücklicherweise gibt es gleich zwei Formate, um 16-, 32- oder 64-Bit-Wörter im Speicher zu repräsentieren.

Abbildung 3 zeigt, dass das Little-Endian-Format das niederwertige Byte eines Wortes auf die niederwertige Adresse legt. So macht es etwa der klassische PC mit x86-Prozessoren. Eine Sun-Workstation mit Sparc-Prozessor legt dagegen das niederwertige Byte an die höherwertige Adresse. Es gibt sogar Prozessoren, die beides können.

Abbildung 3: Prozessoren legen die einzelnen Bytes eines Datenwortes unterschiedlich im Speicher ab. Beim Little-Endian-Format steht das niederwertige Byte an der niederwertigen (»0«), bei Big Endian dagegen an der höherwertigen Adresse.

Abbildung 3: Prozessoren legen die einzelnen Bytes eines Datenwortes unterschiedlich im Speicher ab. Beim Little-Endian-Format steht das niederwertige Byte an der niederwertigen (»0«), bei Big Endian dagegen an der höherwertigen Adresse.

Der Endian-Unterschied wird dann zum Problem, wenn zwei oder mehr Prozessoren, die unterschiedliche Datenablageformate verwenden, auf denselben Datensatz zugreifen. Bei vernetzten Rechnersystemen ist das ohnehin oft der Fall. Das Problem kann aber auch beim Zugriff auf intelligente Hardware auftreten, deren Anbindung zumeist über ein Speicherinterface (Dual-Port-RAM, Shared Memory) realisiert ist. Gleiches gilt beim Speichern von Daten auf einer Festplatte – spätestens seit USB 2.0 und Firewire sind diese portabel und leicht von einem zum anderen Rechner zu transportieren.

Byte-Swapping sparen

Zur Lösung des Problems bietet Linux vom Ablageformat des Prozessors unabhängige Datentypen an: »__le16«,» __le32«, »__le64« und »__be16«, »__be32«, »__be64« (siehe Tabelle 1). Außerdem kann der Programmierer auf Makros zurückgreifen, die zwischen den Formaten konvertieren (siehe Tabelle 2).

Tabelle 2:
Funktionen zur Konvertierung zwischen
Wort-Ablageformaten

 

Erklärung

Funktionsnamen

Umwandlung aus dem Host- ins Netzwerk-Format (Big Endian)

__be16 htons( __u16 value ), __be32 htonl( __u32 value )

Umwandlung aus dem Netzwerk- (Big Endian) ins Host-Format

__u16 ntohs( __be16 value ), __132 ntohl( __be32 value );

Umwandlung aus dem Host- ins Little-Endian-Format

__le16 cpu_to_le16( __u16 value ), __le32 cpu_to_le32( __u32
value ), __le64 cpu_to_le64( __u64 value )

Umwandlung aus dem Host- ins Little-Endian-Format, das
Ergebnis

der Wandlung wird in der ursprünglichen Variablen abgelegt (in
situ)

void cpu_to_le16s( __u16 *value ), void cpu_to_le32s( __u32
*value ), void cpu_to_le64s( __u64 *value )

Umwandlung aus dem Host- ins Big-Endian-Format

__be16 cpu_to_be16( __u16 value ), __be32 cpu_to_be32( __u32
value ), __be64 cpu_to_be64( __u64 value )

Umwandlung aus dem Host- ins Big-Endian-Format, das
Ergebnis

der Wandlung wird in der ursprünglichen Variablen abgelegt (in
situ)

void cpu_to_be16s( __u16 *value ), void cpu_to_be32s( __u32
*value ),

void cpu_to_be64s( __u64 *value )

Umwandlung einer Little-Endian-Variablen ins Host-Format

__u16 le_to_cpu16( __le16 value ), __u32 le_to_cpu32( __le32
value ), __u64 le_to_cpu64( __le64 value )

Umwandlung einer Little-Endian-Variablen ins Host-Format, das
Ergebnis

wird in der Variablen selbst abgelegt (in situ)

void le_to_cpu16s( __le16 *value ), void le_to_cpu32s( __le32
*value ),

void le_to_cpu64s( __le64 *value )

Umwandlung einer Big-Endian-Variablen ins Host-Format

__u16 be_to_cpu16( __be16 value ), __u32 be_to_cpu32( __be32
value ), __u64 be_to_cpu64( __be64 value )

Umwandlung einer Big-Endian-Variablen ins Host-Format, das
Ergebnis

wird in der Variablen selbst abgelegt (in situ)

void be_to_cpu16s( __be16 *value ), void be_to_cpu32s( __be32
*value ),

void be_to_cpu64s( __be64 *value )

Damit bei dieser Umwandlung nicht bereits vorher feststehen muss, ob der verwendete Prozessor ein Little- oder ein Big-Endian-Format unterstützt, wandeln sie immer in das CPU- oder Host-Format respektive aus dem CPU- oder dem Host-Format. Dabei ist natürlich berücksichtigt, ob die CPU selbst das Zielformat unterstützt. Dann wird beim Kompilieren der Wert ohne Byte-Swapping übernommen (siehe »__cpu_to_le32(x)« in Listing 2 , Zeile 2).

Listing 2:
»linux/byteorder/little_endian.h«

01 ...
02 #define __cpu_to_le32(x) ((__u32)(x))
03 #define __le32_to_cpu(x) ((__u32)(x))
04 #define __cpu_to_le16(x) ((__u16)(x))
05 #define __le16_to_cpu(x) ((__u16)(x))
06 #define __cpu_to_be64(x) __swab64((x))
07 #define __be64_to_cpu(x) __swab64((x))
08 #define __cpu_to_be32(x) __swab32((x))
09 #define __be32_to_cpu(x) __swab32((x))
10 ...

Die Makros, die im Netzwerkbereich zum Einsatz kommen, heißen: »htons()« (host to network short), »htonl()« (host to network long), »ntohs()« (network to host short) und »ntohl()« (network to host long). Ein Makro »htonc()« (host to char) gibt es nicht, denn das Problem tritt nur bei Variablen auf, die 16, 32 oder 64 Bit umfassen. Der Name Htonl ist übrigens irreführend: Diese Funktion wandelt das Host-Format in ein 32-Bit-Wort (Integer). Wäre es wirklich ein »long«, käme es zu Problemen mit allen 64-Bit-Maschinen. Das Netzwerkformat ist traditionell vom Typ Big Endian. Auf dem x86-PC müssen daher alle Wörter in den Protokollheadern gedreht werden. Eine Sun-Workstation mit Sparc-Prozessor kann darauf verzichten.

Außerhalb der Netzwerkprogrammierung benutzt der Linux-Kernel die Makros »cpu_to_leX()«, »cpu_to_leXs()«, »cpu_to_beX()« und »cpu_to_beXs()« (siehe Tabelle 2), die auch 64 Bit breite Variablen unterstützen. Das angehängte »s« bei einigen der Makros steht für “in situ”. Diese Makros wandeln direkt die Variable selbst. Deshalb besitzen sie keinen Rückgabewert und bekommen nicht eine Kopie, sondern die Adresse der Variablen übergeben.

Das Makro »htons()« entspricht damit der Funktion »cpu_to_be16()«, »htonl()« entspricht »cpu_to_be32()«. Wie man mit Hilfe der Datentypen und Makros programmiert, demonstriert der Kernelcode, der das Ext-2-Filesystem realisiert. Einen Ausschnitt daraus zeigt Listing 3.

Listing 3: Ausschnitt aus
»linux/ext2_fs.h«

01 ...
02 struct ext2_super_block {
03         __le32  s_inodes_count;         /* Inodes count */
04         __le32  s_blocks_count;         /* Blocks count */
05         __le32  s_r_blocks_count;       /* Reserved blocks count */
06         __le32  s_free_blocks_count;    /* Free blocks count */
07         __le32  s_free_inodes_count;    /* Free inodes count */
08         __le32  s_first_data_block;     /* First Data Block */
09         __le32  s_log_block_size;       /* Block size */
10         __le32  s_log_frag_size;        /* Fragment size */
11         __le32  s_blocks_per_group;     /* # Blocks per group */
12         __le32  s_frags_per_group;      /* # Fragments per group */
13         __le32  s_inodes_per_group;     /* # Inodes per group */
14         __le32  s_mtime;                /* Mount time */
15         __le32  s_wtime;                /* Write time */
16         __le16  s_mnt_count;            /* Mount count */
17         __le16  s_state;                /* File system state */
18 ...
19 #define EXT2_HAS_COMPAT_FEATURE(sb,mask)                        
20         ( EXT2_SB(sb)->s_es->s_feature_compat & cpu_to_le32(mask) )
21 #define EXT2_HAS_RO_COMPAT_FEATURE(sb,mask)                     
22         ( EXT2_SB(sb)->s_es->s_feature_ro_compat & cpu_to_le32(mask) )
23 ...

Portable Hardware-Kommunikation

Gerade beim Zugriff auf Hardware muss der Programmierer die zwischen dem Hauptprozessor und der Peripherie ausgetauschten Daten mit Hilfe der vorgestellten Datentypen definieren und über die Makros umwandeln. Außerdem darf er die Zugriffe selbst nur über die bereitgestellten Funktionen »read[bwl]()« (read byte, word oder long) respektive »write[bwl]()« (write byte, word oder long) programmieren.

Unterstützt der Prozessor einen eigenen IO-Bereich (Portzugriff), kommen zusätzlich die Funktionen »in[bwl]()« respektive »out[bwl]()« in Frage. Die genaue Verwendung der Funktionen ist in einer früheren Kern-Technik-Folge [1] beschrieben. Dass die Breite des Port-Bereichs auf den Plattformen unterschiedlich ist (x86: 16 Bit, ARM: 32 Bit), bleibt für Programmierer transparent.

Allerdings ist darauf zu achten, ob die Hardware die über einen gemeinsamen Speicher ausgetauschten und in einer Datenstruktur zusammengefassten Variablen an spezifischen Adressen erwartet. Der Compiler legt nämlich einzelne Variablen einer Datenstruktur aus Gründen der Performance an so genannten ausgerichteten (aligned) Adressen ab.

Da ein 32-Bit-Prozessor ein anderes Alignment als ein 16-Bit-Prozessor aufweist, muss der Programmierer sicherstellen, dass die Variablen eine eindeutige Adressenlage besitzen. Das GCC-spezifische Schlüsselwort »__attribute__((packed))« hilft ihm dabei. Abbildung 4 stellt das Problem und die Verwendung des Schlüsselworts beispielhaft dar.

Abbildung 4: Mit Hilfe des Schlüsselworts »packed« kann der Programmierer Einfluss auf die Anordnung von Strukturelementen im Speicher nehmen.

Abbildung 4: Mit Hilfe des Schlüsselworts »packed« kann der Programmierer Einfluss auf die Anordnung von Strukturelementen im Speicher nehmen.

Natürlich gibt es noch weitere Fallen. Doch der Linux-Kernel bemüht sich darum, dem Programmierer Portierungsfehler so schwer wie möglich zu machen. So sind im Kernel einige Datentypen über Typedefs abstrakt gehalten (»ssize_t«, »size_t«, »pid_t«, …). Der Typ »ssize_t« beispielsweise ist, abhängig von der Plattform, entweder auf den Datentyp »int« oder aber auf »long« abgebildet. Deshalb sollte man im eigenen Code grundsätzlich die vordefinierten Datentypen verwenden.

Seitengröße

Auch die Seitengröße (Pagesize) führt schließlich noch zu Portabilitätsproblemen. Tabelle 3 zeigt, dass sie zwischen den verschiedenen Plattformen stark variiert. Einige Kernelfunktionen – zum Beispiel Funktionen des Gerätemodells (Sys-Filesystem) – übergeben jeweils eine Speicherseite. Das Einzige, worauf sich der Programmierer hier verlassen kann, ist, dass sie eine Minimalgröße von 4 KByte aufweist.

Tabelle 3:
Portierungsmerkmale ausgewählter Linux-Plattformen

 

Name

Bezeichnung

Wortbreite

Größe (»long«)

Seitengröße

x86

i386

32 Bit

4

4 KByte

AMD64/EMT64

x86-64

64 Bit

8

4 KByte

Alpha

alpha

64 Bit

8

8 KByte

Arm/Strong ARM

arm26

32 Bit

4

32, 16 KByte

HP PA-Risc

parisc

32 oder 64 Bit

4/8

4 KByte

Etrax 100LX

cris

32 Bit

4

8 KByte

IA-64

ia64

64 Bit

8

4, 8, 16, 64 KByte

Mips

mips

32 oder 64 Bit

4/8

4, 8, 16, 64 KByte

Motorola 68xxx

m68k

32 Bit

4

4, 8 KByte

PowerPC

ppc

32 Bit

4

4 KByte

PowerPC64

ppc64

64 Bit

8

4 KByte

S390

s390

32 oder 64 Bit

4/8

4 KByte

Sun Sparc

sparc

32 Bit

4

4, 8 KByte

Ultra Sparc

sparc64

64 Bit

8

8, 64, 512, 4096 KByte

User Mode Linux

um

32 oder 64 Bit

4/8

4 KByte

Zum Abschluss die drei wichtigsten Regeln für plattformunabhängigen Code zusammengefasst: Auf die Datentypen achten, an der richtigen Stelle konvertieren und auf Hardware über Kernelfunktionen zugreifen. (ofr)

Infos

[1] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik”, Folge 3: Linux-Magazin 10/03, S. 81

[2] Greg Kroah-Hartman: Writing Portable Device Drivers, [http://www.linuxdevices.com/articles/AT5340618290.html]

[3] Robert Love, ” Linux Kernel Development”, S. 321: Novell Press, 2. Auflage, 2005

[4] Corbet et.al., “Linux Device Drivers”, S. 288: O\’Reilly, 3. Auflage, 2005

Die Autoren

Eva-Katharina Kunst, Journalistin, und Jürgen Quade, Professor an der Hochschule Niederrhein, sind seit den Anfängen von Linux Fans von Open Source. Unter dem Titel “Linux Treiber entwickeln” haben sie zusammen ein Buch zum Kernel 2.6 veröffentlicht.

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
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben