Pentium II/III, Befehlstypen, Datentypen und Adressierungsarten

Einführung

Um eine Kompatibilität bei Hard- und Software zu gewährleisten sollte bei der Entwicklung eines neuen Prozessors, der Befehlsatz der vorherigen übernommen werden. Die Einführung neuer Befehle gestaltet sich dann für alte Programme unkritisch. Die Prozessoren der Intel-x86-Familie sind aufwärtskompatibel. Ein Programm, das auf einem 8086er lauffähig ist, funktioniert auch auf einen Pentium II/III.
Die Implementierung von Befehlen in Hardware oder Mikroprogramm ist abhängig von den Herstellungskosten und der Leistung, bzw. Nützlichkeit der Befehle. Ein oft genutzter Befehl bringt bei festverdrahteter Logik mehr Leistung, erhöht aber die Herstellungskosten.
Ein grosser Befehlssatz ermöglicht eine leichte Programmierung des Prozessors, kann aber zu komplizierten Kontrollstrukturen durch eine hohe Komplexität der Schaltkreise führen. Ein komplexer Befehl kann bei der Abarbeitung durch den Prozessor mehr Zyklen benötigen, als eine programmierte Folge von einfachen Befehlen.
Befehle können zu Klassen gruppiert werden:

Erläuterungen zu einigen Befehlstypen

Internes Befehlsformat

Das Befehlsformat der Pentium II/III – Prozessoren ist als die unterste Stufe der Befehlsdarstellung anzusehen, da dies die Sprache ist, die die CPU direkt steuert. Dargestellt wird sie logischerweise im binären Format, mit Nullen und Einsen. Obwohl die Leistung der CPU‘s stetig steigt, ist die Kodierung der Befehle in der Intel Architektur seit dem 8086 unverändert geblieben.
Im vorangegangenen Abschnitt wurde ein kleiner Einblick in die Vielfalt der Befehlstypen gewährt und es wurde deutlich, dass jeder Befehl ein für ihn typisches „Aussehen“ hat. So gibt es z.B. Befehle, die Operanden oder Adressen zur Ausführung benötigen. Andere kommen gänzlich ohne weitere Parameter aus. Da manche Befehle unterschiedliche Operationen ausführen können, ist es notwendig, dem Prozessor mitzuteilen, ob nun z.B. ein Register oder eine Speicherzelle adressiert wird, somit ist es zwingend erforderlich, dem Befehl selbst noch Angaben über Operandenadressen und dergleichen mitzuteilen. Im Instruction Format sind der Befehl und die notwendigen Parameter kodiert.

Anhand der Abbildung soll nun die Kodierung verdeutlicht werden. Einem Befehl können bis zu vier ein Byte lange Präfixe vorangestellt sein. Sie sind optional und müssen, falls erwünscht, direkt auf Programmebene angegeben werden. Mit Hilfe dieser Präfixe lassen sich einige Funktionen sehr viel einfacher verwirklichen. Als Beispiel sei hier die Ergänzung „REP“ vorgestellt. Es kann nur in Verbindung mit Stringinstruktionen angewandt werden, und bewirkt eine Wiederholung des folgenden Befehls, so lange wie das ECX-Register, welches bei jeder Ausführung dekrementiert wird, den Wert ungleich Null hat.
Insgesamt gibt es vier verschiedene Gruppen von Präfixen. Diese seinen nur am Rande erwähnt, ohne auf diese näher einzugehen:

Für jede Gruppe gibt es genau ein Byte, somit können manchen Befehle mehrere Präfixe zugeordnet werden, wobei diese niemals aus der gleichen Gruppe kommen können und die Stimmigkeit der damit entstehenden Aussage gegeben sein muss.
Die nächsten ein bzw. zwei Bytes der Instruktion beinhalten den eigentlichen Opcode (Operation Code). In seltenen Fällen reicht der Platz von 16 Bit nicht aus. Dann werden 3 weitere Bits des folgenden Bytes zur Darstellung hinzugezogen.
Seine Länge hängt im wesentlichen von der Befehlsstruktur ab, also ob noch Operanden, Adressen, Register oder ähnliches in der Instruktion folgen werden. Der Prozessor kann erst nach der Dekodierung dieser Bytes erkennen wie die folgenden Bytes zu interpretieren sind, und wo der Befehl endet bzw. der nächste beginnt.
Verlangt ein Befehl die Angabe einer Adresse oder eines Registers, wird dies im dritten möglichen Byte abgelegt. Das ModR/M-Byte ist dreigeteilt in Mod, Reg/Opcode und R/M. Wie schon erwähnt, sind die Bits 3 bis 5 (Reg/Opcode) für die zusätzliche Opcode-Kodierung reserviert. Wird Mod mit R/M kombiniert, stehen zusammen 5 Bits (Bit 0-2 und Bit 6/7) zur Verfügung. Es können in diesem Fall 32 verschiedene Zustände kodiert werden. Sie stehen für die 8 Register und 24 mögliche Adressierungsmodi. Wird das R/M-Feld allein genutzt, spezifiziert es ein Register als Operanden.

Einige Kodierungen des ModR/M-Bytes verlangen zusätzlich das SIB-Feld. Es ist dann notwendig, wenn im ModR/M-Byte ein Adressierungsmodus gewählt wurde. SIB steht für Scale Base Index und ermöglicht die Angabe notwendiger Parameter, die für die erweiterte Adressierung von Bedeutung sind. Unterteilt ist das SIB in das zwei Bit lange Scale-Feld, das Index- und das Base-Feld, beide jeweils drei Bit gross. Auf die speziellen Adressierungsarten, die hiermit ermöglicht werden, soll später im Abschnitt Operandenadressierung näher eingegangen werden.
Moderne Betriebssysteme teilen den logischen Adressraum in Segmente auf, um dann einen virtuellen Speicher zu realisieren. Da die Abbildung des logischen Speichers auf den physikalischen nicht übereinstimmt, zeigen feste Adressangaben nie auf die gewünschte Stelle. Um dies zu umgehen, kann noch ein Offset mit angegeben werden. Dieses Offset, auch Addressdisplacement, gibt die Adresse innerhalb des Segmentes an. Bis zu vier Byte lang kann die Angabe des Displacement werden.
Handelt es sich um Befehle, die vom Programm vorgegebene absolute Werte behandeln, werden diese in Form der Immediate Data in dem letzten bis zu 4 Byte grossen Feld abgelegt.

Obenstehende Abbildung zeigt noch weitere Eigenschaften, die die Funktion der Befehle beeinflussen können. Werden sie von einem Befehl unterstützt, sind sie zusammen mit dem eigentlichen Befehl im bis zu 19 Bit langen Opcode-Feld kodiert.

Datentypen

In der INTEL-Architektur gibt es fundamentale Datentypen mit denen sich jede noch so komplexe Datenstruktur darstellen lässt.
Zu den fundamentale Datentypen gehören das Byte. Auf diesem basieren sämtliche Typen, es ist somit auch die kleinste adressierbare Einheit. Kombiniert man in Form von Verdopplung spricht man von Wort, Doublewort und Quadwort. Das Byte sind acht aufeinanderfolgenden Bits und kann an jeder logischen Adresse stehen. Die Bits sind durchnumeriert von 0 bis 7. Das Bit 0 ist das LSB (niederwertigste Bit).
Ein Wort besteht aus zwei Bytes (16 Bits), ein Doublewort aus vier Bytes (32 Bits) und ein Quadwort aus acht Bytes (64 Bits). Jedem Byte in einem Wort ist eine eigene Adresse zugeordnet. Das Byte mit dem Bit 0 ist das low Byte und dessen Adresse die Anfangsadresse des Wortes im Speicher. Erwähnenswert ist noch ein im Pentium II/III-Prozessor neuer Floatingpoint Datentyp, er ist aus vier Doublewords zusammengesetzt und erreicht somit eine Grösse von immerhin 128 Bit.

Anordnung im Speicher

Die Lage eines Wortes (Doublewort / Quadwort) im Speicher ist nicht gebunden an eine gerade oder ungerade Adresse, dadurch kann durch „geschickte“ Anordnung der Speicherplatz optimal ausgenutzt werden.

Um jedoch den Speicherzugriff zu optimieren, sollten word Operanden an durch vier Byte teilbaren ganzzahligen Adressen beginnen. Doubleword Operanden an 8 Byte teilbaren Adressen, da dann bei einem 32-Bit-Bus-Zugriff nur ein Lese-Zyklus benötigt wird. Befinden sich Teile des Operanden ausserhalb der gelesenen 32-Bit, ist ein weiterer Speicherzugriff nötig.

Numerische Typen, Pointer, Bitfelder und Strings

Operandenadressierung

Im Instruction Set sind viele Befehle vorhanden, die mit Operanden arbeiten. Dem Prozessor muss nun mitgeteilt werden, wo er die Operanden finden kann. Es gibt nun mehrere Möglichkeiten, zum einen kann er als Wert direkt im Befehl mit kodiert sein, oder der Befehl verweist auf ein Register, in dem die für die Operation notwendige Information abgelegt ist. Im Abschnitt Instruction Format, wurde erklärt, wie der Befehl kodiert sein muss, damit er seinen Operanden findet. Hier sollen nun die unterschiedlichen Adressierungsarten dargelegt werden.

Die zweite Gruppe bilden die Segmentregister. Das Memory Mangement sieht Segmente vor, in denen Daten und Programmteile stehen. Insgesamt sechs Segmente können mit Hilfe der Register verwaltet werden. In der Abbildung sind die sechs Register und ihre Funktionen zusammengefasst. Jedes Register beinhaltet einen Pointer, der sich auf jeweils ein Segment bezieht. DS, ES, FS und GS haben praktisch die gleiche Funktion, in ihnen werden Pointer auf Datensegmente gespeichert. In SS liegt die Adresse des Stacks und in CS die des Segmentes in dem der Programmcode liegt.
Die letzte Gruppe bilden die Status- und Kontrollregister. Sie geben Auskunft über den aktuellen Zustand der Prozessors und ermöglichen die Steuerung bestimmter Funktionen. Sobald z.B. bei einer mathematischen Operation das Ergebnis Null ist, wird das Zero Flag (ZF) gesetzt. Kommt es bei einer Addition zu einem Übertrag, wird das Carry Flag (CF) aktiviert. Desweiteren können hier auch die Interrupts gesperrt werden.

Nach dieser kurzen Übersicht soll nun die eigentliche Registeradressierung beschrieben werden.
Man unterscheidet zwischen expliziter und impliziter Registeradressierung. Im Fall Register werden sowohl für die einzelnen Operanden als auch für das Ergebnis Register angegeben. Ist ein Befehl implizit, ist die Angabe bestimmter Register, meistens für das Ergebnis nur indirekt möglich. Das folgende Beispiel verdeutlicht das ganze.

In der Regel sind die Operanden dynamische Werte und können somit nicht im Quellcode implementiert sein. Statt dessen werden sie in den Standardregistern (32 Bit: EAX, EBX, ... 16 Bit: AL, AH, BL, ...) abgelegt. Genauso ist es mit den Segmentregistern, sie erhalten bei jedem Prozesswechsel des Prozessors die aktuellen Parameter der zu bearbeitenden Segmente.
Da es im Grunde genommen (fast) egal ist, welches Register zum Zwischenspeichern von Operanden verwendet wird, ist es notwendig dem Befehl das entsprechende Register mitzuteilen. Der MOV-Befehl z.B. ist nicht an ein einziges Register gebunden, in das er den Operanden schreiben soll. Mit dem Lesen verhält es sich genauso, Quelle und/oder Ziel kann fast jedes Register sein. Wobei selbstverständlich auch hier Einschränkungen vorhanden sind. Es wäre fatal, wenn man mit MOV Daten in das Statusregister schieben könnte. Um einige Registerinhalte zu verändern gibt es spezielle Befehle, die unter anderem unter Bitmanipulation.
Besonders die arithmetischen Operationen verlangen grosse Datenworte um eine möglichst hohe Genauigkeit zu erreichen. Leider sind die Register maximal 32 Bit lang. Will man nun aber eine Division (DIV) oder Multiplikation (MUL) mit quadwords (64 Bit) durchführen, muss man sich eines Tricks behelfen. Es gibt die Möglichkeit, Register zu paaren. Aus EAX und EDX wird durch folgende Anweisung EDX:EAX ist 64 Bit-Register. Die Loworderbits werden in EAX gespeichert und die Highorderbits in EDX. Eine solch geartete Operation, sollte dann so aussehen: MUL EDX:EAX, 5. Hier wird dann der 64 Bit-Wert mit 5 multipliziert, das Ergebnis liegt dann, wie gewohnt, in EDX und EAX.

Memory Operand

Im vorherigen Abschnitt wurden Register adressiert, geht man einen Schritt weiter, kommt die Speicheradressierung. Sie unterscheidet sich zwar wesentlich vom vorherigen, macht aber einen ähnlichen Ansatz. Die Operanden sind wieder dynamisch und ändern sich zur Laufzeit. Mit dem grossen Unterschied, dass diese nicht in Registern, sondern im Hauptspeicher abgelegt werden.
Einen grossen Nachteil hat dies natürlich. Die Zugriffszeit auf den Hauptspeicher ist im Vergleich zum Register ein Vielfaches höher. Doch leider sind diese äusserst schnellen Register nur begrenzt verfügbar. Verständlicherweise haben die Register auch eine ganz andere Aufgabe. Sie dienen dem schnellen Zugriff auf Operanden und Daten, die für die aktuelle Operation notwendig sind. Würde man aufwendige Berechnungen durchführen, und immer wiederkehrende Operanden oder Schleifenzähler jedesmal aus dem Ramspeicher holen, würde dies auf Kosten der Performance gehen. Interessant wird der Hauptspeicher, wenn zum einen die Daten vorübergehend nicht mehr gebraucht werden und Platz für neue Befehlsausführungen benötigt wird und zum anderen, wenn mit grossen Datenmengen gearbeitet wird. Das Stringformat lässt Datengrössen von gigantischen 4GB (!!!) zu. Natürlich hat man selten so grosse einzelne Strings, aber selbst wenige Bytes lassen sich schon nicht mehr als Ganzes in den Registern ablegen oder bearbeiten.
Um das im Ram abgelegte Datum wiederzufinden, wird die Adresse, an der es sich befindet, in einem Register gespeichert. Leicht nachvollziehbar ist dies noch bei einzelnen Wörtern, handelt es sich allerdings um Arrays oder gar um Stringfolgen, müssen andere Strategien angewandt werden.
Im folgenden Bild ist die Vorschrift zur Berechnung von Speicheradressen gegeben. Natürlich ist nicht jede Methode der Adressierung so umfangreich.

Die errechnete Offsetadresse wird auch als effektive Adresse bezeichnet. Das Displacement ist ein optionaler fester vom Befehl gegebener Wert und liegt zwischen 8 und 32 Bit. Base und Index stehen für einen Wert in den Standardregistern, mit der Ausnahme, dass ESP nur als Basisregister angegeben werden darf. Scale ist ein Skallierungsfaktor, der Werte zwischen eins und vier annimmt.

I/O Operand

Heutige Computersysteme zeichnen sich häufig ausserhalb des Prozessors aus (Sound, Video, ...). Da dies externe Geräte sind, müssen sie über den Bus angesprochen werden. Hierfür stehen spezielle Befehle zur Verfügung.
Es gibt zwei Möglichkeiten, die Ein-/Ausgabeports zu adressieren: