(:requiretuid:)
In diesem Kapitel springen wir erst mal auf eine höhere Abstraktionsebene.
Mit Assemblersprachen programmiert man in der Sprache des Computers.
Befehle geben Art der Operation und ihre Operanden an. Dabei gibt es zwei Darstellungen:
MIPS Architektur:
Die MIPS Architektur wurde von John Hennessy und Kollegen in Stanford in den 1980ern entwickelt. Diese wird in vielen Computern und eingebetteten Systemen verwendet (Silicon Graphics, Nintendo, Sony, Cisco, Cavium, NetLogic, ...)
Sie eigntet sich gut zur Darstellung von allgemeinen Konzepten. Vieles davon ist auch auf andere Architekturen übertragbar.
John Hennessy (Stanford) und David Patterson (Berkeley):
1. Regularität vereinfacht den Entwurf
2. Mach den häufigsten Fall schnell
3. Kleiner ist schneller
4. ein guter Entwurf verlangt gute Kompromisse
Hochsprache:
a = b + c;
MIPS Assemblersprache:
add a, b, c
Die Subtraktion ist ähnlich zur Addition. Nur der Befehlsname ändert sich
Hochsprache:
a = b - c;
MIPS Assemblersprache:
sub a, b, c
Regularität vereinfacht den Entwurf
Die komplexeren Abläufe werden durch Folgen von einfachen Befehlen realisiert:
Hochsprache
a = b + c + d; // Kommentare bis Zeilenende /*mehrzeiliger Kommentar*/
MIPS Assemblersprache:
add t, b, c # t = b + c sub a, t, d # a = t - d
Mach den häufigsten Fall schnell
MIPS enthält nur einfache, häufig verwendete Befehle. Die Hardware zur Dekodierung und Ausführung der Befehle kann einfach, klein und schnell sein. Komplexe Anweisungen (die nur seltener auftreten) können durch Folgen von einfachen Befehlen realisiert werden. MIPS ist ein Computer mit reduziertem Befehlssatz (reduced instruction set computer, RISC). Die Alternative dazu ist ein Computer mit komplexem Befehlssatz (complex instruction set computer, CISC).
Ein Prozessor hat physikalische Speicherorte für dei Operanden von Befehlen. Diese möglichen Speicherorte sind:
Da der Speicher langsam ist, haben viele Architekturen eine kleine Anzahl von schnellen Registern. MIPS verfügt über 32 Register. Dabei ist jedes 32b breit. Deshalb wird MIPS auch "32b Architektur" genannt. Es gibt auch eine 64b-Version von MIPS, aber die behandeln wir hier nicht.
Kleiner ist schneller
MIPS stellt nur eine kleine Anzahl von Registern bereit. Dies kann in schnellerer Hardware realisiert werden als ein größeres Registerfeld.
Später mehr...
Hochsprache:
a = b + c;
MIPS Assemblersprache:
# $s0 = a, $s1 = b, $s2 = c add $so, $s1, $s2
Jedes 32-bit Datenwort hat eine eindeutige Adresse
Lesen geschieht durch Ladebefehle (load). Der Befehlsname: load word (lw)
Beispiel: Lese ein Datenwort von der Speicheradresse 1 nach $s3
Assemblersprache (nicht MIPS!)
lw $s3, 1($0) # lese Wort 1 aus Speicher in $s3
Schreiben geschieht durch Speicherbefehle (store). Der Befehlsname: store word (sw)
Beispiel: Schreibe (speichere) den Wert aus $t4 in Speicherwort 7. Der Offset kann dezimal (Standard) oder hexadezimal angegeben werden.
Adressarithmetik:
Assemblersprache (nicht MIPS!)
sw $t4, 0x7 ($0) # schreibe Wert aus $t4 in das Speicherwort 7
Die Adresse eines Speicherwortes muss nun mit 4 multipliziert werden
Beispiel: Lade Wort 1 aus Byte-Adresse 4 nach $s3
MIPS Assemblersprache
lw $s3, 4($0) # Lese Wort 1 an Byte-Adresse 4 nach $s3
MIPS Assemblersprache
sw $t7, 44($0) # schreibe $t7 nach Byte-Adresse 44
Schemata für Nummerierung von Bytes in einem Wort:
Welche Speicherorganisation benutzt wird, ist im Prinzip egal, außer wenn unterschiedliche Systeme Daten austauschen müssen.
Wir nehmen an, dass $t0 den Wert 0x23456789 enthält. Im Programm:
sw $t0, 0($0) lb $s0, 1($0)
Frage, an welchem Wert hat $s0 nach der ausfürhung auf einem...
Ein guter Entwurf verlangt gute Kompromisse
Mehrere Befehlsformate erlauben Flexibilität...
... aber Anzahl von Befehlsformaten sollte klein sein
lw und sw zeigen die Verwendung von konstanten Werten (immediates)
Der Befehl "add immediate" (addi) addiert Direktwert auf Register. Der Direktwert ist eine 16b Zweierkomplementzahl
Hochsprache
a = a + 4; b = a - 12;
MIPS Assemblersprache
# $s0 = a; $s1 = b addi $s0, $s0, 4 addi $s1, $s0, -12
Der Computer versteht nur 0'en und 1'en. Die Maschinensprache ist die Binärdarstellung von Befehlen.
32b Befehle -> Regularität vereinfacht den Entwurf: Daten und Befehle sind beides 32b Worte.
Es gibt drei Befehlsformate:
R-Typ steht für Register Typ. Ein R-Typ hat 3 Registeroperanden:
Dabei gibt es auch andere Angaben, die binär kodierte Befehle sind:
Beispiele für R-Typ Befehle:
Assemblersprache
add $s0, $s1, $s2 sub $t0, $t3, $t5
Beachte die Reihenfolge der Register ist eine andere als in der Assembler-Sprache.
I-Typ steht für Immediate Typ. Ein I-Typ hat 3 Registeroperanden:
Dabei gibt es auch andere Angaben, die binär kodierte Befehle sind:
Es ist keine Angabe über die Funktion nötig (oder vorhanden)!
Beispiele für I-Typ Befehle
Assemblersprache
addi $s0, $s1, 5 addi $t0, $s3, -12 lw $t2, 32 ($0) sw $s1, 4($t1)
Beachte die unterschiedliche Reihenfolge von Registern in Assembler- und Maschinensprache!
addi rt, rs, imm lw rt, imm(rs) sw rt, imm(rs)
J-Typ steht für Jump-Typ und hat einen 26b Adressoperand (addr). Der J-Typ wird für Spurngbefehle (j) verwendet.
32b Befehle und Daten leigen im Speicher. Folgen von Befehlen bestimmen das Verhalten.
Das Ausführen von unterschiedlichen Programmen geschieht ohne Neuverdrahten oder Neubau von Hardware. Das neue Programm wird nur als Maschinensprache im Speicher abgelegt.
Die Hardware des Prozessors führt Programme schrittwiese aus:
Der Programmzähler (program counter, PC) zeigt die Adresse des auszuführenden Befehls an.
Bei MIPS: Programmausführung beginnt auf der Adresse 0x00400000
and, or, xor, nor
Schieben mit variabler Distanz:
Hochsprache:
//int ist ein vorzeichenbehaftetes 32b Wort int a = 0x4f3c;
MIPS Assemblersprache:
# $s0 = a addi $s0, $0, 0x4f3c
(lui lädt den 16-Bit Direktwert in die obere Registerhälfte und setzt die untere Hälfte auf 0)
Hochsprache:
int a = 0xFEDC8765;
MIPS Assemblersprache:
# $s0 = a lui $s0, 0xFEDC ori $s0, $s0, 0x8765
Hierfür verwenden wir die Spezialregister lo, hi
Der Quotient steht in lo Der Rest steht in hi
Verzweigungen und Sprünge ändern die Ausführungsreihenfolge von Befehlen. Dabei gibt es zwei verschiedene Arten von Verzweigungen:
MIPS Assemblersprache
addi $s0, $0, 4 # $s0 = 0 + 4 = 4 addi $s1, $0, 1 # $s1 = 0 + 1 = 1 sll $s1, $s1, 2 # $s1 = 1 << 2 = 4 beq $s0, $s1, target # Verzweigung wird genommen addi $s1, $s1, 1 # nicht ausgeführt sub $s1, $s1, $s0 # nicht ausgeführt target: # Positionsmarkierung (label) add $s1, $s1, $s0 # $s1 = 4 + 4 = 8
Label sind Namen für Stellen (Adressen) im Programm. Sie müssen anders als Mnemonics heißen und haben einen Doppelpunkt am Ende. (Sie dürfen also nicht add, addi, beq, sub, sll usw. lauten)
MIPS Assemblersprache
addi $s0, $0, 4 # $s0 = 0 + 4 = 4 addi $s1, $0, 1 # $s1 = 0 + 1 = 1 sll $s1, $s1, 2 # $s1 = 1 << 2 = 4 bne $s0, $s1, target # Verzweigung nicht genommen addi $s1, $s1, 1 # $s1 = 4 + 1 = 5 sub $s1, $s1, $s0 # $s1 = 5 – 4 = 1 target: add $s1, $s1, $s0 # $s1 = 1 + 4 = 5
MIPS Assemblersprache
addi $s0, $0, 4 # $s0 = 4 addi $s1, $0, 1 # $s1 = 1 j target # Springe zu target sra $s1, $s1, 2 # nicht ausgeführt addi $s1, $s1, 1 # nicht ausgeführt sub $s1, $s1, $s0 # nicht ausgeführt target: add $s1, $s1, $s0 # $s1 = 1 + 4 = 5
MIPS Assemblersprache
0x00002000 addi $s0, $0, 0x2010 0x00002004 jr $s0 0x00002008 addi $s1, $0, 1 0x0000200C sra $s1, $s1, 2 0x00002010 lw $s3, 44($s1)
In Hochsprachen gibt es folgende Konstrukte:
Hochsprache:
if (i == j) f = g + h; f = f - i;
MIPS Assemblersprache
# $s0 = f, $s1 = g, $s2 = h # $s3 = i, $s4 = j bne $s3, $s4, L1 add $s0, $s1, $s2 L1: sub $s0, $s0, $s3
Hochsprache:
if (i == j) f = g + h; else f = f - i;
MIPS Assemblersprache
# $s0 = f, $s1 = g, $s2 = h
# $s3 = i, $s4 = j
bne $s3, $s4, L1
add $s0, $s1, $s2
j done
L1: sub $s0, $s0, $s3
done:
Hochsprache:
// berechne x = ld 128 int pow = 1; int x = 0; while (pow != 128) { pow = pow * 2; x = x + 1; }
MIPS Assemblersprache
# $s0 = pow, $s1 = x addi $s0, $0, 1 add $s1, $0, $0 addi $t0, $0, 128 while: beq $s0, $t0, done sll $s0, $s0, 1 addi $s1, $s1, 1 j while done:
Eine for-Schleife ist wie folgt aufgebaut:
for (Initialisierung; Bedingung; Schleifenanweisung) Schleifenrumpf
Hochsprache
// addiere Zahlen von 0 bis 9 auf int sum = 0; int i; for (i = 0; i != 10; i = i + 1) { sum = sum + i; }
MIPS Assemblersprache
# $s0 = i, $s1 = sum addi $s1, $0, 0 add $s0, $0, $0 addi $t0, $0, 10 for: beq $s0, $t0, done add $s1, $s1, $s0 addi $s0, $s0, 1 j for done:
Hochsprache
// addiere Zweierpotenzen // kleiner gleich 100 int sum = 0; int i; for (i = 1; i < 101; i = i * 2) { sum = sum + i; }
MIPS Assemblersprache
# $s0 = i, $s1 = sum addi $s1, $0, 0 addi $s0, $0, 1 addi $t0, $0, 101 loop: slt $t1, $s0, $t0 beq $t1, $0, done add $s1, $s1, $s0 sll $s0, $s0, 1 j loop done:
$t1 = 1 if i < 101
Arrays sind nützlich, um auf eine große Zahl von Daten gleichen Typs zuzugreifen. Zugegriffen wir auf ein einzelnes Element über einen Index.
Die Größe eines Arrays ist die Anzahl von Elementen im Array.
In unserem Beispiel betrachten wir ein Array mit 5 Elementen.
Die Basisadresse hier ist 0x12348000. Dies ist die Adresse des ersten Array-Elements. Diese Adresse hat den Index 0, geschrieben als array[0].
Der erste Schritt für den Zugriff auf Elemente: Lade die Basisadresse des Arrays in die Register.
Hochsprache:
int array[5]; array[0] = array[0] * 2; array[1] = array[1] * 2;
MIPS Assemblersprache
# Basisadresse von array = $s0 lui $s0, 0x1234 # lade 0x1234 in die obere Hälfte von $s0 ori $s0, $s0, 0x8000 # lade 0x8000 in die untere Hälfte von $s0 lw $t1, 0($s0) # $t1 = array[0] sll $t1, $t1, 1 # $t1 = $t1 * 2 sw $t1, 0($s0) # array[0] = $t1 lw $t1, 4($s0) # $t1 = array[1] sll $t1, $t1, 1 # $t1 = $t1 * 2 sw $t1, 4($s0) # array[1] = $t1
Hochsprache
int array[1000]; int i; for (i=0; i < 1000; i = i + 1) array[i] = array[i] * 8;
MIPS Asseblersprache
# $s0 = Basisadresse von array, $s1 = i #Initialisierung lui $s0, 0x23B8 # $s0 = 0x23B80000 ori $s0, $s0, 0xF000 # $s0 = 0x23B8F000 addi $s1, $0, 0 # i = 0 addi $t2, $0, 1000 # $t2 = 1000 loop: lst $t0, $s1, $t2 # i < 1000? beq $t0, $0, done # if not then done sll $t0, $s1, 2 # $t0 = i * 4 (byte offset) add $t0, $t0, $s0 # address of array[i] lw $t0, 0($t0) # $t1 = array[i] sll $t0, $t1, 3 # $t1 = array[i] * 8 sw $t0, 0($t0) # array[i] = array[i] * 8 addi $t0, $s1, 1 # i = i + 1 j loop # repeat done:
ASCII steht für American Standard Code for Information Interchange.
ASCII definiert für gängige Textzeichen einen 7b breiten Code. Dies ist einfach, aber schon älter. Heute wird der Unicode verwendet. Er hat eine breitere Darstellung für alle Textzeichen.
Beispiel: "S" = 0x53, "a" = 0x61, "A" = 0x41
Klein- und Großbuchstaben liegen um 0x20 (32) Bit auseinander.
Definitionen:
Hochsprache
void main() { int y; y = sum (42, 7); ... } int sum (int a, int b) { return (a + b); }
Aufrufkonventionen:
Konventionen für MIPS:
Hochsprache
int main() { simple (); a = b + c; } void simple () { return; }
MIPS Assemblersprache
0x00400200 main: jal simple 0x00400204 add $s0, $s1, $s2 ... 0x00401020 simple: jr $ra
MIPS Konventionen:
Hochsprache
int main() { int y; ... y = diffofsums (2, 3, 4, 5); // 4 Argumente, aktuelle Parameter ... } int diffofsums (int f, int g, int h, int i) // 4 formale Parameter { int result; result = (f + g) - (h + i); return result; // Rückgabewert }
MIPS Assemblersprache
# $s0 = y main: ... addi $a0, $0, 2 # Argument 0 = 2 addi $a1, $0, 3 # Argument 1 = 3 addi $a2, $0, 4 # Argument 2 = 4 addi $a3, $0, 5 # Argument 3 = 5 jal diffofsums # Prozeduraufruf add $s0, $v0, $0 # y = Rückgabewert ... # $s0 = Rückgabewert diffofsums: add $t0, $a0, $a1 # $t0 = f + g add $t1, $a2, $a3 # $t1 = h + i sub $s0, $t0, $t1 # result = (f + g) - (h + i) add $v0, $s0, $0 # Lege Rückgabewert in $v0 ab jr $ra # Rücksprung zum Aufrufer
Ein Stack ist ein Speicher für temporäres Zwischenspeichern von Werten. Dabei agiert er wie ein Stapel (Beispiel: Teller).
Der Stack wächst bei MIPS nach unten (von hohen zu niedrigeren Speicheradressen)
Ein Stack hat einen Stapelzeiger ("stack pointer"): $sp
Aufgerufenen Prozeduren dürfen keine unbeabsichtigten Nebenwirkungen ("Seiteneffekte") haben.
MIPS Assemblerpsrache
# $s0 = result diffofsums: add $t0, $a0, $a1 # $t0 = f + g add $t1, $a2, $a3 # $t1 = h + i sub $s0, $t0, $t1 # result = (f + g) - (h + i) add $v0, $s0, $0 # Lege Rückgabewert in $v0 ab jr $ra # Rücksprung zum Aufrufer
# $s0 = result diffofsums: addi $sp, $sp, -12 # 3*4 Bytes auf Stack anfordern um drei 32b Register zu sichern sw $s0, 8($sp) # speichere $s0 auf Stack sw $t0, 4($sp) # speichere $t0 auf Stack sw $t1, 0($sp) # speichere $t1 auf Stack add $t0, $a0, $a1 # $t0 = f + g add $t1, $a2, $a3 # $t1 = h + ii) sub $s0, $t0, $t1 # result = (f + g) - (h + add $v0, $s0, $0 # Lege Rückgabewert in $v0 ab lw $t1, 0($sp) # stelle $t1 wieder vom Stack her lw $t0, 4($sp) # stelle $t0 wieder vom Stack her lw $s0, 8($sp) # stelle $s0 wieder vom Stack her addi $sp, $sp, 12 # Platz auf Stack wird nicht mehr benötigt, wieder freigeben jr $ra # Rücksprung zum Aufrufer
proc1: addi $sp, $sp, -4 # Platz auf Stack anlegen sw $ra, 0($sp) # sichere $ra auf Stack jal proc 2 ... lw $ra, 0($sp) # stelle $ra vom Stack wieder her addi $sp,$sp, 4 # Stapelspeicher wieder freigeben jr $ra # Rückkehr zum Aufrufer von proc1
# $s0 = result diffofsums: addi $sp, $sp, -4 # Platz auf Stack für 4 Bytes anlegen, reicht zum Sichern eines Registers sw $s0, 0($sp) # sichere $s0 auf Stack, $t0 und $t1 brauchen nicht erhalten zu werden! add $t0, $a0, $a1 # $t0 = f + g add $t1, $a2, $a3 # $t1 = h + i sub $s0, $t0, $t1 # result = (f + g) - (h + i) add $v0, $s0, $0 # Lege Rückgabewert in $v0 ab lw $s0, 0($sp) # stelle $s0 vom Stack wieder her addi $sp, $sp, 4 # Gebe nicht mehr benötigten Speicher auf Stack frei jr $ra # Rücksprung zum Aufrufer
Hochsprache
int fakultaet (int n) { if (n <= 1) return 1; else return (n * fakultaet (n-1)); }
MIPS Assemblersprache
0x90 fakultaet: addi $sp, $sp, -8 # Platz für zwei Register 0x94 sw $a0, 4($sp) # sichere $a0 0x98 sw $ra, 0($sp) # sichere $ra 0x9C addi $t0, $0, 2 0xA0 slt $t0, $a0, $t0 # n <= 1 ? 0xA4 beq $t0, $0, else # nein: weiter bei else 0xA8 addi $v0, $0, 1 # ja: gebe 1 zurück 0xAC addi $sp, $sp, 8 # Platz wieder freigeben 0xB0 jr $ra # Rücksprung 0xB4 else: addi $a0, $a0, -1 # n = n - 1 0xB8 jal fakultaet # rekursiver Aufruf 0xBC lw $ra, 0($sp) # wiederherstellen von $ra 0xC0 lw $a0, 4($sp) # wiederherstellen von $a0 0xC4 addi $sp, $sp, 8 # Platz wieder freigeben 0xC8 mul $v0, $a0, $v0 # n * fakultaet(n - 1) 0xCC jr $ra # Rücksprung
Wo kommen Operanden für Befehle her?
Aus Register (register operands)
Direktwert aus Instruktion (immediate)
16b Direktwert als Operand verwenden
Relativ zu einer Basisadresse
Adresse eines Operanden im Speicher ist: Basisadresse + Vorzeichenerweiterter Direktwert
Relativ zur nächsten Adresse im Programmzähler
0x10 beq $t0, $0, else 0x14 addi $v0, $0, 1 0x18 addi $sp, $sp, i 0x1C jr $ra 0x20 else: addi $a0, $a0, -1 0x24 jal fakultaet
Auffüllen von entfallenen Bits (mit Nullen und PC+4[31:28])
0x0040005C jal sum ... 0x004000A0 sum: add $v0, $a0, $a1
Im Speicher muss abgelegt werden:
int f, g, y; // globale Variablen int main(void) { f = 2; g = 3; y = sum(f, g); return y; } int sum(int a, int b) { return (a + b); }
.data f: .space 4 # Direktiven für Assembler g: .space 4 # jeweils ein Wort, initialisiert y: .space 4 # auf den Wert 0 .text main: addi $sp, $sp, -4 # Stack Frame anlegen sw $ra, 0($sp) # sichere $ra addi $a0, $0, 2 # $a0 = 2 sw $a0, f # f = 2 addi $a1, $0, 3 # $a1 = 3 sw $a1, g # g = 3 jal sum # Aufruf von sum sw $v0, y # y = sum() lw $ra, 0($sp) # stelle $ra wieder her addi $sp, $sp, 4 # stelle $sp wieder her jr $ra # Rückkehr ins Betriebssystem sum: add $v0, $a0, $a1 # $v0 = a + b jr $ra # return
Diese Spezialregister für Ausnahmebehandlungen liegen außerhalb des regulären Registerfeldes.
Dies lädt den Inhalt des Spezialregisters EPC in das reguläre Register $t0.
Analog funktioniert das auch für Cause.
Der Prozessor speichert bei einer Ausnahme den Grund und den Auftritts-PC in Cause und EPC.
Der Prozessor springt bei der Ausnahmebehandlung an die Adresse 0x80000180.
Die Ausnahmebehandlung funktioniert wie folgt:
Vorzeichenbehaftete und -lose Befehle sind zum Beispiel:
Das u an den Mnemonics steht für unsigned.