restlichen Termine. Der letzte Kapitel wird wahrscheinlich nicht mehr
in der Vorlesung drankommen. Deswegen trotzdem ein bisschen Eile,
aber falls zum letzten Mal noch Fragen sind,
hier wollen wir noch so eine kurze Zusammenfassung der Sachen,
die ich letztes Mal erzählt habe. Nämlich,
es ging um die Frage unter dem Oberbegriff Assembler-Programmierung,
was tatsächlich passiert, wenn wir irgendwie gut in einer höheren Sprache,
wie zum Beispiel C, als Beispiel für die Folien schreiben,
Java oder was weiß ich, Python oder sowas,
die wäre ziemlich ähnlich.
Was eigentlich passiert. Und letztes Mal haben wir uns angeschaut,
die Kontrollstrukturen, die dann eben abgebildet werden,
letztlich auf Sprünge und bedingte Sprünge Und dann Schleifenkonstrukte,
die letztlich mit einer relativ statischen Umsetzung ebenfalls von Dubai,
Wildford auf dieselbe Art und Weise vom Compiler oder eben per
Hand als Assember-Programmierer umgesetzt werden.
Dann haben wir uns die Sprunggefiele ein bisschen im Detail angeschaut
und für die Intel-Architektur halt auch schon ganz kurz den Stack eingeführt.
Das war halt ein besonderer Teil des Hauptspeichers,
dessen Adresse jeweils vom Betriebssystem vorgegeben ist.
Und in diesem Fall haben wir auf der Intel-Architektur ein paar spezielle Befehle,
um den Stack zu verwalten,
um da Daten hineinzupacken
Und Daten wieder rauszubekommen. Und außerdem eben diesen Call and
Return-Befehl, der ebenfalls den Stack nutzt. Auf Architekturen,
zum Beispiel Rüstarchitekturen, die keine expliziten Befehle für den Stack haben,
muss man halt die Datentransfers,
die halt hier mit dem Push- und Pop-Befehl gemacht werden,
dann letztlich von Hand mit ganz normalen Speicheroperationen durchführen.
So, und die Idee war, dass wir durch das
Konzept und die Verwaltung des Stacks in der Lage sind,
Funktionsaufrufe, auch rekursive Funktionsaufrufe und die Verwaltung von lokalen Variablen zu bekommen,
durchzuführen.
Und das war nicht ganz fertig geworden am Freitag,
deswegen gucken wir uns das jetzt nochmal an
Die Idee war halt, dass wir hier diesen gelb markierten Speicherbereich haben,
mit dem wir mit einigen Befehlen hier zum Beispiel den Push-Daten jeweils hineinfüllen können.
Und dann wird das Register ESP, das ist also
eins von diesen acht Universalregistern der Intel-Architektur, automatisch angepasst.
Und der Datenwert in dem Register ESP ist die Speicheradresse von
dem aktuellen obersten Element des Decks, des Stapel-Kellerspeichers.
Und in diesem Fall durch die Konvention bei Intel startet der Stack
halt bei irgendeiner großen Adresse und wächst dann nach unten.
Das heißt, wenn wir irgendwas auf den Stack drauf tun,
dann wird die Adresse tatsächlich kleiner. Und in diesem Fall,
weil wir nur mit 32 mit Datenwerten arbeiten,
eben immer um vier Bytes,
also um den Wert
vier kleiner
Um die Daten wieder abzuholen, hatten wir Pop.
Das holt dann die Daten in einen Register, das man angibt.
Und gleichzeitig wird eben der Deckpointer dann wieder angepasst,
sodass er nach dem Durchführen von Pop wieder auf das Oberste,
das dann oberste Element des Decks zeigt.
Und das, was hier gerade gelesen wurde aus dem Stack
und jetzt nicht mehr gültig ist, das bleibt aber da liegen.
Das wird also nicht explizit gelöscht. Da waren
ein paar Beispiele. Und dann hatten wir den Funktionsaufruf,
Prozeduraufruf mit dem Callbefehl und den Rücksprung mit dem Return
So, die Idee war, dass wir jetzt
Funktionen haben mit verschiedenen Namen, die sich gegenseitig aufrufen.
Und das Beispiel war halt hier mit diesen Funktionen,
die sich teilweise selbst hier als zwei Funktionen,
Funktionsaufruf dieser Funktionen, die kann sich auch rekursiv selbst aufrufen.
Und in dem Beispiel war jetzt halt der komplette Aufrufbaum,
so wie hier, You ruft Fuhrhauf MI, ruft sich rekursiv zweimal selbst auf,
kehrt dann zurück und dann wird das nochmal aufgerufen.
Und im Stack sah das dann halt so aus,
dass wir jeweils die Werte,
die eine Funktion für sich selbst benötigt,
hier auf dem Stack drauf haben.
Das war jetzt hier
die tiefste Verschachtelung in diesem Spielbeispiel
Nachdem das dann zurückspringt, werden die Daten hier liegen gelassen,
sind aber nicht mehr gültig.
Und dann geht es halt zum Hauptprogramm oder zum ursprünglichen Programm
zurück. Das war also dann der Stand von Freitag.
Und jetzt wollen wir uns das im Detail angucken und dann
einigen Beispielen dann ausprobieren. Und damit wird dann nachträglich auch klar,
was diese komischen seltsamen Befehle jeweils am Anfang und am Ende einer Funktion waren,
die in der letzten Woche schon dran waren,
da aber noch nicht so richtig Bedeutung hatten
So, die Idee ist also letzt,
dass wir weiterhin den Speicherbereich Stack in Bereiche aufteilen.
Die sind jetzt meistens auf diesen Folien farbig gefüllt,
wobei jeweils eine Farbe für eine Funktion gedacht ist.
Und wir beginnen halt irgendwo mit dem Hauptprogramm hier oben.
Das ruft dann diverse Funktionen auf und jetzt sind wir an
dieser Stelle, wo diese Funktion hier,
die rosa Funktionen will jetzt halt die gelbe Funktion aufrufen.
Und das funktioniert dann so,
dass die rosa Funktion halt hier ihren eigenen Krempel hat,
irgendwelche Variablen, die sich für sich selbst benötigt.
Und jetzt möchte sie die gelbe Funktion aufrufen und zwar mit irgendwelchen Funktionsargumenten.
Und das passiert dann so, dass die Funktionsargumente jetzt auf den Stack gepackt werden.
Also bei Adressen unterhalb dessen, was hier bereits belegt ist.
Und der Funktionsargument in diesem Beispiel ist, das sind es halt zwei,
es können aber auch 20 oder 1000 sein oder sowas,
dann wird entsprechend viel Platz auf dem Stack reserviert für die Argumente.
Noch in dem Speicherbereich der Funktionen, die halt gerade aktiv ist.
Und dann wird die nächste Funktion aufgerufen und der Callbefehl sorgt dann dafür,
dass die Rücksprungadresse ebenfalls auf diesem Stack abgespeichert wird.
Und dann geht die Kontrolle über an die aufgerufene Funktion
Und die muss jetzt irgendwie dafür sorgen, dass sie später irgendwann wieder zurückspringen kann,
wenn sie mit ihren eigenen Operationen fertig ist.
Und dazu belegen wir halt ein weiteres Register,
zumindest so wie es hier auf den Folien erklärt wird,
weil das nochmal aktuell ein Compiler an Spielbeispielen ausprobiert wird,
feststellen, dass der Compiler clever genug ist,
um diesen Framepointer in vielen Fällen einfach mal zu eliminieren.
Wenn da halt den gesamten Quellcode, was weiß ich,
so 100 Zeilen C-Code vor sich hat und sieht,
da sind ja nur drei Funktionen drin oder sowas,
dann kann man sich dieses Zeug,
was wir hier erklären,
in vielen Fällen
einfach sparen
Und das heißt, das, was sie hier selbst ausprobieren,
enthält möglicherweise diesen Framepointer gar nicht, weil es sich um Spielbeispiele handelt.
Wenn man größere Programme schreibt, wo halt,
was weiß ich, Funktionen aus verschiedenen Quelldateien zusammengebastelt werden müssen,
dann braucht man diesen Framepointer in den meisten Fällen dann doch.
So, also die Idee ist, dass wir die Speicherbereiche,
die die einzelnen Funktionen für sich reserviert haben,
jeweils am Anfang mit diesem Framepointer markieren.
Also wir reservieren ein Register, was angibt,
wo der Speicherbereich der aktuellen Funktionen gerade beginnt.
Und das zweite Register, der Stackpointer zeigt auf das höchste,
oberste Datenelement, was im Moment in dem Stack belegt ist.
So, die aufrufende Funktion hat also die Funktionsargumente hier in ihren eigenen Bereich gepackt.
Der Callbefehl packt die Return-Adresse hierhin. Die neue Funktion macht dann als allererstes,
sie speichert den alten Frame-Pointer hier auf dem Stack und der neue Frame-Pointer ist,
das ist in einem Register EBP, der zeigt also hierhin.
Und was hier drin ist, ist tatsächlich der Wert des alten Frame-Pointers, der irgendwo hier ist.
Das gucken wir uns gleich an. Und dann werden lokale Variablen,
die die Funktion für sich selbst braucht,
halt hier abgelegt Und zusätzlich, das ist auch eine weitere Konvention,
wenn irgendwelche Register benutzt werden,
die halt hier auch schon benutzt worden sind,
müssen wir uns fragen, was passiert mit den Werten?
Die können ja nicht einfach überschrieben werden.
Das heißt, die müssen gegebenenfalls hier gespeichert werden.
Und wenn diese Funktion dann eine weitere Funktion aufrufen will,
dann fängt sie halt an,
ihrerseits die Argumente für die nächste Funktion hier in ihren Bereich der Stacks reinzupacken.
Das sind also eine ganze Menge Details und ein paar Beispiele
wollen wir uns jetzt halt anschauen.
Die erste Frage ist jetzt, was passiert mit irgendwelchen Registern?
Hier haben wir halt einen Ausschnitt aus dieser Funktion Jude
Das war die oberste und Yu möchte an dieser Stelle die Funktion Hu
aufrufen und da ist halt ein Teil des Kurzs von Hu gezeigt.
Auf der End-Architektur haben wir jetzt jedenfalls so,
wie wir sie benutzen, nur acht Register zur Verfügung,
von denen der Stackpointer und dieses EBP, der Basepointer,
tatsächlich schon belegt sind.
Wir haben also jetzt nur noch sechs Register, mit denen
wir tatsächlich rechnen können. So, und jetzt benutzt ihr
diese Funktion halt hier an dieser Stelle des Register EDX.
Die hat also da jetzt den Wert 15.213 reingeschrieben.
Die Postleitzahl der Universität von Herr Professor Brandt
und ruft jetzt hier diese Funktion Hu auf
Und anschließend geht sie davon aus, dass der Wert,
den wir hier da reingeschrieben haben, an dieser Stelle,
nachdem die Funktion Who aufgerufen wurde und zurückgekehrt ist,
natürlich noch zur Verfügung steht.
Darauf muss man sich halt irgendwie verlassen können,
sonst wird das natürlich schwierig. Und diese Funktion Who hat
ihrerseits dann den Rückgabewert per Konvention in dem Register EAX zurückgegeben.
Die hat also irgendwas berechnet, das ist hier drin.
Und hier sollte jetzt das 15.22013 drin sein.
Jetzt braucht aber die Funktion Who natürlich ihrerseits Register umzurechnen.
Und ja, hier passiert irgendwas mit dem Register EDX und das
wird hier mit einem konstanter benutzt und hier dazu addiert.
Und dann geht diese Funktion zurück. Das ist also offensichtlich problematisch,
weil diese Funktion hier einen Wert da reingeschrieben hat und einschließend benutzen möchte,
anschließend aber natürlich aufgerufene Funktionen, da wir sowieso eine Sechsregister haben,
auch irgendwo mit ihren eigenen Werten hin müssen.
Die Frage ist jetzt, wie kann das funktionieren?
Und da gibt es zwei mögliche Konventionen. Das eine ist,
dass der Aufrufer dafür verantwortlich ist, der Register Werte gegebenenfalls
irgendwo abzuspeichern, wo man sie hinterher wiederfinden kann.
Und die Alternative ist, dass halt die aufgerufene Funktion verantwortlich ist,
irgendwelche Werte, die sie selbst benutzen möchte,
halt aus Registern auf den Stack zu retten und wiederherzustellen,
bevor sie zurückspringt
Das sind die beiden Möglichkeiten, die man da hat.
Und tatsächlich wird jetzt beides benutzt, jedenfalls so wie Linux,
in diesem Fall halt wie bei unseren Beispielen, den Code benutzt.
Das heißt, von den acht Registern, die wir
zur Verfügung haben, sind zwei mit diesen Spezialaufgaben belegt.
Er ist Deckpointer und der Basepointer, der den Frame der aktuellen Funktion eingibt.
Und die üblichen sechs Register waren halt aufgeteilt in zwei Dreiergruppen.
A, D und C gelten als Color-Safe-Register.
Das heißt, die aufrufende Funktion muss dafür sorgen,
dass sie diese Werte sichert, falls sie sie anschließend braucht.
Und die aufgerufene Funktion kann diese Dinger hier halt nach
Belieben benutzen
Und die anderen Dreieregister sind Callys-Safe-Register. Das heißt,
die aufrufende Funktion kümmert sich nicht drum,
sondern falls die aufgerufene Funktion die Dinger hier benutzen will,
muss sie selbst dafür sorgen,
dass sie den alten Wert zwischenspeichert auf dem Stack und wiederherstellt, bevor sie zurückspringt.
Das ist natürlich ein Kompromiss. Die Idee ist,
dass eine kleine Funktion, die halt, sagen wir mal,
mit Dreieregistern auskommt, sich, nee,
die mit Dreieregistern auskommt, sich um nichts kümmern muss,
wenn sie halt nur diese Dinger benutzt.
Das ist dann billiger. Und andererseits, also ansonsten,
ich meine, sowas wie 0-6,
6-0 wäre natürlich auch möglich, was ein bisschen übersichtlicher ist.
Aber dieses hier erspart ein paar Instruktionen, wenn man halt mit
drei Registern auskommt. Und die letzte Konvention ist dann halt,
dass das Resultat eines Funktionsaufrufs in dem Register EAX zurückgeliefert wird,
wenn man mehr als nur einen Wert zurückliefern will aus einer Funktion,
dann muss man halt über Pointer bestimmte Speicherbereiche dafür nutzen.
Ich hoffe mal, das ist so plausibel,
dass man sich da halt Gedanken machen muss,
die aufrufende und die aufgerufene Funktion, wer ist dafür verantwortlich,
Werte zwischenzuspeichern, damit die aufgerufene Funktion halt die Werte benutzen kann,
Asylregister benutzen kann.
So, und dazu gibt es jetzt ein paar Beispiele in den Folien.
Das geht natürlich möglicherweise auch ohne weitere Erklärungen,
sich das anzuschauen, wenn man da schon einigermaßen mit vertraut ist.
Aber ich glaube, das hilft dann schon hier ein bisschen zuzuhören.
Kurze Zwischenfrage in die Runde, wer hat sich das Buch besorgt?
Überhaupt jemand? Also das Buch von Bryant Ohelleran ist einfach
schlichtweg exzellent. Wer das mal aufschlägt,
wird da jede Menge interessante Dinge zum Lesen finden und zum Lernen.
Ich kann wirklich nur empfehlen an meiner Bibliothek vorbeizugehen.
Ein Kilo Buchpapier mit nach Hause zu schleppen und dann tatsächlich mal ein bisschen nachzulesen.
Egal. Also dann halt ohne Buch, ohne Nachlesen gibt es halt
Hier haben wir eine Funktion und zwar jetzt erst mal geschrieben in C,
aber das würde natürlich in Java zum Beispiel ganz ähnlich aussehen.
Wir arbeiten wieder nur mit Intager-Operationen,
also es ist eine Fakultät mit 32 mit Intager nicht so fürchterlich nutzt, nützlich,
weil das sehr schnell überläuft, aber das Konzept kann man damit halt ganz gut erklären.
Wir haben also jetzt hier eine Funktion,
die kriegt einen Intager-Wert übergeben,
den nennen wir X und wir wollen die Fakultät von X
berechnen und als Intel der Wert zurückliefern
Okay, was passiert hier?
Wir reservieren uns hier offensichtlich eine temporäre, lokale Variable.
Die müssen wir dann hinterher irgendwo auf dem Stack ablegen.
Falls wir nicht mit Registern auskommen. Und was machen wir?
Wenn X kleiner ist als eins, dann liefern wir den WL1 zurück.
Und ansonsten geht es halt hier weiter. Und wir berechnen
jetzt regulsiv einen Rückgabewährt als Funktionsaufruf-Fakultät von X minus 1.
Also was weiß ich, Fakultät von 30 würde Fakultät von 29 aufrufen.
Fakultät von 29 würde dann in dieser Stelle Fakultät von 28
aufrufen und entsprechend dann irgendwie wieder auf dem Stack belegen
Und wenn diese Funktion hier zurückkommt, dann hat sie halt
die Funktion von Fakultät von X-Minus 1 berechnet.
Und im allerletzten Schritt multiplizieren wir das mit unserem aktuellen Wert von X.
Also dieses hier berechnet Fakultät von X-Minus 1, mal X,
ist dann die Fakultät von X und das liefern wir zurück.
Okay, was machen wir? Wir versuchen,
das Register EAX für diesen Rückgabewert und zum Rechnen zu benutzen,
ohne es vorher zu speichern. Und das Register EEBX,
jetzt müssen wir hier noch mal kurz gucken,
also AX ist irgendwas, was wir speichern müssten.
BX ist irgendwas, was die aufgerufene Funktion ihrerseits speichern muss,
wenn sie das benutzen möchte
So, das ist also das, was wir in unserer Hochsprache geschrieben haben.
Und jetzt schicken wir das durch den Compiler und dann kommt
dieses komische Zeug daraus. Das sagt halt zum ersten Mal,
das ist hier eine Funktion mit dem Namen R-Fakt gibt.
Und dann haben wir jetzt hier diese beiden ersten Zeilen,
die dafür sorgen, dass dieser Basepointer gesetzt wird.
Die Funktion fängt also damit an,
als allererstes den Wert von EBP auf den Stack zu packen
und dann den aktuellen Wert des Stackpointers,
das ist nämlich gerade jetzt die Adresse von dem,
was wir gerade auf den Stack gepackt haben,
ihrerseits in das Register-EBP hineinzutun
Das ist dann also die Verwaltung des Decks. Wir haben den bisherigen Framepointer, EBP,
der aufrufenden Funktionen jetzt auf den Stack gesichert und der aktuelle Stackpointer wird jetzt mit,
also der, wie hat das Stackpointers,
die aktuelle Adresse, die auf dem Stack belegt wird,
merken wir uns in unserem eigenen Framepointer.
Das Register EBP zeigt also jetzt auf den Rahmen,
auf die Speicheradressen der aktuellen Funktion. So,
und jetzt wollen wir mit dem Register EBX rechnen und die Konvention sagt,
dass EBX ein Register ist, was der Corli?
Nee, Corli, also die aufgerufenen Funktionen sichern muss.
Das heißt, damit wir das benutzen können, müssen wir
erstmal den aktuellen Inhalt von EBX auf den Stack packen.
Und anschließend können wir das benutzen. Also wir sichern erstmal
den bisherigen Inhalt von EBX auf dem Stack.
Und dann können wir damit rechnen. Und was machen wir
? Wir holen uns jetzt an auf Sat.8 von dem Basepointer
einen Wert ab und packen den nach EBX.
Ich gehe nochmal zwei Folien zurück hier. Die Idee war
halt jetzt Basepointer, ist der Anfang unseres eigenen Speicherbereichs.
Basepointer plus 4 ist die Rücksprungadresse
Pointer plus 8, ist das erste Argumente aufgerufenen Funktionen,
Basepointer plus 12, das zweite Argument und so weiter.
Wenn wir also irgendwo in dieses hier sehen, Offset 8 vom aktuellen Basepointer,
dann ist das ein Zugriff auf das erste Argument der Funktion.
Das heißt, wir holen uns das X und packen das
in das Register EBX rein. Dann geht es halt,
also wir haben jetzt eben das X in ein Register
gepackt und jetzt kommt halt dieser Vergleich,
if das if X kleiner gleich 1,
das wird dann natürlich hier mit so einem Compare-Befehl gemacht,
der dann seinerseits das Statusregister modifiziert und entsprechend
die Bits da setzt
Wenn das kleiner gleich ist, dann geht es hier in das 78 rein.
Wir packen den Wert eins in das Register EAX
und sind eigentlich fertig und könnten jetzt den Rücksprung machen.
Wir müssen aber vorher noch aufräumen. Wir haben jetzt
den Wert von EBX modifiziert und sind aber dafür verantwortlich,
dass wir das halt als Corny Safe wiederherstellen.
Das passiert dann hier. Das heißt, jetzt wird hier
Basepointer minus vier. Das ist also dann letztlich der erste
Eintrag auf dem Stack, den wir tatsächlich benutzt haben.
Das wird ausgelesen und wieder in das Register EBX zurückgespeichert.
Das heißt, jetzt haben wir den Wert von EBX,
den wir hier ursprünglich gesichert haben,
wieder in das Register zurückgetan
Dann holen wir uns den Wert des Framepointers und packen das
wieder in den Stackpointer hinein. Das heißt, alles,
was wir selbst auf dem Stack belegt haben,
wird jetzt wieder einfach da liegen gelassen.
Und die Verwaltung sagt jetzt, der Stackpointer ist jetzt wieder da,
wo wir beim Aufruf der Funktion waren und dann holen wir jetzt noch den Originalwert des Basepointers,
der aufrufenden Funktion zurück und dann können wir zurückspringen.
Diese beiden Dinger hier, also das Aufbauen des Framepointers,
der aktuell aufgerufenen Funktion und diese Dinger hier,
diese drei Instruktionen,
die sind auch in den meisten vorherigen Beispielen
so gewesen
Das ist halt die Verwaltung, dass jede Funktion halt
ihren eigenen Speicherbereich auf dem Stack bekommt, verwenden kann.
Und wenn sie zurückkehren will, dann muss sie halt das wieder zurücknehmen.
Das heißt, der Sackpointer wird wieder so gesetzt,
dass er nur noch auf die Return-Adresse zeigt,
nach dem Return, es ist dann wieder komplett in dem Speicherbereich,
der Aufruf und dann Aufruf in Funktion.
Und zusätzlich wird halt mit diesem Ding hier dieser Basepointer,
nee, hier, der Basepointer restauriert,
sodass der wieder auf den Rahmen den Speicherbereich der aufrufenden Funktion zeigt.
Das ist also in allen bisherigen Beispielen
dasselbe gewesen
Und jetzt kommt zusätzlich noch hinzu,
dass wir halt das Register EBX benutzen wollen,
das uns aber letztlich nicht gehört, sodass wir den Wert
erstmal retten und an dieser Stelle vom Stack wieder abholen und wiederherstellen.
Und damit hat die aufrufende Funktion ihren eigenen Registerwert wieder.
Ist das plausibel? Ich hoffe mal. Das kann man
dann trotzdem noch mal kurz visualisieren, was da passiert.
Wir haben halt zu Anfang den Speicherbereich für die aufrufende Funktion,
ihren eigenen Basepointer, der noch mal darüber liegenden aufrufenden Funktionen,
dann das Register BX, was wir sichern müssen von der aufrufenden Funktion,
dann den aktuellen Wert von X und dann die Rücksprungadresse.
Das ist also das, was jeder einzelne rekursive Aufruf von unserer Funktion belegt,
4, 32 Bittwerte auf dem Stack, also insgesamt 16 Bytes.
Und wenn das dann hier in diese Funktion reinspringt,
dann fängt es an, das entsprechend normal aufzubauen. Und bei einer Fakultät von,
sagen wir, 30 haben wir halt oben Fakultät von 30,
X, gleich 30 und ruft dann den nächsten Block aus und da ist dann X,
29 drin, ruft wieder ein Rekursiv auf X,
28 und so weiter bis X gleich 1,
wo das dann halt fertig ist und dann sukzessive den Wert
von der Fakultät zurückliefert
Und das ist jetzt der interessante Teil, den ich bis hier übersprungen habe.
Das war halt der Teil zwischen dem Vergleich mit 1 und dem Fertigsein,
wenn es halt eins ist und dann dem Aufräumen des Decks.
Jetzt müssen wir ja noch irgendwie die Fakultät tatsächlich berechnen.
Und das passiert an dieser Stelle hier.
Da ist wieder dieses Load-Effective Adress als Universalbefehl,
mit dem man halt auch Arith Metic ganz gut machen kann.
Wir nehmen also jetzt den Inhalt der Speicherstelle auf die EBX zeigt,
ziehen davon minus hin,
ziehen davon eins ab und packen das nach EAX
Das war jetzt halt so, dass da tatsächlich der Wert von X drin ist.
Das berechnet also jetzt x-minus eins und packt das in das Register ERX.
Dann packen wir das Ding auf den Stack und rufen uns rekursiv auf.
Warum das? Weil wir halt den Wert von Register ERX selber sichern müssen,
falls wir den noch später wieder brauchen, dann rufen wir die Fakultät auf.
Und dann multiplizieren wir den Wert EBX,
den die auf aufgerufene Funktionen, Fakultät zwar modifiziert,
aber dann wiederhergestellt hat, sodass das korrekt ist mit dem,
was wir vorher hatten,
multiplizieren das mit dem EAX und das Resultat kommt wieder in dieses Register EAX 9.
Da war ja letztes Mal die Frage, ob das mit diesem E-Mull tatsächlich funktioniert.
Und das Datenblatt hat zwar irgendwie 30 verschiedene Varianten von diesem Befehl,
je nachdem, ob es mit einem,
zwei oder drei Apparanten aufgerufen wird und ob es im 32 oder 64-Bit-Mut läuft,
aber hier die Variante mit 32 Bitcode und zwei Operanten funktioniert tatsächlich so,
wie man das dann erwartet.
Die beiden Werte werden miteinander multipliziert.
Und von dem potenziell 63-Bit-Ergebnis werden die unteren 32 Bit genommen
und hier reingeschrieben
So, also diese Zeilen hier sind halt dieses hier.
Wir berechnen die Fakultät von X-minus 1 durch diesen rekursiven Aufruf.
Was wir da zurückbekommen, das ist im Register ERX
und das multiplizieren wir mit dem Wert von X,
den wir selbst haben.
Und in Kombination sollte das dann eben das Richtige tun.
Das ist dann hier nochmal visualisiert, was da passiert.
Dieses Lot-Effektive Adress mit minus eins von EBX berechnet halt den Wert von X-minus eins,
packt das in das Register ERX und das Register EBX
enthält dann noch
den Wert von
Dann pushen wir den Wert von x minus 1 auf dem Stack.
Das ist nämlich genau jetzt der Punkt, wo wir den
ersten Parameter der aufgerufenen Funktion hier auf den Stack packen würden.
Wir würden, wenn da mehrere Parameter wären, würden wir halt mehrere davon tun.
Und wenn wir dann einen rekursiven Aufruf durchführen, das Call,
dann wird halt von dem Call noch die Rücksprungadresse hier hingepackt.
Und dann sind die vier Werte, die wir für
unsere Funktion brauchen, auch tatsächlich alle auf dem Stack.
Und das ist jetzt halt der Aufruf.
Und wenn sich das dann wieder aufruft,
dann wird halt dieses Ding hier ergänzt durch einen weiteren Vierer-Blog
mit weiterem EBP,
EBX,
dann x minus 2
und der Rücksprungadresse
So, und der spannende Punkt ist jetzt halt,
wenn das ganze Ding zurückkehrt,
dann haben wir halt in dem Register ERX durch die Art und Weise,
wie das halt aufgebaut wird, eins, mal zwei,
mal drei, mal vier,
den Wert von x minus eins Fakultät und in einem Register EBX ist der Wert von x drin.
Und das wird jetzt halt miteinander multipliziert,
sodass wir dann im ERX in Wertexfakultät haben.
Und hier ist dann weiterhin der WRX drin. Und
wenn wir dann hier zurückspringen aus der obersten aufgerufenen R-Fakt-Funktion,
dann ist der Wert der Fakultät halt im Register ERX und das
ist genau das,
was wir wollen
So, dann nochmal der Visualisierung, dieses Kurzsequenz, gut,
Fragmenz am Ende der Funktion, der für das Aufräumen notwendig ist.
Das war jetzt heizelnd zum einen dieses merkwürdige Move L minus vier EBP,
also EBP ist dieses hier und EBP minus acht war das der temporäre Parameter,
den wir für den rekursiven Aufruf haben.
Und hier hatten wir den vorherigen Wert von EBX gespeichert,
den kaufen wir uns jetzt und packen den Industriegister EBX zurück.
Das ist also dieses hier
Und auf die Art und Weise hat das Register EBX dann anschließend wieder den Wert,
den es hatte, bevor wir es modifiziert haben.
Dann holen wir uns den Inhalt des Spacepointers. Das ist
der alte Basepointer. Und packen das in den Stackpointer hinein.
Das heißt, der Stackpointer wird jetzt wieder nach hier gesetzt.
Und dann machen wir das Pop-EBP, was halt den Wert,
der hier drin ist, aus dem Stack abholt,
da reintut, wo es wieder hingehört.
Also dann den Fremdpointer, der Aufruf aufrufenden Funktionen wiederherstellt.
Und anschließend nach dem Return wird halt diese Adresse abgepflückt.
Und die aufgerufenen Aufrufe eine Funktion ihrerseits ist wieder aktiv. So,
und dieser Teil hier ist dann eben, wenn man den Framepointer benutzt,
den Basepointer, bei allen Funktionen eigentlich immer gleich.
Das sind diese drei Zeilen. Und irgendwelche Zeilen,
die so aussehen wie dieses hier davor, sind halt dafür notwendig,
die Register wiederherzustellen, die wir zwischendurch modifiziert haben.
Okay, auf diese Art und Weise kann man aber
aus einer Funktion immer nur einen Wert zurückgeben.
Das war also letztlich ein Call by Value, was auch interessant ist,
sind halt Funktionsaufrufe, die Parameter bei Reference haben,
also letztlich Pointer auf Dinge oder eben Referenzen auf Objekte oder sowas.
Und das wollen wir uns auch noch kurz anschauen
Hier ist also jetzt eine zweite Variante der Fakultätfunktion,
die jetzt nicht rekursiv funktioniert, sondern irgendwie mit einer Schleife.
Ja, nee, sorry, doch,
das ist doch immer noch rekursiv, aber jetzt zweigeteilt.
Und an dieser Stelle übergeben wir jetzt nicht einen Wert,
sondern eine Adresse. Und die Adresse wird dann ihrerseits genommen,
um das Zwischenergebnis aufzusummieren, aufzumultiplizieren. Also jetzt hier nicht
die Rekursive, sondern die zweite Funktion fakultiert berechnet.
Wir fangen an mit dem Wert 1 und dann rufen wir
diese Hilfsfunktionen auf mit dem Wert von X der Adresse dieser Variablen,
die jetzt hier halt tatsächlich temporär auch auf dem Stack liegt.
Und wenn man damit fertig sind, dann geben wir den berechneten Wert zurück.
Sowas hier ist immer schon mal so ein bisschen ein Indiz,
dass da die Warnlampen angehen,
wenn man irgendwas als temporäre Variale auf dem Stack angelegt hat und davon eine Adresse nimmt.
Das ist schon mal der erste Schritt zu einem potenziellen Programmierfehler.
Aber in diesem Fall geht das halt gut,
weil wir das nur bis hierhin benutzen und anschließend nicht
den Pointer zurückgeben,
sondern nur den berechneten Wert
So, das macht also nichts anderes als hier.
Also eine Variable breitzustellen, die Adresse dieser Varianten zu nehmen
und dann das X an diese Hilfsfunktion zu übergeben.
Und die Funktion ist helper. Die bekommt also zwei Argumente,
nämlich einen Wert, einen Value-Parameter und einen Reference-Parameter.
Das ist also an dieser Stelle einfach eine Speicheraddresse auf der
Assembler-Ebene, auf der Maschinenebene. Was passiert jetzt,
wenn der Wert von X kleiner als 1 ist,
dann springen wir einfach mal zurück in der Erwartung,
dass dann schon alles richtig berechnet worden ist.
Und das stimmt auch, weil hier der Wert
mit 1 initialisiert wurde
Und ansonsten berechnen wir halt einen neuen Wert vom Z,
als Produkt von X mit dem Sterneckum in 10 Notationen
ist jetzt halt der Inhalt des Pointers.
Also das, auf das der Pointer zeigt, also letztlich der Inhalt dieses Accumulators.
Hier haben wir das halt Accumulator genannt, lokal innerhalb dieser
Funktionen und übergeben wird halt an dieser Stelle die Adresse eines Integer-Werts.
So, hier wird also was aufmultipliziert,
dann wird das Resultat dieser Multiplikation in diesem Actumulator abgespeichert und dann rufen wir die Funktion auf,
rekursiv mit x-minus eins und wieder derselben Speicheradresse.
Das heißt, da wird eine Speicheradresse reserviert und dann immer wieder
an diese Funktion Eshelper übergeben, neu übergeben, neu übergeben.
Was aber dann passiert, ist, dass die Speicherstelle,
also hier haben wir eine Adresse letztlich,
weil das halt ein Poiter ist und die Speicherstelle auf dieser Pointer zeigt,
die wird mit diesen Interessanten Zwischenergebnissen gefüllt
Hier wird jetzt ein bisschen Magie gemacht. Im Wesentlichen haben wir
wieder, dass die aufrufende Funktion hier rosa irgendwas macht.
Sie hat halt ein Argument auf den Stack gepackt
und ruft dann die gelbe Funktion auf.
Dann ist die Rücksprungadresse hier. Dann verwalten wir bisher,
wie bisher wieder diesen Basepointer um Stackpointer und Framepointer für diese Funktion zu initialisieren.
Und jetzt brauchen wir halt irgendwo Speicherplatz für diesen Wert hier.
Und der Wert kommt auf den Stack
Das heißt, hier wird mit so einem Push eine Speicherstelle
auf dem Stack reserviert. Und das ist halt das,
wo wir unseren, letztlich den Resultatwert hinterher aufberechnen wollen.
Das passiert also dann an dieser Stelle. Dieses war einfach
das Vorbereiten der Funktion. Das ist wie bei den anderen
Dingern auch. Und jetzt reservieren wir uns an dieser Stelle
einen Speicherbereich auf dem Stack für lokale Variablen.
Das ist jetzt als Beispiel gemacht. Also hier könnten wir
auch nur ein einzelnes Ding auf den Stack packen.
Hier wird jetzt gleich mal ein Bereich von 16 weit,
also vier Variablen
Bereitet für uns reserviert und nur den ersten Eintrag davon nutzen wir.
Was machen wir? Wir holen uns hier mit diesem MoveL8
plus EWP. Das war halt der Funktionswert von X. Den
holen wir uns in das Register EDX.
Und jetzt packen wir den Wert Konstante 1 an diese Adresse
hier. Das ist halt die erste Variable,
in dem für uns reservierten Bereich für lokale Variablen.
So, und jetzt sind wir halt an dieser Stelle.
Das haben wir jetzt gemacht. Und jetzt wollen wir halt
diese Hilfsfunktion aufrufen und anschließend mit dem Wert rechnen
So. Hier haben wir unsere lokale Variable, in der wir die Fakultät berechnen wollen.
Und jetzt müssen wir halt irgendwie dieses Ding an diese Hilfsfunktion übergeben.
Die hat halt das Argument X und das Argument Pointer auf den Wert.
Wenn man das macht, die Werte werden in umgekehrter Reihenfolge
auf den Stack gepackt als Konvention. Das heißt,
bei der bisherigen Funktion hatten wir nur das X als Parameter,
deshalb wurde nur das X auf den Stack gepackt.
Jetzt haben wir zwei Parameter der,
dieser hier kommt als Letzter auf den Stack und der hinterste,
also in diesem Fall haben wir nur zwei,
der hier kommt als allererstes auf den Stack.
Das heißt, hier ist unsere Funktion,
wir haben lokale Variable reserviert und jetzt wollen wir,
dass eine neue Funktion aufrufen und wir müssen die Parameter
dieser Funktion auf den Stack packen.
Und zwar in umgekehrter Reihenfolge. Deshalb wird jetzt dieses
hier als allererstes auf den Stack gepackt,
dann das X und dann können wir die Funktion aufrufen.
Gut, jetzt wollen wir aber nicht den Wert von diesem X übergeben.
Das wäre halt der Inhalt dieser Speicherstelle, sondern wir wollen
die Adresse übergeben. Und die Adresse berechnen wir jetzt
eben Framepointer,
Minus 4. Das ist also dieses hier. Und der inneren,
diese Adresse berechnen wir jetzt wieder mit diesem Lot-Effective-Adress-Befehl und nehmen dafür das Register EAX.
Und das ist also jetzt ein Zwischenergebnis,
das soll als Parameter an die nächste Funktion übergeben werden.
Deswegen müssen wir das auf den Stack pushen.
Das ist also die Adresse, die wir hier berechnet haben.
Das war die Adresse von dem Ding, wird jetzt hier als Argument,
als Parameter der nächsten Funktion auf den Stack gepackt.
Und dann wollten wir noch den Wert von X,
der jetzt hier im Register EDX drin ist,
auf den Stack packen.
Das ist dann dieses hier. Und jetzt brauchen wir noch
einen Rückgabewert. Der wird dann hier berechnet und
dann können wir die Funktion aufrufen
So, und die aufgerufene Funktion, die will jetzt hier
mit dem Zeug rechnen. Und sie bekommt halt einen Wert
. Das ist das X. Das lag halt jetzt,
das ist in Kenum einen zurück, hier als Integer-Wert auf dem Stack.
Und dieses hier ist jetzt die Adresse einer Speicherstelle.
Und der Inhalt der Speicherstelle ist halt der Wert,
mit dem wir rechnen wollen.
Deswegen also jetzt hier in der 10-Notation hier der Wert.
Und hier ein Pointer, also eine Speicheradresse.
Was wollen wir berechnen? Den Wert X mal,
den derzeitigen Inhalt von diesem Pointer,
wird in einer Zwischenvariable gespeichert und dann wird der Speicherstelle
hier mit dem neuen Zwischenergebnis gefüllt
Gut, wie geht das? Wir benutzen jetzt weiterhin
das Register ERX für den Wert von Z. Das heißt,
wir verschieben jetzt den Wert von X in das Register ERX,
dann multiplizieren wir ERX und mit was,
mit dem Inhalt des Pointers.
Und deswegen,
also hier auf Assem der Ebene ist das dann ganz einfach,
wir müssen nichts weitermachen als die Adresse zu nehmen von
dem Ding,
was uns da auf dem Stack übergeben wurde
Und wenn wir damit fertig sind,
packen wir das Zwischenergebnis wieder hier genau an diese Adresse.
Und damit haben wir dann halt das Zwischenergebnis verändert, berechnet.
Und zwar wird dann halt, weil immer wieder diese Adresse übergeben wird,
wird in diesem Ding dann die Fakultät aufmultipliziert.
So, das soll für den ersten Fall hier ausreichen.
Ich kann nur empfehlen, da selbst mal einen Compiler aufzurufen,
Park, kleine Funktionen zu schreiben und dann mal im D-Bagger durchzugehen,
wie das tatsächlich funktioniert
Das ist ein ganz essentielles Konzept. Dadurch,
dass wir halt eine Schachtelung von Funktionen haben,
reicht es aus für jeweils die neue, aufgerufene Funktion,
einen neuen Speicherbereich zu reservieren.
Und der Speicherbereich hat halt dann untersorgt dafür,
dass Funktionen sich selbst direkt rekursiv oder auf Umwegen tatsächlich
mehrfach aufrufen können und sich nicht in die Quere kommen.
Und das funktioniert dann halt durch diese Konventionen,
dass wir die Parameter in einer bestimmten Reihenfolge auf den Stack packen,
dann die Rücksprungadresse und zusätzlich dann halt,
weil wir nur eine begrenzte Anzahl von Registern haben,
entweder die aufrufende Funktion oder die aufgerufene Funktion dafür sorgen muss,
dass dieser Registerwerte gegebenenfalls gesichert und dann wiederhergestellt werden
Das, was jetzt hier gezeigt ist, ist die spezifische
Realisierung für die 32-Bit-Variante von der Insel-Architektur zusammen mit Linux.
Man kann dann, wenn man andere Betriebssysteme nimmt,
auch möglicherweise andere Kombinationen davon haben,
also insbesondere welche Register von wem zu retten sind.
Aber das Grundprinzip ist immer egal, immer gleich.
Und wenn wir keine Call- und Return-Befehle haben,
sondern zum Beispiel nur Sprunggefehle,
dann muss man halt diesen Teil, der was weiß ich,
hier die Rücksprungadresse auf den Stack packt und statt Push und Pop
Direkt mit Speicheradressen hier auf den Stack arbeiten.
Das spielt also keine große Rolle, ob man die Befehle
zur Verfügung hat oder nicht. Wichtig ist dieses Konzept,
dass halt jede Funktion ihren eigenen Speicherbereich bekommt und in der Lage ist,
auf ihre Parameter zuzugreifen, die lokale Variablen anzulegen.
Und wenn sie fertig ist,
halt die Speicherbereich den Rahmen der aufrufenden Funktion wiederherzustellen.
Das ist also dann das essentielle Konzept,
um Unterprogramme und Funktionen
zu realisieren
Was jetzt noch fehlt, Datenstrukturen haben wir gehabt,
wir haben die Rechenbefehle gehabt und wir haben Funktionsaufrufe gehabt.
Was jetzt noch fehlt, sind die Datentypen.
Das ist jetzt hier an den Beispielen nicht so besonders vertieft,
weil wir halt aus Gründen der Einfachheit alles immer nur mit diesen 32 Pit-Integren gemacht haben.
Tatsächlich gibt es dann natürlich auf jeder Maschine meistens noch
unterschiedliche Datentypen und ich würde mal vermuten,
hier soll ein D stehen oder Eldi oder sowas.
Wer das im Skript korrigieren möchte,
gibt die ganze Zeit Typen und dann Floating Point oder Gleitkommandatentypen
unterschiedlicher Größe
Die meisten Maschinen bieten halt Byte, 8-Bit, als einzelnen Datentyp an.
Und ansonsten sowas wie Word oder Double Word,
also 2-Byte, 16-Bit oder 4-Byte, 32-Bit-Datentypen.
Und Gladkomma ist heute meistens standardisiert. Sie erinnern sich,
ein Triple-I 7,54, Flotformat,
ein Double-Format mit 32-Bit und 64-Bit und gegebenenfalls noch höhere Genauigkeit.
Zum Beispiel bei Intel
eben 10-Bit Long Double
Und diese Dinger werden dann halt mit den Maschinenbefehlen,
wenn es welche dafür gibt, direkt modifiziert.
Das sind also dann einzelne Variablen. Das ist nicht weiter spannend
. Das ist genauso, wie man das erwarten würde.
Aber dann fehlen ja jetzt noch interessante Datenstrukturen.
Das Einfachste sind dann Race-Felder, eindimensionale Matrizen.
Die Deklaration hier in C wäre so. Wir haben also
einen Datentyp Tee und dann haben wir einen Harry.
Das ist also das A, ist der Name des RAs.
Das Tee ist der Typ zum Beispiel Double
Und dann halt hier A. Und dann müssen wir noch angeben,
wie viele Elemente da drin sind.
Bei C ist das statisch, da müssen das hier Konstanten sein.
Bei anderen Sprachen kann man das auch dynamisch machen. Das bedeutet also,
wir kaufen uns ein Array ein mit N-Elementen vom Datentyp T.
Und der Datentyp T kann jetzt wieder irgendwas sein,
was wir schon kennen. Also elementare Datentypen und gegebenenfalls eben,
das kommt ein bisschen später, Datentypen,
die ihrerseits zusammengesetzt sind
Ein paar Beispiele. Hier, Char, als Datentyp für
Einbyte. Und dann haben wir halt hier den Datentyp,
hier den Namen. String ist wahrscheinlich kein so schöner Name,
all das gegebenenfalls mit irgendwelchen vordefinierten Namen überkonflikt steht.
Aber es deklariert halt einen Array mit zwölf Elementen vom Datentyp
Char, die jeweils einen weit groß sind.
Und im Speicher bedeutet das dann,
dass abhängig von einer Stadt, ausgehend von der Startadresse,
zwölf Elemente im Speicher angelegt werden.
Und die Konvention bei C ist halt,
die Elemente liegen an der Adresse X plus
Wollte, so sagen wir, erste Element. Und X
plus 11 ist dann das letzte von diesen zwölf Elementen.
Und die Speicherstelle, die Speicheradresse hinter dem RAS, dann halt X plus 12.
Weil jedes einzelne Element von diesen Dingern halt ein
Bei groß ist. Wenn wir unseren Datentyp 32-Bit-Integer nehmen,
dann haben wir halt hier Datentyp, hier den Namen Val.
Das ist also jetzt ein RA mit fünf Elementen.
Und das wird im Speicher abgelegt, da wo wir gerade anfangen,
also an der Adresse Startadresse X, dann die Bytes,
die Adressen X plus 0,
also X bis X plus 3,
sind für den ersten Integer
X plus 4 bis X plus 7 für den zweiten.
Und dann hier X plus 16 bis X plus 19 für den fünften Wert.
Und ab X plus 20 ist es wieder frei im Speicher.
Und wie die einzelnen Werte im Speicher abgelegt sind,
das ist dann halt die einzelnen Bytes, wieder Little-Andien oder Big-Andien.
Da gibt es dann wieder beide Möglichkeiten. Aber das ist
dann halt auf einer Maschinenarchitektur in sich konsistent und einheitlich.
Bei Doubles haben wir halt acht Bytes pro Double.
Was wir dann hier haben, ist eine Reh mit
dem Namen A, mit vier Elementen.
Und das erste Element liegt dann halt hier.
Das zweite Element dort und so weiter.
Also zweite und das dritte Element. Und damit sind dann
die vier Elemente voll
Hier jetzt wieder dieses komische Sternchen als Symbol für Pointer in C
oder sagen wir Referenzen, letztlich also Speicheradressen.
Und bei 32-Bit-Modus 32-Bit-Adressen braucht halt jede Speicheradresse vier Bytes im Speicher.
Das wäre also jetzt ein Array hier mit drei Elementen vom Datentyp Pointer auf Char.
Ein Charl, seinerseits hat natürlich nur einen Byte,
aber einen Pointer auf einen Char, das ist halt eine Adresse im Speicher,
braucht halt an dieser Stelle vier Bytes.
Wenn man eine 64-Bit-Architektur haben,
dann würde halt ein Pointer auf ein Zeichen acht Bytes belegen,
auch wenn das Zeichen selbst nur ein Byte groß ist
Das ist soweit hoffentlich plausibel. Und der genaue Wert von X,
das werden wir noch sehen, wo die Dinger dann im Speicher abgelegt werden.
Das könnte zum Beispiel jetzt eben als lokale Variable
innerhalb einer Funktion einen Bereich auf dem Stack sein.
Für diejenigen, die sich so ein bisschen mit C auskennen,
halt so ein kleines Quiz, was jetzt eigentlich da passiert.
Wir haben also jetzt hier unser RA mit fünf Elementen vom
Typ Intager und startet an einer Adresse X und
der Inhalt der einzelnen Intager ist also jetzt eins
Das ist also jetzt der numerische Wert des ersten Intengers, Value von Null.
Dann hier, weil das die Indie-CSNC mit Null anfangen.
Fünf wäre dann halt der Inhalt, numerische Wert von Value von
1. Und 3, der numerische Wert von Value von 5.4.
Das ist dann also genau das, was hier gezeigt ist.
Das ist der Zugriff auf den Wert des vierten Elements in diesem AA,
der Datentyp ist Int und der Wert ist 3. Also 0,
1, 2, 3,
4. Das ist also das fünfte Element,
kriegt den Index 4 und
der Inhalt ist
Was ist jetzt, wenn wir im Code schreiben will?
Ja, weil es halt hier deklariert als R.R. auf Intager.
Und tatsächlich bedeutet das dann, also sowas wie Array von irgendwas,
ist in C-Äquivalenz zu Pointer auf irgendwas.
Das ist also jetzt hier der Datentyp, ist Pointer auf Integrer.
Und der Wert ist die Startaddresse des gesamten Arrays.
Das ist also hier X. Jetzt kommen wir zu der Pointer
Arrayth Metik, eine beliebte Fehlerquelle in C.
Was ist jetzt hier Value Plus 1?
Also wer zunächst mal sieht ja aus wie so ein Integrer.
Und wenn wir da Plus 1 nehmen, dann würden wir,
was heißt X plus 1 erwarten
Aber das stimmt nicht. Der Compiler weiß,
dass Vell vom Typ Pointer auf Integer ist.
Und wenn man auf einen Pointer von Integer irgendwas drauf addiert,
dann wird halt die Größe des Datenelements analysiert.
Hier ist das halt ein Integer, der ist vier groß.
Deswegen ist Value plus eins, nicht X plus eins,
sondern X plus vier. Hier wird das Ähnliches gemacht,
weil von zwei wäre halt jetzt hier dieses Datenelement.
Und der Und-Operator holt uns halt die Adresse von dem Ding.
Die Adresse des zweiten Elements in dem RAV ist X plus
acht,
das ist genau dieses hier
Welphon 5 ist durchaus legal, das hinzuschreiben. Es ist natürlich dann
klarer Programmierfehler, weil wir haben zwar fünf Elemente reserviert,
aber die InDC-Staaten bei null, sodass Welphon 5 hier wäre.
Und ob man das zugreifen darf oder nicht, das hängt dann davon ab,
ob man das als Glück oder als Pech wertet.
Wenn man Pech hat, lässt der Compiler das zu und
da wird halt hier irgendein Datenelement zugegriffen,
was möglicherweise initialisiert wurde oder was jemand anderem gehört.
Und wenn man Glück hat,
dann ist das hier ein Speicherbereich,
der im Moment noch nicht belegt ist und wo
das Betriebssystem erkennt,
dass was schief geht
Gut, dann kann man noch so was machen wie Value Plus 1.
Das ist halt jetzt wieder diese Pointer-Arenmetik. Das liefert X
plus 4. Das ist dann halt irgendwo hier. Das ist
die Adresse von diesem Element. Und wenn man jetzt mit
dem Sternenapparator dazu zugreift, auf den Inhalt des Pointers,
dann würde man halt hier den Datenwert 5 bekommen.
Gut, die Sprache C ist dann doch so wichtig,
dass man früher oder später wahrscheinlich so ein bisschen Kontakt damit bekommt.
Von daher ist es, glaube ich, nützlich,
sich diese Dinger mal zu merken,
auch wenn man später mit einem besseren Programmiersprachen umgeht,
wo man insbesondere diese Pointer-Arenmetik halt gar nicht zur Verfügung hat
und sich nicht um die Fehlerquellen damit kümmern muss.
Ja, klar
Das geht jetzt gleich. Ja, genau. Der Punkt ist,
Vell ohne weitere Schreibweisen, also hier nach dieser Deklaration weiß
der C-Com-Piler, dass es sich um eine Reihe handelt.
Mit fünf Elementen. Und der Datentyp ist jeweils Intida.
Wenn wir nur dieses hier schreiben,
dann weist der Compiler halt, das ist ein Array.
Und letztlich verwaltet der Compiler das einfach als Pointer
auf irgendwas,
als Speicheradresse
Wenn wir dann hier dieses Plus machen und der Compiler weiß,
das ist irgendwie ein Array oder ein Pointer, dann wird halt die Arratmetik durchgeführt,
aber abhängig davon, wie groß das einzelne Element ist.
Also wenn wir, was weiß ich,
Hugo Plus 1 schreiben und Hugo hat einen Datentyp,
der 70 beides, dann ist Hugo Plus 1 eben Originaladresse plus 70.
Das weiß der Compiler, weil der, man kann halt
hier nur Daten benutzen, die man vorher mal deklariert hat. So,
und hier könnten wir jetzt mal so eine Klammer drum schreiben,
also der Adressoperator von dem Ding hier,
die Adresse von Vell wäre halt, was weiß ich,
X, aber jetzt haben wir hier nicht Vell,
sondern wir haben welleckige Klammer auf,
zwei.
Das ist also jetzt das zweite Element in dem RA
Und die Adresse davon ist halt dann die Adresse des zweiten
Elements in diesem Array. Und das ist dann halt,
hier ist das Nulte-Element,
da ist das Erste und da ist das Element mit in X 2. Genau,
also dieses ist letztlich dasselbe wie Well plus 2. Ja,
also es gibt dann halt einige Regeln,
wann man was schreiben darf und wann nicht und wann man was klammern muss und nicht.
Da kann man sich auch ganz böse ins eigene Bein schießen.
Aber ja, im Wesentlichen ist das hier auf dieser Ebene
erstmal dieses hier,
ist Äquivalent zu Well plus
Also zumindest in nacktem Zeh kann man das so sagen.
C Plus ist dann so ein bisschen in der Lage,
sich selbst zu schützen.
Und dann muss man dann mit irgendwelchen Dynamic-Cast oder so anfangen,
um den Compiler davon zu überzeugen, was man genau haben möchte.
Als Beispiel jetzt hier ein minimal komplizierteres Code-Beispiel.
Wir haben jetzt hier einen Datentyp deklariert,
der die Postleitzahlen mit jeweils fünf Inter-Jet-Ziffern abbildet,
also amerikanische Postleitzahlen.
Und das sind also jetzt hier jeweils
so ein Zip-Dick
Ein Array mit fünf Elementen vom Typ Intiger. Und jetzt
kaufen wir uns Arrays von diesem neu definierten Datentyp ein,
für Kanye G. Mellen,
für das MIT und University of California und packen entsprechend dann hier mit diesen Geschweifenklammern,
dass es dann halt Shortcut zur Initialisierung initialisieren halt unsere drei eingekauften Arrays.
Das startet jetzt aus Gründen des Beispiels mal an der Adresse 16.
Und das heißt, die ersten,
das erste Array für Kanye G. Mellen,
das liegt jetzt ab Adresse 16 im Speicher
Das nullte Element,
eins liegt dann hier von Adresse 16 bis 19,
die 5 von 20 bis 23 und so weiter,
bis halt hier das Ende des RAses halt bei 35.
Und der Compiler hat keinen Grund, dazwischen jetzt irgendwie Platz zu reservieren.
Das heißt, das nächste RA, was wir eingekauft haben,
wird einfach fortlaufend mit dem Speicher belegt.
Ich wäre dann also jetzt hier ab Adresse 36 bis 55.
Und die letzte Postleitzahl ab 56 bis 75. Wenn man halt sowas schreibt,
bekommt man im Speicher sowas in dieser Art fortlaufende Integer-Werte,
die halt dann an dieser Stelle auch gleich schon initialisiert
worden sind
So, was passiert jetzt auf der Ebene des Assembler
Kurzermann darauf zugreifen will? Wir haben jetzt hier eine Funktion,
Get Digit, die so eine Postleitzahl Herey übergeben bekommt.
Also dieses Z ist jetzt von dem Typ Zyptik,
den wir eben als der Reihe von fünf Elementen von Internet definiert haben.
Und jetzt wollen wir eine Ziffer aus diesem Erey uns rausholen.
Das Ding sollte also hier in dieser,
in dieser Offset-Index von 0 bis 4 sein,
weil die ja nur fünf Elemente haben.
Und was wir machen wollen,
wir wollen von dem Erey das Element Nummer Digit zurückgeben
Gut, das ist jetzt auf der Assembler-Ebene, sagen wir
mal, trivial. Wir übergeben also jetzt die Startadresse,
den Pointer auf dieses RA und einem Offset,
also den Index in dieses RA.
Wie bekommen wir den Wert? Einfach so. Wir machen einen Move,
wir greifen auf den Speicher zu und zwar an welcher Adresse.
Das ist also jetzt hier das EDX, das ist die Startadresse des RAs.
Und was wird dazu addiert? Der Wert von EAX,
das ist also jetzt hier der Index in dem RA und das Ganze hier,
dieser Blog geschiftet, multipliziert mit vier,
weil halt das einzelne Datenelement in unserem Array jeweils vier Bytes groß ist.
Das ist also eine Speicheradresse und was wir aus dem Speichern der Adresse rausholen,
das packen wir in das Register ERX oder irgendein anderes Register.
Das heißt, der Errätzugriff, dieses hier ist jetzt tatsächlich
nichts anderes als die passende Speicheradresse auszurechnen und durch diese komische
Adressierungsart bei dem Intel geht das halt in einem einzigen Befehl.
Basisadresse, wo das RAI anfängt im Speicher,
plus der Index des Elements und dann halt hier dieses mit vier skaliert,
sodass wir dann halt tatsächlich die richtige Adresse im Speicher herausbekommen
Man sieht an dieser Stelle auch schon, dass offensichtlich
der Compiler darauf verzichtet hat, da irgendwelche Bereichchecks einzubauen.
Wenn wir halt diese Funktion aufrufen und hier minus 70 übergeben,
dann hat er überhaupt kein Problem, damit hier die Adresse des RAS auszugeben,
dann nach minus 70 mal 4 zu nehmen.
Damit kommen wir halt minus 280 und greifen irgendwo weiter vorne
in dem Speicher zu, ohne dass die einen Compiler das kümmert.
Der häufigere Fehler ist natürlich, dass man genau um ein
Element zu weit zugreift. Und was dann passiert,
da kann man Glück haben und merkt den Fehler oder
man hat halt Pech und merkt den Fehler,
er ist Tage oder Wochen später und hat dann echte Probleme
beim D-Bargängen
So, ein kleines, etwas komplizierteres Beispiel,
was man jetzt hier mit diesem Zugriff machen kann.
Diese Funktion soll jetzt halt so ein Zip-Code-Postleitzahlen nehmen und aus
den einzelnen Ziffern zusammenbauen, zu einem größeren Integer.
Das heißt, was wir jetzt machen wollen, ist,
wenn wir hier sowas in Array haben wie 1,5,
2,1, 3,
dann wollen wir das halt zu dem dezimalen Wert
15.213 zusammensetzen
Wie machen wir das? Was man wahrscheinlich in C
schreiben würde, ist so hier so eine Schleife.
Die läuft von null bis zum Element 4,
also eh kleiner 5 und geht halt durch die Relemente durch.
Und was machen wir? Wir haben halt hier unseren Zwischenwert,
sagen wir mal so ein Akkumulator, der das aufsummiert.
Und was machen wir? Wir nehmen jeweils den Wert,
den wir bisher schon hatten, mal 10 und addieren dann die aktuelle Ziffer dazu.
Da das halt von links nach rechts geschrieben war,
funktioniert das halt. Das würde dann halt anschließend hier
den numerischen Wert der Postleitzahl berechnen und dann zurückgeben
Das ist also das, was ein Programmierer schreiben würde,
das wäre halt,
also zumindest für mich ist das naheliegend die Schleife so zu bauen,
die einzelnen RAs hier mit dem Index-Apparator zuzugreifen und dann halt hier die Berechnung zu machen.
Wenn man solchen Code schreibt, dann ist man gegebenenfalls überrascht,
was der Compiler daraus macht.
Das hängt jetzt auch so ein bisschen davon ab,
welche Optimierungsstufe man eingestellt hat und wie clever der Compiler ist.
Aber das, was hier gezeigt ist,
wäre ganz typisch dafür, was der Compiler tatsächlich aus
diesem Code macht
Der möchte nämlich diese ganzen Index-Berechnungen hier weitgehend vermeiden.
Und stattdessen mit einem Pointer sukzessive auf die einzelnen Elemente des Arays zugreifen.
Was der Compiler also tatsächlich macht, ist dieses Hier.
Wir kaufen uns weiterhin diese Zwischenvariable ein, in der wir
die Zwischenergebnisse aufaddieren und dann jeweils mit zehn multiplizieren und sowas.
Aber die Schleife wird jetzt ersetzt, indem wir halt diesen
Index komplett eliminieren. Also das I ist jetzt weg.
Stattdessen hat der Compiler erkannt, die Schleife läuft von 0
bis 5 und greift aufeinanderfolgende Elemente in diesem Aray zu
Also können wir die Schleife so umschreiben,
dass wir die Abbruchbedingung einfach vorher vorberechnen, indem wir das letzte Element,
das wir zugreifen wollen, indem RA schon mal vorberechnen.
Und was passiert dann? Wir berechnen jetzt weiterhin diesen Zwischenwert hier,
als zehnmal den Wert, den wir schon hatten.
Und jetzt plus den Wert, der sich aus dem aktuellen Pointer ergibt.
Das würde also jetzt hier, wenn das die Funktion aufgerufen wird,
zunächst mal hier mit dem Startwert anfangen, der uns übergeben worden ist.
Und dann wird hier diese Pointer-Erithmetik gemacht,
also Z plus plus ist jetzt nicht etwa Adresse plus 1,
sondern weil der Compiler weiß, dass es sich um Integry handelt,
wird halt hier jeweils um Plus 4 gerechnet.
Und dann ist die Abbruchsbedingung jetzt auch modifiziert worden,
statt halt hier bis I ein Zwischenvarial bei I zu haben und dann hier Index-Adressen auszurechnen,
wird einfach verglichen, ob der aktuelle Pointer,
den wir da haben,
die Adresse noch kleiner ist als die Endadresse.
Und wenn die Endadresse überschritten wurde, dann sind wir fertig
und dann wird der berechnete Zwischenwirt zurückgegeben. Kurze Rückmeldung,
sieht irgendjemand, oder ist das plausibel,
dass dieser Code sich hier, oder der Code hier denselben Wert berechnet,
wie dieser Code, Gegenfrage, wer sieht das nicht?
Ja, das ist wahrscheinlich auch überraschend. Also wer
sich noch nie mit Pointer und Pointer-Arrathmetik auseinandergesetzt hat,
der sieht das jetzt tatsächlich zum ersten Mal.
Also was wir machen wollen, wir wollen halt die einzelnen
Elemente dieses RAS nacheinander durchgehen.
Also dieser Code hier ist wahrscheinlich für die meisten plausibel.
Wir gehen die einzelnen Elemente durch. Unsere Zwischenergebnisse wird mit
zehn multipliziert. Das ist dann halt die nächste Dezimalstelle.
Und der eine wird dann hier da drauf addiert und
dann wird das halt sukzessive zusammengebaut
Das heißt, wir fangen an mit eins,
dann bauen wir 15 daraus, dann waren wir 152,
1.521 und zum Schluss 15.213 und dann sind wir fertig.
Aber statt jetzt hier jedes Mal zu berechnen,
RAI und dann das Nultelement, RAI und das Erstelement,
RAI und das zweite Element RAI und das dritte Element und hier ist es noch einfacher,
aber bei komplizierteren RAIs, insbesondere bei Matrizen oder sowas,
muss man halt jedes Mal diese Adresse auswerten.
Stattdessen kaufen wir uns einen Pointer auf das erste Element und wenn man die eins ausgewertet haben,
dann schalten wir diesen Pointer auf das nächste Element ohne den Index neu zu berechnen,
kaufen es dann halt, greifen auf die 5 zu,
dann schalten wir den Pointer weiter, greifen auf die 2 zu,
schalten auf die nächste Element einfach,
indem wir die Adresse jeweils um 4 erhöhen und dann dieses noch.
Und wenn wir dann die Adresse nochmal erhöht haben,
dann haben wir diese Abbruchbedingung erreicht,
die der Compiler für uns vorberechnet hatte.
Ich empfehle also, sich das mal anzuschauen,
dass das halt tatsächlich algorithmisch denselben Wert liefert.
Und das machen die Compiler heutzutage ziemlich aggressiv,
weil wir halt an dieser Stelle zum einen die Variable I
eliminiert haben und zum anderen hier diesen Array-Zugriff ersetzt haben,
durch einfach einen Pointer plus 4
und damit das nächste Element direkt aggressieren können
Wenn man sich dann den gesamten Gut anschaut,
jedenfalls den entscheidenden inneren Teil dieser Schleife, dann ist das
hier also das, was der Compiler zwischendurch daraus macht.
Und jetzt ist hier annotiert, was jetzt eigentlich die einzelnen Register,
in denen Assembler-Code hier unten dann tatsächlich repräsentieren. Das heißt,
in dem Register EC ist der Wert von Z,
also die Startadresse des RS,
ZI ist dann das Integer-Variable,
in der
diese Postleitzahl aufsummiert wird
Und das Register BX enthält den vorberechneten Pointer für die Abbruchbedingung,
also für das letzte Element des Erräts, das wir zugreifen wollen.
So, und jetzt ist hier dann noch diese komische Multiplikation mit zehn.
Werde ich noch an die zweite Woche der Vorlesung oder so
erinnert, weiß, dass wir da gegebenenfalls tricksen können.
Diese Multiplikation durch irgendwas Einfacheres ersetzen. Und der genug Compiler ist tatsächlich
clever genug, um das zu versuchen. Das heißt,
hier kommt keine Zehen oder irgend sowas in dem Code mehr vor,
weil diese Multiplikation zehnmal ZE plus irgendwas ersetzt wird durch zweimal
ZE plus viermal
Fünfmal irgendwas, zweimal fünfmal ZI, wird genommen statt dem hier.
Und warum wird das gemacht? Weil wir dieses hier halt
tatsächlich mit diesem Lot-Effective-Adress-Befehl in einem Takt bequem erledigen können.
Was also aus diesem Code auf der Assembler-Ebene wird,
ist unter der Annahme, dass halt die Adresse,
Startadresse des Herr Reis in dem Register EC drinsteht.
Ja, wir wollen den Wert von ZI hier auf Null setzen.
Wie machen wir das? Das kennen Sie auch schon aus den
ersten Wochen der Vorlesung, dass die X-Operation irgendeinen Registerwert,
Speicherwert mit sich selbst gex-ordnet, X-Ort liefert halt null.
Stattdessen könnten wir wahrscheinlich auch Move L null irgendwo hinschreiben, aber dieses ist kompakter.
Und dann berechnen wir den Endwert des RAs. Das ist also in diesem Fall,
weil die Schleife ursprünglich bis fünf ging,
können wir jetzt halt hier die Endadresse in dem RA vorberechnen und packen das in das Register EBX.
Das ist also dieses hier. Und wie kriegen wir die Endadresse,
ja, indem wir halt einfach die Startadresse, das war der hier,
plus Das nullte Element ist halt ECX plus null, 4 plus ECX8,
12, 16, 16 plus ECX ist also die Adresse des vierten Elements.
Und das ist dann halt hier die Abbruchbedingung für unsere Schleife. So,
und in der Schleife wird jetzt halt Load-Effect of Adress gemacht,
das ist jetzt halt der Witz, EAX plus EAX mal 4,
das ist also dann eher X plus 4 mal eher X,
das ist also fünfmal EA X, das ist dieser Wert fünfmal ZI,
das kommt in das Register D, als Zwischenspeicherung,
dann holen wir uns den, das aktuelle Element aus dem RAI,
das nimmt also jetzt hier den Wert von Z als Adresse,
adressiert damit den Speicher und das, was wir aus dem Speicher rausholen,
das ist also jeweils das aktuelle Element aus dem RAI packen,
wenn das Register EAX.
Dann kommt hier die Pointer-Arrithmetik, dann wird der Pointer um vier erhöht.
Das ist also dann, Pointer zeigt jetzt auf das
nächste Array-Element und dann kommt halt diese Berechnung hier.
Das ist jetzt wieder so ein Lot-Effekt auf Adress.
Die aktuelle Stelle plus zweimal den Wert,
den wir hier reingetan hatten, was fünfmal ZI war,
das ist also jetzt hier tatsächlich zehnmal ZI plus das
aktuelle Element
Das ist dann dieses hier und dann kommt der Vergleich,
ist der aktuelle Pointer, den wir jetzt hier ständig hochzielen,
immer um plus vier,
haben wir schon das letzte Element erreicht und das hatten wir halt hier reingespeichert und solange wir das noch nicht gemacht haben,
springen wir halt wieder zurück und nehmen die nächste,
das nächste Element aus dem Array.
Abgesehen von diesem leicht kranken Optimierung, um die zehnmal irgendwas zu ersetzen,
durch zweimal, fünfmal irgendwas, ist der entscheidende Punkt halt,
dass wir jetzt keine RA-Indizes mir berechnen müssen,
sondern dass wir sukzessive in dem RA einfach von einem Element zum nächsten fortschreiten,
indem wir halt so einen Pointer reservieren,
den jeweils von einem auf das nächste Element fortschreiten lassen und mit dem Pointer hier direkt als Adresse den Speicher adressieren und uns den Inhalt holen.
Das lohnt sich auch über diese paar Folien nochmal drüber zu gehen,
mit RAys hat man dann früher oder später doch eine ganze Menge
zu tun und dann kann man halt schon mal sehen,
was, also insbesondere weil man versucht, da solchen Code mal zu debuggen
, Muss man darauf gefasst sein, dass halt die ursprünglich
geschriebenen RA-Referenzen an dieser Stelle aus dem Krut komplett entfernt worden sind.
Aber natürlich, das Verhalten ist Äquivalent,
es berechnet genau die gleichen Dinge und in der gleichen Reihenfolge.
Das waren also eindimensionale RAs, dann kann man sich als nächstes fragen,
wie sieht es aus, bei mehrdimensionalen RAs,
also insbesondere bei zweidimensionalen RAs, also Matritzen.
Da wird es schon ein bisschen spannender.
Da gibt es nämlich drei verschiedene Möglichkeiten,
die auch tatsächlich alle genutzt werden und zwar von verschiedenen Programmiersprachen
Das kann einem zunächst mal egal sein,
aber wenn man halt in dieser Situation ist,
dass man, was weiß ich, ein Java-Programm schreibt,
in dem man eine C-Funktion dazulinken möchte,
dann hat man halt schon das Problem,
dass die Repräsentation von diesen Matrizen in Java anders ist als in C.
Und wenn man dann möglicherweise merkt,
ich habe noch eine Vortragsbibliothek rumliegen,
die noch ein bisschen schneller ist,
dann hat man da das Problem,
dass Vortragen oder Madlab halt noch wieder
eine andere Repräsentation der Matrix im Speicher nutzen
Die wesentlichen Sachen sind halt,
was man zunächst mal trial machen kann, ist,
wenn man eine Matrix hat, was weiß ich,
halt so ein Schachbrett mit acht mal acht Feldern,
dass wir einfach acht mal acht Elemente im Speicher reservieren.
Und da halt die Datenelemente drin sind. Und dann ist die Frage,
macht man das zahlenweise oder macht man das spaltenweise?
Und beide Möglichkeiten kommen halt vor und sind mehr oder weniger
auch gleichwertig. Das gibt es also beides und das
ist halt auch kompakt
Und dann gibt es einige andere Sprachen, insbesondere Java,
die das nicht so machen, sondern die nur eindimensionelle Arays kennen.
Und stattdessen halt statt so einem Schachbrett mit acht mal acht Elementen direkt,
würden sie ein Aray für jede Zeile des Schachbretts anlegen und
dann aber davor ein zweites Aray oder letztlich ein neuntes Aray,
das die Adressen der acht unter RAys enthält.
Das hat Vorteile und Nachteile. Man kann insbesondere auch Dinge kreieren,
die nicht rechteckig sind. Also was weiß ich,
so eine dreieckige Matrixstruktur wäre möglich und man kann zum Beispiel,
wenn halt mehrere Zahlen nur aus Nullen bestehen,
da dann halt diese Zeile nur einmal abspeichern
Das wäre das Java-Beispiel. Wir deklarieren jetzt hier einen Datentyp als zweidimensionales RA.
Der kriegt also jetzt hier zwei von diesen rechteckigen Dingern übergeben.
Und in Java muss man das nicht direkt mit festen Größen initialisieren,
sondern das kann man dann halt zur Laufzeit tun.
An dieser Stelle also dynamisch. Wir kaufen uns ein RA
mit einer gegebenen Anzahl von Reihen und einer gegebenen Anzahl
von Spalten
Und um das Ding mit Leben zu füllen, haben wir
jetzt hier zwei geschachtelte Schleifen. Die erste geht halt durch
die Reihen durch und hat halt die Länge der Matrix.
Das ist also dann von hier nach da.
Und innerhalb jeder Reihe füllen wir die Spalten auf.
Und da wird das dann halt an dieser Stelle schon interessant.
Wir können jetzt halt die Größe, die Anzahl der Zeilen
abfragen und damit dann halt anschließend durch die Dinger durchgehen.
Und hier wird jetzt einfach mal hundertmal der Wert der Reihe plus der Werte der Spalte eingetan.
Und was dann im Speicher herumliegt, sind mehrere eindimensionale RAS,
nämlich jeweils eins für jede Zeile der Matrix.
Die haben dann halt hier die Werte 0-1,
2, 3, 100, 100, 100,
2100, 203, weil sie hier so gefüllt worden sind.
Und wie findet Java die komischen Errätsungsspeicher,
indem es halt ein weiteres RA hat,
an das man als Benutzer gar nicht so richtig herankommt.
Es gibt halt irgendwo eine Referenz auf das erste Element,
dieses höherdimensionales, des Spalten-RAS letztlich und der Spalten-RA ist enthert
Referenzen auf die einzelnen inneren RAS
Das kann man machen und auf diese Art und Weise könnte man eben halt auch,
was weiß ich, die einzelnen Zeilen unterschiedlich lang machen.
Also dem hier zum Beispiel zehn Einträge, zehn Spalten verpassen
und den hier nur auf zwei Spalten oder sowas verkürzen.
Und Zugriff auf die Elemente ist dann halt hier in Java,
indem man halt direkt die Aray in die Cs angibt.
Was dann das Ja war im Hintergrund daraus macht,
das kriegt man nicht so leicht zu sehen wie bei C,
weil man da halt normalerweise nicht so schnell in den Assembler-Code reinkommt.
Aber das ist eine Möglichkeit, um halt mehr dimensionale Arays,
um Speicher abzulegen. Und wenn wir jetzt hier nicht in zwei,
sondern ein dreidimensionales Arey hätten,
dann hätten wir halt so ein Master Aray,
was auf die nächsten Referenzerreiß zeigt und die wiederum haben dann
die einzelnen Arays mit den echten Elementen
Ich schätze mal, dass ich schon die ersten Dinge dieser
Art in Yava programmiert habe. Die Frage ist halt,
ob sie dann auch gemerkt haben,
wie das im Speicher liegt, letztlich dann halt hier sowas,
also das erste Array enthält Pointer,
auf die dann die echten Daten sind halt in dem niedrig dimensionierten,
eindimensionalen RAy drin.
InC sieht die Notation sehr, sehr ähnlich aus,
nur dass man das halt hier mit konstanten dimensionieren muss.
Hier halt eine Matrix mit, ich frage mal jetzt hier
vier Zeilen und fünf Spalten die einzelnen Elemente liegen dann fortlaufend im Speicher.
Also das erste Element hier, der erste Integer mit Index 00,
wäre halt ganz vorne und dann kommt halt die erste Zeile fortlaufend.
Also erste Zeile, erste Spalte, erste Zeile, zweite Spalte und so weiter.
Dann die zweite Zeile und dann die letzte Zeile irgendwann.
Und genauso natürlich, wenn das irgendwie eine andere Größe hat,
dann halt hier jetzt die Elemente einfach mal so durchmoderiert.
Jetzt können wir die Werte wieder zugreifen,
indem wir tatsächlich diese eckigen Klammern nehmen als Index-Operator Oder alternativ,
indem wir diesen Adressoperator nehmen, also jetzt hier den Inhalt einer Adresse.
Also Matrix ist halt in diesem Fall jetzt ein Pointer auf
das allererste Element dieses Herr Reis, dieses Feldes.
Und Matrix von null wäre dann halt die erste Zeile.
Matrix von null plus null ist dann also das allererste Element.
Matrix von null plus 1 ist die erste Zeile und das erste,
das Element mit InX 1 da drin und so weiter.
Matrix von 3 wäre halt dann das Element,
was dem Anfang der dritten Zeile markiert,
plus 4 wäre dann halt das letzte Elementieren
Und genauso kann man dann halt mit diesem Adressoperator arbeiten,
kann sich halt die Adresse eines bestimmten Elements in der Matrix holen.
Wenn man jetzt Pointer-Arrathmetik in so einem Array betreiben will,
dann würde halt Element Plus Plus,
das ist jetzt halt ein Pointer auf dem Int,
halt die Adresse um vier erhöhen, plus vier Bytes,
das würde also zum Beispiel von hier nach hier,
nach hier, nach nah, nach da, nach da gehen.
Wenn wir jetzt von hier zum Beispiel auf die nächste Spalte gehen wollen,
dann können wir das einfach dadurch erreichen,
dass wir halt diesem Pointer sagen,
Pointer Plus jetzt halt die Anzahl der Elemente in einer Zeile plus dieses hier,
würde halt von hier nach da die passende Adresse liefern.
Gut, also das ist hier letztlich im Speicher zellenweise orientiert.
Das ist das, was C oder C plus plus machen,
wenn man Vorträume oder Mattlab hat,
dann ist das spaltenweise sortiert und gut,
das eine nennt man halt Roam Major, das andere Culam Major.
Das ist mehr oder weniger Äquivalent,
je nach Operation ist mal das eine ein bisschen besser,
das andere. Der Speicherplatz, der da benötigt wird,
ist halt in beiden Fällen gleich
Gut, dieses hier werde ich jetzt aus Zeitgründen nicht groß diskutieren,
wenn man halt mehr dimensionale RS hat,
dann kriegt man halt entsprechend größere Datenstrukturen und muss sich darum kümmern,
wie man diese einzelnen Adressen berechnet.
Da können Sie sich mal so ein bisschen die Zähne dran ausbeißen,
wie diese RS hier tatsächlich dann indiziert werden,
weil man halt dedimensionale Arrays im Speicher ablegen möchte.
Das geht also auch. Muss dann halt nur die Indizes ein bisschen komplizierter ausrechnen.
Und wenn man sowas halt in Sprachen wie Java machen möchte,
dann kriegt man letztlich Arrays von Arrays, von Arrays,
irgendwann in der untersten Ebene eindimensionale Arrays mit den echten Elementen
Gut, das waren also jetzt die einzelnen Datenelemente und die RAS,
als letztlich die einfachsten Datenstrukturen, was man natürlich häufig hat,
sind Objekte oder eben Strukturen, die Daten verschiedener Elemente kombinieren.
Und das ist jetzt hier in den Folien wieder visualisiert an einem
mehr oder weniger typischen Beispiel, was die wichtigsten Anwendungsfälle tatsächlich enthält.
Wir haben also jetzt hier eine Struct,
ein Jahre würde man vielleicht sagen,
Klaas Rect und dann sind da halt Elementvariablen innerhalb dieser Struktur
innerhalb dieser Klasse,
könnten wir gegebenenfalls auch sagen,
Und zwar zum einen ein Intager.
Und dann ein RA mit drei Elementen vom Typ Intager.
Und dann noch eine Speicheradresse, also ein Pointer auf einen.
Wie liegt sowas dann im Speicher? Na gut.
Zunächst mal würde ein C-Compiler versuchen, die Dinge so platzsparend
wie möglich in der gegebenen Reihenfolge im Speicher abzulegen.
Das heißt, wir haben jetzt hier irgendwo im Speicher den
Anfang unserer Struktur. Und als allererstes kommt dann Intager.
Der wird also jetzt hier ausgehend von der Startadresse
in das Objekt in die Struktur hineingepackt
Dann kommt als nächstes ein RA mit drei Elementen vom Typ Integer.
Und das ist dann genauso, wie wir das eben durchgenommen haben.
Das kriegt dann halt hier ab der Adresse 4 liegt der erste Integer,
also A von Null, ab der Adresse 8,
dann A von 1. Und ab der Adresse 12,
A von 2. Und dann sind die drei Elemente voll.
Und bis zur Adresse, bis zum Offset 16,
ist dann alles belegt. Und dann kommt als letztes dieser
Pointer, das ist also bei 32-Bit-Adressierung auch ein 4-Bit-Wert.
Der enthält dann also eine Speicheradresse und das ist dann
halt hier von 16 bis 19.
Und ab 20
ist dann wieder frei
Wenn man jetzt hier auf die Elemente dieser Struktur zugreifen will,
auf der Assembler-Ebene, dann bedeutet das nichts anderes,
als dass man die passenden Adressen hier sich vorberechnen muss.
Also hier jetzt eine Funktion,
die in eine Struktur übergeben,
also in diesem Fall ein Pointer auf unseren Datentyp Reck übergeben
bekommt und zusätzlich einen Wert und was wir machen wollen,
diesen Internet-Wert wollen wir an diese Stelle
in die Struktur eintragen
Wir haben also jetzt hier als so ein Beispiel die Speicheradresse dieser Struktur im Speicher übergeben bekommen.
Das ist also der Pointer auf das allererste Datenelement hier.
Und dann haben wir hier den Wert, den wir hier eintragen wollen.
Und das geht dann ganz einfach. Wir nehmen halt den Wert,
also was wir tun wollen, ist,
dass wir diese Struktur Pointer dann,
also den Teil E in dieser Struktur,
mit dem wir jetzt setzen wollen.
Und was machen wir damit? Wir holen uns halt den Wert,
den wir schreiben wollen und packen den an der passenden Stelle in den Speicher.
Und jetzt wird man sich fragen, wieso ist denn EDX
die richtige Adressen? Das liegt halt einfach daran,
dass das Element E in dieser Struktur genau am Anfang liegt.
Der Pointer auf diese Struktur ist also genau der richtige Punkt,
um den Wert von EDA einzutragen.
Das ist also an dieser Stelle relativ einfach.
Und wenn wir jetzt, was weiß ich,
nicht hier einen zweiten Wert haben wollen,
dann würden wir halt EDX plus 4
berechnen wollen
Und das wird jetzt hier ein bisschen komplizierteres Beispiel mit demselben
Datentyp dann mal durchexerziert. Wir haben also jetzt hier wieder
den Datentyp Reck als Struktur mit diesen drei Bestandteilen.
Und jetzt wollen wir dieses hier machen. Wir kriegen einen Pointer
auf diese Struktur übergeben und wollen uns aus diesem RAA ein Element herausholen.
Das heißt, und zwar an dieser Stelle wird jetzt tatsächlich
nicht das Element selbst übergeben, sondern die Adresse.
Das ist also jetzt hier schon ein ziemlich finsteres
Ist ein Pointer auf irgendwas. Er pointe auf A,
ist also der Teil A, in dieser Struktur.
Und A ist jetzt ein RA, A von EDX,
ist also das IDX-Element in dem RAA.
Und das wollen wir zugreifen. Und dann holen wir uns aber nicht den Wert,
sondern wir holen uns die Adresse von dem Ganzen.
Das heißt, wir wollen jetzt letztlich hier,
was weiß ich, entweder diese Adresse für das Element Null,
diese Adresse für das Element 1 oder diese Elemente,
diese Adresse hier für das Element 2 holen
Und letztlich, um das IDEX,
das Element in diesem Array zu haben, zu holen,
müssen wir halt anfangen mit der Startadresse,
wo das gesamte Objekt, diese Struktur im Speicher liegt.
Das ist also hier das R. Und dann R plus 4,
weil das das erste Element des RAs ist oder der Start,
also das nullte Element des RAs.
Und weil jedes Element in diesem RA 4 Bytes groß ist,
ist halt die Adresse, die wir suchen, Startadresse der Struktur.
Hoffset des RAs innerhalb dieser Struktur,
das ist diese 4. Und dann halt 4 mal den Index
des Elements als Adresse des gesuchten Elements
Das ist kompliziert beschrieben. Im Messembler-Code ist das dann wieder
vergleichsweise einfach. Das Brigister EDEX enthält wieder die Adresse von
unserer Struktur. Und das Register ECEX enthält den Parameter,
also den Index des Elements hier, das wir adressieren wollen.
Das könnten natürlich in echten Szenarien mehr als nur gerade drei Elemente sein,
aber so passt das ganz gut auf die Folien.
So, was machen wir? ECEX,
multipliziert mit vier offsend 0, wie der Lord Effective Eddris,
das macht also nichts weiter als viermal ECX zu berechnen und packt das Resultat nach EAX.
Das berechnet also jetzt hier letztlich diesen Teil, den Offset innerhalb des RAS.
Und dann berechnen wir, wie der Lord Effective Eddris,
das berechnet also diesen arrethmetischen Ausdruck und packt das da rein.
Vier plus EDX ist also letztlich R plus vier und dann noch plus EAX,
das war dieses viermal EDX, das ist also jetzt tatsächlich dieser Ausdruck, der hier berechnet wird.
Und das kommt in das Register EAX warum man das in das Register ERX,
weil ERX halt bei Konventionen, die das Register ist,
in dem so eine Funktion ihren Rückgabewert hinterlegt.
Gut, jetzt können die Dinger tatsächlich natürlich auch Pointer enthalten,
die auch mitten in diese Struktur hineinzeigen. Hier ist also wieder der Datentyp und jetzt ist hier eine neue Funktion,
die den Pointer, der in diesem Datentyp drin ist,
auf ein bestimmtes Stelle setzen soll,
was die Funktion also jetzt tun will, ist dieser Struktur,
also er ist unser Pointer auf die Struktur,
er minus größer P ist,
also der Pointer auf das Element P innerhalb dieses Objekts R oder der Strukt R soll gesetzt werden auf die Adresse und welche Adresse ist das jetzt?
Er zeigt auf A von R nach I. Das heißt,
wir haben jetzt hier diese Struktur und greifen auf das Element I zu,
um den Index zu holen und das I-T-Element aus dem REA
wollen wir dann suchen und die Adresse des I-T-Elements in A
soll hier reingeschrieben werden
Das ist auch wieder komplizierter erklärt,
als das dann hinterher hier in einem Assembler-Code tatsächlich erforderlich ist.
Wir brauchen jetzt nichts weiter, als hier die Adresse auf unsere Struktur im Speicher.
Und zwar in diesem Fall wieder in dem Register EDX.
Was machen wir? Wir holen uns zunächst mal
den Inhalt von EDX und speichern das nach ECX.
Das ist also hier äquivalent zu Null plus EDX.
Das ist also tatsächlich hier derjenige,
DX ist der Zeiger auf das gesamte Ding,
aber eben gleichzeitig auch der Zeiger auf das allererste Element,
also das I. Und damit holen wir uns halt den Wert von I in das Register ECX.
Da müssen wir jetzt wieder Pointer Arathmetik betreiben.
Dieses R real, was wir ja zugreifen wollen,
enthält Elemente, die jeweils vier Bytes groß sind.
Deswegen wird jetzt hier der Wert, den wir hier genommen haben,
er zeigt auf ihn, also der Inhalt von I,
mit vier multipliziert,
wieder mit diesem Lot-Effective-Adress-Ding und
erstmal in dem Register ERX hinterlegt
Dann berechnen wir jetzt die passende Adresse. Das ist jetzt
Lord Effective Elys. Startadresse der Struktur im Speicher plus 4.
Das ist also die Startadresse hier von dem RA.
Und dann noch das entsprechend idee Element da drin.
Das ist also dann halt Startadresse des Objekts plus 4.
Das ist der Start des RAs plus das idee Element.
Das holen wir uns. Und die Adresse,
die wir dann berechnet haben, packen wir nach ERX rein.
Gut, und jetzt wollten wir diese Adresse halt tatsächlich dann
hier auch in dem Element selbst zwischenspeichern. Das heißt,
wir müssen jetzt hier auf die diesen Pointer zugreifen und den Pointer
mit dem entsprechenden Wert setzen, den wir jetzt hier gerade berechnet haben.
Und das ist dann natürlich nichts weiter als Startadresse des Objekts,
plus Offset des Pointers in dem Objekt,
denn das ist also hier,
streiter des Objekts und plus 16 ist die Startadresse von diesem Element P in dem Objekt.
Also 16 plus Startadresse ist genau da, wo wir hinwollen.
Und was packen wir da rein? Die Adresse,
die wir halt an dieser Stelle vorberechnet haben.
Ja klar
Das will ich jetzt nicht beschwören. Ich glaube,
das ist einfach, weil dieser Befehl halt diesen Offset erwartet,
dass man aus Syntaxgründen da die Null davor schreiben muss.
Also hier sind die Dinge auch. Es kann sein,
dass man das weglassen kann. Also dann ist das möglicherweise...
Ja gut, aber hier haben wir auch die D3-Operanten-Befehle wieder.
Ich will es nicht beschwören. Es kann sein,
dass der Assembler an dieser Stelle jeweils drei Dinge haben möchte.
Es kann auch sein, dass die Null einfach überflüssig ist
Also weil hier das Ding weggelassen worden ist.
Es kann sein, dass man das braucht,
es kann auch sein, dass man das weglassen kann.
Im Zweifelsfall einfach den Compiler in der virtuellen Maschine da,
die Andreas vorbereitet hat, mal ausprobieren und schauen, ob es geht oder nicht.
Das ist dann auch wie halt die Details,
denn der Syntax dieses Befehls, ich will es nicht beschwören,
es kann sein, dass es,
ich habe jetzt den 32-Bit-Compiler hier nicht dabei, sonst könnten wir es einfach ausprobieren.
Und dann sieht man halt, ob es geht oder ob
es nicht geht. Aber natürlich,
die Null ist letztlich halt überflüssig und wenn die Syntax das hergibt,
dann kann man sie halt
einfach weglassen
Ja, ich glaube, die zwei Folien,
hier zeige ich dann noch und dann machen wir Schluss.
Ein interessanter Punkt bei diesen Datenstrukturen ist, was passiert,
wenn man verschiedene Datentypen hat, die unterschiedlich groß sind.
Also insbesondere, was passiert, wenn wir,
was weiß ich, zum kleinen Daten,
die nur ein Beit belegen haben und dann irgendwas mischen mit Dingen,
die acht Beit groß sind, wird das dann fortlaufend im Speicher abgelegt oder nicht.
Und die Idee ist, dass das normalerweise nicht fortlaufend ausgerichtet wird,
sondern Die einzelnen Teile werden an Wortgrenzen ausgerichtet oder sogar ein Doppelwortgrenzen,
weil man halt sonst Probleme bekommt auf vielen Rechnern,
dass man halt diese Misserlined-Zugriffe hat.
Und deswegen erzeugt der Compiler dann Lücken und damit ist das
Objekt möglicherweise ein bisschen größer, als man eigentlich erwartet.
Dazu gibt es dann hier ein Beispiel. Wir haben jetzt
ja halt eine neue Struktur, einen neuen Datentyp.
S1 und der enthält zunächst mal ein Byte.
Dann ein Array von zwei Integern. Die sind also
jeweils vier Byte groß und dann ein Double mit acht Bytes
Und wie sieht das jetzt im Speicher aus? Man könnte sich jetzt also überlegen,
das war jetzt ja hier ausgehend von der allerersten Adresse,
im ersten Byte, das Zeichen dahin haben und dann die Intager
hier ab Adresse 1 bis 5 und ab Adresse 5 bis 7 haben.
Das passiert aber tatsächlich nicht, sondern diese Intager möchten auf Adressen ausgerichtet werden,
die zu ihrem eigenen Datentyp passend zu ihrer eigenen Größe.
Das heißt, die werden jetzt auf Vierer Adressen ausgerichtet. Das heißt,
hier ist ein Byte belegt und weil der Compiler das halt
in der Reihenfolge im Speicher ablegt,
in der das deklariert ist,
haben wir hier eine Lücke von drei Bytes,
die sind nicht benutzt
Dann wird hier der Intager abgelegt und noch ein Intito,
das passt dann wieder. Und jetzt sind wir hier bei zwölf.
Das Double-Ding braucht aber acht und möchte auch wieder ausgerichtet werden
an Adressen, die halt seine eigenen Größe entsprechen.
Das heißt, zwölf passt nicht, die nächste Möglichkeit,
die passt es dann halt hier 16. Und damit ist dann sichergestellt,
dass die einzelnen Datenelemente immer, sagen wir mal,
an geraden Adressen liegen,
die zu ihrer eigenen Größe passen.
Und man muss halt im Zweifelsfall in Kauf nehmen,
dass dann hier zwischen irgendwelche Lücken sind
Der Compiler weiß natürlich, wie er dieses Ding im Speicher angeordnet hat,
sodass halt Referenzen von auf das sagen,
wir haben das IT-Element und dieses V dann halt trotzdem korrekt berechnet werden.
Der Compiler weiß dann halt, dieses I startet an Offset
4 und das V startet dann auf Set 16.
Gut, das andere dauert jetzt noch ein paar Minuten,
deswegen machen wir das dann am Mittwoch.
Es geht dann halt weiter mit so einer ganz kurzen Einführung,
wie man im Prinzip Objektorientierung auch noch im Speicher ablegen kann und dann halt die Frage,
wie wir aus einzelnen Code-Fragmenten größere Programme zusammenbauen.
Okay, ich bedanke mich für eure Aufmerksamkeit und dann
bis Freitag