Herzlich willkommen zur Vorlesung Rechnerstrukturen. Mein Name ist Norman Hendrik,
Andreas Meder hat sich in den Urlaub verabschiedet.
Die nächsten beiden Wochen werde ich dann auch noch die Vorlesung übernehmen.
Da ich das das letzte Mal vor einem Jahr gemacht habe und
jetzt nicht allzu viel Zeit in die Vorbereitung gesteckt habe,
kann es kleinen Aussetzern kommen, aber ich hoffe mal,
wir kriegen das hin.
Was mich so ein bisschen überrascht, ist die Anzahl der
Teilnehmer von über 300 ursprünglich nur noch 50, knapp 50.
Entweder haben wir sehr,
sehr viele Leute vergrätzt oder es gibt ein anderes Problem,
ist die Teilnehmerzahl normal für die letzten Male
Okay, das kann natürlich auch daran liegen,
dass Lecture to Go dann dazu verleitet, sich das zu Hause anzuschauen.
Das ist natürlich hier für die Leute,
die live da live vor Ort sind, nicht so schön.
Kapitel 15, kleine Rekapitulationen, damit der Kontext nochmal klar wird.
Wir sind in der Vorlesung jetzt bei dem Teil,
wo wir endlich mal anfangen, tatsächlich Programm zu schreiben.
Und zwar im Moment auf der Schnittstelle zwischen den Applikationen zwischen
der Applikationsebene, also das, wo wir hinterher viel,
viel Zeit unseres Lebens verbringen.
Und jetzt darunter der Rechner und die Schnittstelle ist jetzt halt
hier die Hardware-Software-Schnittstelle realisiert durch den Befehlssatz des Rechners,
den wir zur Verfügung haben
Und jetzt wollen wir uns anschauen,
wie dann tatsächlich die Schnittstelle zwischen Software und der Hardware aussieht.
Und das machen wir jetzt nicht in der Maschinensprache, sondern auf der Assembler-Ebene.
So, worum soll es gehen? Die Grundlagen sind bereits abgefrühstückt.
Das ist hoffentlich allen Leuten klar. Und die nächsten,
also heute und dann nächste Woche werden wir uns dann anschauen,
wie die verschiedenen, sagen wir mal,
Datenstrukturen und Kontrollstrukturen und Programmorganisationssachen,
die wir hinterher auf der Anwendungsebene benutzen,
um halt Programme zu schreiben,
sei es nun in Java oder C
oder irgendeine andere typische Hochsprache
Halt diese Strukturen der höheren Sprache tatsächlich auf dem Rechner abgearbeitet werden.
Die elementaren Befehle und die Adressierungsarten sind schon fast durch.
Die Operationen auch schon zum Teil. Das heißt,
die arathmetischen Operationen und die logischen Operationen wurden schon vorgestellt.
Und zwar am Beispiel des Intel X86 Prozessors. Und jetzt
fehlen natürlich noch einige Dinge. Also mit dem Sachen,
die wir bisher haben, könnten wir halt so Straight-Line-Code schreiben,
der halt irgendwo anfängt und dann einen Befehl nach dem anderen abarbeitet,
um, was weiß ich,
zum Beispiel irgendwelche Zähler hochzuzählen oder eben arathmetische Operationen zu machen.
Aber spannende Dinge, wie zum Beispiel If & Els oder Schleifen
, können wir noch nicht machen.
Und insbesondere Funktionsaufrufe halt auch noch nicht. Und das ist
das, was jetzt als nächstes dran kommen soll.
Die Schnittstelle hier nochmal kurz visualisiert. Das ist die Ebene,
die uns hinterher am meisten interessieren wird. Nun kann man darüber diskutieren,
ob jetzt hier so ein C-Code-Schnipsel wirklich schon High-Level ist.
Aber zumindest sieht man hier schon, dass wir
RAS haben und Zuweisungen an Variablen und so ein Zeug.
Von hier in das, was wir jetzt machen,
kommt man normalerweise mit dem Compiler. Aber wenn man gut ist,
kann man das eben zumindest für kleinere Programme auch von Hand machen.
Also Hochsprachen transformieren in Assembler-Sprache. Und der Schritt von hier
nach hier ist so einfach, dass das immer automatisiert wird.
Das heißt, da gibt es einen Assembler,
der dann vom Hersteller des Prozesses bereitgestellt wird normalerweise.
Man kann sowas aber auch selbst schreiben. Und dort
haben wir dann halt die einzelnen Instruktionen mit diversen Konventionen,
wie halt Adressen und Register benannt werden.
Und dann auf der Ebene der Hardware. Das ist das,
was der Prozessor sieht, haben wir nur noch Nullen und Einzelne Bits,
die irgendwo im Speicher herumliegen und die dann abhängig von
der Maschinensprache des Prozessors halt die Semantik bewirken,
die wir
eigentlich erreichen wollen
Das, was wir von der gesamten Maschine sehen als Programmierer,
jedenfalls das, was wir jetzt als Anwendungsprogrammierer von der Maschine sehen,
ist im Wesentlichen das hier. Wir haben den Prozessor,
Central Processing Unit mit einigen Registern und in der Version des 86-Prozesses,
die wir benutzen, sind das halt im Wesentlichen diese acht Universalregister,
A, B, C, D und so weiter,
der Programmzähler oder hier eben Extended Instruction Pointer genannt.
Dieses E war halt immer für Extended,
ursprünglich waren die Register mal 8 und 16 Bitt breit in der Intelarchitektur,
dann wurde das auf 32 Bitt ausgeweitet und
all die 32 Bedinger heißen halt eh irgendwas
Und dann gibt es hier noch diese komische blaue Box Condition-Codes,
die wir uns jetzt gleich als nächstes anschauen werden.
Wie speichert der Prozessor eigentlich bestimmte Betriebszustände? Gut, und die CPU,
also der Prozessor tauscht dann Daten mit dem Speicher aus,
an welcher Adresse das passiert, kommt immer von der CPU.
Die Daten werden entweder von der CPU in den Speicher geschrieben
oder vom Speicher in die CPU eingelesen und die Befehl,
wenn man nicht gerade selbst modifizierende Programme hat,
werden halt immer nur aus dem Speicher gewesen.
Der Speicher jetzt hier, das ist nicht ganz in den richtigen Proportionen gezeichnet.
Hier sind gerade mal acht Register und dann hier noch ein weiteres Register drin.
Und hier sind halt typischerweise Gigabyte-Phase-Daten und Befehle drin. Das heißt,
dieser gelbe Block Memory ist eigentlich bei weitem der Wichtigste.
Alles, was der Rechner ausführt, ist halt
irgendwie in diesem Speicher abgelegt worden. Zu dem Zeitpunkt,
wo wir unser Programm geschrieben, kompiliert und assembliert haben.
Und das Layout der Sachen im Speicher,
das ist dann das, was uns nächste Woche beschäftigen wird.
Gut. Charakteristika, der Assembler-Ebene war halt,
wir haben einen Befehl pro Zeile mit irgendwelchen Kurznamen,
die man sich mehr oder weniger gut merken kann.
Und dann halt Konventionen abhängig von dem Tool,
wie die konstanten Register und Adressen benannt werden.
Und der Sprungbefehler haben wir zwar noch nicht gesehen,
aber wir können schon mal ein Label an einen Befehl reinschreiben.
Das ist dann hinterher die Marke,
die symbolisch oder der Name für eine bestimmte Speicheradresse.
Und das wird uns dann bei den Sprunggefählen interessieren.
So, das werde ich jetzt nicht wiederholen. Das haben
Sie alles schon gesehen. Vielleicht ganz kurz noch hier.
Das sind also die Register, die uns dann interessieren werden.
Acht Stück. Und alle haben halt diesen komischen Namen eh,
weil es sich um die 32-bettriges dargelt
Dann gab es die verschiedenen Konventionen auf der Assembler-Ebene,
spezifisch jetzt für diesen Intel-Assembler,
wie halt mit Adressen und Register referenziert werden.
Das war ein Register in runden Klammern, war das die Speicheradresse,
die durch den Inhalt des Registers gegeben wird.
Und dann konnte man dann noch gegebenenfalls aufsetzt draus und raffadieren.
So, die letzte Folie, die Sie gesehen hatten,
war dann wahrscheinlich diese hier. Geh nochmal eine zurück,
damit wir zumindest den gemeinsamen Stand haben,
was jetzt die verschiedenen Assembler-Befehle bedeuten,
die wir da benutzen
Das, was recht häufig vorkommen wird, ist halt Move.
Das tut eigentlich nichts Nützliches,
sondern es schiebt Daten von einem Register in den Speicher,
vom Speichern, ein Register oder zwischen zwei Registern.
In diesem Fall halt bei der Intelarchitektur mit zwei Operanten.
Der Zieloperant steht rechts und der Quelloperant steht links,
was das hier also bedeutet, ist,
dass wir den Inhalt des Registers BEP auslesen.
Da acht drauf addieren, das Ganze als Speicherarchitektur interpretieren,
damit den Speicher adressieren und das,
was der Speicher uns zurückgibt,
das kommt dann in das Register ERX
Und entsprechend hier nochmal. Da wird also weiterer Wert aus dem Speicher gelesen.
Dann haben wir hier dieses Universal-Tool für arithmetische Berechnungen,
geschiftete Additionen mit Offset, Load Effective Adress und so weiter verschiedene Operationen.
Also hier zum Beispiel eine Addition und hier eine Multiplikation.
Logische Operationen gab es auch. Also hier zum Beispiel das X-Ror und
dann hier ein arithmetisches Schiff reiht. Also all die Dinger,
die sie im ersten Teil der Veranstaltung schon abstrakt kennengelernt haben,
die sind hier dann auf der Entelmaschine
auch tatsächlich implementiert
Und dann hier halt noch die Schreibtweise der Konstanten,
wenn ich mich richtig erinnere, war halt das Dollarzeichen,
die Beschreibweise für hexadezimale Zahlen und ohne den Dollar-Präfix halt,
als Desimaler wird interpretiert.
Damit sind wir jetzt in der Lage, eben Werte aus dem Speicher
zu lesen, Wert in Rieste hineinzutun und Berechnungen auszuführen.
Das nächste, was man dann tun will, ist halt,
dass man von diesem Straightline-Cut, der halt in Instruktion 1,
2, 3, 4, 5 ausführt nacheinander,
auch irgendwie zu Programmstrukturen kommt.
Das heißt, dass wir irgendwie Ablaufsteuerung machen.
Und die meisten Programmiersprachen bieten dazu diese Dinger hier an.
Drei verschiedene Varianten. Ein IF. Normalerweise eben kombiniert als
If than Ells, was man dann auch schachteln kann.
Verschiedene Versionen von Schleifen und so einem Mehrfachverzerweigerung in C
ist das dann halt oder CLC plus plus dieses Switch-Statement.
Gut, wie kann man das umsetzen auf der Maschine?
Zunächst mal gibt es in der Architektur ein magisches Register,
was nicht direkt vom Programmierer geschrieben werden kann und auch nicht direkt ausgelesen normalerweise,
sondern man kann einzelne Bits in diesem Register auslesen mit einigen Spezialbefehlen und abhängig von der Operation,
die der Prozessor gerade ausgeführt hat,
werden diese Bits
automatisch aktualisiert
Man sieht jetzt schon, dass da zwar ein 32-Bit-Register vorgesehen ist,
aber die grau eingefärbten Bits in dem Register sind im Moment noch nicht benutzt.
Die sind also letztlich undefiniert, beziehungsweise da ist immer eine Null drin.
Und an dieser Stelle, hier ist halt ein Eins drin.
Und dann haben die übrigen Bits komische Namen. Einige davon
werden wir uns dann anschauen. Also hier ist zum Beispiel S,
ich bin benannt als Oberflohfleck, C oder Carrie als Direction,
Sign, Zero, Oxily Weekelly,
Carrie und Perity und ähnliches Zeug
Das sind also die Dinge, die wir gleich für die Sprunggefähle brauchen.
Und dann gibt es eine Reihe von weiteren Bits,
die zum Beispiel ansagen, ob der Prozessor sich jetzt in dem
uralt 80-86-Modus befindet oder im 3-86er-Modus.
Und ob zum Beispiel IO-Operationen vom aktuellen Code ausgeführt werden dürfen oder nicht.
Das ist also jetzt eine komplett zusammengewürfelte Sammlung von Bits.
Und als Assembler-Programmierer müsste man sich jetzt halt anschauen,
was all diese Dinge hier tatsächlich bedeuten. Wir gucken uns
nur ein paar davon an. Nämlich hier letztlich die Ältesten,
die halt hier in den unteren Witz vorgesehen sind.
Und dann sind später immer weitere Bits dazugekommen mit Aktualisierung des Befehlssatzes.
Also schauen wir jetzt erstmal hier an den unteren Witz.
Das ist ein CF, SF, ZF, OF,
das Carry Fleck. Das ist also dann gesetzt,
wenn und bei einer Addition zum Beispiel eben ein Carry auftritt,
wenn also ein Überlauf in die nächste Stelle auftreten würde,
sein, ist dann halt gesetzt, wenn die Zahl negativ ist.
Zero Fleck, weil halt ganz häufig mit Null verglichen wird,
macht es Sinn, dass speziell mit speziell der Hardware bereitzustellen,
dass Zero-Flag wird gesetzt, wenn das Resultat der letzten Arithmetischen Operationen null war.
Und das Overflow-Flag halt, wenn bei einer Addition so Praktionen ein Überlauf passieren würde. So,
da ist Manual für die X86-Architektur halt dann nicht ohne Grund,
sagen wir, ich glaube im Moment so 3000 Seiten oder so,
weil dann halt für jeden Befehl detailliert dokumentiert wird,
wie sich der Befehl auf verschiedene andere Sachen auswirkt.
Also zum Beispiel hier diese Addition, die wir uns schon angeschaut haben,
was der Befehl eigentlich tun soll, ist,
dass wir einen Operanten hier mit einem zweiten Operanten addieren und das Resultat kommt dann wieder hier hinein.
Also dieses ist so ein Register und dieses kann ein Register oder eine Sprecherstelle sein.
Und das Ding addiert halt und das Ergebnis der Addition kommt hier rein.
Zusätzlich passieren dann aber diese Dinge, die hier unten aufgelistet sind. Zum Beispiel,
wenn also das Resultat, was da rauskommt aus der Addition,
also Tee ist jetzt hier für die temporäre Variablen,
die dann später, also das ist das,
was aus der Adu rauskommt und das wird dann halt später hier in das Destination Registerp reingeschrieben.
Wenn das T0 ist, dann wird gleichzeitig das Zero Flag
in dem Status Register gesetzt. Der Science Flag,
wenn der Wert von Tee kleiner Null ist,
Das macht dann also nur Sinn für Signed-Integer-Werte,
aber das ist auch die einzigen Dinge,
um die wir uns hier kümmern.
Das heißt, wenn der Wert, der hier aus der Addition rauskommt,
kleiner als null ist, dann wird das Signfleck gesetzt.
Overflow-Fleck wird gesetzt, wenn das Zweierkompliment, sie erinnern sich, wie das war.
Das heißt, wenn beide Zahlen dasselbe Vorzeichen haben,
dann kann es passieren, dass es bei der Addition überläuft.
Und das heißt dann halt unter bestimmten Bedingungen,
das ist also das hier,
was die Hardware dann tatsächlich realisieren muss,
um dieses Fleck zu setzen
Gut, und dann wird zusätzlich,
das ist so ähnlich wie das Overflow-Fleck,
aber nicht genau gleich, das Carry-Fleck gesetzt,
weil man halt das Ganze als Unsigned-Addition betrachtet und dann halt,
wenn man aus, sagen wir mal, 32 bezahlen,
64 oder 128 bezahlen oder sowas machen will,
dann ist es ganz praktisch, dieses Carry-Fleck zusätzlich zur Verfügung zu haben.
Das ist ein Befehl. Wir haben also hunderte von Befehlen
insgesamt und für jeden von diesen Befehlen ist dann dokumentiert,
wie sich der Befehl im Detail auf diese Flecks in dem Statusregister auswirkt.
Dann gibt es eine Reihe von Befehlen,
die dieses Statusregister explizit manipulieren können
Die Sache, die wir später mal brauchen werden, ist dann hier das Compare.
Das funktioniert so ähnlich wie eine Subtraktion, also so ähnlich
wie das Subtract auf dem Datentyp-Long, also auf 32 Bitwerten.
Allerdings das Ergebnis der Subtraktion wird einfach weggeschmissen. Das heißt,
wir machen diese Traktion und das arithmetische Resultat wird weggeschmissen,
aber stattdessen werden jetzt hier durch diesen Befehl die Flex im Statusregister auch aktualisiert.
Und zwar wird jetzt hier bei diesem Befehl das Zero-Flag gesetzt,
wenn halt das Ergebnis der Subtraktion null wäre.
Also wenn die beiden Operanten gleich sind, Das motiviert dann
auch diesen Namen, Compare. Das Science Flag wird gesetzt,
wenn Erstapparant kleiner ist als der zweite Apparant.
Und das Overflow Flag würde wiedergesetzt, wenn das Zweierkompliment,
in diesem Fall also jetzt die Subtraktion, nicht die Addition,
einen Überlauf generiert. Das ist also dann eine Möglichkeit,
um beim Bit im Statusregister zu setzen oder zurückzusetzen.
Und es gibt dann hier noch einen weiteren Befehl,
Test, der funktioniert nicht so ähnlich,
der funktioniert halt hier wie ein Undoperation,
allerdings auch wieder so, dass das Resultat nicht abgespeichert wird,
sondern dass halt nur die Flex in dem Status-Registik ersetzt werden und in diesem Fall halt nur das Zero-Flag und das Signflag.
Gut, das waren also jetzt schon drei Möglichkeiten,
diese Bits in dem Status-Register zu modifizieren.
Und hier sind jetzt noch ein paar mehr. Da gibt es
also jetzt tatsächlich ein paar Befehle, die alle einzelnen Namen haben,
die die Flex in dem Status-Register einzeln setzen oder zurücksetzen können.
Also hier Set E, das wurde das Zero-Flag setzen oder zurücksetzen,
dann kann man hier das Signflag setzen, zurücksetzen, Greater,
Greater Eagle würde dann halt hier Kombinationen von Signflag und Overflow-Flag setzen,
entsprechend für Less, Less Equal und Herr Buff und Below.
Das werde ich jetzt hier nicht im Detail durchspielen,
da können Sie selbst mal nachschauen,
ob diese Tabelle dann Sinn macht, das heißt,
ob tatsächlich jetzt hier, wenn diese Dinger hier gesetzt sind,
ob das dann,
wenn man das halt hier zum Beispiel mit dem Compare vergleicht,
ob da tatsächlich
das Rottengleiche herauskommen würde
Wir haben also jetzt mehrere Möglichkeiten,
diese Bits im Statusregister zu manipulieren,
direkt zu setzen oder zurückzusetzen mit dem Testbefehl,
dem Komperbefehl oder als Nebenwirkung von den normalen arathmetischen und logischen Gefehlen.
So, was kann man jetzt mit dem Ding machen?
Was wir jetzt tun wollen, ist eine Funktion implementieren,
die heißt der zielhaltige GT, das steht natürlich für einen Greater.
Und was soll die machen? Die kriegt zwei Intager-Argumente übergeben,
X und Y. Und was soll sie machen? Sie soll
halt zurückgeben, wenn X größer als Y ist,
dann soll eins rauskommen und ansonsten null
Gut. Das sieht hier also recht harmlos aus, aber tatsächlich bedeutet das natürlich,
dass man letztlich irgendwie so ein If-than Ells implementieren muss.
Oder eben diese schwarze Magie der entsprechenden Statusregisterwitzen. Okay,
und was passiert da jetzt,
wenn der Compiler das eingelesen und kompiliert hat, dann,
ja, je nach Compiler-Version, dann könnt ihr zum Beispiel dieses hier herauskommen.
Die ersten beiden Zeilen enthalten jetzt wieder diese komischen Adressen,
die daherkommen, dass halt eine Funktion ihre Argumente an einer
bestimmten Stelle erwartet
Das hat ja Andreas schon erläutert und das werden wir
uns dann aber nächste Woche noch wirklich im Detail anschauen,
wie das funktioniert. Das hier greift auf den zweiten,
auf das zweite Argument dieser Funktionen zu.
Also dieser Move-Befehl liest jetzt hier aus dem Speicher den Wert
von Y und packt das in das Register EA XN9.
Und als nächstes lesen wir an dieser Stelle halt das Argument X
aus dem Speicher aus und machen jetzt einen Compare mit dem Register,
dem Inhalt des Registers EA X,
das enthält also jetzt Y und vergleichen das also jetzt hier
mit dem Wert von
So. Und dann wird jetzt hier mit Set Greater
der Wert von dem Register A ausgewertet.
Das ist jetzt so ein bisschen kurios, weil diese Setbefehle
aus der Uhrzeit, aus der Frühzeit der X86-Architektur kommen.
Und die arbeiten jetzt tatsächlich nicht auf 32-Bit-Daten, sondern dieses Register,
die ersten Register hier sind halt aufgeteilt in den vollen 32-Bit-Bereich,
in den 16-Bit-Bereich.
Das ist also High and Low und die untersten 8-Bit
in den Low-Bereich. Und dieser Set-Befehl hier arbeitet nur auf
diesen unteren 8-Bits
Der würde also jetzt hier abhängig davon,
was aus dem Compere herausgekommen ist, den ein Fleck,
nein, sorry, das packt,
liest die Flex aus und packt das in das Register A,
aber eben nicht in den gesamten Teil von A,
sondern nur in denen hier.
Und die oberen Teile von EAX bleiben unverändert. Das ist aber nicht das,
was wir haben wollen, sondern was wir haben wollen,
ist, dass in dem Register entweder 0 oder 1 drinsteht
insgesamt und da gibt es dann hier einen zweiten merkwürdigen Befehl,
der halt den Wert aus diesem AL nimmt und das in das ERX erweitert.
Das heißt, dieser Befehl lässt die unteren Bits von dem Register A unverändert,
diese hier. Und die Oberen werden einfach auf Null gesetzt.
Das ist schon gar nicht so schlecht,
was dann jetzt hier noch fehlt, ist halt das Return von der Funktion,
aber das werden wir uns dann gleich anschauen.
So, damit sind also zumindest die Möglichkeiten gegeben,
um abhängig von aratmetischen Operationen oder von den Inhalten,
von Registerwerten diese Bits in dem Statusregister zu setzen und zurückzusetzen
So, was machen wir jetzt damit? Eine Möglichkeit,
damit was zu machen, sind halt die verschiedenen Sprungbefehle.
Das sind dann also letztlich Befehle, die sagen,
dass wir der Prozessor nicht befehle in Nina-Ara-Reihenfolge abarbeitet,
nach jedem Befehl den Programm-Account inkrementiert,
sondern dass man die Programmausführung an einer bestimmten Stelle fortsetzt,
wenn die dann halt als Adressargument für die Frohmoschile übergeben wird.
Und die einfachste Möglichkeit ist halt der unbedingte Sprung.
Das ist der Jump-Befehl, der wird Letztlicherheitsbedingung,
hat der gar nichts, der wird halt,
wenn er in dem Programm drinsteht,
Und den gucken wir uns auch gleich an. Und dann gibt es
die bedingten Sprünge, das sind die Dinger hier unten.
Zum Beispiel eben ein Jump-If-Equal, der liest den Wert aus dem Statusregister aus,
nämlich das Zero-Flag. Und wenn das Zero-Flag gesetzt ist,
dann würde ein Jump-If-Equal springen, ansonsten nicht springen.
Das heißt, die nachfolgende Instruktion ausführen. Das Inversum gibt es auch,
das ist dann also ein Jump-If-Not-Equal, Jump if Science-Flag-Set.
Es ist also sowas wie Jump-If negativ, Jump-If-Non sein,
Greater, Greater Equal, Lass, Ness Equal, Baf in Billo.
Da sind also jetzt all diese komischen Befehle,
die irgendwas in dem Statusregister auslesen, jetzt mal tatsächlich verwendet.
Wir haben also jetzt hier eine Reihe von Sprungbefehlen,
die einen Sprung ausführen, wenn jeweils die Bedingungen,
die hier gelistet ist, wahr ist.
Und diese Bedingungen kriegen wir halt dadurch,
dass vorher eine arathmetische Operation die Flex entsprechend hinterlassen hat oder
dass wir sie mit diesen Set-Befehlen explizit setzen. So,
das war jetzt ganz in der ersten Vorlesung schon mal erwähnt,
das normale Ausführungsprinzip beim Vernorman Rechner ist halt,
es werden Befehle sukzessive an aufeinanderfolgenden Speicheradressen aus dem Speicher geholt und ausgeführt.
Es gibt dann bestimmte Speicheradresse, nach den Staaten des Rechners,
nach dem Reset sozusagen die Programmausführung beginnt.
Und ja, das geht dann halt sequenziell dadurch.
Jetzt wollen wir bei Sprung Befehle machen.
Und wie geht das dann auf der Assembler-Ebene?
Es hat sich, es ist einfach viel zu fehlerträchtig,
wenn man da nur Adressen beschreiben würde.
Deswegen hat es sich eingebürgert, da solche Labels zu erlauben.
Das sind also letztlich menschenlesbare Namen für Speicheradressen
In der Syntax bedeutet das, dass man am Beginn
eines Essembler-Befehls so einen Namen setzen kann oder eben Zwischenzeiten,
also wenn das nicht komplett zeilenorientiert ist, dann kann man das irgendwo hinschreiben.
Und der Label bezieht sich dann auf den nächsten nach folgenden Gefährten.
Die Namen haben natürlich bestimmte Regeln, wie die auszusehen haben,
aber im Prinzip kann man abhängig in diesen Limits, was weiß ich,
bis zu 32 Zeichen oder irgend sowas und keine Sonderzeichen,
kann man sich diese Namen selbst aussuchen.
Und wenn der Compiler dafür verantwortlich ist, dann wird der
Compiler halt gegebenenfalls Funktionsnamen verwenden oder irgendwelche intern erzeugten Namen,
die man dann gegebenenfalls zuordnen muss.
Und der Witz ist jetzt, dass man halt diese
symbolischen Adressen auf der Assembler-Ebene auch für Sprünge verwenden kann.
Das heißt, wir müssen uns jetzt nicht direkt irgendeine Adresse
ausrechnen und die dann in diese Sprungbefehlereien kodieren,
sondern das übernimmt der Assembler für uns.
Wir können also sagen, wir möchten jetzt, was weiß ich,
zu einer Schleife springen und da muss man da nicht die Adresse eingeben,
sondern kann den Namen nehmen.
Als weitere Konvention, aber das ist dann abhängig von dem jeweiligen Tool,
also zum Beispiel von dem Genug Compiler und dem Genuis Sembler.
Dinge, die mit so einem Unterstrich beginnen,
sind normalerweise global
Also dann zum Beispiel eine Funktion, die Max heißt,
aber aus verschiedenen, also ein Unterprogramm namens Max,
was von woanders aufgerufen werden soll,
das würde dann auf der Assembler-Ebene halt unterstrich Max heißen und im Gegensatz dazu Labels,
die Summ L haben und dann eine Nummer,
das sind normalerweise halt Labels,
die nur lokal verwendet werden.
Und damit das funktioniert,
müssen die Dinge halt eindeutig sein,
wir können halt jetzt nicht
einen Namen für verschiedene Adressen verwenden
So, jetzt wird diese Funktion halt komplettiert.
Der obere Teil hier und der unteren Teil, da vertröste ich
wieder auf später. Das ist halt der magische Code,
mit dem die Funktion ihre Parameter ausliest und ihren Rückgabewert zurückgibt.
Das eigentlich Interessante ist, denn jetzt halt dieser Teil hier,
das andere haben wir eben schon gesehen.
Wir holen uns das Argument X,
wir holen uns das Argument Y. Wir vergleichen die beiden miteinander.
Das setzt jetzt halt die Flex in dem Statusregister und an
dieser Stelle wird jetzt einen Jump gemacht.
Jump Und zwar wann? Less Equal. Und wenn das
erfüllt ist, dann geht es zu dem Label L9.
Also L9 ist jetzt hier so ein lokaler Label.
Und unter Strich Max ist der globale Label,
globale Name für diese Funktion Max.
So, was passiert jetzt hier tatsächlich konkret?
Wir haben hier die beiden Werte miteinander verglichen. Und ich
gehe jetzt nochmal ganz kurz zurück zur Definition von diesem Comfort-Befehl.
Da war halt hier erster und zweiter Apparant.
Das war hier. Das berechnet also den hinteren minus
den vorderen
Und würde dann halt die entsprechenden Flexs setzen, abhängig davon, was hier passiert.
Also Science Flag, wenn der hintere kleiner ist als der Vorderwand.
So, dann ist das hoffentlich korrekt so. Also hier
steht X größer Y. Das würde also an dieser Stelle berechnet.
Da werden jetzt die Zero und Science Flag und so weiter gesetzt.
Das wird jetzt hier vor diesem Jump-Affless-Equiel ausgelesen.
Wenn das erfüllt ist, dann springen wir hier hin.
Und was tun wir dann? Dann machen wir eigentlich
gar nichts mehr und implizit ist dann der Wert,
der in dem Register EAX drin steht, derjenige, den wir dann zurückgeben.
Das heißt, hier holen wir uns einen Wert.
Das ist das X, in das Register DX.
Hier holen wir uns das Y, in das Register EAX.
Jetzt vergleichen wir die beiden miteinander. Und wenn hier jetzt Jumplas Equal falsch ist,
dann, nee, wenn Les Equal wahr ist,
dann springen wir hier hin und machen gar nichts mehr.
Das heißt, die Funktion gibt jetzt den Wert zurück,
der in EAX drin steht.
Und das war der Wert von Y. Und ansonsten,
wenn das hier nicht wahr ist,
dann geht die Befehlsausführung halt nicht der Sprung wird nicht ausgeführt,
sondern wir führen die nachfolgende Instruktion aus.
Und das ist dann halt hier ein Move. Und was macht das Move?
Das transferiert den Inhalt vom Register EDX in das Register EIX.
Das heißt, das kopiert jetzt den Wert von X,
den Vehik da reingeschrieben hatten, in das Register EAX,
wo bis vor kurzem noch der Wert von Y da ist.
Und dann wird er wieder zurückgegeben. Das ist also
dann in diesem Fall der Wert von X. So,
und wenn man das dann alles zusammenbaut, dann stellt man fest,
dass das halt tatsächlich diese Semantik hier hat,
wenn X größer als Y ist,
dann springen wir hier rein,
schieben den Wert von
Ich finde das Register ERX und sind fertig.
Und ansonsten geben wir halt den Wert von Y zurück,
der durch diese Instruktion schon in dem Register eher extrem stand.
So, die Details müssen sich maximal bis zur Klausur merken.
Also ich schätze mal, dass auch dieses Jahr in der Klausur
so ein oder zwei Aufgaben grob mit Assembler-Programmierung zu tun haben werden.
Und da muss man dann halt gegebenenfalls diese Konvention, wie die Register heißen,
wie die Offsets geschrieben werden und so was dann kennen.
Ansonsten kann man sich global merken,
dass halt so ein bedingter Ausdruck, if then else,
letztlich umgesetzt wird in irgendwas,
was aus einem Compare oder einem der anderen Vergleichsbefehle besteht und dann einem bedingten Sprung.
Könnte natürlich jetzt auch hier statt hier ein Jump irgendwo nach
im Label 9 und dann einfach den Rest hier weglassen,
auch hier das If mit einem Sprung machen und das Else auch mit einem Sprung irgendwo hin versehen.
Das wäre vielleicht übersichtlicher bei der Assembler bzw. der Compiler
in diesem Fall versucht,
das halt zu optimieren und hat dann halt diese Kurzsequenz gefunden,
die halt tatsächlich minimal ist
Ist das soweit plausibel, was da passiert?
Also wir haben auf der Hochsprache ein If than Ells
und auf der Assembler-Ebene, was der Compiler draus macht,
ist halt Kombination aus einem Vergleichsbefehl und einem bedingigenden Sprung.
So, ein Go-To-Concert hat Harmful,
aber hier ist dann trotzdem mal eins drin.
In C darf man halt solche Sachen tun.
Das heißt, wir können auch im C-Code tatsächlich Label vergeben.
Hier haben wir jetzt also eine zweite Funktion,
die so ein bisschen unübersichtlicher ist als die alte,
die aber ein Go-To-Beding verwendet und in der man dann
nochmal zeigen kann,
was dann hier passiert
Also was wir eigentlich tun wollen, ist wieder den Maximalwert
von X und Y berechnen und zurückgeben. Naja,
wir holen uns also jetzt hier erstmal den Wert von Y
in so ein Register und berechnen einen logischen Wert,
ob X kleiner gleicher als Y ist.
Wenn das der Fall ist, dann sind wir fertig und
geben den Rückgabe wieder zurück. Und ansonsten geben wir,
überschreiben wir halt den Rückgabewert mit X und machen dann die Returnananweisung und geben den wieder zurück.
Das ist also letztlich von dem Assembler Code,
den wir eben hatten, zurückübersetzt in den C-Code,
wie diese Funktion letztlich auf der Eshembler-Ebene aussieht.
Das ist das, was wir haben wollen.
Das ist das, was der Compiler an uns gibt.
Und wenn wir das jetzt dieses hier wieder zurückübersetzen in C-Code,
Schritt für Schritt, dann ist letztlich das das,
was der Assembler, mit Compiler aus dem Code mal gemacht hat.
Das heißt, wenn Sie in die Folien anschauen und an dieser Stelle Schwierigkeiten haben,
dieses hier mit dem irgendwie zu korrelieren,
dieses hier mit diesem zu korrelieren ist vergleichsweise einfacher,
weil hier dann klar wird, was da eigentlich als Datentransfers und Berechnungen passiert.
Okay, einzelne if than Ells sind natürlich jetzt nicht ganz
so spannend, als nächstes wollen wir Schleifen umsetzen.
Und zwar hier wieder als die Version, die man als Programmierer schreiben würde,
das ist das Linke. Und dann rechts eine Version,
die mit expliziten Labels auch immer auf dem C-Code arbeitet.
Also in dieser Stelle jetzt hier eine Schleife und zwar Wahlschleife.
Das heißt, der Körper der Schleife wird auf jeden Fall
einmal ausgeführt und dann wird überprüft, ob die Bedingung noch wahr ist.
Und wenn ja, dann wird wieder hier hingegangen und die nächste Iteration ausgeführt.
Was das, was die Funktion tun soll,
das werden sie aus dem Namen erraten.
Es ist also Factorial, das berechnet also die Fakultät des übergebenen Intinger-Werts.
Das läuft also bei Inter-der-Zahlen recht schnell über,
aber man kann das ganz gut als Demo-Beispiel verwenden.
So, wie war die Fakultät definiert? Fakultät von Ennis,
also das Produkt von N mal N minus 1,
mal N minus 2 bis zu 1 oder eben von aller Zahlen, von 1 bis N.
Und hier halt iterativ als Schleife realisiert. Wir wollen die
Fakultät von X berechnen. Wir fangen an mit dem Resultat
Wert 1 und multiplizieren jetzt diesen temporären Wert mit dem X,
dekrementieren X, also aus X wird X minus 1. Und
solange dieser Wert von X noch größer als eins ist,
gehen wir in die nächste Literation der Schleife.
Dann haben wir also jetzt hier das 1,
wird im ersten Mal mit X multipliziert,
im nächsten Schleifendurchgang mit X minus 1,
dann mit X minus 2 und so weiter.
Und wenn wir aus dieser Schleife rausspringen, weil wir fertig sind,
dann geben wir den Berechneten Zwischenwert als Fakultät zurück.
Gut, das ist die Du-Weihschleife. Hier ist dann schon
mal angedeutet, was der Compreiler vermutlich daraus machen wird.
Der Compiler erzeugt, also jetzt hier so ein Label,
in diesem Fall halt Lupin dann für die Schleife.
Wir multifizieren innerhalb dieser Schleife den temporären Wert mit dem aktuellen Wert von X,
dekrementieren X und wenn X größer als eins ist,
dann springen wir halt nochmal wieder zurück und fielen die Schleife nochmal aus.
Und wenn X nicht größer als eins ist,
dann wird diese Instruktion übersprungen und wir sind soweit fertig und gehen den Resultatwert zurück.
Ich hoffe mal, das macht plausibel,
dass diese Funktion halt eine Fakultät berechnen würde.
Und das ist die Version, die man hoffentlich einigermaßen sauber schreiben würde.
Das ist die weniger schöne Version mit einem expliziten Label,
die aber eben auch,
sagen wir mal,
zumindest zulässig ist für den Compiler
Das wird jetzt also das erste Mal sein,
dass wir eine etwas kompliziertere Kontrollstruktur in einem Assembler-Code sehen.
Und zwar jetzt als erstes Mal diese Version mit
der Schleife vom C-Code in den Assembler-Code übersetzt. Gut,
in diesem Teil hier, den ignorieren wir es nochmal,
das wird erst dann klar werden.
Nächste Woche werden wir tatsächlich den Stack gesehen haben,
wie halt die Argumente verwaltet werden.
Hier werden halt jetzt das Register EBP initialisiert
Und da der aktuelle Stackpointer in dieses Übriges der EDP hineingeschrieben.
Das ist also letztlich was, was jetzt allgemein beim Funktionsaufrufen
notwendig ist, die eigentliche Initialisierung der Funktion beginnt jetzt hier.
Die ganzen arathmetischen Operationen bei der IntoX 86 Architektur waren
ja so gemacht, dass das Zielregister immer rechts steht.
Und später werden wir das Übrigester A, also EAX,
nehmen, um den Rückgabewert zurückzugeben.
Von daher wird jetzt dieses Register EAX gleich reserviert,
um diese Zwischenresultate aufzunehmen
Das heißt, wir haben uns jetzt entschieden, diese Variable Result,
die kommt in das Register EAX und die wird mit eins initialisiert.
Also packen wir die Konstante eins in das Register EAX.
So, und dann müssen wir uns natürlich den übergebenen Argumentwert
von dem X irgendwo holen. Das tun wir an dieser Stelle
. Das ist also jetzt hier wieder diese magische Adressberechnung,
die auf das erste Argument dieser Funktion zugreift.
Das ist also jetzt die Adresse im Speicher,
wo dieses Argument X beim Funktionsaufruf abgelegt ist.
Wir holen uns also den Wert von X aus dem Speicher
Und packen den Wert von X in das Register.de.
So, jetzt ist dieses Ding hier umbenannt worden.
Das Loop heißt also jetzt hier Label 11. Und was machen wir?
Wir multiplizieren den Inhalt von EDX, also das aktuelle X,
mit dem Inhalt von E-A-X und packen das Resultat wieder nach EA-X.
Das ist also dann hier genau diese Zeile. Das Zwischenergebnis,
was im Registert EA-X abgelegt wurde,
das wird mit dem X multipliziert und wieder in diese Variante
Result,
also ins Register
ER-X zurückgeschrieben
Dann machen wir x gleich x minus eins.
Das ist also hier ein einfaches Dekrement für das Register.
Dann vergleichen wir den Wert von eins mit dem Inhalt von EDX.
Und wenn das größer ist, dann springen wir halt wieder
hier rein und machen die nächste Iteration.
Das heißt, der Inhalt dieser Schleife besteht tatsächlich im Wesentlichen
nur aus diesen drei Instruktionen und dann dem bedingten Sprung,
der dann hier die Schleife wieder die nächste Iteration anspringt.
Und ansonsten sind wir fertig. Und wir müssen eigentlich gar nichts mehr tun,
weil der Rückgabewert schon korrekt im Register EAIX drinsteht.
Und dieses ist dann halt, dieser Code hier oben und
der Code hier unten sind halt bei jedem Funktionsaufruf und Funktionsrücksprung notwendig,
um ansonsten den Speicher aufzuräumen.
Ist, ja klar. Oh Gott, Sie glauben nicht
ernsthaft, dass ich diese Dinge auswendig weiß, oder?
Da müssten wir ja jetzt tatsächlich, also das Manual anschauen,
wie die Multiplikation definiert ist. Ich bin davon ausgegangen,
dass Imul bisher halt 32 Bittbilder miteinander multipliziert und auch nur in 32-Bit-Resultat wird liefert.
Ich will nicht ausschließen, dass das Ding tatsächlich so dokumentiert ist,
dass halt die höheren Bits auch irgendwo abgelegt werden.
Es würde mich überraschen, dann wäre halt die Folie falsch,
aber ausschließen will ich es nicht.
Es müssten jetzt halt Emmanuel nachgucken, wie dann der E-Muel-Befehl definiert ist.
Ich schlage vor, dass wir das dann am Ende
dieser Veranstaltung machen und dann da mal gucken.
So, da ist jetzt noch eine Frage.
Das heißt also, das E-Muel dürfen wir an dieser Stelle
dann nicht verwenden oder zumindest nicht in Kombination mit dem EDX
Okay? Okay, also ich würde mich wundern,
wenn diese Folie falsch ist, weil das Original von Bryant ist,
aber ausschließen will ich das nicht.
Gut, da müssen wir jetzt tatsächlich die die 32-Bit-Version,
auf die sich halt die ganzen Folien hier beziehen,
dann das Manual anschauen wie das passiert, was dann passiert.
Also im 64-Bit-Manual sind ja die,
ist das Verhalten für die 32-Bit-Versionen auch entsprechend mit drin.
Aber ich mache mir da gleich mal eine Notiz,
dass wir an dieser Stelle aufpassen sollten.
Wenn das an dieser Stelle halt tatsächlich das Register EDX überschreibt,
dann würden wir ein anderes Register verwenden müssen,
damit dann halt hier die Semante korrekt ist.
So, auch hier hoffe ich dann, dass die Zwischenfragen
so weit angekommen sind. Also die Frage war halt,
ob dieser Befehl hier nicht nur das Resultat hier in das ERX-Franken schreibt,
sondern die bei der 32-Bit-Multiplikation durchaus auftretenden oberen 31-Bits nicht automatisch
hier reinschreibt und damit dann natürlich den eigentlichen Wert von X überschreiben würde.
Da muss man dann halt tatsächlich detailliert aufpassen,
wenn man sowas von Hand schreibt.
Wenn man jetzt so ein bisschen zurückgeht und sich anschaut,
was da eigentlich passiert, dann war das halt eine Transformation dieser Art.
Wir haben eine Duweil-Schleife mit der Abfragebedingung hier unten und dazwischen halt den Inhalt,
den Körper der Schleife und mehr oder weniger
kann man das automatisiert halt übersetzen in dieses hier
Wir kaufen uns ein Label,
sowas wie L11 oder 11 Labels 42.000 irgendwas,
haben dann hier den Inhalt,
die Instruktionen in dem Körper der Schleife und dann halt hier eine Abfrage,
eine bedingten Sprung, wenn diese Bedingung wahr ist,
dann springen wir wieder zurück zur Schleife.
So, die nächste Version ist das also jetzt nicht die Schleife,
die Duwei-Schleife, sondern die Waldschleife, die halt die Abfrage hat,
bevor der Inhalt, der Körper der Schleife ausgeführt werden soll.
Und auch das kann man, sagen wir mal,
mechanisch umsetzen in dieses Hier. Wir invertieren jetzt hier einmal
diese Abfrage. Wenn die Abfrage am Anfang der Wahlschleife
nicht erfüllt ist, dann sind wir halt schon fertig.
Dann springen wir zum Ende. Und ansonsten haben wir jetzt
halt die Wahlschleife mit dem Test oben,
ersetzt durch den Test einmal am Anfang und anschließend dann du,
Wile, was wir schon ausführen können.
Das heißt, wenn wir sowas hier kompilieren wollen,
dann können wir das einmal so umsetzen,
einmal den Test invertieren und einen zusätzlichen Label bis zum also
ein Label einführen und dann ans Ende dieser Schleife springen.
Und ansonsten dieses hier mechanisiert umsetzen in diese Label-basierte Version mit dem Körper der Schleife,
dem Test am Ende und dem Goto zurück zum Körper der Schleife.
Das heißt, auf diese Art und Weise haben wir jetzt
nicht nur so ein Dubai, sondern auch die Wahlschleife schon fertig.
Und wenn man das noch einen Schritt weitermacht,
dann können wir halt auch die Vorschleifen umsetzen.
Das ist also dann die nun letztlich gängigste Version von Schleifen
, zumindest im
Das ist ja dann in Jahre auch ganz ähnlich.
Das ist also dreigeteilt hier. Wir haben eine Initialisierung von den Schleifenvariablen.
Wir haben dann den Schleifentest und am Ende eine Regel,
die uns sagt, wie die Schleifenvariablen aktualisiert werden und dann halt den Inhalt der Schleife.
Gut, und das kann man halt umsetzen im ersten Schritt
in die Initialisierung der Variablen, dann halt eine Wahlschleife,
solange die Testbedingungen hier wahr ist, wird das hier ausgeführt.
Und dann werden die Schleifenvariablen aktualisiert und dann geht es halt
gegebenenfalls wieder hier hin
Das geht also mehr oder weniger einfach durch so syntaktische Transformationen.
Da muss man nicht groß nachdenken. Und wenn wir hier sind,
dann können wir das halt in diese Dubai-Version umsetzen, in die gute Version.
Und damit sind jetzt die Schleifenstrukturen dann auch schon fertig. Das heißt,
die normalen Kontrollstrukturen, die man in den gängigen Programmiersprachen,
hier am Beispiel von C, halt hat.
Aber ich meine, die anderen Dialekte, wie Java oder
Pryson oder sowas, da ist das ja sehr ähnlich.
Da kann man halt letztlich sämtliche Schleifenkonstrukte dadurch umsetzen,
dass wir halt dieses Jahr hier letztlich auf der Maschinenebene ausführen.
Wir haben ein Label, der auf den Anfang der eigentlichen Inhalte
der Schleife zeigt und ersetzen halt dann am Ende dieser Dinge,
führen wir den Test aus auf die Schleifenbedingungen.
Und solange das hier noch wahr ist,
springen wir dann halt in die nächste Iteration der Schleife.
Gut, das sind also die Kontrollstrukturen, die einfachen.
Und jetzt wird es noch ein bisschen interessanter,
die Mehrfachverzweigung und Switch-Anweisung
Ich schätze mal, das haben sie mittlerweile auch
zumindest in Java schon mal gesehen. Die als Beispiel,
was auf den Folien ist, bezieht sich jetzt wieder auf C.
Da gibt es dann halt hier diese magische Type Tive.
Das ist also letztlich so ähnlich wie eine Typendefinition,
wo halt ein neuer Datentyp eingeführt wird, der jetzt hier Operation Type heißt.
Und der kann sechs verschiedene Werte an sechs verschiedene Werte annehmen.
Also was wir hier als Operation machen wollen,
ist was weiß ich, Addition, Multiplikation, Retraktion,
Division, Modulo und irgendetwas,
was ungültig ist
Und das soll jetzt verwendet werden. Wir wollen jetzt irgendeinem Parser bauen,
der halt dieses Operations-Ding übergeben bekommt. Und abhängig davon,
was wir halt als Symbol übergeben bekommen haben, wollen wir verschiedene Dinge tun.
Nämlich die Mehrfachauswahl jetzt hier, welche Werte sind möglich,
Addition, dann wollen wir halt einen Plus zurückliefern,
Multiplikationen sollen mal zurückliefern, minus minus Divisionen,
die slash Modulo Prozent und Bert irgendwie das Freizeichen.
Das ist also jetzt hier ein typisches Anwendung für dieses Switch-Konstrukt,
wo wir halt eine Variable haben,
die verschiedene Werte annehmen kann und abhängig von dem Wert
wollen wir
verschiedene Dinge tun
So, was kann man da machen? Relativ klar ist,
dass man das umsetzen kann in so einem Baum von If Than Ells.
Das heißt, wenn wir hier so einen Switch haben,
dann könnten wir fragen, If-Operation, equal at, da mach irgendwas,
Ells, if equal mult, dann und so weiter.
Das ist also vergleichsweise klar, was man da tun kann.
Ep Than Ells haben wir uns schon angeschaut und das ist
tatsächlich auch eine gute Lösung, nämlich insbesondere dann,
wenn wir nur vergleichsweise wenig Alternativen haben.
Also hier zum Beispiel haben wir nur 6,
dann ist das also If Than Ells,
If Than Ells,
bla bla
Sind wir also nach fünf Ifs, schlimmstenfalls, wenn wir
den auch noch testen, mit sechs Ifs, fertig.
Man kann sich aber überlegen, dass das problematisch wird.
Nehmen wir mal an, wir haben jetzt hier nicht nur
sechs Operationen, sondern wir haben sechs Milliarden Operationen.
Dann wäre dieser Baum von If-than Els schon etwas unhandlich.
Gut, die Alternative ist jetzt, das Ganze auf der Assembler-Ebene ein bisschen trickreich vorzuberechnen.
Und zwar mit seiner sogenannten Sprungtabelle oder Jump-Table. Die Idee ist jetzt,
dass wir tatsächlich eine Datenstruktur vorbereiten, die halt Sprungadressen vorberechnet bekommt.
Und wir springen dann in dieser Tabelle an einen bestimmten Eintrag
von dort zu dem Tod, der zu dem entsprechenden Zweig hier gehört.
Das funktioniert nur, wenn diese Abfrage hier, die in
dem Switch drin ist, eine ganz zahlige Konstante ist.
Und auch das ist natürlich, wenn die Anzahl zu groß wird, irgendwann unhandlich.
Aktuelle Compiler, also hier am Beispiel der Nuzi-Compiler,
werden von diesen beiden Alternativen zur Implementierung abhängig davon,
was wie der Code aussieht, thematisch eine Variante auswählen.
Da kann man also auf der C-Ebene nicht weiter heran.
Das ist vom Compiler abhängig. Auf der Assembler-Ebene könnte man das
aber in beiden Fällen, also wenn man das selbst macht auf Assembler-Ebene, entsprechend erzwingen.
Das ist eine der beiden Varianten genutzt wird. So,
auch das hier jetzt erstmal wieder als pauschal mit so ein paar Blöcken visualisiert,
was wir eigentlich tun wollen, ist hier eine Switch-Anweisung,
die in unserem Code drin steht,
Also hier war unser Datentyp, der die sechs verschiedenen Werte annehmen kann.
Und dann haben wir halt, was soll getan werden,
wenn der Vietnull ist, dann wollen wir Kot ausführen,
der halt zu diesem Wert gehörte.
Ansonsten bei dem nächsten Wert halt die nächsten Kotblock und
so weiter, hier den letzten Block.
Dann bereiten wir eine Tabelle vor im Speicher mit verschiedenen Sprungadressen.
Die kriegt also jetzt hier ein Label,
die liegt also selbst an einer bestimmten Adresse im Speicher
Und dann gibt es die verschiedenen Kotblöcke,
die halt auch vom Compiler als der Sembler-Kurt vorbereitet werden.
Und jeder Codeblock hat hier seinen eigenen Label bekommen. So,
was jetzt passiert, wenn wir das so tun können,
also wir müssen jetzt hier die einzelnen Blöcke einzeln kompilieren,
assemblieren, das sind halt die Dinger hier,
die kriegen alle einen eigenen Namen.
Sorry. Und dann würden die Startadaddressen dieser einzelnen Codeblöcke halt hier
in dieser Tabelle zusammengefasst. Und das gesamte Switch-Statement wird dann
halt umgesetzt in diese witzige Ausführung hier. Das heißt,
das Switch-Statement besteht jetzt letztlich nur noch aus zwei Anweisungen,
nämlich zum einen, wir gucken in der Sprungtabelle,
die wir uns vorberechnet haben, nach. Für die gegebene Operation,
also dieses hier, was hier in der Switch reinkommt,
suchen wir uns die passende Zeile in der Sprungtabelle aus und kaufen uns
die Adresse ein und dann machen wir einen unbedingten Sprung auf C,
also letztlich ein Goto an die entsprechende Adresse.
Das heißt, dieses Switch-Statement hier wird zerlegt in die einzelnen Kurtblöcke,
vorberechnete Sprungtabelle und anschließend, das einzige,
was übrig bleibt, sind dann halt die Assembler-Anweisungen dafür,
um in dieser Sprungtabelle die richtige Zeile zu finden und dann diese Zeile,
die wir ausgelesen haben, als Adresse zu nehmen und dann springen wir einfach dahin und führen den Code aus.
Das heißt, jetzt haben wir hier eine Verzweiflung,
die hatte nur sechs Einträge, aber die könnt ihr eben
auch tausend oder sowas haben und statt tausend geschachtelte
If and Ads zu machen,
haben wir jetzt hier letztlich zur Ausführungszeit nur eine einzige Operation,
also für diejenigen,
die die O-Nationen schon kennen,
eine O-Von-1-Operation statt
So, das sieht dann tatsächlich auf der Ebene des Assembler-Codes
dann so aus. Wir haben also jetzt hier wieder unseren
Datentypen mit den verschiedenen Einträgen. Und hier die Funktion,
die irgendwas tun soll mit dem Switch. So,
dieses Bush-All und das Move-All hier und das ist also wieder der magische Prolog,
den wir für diese Funktion kriegen, um den Stack zu initialisieren.
Und dann holen wir uns das Argument,
was wir ergeben haben,
also Inhalt des Registers EBP plus 8 ist wieder die Adresse von dem Wert,
der uns übergeben wurde,
in den Wert packen wir in das Register EAX
Dann haben wir hier eine Sicherheitsabfrage,
wenn dieser Wert halt größer als fünf ist, dann wollen wir,
sind wir letztlich schon fertig, weil da nichts weiter,
mal in diesem Fall halt der Compiler weiß,
dass wir nur die sechs Auswahlen haben.
Und ansonsten machen wir einen unbedingten Sprung. Und zwar,
das ist jetzt eine interessante Syntax,
die auch sonst nicht häufig in den Folien vorkommt.
Wohin springen wir? An die Adresse,
die sich ergibt, ausgehend von dem Label 57,
das werden wir gleich sehen,
das ist der Anfang der vorberechneten Sprungtabelle
Plus den Inhalt des Registers ERX geschiftet um vier. Das wäre beziehungsweise normal vier,
war das, sodass wir halt aus diesen vorberechneten Intege-Konstanten,
die ja jetzt hier 0-1, 2,
3, 4, 5 sind, Offsets machen,
die halt 0-4, 8, 12,
16 und 20 sind, dadurch,
dass das hier mit vier multipliziert wird und damit haben wir dann halt die Sprungadresse berechnet,
wo unser Code tatsächlich steht
So, ich glaube mal, das können wir uns gleich nochmal anschauen.
Erzählt habe ich das alles schon. Das war also jetzt letztlich
die Umsetzung des Switch-Statements mit sechs Verzweigungen, in tatsächlich einen einzelnen Sprung.
Was jetzt noch fehlt, ist natürlich diese Sprungtabelle und die
einzelnen Kurzsequenzen, die zu den einzelnen Blöcken gehören.
Das war ja jetzt so, dass wenn der Operant null war,
dann wollten wir das Pluszeichen zurückgeben,
sonst nochmal minus slash Prozent und Fragezeichen.
Das hier sind also jetzt die einzelnen Assembler-Kurzsequenzen,
die zu diesen verschiedenen Code-Blöcken gehören.
Ich gehe nochmal
ganz kurz zurück
Es hier, waren also jetzt hier unsere Kurtblöcke für die verschiedenen Varianten.
Die sind natürlich relativ einfach, weil da nichts weiter passiert,
als dass wir einen konstanten Wert in dieses Register ERX schreiben,
was dann anschließend den Rückgabewert enthält.
Und wenn wir das gemacht haben, sind wir schon fertig,
dann springen wir halt in diesem Label 49,
der hinter dem ganzen Sprung hinter dem ganzen Switch-Statement steht.
Also dieses hier verschiebt nichts anderes als den Wert plus in das Register ERX,
hier den Wert mal nach ERX und so weiter.
Und am Ende des Ganzen sind wir fertig
und springen halt dann hier zu Label
Und was jetzt noch fehlt, ist diese Sprungtabelle. Das ist also jetzt hier das,
was der Compiler uns generiert hat, an ausgehenden Staaten von der Adresse L57.
Ja, das ist also jetzt hier der Inhalt,
Herr 57 plus 0, ist L 51,
L 57 plus 4 ist L 52 und so weiter.
Das sind also jetzt hier die Startadressen der Blöcke hier.
Und bei der Ausführung würde das Switch-Statement halt diesen Operanten nehmen. Dann hier die Adresse,
Fluss in dieser Tabelle halt so viel Zeilen heruntergehen,
wie der Operant, der da übergeben wurde, aussagt,
dann das als neue Adresse nehmen und dann,
was weiß ich, hier halt dann hier hinsprechen.
Gut, das kann man sich dann jetzt hier auch nochmal anschauen,
wenn man versucht, das Ganze, was da dann erzeugt worden ist, zu dissassemblieren.
Im Speicher liegen dann halt hier so komische Werte herum,
die halt die entsprechenden Daten enthalten,
also an der einen Stelle ist dann halt hier die,
sind die Befehle dazu und das andere sind dann halt hier entsprechenden Sprung,
die vorberechneten Sprung malten.
So, das muss man jetzt hier wieder zurückinterpretieren,
wegen Little und Big Indian, dass halt das,
was hier zum Beispiel 308, 704, 08 ist,
müssen wir als Adresse von hinten nach vorne lesen.
Also die Adresse wäre dann hier 08, 04, 8,
73, 0. Und das sind dann halt hier Adressen,
die dann entsprechend in dem Programm, in dem diserservierten Programm drin liegen.
So, das war jetzt schon eine ganze Menge Zeug,
obwohl das eigentlich relativ einfach ist
Es ging halt darum, auf der S-Hembler-Ebene die einzelnen Operationen,
die wir schon kennengelernt haben, so was wie Addition oder logische Operationen umzusetzen und für die meisten Dinge,
die man da hat, hat man halt tatsächlich einen passenden Maschinenbefinden.
Dann, damit man ganze Programme schreiben kann,
braucht man halt diese Kontrollstrukturen und IF wird halt einfach in bedingte Sprünge übersetzt und die Schleifen,
du, weil, weil, vor und ähnliche Varianten werden halt einfach umgesetzt,
in lesslich mechanisch in dieser Abfrage ein Label, Schleifeninhalt,
gegebenenfalls Rücksprung, also einfach einen bedingten Sprung.
Und der Switch können wir entweder als if than else,
if than else machen oder mit dieser vorberechneten Sprungtabelle.
Das ist auch eine Sache,
die man auf der Hochsprachenebene sonst nicht so schnell zur Verfügung hat,
dass man halt Sprünge zu dynamisch berechneten Adressen machen kann.
Das bieten die meisten Hochsprachen nicht an. Das wäre also
dann hier eine Sache, die man so in C sonst nicht schreiben kann.
Wir springen also irgendwo hin und der Wert,
wo wir hinspringen, ist halt dynamisch zur Laufzeit berechnet,
abhängig hier von dem Registerwert
Und den magisch vorkombinierten Konstanten, die in dieser Sprungtabelle drinstehen.
Aber im Wesentlichen ist das halt alles vergleichsweise übersichtlich.
Die Hochsprachenkonstrukte werden halt umgesetzt in mehr oder weniger passende Sam-Level-Fehler. So,
und das macht man, das kann man zwar von Hand machen,
also insbesondere dann, wenn man, was weiß ich,
eine nagelneue Maschine hat oder sowas,
wo man noch keinen Compiler zur Verfügung hat.
Ansonsten nimmt man natürlich dann einen Compiler,
der halt dann die entsprechenden Transformationen für einen übernimmt
So, das ist dann jetzt hier so eine kleine Zäsur.
Damit sind wir jetzt in der Lage,
letztlich Programme zu schreiben, die irgendwo starten,
Werte im Speicher entgegennehmen, berechnen, berechnen, berechnen,
berechnen und dann irgendwelche Werte im Speicher hinterlassen.
Was jetzt natürlich noch fehlt, ist die Frage,
wie wir Funktionen realisieren können.
Und das ist relativ klar, wie man das machen kann.
Hier auf der Intelarchitektur gibt es da spezielle Befehle dafür und
ein bestimmter Teil des gesamten Speichers
wird halt reserviert
Und das nennen wir dann Stack und verwenden das, Deutsch und Kellerspeicherstapel,
als Speicherbereich für die lokalen Variablen und Zwischenergebnisse, die wir bei geschachtelten Funktionsaufrufen brauchen.
Also Stack in der Bedeutung des Wortes,
wie wir das jetzt hier verwenden, ist einfach ein bestimmter Teil,
eine Region im gesamten Hauptspeicher der Maschine und die Startadresse wird dann entweder von der Maschine oder vom Betriebssystem vorgegeben. Das heißt,
wir haben jetzt hier irgendwo das Element in dem Speicher,
da fängt dieser Speicherbereich Zack an und der wird dann,
je mehr wir da reinfüllen,
wachsen und irgendwo ist das oberste Element der Stacks und das Top-Element
und auf der Intelarchitektur wird das durch Befehle namens Push und Pop verwaltet,
die automatisch dieses Register ESP aktualisieren,
sodass normalerweise der Inhalt des Registers ESP immer die Speicherstelle enthält,
an der das oberste Element des Stacks
sich gerade befindet
Auf anderen Maschinen gibt es das nicht unbedingt,
dass man halt spezielle Befehle und ein Fest, sagen wir,
zweckentfremdetes Register hat, sondern da könnte man verschiedene Register dafür nehmen.
Da müsste man sich halt in der entsprechenden Dokumentation anschauen,
welche Konvention benutzt wird. Aber die Tatsache,
dass man halt einen bestimmten Bereich des gesamten Speichers als Deck verwendet,
das ist eigentlich universell gültig für alle Maschinen.
So, und dann ist die nächste Frage,
was passiert, wenn das Ding wächst,
starten wir dann Nehmen wir dann die nächsthöhere oder die nächst niedrigere Adresse,
um da neue Elemente reinzupacken.
Und hier ist es halt so,
dass der Speicher irgendwo hier oben eine hohe Adresse hat und
je mehr Elemente wir da reinpacken, desto niedriger werden die Adressen.
Also hier wächst der Stack halt von hohen Adressen nach unten.
Das könnte man gegebenenfalls auch umdrehen. Das ist dann halt
die Konvention für eine bestimmte Maschine. So, wozu können
wir das gebrauchen? Zum einen halt zur Implementierung,
zur Umsetzung von Funktionsaufrufen, Funktionen oder Prozeduren,
also hier im Sinne von Funktionen die Werte zurückgeben oder Prozeduren,
die halt aufgerufen werden
Aber nichts zurückgeben. Und im Detail aufgeschlüsselt, was brauchen
wir? Wir brauchen auf jeden Fall irgendwie Speicherplatz,
um die Parameter mit dir, wer die Funktion aufrufen, abzuspeichern.
Dann braucht die Funktion, wenn sie nicht komplett der Trivial ist,
meistens auch Speicherplatz für ihren eigenen Bedarf.
Das heißt, sie möchte lokale Variablen haben. Und wir
müssen uns überlegen, wie wir die Funktionswerte an die aufrufende Funktion zurückgeben.
So für ein, zwei Funktionen oder sowas kann man das
natürlich relativ einfach hardcoolen. Der Witz ist dann halt,
dass man insbesondere natürlich auch rekursive Strukturen unterstützen möchte.
Das heißt, eine Funktion darf sich selbst aufrufen
Entweder direkt oder eben zwischendurch andere Funktionen und dann wieder sich selbst und so weiter,
sodass man nicht von vornherein festlegen kann,
wie viel Platz müssen wir reservieren,
sondern das möchte dynamisch zugeordnet werden.
Das muss also idealerweise so lange wachsen können,
bis sie halt keine Speicher mehr übrig ist.
Das ist also das, was wir haben wollen.
Das wäre sozusagen das Lastenheft und da müssen wir uns jetzt
überlegen, wie wir das tun können.
Und wenn man da noch weiter reinschaut,
dann muss man sich noch irgendwie entscheiden,
wie denn halt überhaupt so Werte,
Parameter,
eine Sendungfunktion übergeben werden
Eine Möglichkeit ist, dass man diese Parameter in Registern übergibt.
Das ist natürlich bei der Intel-Architektur,
wo wir nur acht Register zur Verfügung haben und gleich sehen werden,
dass zwei eigentlich schon verwendet sind,
würde uns das auf maximal sechs Parameter irgendwie limitieren.
Aber bei anderen Architekturen, anderen Prozessoren,
mit einem wesentlich mehr Registern, ist das durchaus eine Option.
Dann ist die Frage, wenn ich schon einen Wert in
einem Register drinstehen habe und jetzt eine Funktion aufrufe,
darf die Funktion dieses Register benutzen und umsetzen oder nicht
Und natürlich, weil wenn du sechs zur Verfügung haben letztlich,
muss natürlich eine Funktion irgendwie die Register neu verwenden können.
Und dann ist die Frage, wer ist dafür verantwortlich,
die Inhalte der Register irgendwie zu retten?
Nämlich macht das der Aufrufende oder macht das die aufgerufene Funktion?
Und damit das alles funktioniert, müssen sich natürlich alle Teile der Software,
die da verwendet werden, einig sein. Das heißt,
wenn ich jetzt hier eine Funktion, sagen wir mal,
von Microsoft als Binärobjekt kriege und dann eine andere Funktion von jemand anderem
und dann noch meine eigene Funktion dazuschreibe, damit das zusammenspielt,
halt alle Software-Komponenten, die entsprechende Konventionen berücksichtigen und einhalten.
Das heißt, das ist dann gegebenenfalls abhängig vom Betriebssystem plus
der Version des Compilers und der Bibliotheken, die man da verwendet,
wenn das nicht zusammenpasst, weil halt eine Funktion erwartet,
dass der Aufrufer irgendwelche Registerwerte gesichert hat und das nicht tut.
Wenn das nicht passte, dann geht es halt schief.
Das heißt, all das funktioniert nur,
wenn sich alle verwendeten Software-Komponenten einig sind,
also dieselbe Konvention berücksichtigen
So, und das wollen wir uns jetzt anschauen.
Das erste ist halt der Befehl, der einen wert,
auf den Stack packt. Das ist also dann hier
das Push. Und die Syntax ist Push L,
also für 32 Bit-Werte und dann einen Operanten.
Das kann jetzt wieder ein Register sein oder eine Speicherstelle oder irgend sowas.
Und was macht dieser Befehl? Der holt halt
den Wert, der hier effektiv übergeben wird.
Und dann dekrementiert er den Inhalt des Registers ESP um
Das ist also jetzt gerade bei 32 Bit ein Speicherwort mit 4 Bytes.
Das heißt, vor dem Aufruf dieses Befehls ist der Stackpointer,
der Inhalt von ESP hier auf diese Speicheradresse gesetzt.
Und wenn wir jetzt Push aufrufen, dann wird halt
der Wert, den wir pushen wollen, zwischengespeichert.
Dann wird der Stackpointer um 4 reduziert.
Das zeigt also dann auf das nächste Speicherwort.
Und der übergebende Operant wird dann an Dieselspeicherstelle abgespeichert und der
Inhalt und ESP enthält also jetzt die Adresse dieser Speicherstelle,
also letztlich ESP, auf das oberste Element auf dem Stack.
Und wenn man das nochmal macht, dann wird halt hier unten
das nächste Element angelegt und darunter dann wieder das nächste.
Wenn es Push gibt, dann möchte man auch eine Möglichkeit haben,
die Daten wieder abzuholen aus dem Stack.
Das wird mich groß überraschen. Da gibt es dann halt den Befehl Pop,
der einen Eintrag aus dem Stack ausliest und zurückgibt,
beziehungsweise dann eben an die Stelle speichert,
die hier angegeben wird, zum Beispiel ein Register.
Und der liest also dann hier den Wert vom obersten Element
des Stacks
Inkrementiert den Stackpointer um 4. Und der Wert,
der hier drin steht, der wird halt zurückgegeben.
Der Wert bleibt aber ansonsten hier drin. Das heißt,
der Inhalt des Speichers an dieser Stelle wird nicht überschrieben.
Das heißt, wenn man mehrfach Push, Push,
Push, Push macht und dann Pop, Pop, Op,
dann bleiben all die Werte erstmal auf dem Stack liegen.
Das ist dann die Quelle für einen beliebten Programmierfehler,
wenn man diese Werte noch benutzt,
kurz nachdem man das gemacht hat, sind sie noch da.
Wenn dann zwischendurch weitere Funktionen aufgerufen werden,
dann wird das halt früher oder später verändert
So, Visualisierung als Beispiel dafür.
Hier haben wir jetzt halt unseren Stack an einer Startartresse.
Und hier haben wir jetzt drei Adressen davon. Und an
der Adresse 108 ist halt jetzt hier der Wert 123 abgelegt worden.
Jetzt sind hier so ein paar Registerwerte skizziert. Also das ist
der Speicher, und zwar speziell der Speicherbereich für den Stack.
Und hier haben wir Registerwerte 213 im Register A, 555 im Register D,
und der Stackpointer zeigt natürlich jetzt auf das oberste Element der Stacks.
Das ist dieses hier
Wenn wir jetzt einen Push machen, Push, EAX,
dann wird halt der Inhalt des Registers EAX gelesen.
Das ist 213 und an die neue Stelle,
an die neu angelegte Speicherstelle im Stack gepusht.
Das heißt, der Stackpointer wird um vier reduziert, von 108 auf 104.
Und an diese Speicheradresse wird jetzt der Wert geschrieben,
nämlich der Inhalt von EAX. Das heißt,
nachdem wir das gemacht haben, enthält EAX immer noch,
den wir 213,
wir haben das halt nur von hier nach da kopiert und
der Stackpointer ist jetzt auf die Adresse
Sollen wir jetzt Pop machen und den Wert in das Register EDX hineintun,
dann wird halt der Inhalt dieser Speicherstelle,
auf die das Deckpointer gerade zeigt,
an die angenehme Stelle gepackt, nämlich hierhin,
der wird nicht verändert.
Und der Stackpointer zeigt jetzt wieder auf 108,
weil das halt von hier, der wird wieder nach da umgesetzt.
Und die anderen Werte hier, die bleiben halt auf dem Stack liegen.
Gut, das ist soweit plausibel,
das ist also zunächst mal sind einfach nur zwei weitere Assembler-Befehle,
die es also erlauben,
wert auf diesem Stack
drauf zu tun und Werte herunterzunehmen
So, jetzt wollen wir aber eigentlich Prozeduren und Funktionsaufrufe realisieren.
Und dazu gibt es weitere Assembler-Befehle, nämlich zum einen
den Call-Befehl und dann zum anderen Return als Rücksprung.
Beide Funktionen haben eine ähnliche Funktion. Es wird nämlich dann direkt der Programm-Counter modifiziert,
egal an welcher Stelle wir im Programm sind, wenn wir Call aufrufen,
dann wird halt der Programm-Counter auf die Startadresse der Funktionen gesetzt.
Und beim Rücksprung dann entsprechend auf die Speicherstelle,
auf den Befehl nach dem Kräubefehl
Die Parameter-Übergabe erfolgt jetzt nicht auf dem, nicht in Registern,
sondern nur der Return wird, das hatten wir schon,
wird im Register A zurückgegeben und damit das funktioniert,
brauchen wir nicht nur diese beiden Assembler-Befehle, sondern zusätzlich noch eine Konvention,
nämlich zwei Register werden jetzt hier mit Spezialaufgaben versehen.
Das eine ist halt der Stackpointer,
weil die Parameter über den Spitness-Deck übergeben werden und das andere,
das werden wir gleich sehen, wozu wir das brauchen,
das ist der sogenannte Basepointer und das ist letztlich der Teil,
das ist die Speicheradresse,
an dem die Daten der aktuell aktiven Funktionen auf dem Stack liegen.
Aber zunächst jetzt nochmal die Aufruf und Rücksprung ist ja mit der Befehle,
wenn man das immer ins Gesammler-Code schreibt,
dann macht man halt einfach Call und dann einen Label oder eben,
wenn man die Direktadressen verwenden möchte,
dann kann man halt hier statt der Symbolischen Adresse auch direkt eine Speicheradresse nehmen.
Was passiert dann, die Rücksprungadresse, die wir für später
brauchen, die wird auf den Stack gepackt.
Das ist also so ähnlich wie ein Push und dann ein Jump
anschließend kombiniert
Also genauso gut könnten wir halt erst die Adresse berechnen,
dann auch mit dem ganz normalen,
mit dem Push auf den Stack packen und dann einen Jump ausführen
und hier wird mit diesem Prozessuraufrufe das beides gemeinsam gemacht.
Und der Rücksprungbefehl, Return, der macht dann halt das Umgekehrte,
der holt sich die Rücksprungadresse vom Stack,
macht also einen Pop und das, was da rauskommt,
wird dann als Sprungadresse genommen und dann so ein Jump letztlich zu dieser Adresse gemacht.
Auch dieses hier ist also eigentlich redundant,
könnt ihr das halt durch einen Pop und einen Sprung ersetzen
So, um das zu verdeutlichen, was da passiert,
jetzt hier ein ganz kleiner Ausschnitt aus so einem Assembler-Programm,
nämlich hier an der Adresse 8-04 8-5 4E. Das ist also irgendwas,
wo halt unsere Funktionen gerade hinein, die Stelle,
an der die Funktion gerade liegt.
Und dort ist jetzt der Inhalt, ED 3D 06 00 00. Und ja,
dann ist halt hier, das müssen wir weiterzählen,
4E, jetzt haben wir hier 1, 2, 3,
4, 5 Bytes, wer das jetzt machen will,
eh,
das ist also dann hell
012 und dann 3. Das ist also dann die nächste Speicheradresse nach diesem Befehl hier.
Da ist also jetzt der nächste Speicherbefehl, an dem es weitergeht.
Wenn du sie hier einen Call machen wollen,
springen wir zu einer der Funktionen, die jetzt an dieser Adresse liegt.
Und wenn der Rücksprung passiert, dann springen wir von hier zurück an den Befehl,
der nach dem Call liegt, also an dieser Adresse.
Das heißt, dass ein Call muss jetzt zum einen dafür sorgen,
dass wir an dieser Adresse springen und zum anderen muss es dafür sorgen,
dass halt die Rücksprungadresse berechnet wird und entsprechend auf den Stack gepackt wird.
Also dieses Call würde ja Und jetzt an dieser Stelle halt die Rücksprungadresse berechnen.
Das ist also seine eigene Adresse plus 5,
weil das hier mit 5 beiscodiert ist.
Das wird auf den Stack gepackt. Und dann geht die
Befehlsausführung erstmal hier weiter. Und wenn der Rücksprung kommt,
wird die Rücksprungadresse vom Stack geholt.
Das war diese hier. Und dann geht das halt hier
zum Anfang zurück. Auch dazu wieder ein paar Beispiele.
Wir haben jetzt hier in dem gelben Bereich wieder unseren Stack.
Das liegt also dann als Teil des Hauptspeichers.
Und im Moment zeigt der Stackpoint,
der hier auf die Adresse 108 und wieder mit dem Wert
Und das ist also jetzt hier der Inhalt unseres Programm-Counters.
Und jetzt wird der Call-Befehl ausgeführt mit der entsprechenden Adresse hier.
Das Call berechnet dann also die Rücksprungadresse in diesem Fall,
weil das hier mal in dieser Adresse 5 Bytes insgesamt benötigt,
dieses Ding hier, ist halt die Adresse des nachfolgenden Befehls, dieses 8.04853.
Und das wird jetzt hier auf den Stack gepackt und entsprechend
dann halt der Stackpointer angepasst. Und dann wollen wir ja
jetzt die Befehlsausführung an dieser Adresse fortsetzen. Das heißt,
der Programm-Counter wird dann halt mit dieser Adresse geladen und von
dieser Adresse wird dann halt der nächste Befehl erstmal geholt.
Auch hier nochmal kurz einen Blick in die Runde,
das ist hoffentlich auch einigermaßen plausibel. Und den Aufruf hatten wir
jetzt gemacht und jetzt kommen wir irgendwann an zu dem Rücksprung.
Was passiert dann da? Dann wird halt der Wert der Rücksprungadresse
wird vom Stack geholt. Dieses Ding wird also dann hier
in den Programmcounter geladen und der Snackpointer wird halt jetzt entsprechend aktualisiert.
Wir haben einen Wert aus dem Stack abgepflückt,
das heißt von 104 wird das Ding hier wieder auf 108 gesetzt.
Also das ist dann jetzt hier das neue aktuelle obere Element
des Stacks. Und nach der Return-Befehl ausgeführt wird,
geht es dann halt an dieser Adresse in einem Speicher weiter.
Von dort wird der nächste Befehl geholt. So,
das klingt zunächst mal schon mal ein bisschen kompliziert,
aber das Gute dabei ist,
wenn man das mal verstanden hat, dann hat man verstanden,
wie letztlich alle aktuellen Programmiersprachen ihre Funktionen verwalten und aufrufen.
Das gilt also jetzt
nicht nur für
Oder für Java sollten, sondern also eigentlich alle aktuellen Programmiersprachen benutzen diesen Mechanismus,
nicht unbedingt mit dem Call- und Returnbefehlen von Intel,
aber das entsprechende Konzept.
Das heißt, wir werden Parameter und Rücksprungadressen und lokale Variablen
in einem Teil des Hauptspeichers speichern, nämlich dem Stank.
Gut, dann haben wir natürlich Programmiersprachen, die rekursive Funktionen unterstützen,
weil sie eine Funktion sich selbst aufrufen kann.
Und auch das wird, wie wir gleich sehen werden,
unterstützt, ohne dass wir irgendwas extra pfüllen müssen
Das heißt, eine Funktion kann durchaus mehrfach sich direkt oder indirekt neu aufrufen. Und damit das funktioniert,
reservieren wir halt für diese Funktionen ausreichend Platz auf dem Stack,
nämlich für die Funktions-, die Argumente,
die da übergeben werden an die Funktionen,
zusätzlich für lokale Variablen, die wir brauchen.
Und dann, damit wir aus der Funktion wieder zurückspringen können, entsprechend die Rücksprungadresse.
So, das funktioniert zunächst mal nur für Funktionen,
die andere Funktionen auch hoffen, die wieder andere Funktionen aufrufen.
Wenn man dann etwas anderes, nämlich Routinen haben möchte,
wo, was weiß ich,
zwischen verschiedenen Dingen hin und her gewechselt werden kann,
ohne dass sie sich in so einer hierarchischen Struktur wieder zurückspringen.
Auch das kann man auf diese Art und Weise tun.
Das schauen wir uns aber in dieser Vorlesung nicht weiter an.
Gut, jetzt wird hier noch ein neuer Name eingeführt,
nämlich ein sogenannter Frame oder Rahmen.
Das ist dann halt der Bereich des Speichers und speziell ist
der Bereich des Stacks, in dem alle Daten sind,
die wir für einen einzelnen Prozeduraufruf,
Funktionsaufruf benötigen
Genau. Und das nennt man manchmal auch eine Closure.
Das wären dann also alle Daten, die wir brauchen,
um einen Funktionsaufruf zu kapseln. So. Dazu dann erstmal ein Beispiel.
Wir haben jetzt also hier eine äußere Funktion. Jo,
das ist die gelbe, die liegt irgendwo im Hauptspeicher drin.
Normalerweise diese Funktion eben nicht im Stack, sondern halt irgendwo anders im Speicher.
Und diese Funktion möchte hier eine Funktion Who aufrufen.
Der Kurt für die Funktion ruft hier MI auf und nochmal.
Und hier hat das MI einen rekursiven Aufruf auf sich selbst.
Wenn man das als Graf darstellt,
dann würde halt die äußerste Funktion diese Sue aufrufen.
Das Who ruft erstmal sich hier auf,
dann hier und hier nochmal rekursiv. Und dann am Schluss,
also hier und dann am Schluss hier nochmal, abhängig davon,
was jetzt in dem Inneren von dem MI passiert.
Was wäre also eine Möglichkeit, wie das geschachtelt wird.
Wir brauchen also auf jeden Fall hier einen Speicherbereich für
diese Funktion zusätzlich für diese. Und dieses MI,
weil es sich hier rekursiv aufrufen kann,
gegebenenfalls mehrfach Speicherbereich für sich selbst
Okay, wir fangen jetzt mal an,
diese Funktion auszuführen und also die liegt jetzt hier irgendwo anders im Speicher und das ist jetzt hier der Speicherbereich im Stack,
wo wir halt den aktuellen Top-of-Stack haben,
das ist also die, die in diesem Fall niedrigste Speicheradresse,
die in dem Stack gerade benutzt wird und der Bereich der Stacks,
der für diese Funktion Yu benutzt worden ist,
der ist jetzt hier halt gelb markiert.
Und wir verwenden jetzt hier nicht nur den Stackpointer,
sondern ein zweites Register, das Register EBP,
Das erlaubt uns oder mit dem merken wir uns,
wo der Speicherbereich für die aktuell aktive Funktion,
also in diesem Fall halt das You beginnt.
So, dann führen wir halt hier den Code aus und dann irgendwie früher,
später werden wir jetzt an die Stelle kommen,
wo die Funktion, hu aufgerufen wird.
Das heißt, das ist jetzt hier die aktuelle Funktion,
die aktuell aufgerufen wird und die wiederum bekommt auf dem Stack
auch wieder ihren eigenen Speicherbereich zugeordnet.
Abhängig davon, wie viele lokale Variablen sie benutzt und sowas,
wird sich halt weitere Varianten auf diesem Stack packen
Und der Stackpointer markiert jeweils das oberste Element.
Auf der Entel-Architektur eben mit der niedrigsten Adresse.
Und jetzt ist halt der Inhalt von dem BP gesetzt worden
auf den Speicherbereich, der jetzt das markiert.
Und die Werte, die das EU benutzt hat,
die sind alle noch auf dem Stack verfügbar.
Und die Werte hier drunten, die sind noch nicht initialisiert.
Wenn die Funktion jetzt hier so weit gekommen ist,
dass sie dieses hier aufruft, dann wird halt die Funktion MI aktiv.
Die wiederum bekommt ihren eigenen Speicherbereich hier. Wenn sie weitere Werte angelegt hat,
dann ist halt der Stackpointer entsprechend wieder erniedrigt worden.
Und jetzt ist halt das Register BEP so initialisiert worden,
dass es halt auf den Anfang der Speicherreichs,
Speicherbereichs der Funktion MI erreicht. Dieses Layout bedeutet,
dass halt die aktuelle Funktion einen Zugriff auf ihre eigenen Daten hat.
Und wenn sie entsprechend die Adressen hier zurückverfolgt,
kann sie eben auch an Daten der aufrufenden Funktion und an Daten der aufrufenden,
aufrufenden Funktion heran und so weiter.
Und die Werte hier unten sind halt noch nicht initialisiert.
Wenn sich das Ding jetzt hier rekursiv selbst aufruft,
dann bedeutet das,
dass die Funktion halt für ihren neuen Aufruf
tatsächlich einen neuen Speicherbereich bekommt
Der dann entsprechend hier auch aktiviert wird,
indem halt der Stackpointer wieder auf das aktuell oberste Element zeigt.
Und der Basepointer auf den Anfang des Speicherbereichs für die aktive Funktion.
Die hat sich also jetzt hier einmal selbst rekursiv aufgerufen,
kann an ihre lokalen Daten ran. Und wenn sie das Speicher-Layout kennt,
entsprechend auch an die Daten der aufrufenden Funktion und so weiter.
Und wenn das dann nochmal passiert, dann würde eben noch
ein weiterer Rahmen auf diesem Stack angelegt werden.
Und das sind also jetzt hier die lokalen Parameter-Funktionswerte und Variablen der aktuellen,
das ist also jetzt hier Die dritte Instanz dieser Funktion MI.
Und wenn das dann zurückspringt, dann wird dieses hier halt abgeräumt. Und entsprechend,
sorry, so, dann halt hier liegen die Daten noch rum,
aber sie sind nicht mehr aktiv,
weil wir halt den Zweckpointer jetzt hier auf das aktuell oberste Element
initialisiert haben und von dem vorgehenden Rahmen jetzt von hier zurücksetzen müssen,
diesen hier auf den Framepointer für die zweite,
zweiten Aufwurf von einem Ei,
wenn man da wieder zurückspringt,
müssen wir den hier auf den setzen und den Stackpointer
noch da
Und dann ist das soweit abgeräumt. Und wenn das jetzt
auch wieder zurückspringt in die Funktion, hu, dann würde das halt hierhin gehen.
Aber wenn dann das MI als nächstes nochmal,
ein zweites Mal jetzt aufgerufen wird, dann wird das entsprechend hier wieder ergänzt.
Und die Werte, die jetzt hier zwischendrin da noch liegen,
die werden jetzt halt hier mit den aktuellen neuen Werten des zweiten äußersten Funktionsaufrufs von dem MI hier überschrieben.
So, und dann springen wir da nochmal wieder zurück.
Das heißt, das Zeug hier ist jetzt ungültig geworden.
Die Variablen sind jetzt wieder die Aktiven. Und wenn wir
dann von dem Hu auch noch zurückspringen auf das Y,
sind wir jetzt hier wieder an der Situation ganz zu Anfang.
Und das ist zwar jetzt hier nur über drei Ebenen illustriert,
wie das funktioniert, aber ich glaube, es ist klar,
dass man das beliebig tief schachteln kann,
solange man halt noch speicherfrei hat für diesen Stack, kann man immer weiter Funktionen aufrufen.
So, das war halt das grobe Prinzip, wie wir das machen wollen.
Jetzt muss man natürlich noch die Details vereinbaren.
Was kommt wohin? Wie schon gesagt,
dass es dann gegebenenfalls Betriebssystemen und architekturabhängig und auch ab abhängig von den verschiedenen Tools.
Also kann durchaus sein, dass ich das von Version X
des Betriebssystems zur Version X plus 1 oder nicht nur Betriebssystem,
sondern auch Tools, zum Beispiel ein neuer Compiler ändert.
Die Konvention, die jetzt hier im Folgenden benutzt wird, ist halt X686 und Linux.
Wobei wir halt dann diese Struktur haben. Das hier ist also jetzt,
also das Rosa ist der Speicherbereich der aufrufenden Prozedur, aufrufenden Funktionen Und gelb,
der Speicherbereich der aufgerufenen Funktion, und zwar der Stack halt,
dass der Stackpointer zeigt zu jedem Zeitpunkt auf das oberste Element vom Stack.
Der Basepointer wird so verwaltet, dass er auf das erste
Element der Daten, der aufgerufen, der aktiven Funktion zeigt.
Und also das gibt halt an, wo die Funktion gerade lebt.
Das erste Element in dem Rahmen, also in diesem Speicherbereich,
dem Frame, für die aktuelle Funktion ist dann immer der Basepointer,
der vorher aktiven Funktionen auf diese Art und Weise können wir halt von hier noch da zurückkehren,
weil wir hier den Wert hatten, den wir vorher in EBP drin hatten.
Und dann oberhalb ist halt der Bereich,
den die Funktion für sich selbst hat,
zum Beispiel um Registerwerte zu speichern und lokale Variablen aufzunehmen.
Und wenn die Funktion dann eine weitere Funktion aufrufen will,
dann packt sie die Argumente des Funktionsaufrufs ganz unten auf dem Stack.
So, und wenn man das jetzt rekursiv verfolgt,
wenn die Argumente der gelben Funktion hier unten liegen, dann müssen offensichtlich die Argumente,
mit der die gelbe Funktion aufgerufen wurde, hier liegen.
Und das tun sie dann auch
Der Stack wird also so aufgebaut, wenn wir eine Funktion aufbauen wollen,
packen wir die Allgemeinde hier auf den Stack.
Und dann die Rückgabeadresse als allerletztes und alles anschließend gehört dann halt zu aufrufenden Funktionen.
So, ich glaube, die Zeit ist jetzt um.
Es macht keinen Sinn, dass alles noch fertig zu kriegen.
Von daher mache ich jetzt an dieser Stelle dann Schluss und
wir gucken uns die Details für den Funktionsaufruf dann halt nächste
Woche Mittwoch an.