| Vorheriges Kapitel (Über TASM) | Nächstes Kapitel (Zahlensysteme) |
Der Prozessor besteht u. a. aus einer Reihe von Registern, einem Befehlsdecodierer,
der Arithmetisch-Logischen Einheit (ALU - Arithmetic Logic Unit), dem Taktgeber
sowie dem Microcode.
Während der Programmausführung erreicht den Prozessor ein Strom von
Bitmustern, in dem die einzelnen Maschinenspracheinstruktionen codiert sind.
Zum Befehlsaufbau gibt es noch ein Kapitel. Hier sei nur soviel gesagt, dass
ein Befehl 1 bis 15 Byte lang sein kann. Der Bitstrom wird vom Befehlsdecodierer
entschlüsselt. Die ALU ist für die Durchführung von arithmetischen
und logischen Operationen verantwortlich. Die Prozesse im Prozessor werden
über einen einheitlichen Takt gesteuert. Dieser Takt kommt vom Taktgeber.
Je schneller der Takt, desto schneller die Verarbeitung der Einzelprozesse.
Deswegen war bis vor einiger Zeit eine Leistungssteigerung in aller Regel mit
einer Steigerung des Prozessortaktes verbunden, spätestens seit den Pentium
XEON und den verschiedenen 64-Bit-Prozessoren von Intel und AMD ist diese Aussage
nur noch eingeschränkt gültig.
Im Microcode ist verschlüsselt wie der
Prozessor die Befehle ausführt, es handelt sich sozusagen um die tiefste
Ausführungsschicht vor den Schaltkreisen (ICs). Bei den Prozessoren der
x86-Familie handelt es sich traditionell um CISC-Prozessoren (Complex Instruction
Set Computing/Computer). Das Gegenteil davon sind RISC-Prozessoren (Reduced ISC).
RISC-Prozessoren zeichnen sich durch eine Vielzahl von Mehrzweckregistern und eine
geringe (reduzierte) Zahl verschiedener Instruktionen aus. Jeder Befehl wird durch
die gleiche Anzahl Bytes codiert (beschleunigt die Befehlsdecodierung, höhere
Taktzahl möglich) und
führt nur eine einfache Aufgabe aus (mehrere Befehle für komplexere
Aufgaben notwendig).
Bei CISC-Prozessoren kann ein Befehl sehr komplexe Aufgaben ausführen
(Kopieren eines Speicherblocks), dafür sind die Instruktionen unterschiedlich
lang, was die Decodierzeit erhöht (Ermittlung der Befehlslänge und der
einzelnen Befehlsbestandteile). Die komplexen Befehle werden durch den Microcode
weiter zerlegt, bis sie durch die Schaltkreise im Prozessor ausgeführt werden.
Die Registerzahl ist geringer. In den aktuellsten Pentium-Varianten kann der
Microcode aktualisiert werden. Bisher war für einen anderen Microcode auch
ein anderer Prozessor notwendig. Desweiteren werden mehr und mehr Befehle direkt
in den ICs verdrahtet, eine Zerlegung über Microcode fällt dann weg.
Bei RISC-Prozessoren arbeiten die Prozessorhersteller und Compilerhersteller in der Regel sehr eng zusammen, um das Optimum aus den Prozessoren herauszubekommen. Eine Optimierung von Hand war aufgrund der komplexen Vorgänge im CPU-Innern nicht mehr gangbar. Eine ähnliche Entwicklung gibt es jetzt bei den RISC-Prozessoren auch. Durch mehrere hintereinander geschaltete, in Stufen arbeitende Decodier- und Ausführungseinheiten sowie eingebaute Caching- und Sprungvorhersage- Mechanismen kann es heute passieren, dass durch manuelles Eingreifen ein verschlimmbessertes Ergebnis entsteht. Optimierungen sind immer im Kontext der ausführenden Maschine zu betrachten.
Der Prozessor wird mit den umliegenden Bausteinen (E/A-Geräte, Speicher) durch verschiedene Busse verbunden. Mit dem Adress/Speicherbus wird eine bestimmte Speicherstelle angesprochen. Die Aufgabe der Datenübertragung von bzw. zu dieser Speicherstelle übernimmt der Datenbus. An einem Datentransfer sind also immer Adress- und Datenbus beteiligt. Mit dem Steuerbus werden bestimmte Signale angelegt, die die Kommunikation der mit dem Bus verbundenen Systeme regeln, z. B. Schreib/Lesezugriff.
Der kleinste gemeinsame Nenner bei Prozessoren der x86-Familie ist der 8086. Hierbei handelt es sich bereits um einen 16-Bit-Prozessor, d. h. über den Datenbus können 16 Bit auf einmal in den Prozessor geladen werden. Kurz nach Erscheinen dieses Prozessors gab Intel noch eine Variante für den schmaleren Geldbeutel heraus, den 8088. Dieser verfügte nur über einen 8 Bit breiten Datenbus, d. h. um 16 Bit zu laden, musste dieser Prozessor zwei Speicherzugriffe durchführen, statt nur einen wie beim 8086. Aber das wusste der Prozessor selbst, für die Programme war das transparent. Aus programmiertechnischer Sicht können der 8086 und der 8088 gleich behandelt werden. Alles, was sich im 8086 findet, wurde in den nachfolgenden Prozessoren nicht entfernt, deshalb gelten die folgenden Ausführungen für alle Prozessoren (der x86-Familie).
Der Prozessor enthält sogenannte Register, die man sich als kleine
Speicherstellen vorstellen kann. Der Programmierer kann diese Register über
einen Namen ansprechen, die tatsächliche physische Realisierung ist damit
für den Programmierer ohne Belang, er muß sich also keine Adressen merken.
Ursprünglich hatte jedes Register eine genau zugeteilte Aufgabe, aber in den
späteren Prozessoren verschwand diese strikte Unterteilung immer mehr.
Bis zum 80286 waren die Register maximal 16 Bit breit, d. h. es paßten Werte
mit einer maximalen Größe von 16 Bit in diese Register. Ab dem 80386 wurden
einige Register auf 32 Bit erweitert und seit dem Pentium gibt es auch
64-Bit-Register.
Die Register können grob in allgemeine Register, Segmentregister, Pointer-
bzw. Indexregister und sonstige Register unterteilt werden.
Die allgemeinen Register sind 16 Bit breit, können aber jeweils in zwei 8 Bit breite Register aufgeteilt werden. Das niederwertige Register wird mit dem Buchstaben L, das höherwertige Register mit dem Buchstaben H gekennzeichnet. In Verbindung mit dem Buchstaben A, B, C oder D ergibt sich ein vollständiger Registername. Zum Beispiel wird aus AX (16 Bit breit) das Register AL (8 Bit) und das Register AH (8 Bit). Änderungen an AL oder AH schlagen sich immer auch auf AX nieder.
| Bits 15-8 | Bits 7-0 |
|---|---|
| AH | AL |
| AX | |
| Register | Bedeutung |
|---|---|
| AX | Akkumulator Wird häufig für arithmetische Operationen(Division, Multiplikation)verwendet Der höherwertige Teil AH nimmt häufig die Funktionsnummer bei interruptbasierten Operationen auf (s. erstes Programm). |
| BX | Basis Wird meist bei der Adressierung von Werten im Speicher verwendet |
| CX | Counter Vielfach in Schleifenstrukturen als 'Laufvariable' eingesetzt |
| DX | wird ebenfalls für die Adressierung eingesetzt |
Um diese Register zu erklären, muß etwas weiter ausgeholt werden.
Wie bereits gesagt wurde, verfügt der 8086 über einen 16 Bit breiten Datenbus.
Zusätzlich enthält er auch noch einen Adressbus, den man sich als ein Bündel von
Adressleitungen vorstellen kann. Dieses Bündel umfasst 20 Leitungen.
Adressierbar sind damit also 2^20 Bytes (entspricht 1048576 Bytes oder einem
Megabyte).
Sie haben sicherlich schon gehört, daß MSDOS nur ein Megabyte
Speicher ansprechen kann. Das ist allerdings weniger ein Problem von DOS,
sondern liegt eher in der verzahnten Entstehung von DOS und dem 8086. Aus
Kompatibilitätsgründen wurde aus dieser Begrenzung später auch kein richtiger
Ausweg gesucht.
Um auf eine Adresse im Speicher zugreifen zu können, muß sie in einem Register
hinterlegt sein. Die Register des 8086 waren aber nur maximal 16 Bit breit. Mit
16 Bit lassen sich aber nur 2^16=65536 Bytes (64 KB) ansprechen.
Um diesem Dilemma aus dem Weg zu gehen, entschloß man sich, zwei Register für
die Adressierung zu verwenden. Damit ließen sich dann immerhin 2^32 Bytes
adressieren. Da der Adressraum des 8086 aber nur ein Megabyte (2^20) umfaßte, blieben
von den 32 Bit (4 GB) 12 Bit unbenutzt (32-20=12). Aus diesem Grunde verfiel man
bei Intel auf die Idee der Segmentierung.
Als Voraussetzung für die folgenden Überlegungen gehen wir davon aus, daß ein Segment eine bestimmte Anzahl Bytes enthalten kann und im Prozessor ein Register existiert, das eine Segmentnummer enthalten kann. Dieses Register ist 16 Bit breit.
An welchen Adressen beginnen nun Segmente? Dividieren wir die maximale Größe des
Adressraumes durch die maximal mögliche Anzahl an Segmenten, so erhalten wir
2^20 / 2^16 = 2^4 = 16. Ein Segment ist also ein Block mit einer Größe von 16
Bytes. Ein Segment kann demzufolge an jeder Adresse beginnen, die ohne Rest
durch 16 teilbar ist. In das Segmentregister muß dann die Nummer des Segmentes
eingetragen werden. Dessen Adresse errechnet sich dann so:
Segmentnummer * 16 Bytes.
Bis jetzt können wir nur auf Segmente oder besser Segmentgrenzen zugreifen. Um
den Zugriff auf einzelne Bytes innerhalb der Segmente zu ermöglichen, wird ein
zweites Register verwendet. Dieses enthält einen Zeiger auf ein bestimmtes Byte,
letzlich auch nur eine Zahl. Um also das 4. Byte im 6. Segment ansprechen zu
können, muß ins Segmentregister der Wert 6 und in das zweite Register der Wert 4
eingetragen werden. Eine vollständige Adresse errechnet sich folgendermaßen:
Segmentnummer * 16 + Offset(Abstand) zum Segmentanfang
Die logische Darstellung von Segment und Offset sieht so aus:
Segment:Offset,
also zum Beispiel 00006:00004 (üblicherweise erfolgt die Darstellung in
hexadezimaler Schreibweise)
Mit einem 16-Bit-Register kann man wesentlich mehr als nur
die 16 Bytes bis zur nächsten Segmentgrenze adressieren. Dies bedeutet, daß man
mit diesem Zeigerregister über mehrere Segmentgrenzen hinweg operieren kann. Im
Umkehrschluß bedeutet das auch, daß eine lineare Speicheradresse mit
verschiedenen logischen Adressen ansprechbar ist. So ist die logische Adresse
00006:00004 gleichbedeutend mit der logischen Adresse 00005:00020 (Sie können es
nachrechnen - es ergibt sich die physikalische Adresse 100).
Und weil mit einem Zeigerregister 64 KB adressierbar sind, ist es tatsächlich
möglich, mit einer Segment:Offset-Kombination mehr als ein Megabyte
anzusprechen. Dies ergibt sich aus folgender Rechnung:
max. Anzahl Segmente: 65536
Größe eines Segments: 16 Bytes
max. Größe des Offsets: 65535 Bytes
physikalische Adresse: Segmentnummer * Segmentgröße + Offset
max. ansprechbare Adresse: 65536 * 16 + 65535 = 1114111
Der Überhang über ein MB wird unter DOS als High Memory Area (HMA)
bezeichnet. Dieses Feature ist aber nicht beim 8086 erhältlich, da er nur
über 20 Adressleitungen verfügt, aber 21 Leitungen (Bits) für
diese Adressierung notwendig sind.
Ein Segmentregister ist immer 16 Bit breit und läßt sich im Gegensatz zu den
allgemeinen Register nicht in einen nieder- und einen höherwertigen Teil
zergliedern.
Es gibt die folgenden Segmentregister
| Register | Bedeutung |
|---|---|
| CS | enthält die Nummer des Segmentes, das den aktuellen Code enthält siehe Informationen zum Register IP |
| DS | enthält die Nummer des Segmentes, das die Daten enthält Der Offset kann in verschiedenen Registern stehen oder als Konstante übergeben werden |
| ES | findet vor allem bei den Stringoperationen Verwendung in diesen Fällen steht der Offset im Register DI |
| SS | enthält die Nummer des Segmentes, das den Stack enthält Offsets stehen in den Register BP oder SP Die Bedeutung des Stack wird später geklärt. |
| FS | erst ab dem 80386 Dient vor allem als Zusatzsegmentregister, wird hauptsächlich im Protected Mode verwendet. |
| GS | erst ab dem 80386 Dient vor allem als Zusatzsegmentregister, wird hauptsächlich im Protected Mode verwendet. |
Im Befehlssatz des 8086 gibt es die sogenannten Stringbefehle. Den Begriff
String darf man an dieser Stelle aber nicht mit gleichlautenden Datenstrukturen
aus Hochsprachen verwechseln. Ein String ist für den Prozessor eine
Aneinanderreihung von Daten eines bestimmten Typs (Byte, Word). Dazu sei gesagt,
daß der Prozessor Zeichen (Char) als Bytes behandelt. Der Prozessor sieht
lediglich den ASCII-Code (oder eine andere numerische Codierung), die
Interpretation als Zeichen muß der Programmierer vornehmen. Aufgrund der
prozessorseitigen Interpretation als Zahl ist es sehr einfach, mit Buchstaben zu
rechnen.
Die Stringbefehle umfassen das Kopieren aus und Schreiben in einen String,
kopieren eines ganzen Strings oder Teile davon, den Stringvergleich und das
Suchen in Strings.
Die Indexregister übernehmen zusammen mit einem vom jeweiligen Stringbefehl
abhängigen Segmentregister die Aufgabe, auf die nächste zu bearbeitende
Adresse im String zu zeigen.
Wie die Segmentregister sind die Indexregister 16 Bit breit und nicht
teilbar.
Es gibt die folgenden Indexregister
| Register | Bedeutung |
|---|---|
| DI | Destination Index Kann im Grunde frei verwendet werden, dient aber bei den Stringbefehlen in Verbindung mit dem Register ES als genaue Adressangabe. |
| SI | Source Index Kann im Grunde frei verwendet werden, dient aber bei den Stringbefehlen in Verbindung mit dem Register DS als genaue Adressangabe. |
Da diese Register in engem Zusammenhang zum sogenannten Stack stehen, wird hier zuerst geklärt, was ein Stack denn überhaupt ist.
Stack bedeutet übersetzt soviel wie Stapel und übertragen auf einen Stapel Teller handelt es sich um einen sogenannten LIFO-Speicher. LIFO steht für Last In First Out. Dessen Gegenteil ist der FIFO-Speicher (First In First Out). In der Praxis bedeutet LIFO, daß das letzte Element, das auf den Stapel gelegt wird, auch als erstes wieder vom Stapel entfernt wird. Genau wie der Tellerstapel: der letzte Teller, der oben aufgelegt wird, ist auch der erste Teller, der wieder entnommen wird.
Im Umfeld eines Prozessors dient der Stack hauptsächlich als Zwischenspeicher für Adressen, wenn es um Unterprogramme geht, dient er auch als Medium, um Parameter zu halten.
Wozu benötigt man den Stack jetzt konkret? Ganz einfach. Wenn der Prozessor ein Unterprogramm anspringt, dann muß er sich irgendwo merken, an welcher Adresse im Codesegment dieses Unterprogramm aufgerufen wurde, da nach Beendigung des Unterprogramms das aufrufende Programm hinter dieser Stelle fortfahren muß. Folgender Pseudocode soll zur Erklärung beitragen:
Anweisung1 Anweisung2 Anweisung3 Unterprogrammaufruf Anweisung5 . . . AnweisungN |
Der Prozessor muß sich also die Rücksprungadresse merken, damit er ordnungsgemäß
mit Anweisung5 fortfahren kann.
Der Hinterlegungsort für diese Adresse ist der Stack, dem Intel gleich ein
eigenes Segment spendiert hat. Dessen Adresse steht im Register SS.
Als zweites Hauptanwendungsgebiet werden im Stack Parameter für Unterprogramme untergebracht.
Und wenn gerade mal kein Prozessorregister mehr frei ist um einen Wert zu speichern und man die Einrichtung einer eigenen Variablen vermeiden will, dann ist der Stack einfach nur ein Zwischenspeicher.
Üblicherweise sollte jedes Programm über einen Stack verfügen. Sollte dies nicht zutreffen und es wird Platz auf dem Stack benötigt, dann bedient sich der Prozessor beim aufrufenden Programm, auch wenn es sich dabei um das Betriebssystem handelt. In aller Regel sehen Programme (und Betriebssyteme erst recht) nicht vor, daß andere Programme als sie selbst den Stack verwenden. Aus diesem Grunde kann es ganz schnell zum sogenannten Stack Overflow kommen, der vielleicht noch abgefangen wird, oft genug aber den ganzen Rechner abstürzen läßt.
Als Besonderheit des Stacks ist zu beachten, daß die Adresse der aktuellen Stackspitze von oben nach unten wächst, d. h. je mehr Elemente auf den Stack gelegt werden, desto niedriger ist die Adresse der Stackspitze.
In unserem ersten Programm wurden für den Stack hundert Bytes reserviert
(STACK 100). Wo der Stack dann letztlich liegt wird erst beim Start des Programms
festgelegt. Die benötigte Größe des Stacks ist im Voraus oft nur schlecht
bestimmbar. Wenn man mit rekursiven Unterprogrammen arbeitet, also Prozeduren,
die sich selbst aufrufen, die Anzahl der Aufrufe aber größer ist als es der
Stack zuläßt, dann kann im schlimmsten Fall der gesamte Rechner abstürzen, weil
wichtige Daten überschrieben werden. Besonders bei COM-Dateien, die ja nur
maximal eine Größe von 64 KB erreichen können und alles im gleichen Segment
liegen muß, kann es sein, daß der nach unten wachsende Stack Teile des Codes
überschreibt.
Bezüglich Größe und Teilbarkeit gilt das gleiche wie bei den Indexregistern.
| Register | Bedeutung |
|---|---|
| SP | Stack Pointer Dieses Register zeigt auf die aktuelle Stackspitze |
| BP | Base Pointer Dieses Register zeigt auf die für den aktuellen Kontext gültige Stackbasis. Meist verwenden Unterprogramme einen eigenen sogenannten Stackrahmen und mit Einbeziehung dieses Registers lassen sich Adressen von übergebenen Parametern errechnen. |
Es gibt beim 8086 zwei Register, die sich in keine der obigen Kategorien einordnen lassen.
| 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| _ | _ | _ | _ | Overflow | Direction | Interrupt Enable | Trap/Single Step | Sign | Zero | _ | Auxiliary | _ | Parity | _ | Carry |
Die Entwicklung ist selbstverständlich nicht beim 8086 stehengeblieben. Intel
brachte noch eine Reihe weiterer Prozessoren heraus, die natürlich um Befehle,
Register und Fähigkeiten erweitert wurden, trotzdem aber Kompatibilität zum
jeweiligen Vorgänger wahrten.
| Bits 31-16 | Bits 15-8 | Bits 7-0 |
|---|---|---|
| k.N. | AH | AL |
| k.N. | AX | |
| EAX | ||
Es ist natürlich ganz wichtig, daß man mit einem Programm, das Befehle des 80386 verwendet (die erst bei diesem Prozessor eingeführt wurden), die älteren Prozessoren ausschließt. Soll das Programm auf einem bestimmten Prozessor laufen, dann darf man keine Befehle verwenden, die erst in späteren Prozessoren eingeführt werden.
Der Cache-Speicher sitzt zwischen der CPU und dem Hauptspeicher (RAM). Er
besteht aus sehr schnellen Speicherbausteinen und ist im Vergleich zum normalen
RAM relativ klein. Im Cache befinden sich Speicherstellen, auf die vor kurzem
zugegriffen wurden. Im Cache selbst gibt es kein Adressierungsschema wie man
es vom RAM kennt. Stattdessen prüft die Cache-Kontroll-Einheit, ob die
momentan am Adressbus anliegende Adresse im Cache enthalten ist. Sollte dies
der Fall sein, so wird die Speicherstelle dem Cache entnommen und der
Hauptspeicher ignoriert. Der Sinn des Cache liegt darin, kürzlich
verwendete Codesequenzen oder Daten nicht jedesmal aus dem relativ langsamen
Hauptspeicher zu ziehen, sondern diese Daten in einem Speicher vorzuhalten,
auf den fast ohne Zeitverzug zugegriffen werden kann. Wichtig wird dies in
Schleifen und bei Arrayverarbeitung. Wenn eine Speicherstelle im Cache
gefunden wird, dann bezeichnet man das als Cache Hit, anderenfalls als
Cache Miss. Bei einem Cache Miss wird die Speicherstelle in den Cache geladen.
Hier wird dann auch davon ausgegangen, dass das Programm auch weitere
Speicherstellen in diesem Bereich ansprechen wird. Deswegen werden gleich
mehrere Bytes auf einmal eingelesen, die sogenannte cache line. Beim 80486 ist
sie beispielsweise 16 Bytes lang.
Seit dem 80486 ist der Cache auf der CPU untergebracht. Seitdem gibt es auch
das Zwei-Stufen-Cache-Konzept. Auf der CPU liegt der First-Level-Cache,
zwischen CPU und Hauptspeicher liegt der vergleichsweise wesentlich
größere Second-Level-Cache. Der 2nd-Level-Cache ist immer noch
wesentlich schneller als der Hauptspeicher, aber nicht ganz so schnell wie
der 1st-Level-Cache. Der schnelle Cache-Speicher ist einfach viel zu teuer um
ihn in größerer Menge als First-Level-Cache zu verbauen.
| Vorheriges Kapitel (Über TASM) | Nach oben | Nächstes Kapitel (Zahlensysteme) |
| Zum Inhaltsverzeichnis | ||
| Zur Startseite |