Wirtschaftsinformatik (Bachelor-Studiengang): Rechnerarchitketur & Betriebssysteme (1. Semester)
Sie sind hier: Startseite › Wirtschaftsinformatik › Rechnerarchitektur/Betriebssysteme: Maschinen-Ebenen
BM / CM, Kurs vom 01.04.2002 - 30.09.2002
Konventionelle Maschinenebene
Die Konventionelle Maschinenebene ist die Ebene, auf der eine CPU die Instruktionen aus dem Arbeitsspeicher (RAM) holt, diese dekodiert und ausführt (interpretiert).
Bildbeschreibung "Ebenenaufbau": Von oberster zu unterster Ebene: Anwendungen (realisierte Anwendungsprogramme), Betriebssystem (realisierte Komponenten des Betriebssystems), Compiler (Programme höherer Programmiersprachen), Assembler (Assembler-Maschinenprogramme), Maschine (CPU-Maschinenprogramme), Hardware (Aufbau der Hardware: CPU, Bus, I/O-Geräte), Physik (Technologien, z.B. DTL, TTL). Die Hardware-Ebene ist weiter unterteilbar in die drei Ebenen (von oben nach unten) Komponenten, Bausteine, Gatter.
Ein direkt von der CPU ausführbares Programm heißt Binärprogramm oder Programm im Maschinencode; verkürzt: Maschinencode.
Binärprogramme sind interne Repräsentationen der Instruktionen und lassen sich nur schwer vom Menschen sichtbar machen (Repräsentation); noch schwieriger ist ihre Erstellung.
Die Präsentation von Binärprogrammen erfolgt meistens in hexadezimaler (Basis 16) oder oktaler (Basis 8) Form.
Um diese Folien leichter lesen zu können, wird eine symbolische Ersatzdarstellung gewählt (die einem fiktiven Assembler entspricht).
Präsentation in Hexadezimal | Präsentation in symbolischer Darstellung (Assembler) | ||||
---|---|---|---|---|---|
Adresse (jeweils die Adresse des ersten Bytes) |
Maschinencode (jeweils vier Bytes) |
Adresse | Maschinencode | ||
00000 | 34A3 56ee | 00000 | load | R3,@56ee | Register R3 = Inhalt von 56ee |
00004 | 67A2 1000 | 00004 | load | R2,#1000 | Register R2 = 0x1000 |
00008 | 7F23 1200 | 00008 | add | R2,R3 | Register R2 = R2 + R3 |
0000E | 0124 2300 | 0000E | sub | R2,R4 | Register R2 = R2 - R4 |
00010 | 66A3 56ee | 00010 | store | R3,@56ee | Inhalt von 56ee = Register R3 |
. . . | . . . | . . . | . . . |
Grundsätzliche Arten von Speicherinhalten
An einer Speicherstelle wird ein Wert folgender Typen abgeklegt (genauer: die Repräsentation bzw. Codierung):
- Ganze Zahl (Integer, int)
- Rationale Zahl (Fest- und Fließpunkt)
- Zeichen (Character, char)
- Instruktion
- Adresse einer anderen Speicherstelle
1. bis 3. wurden schon behandelt; 4. betrifft die Speicherung von Befehlen, während 5. Pointer betrifft.
Pointer = Variable (Speicherzelle) mit den Werten von
Adressen
Zeiger = Pointer (Deutsche Übersetzung)
Adresse = Nummer einer Speicherzelle im RAM
Aufbau von Instruktionen (Befehlen):
Prinzipieller Aufbau eines Befehls:
- Operation: Was soll geschehen?
- Operanden: Mit was soll dies erfolgen?
- Nächste Instruktion: Wo geht es weiter?
Bildbeschreibung "Aufbau eines Befehls": Eine Instruktion (auch "Befehl" genannt) setzt sich zusammen aus Operation und Operand. In einem Befehl können mehrere Operanden aufeinander folgen. Somit gibt es verschiedene Arten von Befehlen: 0-Adress-Befehl = 0 Operanden, 1-Adress-Befehl = 1 Operand, 2-Adress-Befehl = 2 Operanden, 3-Adress-Befehl = 3 Operanden.
Arten von Operanden:
- Register: Nummer des Registers oder andere Codierung
- Adresse: Adresse im RAM (Länge entsprechend der Architektur)
- Index: Wert zum Adressieren in Feldern oder im RAM
- Konstante: Fester Wert als Bestandteil des Befehls
Die Fälle werden durch die Adressierungsarten unterschieden:
Bildbeschreibung "Adressierungsarten": Repräsentation (Codierung) der Operation durch Opcode (Operationscode). Operand setzt sich zusammen aus Codierung der Adressierungsart und Registernummer. In der Praxis hängen mögliche Adressierungsarten von der Operation ab. Die Adressierungsart bestimmt den Aufbau des gesamten Befehls.
Aufbau von Befehlen (Beispiel):
LOAD R3,#00FF
(Lade die Konstante 0x00FF
ins Register R3)
Bildbeschreibung "Aufbau von Befehlen":
Der 8 bit Opcode enthält die Codierung [0x15]
. Der 8 bit
Operand 1 setzt sich aus Modus 0 (Register) [0x0]
und
Registernummer 3 [0x3]
zusammen. Der 16 bit Operand 2 enthält
[0x00FF]
. Der endgültige Befehl setzt sich somit aus Opcode,
Operand 1 und Operand 2 zusammen (0x15 | 0x03 | 0x00FF
).
Zeiger, Pointer und Adressen
Bildbeschreibung "Zeiger, Pointer und Adressen": Variable P (Typ Adresse) verweist / referenziert auf Variable V (Typ Integer).
Adressen sind die "Namen" von Speicherstellen, an denen sich Variablen befinden.
Variablen mit Adressen als Werte werden Pointer (Zeiger) genannt. Diese Variablen werden analog zu den anderen (int ...) benutzt, wobei die Operationen anders sind:
- Setzen mit gültiger (!) Adresse
- Setzen auf den Wert nil/null (keine gültige Adresse)
- Dereferenzieren: Wert der Variablen, auf die verwiesen wird
- Addieren / Indizieren
- Subtrahieren
- Differenz zwischen zwei Adressen bilden
Dereferenzieren = Den Inhalt eines Zeigers holen und den Wert der Variablen an dieser Adresse holen.
Eine Adresse ist gültig, wenn ein Dereferenzieren sinnvoll ist - das ist es nur dann, wenn an der Adresse (Wert des Pointer) eine Variable sich befindet. Ansonsten sind alle Werte eines Pointer ungültig.
Zur absichtlichen Kennzeichnung ungültiger Pointer-Werte wird
eine bestimmte Adresse (Wert) reserviert. Dieser Wert wird nil
(nichts) oder null genannt. Meistens wird dafür eine 0 benutzt
(daher der Name null). Es kann aber auch eine hohe Adresse
verwendet werden, z.B. 0xFFFF FFFF
.
Die Operation Dereferenzieren ist ungültig, wenn der Pointer-Wert ungültig ist.
Bei Addieren bzw. Indizieren werden Integer-Konstanten oder Registerinhalte addiert. Das Addieren zweier Adressen ist ungültig. Addieren und Subtrahieren von Konstanten ist zum Durchlaufen von Feldern sinnvoll.
Um die Größe eines Objektes bzw. den Adressabstand zwischen zwei Objekten zu bestimmen, wird die Differenz zwischen zwei Adressen (Pointer-Werte) berechnet.
Folgende Register sind Pointer (enthalten Adressen):
- Programm Counter (PC)
- Stack Pointer (SP)
Indizieren in Feldern (Arrays):
Bildbeschreibung "Indizieren in Feldern (Array)": Die Startadresse wird abgebildet durch "Element [0]". Dem folgen Element [1], [2], [3] bis zum letzten Element, bezeichnet als "Element [max]". Die Adressdifferenz D (auch bezeichnet als Offset oder Displacement) beschreibt die Differenz zwischen einem Element und dem Startelement. Index = Nummer der Elemente beginnend mit 0. Adresse des Elements = Startadresse + (Elementgröße × Index). Adressdifferenz D = Elementgröße × Index. Beispiel: Gegeben sind "int vector[10]; vector[3]=10". Elementgröße: 4 byte (32 bit integer); Index: 3; Adressdifferenz: 12; Element: Dereferenzieren(Startadresse + 12); Indizieren: Dereferenzieren(Startadresse + 12).
- Verweis = Referenz = Adresse
- Feld = Array = Lineare Reihung von Elementen gleicher Größe und gleichem Adressabstand
- Vektor = Feld mit Elementen, die selbst keine Felder sind
- Matrix = Feld mit Elementen, die selbst ein Vektor mit Elementen sind (2-dimensionales Feld)
- Cubus = Feld mit Elementen, die Matrizen sind (3-dimensionales Feld)
- Index = Nummer (nicht Adresse!) eines Elements in einem Feld
- Indizieren = Berechnen der Adresse eines Elements anhand Startadresse und Index sowie anschließendes Dereferenzieren
- Offset = Displacement = Adressdifferenz als Abstand von einem bestimmten Adresswert zu einem anderen
Beim Indizieren muss in der Regel multipliziert werden, was zu langsamen Programmen führt (Multiplikation ist nach der Division die in der Regel langsamste Operation).
Operationsmodi
Bildbeschreibung "Operationsmodi": Register, Register Indirekt, Adresse Indirekt, Adresse Indiziert, Offset Indiziert, Immediate.
Arten von Operationen:
Transfer-Operationen
- Kopieren in Register ("load")
- Speichern in RAM ("store")
- Kopieren im RAM, zwischen Registern
Arithmetische Operationen
- 1-, 2- oder 3-Address-Operationen
- Addition, Subtraktion, Multiplikation, Division
- (Vorzeichenbehaftete) Integer, Float
Logische Operationen
- "und"-, "oder"- und "Negation"-Operation
- (Vorzeichenbehaftetes) Schieben von Bits ("Shift")
Kontrollflussoperationen
- Unbedingte Sprünge
- Bedingte Sprünge
- Sprung zu Subroutinen
Sonderoperationen
- Reset-Operation
- Stop/Trace-Operationen
Diese Gruppen von Operationen können 1-3 Operanden mit jeweils verschiedenen Adressierungsarten haben.
CPU-Register für Programme
Grundelemente der meisten (CISC-)Architekturen:
Bildbeschreibung "Grundelemente der meisten (CISC-)Architekturen": N-bit-Adressregister + M-bit-Datenregister + N-bit-PC (Programm Counter) + N-bit-SR (Status Register) + N-bit-SP (Stack Pointer).
- Programm Counter (PC) = Register in der CPU, das die Adresse der nächsten bzw. aktuell ausgeführten Instruktion enthält
- Instruction Pointer (IP) = PC (andere Bezeichnung)
- Status Register (SR) = Register, dessen einzelne Bits den gesamten Status der CPU sowie die Ergebnisse einer Operation beschreiben
- Processor Status Wort = SR
- Datenregister = Register, mit denen allgemeine Operationen (arithmetisch, logisch) durchgeführt werden
- Address Register = Register, mit denen primär Adressen behandelt werden (diese sind dann auch Pointer)
- CISC = Complex Instruction Set Computer
Stack (Stapel, Keller)
Ein Stack ist eine Organisation eines Stück Speichers, um mit minimalen Aufwand beliebig komplexe arithmetische Ausdrücke sowie das Aufrufen von Unterprogrammen zu realisieren.
- Die
push
-Operation bringt einen Wert auf den Stack. - Die
pop
-Operation holt den obersten Wert vom Stack und schreibt ihn in eine Variable.
In den folgenden Beispiele möge der
Stack Pointer (SP)
immer auf das oberste Element
des Stack zeigen. Der Stack möge von
den hohen Adressen zu
den niedrigen wachsen, d.h. mit
voller werdenden Stack hat der SP einen
immer kleineren Wert.
Es gibt Stacks, die anders organisiert sind!
Stack-Aufbau
Bildbeschreibung "Stack-Aufbau": Speicherbereich von hohen Adressen bis hin zu niedrigen Adressen. Diese ist auch die Richtung, in der der Stack wächst. Hohe Adressen sind durch den Stack belegt. Dann folgt ein freier Bereich und der Bereich der niedrigen Speicheradressen ist dann wieder belegt.
Der Stack-Pointer ist eine Variable, deren Werte Adressen auf eine bestimmten Speicherbereich sind.
Stack-Operation push(integer)
:
Bildbeschreibung "push(integer)
":
Stack-Operation. Vorher: Der Stack-Pointer
zeigt auf den Stack.
Nachher: Ein Integer-Bereich wurde an den Stack-Bereich
angefügt. Der Stack-Pointer zeigt auf den Integer-Bereich.
Stack-Operation pop(integer)
:
Bildbeschreibung "pop(integer)
":
Stack-Operation. Vorher: Der Stack-Pointer
zeigt auf einen
dem Stack angefügten Integer-Bereich. Nachher:
Der Stack-Pointer zeigt auf den Stack. Der
Integer-Bereich
wurde entfernt.
Der Zugriff auf den Stack erfolgt über den Stack-Pointer bzw. über andere Zeiger (Pointer), die in den Stack zeigen.
Diese neue Architektur - Stack-Maschine genannt - ist ein gedankliches Gebilde, das auf realen Architekturen per Software simuliert werden kann. Diese simulierende Software realisiert dann eine Virtuelle Maschine mit einem Interpreter. Dies wird zur Realisierung von Java häufig benutzt (diese Maschine arbeitet stark Stack-orientiert, also sehr ähnlich der hier vorgestellten Idee).
Stack werden in zwei Bereichen angewendet:
- Ausrechnen von Formeln
- Organisieren von lokalen Variablen
Ausrechnen von Formeln: (a+b) × (c−d)
Bildbeschreibung "(a+b) × (c−d)": Erstens: a hinzufügen. Zweitens: b hinzufügen. Drittens: a und b addieren. Viertens: c hinzufügen. Fünftens: d hinzufügen. Sechstens: c und d subtrahieren. Siebtens: Ergebnis aus Schritt drei und Schritt sechs multiplizieren.
Werte, die erst später benutzt werden, werden auf den Stack gebracht und bleiben dort, bis sie gebraucht werden.
Werte, die sofort gebraucht werden, werden von der Operation während der Ausführung vom Stack geholt; dafür wird das Ergebnis wieder auf den Stack gebracht.
Mit einem Stack können beliebig komplexe, geschachtelte arithmetische Ausdrücke sowie Ausdrücke zur Berechnung von Adressen ausgewertet werden.
Subroutinen
Es gibt immer wieder zu verwendende Code-Stücke. Um diese nicht immer wieder neu programmieren zu müssen, werden diese einmal im Speicher abgelegt und bei Bedarf dynamisch angesprungen.
Prozedur = Procedure = Routine = Methode = Funktion = Benanntes Stück Code, das angesprungen und ausgeführt werden kann, um am Ende wieder zurückzuspringen [die Begriffe werden später noch etwas differenziert].
Unter einer Subroutine wird hier der Oberbegriff für alle eben genannten Begriffe benutzt.
Schritte beim Aufruf und bei der Rückkehr einer Subroutine:
Bildbeschreibung "Schritte beim Aufruf und bei der Rückkehr einer Subroutine": Erstens: Das Hauptprogramm wird gestartet und teilweise ausgeführt. Zweitens: Die Subroutine S wird aufgerufen. Drittens: Die Subroutine S wird ausgeführt. Viertens: Rückkehr zum Hauptprogramm. Fünftens: Ausführen des restlichen Teils des Hauptprogramms.
Die Pfeile geben den Weg an, den der PC
nimmt.
Der Code beider Routinen befindet sich im selben Adressraum.
Subroutinen können sich auch geschachtelt aufrufen. Die Subroutine, die am Anfang als erstes gestartet wird, wird Hauptprogramm genannt.
Wenn sich Subroutinen selbst aufrufen, wird dies Rekursion genannt:
- Direkte Rekursion liegt vor, wenn sich die betreffende Subroutine selbst aufruft.
- Indirekte Rekursion liegt vor, wenn die eigene Subroutine von einer anderen Subroutine aufgerufen wird, die sie selbst aufgerufen hat.
Das Phänomen der Rekursion ist hier nur in dem Sinne wichtig, als dass es möglich sein soll - auch wenn die sinnvollen Anwendungsfälle (noch) unbekannt sind.
Bildbeschreibung "Rekursion": Erstens: Das Hauptprogramm wird gestartet und teilweise ausgeführt. Zweitens: Die Subroutine S wird aufgerufen. Drittens: Die Subroutine S wird teilweise ausgeführt. Viertens: Die Subroutine T wird aufgerufen. Fünftens: Die Subroutine T wird ausgeführt. Sechstens: Rückkehr zu Subroutine S. Siebtens: Ausführen des restlichen Teils der Subroutine S. Achtens: Rückkehr zum Hauptprogramm. Neuntens: Ausführen des restlichen Teils des Hauptprogramms.
Realisierung:
"call xyz" wird
durch die Instruktion jsr realisiert.
(jsr = jump to subroutine).
Es
wird die Rückkehradresse auf den Stack geschrieben und dann
zur Routine gesprungen.
Bildbeschreibung "jsr-Instruktion": Vorher: Der Stack-Pointer zeigt auf den Stack. Nachher: Ein Adress-Bereich wurde an den Stack-Bereich angefügt. Der Stack-Pointer zeigt auf den Adress-Bereich.
"return" wird
durch die Instruktion rts realisiert.
(rts = return from
subroutine). Es wird die Adresse vom Stack geholt und
dorthin gesprungen.
Bildbeschreibung "rts-Instruktion": Vorher: Der Stack-Pointer zeigt auf einen dem Stack angefügten Adress-Bereich. Nachher: Der Stack-Pointer zeigt auf den Stack. Der Adress-Bereich wurde entfernt.
Durch die Verwendung des Stack ist eine einfache Realisierung bei geschachtelten sowie auch bei rekursiven Aufrufen möglich.
Die Instruktionen jsr und rts sind hier fiktiv. Bei jedem Prozessor heißen sie anders, haben aber im Prinzip dieselbe Bedeutung (Funktion) wie jsr und rts.
Lokale Variablen:
Subroutinen benötigen für das Speichern von Zwischenergebnissen auch Variablen, die aber während der Subroutinen-Aktivierung vorhanden sein sollen.
Diese Variablen müssen beim Aufruf erzeugt sowie bei der Rückkehr wieder entfernt werden.
Da dies auch bei Schachtelungen bzw. Rekursion erfolgen soll, müssen diese Variablen auf den Stack gebracht werden: So geschachtelt wie die Aufrufe, so geschachtelt sollen diese Variablen vorhanden sein.
Variablen, die während der gesamten Laufzeit einer Subroutine existieren und mit deren Beendigung verworfen werden, werden lokale Variablen genannt.
Parameter:
Wenn eine Subroutine zum Ausgeben einer Integer-Variablen geschrieben werden soll, dann muss es möglich sein, zu verschiedenen Zeitpunkten auch verschiedene Integer-Variablen auszugeben.
Um derartige Subroutinen schreiben zu können, werden Parameter benötigt, über die der Subroutine mitgeteilt wird, was und womit sie ihre Aufgabe lösen soll.
Parameter = Wert einer Variablen aus dem Bereich der aufrufenden Subroutine, mit der die aufgerufene Subroutine arbeiten soll.
Auch hier muss das Problem der Schachtelung gelöst werden.
Lokale Variablen und Parameter auf dem Stack
Stack-Aufbau während der Ausführung der Subroutine:
Bildbeschreibung "Stack-Aufbau während der Ausführung der Subroutine": Vier Bereiche (in Richtung, in der der Stack wächst): Parameter, Rückkehr-Adresse, Lokale Variablen, freier Bereich.
Schrittweiser Aufbau des Stack:
Aufruf: Subroutine(Parameter 1, ..., Parameter N):
Bildbeschreibung "Aufruf: Subroutine(Parameter 1, ..., Parameter N), Bild 1": Erstens (Aufrufer): Aufruf des Stack (Inhalt des Stack = Lokale Variablen + freier Bereich). Zweitens (Aufrufer): Hinzufügen der Bereiche Parameter 1, 2 und N (Inhalt des Stack = Lokale Variablen + Parameter 1 + Parameter 2 + Parameter N + freier Bereich). Drittens (Aufrufer): Aufruf der Subroutine (Inhalt des Stack = Lokale Variablen + Zusammengefasster Parameter-Bereich + Return-Adresse + freier Bereich). Viertens (Subroutine): Hinzufügen Lokale Variablen (Inhalt des Stack = Lokale Variablen + Parameter + Return-Adresse + Lokale Variablen + freier Bereich). Fortsetzung in nachfolgender Grafik!
Bildbeschreibung "Aufruf: Subroutine(Parameter 1, ..., Parameter N), Bild 2": Fortsetzung der vorangegangenen Grafik! Fünftens (Subroutine): Hinzufügen von Berechnungen (Inhalt des Stack = Lokale Variablen + Parameter + Return-Adresse + Lokale Variablen + Ausdrücke + freier Bereich). Sechstens (Subroutine): Schließen der lokalen Variable (Inhalt des Stack = Lokale Variablen + Parameter + Return-Adresse + freier Bereich). Siebtens (Subroutine): rts (Inhalt des Stack = Lokale Variablen + Parameter + freier Bereich). Achtens (Aufrufer): Schließen der Parameter (Inhalt des Stack = Lokale Variablen + freier Bereich).
Nach dem nachfolgenden Schema arbeiten die (meisten) Aufrufe von Subroutinen. Bei einer Subroutine ohne Parameter entfällt das "push Parameter" bzw. "pop Parameter". Bei einer Subroutine ohne lokale Variablen entfällt das "push Lokale Variablen" bzw. "pop Lokale Variablen".
Bildbeschreibung "Schema Subroutinen": Aufrufer = push Parameter + jsr Subroutine + pop Parameter. Subroutine = push Lokale Variablen + Berechnungen + pop Lokale Variablen + rts.
Wenn die Subroutine selbst einen Wert als Ergebnis zurückliefern soll, z.B. wie sin(x), dann wird der Platz für das Resultat vom Aufrufer als besonderen Parameter gleich zum Anfang auf den Stack gebracht, so dass nach dem Aufruf das Resultat als oberstes Element auf dem Stack steht (und damit leicht in die Berechnungen beim Aufrufe benutzt werden kann, siehe 1. Stack-Anwendung).
Eine Subroutine, die einen Wert liefert, wird Funktion genannt, eine, die keinen Wert liefert Prozedur bzw. Procedure.
Routine und Subroutine verbleiben als Oberbegriffe. Statt Routine bzw. Subroutine wird beim objektorientierten Ansatz von Methoden gesprochen.
Activation Record = Teil des Stack, der alle temporären Daten eines Subroutinenaufrufs beinhaltet.
Kopf und Körper einer Subroutine:
Kopf = Schnittstelle der Subroutine nach außen.
Körper = Body = Code einer Subroutine ohne Parameter, d.h. der Teil nach dem Anlegen der lokalen Variablen bis zum Entfernen der lokalen Variablen.
Bildbeschreibung "Kopf und Körper einer Subroutine": Der Kopf setzt sich zusammen aus dem zu liefernden Resultat, Namen und Parametern. Der Körper beinhaltet Lokale Variablen und Resultate.
Makro:
Makro = Stück Code, der bei jedem Aufruf an die Stelle des Aufrufs hineinkopiert wird.
Wie eine Subroutine hat auch ein Makro einen Kopf und einen Körper.
Der Kopf definiert den Namen und die Parameter, der Körper
das, was beim Aufruf hinein-kopiert werden soll.
Der Aufruf selbst wird entfernt und stattdessen der Körper
eingesetzt.
Beispiel eines Makro in der Sprache C:
Bildbeschreibung "Beispiel eines Makro in der
Sprache C": # define TEST (a==b)
, wobei #
define
für die Definition des Makros steht,
TEST
den Namen des Makros angibt und (a==b)
den Körper darstellt. if TEST {a = 0; b = 1;}
wird
durch Expansion zu if (a==b) {a = 0; b = 1;}
.
Assembler
Assembler = Übersetzer für Programme in einer symbolischen Maschinensprache. Die Sprache Assembler ist für jeden CPU-Typ anders und spiegelt die Eigenarten der CPU-Architektur (Register, Befehlssatz usw.) wieder. Zur Assembler-Sprache gehören
- Befehle (Instruktionen) der CPU
- Makros als Zusammenfassungen mehrerer Befehle
- Anweisungen zur Reservierung von Speicherplatz
- Anweisungen zur Belegung von Speicherplatz
Der Assembler übersetzt das Assembler-Programm in ein maschinen-codiertes Format, dem Objekt-Format. Diese Dateien heißen daher Objekt-Dateien.
Assembler:
- Label:
LOAD R3,@56EE
Register R3 = Inhalt von56EE
- Label:
LOAD R2,#1000
Register R2 =0x1000
- Label:
ADD R2,R3
Register R2 = R2 + R3 - Label:
SUB R2,R4
Register R2 = R2 - R4 - Label:
STORE R3,@56EE
Inhalt von56EE
= Register R3
Objekt-Datei:
34A3 56EE
67A2 1000
7F23 1200
0124 2300
66A3 56EE
Die Assemblersprachen sind in der Regel spaltenorientiert, d.h. die Zeilen haben ein festes Format, das einzuhalten ist.
Im vorangegangenen Beispiel sieht das wie folgt aus:
- Spalte zur Definition von Sprungmarken, z.B. Label
- Befehle mit den Parametern, z.B.
LOAD R3,@56EE
- Kommentar, z.B.
Register R3 = Inhalt von 56EE
Ein wichtiges Charakteristikum eines Assemblers ist, dass die Assembler-Befehle fast immer 1:1 zu Maschinenbefehlen umgesetzt werden.
Sprungmarken = Label = Namen für Speicherstellen (symbolische Adressen) von bestimmten Instruktionen, z.B. zum Beginn einer Subroutine.
Das Programmieren in Assembler ist sehr mühselig, da:
- es viel Zeit kostet,
- viele Fehler gemacht werden können.
Aber: In Assembler sind die effizientesten Programme schreibbar.
Compiler
Höhere Programmiersprachen, wie z.B. C oder Java, werden durch Compiler in Maschinensprache übersetzt.
Compiler = Übersetzer für Programme in einer höheren Programmiersprache, die sich dadurch auszeichnet, dass ein Statement dieser Sprache in mehrere Befehle in der Maschinensprache übersetzt werden muss.
Bildbeschreibung "Beispiel Compiler": i = j + 1
.
Ablauf: LOAD R3,@56EE
(R3 = Inhalt von j); ADD
R3,#0001
(R3 = R3 + 1); STORE R3,@56FF
(i = R3).
Beispiel: Compiler für C
Es wird hier C als Beispiel benutzt; dies kann analog auf C++ oder Java übertragen werden.
Das Übersetzen erfolgt in mehreren Durchläufen (Pass), in denen das gesamte Programm vollständig gelesen und interpretiert wird.
Nach jedem Durchlauf wird das Programm in überarbeiteter Form neu in einer speziellen Datei angelegt; diese wird bei dem nächsten Durchlauf benutzt, so dass beginnend vom ursprünglichen Programm über mehrere Dateien am Ende das Maschinenprogramm entsteht.
Die Steuerung der Durchläufe übernimmt ein spezielles Hauptprogramm.
Bei Compiler sind 4 bis 5 Durchläufe üblich, es können auch erheblich mehr sein, z.B. PL/1 hatte bis zu 60 Durchläufe.
Ablauf einer Übersetzung:
Bildbeschreibung "Ablauf einer Übersetzung": Erstens: "Beispiel.c" (C-Programm). Durch Preprozessor (Dateien mit Makrodefinitionen) folgt zweitens: "Beispiel.i" (C-Programm mit expandierten Makros). Dann folgt der eigentliche Übersetzer und drittens: "Beispiel.o" (übersetztes Programm ohne Bibliotheksroutinen, Objekt-Datei). Durch den Linker / Binder (Hauptprogramm, Bibliotheken) folgt viertens: "Beispiel" (fertiges ausführbares Programm, Maschinencode). Am Ende der Kette steht das Programm in Ausführung.
- 1. Durchlauf: Makroexpansion
Es werden die Makrodefinitionen (C hat die Möglichkeit von Makros, in Programmiersprachen ohne Makros wird dieser Schritt ausgelassen) vermerkt und alle Makro-Aufrufe mit den Makrokörpern ersetzt. - 2. Durchlauf: Syntaktische Prüfung
Entspricht der entstandene Text den Regeln der Sprache? Z.B. Hat jede öffnende Klammer (rund oder geschweift) eine korrespondierende schließende? Wird jedes Statement durch ein Semikolon abgeschlossen? - 3. Durchlauf: Semantische Prüfung
Sind alle Variablen und Funktionen deklariert? Werden sie übereinstimmend damit benutzt? - 4. Durchlauf: Optimierung (optional)
Können Deklarationen weggelassen werden, da die Variablen nicht benutzt werden? Lassen sich Schleifen verkürzen? - 5. Durchlauf: Erzeugung von Assembler-Code
Für jedes Statement wird der entsprechende Assemblercode generiert, so dass das generierte Programm das tut, was es laut Sprachdefinition tun sollte. - 6. Durchlauf: Assemblieren
Der Compiler ist jetzt eigentlich fertig; es wird ein Assembler gestartet, der das generierte Assembler-Programm in eine Objekt-Datei übersetzt.
Der generierte Objectcode ist aus folgenden Gründen nicht ausführbar:- Es fehlen aufgerufene und nicht programmierte Routinen, z.B. system.out.print().
- Globale Variablen haben noch keine feste Position (Adresse), sie können an andere Stellen verschoben werden.
- 7. Durchlauf: Binden
Der Binder durchsucht Objektbibliotheken, um ein unvollständiges Programm mit den nicht selbst programmierten, aber aufgerufenen Subroutinen zu ergänzen. Am Ende ist eine maschinen-ausführbare Datei entstanden.
Jetzt kann die Datei mit Maschinencode von der CPU in den RAM geladen und ausgeführt werden.
Bibliothek = Archiv = Library = Datei mit mehreren benannten Informationsblöcken einschließlich eines Verzeichnisses.
Objektbibliothek = Bibliothek mit Objekt-Dateien.
Binärcode = Maschinencode.
CISC - RISC
Anfang der 80er Modewelle: Statt den komplizierten CPU-Architekturen sollten nur sehr schnelle, sehr einfache Instruktionen benutzt werden.
- Vorteile:
- Einsparung von Chipplatz aufgrund Reduktion der Komplexität der Instruktionen
- Sehr einfache Dekodierung der Instruktionen und einfache Pipelines (Super-Skalar-Pipelining)
- Eingesparte Chipfläche wird in sehr vielen Registern (Register Files) und umfangreichem Cache investiert
- Reduktion der Adressierungsmodi: "Normale" Operationen arbeiten nur auf Registern, während andere lediglich Register aus dem RAM laden bzw. deren Inhalte in den RAM schreiben (Transfer-Operationen)
- Nachteile:
- Es werden für dieselben Aufgaben mehr Instruktionen benötigt, was zu einer höheren Belastung des Bus führt (Nadelöhr Bus in der Von-Neumann-Architektur)
- Compiler sollten einen großen Teil der Optimierungen übernehmen
Die alten (komplexen) Architekturen wurden CISC, die neuen RISC genannt:
- CISC = Complex Instruction Set Computer
- RISC = Reduced Instruction Set Computer
In den 90er Jahren haben sich die Unterschiede aufgelöst:
- RISC-CPU haben nun auch komplexe Operationen
- CISC-CPU arbeiten intern mit RISC-CPU, z.B. Pentium 4
Die nunmehr entstandene Vereinigung beider Ideen führt zu "modernen" CPU-Architekturen, die historisch bedingt immer noch in die beiden Lager RISC/CISC gepackt werden:
- Einfache Dekodierung der Instruktionen: Super Skalare Pipeline
- Voller Komplexität bei der Semantik von Operationen
- Viele (benötigte) Instruktionen (Entlastung der Compiler)
- Load/Store-Architektur: Entweder komplexe Semantik, dann aber nur Register als Operanden, oder Transfer-Semantik
- Großen (mehrstufigen) Cache und viele Register
Beispiele sind: Alpha, PowerPC, Itanium.