Assembler-Programmierung für x86-Prozessoren/ Der Prozessor
Die 80x86-Prozessorfamilie
BearbeitenDirekt nach dem Einschalten des PCs befindet sich jeder x86-Prozessor im 8086-Kompatibilitätsmodus, dem sog. Real-Mode. Deshalb beginnen wir mit der Betrachtung dieses Urahns der x86-Architektur, da alles, was seine Programmierung und Architektur betrifft, noch immer Gültigkeit besitzt.
Der 8086 und 8088
BearbeitenDer 8086 ist ein 16-Bit-Prozessor. Er wurde 1978 vorgestellt und basiert auf seinen 8-Bit Vorgängern, dem 8080 und 8085. Dies hat Folgen z.B. bei der Speicheradressierung oder dem Registersatz.
Der 8088 ist aus Programmierersicht identisch mit dem 8086, wurde allerdings als Low-Cost-Variante des 8086 für den Einsatz mit älterer 8-Bit-Hardware entworfen. So kann der 8086 mit einem Speicherzugriff 16 Bit Daten übertragen, der 8088 nur 8 Bit. Die Aufteilung eines 16-Bit-Speicherzugriffs auf zwei 8-Bit-Zugriffe erfolgt automatisch durch die Hardware des 8088, der Programmierer merkt davon nichts. Dadurch ist die Rechenleistung des 8088 geringer als die des 8086.
Die Register
BearbeitenEin Register ist ein sehr schneller Speicherplatz im Prozessor, auf den Befehle direkt zugreifen können. Da Register relativ viel Platz auf dem Silizium belegen, besitzen grade ältere Prozessorarchitekturen meist sehr wenige davon. Je mehr Register ein Prozessor beinhaltet, desto mehr Zwischenwerte können während des Programmablaufes direkt im Prozessor gehalten werden. Dadurch sind weniger langsame Zugriffe auf den Arbeitsspeicher nötig und die Rechenleistung steigt. Die Register des 8086 sind alle 16 Bit breit, dies ist kennzeichnend für eine 16-Bit-Architektur.
Die Register können in verschiedene Typen unterteilt werden:
- Allzweck-Datenregister sind in der Regel universell einsetzbar und nehmen Operanden auf.
- Indexregister werden für die indirekte Adressierung verwendet, können jedoch auch wie Allzweckregister genutzt werden.
- Stackregister werden für die Verwaltung des Stacks benötigt. Der Stack ist eine Datenstruktur, auf der der Inhalt einzelner Register wie auf einem Stapel vorübergehend abgelegt werden können. Dies ist vor allem bei Unterprogrammaufrufen oder Interrupts nützlich.
- Spezialregister werden vom Prozessor intern benutzt und sind kaum durch den Programmierer beeinflussbar. Der Programmierer kann sie meist nur indirekt ändern.
- Segmentregister spielen eine besondere Rolle bei der Speicherverwaltung. Ihre Funktion wird im Abschnitt zur Speicheradressierung beschrieben.
Die Allzweckregister
BearbeitenDer 8086 ist mit vier Allzweckregistern ausgestattet.
- AX - Das Akkumulator-Register
- BX - Das Basis-Register
- CX - Das Counter-Register
- DX - Das Data-Register
Eine Besonderheit dieser Register ist, dass sie auch als Teilregister mit je 8 Bit benutzt werden können. So lässt sich das Low-Byte von AX, also die Bits Null bis Sieben, als AL ansprechen, das High-Byte heißt AH. Das funktioniert nur mit den Registern AX, BX, CX und DX, mit keinem anderen!
Auch wenn diese Register für fast alle Operationen verwendet werden können, spielen sie bei bestimmten Befehlen eine Sonderrolle. So wird das AX-Register häufig von arithmetischen Befehlen benutzt, das CX-Register dagegen ist bei der Assemblerversion von FOR-Schleifen der Schleifenzähler. Das BX-Register wird häufig im Zusammenhang mit indirekter Adressierung genutzt und das DX-Register erweitert bei bestimmten Befehlen das AX-Register auf die doppelte Breite.
Ein schönes Beispiel für so einen Befehl ist MUL. Der Befehl MUL BX etwa multipliziert AX mit BX und speichert das Ergebnis in dem Registerpaar DX:AX. In DX steht der höherwertige Teil des Ergebnisses, in AX der niederwertige. Der MUL-Befehl verwendet immer AX oder AL als einen Operanden der Multiplikation.
Die Indexregister
BearbeitenEs gibt zwei Indexregister, SI, den Source-Index und DI, den Destination-Index. Diese Register sind besonders für die Benutzung mit Stringbefehlen optimiert. String meint hier nicht unbedingt eine Zeichenkette sondern steht allgemein für Arrays. In den Datenblättern sind sie jedoch entsprechend benannt. Diese Register können auch wie Allzweckregister verwendet werden.
Die Stackregister
BearbeitenDie Stackregister bestehen aus dem Stacksegmentregister SS, dem Stackpointer SP und dem Basepointer BP. SS wird bei den Segmentregistern erläutert. SP zeigt immer auf die Spitze des aktuellen Stapelspeichers, BP ist nützlich bei der Parameterübergabe per Stack und kann sonst auch als weiteres Universalregister verwendet werden. Näheres zum Stack im entsprechenden Kapitel.
Der Instruction Pointer
BearbeitenDer Instruction-Pointer, kurz IP, enthält immer die Adresse des nächsten auszführenden Befehls und wird automatisch weitergezählt. Er wird nur durch Sprungbefehle und Unterprogrammaufrufe neu geladen, ansonsten kann auf ihn nicht zugegriffen werden.
Die Flags
BearbeitenDas Flag-Register enthält Status- und Steuerbits des Prozessors, die sog. Flags. Diese Bits zeigen bestimmte Vorkommnisse im Prozessor an, etwa ob das Ergebnis der letzten arithmetischen Operation Null war, und werden von bedingten Sprungbefehlen ausgewertet. Einige andere Flags können bestimmte Funktionen des Prozessors steuern. Ein gesetztes Flag hat den Wert Eins, ein gelöschtes den Wert Null. Bedingte Sprünge entsprechen in etwa if-Anweisungen.
Die Flags im Einzelnen:
- Bit 11 – Overflow Flag (OF)
- Bit 10 – Direction Flag (DF)
- Bit 9 – Interrupt Flag (IF)
- Bit 8 – Trap Flag (TF)
- Bit 7 – Sign Flag (SF)
- Bit 6 – Zero Flag (ZF)
- Bit 4 – Auxiliary Flag (AF)
- Bit 2 – Parity Flag (PF)
- Bit 0 – Carry Flag (CF)
Das Carry Flag (Bit Nr. 0)
BearbeitenDas Carry Flag wird gesetzt, wenn das Ergebnis einer Operation zu einem Werteüberlauf geführt hat, also wenn das Ergebnis größer als die größte darstellbare Zahl war oder kleiner als die Kleinste. Es wird auch gerne benutzt, um anzuzeigen, dass in einer Unterfunktion ein Fehler aufgetreten ist.
Das Parity Flag (Bit Nr. 2)
BearbeitenDas Parity Flag zeigt an, ob die Anzahl an Einsen des untersten Bytes einer vorangegangenen arithmetischen, logischen oder einer Vergleichsoperationen grade oder ungerade ist. Ist die Anzahl gerade, ist es gesetzt.
Dieses Flag stammt noch aus einer Zeit, in der häufig serielle Verbindungen genutzt wurden, die Paritätsbits zur Fehlererkennung benutzten. Heute wird es in dieser Funktion praktisch nicht mehr benötigt.
Das Auxiliary Flag (Bit Nr. 4)
BearbeitenDas Auxiliary Flag wird auch als Half-Carry bezeichnet und zeigt, wie das Carry Flag, einen Werteüberlauf an. Allerdings wird es bei einem Über- oder Unterlauf der unteren vier Bits eines Bytes gesetzt. Das wird vor allem für die Verarbeitung von BCD-Zahlen gebraucht.
Das Zero Flag (Bit Nr. 6)
BearbeitenDas Zero Flag wird gesetzt, wenn das Ergebnis einer arithmetischen oder logischen Operation Null ist. Es wird häufig benötigt, z.B. für die Konstruktion von Schleifen.
Das Sign Flag (Bit Nr. 7)
BearbeitenDas Sign Flag enthält jeweils eine Kopie des höchstwertigen Bits vom Ergebnis einer arithmetischen oder logischen Operation. Bei vorzeichenbehafteten Zahlen entspricht dies dem Vorzeichen. Es ist genau dann gesetzt, wenn das Ergebnis negativ ist. (Hierbei zählt 0 als positive Zahl)
Das Trap Flag (Bit Nr. 8)
BearbeitenDieses Flag wird von Debuggern und ähnlichen Programmen gesetzt, um eine Ausführung des Programmes in Einzelschritten zu ermöglichen.
Das Interrupt Flag (Bit Nr. 9)
BearbeitenDieses Flag zeigt an, ob der Prozessor Interrupts (hierzu später mehr) zulassen soll oder nicht.
Das Direction Flag (Bit Nr. 10)
BearbeitenDas Direction Flag indiziert eine Richtung (hin zu höheren oder hin zu niedrigeren Adressen), die eine Auswirkung auf die Funktion einiger Befehle hat. Die genaue Funktion wird bei den entsprechenden Befehlen beschrieben.
Das Overflow Flag (Bit Nr. 11)
BearbeitenDas Overflow Flag wird gesetzt, wenn das Ergebnis einer Operation zu einem Übertrag auf der höchsten Stelle geführt hat. Dies hat bei vorzeichenbehafteten Zahlen in etwa dieselbe Bedeutung wie das Setzen des Carry Flags bei Vorzeichenlosen.
80186, 80188
BearbeitenDer Vollständigkeit halber sollen hier noch kurz die Nachfolger dieser beiden Prozessoren erwähnt werden. Deren Befehlsumfang wurde erweitert und stellenweise optimiert, so dass ein 80186 etwa ein Viertel schneller arbeitet als ein gleich getakteter 8086. Der 80186 ist eher ein integrierter Mikrocontroller als ein Mikroprozessor, denn er enthält auch einen Interrupt Controller, einen DMA-Chip und einen Timer. Diese waren allerdings inkompatibel zu den vorher verwendeten externen Logiken des PC/XT, was eine Verwendung dort sehr schwierig machte. Darum wurde er im PC selten verwendet, war jedoch auf Adapterkarten als selbstständiger Mikroprozessor relativ häufig zu finden, beispielsweise auf ISDN-Karten.
Der 80188 ist wiederum eine abgespeckte Version mit 8-Bit-Datenbus, der 16-Bit-Zugriffe selbsttätig aufteilt. Wie beim 80188 sind 8-Bit-Zugriffe genauso schnell wie beim 80186, 16-Bit-Zugriffe benötigen die doppelte Zeit.
80286
BearbeitenMan kann den 80286 als direkten Nachfolger des 8086 sehen, da der 80186/80188 durch die Inkompatibilitäten in PCs nicht weit verbreitet war. Das interessanteste neue Feature war der 16 Bit Protected Mode. Damit ist es möglich, mehrere (quasi) gleichzeitig laufende Programme voneinander abzuschotten.
Dieser Modus ist in der x86-Architektur die Basis für moderne Betriebssysteme, die Multitasking und geschützte Speicherbereiche unterstützen. Zusätzlich steigt mit dem 80286 im Protected Mode der adressierbare Arbeitsspeicher von 1 MiB auf 16 MiB. Leider kann man den Protected Mode auf dem 286er nur durch einen Reset verlassen. Dadurch war er, zur Hochzeit von DOS, für die meisten Anwendungen eher unattraktiv und konnte sich auf dieser Platform nie richtig durchsetzen.
80386
BearbeitenMit dem 80386 entwickelte Intel erstmals eine 32-Bit-CPU. Damit konnte der Prozessor 4 GiB Speicher linear ansprechen. Da sich der Protected Mode beim 80286 anfänglich nicht durchsetzen konnte, enthielt die 80386-CPU einen virtuellen 8086-Modus. Er kann aus dem Protected Mode heraus gestartet werden und simuliert einen oder mehrere 8086-Prozessoren. Die internen Speichereinheiten des Prozessors, die Register, wurden von 16 Bit auf 32 Bit verbreitert.
Wie schon beim 8086 brachte Intel mit dem 80386SX eine Low-Cost-Variante in den Handel, die statt des 32 Bit breiten nur einen 16 Bit breiten Datenbus hatte.
80486
BearbeitenMit dem 80486-Prozessor brachte Intel eine überarbeitete 80386-CPU auf den Markt, die um einen Cache und einen internen mathematischen Coprozessor erweitert wurde. Da der Cache sehr viel schneller als der normale Hauptspeicher ist, beschleunigt er die Ausführung von Programmen erheblich. Dies wird dadurch erreicht, dass Daten im Voraus aus dem Hauptspeicher geladen werden können und dann sofort zur Verfügung stehen, wenn die CPU sie benötigt. Hin und wieder muss der Prozessor zwar immer noch auf den langsamen Hauptspeicher warten, nämlich wenn die Daten nicht im Cache vorhanden sind, aber eben viel seltener.
Den mathematischen Coprozessor kennen wir heute eher als FPU, die Fließkommaeinheit. Bis zum 80486 war das grundsätzlich ein zweiter Chip, der auf einem eigenen Sockel auf der Hauptplatine nachgerüstet werden konnte. Mit dem 80486SX gab es wieder eine „Sparvariante“ des normalen 80486, die ohne den integrierten mathematischen Coprozessor auskommen musste. Es gab dafür aber auch wieder einen Externen, den 80487. Der war aber so teuer, dass ein vollwertiger 80486 billiger als der Nachrüstchip war. Der Datenbus wurde beim 80486SX nicht beschränkt. Mit seinem Cache verhielt er sich wie ein leistungsfähigerer 80386.
80586/Pentium-Architektur
BearbeitenMit dem Pentium-Prozessor (und den kompatiblen Varianten der Konkurrenz) wurde unter anderem ein Konzept der parallelen Instruktionsausführung in die Prozessoren aufgenommen, die superskalare Ausführung. Der Prozessor analysiert die Befehle, die demnächst auszuführen sind. Falls keine direkte Abhängigkeit der Instruktionen untereinander besteht und die nötige Ressource (z. B. der Coprozessor) frei ist, wird die Ausführung vorgezogen und die Instruktion parallel ausgeführt. Dies verbessert die Ausführungsgeschwindigkeit deutlich, macht jedoch auch das Assembler Programmieren komplexer, denn die Ausführungszeit von Instruktionen hängt nun vom Kontext der vorher und nachher auszuführenden Instruktionen ab.
Mit der späteren MMX-Erweiterung der Pentium-Prozessor-Architektur fand eine weitere Form der parallelen Ausführung ihren Platz in der x86-Architektur, die parallele Datenverarbeitung auf Vektoren (SIMD). Hierzu wurde der Befehlssatz um Vektoroperationen erweitert, welche mit den Registern des Coprozessors verwendet werden können.
x86-64-Architektur
BearbeitenMit der x86-64-Architekturerweiterung, eingeführt von AMD und später von Intel übernommen, wurden die bisherigen Konzepte und Instruktionen der 32-Bit-x86-Prozessorarchitektur beibehalten. Die Register und Busse wurden von 32 Bit auf 64 Bit erweitert und ein neuer 64Bit Ausführungsmodus eingeführt, die 32-Bit-Betriebsmodi sind weiterhin möglich. Allerdings fiel hier der virtual protected Mode weg, der eh nicht mehr verwendet wurde.
Die Register der 8086/8088-CPU
BearbeitenDie Register im Einzelnen:
Adressregister
CS-, DS-, ES- und SS-Register
- Eine spezielle Bedeutung unter den Registern haben die Segmentregister CS (Codsegmentregister), DS (Datensegmentregister), ES (Extrasegmentregister) und SS (Stacksegmentregister). Sie bilden im so genannten Real Mode gemeinsam mit dem Offset die physikalische Adresse. Man kann sie nur dafür nutzen; sie können weder als allgemeine Register benutzt, noch direkt verändert werden.
Interrupts
BearbeitenWährend die CPU ein Programm bearbeitet, können verschiedene Ereignisse wie diese eintreten: Der Anwender drückt eine Taste, dem Drucker geht das Papier aus oder die Festplatte ist mit dem Schreiben der Daten fertig.
Um darauf reagieren zu können, hat der Prozessor zwei Möglichkeiten:
- Die CPU kann ständig nachfragen, ob so ein Ereignis eingetreten ist. Dieses Verfahren bezeichnet man als Polling oder auch "aktives Warten". Der Nachteil dieser Methode ist, dass der Prozessor die ganze Zeit mit Warten beschäftigt ist und nichts anderes machen kann.
- Das Gerät erzeugt ein Signal, sobald ein Ereignis eintritt, und schickt dieses über einen Interrupt-Controller an den Prozessor. Die CPU unterbricht das gerade laufende Programm, verarbeitet das Ereignis und setzt anschließend die Verarbeitung des unterbrochenen Programms fort. Das unterbrochene Programm bekommt von dem aufgetretenen Interrupt nichts mit.
Ein solches Interrupt-Signal ist ein Hardware-Interrupt, im Unterschied zu Software-Interrupts, die durch den INT
-Befehl ausgelöst werden. Hardware-Interrupts sind asynchron, d.h. es ist nicht vorhersagbar, wann sie ausgelöst werden. Ein Software-Interrupt hingegen wird vom laufenden Programm ausgelöst und ist damit synchron zum Programmablauf.
Manchmal ist es nötig, die Unterbrechung des Programms durch Interrupts zu verhindern, zum Beispiel bei der Manipulation der Interrupt-Vector-Tabelle. Dazu löscht man das Interruptflag (IF = 0) mit dem Befehl cli
(Clear Interruptflag). sti
(Set Interruptflag) setzt es (IF = 1). Sind die Interrupts auf diese Weise gesperrt, kann das System auf keine Ein- oder Ausgabe mehr reagieren. Der Anwender merkt das daran, dass das System seine Eingaben über die Tastatur oder die Maus nicht mehr bearbeitet.
Die 80x86-CPU hat zwei Interrupt-Eingänge, den INTR (Interrupt Request) und den NMI (Non Maskable Interrupt). Der Interrupt INTR ist für normale Anfragen an den Prozessor zuständig, während der NMI nur besonders wichtigen Interrupts vorbehalten ist. Das Besondere am Non Maskable Interrupt („nicht maskierbar“) ist, dass er nicht durch Löschen des Interruptflags unterdrückt werden kann.
Adressierung des Arbeitsspeichers im Real Mode
BearbeitenDer Adressbus dient der CPU dazu, Speicherplätze im Arbeitsspeicher anzusprechen, indem sie die Adresse eines Speicherplatzes über die Leitungen des Busses sendet. Die 8086/88-CPU hat dafür 20 Adressleitungen, die den Adressbus bilden. Damit könnte der Prozessor unmittelbar 220 = 1 MByte adressieren, wenn auch das zuständige Register in der CPU 20 Bit breit wäre. In dem Register muss sich die Adresse befinden, bevor sie auf den Adressbus geht. Mit ihren 16-Bit-Registern kann die 8086/88-CPU jedoch höchstens 216 = 65.536 Byte direkt adressieren.
Um mit dieser CPU ein Megabyte Speicher anzusprechen, benötigt man zwei Register. Mit zwei 16-Bit-Registern können wir 232 = 4 Gigabyte Arbeitsspeicher ansprechen - mehr als wir benötigen. Eine 20-Bit-Adresse könnte mit Hilfe der zwei Register gebildet werden, indem man auf das erste Register die ersten 16 Bit der Adresse legt und auf das zweite die restlichen 4 Bit. Ein Vorteil dieser Methode ist, dass der gesamte Adressraum linear angesprochen werden kann. „Linear“ bedeutet, dass die logische Adresse – also die, die sich in den Registern befindet – mit der physikalischen Adresse der Speicherzelle im Arbeitsspeicher übereinstimmt. Das erspart uns die Umrechnung zwischen logischer und physikalischer Adresse.
Das Verfahren hat allerdings auch Nachteile: Die CPU muss beim Adressieren immer zwei Register laden. Zudem bleiben zwölf der sechzehn Bits im zweiten Register ungenutzt.
Deshalb ist im Real Mode ein anderes Adressierungsverfahren üblich: Mit einem 16-Bit-Register können wir 65.536 Byte an einem Stück ansprechen. Bleiben wir innerhalb dieser Grenze, brauchen wir für die Adressierung einer Speicherzelle kein zweites Register.
Um den ganzen Speicherbereich von 1 MByte zu nutzen, können wir mehrere 65.536-Byte-Blöcke über den gesamten Speicher von 1 MByte verteilen und jeden von ihnen mit dem zweiten Register adressieren. So ein Block heißt Segment. Das zweite Register legt fest, an welcher Speicheradresse ein Segment anfängt, und mit dem ersten Register bewegen wir uns innerhalb des Segments.
Der Speicher von 1 MByte, der mit den 20 Leitungen des Adressbusses adressiert werden kann, ist allerdings nicht in ein paar säuberlich getrennte Segmente unterteilt. Vielmehr überlappen sich eine Menge Segmente, weil jedes 16. Byte, verteilt über 1 MByte, als möglicher Beginn eines 64-KByte-Segments festgelegt wurde. Der Abstand von 16 Byte kommt zu Stande, weil die erlaubten Anfänge der Segmente gleichmäßig auf die 1 MByte verteilt wurden (Division 1.048.576 Byte durch 65.536).
Die Überlappung der Segmente ist eine Stolperfalle für Programmierer: Ein und dieselbe Speicherzelle im Arbeitsspeicher kann durch viele Kombinationen von Adressen des ersten und des zweiten Registers adressiert werden. Passt man nicht auf, dann überschreibt ein Programm unerlaubt Daten und Programmcode und stürzt ab.
Die Adresse im zweiten Register, die den Anfang eines Segments darstellt, heißt Segmentadresse. Die physikalische Adresse im Arbeitsspeicher errechnet sich aus der Segmentadresse und der Adresse des ersten Registers, der so genannten Offsetadresse. Erst wird die Segmentadresse mit 16, dem Abstand der Segmente, multipliziert. Zur Multiplikation mit 16 hängt man rechts einfach vier Nullen an. Hinzu wird die Offsetadresse addiert:
Physikalische Adresse = Segmentadresse * 16 + Offsetadresse
Die Offsetadresse im ersten Register erlaubt, auf ein 65.536 Byte großes zusammenhängendes Speicherfeld zuzugreifen. In Verbindung mit der Segmentadresse im zweiten Register kann man den gesamten Speicher erreichen. Beschränkt sich ein Programm auf die Speicherplätze innerhalb eines Segments, braucht die Segmentadresse gar nicht geändert werden.
Sehen wir uns dazu ein Beispiel an: Nehmen wir an, dass das Betriebssystem ein Programm lädt, das 40 KByte Daten und 50 KByte Code umfasst. Daten und Code wandern in den Arbeitsspeicher, wo sie säuberlich getrennt eigene Speicherbereiche einnehmen sollen. Um separate Speicherbereiche für die Daten und den Code zu schaffen, initialisiert das Betriebssystem zuvor das Datensegmentregister (DS) und das Codesegmentregister (CS) mit den Adressen, an denen beginnend die Daten und der Code im Arbeitsspeicher gelagert werden.
Wollen wir beispielsweise Text ausgeben, müssen wir eine Betriebssystemfunktion aufrufen, der wir die Adresse des Textes übergeben, den sie ausgeben soll. Die Segmentadresse des Textes steht schon im Datensegmentregister. Wir müssen der Betriebssystemfunktion nur noch die Offsetadresse nennen. Man sieht: Das Betriebssystem erfährt die vollständige Adressinformation, obwohl wir der Funktion nur den Offsetanteil übergeben haben.
Kommt ein kleines Programm mit weniger als 64 kByte inklusive aller Daten und des Codes aus, braucht man das Segmentregister beim Programmstart nur einmal zu initialisieren. Die Daten erreicht man alle allein mit dem Offset – eine ökonomische Sache.
Fassen wir die Eigenschaften von Segmenten und Offsets zusammen:
- Jedes Segment fasst maximal 64 kByte, weil die Register der CPU nur 16 Bit breit sind.
- Speicheradressen innerhalb eines Segments erreicht man mit Hilfe des Offsets.
- Es gibt 65.536 Segmente (216).
- Die Segmentanfänge liegen immer mindestens 16 Byte weit auseinander.
- Segmente können sich gegenseitig überlappen.
- Die Adressierung mit Segmenten und Offset erfolgt nur im Real Mode so wie beschrieben.
Neuere Prozessoren und Betriebssysteme besitzen die genannte Adressregister-Beschränkung nicht mehr. Ab dem 80386 können bis zu 4 GB linear adressiert werden. Deshalb setzen wir uns nicht weiter im Detail mit Segmenten und Offset auseinander. Die Programme jedoch, die wir im Folgenden entwickeln, sind dennoch höchstens 64 kByte groß und kommen deshalb mit einem Segment aus.