|
RECHNERARCHITEKTUR WS 04 - Vorlesung mit Übung
|
Ziel der heutigen Vorlesung ist es, einmal den Softwarerahmen zu klären, innerhalb dessen sich die folgenden Untersuchungen und Übungen abspielen, zum anderen mit der Assemblersprache der virtuellen Maschine IJVM vertraut zu machen. Der nächste Schritt wird dann darin bestehen, die Arbeitsweise der IVJM am Beispiel des Softwaresimulators mic1sim zu illustrieren. Dort wird insbesondere das Zusammenspiel von Microcode und Hardware von Interesse sein.
Es sei aber auch hingewiessen auf einen Nachtrag zur VL2, in dem die in der Praxis bewährten Reduktionsverfahren Karnaugh-Tabellen und Verfahren nach Quine Mc.Clusky kurz vorgestellt werden.
In der letzten Vorlesung wurde ein Zusammenhang zwischen dem logischen Konzept einer universellen Turingmaschine (siehe VL Theoretische Informatik) und einem Beispielrechner hergestellt, der eine virtuelle Maschine IJVM für Java bereitstellt, beschränkt auf den Datentyp integer. Der Beispielrechner liegt als frei erhältlicher Softwaresimulator vor und heisst mic1; mic1 dient auch als Begleitmaterial für das Kap.4 des Buches von [TANENBAUM/GOODMAN 2001]. Der Quellcode mit kompletter Dokumentation findet sich hier.
Um die Orientierung in dem zugegebenermassen nicht mehr ganz einfachen Geflecht von Softwareebenen und Simulationsebenen zu erleichtern, sei hier zu Beginn ein Überblick gegeben.
Benutzte Dateien
In der linken Bildhälfte ist die Implemenierung einer Java Virtual Machine (JVM) dargestellt samt ihren Interaktionen mit der Umgebung. Man kann hier erkennen, dass die JVM normalerweise oberhalb der Ebene des Betriebssystems eines Computers operiert. Sie realisiert die Eigenschaften einer abstrakten Maschine, die für unterschiedliche Betriebssysteme implementiert werden. Das Betriebssystem selbst wiederum operiert in der Regel oberhalb einer Mikroarchitektur, in derem Kern sich ein Mikroprozessor befindet, der neben Registern und arithmetisch-logischer Einheit (ALU) vor allem auch einen Mikrokode besitzt, der das Verhalten der CPU steuert. Der Input einer JVM sind Binärdateien im Bytecodeformat mit dem Suffix '.class'. Diese .class-Dateien werden vom Javacompiler 'javac' (oder einem äquivalenten Programm) aus Java-Quelldateien mit dem Suffix '.java' erzeugt.
Aus dem Bild kann man erkennen, dass der Simulator mic1 selbst eine Java-.class-Datei ist, die von einer JVM ausgeführt wird. Der wichtige Punkt ist jetzt nur, dass der Simulator mic1 inhaltlich eine komplette Mikroarchitektur IJVM simuliert, die einer JVM entspricht, die auf Daten mit dem Format 'int' beschränkt ist.
Der Simulator mic1 bietet dabei die Möglichkeit, parallel zwei verschiedene Kodes zu simulieren:
Einlesen und Simulieren des Mikrokodes der IJVM-Mikroarchitektur aus Dateien mit dem Suffix '.mic1'
Einlesen und Simulieren von Java-Binärcode, der auf Java mit 'int' beschränkt ist, aus Binärdateien mit Suffix '.ijvm'.
In beiden Fällen gibt es Assemblerformate, aus denen heraus die Binärdateien erzeugt werden. Der Java-Assembler-Kode findet sich in Dateien mit dem Suffix '.jas', die vom Assembler 'ijvmasm' (bzw. mit GUI: 'gijvmasm') erzeugt werden, und der IJVM-Assemblerkode findet sich in Dateien mit dem Suffix '.mal', die vom Assembler 'mic1asm' (bzw. mit GUI: 'gmic1asm') erzeugt werden. Im Falle von IJVM-Binärdateien gibt es auch noch einen Dissassembler 'mic1dasm', mittels dessen sich Binärdateien in das IJVM-Mikrokode-Assemblerformat zurückübersetzen lassen.
In der heutigen Vorlesung soll die Struktur der Java-IJVM-Assemblersprache näher untersucht werden und ihre Anforderungen an die Umgebung. Dazu werden möglichst einfache Beispiele betrachtet. Bezogen auf diese einfachen Beispiele soll dann die Arbeitsweise der CPU unter Berücksichtigung des steuernden Mikrokodes betrachtet werden.
Eine Beschreibung der IJVM-Assemblersprache, wie sie in den .jas-Dateien abgelegt wird, findet sich in der Dokumentation zum mic1-Simulator (weitere hilfreiche Erläuterungen gibt ein FAQ-Dokument). Das allgemeine Schema ist im folgenden Bild zusammengefasst:
IJVM-Assembler Sprache Schema
Im ersten Abschnitt einer ijvm-Datei kann ein Konstantenblock stehen. Die hier vereinbarten Variablen sind globale Variablen. Ihre Werte können in folgenden Formaten angegeben werden: hexadezimal (0x)-präfix), octal (0-Präfix) sowie dezimal (kein Präfix).
Es muss dann auf jeden Fall ein Block mit .main folgen. Dieser kann einen Variablenblock (.var ...) enthalten sowie beliebig viele Befehle. Bestimmte Befehle können mit einem Label versehen werden, so dass diese Befehle über diese Labels direkt addressiert werden können. Ferner gibt es einen speziellen Befehl, der andere Methoden aufrufen kann (INVOKEVIRTUAL).
Eine Methode ähnelt in der Grundstruktur einem .main-Block. Im Unterschied zu einem .main-block hat eine Methode aber noch einen individuellen Methoden-Namen und mann kann ihr über Parameter Werte übergeben. Die Kommunikation zwischen dem aufrufenden Block und der aufgerufenen Methode mittels der Parameter geschieht über den Stack. Auch der Return-Wert einer Methode wird über den Stack zurückgegeben.
Hier nochmals die Liste aller IJVM-Assembler-Befehle. In der ersten spalte steht der Hex-Code, in der zweiten spalte ein mnemonischer Code und in der dritten spalte eine kurze Erklärung dessen, was die ser Code tut.
// configuration file for IJVM Assembler 0x10 BIPUSH byte // Push byte onto stack 0x59 DUP // Copy top word on stack; push onto stack 0xA7 GOTO label // Unconditional jump 0x60 IADD // Pop two words from stack; push their sum 0x7E IAND // Pop two words from stack; push Boolean AND 0x99 IFEQ label // Pop word from stack; branch if it is zero 0x9B IFLT label // Pop word from stack; branch if it is less than zero 0x9F IF_ICMPEQ label // Pop two words from stack; branch if equal 0x84 IINC varnum const // Add a constant to a local variable 0x15 ILOAD varnum // Push local variable onto stack 0xB6 INVOKEVIRTUAL offset // Invoke a method 0xB0 IOR // Pop two words from stack; push Boolean OR 0xAC IRETURN // Return from method with integer value 0x36 ISTORE varnum // Pop word from stack; store in local variable 0x64 ISUB // Pop two words from stack; push their difference 0x13 LDC_W index // Push constant from constant pool onto stack 0x00 NOP // Do nothing 0x57 POP // Delete word on top of stack 0x5F SWAP // Swap the two top words on the stack 0xC4 WIDE // Prefix instruction; next instruction has 16-bit index 0xFF HALT // halt the simulator 0xFE ERR // print ERROR and halt 0xFD OUT // Pop a word from the stack and use the low order 8-bits as an ASCI character to display on screen 0xFC IN // Read a character from standard input and put it in the low order 8-bits of a word pushed onto the stack
Das folgende kommentierte Beispiel ist dem FAQ-Dokument entnommen.
// --- Start program --- .constant OBJREF 0x10 .end-constant .main LDC_W OBJREF BIPUSH 0x10 BIPUSH 0x15 INVOKEVIRTUAL DIFF_NEG_YN POP HALT .end-main .method DIFF_NEG_YN (p1,p2) .var diff .end-var ILOAD p1 // Push the first parameter ILOAD p2 // Push the second parameter ISUB // Subtract ISTORE diff // Store the difference in diff ILOAD diff // Push diff IFLT lt // If diff < 0, goto lt BIPUSH 0x4E // else, print "N" OUT GOTO return lt: BIPUSH 0x59 // (diff < 0) OUT // print "Y" return: ILOAD diff // Push diff IRETURN // Return (value of diff will be pushed onto the // top of the invoking method's stack) .end-method // --- End program ---
Nennen wir dies Programm first.jas und lassen es kompilieren:
gerd@kant:~/public_html/fh/I-RA-WS04/I-RA-TH/VL6> java ijvmasm first.jas first.ijvm
Das Ergebnis ist die Datei first.ijvm, die Bytekode darstellt. Diese Datei kann man nun als Input für den Simulator mic1 verwenden:
gerd@kant:~/public_html/fh/I-RA-WS04/I-RA-TH/VL6> java mic1sim mic1ijvm.mic1 first.ijvm
mic1-Simulator am Start
Dabei ist für einen ersten Test wichtig zu wissen, dass das Register PC auf den nächsten auszuführenden Befehl als einzelnem Byte zeigt und das niederwertige Byte des Registers MBR das aktuelle Befehlsbyte bzw. einen zu einem Befehl gehörigen Operanden anzeigt. Die weiteren Anzeigen werden mündlich erklärt (siehe [TANENBAUM/GOODMAN 2001 Kap.4:243-292]).
Ein möglicher Ablauf des obigen Programms:
.main // Start mit main
LDC_W OBJREF // Die Konstante wird gelesen und auf den Stack von main (=stack1) gelegt.
mic1-Simulator mit Befehl 0x13
BIPUSH 0x10
BIPUSH 0x15 // Zwei Werte werden auf den Stack1 gelegt: erst x10 und dann x15.
INVOKEVIRTUAL DIFF_NEG_YN // Dann wird die Methode DIFF_NEG_YN aufgerufen. Bei dem Aufruf werden die beiden obersten Werte vom Stack1 an die Parameter der Methode übergeben: der letzte Parameter p2 bekommt den obersten Wert vom Stack1 (x15), der erste Parameter p1 bekommt den nächsten Wert (x15).
ILOAD p1
ILOAD p2 // die Methode speichert die Werte der Parameter p1 und p2 auf dem Stack der Methode (=stack2).
ISUB // zwei Werte werden vom Stack2 geholt, die Differenz wird gebildet, und das Ergebnis wird wieder auf den Stack2 gelegt.
ISTORE diff // Holt Wert vom Stack2 und speichert ihn in der lokalen Vaiable diff.
ILOAD diff // Schreibe lokale Varibale diff auf den Stack2
IFLT lt // Wenn der Wert kleiner 0 ist, dann verzweige zum Label lt; sonst weiter. x10 - x15 ist kleiner 0, also Verzweigung nach Label lt.
BIPUSH 0x59 // Schreibe Wert x59 (= ASCII für 'Y') auf Stack2
OUT // Hole Wert vom Stack2 und schreibe letzten 8-Bit als ASCII-zeichen in die Ausgabe
ILOAD diff // Schreibe Wert der lokalen Variable diff auf Stack2 (= '-5').
IRETURN // Kehre zur aufrufenden Methode (hier: main) zurück. Der oberste Wert von Stack2 wird als oberster Wert auf Stack1 geschrieben.
POP // Hole obersten Wert von Stack1
HALT // Halte Simulator an.
Versuchen Sie in einem Text Antworten auf folgende Aufgaben zu formulieren:
Schreiben Sie ein kleines Programm mit dem IJVM-Assembler, das folgendes Verhalten zeigt:
Sie können auf der Tastatur eine der Tasten {a,b,c,d,e} drücken. Abhängig davon, welche Taste Sie drücken, druckt das Programm den zugehörigen Grossbuchstaben aus. Dies geschieht in einer Schleife mit 10 Wiederholungen; danach bricht das Programm ab. Versuchen sie einen Methodenaufruf zu benutzen. Kommentieren Sie den Ablauf Ihres Programms