1 Transskript zur Lektion 8.3 Code Riffs Karl C. Posch, 2. März 2011 Vorbemerkung Dieser Text ist aus einem Transskript der Video Lektion 8.3 zur Lehrveranstaltung Rechnerorganisation bzw. zur Lehrveranstaltung Rechnernetze und Organisation an der Technischen Universität Graz entstanden. Der Text ist deshalb in großen Teilen ohne die visuelle Unterstützung durch diese Video Lektionen nur schwer verständlich. Ich rate deshalb dringend an, als primären Lese Stoff die Lektion selbst zu sehen. Ich mache diesen Text Studierenden zugänglich, damit diese beim Durcharbeiten der Lektion und bei Gesprächen über den Lernstoff Bezugspunkte besitzen, welche auch in ausdruckbarer Form vorliegen. Nicht unerwähnt sollen als zusätzliche Wissensbasis die Dateien mit den Quell Codes bleiben, auf welche in den Lektionen immer wieder verwiesen wird. Dabei sind vor allem die C Quell Codes interessant. Karl C. Posch Graz, 2. März 2011 Materialien am Web: http://www.iaik.tugraz.at/content/teaching/bachelor_courses/rechnerorganisation/ 1
2 8.3 Code Riffs In der dritten Lektion dieses Teils möchte ich mit dir über Code Riffs sprechen. Werfen wir zunächst einen Blick auf unseren TOY Computer. Wir haben in der vorigen Lektion begonnen, ein Programm zu entwickeln, welches Daten vom Eingang liest, diese dann sortiert, und danach am Ausgang out ausgibt. Wir sind dabei soweit gekommen, dass die Daten nach dem Einlesen nun im Hauptspeicher ab Adresse 0x40 zu finden sind. Einen Pointer auf diese Adresse 0x40, also auf die Adresse des nullten Elements unseres Array, haben wir im CPU Register RE. Die Länge des Array befindet sich im Register RF. Das gesamte Programm besteht aus folgenden Schritten. Einlesen der Anzahl der zu sortierenden Elemente. Einlesen der Elemente Sortieren der Elemente Und schließlich Ausgabe der sortierten Liste Wie gesagt: Die ersten beiden Schritte haben wir bereits in der vorigen Lektion studiert. Bevor wir jetzt den Sortieralgorithmus genauer ansehen, möchte ich mit dir über sogenannte Code Riffs sprechen. Ich bezeichne damit immer wiederkehrende Programm Sequenzen in C. Deren Umsetzung 2
3 in Assemblersprache solltest du verstehen. Im Prinzip handelt es sich hier um eine Art Wiederholung der Essenz der vorigen Lektion. Dort haben wir zum Beispiel folgenden Riff kennen gelernt: Die Übersetzung einer For Schleife in eine While Schleife. Eine For Schleife besteht aus der Initialisierung einer Laufvariablen hier trägt diese den Namen i. Wir setzen also z.b. i gleich 0. Dann folgt die Überprüfung der Schleifen Bedingung. Diese heißt hier i < length. Und schließlich wird nach dem Durchlauf des Body der For Schleife diesen habe ich hier nicht dargestellt die Laufvariable verändert; hier also etwa i++. Eine For Schleife ist also lediglich eine komfortable Darstellung der etwas längeren Beschreibung mit einer While Schleife. Man spricht auch von syntactic sugar. Gehen wir mit dieser Idee noch ein Stück weiter. Eine While Schleife ist identisch mit einer If Anweisung mit abschließender Goto Anweisung zum Start der If Anweisung. Hier habe ich den Start der If Anweisung mit dem Label L0 bezeichnet. Auch dieser Code ist gültiger C Code, wenngleich es verpönt ist, in C Programmen Goto Anweisungen zu verwenden. Ich möchte dich hier jedoch von der relativ hohen Abstraktionsebene der Sprache C auf niedrigere Betrachtungsebenen bringen. Am Ende dieses Weges werden wir TOY Assemblerbefehle vorfinden und zwar mit Hilfe von C dargestellt. 3
4 Sehen wir uns die Bedingung der If Anweisung mal genauer an. Es steht hier i kleiner length. Solche Bedingungen lassen sich immer so umformen, dass eine Abfrage gleich 0 oder größer 0 vorkommt. Die Bedingung i kleiner length ist ja identisch mit der Bedingung length i > 0. Und noch ein Stück weiter: Wir könnten ja auch eine Hilfsvariable x einführen. Diese kriegt zuerst die Differenz von length und i. Und sodann wird die If Bedingung noch einfacher: if x > 0. Solche einfachen If Anweisungen sind in TOY Assemblersprache verfügbar. Der Befehl Branch on Zero stellt einen bedingten Sprung dar. Damit können wir die C Anweisung if (R4==0) goto Label0 direkt in den rechts stehenden Assemblerbefehl übersetzen. Manches Mal will man bedingungslos springen. In der Sprache C würden wir sagen goto Label1; Der dazu passende Assemblerbefehl ist BZ R0, Label1; Mit diesem Wissen sind wir in der Lage, jede For Schleife, jede While Schleife und jede If Bedingung in Assemblersprache zu formulieren. Sehen wir uns weitere Code Riffs an. Betrachten wir Datenfelder, also Arrays. 4
5 In C kann man ein Datenfeld z.b. so deklarieren: int array[5]. int drückt den Datentyp aus, array stellt einen symbolischen Namen dar, und mit 5 bezeichnen wir die Anzahl der Array Elemente. Im Speicher sollen also 5 Datenwörter reserviert werden. Ich gehe dabei von der einfachen Annahme aus, dass der Datentyp int in einem Speicherwort darstellbar ist. Also aufpassen: TOY hat Speicherwörter zu je 16 Bit. Mein Datentyp int hier hat also eine Größe von 16 Bit. Auf deinem PC ist dies wohl anders da hat der Datentyp int wohl eher 32 Bit. Der symbolische Name des reservierten Speicherplatzes heißt hier array. Im Speicher wollen wir die Startadresse als array bezeichnen. Wenn ich also in C schreibe: &array[0] oder anders gelesen Adresse des nullten Elements von array, dann könnte ich auch einfach nur array sagen. Denn das ist ja die Adresse des nullten Elements. Auf gleiche Art und Weise kann ich die Adresse des ersten Elements des Array mit array + 1 bezeichnen. Und die Adresse des 2. Elements mit array + 2 und so weiter. Aufpassen: man kann auch array + 7 sagen. Auch diese Speicherstelle existiert vermutlich. Doch der Zugriff auf diese Speicherstelle ist nicht geplant. Doch was dann, wenn er trotzdem stattfindet? Geplant oder ungeplant. Vermutlich 5
6 stößt du dabei auf jede Menge Überraschungen. Doch bleiben wir vorerst auf dem tugendhaften Pfad. Wir verwenden jetzt drei weitere Speicherwörter: R2, R3 und R4. Du kannst dir darunter TOY CPU Register vorstellen, aber nicht notwendigerweise. Wenn wir über diese drei Speicherwörter sprechen wollen, dann können wir in C vielleicht so schreiben: int R; Also R2 ist der symbolische Name für das Datenwort in R2. int *R3; Damit sagt man in der Sprache C, dass der Wert der symbolischen Variablen R3 eine Adresse darstellt. Oder mit anderen Worten: R3 ist ein Pointer auf ein Speicherwort. Dort befindet sich ein Datum vom Datentyp int. In unserer einfachen TOY Welt hat der Datentyp int ja eh genau 16 Bit; er kann also in einem einzigen Speicherwort dargestellt werden. Und dann int R4; R4 ist also der symbolische Name für das dritte Speicherwort. Sehen wir uns folgende C Zuweisung an: Das erste Element des Array soll den Wert 4 kriegen. Wir könnten dies unter Zuhilfenahme der Variablen R2 auch so formulieren: Zuerst kriegt die symbolische Variable R2 den Wert 4. Und dann wird der Wert von R2 dem ersten Element des Array zugewiesen. Bildlich sieht das so aus: Der Wert des Speicherworts mit dem symbolischen Namen R2 wird 4. Und danach wird dieser Wert auf die symbolische Adresse array + 1 kopiert. 6
7 Wir können diesen Sachverhalt auch so darstellen: R1 kriegt den Wert 1. R2 kriegt den Wert 4. R3 kriegt den Wert der symbolischen Variablen array. Hier haben wir also in R3 die Adresse des nullte Array Elements. R3 wird sodann um 1 erhöht. Jetzt zeigt R3 also auf das erste Array Element. Und schließlich soll das Datum, auf welches die Pointer Variable R3 zeigt, den Wert von R2 kriegen. Damit haben wir ebenfalls das erste Array Element mit der Zahl 4 beschrieben. Sehen wir uns als Nächstes die Übersetzung der 5 C Anweisungen in TOY Assemblercode an. Die ersten 4 davon sind einfach: Lade die Variable mit dem symbolischen Namen R1 mit dem Datum 1. Oder wir sollten jetzt wohl genauer werden: Lade das Register R1 mit dem Datum 1. Dann: Lade R2 mit dem Wert 4. Lade R3 mit dem Wert der symbolischen Variablen namens array. Und schließlich: Addiere zum Wert im Register R3 den Wert vom Register R1. Der letzte Befehl ist vielleicht der am schwierigsten zu verstehende: Es ist ein indirekter Store Befehl: Store, also speichere den Wert des Registers R2 auf die Speicherstelle mit der Adresse, welche sich in R3 befindet. 7
8 Hier nochmals in einer anderen Schreibeweise. Damit haben wir das Beschreiben eines Array Elements im Detail untersucht. Wie sieht dies mit dem Lesen eines Array Elements aus? Betrachten wir dieses wiederum in der Sprache C formulierte Beispiel: R4 kriegt den Wert des ersten Array Elements. Das kann man etwas umständlicher, jedoch mit einfacheren Schritten formuliert, so sehen: 8
9 R1 kriegt den Wert 1. R3 kriegt den Wert der symbolischen Variablen namens array. Sodann wird R3 um 1 erhöht. Und schließlich wird das durch R3 adressierte Speicherwort nach R4 kopiert. Also R4 ist gleich Stern R3. Damit sind wir wieder in der Lage, jede C Codezeile direkt in einen TOY Assemblerbefehl zu übersetzen. Der letzte dieser 4 Befehle ist dabei wiederum der interessanteste: Es ist ein indirekter Ladebefehl. In Register Transfer Sprache soll das Datum im Speicher, auf welches der Wert von R3 zeigt, in das Register R4 kopiert werden. Damit haben wir recht detailliert den Zusammenhang zwischen Array, Array Elementen, Pointer und Adressen untersucht. Wenden wir uns unserem Beispielprogramm zu. Wir wollten ja ein Array einlesen, die Werte dieses Array dann sortieren und schließlich die sortierten Werte ausgeben. Einen Teil dieses Beispielprogramms haben wir bereits in der vorigen Lektion erledigt. 9
10 Kommen wir also zum Sortieren. Wir wollen den Sortieralgorithmus Bubblesort verwenden. Ich habe hier das Modell unseres Programms als C Quellcode formuliert. Du findest diesen Code in der Datei in_sort_out1.c". Dieser besteht aus der Eingabe der Daten. Diesen Teil haben wir bereits in der vorigen Lektion besprochen. Und dann aus dem Sortieralgorithmus selbst. Es handelt sich um 2 verschachtelte For Schleifen. Im Kern der inneren For Schleife befindet sich eine If Anweisung. Sehen wir uns den Kern des Sortieralgorithmus einmal genau an: 10
11 Es werden zwei benachbarte Array Elemente verglichen und falls das mit dem kleineren Index größer ist als dasjenige mit dem größeren Index, dann werden die beiden Array Elemente vertauscht. Genauer gesagt: Es werden die beiden Werte im Speicher vertauscht. Sehen wir uns dieses Vertauschen mal genauer an. Wir könnten dies ja so formulieren: Zuerst kopieren wir das Array Element mit dem Index j 1 in die Variable R6. Dann kopieren wir das Array Element mit dem Index j in die Variable R7. Wir lesen also zwei Array Elemente. Als Nächstes bilden wir die Differenz, also R7 minus R6, und speichern diese Differenz in der Variablen R8. Wenn R8 größer 0 ist, also wenn der Wert in R7 größer war als der Wert in R6, dann vertauschen wir die beiden Werte sonst nicht. Das Vertauschen geht so: Wir schreiben auf das Array Element mit dem Index j 1 den Wert von R7 und auf das Array Element mit dem Index j den Wert von R6. 11
12 Doch kommen wir zurück zum gesamten Bubblesort Algorithmus. Es sind ja 2 verschachtelte For Schleifen. Die innere For Schleife hat die Laufvariable j. j läuft von 1 bis i 1 und wird in jedem Schleifendurchlauf inkrementiert. Wir vereinfachen diese Schleifenbedingung. Statt j < i schreiben wir i j > 0. Diese Bedingung ist identisch. Und da wir den C Code ja exekutieren können, können wir uns dessen auch vergewissern. Einfach kompilieren und ausprobieren. Die beiden Dateien heißen in_sort_out1.c und in_sort_out2.c und befinden sich im Unterverzeichnis Version8.5. OK. Massieren wir den C Code weiter. 12
13 Die äußere For Schleife hat die Laufvariable i. i läuft von length bis 2 und wird in jedem Durchlauf um 1 erniedrigt. Wir ersetzen diese For Schleife mit einer äquivalenten While Schleife. Dies kennst du ja schon: Wir setzen zuerst i gleich length. Dann kommt die While Schleife, in deren Kern wir am Ende immer die Laufvariable i entsprechend verändern müssen. In diesem Fall also dekrementieren müssen. OK. 13
14 Wir machen das Gleiche mit der inneren While Schleife: j kriegt zuerst den Wert 1. Dann die While Schleife. Und im Kern der While Schleife dürfen wir auf das Inkrementieren der Laufvariablen j nicht vergessen. OK. 14
15 Kommen wir zu While i größer 1. i wird in jedem Schleifendurchlauf dekrementiert. Wenn i also den Wert 1 kriegt, dann fliegen wir aus der While Schleife hinaus. Dies können wir so formulieren: if (i 1 == 0) goto L1. Und am Ende dürfen wir das goto L0 nicht vergessen. Das Label L1 befindet sich am Ende unseres Codes. Wir haben jedoch noch ein paar weitere Veränderungen vorzunehmen. 15
16 Sehen wir uns die zweite While Schleife an. Die Bedingung lautet hier: i j > 0. Auch diese Schleife können wir in eine If Anweisung mit Goto Anweisungen umbauen. Die Laufvariable dieser Schleife ist j. Diese wird in jedem Durchlauf inkrementiert. i j wird demnach in jedem Schleifendurchlauf um 1 kleiner. Solange i j > 0 ist, wird der Kern der Schleife ausgeführt. Wenn jedoch und jetzt spreche ich über die Bedingung der If Anweisung wenn jedoch i j == 0 wird, dann springen wir zum Ende der Schleife, also zum Label L3. Wenn jedoch die Schleife ausgeführt wird, dann wollen wir am Ende der Schleife wieder zurück zu Label L2 gehen. Also: goto L2. Diese Quellcode Variante findest du in Datei in_sort_out6.c. Am besten selbst ausprobieren und sicher stellen, dass alles so ist, wie ich es erläutere. Als Nächstes wollen wir uns diese If Bedingung ansehen: Es werden zwei benachbarte Array Elemente der Größe nach verglichen. Je nach Ergebnis dieses Vergleichs werden die Array Elemente vertauscht oder eben nicht vertauscht. 16
17 Hier eine identische Umformung dieses Sachverhalts. Zuerst lesen wir beide Array Elemente in die beiden Variablen R6 und R7. Dann führen wir die Subtraktion R7 minus R6 aus und speichern das Ergebnis in R8. Sodann können wir eine einfache If Bedingung formulieren: Wenn R8 größer als Null ist, dann vertauschen wir nicht also dann springen wir zum Label L4. Wenn die Goto Anweisung nicht ausgeführt wird, dann kopieren wir die Werte von R6 und R7 in umgekehrter Reihenfolge zurück auf die Array Elemente. Du kannst dir wohl vorstellen, dass wir mit den Variablen R6, R7 und R8 CPU Register meinen. Wir haben also vor, die Array Elemente vom Hauptspeicher in zwei CPU Register zu kopieren, diese dann dort mittels Subtraktion zu vergleichen, und dann bei entsprechendem Resultat des Vergleichs die Werte in vertauschter Reihenfolge zurück in den Hauptspeicher zu kopieren. 17
18 Diese Version des Quellcodes findest du in Datei in_sort_out7.c. Jetzt wollen wir statt der Variablen i, j und length die CPU Register R2, R9 und RF verwenden. Von der Variablen length wissen wir ja schon aus der vorigen Lektion, dass wir diese im CPU Register RF abgelegt haben. Für i hatten wir dort ja auch schon R2 verwendet. j ist neu. R9 wird noch nicht verwendet, also nehmen wir dieses Register eben. OK. Ein paar Umformungen fehlen noch: 18
19 if (R2 1 == 0) goto Lout Diese If Anweisung zerlegen wir in die beiden Anweisungen, welche du rechts oben in Grün eingerahmt siehst: Zuerst ziehen wir von R2 die Konstante 1 ab. Und dann haben wir eine einfache If Bedingung, welche sich direkt in einen TOY Maschinenbefehl übersetzen lässt. Oder hier: if (R2 R9 == 0) goto Lsort3 Wir berechnen zuerst R2 minus R9 und speichern die Differenz in RA. Dann entsteht eine einfache If Bedingung, nämlich if (RA == 0) Diese lässt sich wiederum in einen TOY Maschinenbefehl übersetzen. Jetzt sehen wir uns den Zugriff auf ein Array Element genauer an: Wie können wir das Array Element mit dem Index R9 1 finden? Und dieses dann der Variablen R6 zuweisen? Siehe rechts oben wiederum grün eingerahmt: 19
20 In RE steht die Anfangsadresse des Array. RE zeigt also auf das nullte Array Element. In R9 steht ja die Laufvariable, welche ursprünglich j geheißen hat. Wir addieren also zu RE den Wert von R9. Damit erhalten wir in R5 die Adresse des R9 ten Elements, also des j ten Elements von Array. Jetzt müssen wir noch 1 subtrahieren, dann steht in R5 die Adresse des Array Elements mit dem Index R9 1. Wir dereferenzieren so heißt das in der Sprache C ja R5 und erhalten damit den Wert der Speicherstelle, auf welche R5 zeigt. *R5 ist also der Wert des gesuchten Array Elements. Diesen Wert kopieren wir nach R6. Jetzt sollte es ein Einfaches sein, zu verstehen, wie wir das Array Element mit dem Index R9 kriegen. R5 wird RE plus R9. Und dann dereferenzieren und nach R7 kopieren. Das Interessante ist, dass sich alle diese Anweisungen jetzt zeilenweise direkt in TOY Maschinensprache übersetzen lassen. Dazu kommen wir gleich. Jetzt geht es in die andere Richtung. Wir wollen hier den in R7 gespeicherten Wert auf das Array Element mit dem Index R9 1 kopieren. Das geht so wie rechts in grün dargestellt: 20
21 Zuerst berechnen wir die Adresse des richtigen Array Elements: Der Wert von RE ist die Adresse des nullten Array Elements. R9 dazu gezählt ergibt die Adresse des Array Elements mit dem Index R9. Eins davon abgezogen und wir zeigen mit R5 auf die Adresse des gewünschten Array Elements. Jetzt nehmen wir den Wert von R7 und kopieren diesen auf die Speicherstelle, auf welche R5 zeigt. Fertig. Hier noch eine zweite solche Operation. Wir berechnen den Pointer in R5 und dann kopieren wir den Wert. So. Das war alles. Ich betrachte diesen C Code jetzt als eine Art Pseudo Code für unser TOY Maschinenprogramm. Ich habe alle Anweisungen des ursprünglichen C Programms so lange vereinfacht, bis wir zu so einfachen Anweisungen gelangt sind, sodass wir diese jetzt Zeile für Zeile in TOY Assemblersprache ausdrücken können. Sehen wir uns das an. Beginnen wir mit einer Umsetzung in die Register Transfer Sprache. 21
22 Es beginnt mit R2 wird der Wert von RF zugewiesen. Rechts wird dies mit dem Pfeil ausgedrückt. Oder hier: RB wird der Wert von R2 minus 1 zugewiesen. Auch hier ist die Umsetzung trivial. Sehen wir uns ein Beispiel mit der Dereferenzierung eines Pointer an. R6 kriegt den Wert von *R5. Also den Wert der Speicherstelle an der Adresse R5. In Register Transfer Sprache ist dies sehr explizit gemacht und einfach lesbar. 22
23 Vielleicht noch ein letztes Beispiel: Hier geht es in die umgekehrte Richtung. R5 zeigt auf eine Speicherstelle; deren Wert soll den Wert des Registers R7 kriegen. 23
24 Also: mem von R5 wird R7. Die Anweisungen der Register Transfer Sprache können wir jetzt in Assemblersprache umschreiben. Zeile für Zeile. Das sieht dann so aus: 24
25 Sehen wir uns die Pointer Beispiele von zuletzt an. Mit dem indirekten Ladebefehl also LDI können wir das Datum der Speicherstelle, auf welche R5 zeigt, nach R7 kopieren. Hier nochmals sicherheitshalber der gleiche Sachverhalt in C dargestellt. So einfach geht das eigentlich mit dem Pointer. Hier in die umgekehrte Richtung. Mit einem indirekten Store Befehl können wir den Wert des Registers R6 auf die Speicherstelle mit der Adresse R5 kopieren. OK. Das war also die Version des Sortieralgorithmus in Assemblersprache. Wie können wie sicherstellen, dass dieses Assemblerprogramm auch funktioniert? 25
26 Wir müssen es assemblieren, also die Maschinensprache erzeugen, und diese dann auf TOY laufen lassen. Ich habe mit Hilfe des Assemblers toyasm du kannst es auch mit Papier und Bleistift machen Zeile für Zeile in Maschinencode übersetzt. Sehe dir einige der Zeilen an. Du solltest keine Schwierigkeiten haben, die Übersetzung auch zu machen. Wie geht es weiter? Was wollten wir eigentlich machen? Sehen wir uns das Ganze nochmals an. Eingabe Sortieren und Ausgabe. 26
27 Zuletzt haben wir im Detail über den Sortiervorgang gesprochen und das Maschinenprogramm daraus entwickelt. Zuvor haben wir in der vorigen Lektion also über die Eingabe von Daten gesprochen. Das war also dieser Teil hier. Fehlt noch die Ausgabe. Was haben wir hier? Eine For Schleife mit einer printf Anweisung in unserem C Modell. Doch wie man eine For Schleife so lange massiert, bis daraus ein C Programm entsteht, welches direkt Zeile für Zeile in Maschinencode übersetzbar ist, weißt du jetzt. Ich mache es also schnell. Hier das ausgewalzte C Programm mit der gleichen Funktionalität. 27
28 Und jetzt noch zeilenweises Übersetzen. Hier die Version in Register Transfer Sprache. Und hier in Assemblersprache. 28
29 Und hier das Maschinenprogramm. Dieses Maschinenprogramm musst du dann noch samt allen anderen Teilen des Programms, also Input, Sortieren und Output, in Verilog Manier hinschreiben. Ist schon etwas lang und deshalb auf diesem Schirm hier etwas zu klein geraten. Siehe dir deshalb selbst die Datei mit dem Namen in_sort_out10.v an. Jetzt musst du nur noch das TOY Modell mit der Version 8.5 einlegen und dann simulieren. Also die Datei ro_kap8_toy_5.v simulieren. Dieses TOY Modell exekutiert das vorhin breit besprochene Sortier Programm in_sort_out10.toy. 29
30 Das Programm liest Daten von der Datei liste_von_daten.dat ein; sortiert diese Daten und gibt die Daten sortiert in aufsteigender Reihenfolge aus. Diese Daten werden in der Datei mit dem Namen std_out5.dat gespeichert. Probiere das unbedingt aus. Damit sind wir am Ende dieser Lektion angelangt. 30