x86-Programmierung und -Betriebsarten (Teil 1)

09.01.2004 von Hans-Peter Messmer und Klaus Dembowski
Die sechsteilige Artikelserie behandelt die Speicheradressierung und die x86-Betriebsarten. Dieser Teil widmet sich dem Code- und Datensegment sowie dem Befehlszähler und der Verwendung des Stacks.

In der logischen Adressierung des Speichers bei x86-Prozessoren findet sich auch der Schlüssel für die mitunter recht eigentümlich anmutende Speicherverwaltung, die mit den Prozessoren für PCs einhergeht, was oft zu Missverständnissen und Fehlinterpretationen führt. Demnach ist eine eingehende Untersuchung der (segmentierten) Speicherorganisation und der beteiligten Register notwendig, was durch entsprechende Programmierbeispiele verdeutlicht wird.

Außerdem unterscheidet sich die Speicheradressierung abhängig vom aktiven Betriebsmodus, also je nachdem, ob die CPU (ab 80386) im Real, Protected oder Virtual 8086 Modus arbeitet und ob die Paging Unit aktiv ist oder nicht.

Zur Erklärung der Funktionen muss man keinen der aktuellen hochkomplexen Prozessoren als Referenz heranzuziehen. Die Artikelreihe verwendet deshalb als "einfache" und überschaubare Beispiel-CPU den 80386. Er war der erste 32-Bit-Prozessor in PCs. Seine grundlegende Architektur wurde mit den Nachfolgemodellen (486 bis Pentium III) weitergeführt und jeweils um einige On-Chip-Schaltungselemente (mathematischer Coprozessor, Cache) erweitert. Der 80386 ist zum Verständnis der logischen Adressierung und der Speicherverwaltung von x86-CPUs daher bestens geeignet, das er vergleichsweise einfach zu verstehen ist und die Erkenntnisse trotzdem noch aktuell sind.

Die Artikelserie basiert auf dem Kapitel 6 des "PC Hardwarebuch" von Addison Wesley. Sie können in unserem Buch-Shop das über 1200 Seiten starke Kompendium bestellen oder als eBook downloaden.

Serie: x86-Programmierung und -Betriebsarten

Teil 1

Code-/Datensegment, Befehlszähler und Stack

Teil 2

Adressierungsarten und Befehlsdekodierung sowie Real Mode

Teil 3

Interrupts und Exceptions

Teil 4

Der Protected Mode

Teil 5

Paging und die MMU

Teil 6

Der Virtual-8086-Modus

Codesegment und Befehlszähler

Im Kapitel 5 "Einstieg in die 32-Bit-Welt - Der 80386" des "PC Hardwarebuch" ist bereits erwähnt, dass der Prozessor zur Programmausführung Befehle aus dem Speicher holt (auch als Befehls-Fetching bezeichnet) und ausführt. Grundlage für dieses automatische Lesen bilden Codesegment und Befehlszähler. Das Codesegment gibt dabei (natürlich) das Segment an, aus dem der nächste Befehl gelesen werden soll. Der Befehlszähler ist der Offset des nächsten zu lesenden Befehls. Die Bezeichnung Befehlszähler hat historische Gründe. Das Paar Codesegment:Befehlszähler bildet somit die Adresse des nächsten auszuführenden Befehls im Speicher. Der Prozessor kann damit diesen Befehl einlesen und ausführen. Am Code des Befehls erkennt der Prozessor, wie viele Bytes er einlesen muss, damit sich der vollständige Befehl im Prozessor befindet. Befehle für den 80x86 sind zwischen einem und 15 Byte lang.

Ist der Befehl ausgeführt worden, so wird der Befehlszähler um die Zahl der Bytes inkrementiert, die der gerade ausgeführte Befehl aufwies. Bei einem kurzen 2-Byte-Befehl wird der Befehlszähler also um zwei erhöht, das Paar Codesegment:Befehlszähler verweist dann auf den nächsten auszuführenden Befehl. Dieser wird in gleicher Weise eingelesen und ausgeführt. Anschließend inkrementiert der Prozessor seinen Befehlszähler erneut. Dieses Einlesen und Inkrementieren führt der Mikroprozessor dabei völlig selbstständig aus, es ist kein Eingriff eines Steuerprogramms oder gar des Benutzers notwendig. Einmal "angestoßen", fährt die CPU damit ständig fort, Befehle einzulesen und auszuführen.

Für das Befehls-Fetching spielen Prefetch-Queue und Busschnittstelle eine wichtige Rolle. Befehle werden zuerst in die Prefetch-Queue eingelesen, die diese dann an die Befehlseinheit weitergibt. Sind in der Prefetch-Queue durch die ständige Weitergabe der Befehlsbytes an die Steuereinheit so viele Bytes frei geworden, wie der Breite des Prozessordatenbusses entspricht, dann liest die Busschnittstelle entsprechend viele Bytes aus dem Speicher in die Prefetch-Queue ein. Der 80386 beispielsweise besitzt einen Datenbus, der vier Byte (32 Bit) breit ist. Er liest also immer dann selbstständig vier Bytes ein, wenn in der Prefetch-Queue vier Byte frei sind. Dadurch nutzt der Prozessor den Datenbus in optimaler Weise: Wenn der 80386 gerade einen Befehl ausführt, der keinen unmittelbaren Speicherzugriff erfordert (z.B. die Addition zweier Register), kann die Busschnittstelle die Prefetch-Queue nachladen, ohne dass die Abarbeitung des gerade aktiven Befehls behindert wird. Damit steht sofort nach der Abarbeitung des gerade aktiven Befehls bereits der nächste Befehl zur Verfügung und muss nicht erst aus dem Speicher gelesen werden.

Speicherzugriff und Befehlsausführung

Selbstverständlich hat ein Speicherzugriff durch einen aktiven Befehl - beispielsweise als Folge der Anweisung

MOV register, speicher

Vorrang vor dem Nachladen der Prefetch-Queue. Parallel zur Ausführung des gerade aktiven Befehls und dem Nachladen der Prefetch-Queue dekodiert die Befehlseinheit den nächsten Befehl und bereitet ihn für die EU (Execution Unit, Ausführungseinheit) zur Ausführung vor. Die Ausführungseinheit führt den Befehl nun in einer bestimmten Zahl von Taktzyklen aus. Einfache Befehle wie CLC benötigen hierzu zwei Taktzyklen, umfangreichere wie ein Task Switch über ein Gate im Protected Mode dagegen bis über 300 Taktzyklen. Man erhält somit einen gleichmäßigen Befehlsstrom vom Speicher zum Prozessor. Anders ausgedrückt wird ein Programm Schritt für Schritt sequenziell ausgeführt. Ab dem Pentium wird allerdings versucht möglichst viele, aber voneinander unabhängige Schritte parallel auszuführen, um die Leistung des Computers zu steigern. Der Pentium z.B. weist hierfür drei unabhängige Pipelines auf, die parallel arbeiten können.

Der gleichmäßige Befehlsstrom kann durch bedingte und unbedingte Sprünge und Verzweigungen unterbrochen und an einer anderen Stelle fortgesetzt werden. Hierzu muss nur der Wert des Befehlszählers und ggf. des Codesegments verändert werden. Bei einem Near-Aufruf oder -Sprung bleibt das Codesegment unverändert, es wird nur der Wert des EIPs neu geladen. Demgegenüber wird bei einem Far-Aufruf oder Sprung auch der Wert des Codesegments verändert und ein so genannter Intersegmentaufruf oder -sprung ausgeführt. Der Prozessor fährt an einer anderen Stelle des im Speicher befindlichen Programms fort. Durch einen solchen Sprung wird außerdem die Prefetch-Queue vollständig geleert, da die im Vorgriff eingelesenen Bytes nicht dem neu angesprungenen Befehl entsprechen. Sprünge, Aufrufe - allgemeiner auch als Verzweigungen oder Branches bezeichnet - sind für den logischen Aufbau von Programmen sehr wichtig, weil ein Computer häufig in Abhängigkeit von bestimmten Bedingungen verschiedene Dinge ausführen soll.

Beispiele

Beispiel: Der Wert des Codesegments CS lautet 24D5, der Wert des Befehlszählers 0108. Der nächste Befehl befindet sich damit bei der Adresse 24D5:0108. Der Code an dieser Adresse lautet 8CC0. Die Steuereinheit CU dekodiert diesen Code und ermittelt den Befehl

MOV EAX, ES

Es soll also der Wert des Extrasegments ES in das 32-Bit-Akkumulatorregister EAX übertragen werden. Nach der Ausführung des Befehls wird der Wert des Befehlszählers um zwei erhöht, da MOV EAX, ES ein Zwei-Byte-Befehl war. Der Wert von EIP lautet somit 10a0, der Wert des Codesegments CS bleibt unverändert.

Beispiel: Der Wert des Codesegments CS lautet 80B8, der Wert des Befehlszählers 019D. Der nächste Befehl befindet sich damit bei der Adresse 80B8:019D. Der Code an dieser Adresse lautet 7506. Die Steuereinheit CU dekodiert diesen Code und ermittelt den Befehl

JNZ 01A5

Es handelt sich also um eine bedingte Verzweigung (Jump if Not Zero) zum Befehl bei der Adresse 01A5. Das Codesegment wird dabei auf keinen Fall verändert. Für solche bedingten Verzweigungen ist das Flagregister von großer Bedeutung. Einer bedingten Verzweigung geht üblicherweise ein Vergleich zwischen zwei Werten voraus. Entsprechend dem Vergleichsergebnis werden bestimmte Flags im Flagregister gesetzt oder gelöscht. Hat der Vergleich zu einem "ungleich Null" geführt, so wird verzweigt: Der Befehlszähler EIP wird mit dem Wert 01A5 geladen. Somit setzt der Prozessor die Arbeit bei 80B8:01A5 fort. Führte der Vergleich zu einem "gleich Null", so findet keine Verzweigung statt, der Befehlsstrom wird gleichmäßig fortgesetzt. Hierzu wird EIP um zwei auf 019F erhöht, da es sich um einen Zwei-Byte-Befehl handelte.

Limitierungen

Mit dem 32-Bit-Befehlszähler EIP wären Programme von höchstens 4 GByte Größe möglich, beim 16-Bit-Vorgänger 80286 durch die 16-Bit-Offsetregister darüber hinaus nur Programme mit 64 KByte Größe. Durch Ändern des Codesegments mit einem gesonderten Befehl (Laden von CS mit einem bestimmten Wert) werden umfangreichere Programme möglich. Beachten Sie, dass zwar das Codesegment CS, nicht aber der Befehlszähler EIP explizit verändert werden kann. Es gibt keinen direkten Ladebefehl mit EIP als Ziel. Der Wert des Befehlszählers EIP kann nur implizit über einen bedingten oder unbedingten Sprung sowie einen Call zu einem Unterprogramm verändert werden. FAR-Calls und FAR-Sprünge beeinflussen zusätzlich auch den Wert im Codesegmentregister CS.

Bereits der 80386-Befehlssatz ist mit 206 Befehlen und zusätzlich vielen Befehlsvariationen hinsichtlich Registerbreite und Adressierungsart sehr umfangreich. Es ist daher unmittelbar ersichtlich, dass eine eingehende Erläuterung aller möglichen Befehle den Rahmen dieser Artikelreihe sprengen würden. Bei der Behandlung der folgenden Mikroprozessoren wird im Verlauf der Serie jedoch auf besondere Befehle noch im Zusammenhang eingegangen.

Stacksegment und Stack-Zeiger

Eine besondere Bedeutung besitzt das Stacksegment sowie der zugehörige Stack-Pointer oder Stapelzeiger. Jedes Programm besitzt normalerweise ein eigenes Stacksegment, auf dem mit PUSH der Wert eines Registers oder eines Speicherwortes abgelegt werden kann. Mit PUSHF können die Flags und ab dem 80186 mit PUSHA alle Vielzweckregister auf dem Stack gespeichert werden. Umgekehrt werden mit POP, POPF bzw. POPA (ab 80186) die entsprechenden Daten vom Stack wieder abgenommen.

Beachten Sie, dass der Stack nach unten "wächst", d.h. zu kleineren Werten des Stapelzeigers SP (siehe Abbildung). Werden Daten auf dem Stack abgelegt, so vermindert sich der Wert von ESP um vier, weil immer ein ganzes Doppelwort (vier Byte) auf den Stack geschoben wird. Wenn Sie einen 80386 im 16-Bit-Modus betreiben, so werden natürlich stets nur zwei Byte auf dem Stack abgelegt und der Wert von SP mit jedem PUSH nur um zwei vermindert. Das gilt natürlich auch für die 16-Bit-Vorgänger 8086 und 80286. Ist der Stack leer, dann nimmt der Stapelzeiger ESP seinen größten Wert an. Nach dem Ablegen des Wortes zeigt der Stapelzeiger auf das zuletzt abgelegte Wort auf dem Stack. Ein Befehl PUSH vermindert also zuerst den Wert des Stapelzeigers ESP und anschließend wird der Register- oder Speicherwert auf dem Stack abgelegt.

Durch das Wachsen des Stacks nach unten kann ein Stapelüberlauf auf einfache Weise erkannt werden: Nimmt ESP den Wert 0 an, so ist der Stack erschöpft. Bei entsprechend programmierten Anwendungsprogrammen, die den Stack laufend überprüfen, erscheint dann die Mitteilung Stapelüberlauf! oder Stack Overflow!. Programmierer sehen jedoch meist einen ausreichend großen Stack (genügend großer anfänglichen Wert für ESP) vor, sodass ein solcher Stapelüberlauf nur bei einem Programmfehler oder einer fehlerhaften Programmbedienung auftreten sollte. Nachteilig ist, dass im Real Mode das Anwendungsprogramm vor jedem PUSH explizit prüfen muss, ob noch ausreichend Kapazität auf dem Stack frei ist. Im Protected Mode wird die Prüfung, ob ein Stacküberlauf vorliegt, von der Hardware des Prozessors erledigt. Damit ist ein sehr schneller Check möglich, ohne dass zusätzliche Software-Routinen notwendig sind.

Verwendung des Stacks

Der Stack kann als Zwischenspeicher für Daten benutzt werden, die ohne PUSH und POP nur schwierig anzusprechen sind (wie beispielsweise das Trap-Flag). Am häufigsten wird der Stack jedoch zur Übergabe von Parametern an Prozeduren und Subroutinen verwendet. Hierzu schiebt das aufrufende Programm die zu übergebenden Parameter mit einem oder mehreren PUSH-Befehlen auf den Stack. Die aufgerufene Prozedur holt sich diese Parameter über einen oder mehrere entsprechende POP-Befehle oder mittels einer Adressierung über den Basiszeiger EBP. Für die Erstellung dieses "Prozedur-Stacks" ist ab dem 80386 ein eigenes Befehlspaar vorgesehen, nämlich ENTER (Stack-Frame erstellen) und LEAVE (Stack-Frame entfernen). Außerdem legt die aufgerufene Prozedur ihre lokalen Variablen auf dem Stack ab. Je nach Programmiersprache bereinigt bei einer Rückkehr die aufgerufene Prozedur oder das aufrufende Programm den Stack, das heißt stellt den Zustand vor dem Aufruf der Prozedur wieder her. Damit werden spätestens beim nächsten Aufruf einer Prozedur oder Subroutine mit Parameterübergabe die zuvor auf dem Stack abgelegten Parameter und lokalen temporären Variablen zerstört.

Bei der Ausführung des Befehles PUSH SP (das heißt, es wird der Stapelzeiger SP selbst auf den Stack geschoben) zwischen dem 8086/88 und allen anderen 80x86-Prozessoren besteht ein wesentlicher Unterschied:

Datensegment DS und Adressierung

Neben dem Code- und Stacksegmentregister hat auch das Datensegmentregister DS eine besondere Bedeutung. Es ist immer dann wichtig, wenn ein Befehl Daten aus dem Speicher liest oder in ihm abspeichert, d.h. wenn Speicheroperanden betroffen sind. Der Offset des Speicheroperanden wird üblicherweise in einem der Vielzweckregister bereitgehalten und das Paar DS:Offset verweist auf den anzusprechenden Wert. Das Datensegmentregister DS wird standardmäßig als das einem Offset zugeordnete Segmentregister verwendet. Wenn Sie dagegen einen Wert in einem anderen Segment schreiben oder lesen möchten, müssen Sie das Segmentregister DS mit dem Wert des neuen Segments laden oder ein Segmentpräfix benutzen, das das Datenregister DS durch eines der Extrasegmentregister ES bis GS ersetzt. Hierzu erfahren Sie in den nächsten teilen der Artikelserie mehr.

Die Daten des Codesegments sollten dabei nur ausführbar und höchstens noch lesbar, nicht aber überschreibbar sein. Ein Überschreiben von Code führt notwendigerweise zum Absturz eines Programms. Nur-ausführbare Daten sind zwar in die Prefetch-Queue, nicht aber in ein Vielzweck- oder Segmentregister einlesbar. Ein Programm kann sie also nicht im Sinne von Daten verwenden, die bearbeitet werden.

Die Verwendung verschiedener Segmente für Code, Stack und Daten gestattet eine Trennung der verschiedenen Abschnitte eines Programms. Im Protected Mode wird davon intensiv Gebrauch gemacht, um ein versehentliches Überschreiben von Code durch einen Programmfehler (eine häufige Ursache hängender Programme) zu vermeiden. Selbstverständlich können alle Segmentregister denselben Wert aufweisen. In diesem Fall findet keine Trennung von Code, Stack und Daten statt. Die .COM-Programme unter MS-DOS sind in dieser Weise strukturiert. Sie sind Relikte aus den Zeiten von CP/M, einem Betriebssystem für einfachere 8-Bit-Prozessoren. Sie unterstützten keinen in Segmente aufgeteilten Speicher. Segmentregister in .COM-Programmen haben also keine Bedeutung, es wird nur mit den Offsets gearbeitet und alle Segmentregister besitzen denselben Wert. Damit ist der Adressraum für Code und Daten auf zusammen 64 KByte (ein Segment) begrenzt.

Ausblick

Im nächsten Teil widmen wir uns den Adressierungsarten und der Befehlsdekodierung. Aus DOS-Zeiten dürfte außerdem vielen Lesern der Real Mode sowie die High-Memory-Area bekannt sein.

Serie: x86-Programmierung und -Betriebsarten

Teil 1

Code-/Datensegment, Befehlszähler und Stack

Teil 2

Adressierungsarten und Befehlsdekodierung sowie Real Mode

Teil 3

Interrupts und Exceptions

Teil 4

Der Protected Mode

Teil 5

Paging und die MMU

Teil 6

Der Virtual-8086-Modus

Diese Artikelserie basiert auf dem Kapitel 6 des "PC Hardwarebuch" von Addison Wesley. Sie können in unserem Buch-Shop das über 1200 Seiten starke Kompendium bestellen oder als eBook downloaden.

tecCHANNEL Buch-Shop

Weitere Literatur zum Thema Hardware

Titelauswahl

Titel von Pearson Education

Bücher

PDF-Titel (50% billiger als Buch)

Downloads