Der MIPS Simulator SPIM


Unterpunkte dieser Seite

Installation unter Ubuntu
Erstes SPIM Program
SPIM Dokumentation
Kommentare in SPIM Quellcode
Aufbau eines Assemblerprogramms
Die Oberfläche von xspim
Die Ausführung eines Programms abbrechen
Debugging
Die MIPS Register
Ein SPIM Programm erneut ausführen
Assembler Direktiven
Arrays mit SPIM
Die main Funktion
CallingConventions
System Calls
Instruktionsreferenz
Die Aufteilung des Speichers in SPIM
Stackframes bzw. Aktivierungsblöcke

Installation unter Ubuntu

Der SPIM-Quellcode kann von der Homepage http://www.cs.wisc.edu/~larus/spim.html heruntergeladen werden. In der zip Datei befindet sich der Quellcode für die Konsolen Version spim und für die GUI Version xspim. Um xspim zu compilieren benötigen Sie die Packete bison, flex und libxaw7_dev, die sich über Synaptic installieren lassen.

Erstes SPIM Program

Geben Sie folgendes Programm in eine Textdatei namens printstr.s ein:
    .option pic2
    .set    noreorder

    .data
    .align  4
hellostr:  .ascii  "Hello, World!\n"

    .text
    .align  4
    .globl main

main:
    # create a simple stack frame
    subu      $sp, $sp, 16
    sw        $31, 0($sp)

    # spim print_str syscall
    li        $2, 4
    la        $4, hellostr
    syscall

    # return
    lw        $31, 0($sp)
    addu      $sp, $sp, 16
    j         $31
    nop
Starten Sie xspim und laden Sie die Datei printstr.s indem Sie auf den load Button klicken. Führen Sie das Program aus indem Sie auf den run Button klicken.

SPIM Dokumentation

Auf der gleichen Seite von der Sie SPIM heruntergeladen haben, finden Sie die SPIM Dokumentation.

Kommentare in SPIM Quellcode

Kommentare in SPIM Programmen sind immer einzeilig und werden mit dem Raute-Zeichen eingeleitet
# Die ist ein einzeiliger SPIM Kommentar

Aufbau eines Assemblerprogramms

Aufbau einer Zeile Assemblercode
<label:> <Instruktion> <Arg1> <Arg2> <Arg3> # Kommentar
Das Label und der Kommentar sind optional. Instruktionen besitzen unterschiedliche Anzahlen an Parametern, daher müssen nicht jedesmal alle Parameter verwendet werden. Die Argumente können durch Spaces, Tabulatoren oder Kommata getrennt werden.
Bestandteile eines Assemblerprogramms
In Van Neumann Rechnern liegen Daten und Programmcode im gleichen Speicher. Man versucht eine Abgrenzung zwischen beiden Teilen zu verwirklichen, indem ein Assemblerprogram aus mehreren sogennanter Segmente besteht. Es gibt ein Datensegment, ein Textsegment und ein Stacksegment für den Stack.
In einem Assemblerprogram leitet die .data Direktive das Datensegment und die .text Direktive das Textsegment ein.
Ein minimales Programm deklariet den Inhalt seines Datensegments, den Inhalt seines Textsegmentes und verwendet innerhalb des Textsegmentes das Label main. Dieses Label spielt eine spezielle Rolle. Wenn SPIM ein Programm ausführt, so ist der Einsprungpunkt in das Programm durch das Label main festgelegt. Falls das Label main fehlt, kann der Simulator das Programm nicht ausführen.
.data 
  x:  .word 12
  y:  .word 14
  z:  .word 5
  u:  .word 0
.text
main: lw $t0, x           # $t0 := x
      lw $t1, y           # $t1 := x
      lw $t2, z           # $t2 := x
      add $t0$, t0$, t1   # $t0 := x+y
      add $t0, $t0, $t2   # $t0 := x+y+z
      sw $t0, u           # u := x+y+z
      li $v0 10           # EXIT

Die Oberfläche von xspim

HINWEIS: Das Fenster, das der WindowManager beim Start von xspim öffnet ist nicht breit genug um die gesammte Ausgabe in den Textbereichen von spim darzustellen. Sie sollten das xspim Fenster auf jeden Fall verbreitern. (Auf dem unteren Screenshot ist das Fenster nicht verbreitert)

Kein Bild gefunden. Tja

Jede Zeile des Textsegmentes ist nach einem einheitlichen Schema aufgebaut:
[Speicheradresse der Instruktion] (Befehlscode) (Mnemonic des Befehls) ; (Zeilennummer in der Quellcodedatei) (Befehl in der Quellcodedatei)
Jeder Eintrag in einer solchen Zeile, der hinter dem Komma steht, bezieht sich auf die Quellcodedatei, in der das Programm steht, das gerade von SPIM simuliert wird. Hinter dem Semikolon ist die Programmzeile angegeben in der der Befehl steht und hinter der Zeilennummer ist der Quellcode angegeben, der die gesammte Zeile im Textsegement verursacht hat.
Falls hinter einem Semikolon einer Zeile nichts steht, bedeutet das, das die Zeile aus einer Pseudoinstruktion entstanden ist, die SPIM in mehrere Zeilen im Textsbereich übersetzt hat.
Die Adresse der Instruktion und der Befehlscode sind Zahlen im Hexadezimalsystem.

Die Ausführung eines Programms abbrechen

Um die Ausführung eines laufenden Programms abzubrechen (falls es eine Endlosschleife darstellt) bewegt man den Cursor über das xspim Fenster, so das dieses den Eingabefocus erhällt, und führt Strg+c aus.

Debugging

Single-Stepping
Ein Program kann von SPIM während der Simulation angehalten werden. SPIM wartet nun auf Eingaben des Benutzers. Man kann SPIM anweisen eine Zeile auszuführen und danach wieder anzuhalten. Dies wird Single-Stepping genannt. Dabei erhällt man die Chance die Registerinhalte zu untersuchen und somit Fehler des Quellcodes ausfindig zu machen.
Um Single-Stepping durchzuführen, laden Sie ein Programm und betätigen nicht den run Button sondern den step Button. Wenn Sie den step Button angeklickt haben, öffnet sich ein Dialog, der unter anderem einen continue Button enthällt. continue verlässt Single-Stepping und führt das Programm ohne Anhalten am Stück aus.
Breakpoints
Single-Stepping versagt in zwei Situationen:
  1. Sehr große Programme mit vielen Anweisungen können Fehler enthalten, die erst nach 1000 Anweisungen auftreten. Single-Stepping (auch in größeren Schrittweiten als eine Anweisung pro Step) ist hier umständlich.
  2. Ein Programm kann zwei oder mehr Fehler enthalten (Ja, solche Programme solls geben !). Falls der nach dem ersten Fehler continue gedrückt wird, rauscht das Programm in den zweiten Fehler rein. Man muss also zwischen je zwei Fehlern Single-Stepping machen, was nicht praktikabel ist.
In diesen zwei Fällen sind Breakpoints einsetzbar. Breakpoints werden auf Zeilen im Textsegment von SPIM gelegt und SPIM hällt das Programm an, sobald es auf eine Zeile mit Breakpoint stösst. Dabei wird die Ausführung angehalten bevor die betreffende Zeile mit dem Breakpoint ausgeführt wird. Somit können ausgewählte Stellen im Programm angesprungen werden. Dies setzt vorraus, das man weis wo der Fehler liegt und darin liegt der Nachteil von Breakpoints. Der Fehler muss eingegrentzt sein, bevor man Breakpoints sinnvoll im Code setzen kann.

Um einen Breakpoint auf eine Zeile im Textbereich zu setzen müssen Sie zunächst die Adresse der Zeile nachschauen oder, falls ein Label existiert, den Labelnamen der Zeile feststellen. Danach drücken Sie auf den Button breakpoints. Um Breakpointdialog, der sich daraufhin öffnet können Sie nun die Adresse oder den Labelnamen einsetzen und den Breakpoint mit add in den Textbereich einsetzen.

Ein SPIM Programm erneut ausführen

Nachdem SPIM ein Programm simuliert hat, stehen die Werte der Ausführung im Speicher und sind dort vorhanden bis sie überschrieben werden. Es besteht die Gefahr das eine erneute Ausführung durch den Speicherinhalt den die vorherige Ausführung zurückgelassen hat irritiert wird. Daher ist es unbedingt notwendig den Speicher und die Register zu löschen.
Um ein Programm in SPIM erneut zu simulieren, gibt es den Button clear. Wenn man auf diesen Button klickt und die linke Maustaste gedrückt hällt, öffnet sich ein DropDown-Menu indem man memory & registers auswählen kann. Damit wird ein Reset des SPIM Simulators durchgeführt. Ein Program kann nun bedenkenlos erneut ausgeführt werden.

Assembler Direktiven

Assemblerdirektiven stehen im Quellcode und steuern die Arbeitsweise des Assemblers. Sie beeinflussen also die Art und Weise in der der Assembler den Quellcode übersetzt. Das bedeutet Assembler Direktiven werden nicht in Maschinenbefehle übersetzt. In diesem Punkt unterscheiden sich Assemblerdirektiven von Instruktionen und Pseudoinstruktionen.

Übersicht Assemblerdirektiven
Direktive Wirkung
.align n .algin n beeinflusst die Speicherausrichtung. Mit .align n wird jedes Datum in einen eigenen 2^n Byte großen Block in den Speicher abgelegt. .align 4 legt alle Daten in ein eigenes 32 Bit Wort ab.
.data <addr> Alle auf .data folgenden Objekte sollen in das Datensegment gelegt werden. Falls zusätzlich eine Adresse <addr> angegeben ist, wird der Assembler die Daten an diese Adresse ins Datensegment ablegen.
.option ???
.text <addr> Alle auf .text folgenden Objekte werden vom Assembler ins user text segment abgelegt. In das user text segment dürfen nur Instruktionen, Pseudoinstruktionen oder .word Direktiven abgelegt werden. Falls eine Adresse <addr> angegeben wird, wird der Assembler die Daten an diese Adresse ins user text segment legen.

Arrays mit SPIM

1. You should need to reserve memory space in your data segment to store 7 arrays. The initial values of these arrays are not important. A useful construct in SPIM is ".space" directive. To use it, you need to know how big the size you will need for each array.

2. You will need to load/store array elements from data segment. So be careful with calculating the addresses. Choose a simple and flexible addressing mode, say "($register)" or "label($register)" to access your data. Since there is no concept of variables in SPIM, you need to keep track of the value you compute in different registers. Writing a lot comments (start with an '#') is always helpful.

There are many different ways of doing this, these are only my suggestions. It is perfectly fine to come up with your own solution. If you have bizzare errors, use the "step" option in SPIM interface to debug. Debugging through steps is a precious way of learning assembly language.

1. A way to define 7 arrays for our purpose is:
	.data
array0:	.space (array size in bytes)
array1: .space (array size in bytes)

	......
array6: .space (array size in bytes)
	......
	.text
You need to know the size (in bytes) of each array to do it. Be aware of the alignment problem. Make sure all arrays start at address mod 4 = 0.

2. You could use "Jump Table" approach in page 115 if you have not figured out how to do that. On page 116 there is a MIPS assembly translation of the switch statement. Be careful with the initialization of the jump table.

In the high-level abstraction of your code, your algorithm may have the following form:
	r = (2*X mod 7);
	switch (r) {
	case 0:
		sw X into array0;
		break;
	case 1:
		sw X into array1;
		break;
	........
	........
	case 6:
		sw X into array6;
		break;
	}
You might want to define/initialize your jump table in this way:
        	.data
        	......
JumpTable: 	.space 28		# why 28?
        	......
        	.text
		......
		......
		la	$10, L0		#loading address of label L0 into $10
					# L0 corresponds to the address of case 0
		sw	$10, JumpTable 	# L0 goes to the first entry of table
		la	$10, L1 	#loading address of label L1 into $10
                                        # L1 corresponds to the address of case
1

		sw	$10, JumpTable+4  # L2 goes to the second entry of table
		.......                 # do so for all seven labels

Die main Funktion

Der Aktivierungsblock
Der Aktivierungsblock der main Funktion befindet sich in den ersten Zeilen des Textsegmentes man erkennt ihn an dem . Hier werden die Anzahl der Kommandozeilenargumente argc in Register geladen und die angebenen Argumente selbst argv wreden ebenso in Register geladen.

CallingConventions

Allgemeines
Sobald eine Funktion aufgerufen wird, die Parameter besitzt, muss der Aufrufen die Parameter in bestimmte Register oder auf dem Stack ablegen. Die aufgerufene Funktion wird die Daten aus bestimmten Registern abholen. Die Kommunikation läuft also über Register. Es muss Konventionen geben, in welchen Registern, die aufgerufene Funktion Daten abholen soll und wohin sie Rückgabewerte schreibt. Solche Konventionen werden CallingConventions genannt.
StackFrames
Ein Frame besteht aus dem Speicher zwischen dem Framepointer R30 $fp und dem Stackpointer R29 $sp. Der Framepointer zeigt auf das Wort hinter dem zuletzt auf den Stack (innerhalb dieses Stackframes) abgelegten Parameter und der Stackpointer zeigt auf das erste Wort des Stackframes. Damit umgeben $fp und $sp einen Frame. In Unix Systemen wächst der Stack von niederen Adressen in Richtung höherer Adressen. Damit hat der Framepointer einen großeren Wert als der Stackpointer. Der Stack wächst in Richtung des Datensegments in dem die Daten des Programms abgelegt sind.
Ein Funktionsaufruf
  1. Die ersten vier Parameter werden in die Register R4 - R7 (= $a0 - $a3) abgelegt. Falls es weitere Parameter gibt, müssen diese auf den Stack gelegt werden.
  2. Die Registerinhalte des Aufrufers müssen gesichert werden, falls diese von interresse sind. Das bedeutet die Register R8 - R15, R24, R25 (= $t0 - $t9) müssen gesichert werden.
  3. eine jal Instruction (= unconditionally jump to instruction at target) muss ausgeführt werden.
Innerhalb einer aufgerufenen Funktion
  1. establish the stack frame by subtracting the frame size from the stack pointer (Was in aller Welt soll das bedeuten ???)
  2. Save the callee-saved registers in the frame. Register $fp is always saved. Register $ra needs to be saved if the routine makes calls. Any of the registers $s0-$s7 that are used by the callee need to be saved.
  3. Estbalish the frame pointer by adding the stack frame size -4 to the address in $sp
Aus einer aufgerufenen Funktion returnen
  1. Restore any callee-saved registers that were saved upon entry (including the frame pointer &fp)
  2. Pop the stack frame by adding the frame size of $sp.
  3. Return by jumping to the address in the register &ra

System Calls

Um einen System Call durchzuführen, laden Sie den Code des SystemCalls in das Register $v0 = R2 und die Argumente in $a0-$a3 = R4-R7.

Die Aufteilung des Speichers in SPIM

Jedes SPIM Program bekommt einen Speicherbereich (= user address space) zugeteilt. Dieser Speicherbereich enthällt das Datensegment, das Textsegment, in dem der Quellcode liegt, und dieser Speicherbereich enthällt auch den Stack. Der Speicherbereich beginnt bei der niedrigsten Adresse 0x00000000. Von 0x00000000 an bis zur Adresse 0x00400000 befindet sich ein reservierter Bereich, in dem man nichts tun darf. Das Textsegment beginnt dann an der Adresse 0x00400000. An dieser Adresse beginnt das Textsegment (= user text segment), das den Quellcode enthällt. Über dem Textsegment beginnt ab der Adresse 0x1000000 das Datensegment. Ganz oben im Speicherbereich an der höchsten Adresse 0x7FFFFFFF beginnt der Stack. Er wächst in Richtung der niederen Adressen im Speicherbereich, also in Richtung des Daten- und des Textsegmentes. Wenn man einen neuen Stackframe erzeugen will muss man also den Stackpointer verkleinern, so das der Stack in Richtung der kleineren Adressen wächst.

Falls man in seinem SPIM Program die Assemblerdirektiven .word usw. verwendet hat um im .data segment Variablen zu deklarieren, so kann man bei der Ausführung des Programmes ab der Adresse 0x10000000 im Speicher nachsehen. Dort wird man die Werte vorfinden, die man im Quellcode deklariert hat.

Das Register $sp zeigt nach dem Start eines SPIM Programmes auf

Des weiteren gibt es noch die global memory area. Nach dem Start eines SPIM Programms ist das Register $gp automatisch mit der Adresse der global memory area gefüllt. In der global memory area kann man Speicher gebrauchen falls man Speicher benötigt.

Stackframes bzw. Aktivierungsblöcke

Der Stack liegt im User Addressspace also im gleichen Speicherbereich wie das Data- und das Textsegment. Der Stack beginnt an der höchsten Adresse 0x7FFFFFFF und wächst von oben herab in Richtung der niederen Adressen und auf das Datasegment zu.

MIPS kennt keine Push und Pop Befehle. Man muss Daten an die Adresse des stack pointers mit dem mov Befehl kopieren und dann den stack pointer erniedrigen um push zu simulieren. Um pop zu simulieren muss man Daten mit mov und der Adresse im stack pointer vom Stack lesen und den stack pointer dann erhoehen.

Ein Stackframe liegt auf dem Stack und schafft einen lokalen Sichtbarkeitsbereich für die Hauptfunktion und jedes Unterprogramm, das in der Hauptfunktion aufgerufen wird. Die Hauptfunktion legt einen Stackframe auf dem Stack an und baut diesen Stackframe wieder ab, nachdem sie alle ihre Aufgaben erledigt hat und das Program terminieren soll. Falls innerhalb der Hauptfunktion ein Unterprogramme aufgerufen wird, so legt dieses Unterprogram einen Stackframe für sich selbst an und baut diesen auch wieder ab nachdem es mit seinen Aufgaben fertig ist. Man beachte, das der Stackframe des Unterprogrammes innerhalb des Stackframes des Hauptprogramms liegt. Falls das Unterprogram selbst wieder ein Unterprogram aufruft, so wird dieses erneut einen Stackframe innerhalb des umgebenden Stackframes anlegen usw.

Ein Stackframe wird durch zwei MIPS Register mit besonderen Aufgaben beschrieben. Das sp Register (stack pointer) und das fp Register (frame pointer). Sie zeigen auf Adressen auf dem Stack. Der frame pointer beschreibt den Ort an dem der Stackframe an den umgebenden Stackframe angrenzt. Er befindet sich also an der höchsten Adresse des Bereichs den der Stackframe einnehmen wird und markiert sozusagen den Anfang des Stackframes. Der stack pointer markiert das Ende des Stackframes, an dem der Stackframe noch in Richtung Datasegment wachsen könnte. Der stackpointer befindet sich also an einer niedrigeren Adresse als der frame pointer. Während der framepointer seine Adresse erst ändert, wenn der Stackframe abgebaut wird, kann der stack pointer seine Adresse ändern falls der Stack verkleinert werden muss (stack pointer wird dann erhöht) oder falls das Unterprogram noch mehr Platz auf dem Stack benötigt (stack pointer wird dann erniedrigt).

Da nur zwei Register (sp und fp) vorhanden sind um alle Stackframes (der des Hauptprogrammes und alle Stackframes aller Unterprogramme) zu beschreiben, muss der Inhalt der sp und fp Register zwischengespeichert werden, bevor sie einen neuen Stackframe beschreiben können. Speichert man sp und fp nicht, so hat man die Information überschrieben, die einem sagt wo der Übergeordnete Stackframe auf dem Stack liegt.
MIPS calling conventions
Falls man ein Unterprogram aufrufen möchte, so muss man die Parameter für das Unterprogram auf den Stack ablegen und die Register sichern, die per Konvention Calle-saved sind. Der Caller muss sich nicht darum kümmern, das sein Stackframe (also sp und fp) gesichert sind. Der Caller kann erwarten, das nach dem Unterprogramaufruf, der Stack genauso aussieht wie er ihn hinterlassen hat, bevor er das Unterprogramm aufgerufen hat.
Der Callee muss dagegen den frame pointer inhalt auf dem Stack sichern, dann kann er den frame pointer mit einer neuen Adresse laden um einen neuen Stackframe zu beginnen. Er kann dann auch den stack pointer verschieben und damit die Größe des stackframes festlegen. Außerdem muss der Callee die Register sichern, die per Konvention Callee-saved sind.
Beim beenden muss der Callee die Register wieder herstellen und den stack pointer und den frame pointer wieder in den ursprünglichen Zustand zurückversetzen.
  1. Der Aufrufer schreibt die Parameter in umgekehrter Reihenfolge auf den Stack, so das zuerst der letzte Parameter gepusht wird und zuletzt der erste Parameter gepusht wird. Das bedeutet, der framepointer zeigt auf den letzten Parameter, da der letzte Parameter zuerst gepusht wurde (Parameter in umgekehrter Reihenfolge auf den Stack). Alternativ dazu kann man auch die ersten vier Parameter in die Register $a0 bis $a3 ablegen und falls es mehr als vier Parameter gibt werden diese dann in umgekehrter Reihenfolge auf den Stack gepusht. Der frame pointer zeigt dann auf gar nichts (falls es weniger als fünf Parameter gibt) oder er zeigt auf den letzten Parameter, falls es mehr als vier Parameter gegeben hat.
  2. Die caller-saved register ($t0 - $t9) werden auf den Stack gepusht. Dabei wird $t9 zuerst und $t0 zuletzt gepusht.
  3. jal aufrufen (jal = unconditionally jump to the instriction at the label or whose address is in the Register Rsrc. Save the address of the next instruction into the Register $31.)



zum Seitenanfang
zur Hauptseite

Letzte Änderung: 20.12.2005