Oh, ich hatte am Mittwoch ja damit aufgehört. Letztes vorgestellt,
wie im Prinzip jetzt auf Assembler-Ebene bedingte Sprünge abgearbeitet werden,
dass man also jetzt beispielsweise ein Hochsprachenkonstrukt in C,
also eine Verzeigung, Zweifel verzeugen,
jetzt durch eine ganz normale F-Abfrage,
dass das eben eher eigentlich einem Code entspricht,
wo ich dann im Prinzip einen Sprung zu einem Label habe,
was dann also ein C dann quasi so eine Initialisierungabfrage
Sprung zu diesem Label entsprechen würde und ich habe dann eben
quasi mein Ergebnis schon mit einem Wert vorbesetzt und nur für den Fall,
dass diese Abfrage jetzt eben quasi das andere Ergebnis liefert wurde,
dass dann eben entsprechend geändert werden.
Mir hängt genau das Gleiche findet man dann eben auch in entsprechenden Strukturen im Assembler-Code wieder. Gut,
bei den anderen Kontrollstrukturen, das heißt also irgendwelche Schleifen,
irgendwelche, ja also Schleifen in Form von Wahlschleifen in Form von irgendwelchen Vorschleifen,
läuft das im Prinzip genauso, dass das also entsprechend übersetzt wird.
Das heißt, wenn ich jetzt also beispielsweise hier einen Code & C hätte,
der mir also die Fakultät berechnet,
wo ich also im Prinzip das Ergebnis mit eins vollinitialisiere,
dann eben entsprechend mit dem jeweiligen Wert von X multipliziere,
X anschließend eben dekrementiere und eben hier quasi als Endabfrage dann eben frage, bin ich immer noch größer als eins?
Ansonsten habe ich dann eben hier entsprechend viele Iterationen dann hier
mein Ergebnis gemacht. Das würde man dann eben auch
entsprechend umsetzen
In einem C-Code, der jetzt eben eigentlich dem hier entspricht. Das heißt,
ich habe dann eben auch so ein Label,
also eine Sprungadresse, dann nachher auch im Assembler,
hätte dann eben hier die entsprechenden Anweisungen und würde dann eben
quasi hier am Ende mit dieser Abfrage eben entweder bei der
nächsten Iteration dann wieder auf diese Adresse springen oder dann eben hier entsprechend das Ergebnis machen.
Und diese Art C-Code, wie man sagen wir jetzt als normaler Programmierer natürlich das jetzt nicht machen sollte,
entspricht aber eben eigentlich dann den war es der Compiler, dann als Assembler-Quote daraus generiert. Das heißt,
ich hätte hier dann eben auch erstmal die Initialisierung dieses Unterprogramms,
was es damit auf sich hat,
wird dann also gleich im nächsten Abschnitt noch genauer erklärt,
wo es hinter so einem Stack und Unterprogrammaufrufe geht,
würde mir dann im Prinzip jetzt die Werte einmal den Die Vortrückgabe wird von eins und eben einmal das Argument in die Register EAX bzw. EDX laden.
Hätte dann also hier das Label,
was quasi jetzt genau diesen Label-Loop in dem C-Code entspricht,
anschließend dann eben die Multiplikation von dem Inhalt von EAX und X an das ganze Ergebnis wird,
wie gesagt, in X686 Assemble habe ich immer zwei Abreissbefehle,
das heißt, das Ergebnis wird diese Operation,
also EAX ist gleich eher X mal EDX,
wird dann eben auch wieder in diesem Register gespeichert.
Nicht dieser Zähler für das X wird dekrementiert,
verglichen ist er gleich eins und solange er größer gleich Null ist,
Jump, nee, größer Null ist, also Jump Creator,
das heißt also letztendlich dein großer gleiche Eins,
springe ich dann eben hier in die nächste Iteration,
andernfalls kommt jetzt hier noch ein entsprechender Code um diesen Unterprogrammaufruf
letztendlich dann auch wieder sauber zu beenden und das Ergebnis der ganzen Berechnung befindet sich dann in diesem Register EAIX.
Generell kann man also sagen, bei solchen Dualschleifen,
das ist der C-Code,
wenn ich ihn jetzt eben mir so als Programmiere überlege,
letztendlich in so eine Go-To-Version entsprechend übersetzt wird,
wo ich dann eben hier entsprechend beliebige Anweisungen im Schleifenkörper
habe und eben nur diese Endabfrage mit dem Wahl dann
quasi durch so ein IF und einen Sprung auf dieses Label
entsprechend mache
Bei Waldschleifen, wo ich quasi jetzt die Abfrage zu Beginn der Schleife mache,
würde man das Ganze dann jetzt erst mal auch auf C-Ebene so übersetzen,
dass ich das erst mal umsetze in eine Schleife mit Endabfrage.
Das heißt also, es gibt eben einmal ganz am Anfang
eine Abfrage, also if not test. Das heißt,
dann wäre ich entsprechend schon fertig und ansonsten dann eben hier eben diese Iteration in dieser Schleife,
also Schleifen, Körper und Die Endabfrage,
solange Test war es, das lässt sich dann eben,
also sprich dieser innere Teil, entsprechend diesem Schema von vorhin,
umsetzen in so eine Version mit einem Sprungziel und dann eben dem entsprechenden Go-To.
Und das wird dann eben entsprechend auch wieder vom Compiler in
den passenden Assembler-Code umgesetzt. So, jetzt noch einen drüber gesetzt,
ist das Ganze im Prinzip jetzt bei einer ganz normalen Vorschleife bei einer Generation,
wo ich im Prinzip am Anfang erstmal Initialisierungscode habe,
dann hier quasi meine Testanweisung, die dann quasi dem Wile mit Anfangsabfrage entspricht.
Code, der dann eben noch am Ende der Schleife letztendlich ausgeführt wird,
in der Regel dann eben sowas wie irgendwelche Zähle ankramentieren,
dekrementieren, was auch immer.
Und dann im eigentlichen Schleifen körpern. Und das würde dann eben übersetzt werden,
erstmal in so eine Art, weil Code in meinem Programmiersprache,
dass dann umgesetzt werden diese Dual-Version und das dann entsprechend umgesetzt werden,
so eine Version,
die dann also mit diesem Goto entspricht
Also letztendlich einziger Unterschied zu dem anderen Teil vorher,
ist eben, dass ich hier den Initialisierungscode dann letztendlich hätte und
am Ende des Schleifenkörpers dann eben nach der entsprechenden Code,
der mir jetzt also irgendwelche Zähler verändert oder Abfragefunktionen macht.
Gut, das ist eigentlich relativ einfach, straightforward so umzusetzen.
Wie sieht das jetzt bei Mehrfachverzweigungen aus? Also das,
was dann okay ist, also im Prinzip eine NC,
Entschuldigung,
sowas wie eine Switch-Anweisung wäre
Auf Assembler-Ebene habe ich im Prinzip zwei Möglichkeiten, das umzusetzen.
Die erste Möglichkeit ist jetzt natürlich erst mal so ein bisschen kontraproduktiv,
dass ich das einfach an geschachtelte IF-Bedingungen umsetze.
Das heißt einfach eine Serie von Abfragen. Das funktioniert natürlich immer noch relativ gut,
wenn ihr jetzt vergleichsweise wenig Alternativen habe, ist aber dadurch,
dass ich dann entsprechend viele Tests mache und dann eben auch vielleicht relativ viele unnötige Tests mache,
gerade wenn man dann also sehr viel mehr Fälle hat,
also eben vielleicht auch langsam.
Und das, was man dann eigentlich besser macht,
zumindest solange die Anzahl der Sprungverzweigung, also noch entsprechend vernünftig
sich da einarbeiten lässt, ist dann eben eine sogenannte Sprungtabelle.
Also im Englischen ein Jump-Table. Damit kann man dann im Prinzip
wirklich einzelne abfragen, vermeiden und das funktioniert sehr gut,
solange ich also diese Sprungbedingungen jetzt wirklich in sowas wie beispielsweise kleine Entager-Werte direkt umrechnen kann.
Und letztendlich muss ich mich als Programmierer nicht darum kümmern,
wie jetzt also aus meiner Switch-Anweisung in C,
also irgendwo auf Assembler-Ebene, jetzt also irgendwie der Code umgesetzt wird,
sondern das macht also der Compiler in der Regel natürlich automatisch.
Aber das nur als Hinweis, also die Möglichkeiten gibt es. Wie so eine Sprungtabelle funktioniert,
sieht man also jetzt gleich ganz gut an, folgendem Beispiel,
also angenommen, ich habe jetzt hier also irgendwie so einen selbstdeklarierten Aufzählungstyp
Also das Ganze ist irgendwie im Kontext, was weiß ich,
so eines kleinen Parsas für irgendeine einfache Grammatik geschrieben.
Ich habe jetzt quasi mein Symbol eingelesen und möchte jetzt abhängig von diesem Symbol was,
also es ist quasi einem dieser Werte meines Aufzählungstyps irgendwie entspricht,
irgendwo ein Charakter in diesem Fall, also quasi das Zugehörige Zeichen,
beziehungsweise wenn das Symbol unbekannt ist, eben so ein Fragezeichen,
zurückgeben.
Gut, das würde dann eben jetzt bei so einer Sprungtabelle letztendlich so umgesetzt werden.
Im C-Code habe ich dann ja also irgendwie sowas
wie so ein Switch abhängig jetzt von diesem Dieser Bedingung,
dann eben Case-Anweisung und hier darin dann eben irgendwie jeweils einen entsprechenden Satz von irgendwelchen Operationen in meinem Code.
Und das, was denen dann entspricht, ist,
dass ich im Prinzip eine, oder dass der Compiler da vielmehr,
also eine Sprungtabelle anlegt,
wo abhängig von dieser Verzweigung quasi jetzt Anfangsaddressen beziehungsweise Startadressen von Kotblöcken stehen,
wo dann eben für die zugehörigen Sprünge letztendlich die Anweisung
hinterlegt sind
Das heißt, man kann sich das immer so vorstellen, also jetzt ein C-Code übersetzt.
Nicht, dass sich hier so ein Array habe,
in das ich mit diesem Wert hinein adressiere quasi als Index
und in dem Array sind dann eben die entsprechenden Adressen von dem eigentlichen Co-Teilen.
Und ich würde dann eben einfach nur das Ganze hier
entsprechend der Referenzieren und dann eben auf diese Adresse sprengen.
Vorteil bei dem Ganzen ist natürlich, dass sich im Prinzip
solche Mehrfachverzweigungen mit konstantem Aufwand umsetzen kann. Nicht,
wenn ich also geschachtelte EFS hätte,
dann hätte ich ja so im Prinzip einen linearen Aufwand,
also O von N. Nicht, das heißt, mit der polemopolinen Größe N würde der Aufwand linear wachsen.
Hier ist der Aufwand letztendlich konstant. Nicht, weil wenn ich sowas anlege, ist es egal, also ob ich hier fünf Fälle oder 500 Fälle letztendlich hinterlege. Gut,
ihr nehmt Beispiel,
wenn man sich das jetzt also noch ein bisschen weiter mal wirklich so in C-Code umsetzt,
würde das dann eben so aussehen, dass jedem dieser Symbole dann jetzt,
also beispielsweise eben irgendwie so ein Entertainment wert,
also in diesem Fall zwischen 0 und 5 entspricht und im zugehörigen Assembler-Code hätte ich dann im Prinzip hier die Initialisierung von dem Ganzen,
wurde dann in das Register EAX, das Argument,
also sprich, das entsprechende Symbol OP hineinlesen,
das ist dann also quasi dieser kleine Integra-Wert,
würde dann eben vergleichen mit der Konstanten 5,
wenn ich 5 bin,
dann gut, dann bin ich hier am Ende,
würde dann also entsprechend dort irgendwo hinspringen und ansonsten habe ich dann hier einen Sprung in diese Tabelle,
über so eine Adressberechnung, die im Prinzip nach folgender Mechanismus funktioniert,
dass der Assembler im Prinzip diese symbolischen Label in dieser Form Punkt Eltern,
also irgendwo in die entsprechenden Speicheradressen umsetzt.
Ich habe dann eben eine Tabellenstruktur, wo dann jedes Ziel,
also mit Ziel ist dann quasi jetzt immer hier dieser Jump Target gemeint.
Das heißt, die Adresse wird ja dann quasi in dieser Tabelle eingetragen. Nämlich das Wort,
was dort eingetragen wird als Adresse, ist dann quasi,
sind noch nie was zu entsprechend vier Byte und Basisadresse ist dann quasi die Speicheradresse,
wo diese Tabelle dann hinterlegt ist.
Das heißt, man hätte dann im Assemblaco 2 Sprünge,
nämlich einmal für den Fall hier vorne,
dass ich da im Prinzip jetzt als Ergebnis größer gleich fünf bin,
dann wird eben direkt zum Ende gesprungen
Das heißt, da wird jetzt also gar nichts zurückgegeben,
beziehungsweise der Wert selber bleibt in diesem Register stehen. Nicht,
dass es eben dieses Jump auf das Label 49 und ansonsten
wird über diesen Ausdruck im Prinzip dann die entsprechende Adresse berechnet,
indem ich dann quasi hier als Label 7.50 Uhr die Basisadresse der Sprungtabelle habe,
in dem Register ERX ist ja quasi der Wert,
ob also quasi diese Zahl null bis fünf
in dem Beispiel
Die wird jetzt also bei diesem Adressierungsmod, also sprich der Inhalt,
also diese Zahl wird genommen mit vier multipliziert.
Das ist da quasi jetzt genau der Offset der Zugehörigen Werte
in der Adresse. Und das,
was dann also letztendlich mit diesem Ausdruck herausgerechnet wird,
ist dann eben die Basisadresse von dieser Tabelle plus Operant mal vier.
Das heißt, da habe ich dann im Prinzip genau entsprechend diese Adresse,
diese Adresse dieser Asse und so weiter.
Und die wird dann entsprechend referenziert
Gut, im Kot wurde das dann, also im Assembler-Code,
würde das dann also wirklich jetzt so aussehen,
dass sich quasi in der,
wenn man sich das jetzt mit einem Dissessembler ansieht,
gibt es dann immer noch eine Section oder einen Bereich RO-Data.
Das ist dann also Read-Only-Data. Da liegt dann im Prinzip der Compiler
solche Datenstrukturen mit Konstanten ab. Das heißt also irgendwas,
wo der Benutzer mit seinen eigenen Programmen letztendlich gar nicht reinschreibt,
sondern etwas,
was dann also quasi zur Compile-Zeit erzeugt wird
Und in diesem Fall habe ich dann eben hier als Einsprungziel
dieses Label 57 und hätte dann eben hier jeweils die entsprechenden Adressen oder die Label von dem einen Code,
das hier hinten ist Kommentar,
das heißt also dieses Label entspricht dann dem Operanten null dieses Label im Operanten 1 und so weiter und so fort.
Also entsprechend an dieser Reihenfolge. Und unter diesen Labeln wird dann,
also in einem Assembler-Code, also quasi dann eben der entsprechende Anweisung, also in diesem Fall kopieren des Aski-Zeichens in das Register EAX im Prinzip nur gemacht und dann eben einen Rücksprung hier auf das Ende. Gut,
wenn man sowas jetzt versucht, irgendwie als per Disressemblieren,
also direkt jetzt aus dem Binärcode herauszuholen, muss dann immer sagen,
das ist also dann immer etwas, zwar im Prinzip auch möglich,
also man kann mit diesem Befehl Object-Dump dann also auch wirklich dieses Read-only-Date,
also solche Sprungtabellen beispielsweise herauslesen, wie dieses Beispiel hier allerdings zeigt,
also was jetzt aus dem Point und Helleron genommen ist,
ist es dann aber eben doch vergleichsweise schwer zu lesen,
weil hier liegen dann also irgendwelche Binärwerte drin,
die wären jetzt also quasi vom Disassembler natürlich jetzt nicht in irgendwelche Code-Anweisungen übersetzt,
sondern man sieht jetzt hier wirklich nur die nackten Binärwerte,
wie sie im Speicher stehen,
wenn ich jetzt versuche hier raus, also irgendwelche Adressen herauszulesen,
muss ich dann eben auch immer noch berücksichtigen,
dass also beispielsweise, wie hier ganz gut zu sehen ist,
also war der Intel X-86-Architektur mit einer Little-Andiance-Zahlen-Darstellung
Im Prinzip dieses hier dann die Adressen sind. Ich dann aber im Prinzip
hier die Bar jetzt immer noch entsprechend jeweils vertauschen muss.
Also hier sieht man jetzt, diese Adressen gehen irgendwie so 8-0,
4-8, B-C-0. Und wenn ich das hier vertausche,
also das wäre jetzt das Höchstwertigste bei,
dann hätte ich also 8, 0-4,
8-7, 3-0,
das liegt dann also hier schon irgendwo so in der Nähe dieses Adressbereichs.
Aber das ist eben dann doch ein bisschen schwieriger dann rauszulesen,
beziehungsweise da dann weiterzuarbeiten,
wenn man also wirklich jetzt mal in die Verlegenheit kommen sollte,
bei solchen Brot per dissassemly zu untersuchen.
Gut, zusammenfassend zu den Kontrollstrukturen, also auf Assembler-Ebene,
kann man eben sagen, gut, also lassen sich im Prinzip alle direkt umsetzen.
Das, was ich auf Assembler-Ebene einfach immer nur brauche,
ist Sprung unbedingt oder eben ein bedingter Sprung.
Was man sich dann auch nochmal merken kann oder ansehen kann,
also diese Sache, dann eben diese Umsetzung von diesen ganzen
Strukturen dann eben entsprechend immer in diese diesen C-Code mit diesen Goto-Beispielen,
was sie jetzt ja eingangs gezeigt hatte.
Und das wird dann eben entsprechend passend aus dem Funk-Compiler heraus erzeugt.
Und gut, bei Mehrfachverzweigung, also bei Cases,
gibt es dann eben noch als andere Möglichkeit dann eben diese Sprungtabelle einzusetzen.
Gut, um jetzt Bedingungen abzufragen,
hatte ich ja am Mittwoch schon gezeigt,
gibt es ja im Prinzip jetzt also auf dem X-86-Assembler
dann eben entsprechende Befehle,
die dann im Prinzip auf diesem Flag-Register arbeiten
Das ist typischerweise was, was also bei Cisk-Rechnern eigentlich passiert,
während bei Riskmaschinen hat man meistens keine speziellen Fleck-Register,
sondern dort werden für irgendwelche Bedingungen oder für irgendwelche Abfragen,
die ich in Bedingungen mache, dann eben besondere Anweisungen genommen.
Das heißt, wenn man irgendwann mal in die Verlinkenheit kommen sollte,
also jetzt eben wirklich sowas wie eine Rissmaschine zu programmieren,
das heißt,
wenn sie jetzt also irgendwie so ein eingebettetes System für irgendein Smartphone oder irgendwas programmieren,
was dann eben entsprechenden Befehlssatz hat.
Da wird es dann eben in etwas anders aussehen. Aber wie gesagt,
das ist dann eben auch in der Regel in Beispielen zu
den ganzen Programmierumgebungen, zu den entsprechenden Assemblern beschrieben.
Gut, jetzt werde ich noch mal etwas ausführlicher auf die Verwaltung von Funktionsaufrufen eingehen. Also das,
was dann beim X86-Assembler über ein Stack, also dann,
ich bleibe dann hier eigentlich in der Regel immer jetzt beim englischen Ausdruck,
sonst im Deutschen, wenn man das immer als Kellerspeicher übersetzen,
Was also über ein Stack passiert, der eben genutzt wird,
um bei Funktionsaufrufen Argumente zu übergeben und lokale Speicherbereiche für Unterprogramme zur Verfügung zu stellen.
Betriebssystemseitig kann man sich immer vorstellen,
ist der Stack also erstmal nichts anderes,
als also einfach für einen laufenden Prozess,
eine diesem Prozess hoffentlich exklusiv zugeordnete Speicherregion,
über die ich auf Assembler-Ebene jetzt eben nur mit speziellen Befehlen in der Regel darauf zugreifen kann,
beziehungsweise über die Referenzierung entsprechender Stackpointer.
Und adressmäßig oder im Speicherraum selber ist das Stack so angeordnet,
dass das immer so aussieht, dass bei irgendeiner Adresse der Stack beginnt.
Das ist in der Regel, ich weiß jetzt auch nicht,
warum das jetzt so rumorganisiert ist,
aber dass es in der Regel immer erstmal eine höhere Adresse.
Und wenn das Stack jetzt also aufgebaut wird,
das heißt Elemente dort reingespeichert werden, dass der größer,
indem die Adresse dann immer kleiner wird.
Interessant oder ist, was man daran eigentlich immer braucht?
Also ich habe ja immer nur ein einziges Element beim Stack
eigentlich von der Idee her erstmal im Zugriff. Das heißt also,
dieses Strop-Element, auf das wird dann eben mit dem Inhalt dieses speziellen Registers ESP verwiesen,
was dann eben genau der Stackpointer ist, der dann eben auf dieses oberste Element im Speicher weist.
So, um den Stack zu bearbeiten, gibt es dann
eben typischerweise zwei Operationen, Push und Pop. Das heißt also,
Hier jetzt bei einem Assembler,
wenn nicht also ein Porschlong von irgendeinem Ausdruck für Source mache,
wird eben quasi dieser Wert geholt, je nachdem,
was das ist, kann das eben ein Imediate-Wert sein,
Inhalt eines Registers oder eben irgendeinen Speicherinhalt,
den ich da also über die üblichen Adressierungsmodi wieder auswählen kann.
Anschließend wird dann der Stackpointer um vier dekrementiert,
das heißt, der zeigt auf das nächste Speicherwort.
Eine Vier ist eigentlich jetzt auch klar, Das,
was ich hier beschreibe, ist 32-Bit.
Wortbreite, Speicher ist beidweise adressiert. Das heißt,
der Offset zwischen zwei Worten ist dann immer genau vier Byte,
demzufolge wird da immer plus vier, beziehungsweise minus vier gerechnet.
Ja, und anschließend wird dann eben dieser Wert,
der also quasi hiermit geholt wurde, auf diese speichere Adresse,
auf die der Stackpointer dann verweist, gespeichert.
Analog dazu gibt es dann eben eine entsprechenden Popbefehl,
wo eben Der Wert, auf den der Stackpointer verweist,
ausgelesen wird, in Destination gespeichert wird.
Nicht anschließend wird eben der Stackpointer um 4 inkrementiert und sagt dann
eben auf das Nächst oder da drunter liegende Element des Stacks.
Gut, hier ist das Ganze nochmal mit entsprechenden Beispielen hinterlegt.
Anfangs habe ich also jetzt hier irgendwo meinen Stack-Bereich mit irgendwelchen
Adressen,
Exzellenzen mal
10C, 108. Hier also dein Element 1, 2,
3 drin. Hier jetzt die beiden Universalregister EIX und EDX
mit irgendwelchen willkürlichen Werten gefüllt. Stackpointer verweist eben auf die Adresse 1,
08, also genau diese 1, 2,
3. Wenn jetzt also als Befehl ausgeführt wird,
push long EAX, passiert eben,
dass der Inhalt von ERX hier auf den Stack aufgeschrieben wird unter der Adresse 104,
also quasi der nächsten Adresse und die ist dann natürlich auch logischerweise dann Inhalt der Stack,
der Stackpointers.
Und beim Pop wird quasi jetzt dieser Wert hier gelesen,
in diesem Fall jetzt in EDX gespeichert.
Das ist eben genau dieser Transfer und der Stackpoint,
da wird dann quasi jetzt wieder zurückgerechnet und verweist dann auf dieses Element.
Im Hauptspeicher verbleibt an dieser Stelle natürlich immer noch der alte Wert,
dadurch, dass der Stackpoint jetzt aber hierhin zeigt,
ist das quasi jetzt für den Prozess erstmal wieder ungenutzter Bereich,
der dann also letztendlich irgendwann wieder mit neuen Inhalten des Stacks
überschrieben werden kann
Gut, mit dem Stack wird jetzt im Prinzip bei Prozedur aufrufen oder generell Unterprogrammen aufrufen,
also sei das jetzt Prozedur, sei das Funktion,
im Prinzip gearbeitet,
indem ich für diese Unterprogrammaufrufe dann eben Call und Return zur Verfügung stelle und mit Call und einem Label,
was hier dann eben folgendes,
dass auf den Stack des gerade laufenden Prozesses im Prinzip eine Rücksprungadresse hinterlegt wird,
nämlich quasi die nächste Adresse,
wo dann der Prozess nach Beendigung des Unterprogramms quasi weiterarbeiten würde.
Und anschließend springt oder verzweigt die Maschine in der Abarbeitung dann quasi auf diese Adresse Label.
Das heißt, der Programmzähler wird dann mit diesem Wert hier geladen.
Das Ganze würde als Beispiel dann quasi so aussehen, also angenommen,
ich habe jetzt hier unter diesen Adressen irgendwo ein Callbefehl stehen,
hier die entsprechende Adresse,
wo dann quasi der Einsprung in der Prozedur stattfindet.
Ich hätte jetzt hier als nächsten Befehl jetzt eben einen Befehl,
der nach der Prozedurabarbeitung,
also quasi nach dem Rücksprung aus der Prozedur als nächster abgearbeitet wurde.
Ich finde, dieses ist jetzt also diese Adresse quasi das Argument
jetzt hier von diesem Call, wo diese Prozedur abgelegt wird.
Dann passiert hier irgendwas, irgendwann hätte ich dann eben entsprechend das Return.
Gut, und die Rücksprungadresse, die dann eben auf dem
Stack gespeichert wird, ist dann eben genau diese 8048 553.
Das heißt, die das, was dann quasi die Folgeadresse
des aufrufenden Programms entsprechend wäre, wenn der Rücksprung erfolgt ist.
Gut, hier ist das Ganze dann eben auch nochmal.
Als Beispiel gezeigt, das heißt, mit dem Call,
auf dieser Adresse aus meinem Hauptprogramm oder aus meinem aufrufenden Programm.
Würde dann ihm die Vorgeadresse des aufrufenden Programms auf den Stack gesichert werden.
Nicht dem zufolge wird natürlich dann entsprechend wie beim Pop quasi der Stackpointer aktualisiert.
Und der Programmzähler, der mir jetzt ja sagt,
unter welcher Adresse ich jetzt den nächsten Befehl zu lesen habe,
würde dann eben nicht diesen Wert erhalten,
sondern quasi die Adresse,
wo dann,
also dies unter Programm beginnt
Beim Rücksprung genau das Umgekehrte dazu. Das heißt,
wenn es also im Code das entsprechende Return erfolgt,
wird eben der Stackpointer zum einen nachher inkrementiert.
Und hier hatte der Stackpointer dann eben entsprechend auf diese Programmzeile in einem aufrufenden Programm verwiesen.
Und die würde dann eben entsprechend in den Program Counter geladen,
der dann eben von dieser Adresse jetzt quasi hier den nächsten Rot liest,
der jetzt also nach dem Prozedur-Aufruf weiterarbeiten würde.
Gut, damit kann man dann eben entsprechend arbeiten und
eben jetzt nicht nur einfache Programme oder Prozeduraufrufe,
sondern eben auch entsprechend rekursive Aufrufe direkt in Assembler-Ebene mit unterstützen.
Das heißt, das würde dann eben eigentlich für alle Sprachen
funktionieren, die dann eben auch entsprechend Rekursion unterstützen.
Ich sei das Java oder sahen das eben imperative Programmiersprachen,
also C, Pascal,
was auch immer
Voraussetzung bei dem Ganzen ist natürlich, also wie jetzt also bei
allen diesen Sachen, also der Code muss reentrend sein.
Das heißt also, es müssen mehrfache simultane Instanzierungen der Prozedur möglich sein. Das heißt,
die Puzedur darf oder sollte dann in der Regel natürlich keine Seiteneffekte haben, die jetzt also sich da dann auch irgendwo auswirken. Gut,
was dann bei der Rekursion eben passiert über den Stack,
ist, dass dann eben für jede Instanzierung, die also während der Rekursion stattfindet,
Auf dem Stack dann letztendlich die Argumente beim Aufruf irgendwo hinterlegt werden,
im Stack dann eben einen Speicherbereich geschaffen wird,
wo dann quasi jede Instanz lokale Variablen letztendlich hinterlegen kann und damit rechnen kann und dann eben quasi über diesen Call- und Return-Mechanismus,
das was ich gerade eben vorgestellt habe, dann eben die entsprechende Rücksprungadresse hinterlegt wird.
Gut, das Ganze wird dann eben im Prinzip über den Stack-Mechanismus verwaltet. Beispiel dazu vorgleich. Voraussetzung dabei ist dann eben auch immer noch,
dass diese sich jetzt eigentlich erstmal nur auf einfache Prozesse beziehungsweise,
wenn man jetzt vergleichsweise einfache Mechanismen hier bezieht,
wo ich jetzt also nicht anfange,
mehrere oder einen Prozess jetzt in mehrere Tasks aufzusplitten,
die jetzt also irgendwie parallel laufen,
sondern wo ich jetzt eben erstmal immer davon ausgehen kann,
dass ein Aufrufendes Programm,
letztendlich durch den Aufruf des Unterrprogrammes quasi anhält,
stehen bleibt,
nicht anschließend das Unterprogramm anfängt zu arbeiten bzw. abgearbeitet wird und dann eben auch immer vor oder eben immer sauber beendet wird und dann erst das aufrufende Programm dann eben entsprechend weiterarbeitet.
Damit funktioniert das dann aber eben entsprechend sehr gut. Wie das aussieht,
wird also jetzt gleich nochmal in passenden Beispielen verwiesen in dem Kontext nochmal,
möchte ich nur noch kurz motivieren, also diesen Begriff des Stackframes.
Das ist im Prinzip der Teil, das,
was sie hier oben schon beschrieben habe,
dass ich also jetzt für jede Instanz oder für jeden Aufruf
eben einen Bereich habe, der quasi jetzt diesem Aufruf entspricht.
Das ist eben dieser Stackframe,
auf dem dann eben die Parameter für den Aufruf irgendwo hinterlegt werden,
wo dann eben im Prinzip lokale Variablen sind,
Rücksprungadresse und wo dann eben gegebenenfalls auch noch Bereich für
irgendwelche temporären Dateien ist
Nicht,
das Ganze wird dann im Prinzip immer verwaltet oder dieses gesamte Stack-Verwaltung durch diese beiden Codeblöcke,
die ich, also ja auch immer schon,
jetzt erstmal so ein bisschen übersprungen hatte bei den ganzen Beispielen zu irgendwelchen Prozeduren,
wo dann also gezeigt wird,
wie beim Aufbruch letztendlich der Speicherbereich auf dem Stack jetzt irgendwie aluziiert wird,
beziehungsweise also sprich,
wie ich da also jetzt mir auf dem Stack mit irgendwelchen Push-Anweisungen da was Neues generiere,
den Stackpoint da passend setze
Und alles, was ich am Anfang zu Beginn einer Prozedur gemacht habe,
dann eben am Ende entsprechend rückwärts genauso wieder zurückrechne.
Das heißt also, da ist also irgendwelche Bereiche auf dem
Stack dann eben auch passend frei, weil ich gebe.
Gut, dafür gibt es typischerweise dann eben zwei Zeiger.
Zum einen eben diesen Stackpointer, der immer dynamisch
das obere Ende des Stacks angibt.
Das heißt also immer diese Adresse,
die dann bei jedem Push
quasi um vier inkrementiert
Wird beim Pop entsprechenden wieder plus Filetkrementiert wird, ist Stackwork kleiner.
Und was man dann eben meistens hat,
ist dann eben ein Framepointer, der quasi den Anfang dieses Bereichs angibt,
wo quasi jetzt die Daten für einen Funktionsaufruf quasi beginnen und dort hinterlegt werden.
Gut, wie sieht das beispielsweise aus? Also ich habe jetzt hier eine Funktion oder eine Prozedur You,
die ruft eben hier irgendwo Hu auf in Wu,
habe ich jetzt hier auch irgendwo Code, rufe dann zweimal,
also eine Prozedur MI auf und in dieser Prozedur MI habe ich jetzt gegebenenfalls eben auch noch sowas wie Rekursionen mitenthalten.
Wenn abgearbeitet werden soll, also im Prinzip hier die folgende Aufrufkette,
das heißt also ich fange an, also die SIO abzuarbeiten,
stoße dann hier drauf, rufe dann eben hier das ein erstes Mal,
also diese Prozedur auf
Dabei kommt es dann eben in diesem Fall jetzt auch noch mal irgendwie zur Rekursion.
Irgendwann ist das Ganze fertig. Dann bin ich hier im
Code und irgendwann komme ich dann eben auf den zweiten Aufruf,
bis dann eben auch hier das Unterprogramm beendet wird und ich
dann also hier in einem aufrufenden Programm weiter arbeite.
Das Ganze kann man sich dann eben so vorstellen,
ja Sven, die Prozedur quasi gestartet ist,
habe ich hier mal Stack, der Framepointer,
also die ist EBP,
verweist dann quasi auf den Beginn des Bereichs,
der dann quasi zu dieser Prozedur gehört
Backpointer ist dann quasi jetzt immer hier unten das Top-Element in einem Stack.
Ich bin jetzt anschließend eine Prozedur von da aus jetzt eben weiter aufgerufen wird,
muss dann eben der Code dafür sorgen, dass also der Framepointer entsprechend umgesetzt wird.
Nicht, dann passiert hier irgendwas in dem Zusammenhang,
wo dann natürlich dann auch der Stackpointer noch passend gesetzt.
Anschließend kommt dann das gleiche Spiel dann wieder,
wenn ich jetzt eben hier eine oder in dieser Funktion hier das erste Mal.
Also jetzt diese Unterfunktion MI-Aufrufe mit entsprechendem Sätzen dieser Point da umsetzende Stackpointers,
irgendwas, wird dann hier gespeichert,
der Stackpoint wird demzufolge dann quasi hier auch immer kleiner, Stack wächst.
Und genau das Gleiche passiert dann eben auch bei allen rekursiven Aufrufen. Das heißt also,
wenn sich eine Prozedur dann selber rekursiv aufruft,
ist das also im Prinzip von der Funktion oder von der Abarbeitung auf dem Stack beziehungsweise von der Abarbeitung von dem,
was sich da dann eben letztendlich dann Im Assembler-Programm dann,
also bei diesem Beginn und bei diesem Endcode stattfindet,
genau das Gleiche wie eben ein beliebiger anderer Prozedur-Aufruf auch.
Und so würde der Stack dann quasi jetzt also im Sinne
so einer Aufruf-Hierarchie irgendwie dann immer weiter wachsen.
Nicht die Prozedur rechnet dann jetzt hier irgendwie durch,
stellt jetzt eben irgendwo fest, okay,
ich habe jetzt keine Rekursion mehr oder bin jetzt also hier fertig, wird entsprechend beendet.
Nicht anschließend wird dann quasi die aufrufende Prozedur dann
eben nach diesem Aufruf irgendwo hier als Programmzähler dann weiterarbeiten.
Hätte dann eben ihren früheren Framepointer und Stackpointer hoffentlich wieder zurückerhalten,
rechnet dann eben bis zum jeweiligen Aufrufende und das Ganze setzt sich dann eben entsprechend fort.
Dann eben auch über die Hierarchie, wenn es dann hier zum zweiten Aufruf kommt, eben wieder genau das gleiche Spiel und so weiter und so fort. Gut,
wenn man sich jetzt ansieht,
was muss beim Prozeduraufruf auf dem Stack gespeichert werden bzw. wie wird jetzt eigentlich diese Verwaltung von diesem Framepointer und von diesem Stackpointer passieren,
Dann kann man immer sagen,
also die ist jetzt hier als Beispiel für ein X86 Stack Frame,
wie das also jetzt hier zum Beispiel beim Linux organisiert ist,
weil das Ganze ist dann natürlich auch immer noch Betriebssystem abhängig,
wie Stacks verwaltet werden oder wie sowas gemacht werden,
wo man dann auch immer sagen kann,
also letztendlich verwenden da alle Betriebssysteme vergleichbare Mechanismen,
nicht bloß wie da, wo,
welcher Befehl genau aussieht oder wie das dann implementiert ist,
das unterscheidet sich dann natürlich betriebssystemabhängig dann doch immer noch schon
Gut, also in diesem Fall kann man jetzt eben davon ausgehen,
beim aktuellen Stackframe, das heißt also,
wenn ich jetzt in einer aufgerufenen Prozedur drin bin,
dann habe ich eben immer quasi diesen Framepointer,
der mir quasi markiert,
wo mein eigener Datenbereich beginnt und unter der Adresse dieses Framepointers ist dann quasi der alte Framepointer,
der aufrufenden Funktion gespeichert,
das heißt also irgendwo beim Return von dem Ganzen muss das Ding dann eben entsprechend hierüber restauriert werden,
beziehungsweise wenn eine Funktion aufgerufen wird, wird das eben entsprechend gesetzt.
Beispiele dazu kommen gleich. Nicht dann habe ich hier eben irgendwelchen Speicherbereich,
wo man dann eben sowas wie lokale Variablen speichern kann,
wo aber dann eben auch teilweise irgendwelche Register zwischengespeichert werden.
Komme ich dann auch gleich noch zu, weil Frage ist natürlich,
wenn ich jetzt also irgendwelche rekursiven Aufrufe mache,
was passiert jetzt eigentlich mit den Registern
Also irgendeine aufrufende Funktion rechnet, also auf diesen Universalregistern rum,
ruft dann was anderes auf.
Darf diese andere Funktion jetzt mit dem Registern was machen?
Zerschreibt dabei vielleicht Werte, die die aufrufende Funktion hinterher
nach dem WN des Unterprogramms wieder bräuchte?
Oder wie ist das organisiert? Also das werde ich
mir auch gleich noch vorstellen. Da gibt es dann eben
auch so Standardkonventionen, wie sowas letztendlich zu handhaben ist.
Das heißt also, hier liegen gegebenenfalls noch
irgendwelche gesicherten Werte
Und dann eben irgendwelche lokalen Variablen. Gut, und am,
ja gut, hier, das wäre dann dementsprechend dieser Tag
auf den vorherigen Frame und hier hätte ich dann eben irgendwo meinen Stackpoint da. Gut,
bei der aufrufenden Funktion, wenn ich letztendlich den Call ausführe,
wird dann eben vor Beginn dieses Calls auf dem Stack quasi jetzt da rein,
oder wenn dann,
also auf dem Stack der Reihe nach die gesamten Argumente hinterlegt,
weil ich muss ja auch irgendeinen Datenbereich haben,
wo ich für eine aufzurufende Funktion jetzt irgendwie die Argumente
lassen kann
Die werden eben vor dem Call auf den Stack gelegt.
Anschließend kommt dann eben die Call-Anweisung, also dieser Call-Befehl.
Nicht und der sorgt dann eben dafür,
dass diese Rücksprung-Adresse quasi auf den Stack gelegt wird und anschließend dann
der Programmzähler quasi jetzt mit dem Code von der aufgerufenen Funktion initialisiert wird.
Gut, wie würde sowas dann praktisch aussehen.
Also hier ist jetzt eben nochmal so ein Beispiel,
wo es also eine Funktion, jo,
eine andere Funktion Who aufruft, wenn ich das hier nochmal in Klammern,
die aufgerufene Funktion im Englischen, eine Macorli genannt,
die aufrufende Funktion im Englischen immer Crawler genannt.
Wie gesagt, da die Folien jetzt quasi immer hier von
dem Englischen heraus sind, nicht nur,
dass die Begriffe dann soweit klar sind.
Gut, also hier wäre dann eben
der eigentliche Aufruf
Und was man hier in diesem Code jetzt eben sieht,
ist jetzt genau diese Fragestellung, die ich eben ja schon angesprochen habe.
Also ich habe hier in der aufrufenden Funktion irgendein Kopieren von der Konstanten in das Register EDX.
Nicht dann rufe ich die Funktion auf, die macht irgendwas,
nicht und hinterher rechne ich mit diesem Register EDX weiter.
Und die Frage ist jetzt, was passiert oder wie sieht das jetzt aus,
wenn ich in dieser aufgerufenen Funktion selber jetzt mit dem EDX rechnen möchte,
darf da dieser Wert überschrieben werden.
Oder entsprechend nicht. Gut, in dem Zufolge kann man
sich jetzt für das Sichern von Registern quasi mal zwei Konventionen überlegen.
Nämlich zum einen muss die aufrufende Funktion dafür sorgen,
dass die Register gesichert werden. Das heißt,
diese Funktion müsste dann eben vor dem Aufruf von dem Call dafür sorgen,
dass also die gesamten eigenen Register irgendwo erstmal auf den Stack gelegt werden.
Hinterher entsprechend wieder zurückgeschrieben werden. Man kann es aber eben auch genau umgekehrt machen,
dass also die aufrufende Funktion er die Blödsinn, die aufgerufene Funktion, dafür verantwortlich ist,
dass wenn sie beispielsweise jetzt hier auf irgendwelchen Registern rechnet,
wie in diesem Fall EDX, dann eben diese vorher auch sichern und hinterher wieder restaurieren muss.
Wenn ich zu diesen Registerkonventionen nochmal meistens und also auch in den gesamten Beispielen ist das eigentlich immer so,
also dies Register EAX, da möchte man die Wechselwirkung haben,
weil über dieses Register wird dann typischerweise sowas wie ein Rückgabewert zurückgegeben,
sofern sich das, also da ist also mein ein bisschen das Wort handelt.
Nicht aber ansonsten sollen die Register letztendlich ja bei aufrufender Funktion und bei aufgerufener Funktion dann beliebig, frei benutzbar sein. Gut,
bei diesen Konventionen, das heißt,
ob der aufrufende Prozess oder der aufgerufene Prozess,
das speichern muss, sieht das also in diesem Fall jetzt,
also bei Linux-Systemen dann immer so aus, dass man sagt,
von diesen acht Universalregistern werden Diese beiden, sagen wir mal sowieso,
erst mal nicht von den Prozessen verwendet,
beziehungsweise die haben dann eben quasi wirklich diese speziellen Verwendungen,
dass sich dann eben Basepointer und Stackpointer hier habe.
Und von den verbleibenden sechs Registern gelten dann eben drei als Corly-Safe. Das heißt, EBX,
ESI und EDI müssen quasi von der aufgerufenen Funktionen gesichert werden,
wenn die damit arbeitet oder arbeiten möchte,
während die ersten drei Register, also EAX,
EDX und ECX, gelten quasi als Callersafe.
Das heißt, die aufrufende Funktion,
wenn sie eben wirklich dort eigene Werte hat, muss dafür sorgen,
dass die dann eher so gegebenenfalls vorher gesichert werden,
bevor also im Prinzip war es, der das Unterprogramm aufgerufen wird.
Gut, und das Register EAX hatte ich ja eben schon gesagt,
wird in der Regel dafür genommen,
eben sowas wie so ein Returnwert zurückzuliefern
Gut, wie sieht sowas dann als Beispiel aus,
hier immer genommen, gerne genommen dann die Berechnung der Fakultät,
also ich habe jetzt hier also ein kleines, rekrusives Programm,
wo ich jetzt also hier sage, ein Argument X,
wenn X-Kleiner gleich eins ist, soll eins zurückgegeben werden,
ansonsten habe ich in der Inducer Variable,
in die Wörtern eben quasi das Ergebnis von Fakultät minus eins erreichen,
gespeichert und dann entsprechend mit dem eigenen Argument X noch ansprechend multipliziert und das ist dann eben eine Rückgabe wert.
Gut, bei dem Umsetzung von diesem Code,
jetzt hier ähnelt als Samler-Code, würde man dann eben auch beispielsweise sehen,
dass im Aufruf dieser Funktion das Register,
also hier werden im Prinzip zwei Register benutzt,
nämlich einmal E-Ax und einmal E-B-X, mit denen gerechnet wird,
dass eben AX, was hier als Caller-Safe deklariert ist,
nicht weiter gesichert werden muss.
Das heißt, da wird also direkt auf Romund gerechnet,
beziehungsweise darüber wird dann eben auch diese Rückgabe wert,
also die Fakultät von x minus 1 zurückgegeben,
während das Register EBX, was ally safe ist,
wenn das wie hier temporär jetzt irgendwie genutzt wird,
muss diese Funktion eben dafür sorgen, dass das der Inhalt dieses Registers,
den also die aufrufende Funktion ja vielleicht also schon irgendwo benutzt hat,
einmal gespeichert wird.
Das passiert eben dadurch, dass also hier zu Beginn im Stack eben dieses Pushlong EBX passiert.
Das heißt, es wird einmal auf dem Stack gespeichert.
Und Ende des Ganzen wird es dann eben entsprechend jetzt also
über diesen Befehl Move Long minus vier von Basepointer,
wenn ich das würde, dann eben genau auf dieses entsprechende Stack-Adresse referenzieren,
würde es dann eben wieder zurückgeschrieben werden.
Und diese Thale hier oben für Beginn des Codes bzw. Ends des Codes
Das sind dann eben noch genau die Tale, die letztendlich dafür verantwortlich sind,
dass bei diesen Aufrufen dann eben Framepointer und Stackpointer entsprechend gesetzt werden.
Und wenn man sich also die Beispiele und auch alle Beispiele,
die ich ja jetzt am Mittwoch schon gebracht habe, daraufhin also nochmal irgendwie ansieht,
da wird man dann in der Regel immer im Prinzip diesen Code oder eben irgendwie was Vergleichbares finden.
Nicht generell kann man eben sagen, bei solchen Funktionsaufrufen,
also auch in der allen Übungsaufgabe,
wo dann ja so ein bisschen Code vorgegeben ist,
Wenn man sich da da ansieht von Einsprungstelle bis zu dem Teil vor dem Return,
wird sich also hier irgendwie spiegelbildlich quasi immer irgendwo so ein Teil finden,
wo ich also quasi jetzt hier irgendwie einen Push habe,
hier das entsprechende Pop, hier Move, Long,
ESP nach EBP, Move Long,
EBP nach ESP.
Das heißt, das sind immer genau die Teile,
die dafür sorgen,
dass bei den einzelnen Aufrufen quasi immer Basepointer und Stackpointer
beim Unterprogrammaufruf entsprechend gesetzt bzw. zurückgesetzt werden
Gut, wie sieht dieser Aufruf dann einzeln aus,
also ich hätte jetzt quasi vor Beginn des Aufrufs,
also hier in der Aufrufenden Funktion irgendwo ein Basepointer,
der zeigt also quasi auf den Beginn des Teils,
wo die Funktion jetzt aufgerufen wird, dann eben einmal,
weil er in dieser Funktion jetzt dies EBX Nehmen wir den hier.
War hier das EBX, also auch selber, benutzt wird,
wo das also jemandem Push reingeschrieben. Das ist also dann dieser Teil hier,
dann eben irgendwo ein Teil für eine lokale Variable.
Und dann nachher dementsprechende Stackpointer. Und wenn jetzt quasi die Funktion aufgerufen wird,
wird eben als allererstes mit dem Befehl Pushlon von Basepointer quasi der Alte Werte,
Spacepointers, also sprich das, was hier in dem Register ist,
also dieser Verweis quasi in den Stack gepusht.
Das heißt, hiermit hätte ich dann letztendlich genau diesen Zeiger hierhin,
der also jetzt hier direkt in einem Register drin ist.
Und dann gibt es eben dieses Movelong, Stackpointer, Basepointer,
das heißt, da wird der Basepointer auf den Wert des aktuellen Stackpointers gesetzt,
der Stackpointer zeigt zu dem Zeitpunkt quasi hierhin,
dem zuvor zeigt dann eben der neue Basepointer Genau, hier drauf.
Nicht in Anschließen gibt es dann eben noch das Pushlon,
EBX, wo dann eben diesen Fall ist, also hier dieses Register,
was lokal benutzt wird, in den Stack noch gesichert wird.
Und der Stackpointer zeigt dann eben auf dieses entsprechende Element noch allen weiter.
Das ist also im Prinzip dieser Initialisierungscode,
der in diesem Fall dieser Funktion jetzt also sehr direkt nach dem Aufruf stattfindet,
der dann eben dafür sorgt, dass diese beiden Pointer,
die dann vor Beginn des Aufrufs so entsprechend gesetzt wird,
gesetzt sind, dann eben einmal hier gesichert sind und eben dann passend Basepointer und Stackpointer angepasst werden.
Gut, dann hätte man ja im Prinzip anschließend hier
den rekursiven Aufruf. Da geht es jetzt darum,
wie kriege ich jetzt diese Funktionen hin,
dass ich zum einen irgendwo den Wert X,
der mir irgendwo übergeben wird,
gespeichert bekomme
Beziehungsweise wo dann im Prinzip der Return wird, dieses rekusiven Aufrufes
dann genommen wird und dann also letztendlich das Ergebnis zu berechnen.
Das ist dann also hier in einem Code in diesem inneren Teil dieser Schleife,
wo dann hier einmal eben in das Register ERX
temporär jetzt quasi das Argument gespeichert wird für den rekursiven Aufruf
Das wird dann eben auf den Stack gepackt,
als Argument für den Aufruf,
dann passiert eben dieses mit dem Call der entsprechende Aufruf.
Und hinterher wird dann eben der Returnwert,
der sich dann also in EAX befindet,
nach dem Aufruf mit dem eigenen Argument X multipliziert,
was dann also diese Mull-Anweisung ist.
Damit hätte man dann im Prinzip
genau diese beiden Anweisungen über diesen Mechanismus
letztendlich umgesetzt
Gut, hier sieht man das Ganze dann eben auch nochmal für so einen Teil Aufrufhierarchie,
wie das dann also auf dem Stack aussieht. Das heißt,
ich habe jetzt hier in dem Register IBX nach der Abarbeitung,
das ist so dieser Teil hier oben, das Argument X,
lokal gespeichert, hätte dann über die Berechnung in ERX den Wert x-minus 1,
das ist im Prinzip genau dieser Befehl, der das dann berechnet,
dann kommt eben das entsprechende Push ERX, was dafür sorgt,
dass dann eben Dieses Argument war es dann quasi für den Unterprogrammaufruf übergeben wird,
auf den Stack gelegt wird.
Dann kommt eben das entsprechende Call, was dafür sorgt,
dass die Rücksprungadresse auf den Stack kommt und ich dann
eben entsprechend jetzt hier einen weiter,
dann eben das Argument der aufgerufenen Funktionen habe.
Und anschließend bei der Rekursion würden dann eben wieder diese Schritte beginnen,
wo dann eben entsprechend jetzt hier
die Point Dog umgesetzt werden
Gut, beim Rücksprung nach Aufruf geht es jetzt ja darum, das Ergebnis zu übergeben.
Wie schon angedeutet, das passiert eben immer in dem Register EAX. Das heißt,
da habe ich jetzt im Prinzip schon durch irgendwelche rekursiven Aufrufe
letztendlich jetzt also den Wert von x-minus-1 Fakultät,
würde dann anschließend dann eben noch diese Multiplikation von dem Wert,
mit EBX machen, das Ganze dann eben auch wieder dort speichern.
Das heißt, letztendlich wäre anschließend dann also in dem Register
der Wert von X-Fakultät gespeichert. Und der Prozess würde sich
dann eben, wäre dann quasi hier auch beendet,
beziehungsweise wir entsprechend hier würde sich dann eben entsprechend dann auch beenden.
Und eine Rekursionsstufe höher wird es
entsprechend weiter berechnet werden
Gut, wenn der Prozess jetzt beendet wird,
ich hatte ja schon gesagt,
ganz am Anfang habe ich immer diesen Set-up-Code, der dafür sorgt,
dass also quasi jetzt Stackpointer und Basepointer irgendwie passend gesetzt werden.
Am Ende muss sich das Ganze jetzt alles irgendwie rückgängig machen.
Das heißt, der aufgerufene Prozess hat hier irgendwo Basepointer,
hier jetzt irgendwo noch einen Stackpointer. Nicht das,
was dann eben passiert, ist das einmal entsprechend der,
dass Register EWX, dann wieder zurückgesichert wird, was hier gespeichert war.
Das ist dann eben genau unter der Adresse Basepointer minus 4.
Das war ja quasi ganz am Anfang als erstes einmal gesichert worden.
Also Basepointer minus 4 wird dann also wieder in EBX gespeichert.
Das ist dann im Prinzip der Wert,
den die aufgerufene Funktion hier irgendwann mal in dem Register hatte,
nicht anschließend wird,
dann eben wieder
der Base
Oder der Stackpoint hat leicht den Basepointer gesetzt. Das heißt also,
der Stackpointer zeigt dann eben nicht mehr hierhin,
sondern es wird dann passiert dann quasi diese Speicherfreigabe.
Das heißt, der Stackpointer verweist dann eben auch hierhin.
Nicht mehr kommt eben das entsprechende Pop von dem Basepointer.
Das heißt, da wird dann eben dieser alte gesicherte Wert wieder zurückgesetzt.
Der Basepointer verweist jetzt quasi auf den Beginn des Stackframes der aufgerufenen Funktion
und gleichzeitig führt das Ganze dann eben auch dazu natürlich,
dass dann entsprechender Stackpoint, der dann also hier auf hier Return-Adresse verweist.
Und wenn jetzt also der die Anweisung Return kommt,
sorgt das dann eben dafür,
dass eben die dort hinterlegte Return-Adresse dann eben entsprechend in den
Programmzähler geladen wird und die aufgerufene Funktion eben an entsprechender Stelle weiterrechnet.
Gut, ist immer irgendwie so ein bisschen länglich mit den Pointern,
aber so kann man sich dann eigentlich immer ganz gut veranschaulichen,
was jetzt eigentlich wirklich beim Prozeduraufruf beziehungsweise beim Rücksprung aus Prozeduren
dann also wirklich genau auf dem Stack passiert
Dazu noch eine Anmerkung in diesem Beispiel. Mal gucken,
ob ich Junge ein bisschen gut finde. Na gut.
Also in diesem Beispiel wird eigentlich immer damit gerechnet,
dass ich also jetzt wirklich für jeder,
für jeden Stackframe letztendlich beide Pointer benutze und verwende.
Das ist dann, sagen wir mal,
so der
entsprechend lehrbuchmäßige Ansatz
Wo ich eben quasi immer sage,
für eine aufgerufene Funktion, gibt mir Basepointer an,
da beginnt der eigene Speicherbereich und dann geht er ihm,
je nachdem, was sich da alles schon reingeschrieben habe,
bis quasi hier Stackpoint, alles dazwischen ist eben das,
was die Funktion irgendwo, ich sag mal, im Zugriff hat.
Wenn man sich Compiler-generierten Code ansieht, hier nach Optimierung,
sieht das dann also auch teilweise noch sehr stark unterschiedlich aus. Das,
was sich in der einen heutigen Übungsaufgabe auch habe,
Und da wird also dieser, da taucht dieser Basepointer nicht auf,
weil man kann natürlich auch sagen, okay,
den lasse ich eigentlich auch noch als freies Register und rechne eigentlich auch sämtliche anderen Adressen immer nur relativ zu dem Stackpointer aus.
In der aufgerufenen Funktion. Kann der Compiler letztendlich machen,
da der quasi die Kontrolle hat oder genau weiß,
also wie oft sind schon irgendwelche Push- und Pop-Operationen passiert und
eben genau weiß, zeigt, dass Stackpointer jetzt hin.
Und wenn ich jetzt also zum Beispiel hier auf ein aufrufendes Argument X verweisen möchte,
wo ich dann also im Prinzip hier irgendwelche Offsets zu diesem Point dann brauche,
kann ich das eben zum einen natürlich sehr elegant immer berechnen als Offset zu dem Basepointer,
wo ich immer nur weiß,
plus 8 ist das erste Argument plus 12 ist das zweite Argument.
Das heißt, das hätte dann der aufrufende Prozess You
hier drüber abgelegt und so weiter und so fort.
Wenn ich das jetzt relativ zu dem Stackpointer mache,
der sich vielleicht zwischenzeitlich irgendwo auch schon durch irgendwelche Push-and-Pop-Anweisungen verändert hat,
ist das natürlich ein bisschen unübersichtlich, ja,
beziehungsweise jetzt für einen selber, also auch ein bisschen schwieriger zu lesen.
Nichtsdestotrotz, dadurch, dass sowas natürlich in der Regel vom Compiler her erzeugt wird,
der dann also genau weiß, wo der Stackpointer liegt,
kann er dann also die entsprechenden Offsets dann eben auch berechnen.
Also das nur als Hinweis war,
also der Code in der Übungsaufgabe verwendet,
also diesen Basepointer nicht,
sondern da wird immer nur relativ schlimm Stackpointer gerechnet
Gut, anschließend ist jetzt hier noch mal ein kurzes Beispiel,
beziehungsweise noch mal gezeigt, wie man jetzt Parameter-Verwaltung macht,
die jetzt nicht dadurch passiert,
dass ich also jetzt wirklich die zu übergebenen Werte direkt irgendwo hin kopiere,
also wie der ist jetzt immer in den Beispielen war,
also jetzt wirklich direkt auf den Stack lege von der aufrufenden Funktion anschließend,
wenn er Scroll mache und das holt sich dann die,
eben die übergebenen Werte direkt vom Stack runter, sondern das,
was er nun in der Programme eigentlich auch so häufig hat,
ist oder machen möchte,
sondern im Prinzip sowas wie ein Kolber Referenz,
wo ich dann eben wirklich sage,
die aufgerufene Funktion soll jetzt eben direkt irgendwelche Werte verändern in der aufrufenden Funktion.
Das heißt, das, was man dann eben typischerweise übergibt,
ist eben nicht der Wert, sondern ein Pointer auf den Wert und über den kann dann eben entsprechend derreferenziert werden.
Dazu ist jetzt also hier auf den nächsten Parfolien das ganze Beispiel mit dieser Fakultät dann also nochmal um eine rekursive Prozedur erweitert, die dann im Prinzip das Ganze mit Kolber-Referenz macht. Das heißt,
ich habe hier meine eigentliche Funktion, die dann diesen rekursiven Teil aufruft,
der aber jetzt quasi hier eben einen Wert X bekommt und dann eben den,
also jetzt in C-Sindax quasi dann eben entsprechend die Adresse von dem Wert Value,
mit dem dann letztendlich gerechnet wird,
nicht und hier drin habe ich dann eben letztendlich genauso mein rekursiven Kotaufruf,
wo ich dann also quasi immer die Laufvariabler,
oder ein Halauffahrer oder nicht,
aber also das Argument X entsprechend dekrementiere dann damit Wiederaufrufe,
nicht und der einzige Unterschied zu dem anderen ist dann,
aber natürlich,
dass ich quasi jetzt hier immer mit der Adresse arbeite,
beziehungsweise das dann also hier bei der Berechnung der Funktion entsprechend der referenziere.
Gut, hier wäre dann eben auch nochmal der entsprechende Code,
wo ich dann sowas habe,
wo am Anfang dann eben auch erstmal wieder diese üblichen Schritte passieren,
Basepointer speichern, Stackpointer entsprechend setzen.
Anschließend wird jetzt also hier mit diesem Befehl explizit der Stackpointer einfach weiter dikrementiert.
Damit wird jetzt einfach jetzt hier für die Funktion Esfact in diesem Fall erreicht,
dass der Stackpoint jetzt nicht nur irgendwo hier auf einem Bereich zeigt,
sondern also irgendwie noch entsprechend Weitergelaufen ist und man dann also hier letztendlich irgendwie freien Bereich hat,
den man jetzt beispielsweise für lokaler Variablen nutzen könnte.
Das heißt, wenn ich jetzt also irgendwelchen Assembler-Code habe,
wo jetzt also irgendwelche lokalen Datenstrukturen auch noch auf dem Stack-up belegt werden sollen,
dann würde man im Prinzip auch sowas finden,
wo dann einfach explizit der Stackpointer weitergerechnet wird.
Nicht in alles,
was man da dann irgendwo so als freie Adressen in einem Stack hat,
könnte dann entweder relativ zum Basepointer oder relativ zum Stackpointer
letztendlich benutzt werden,
um Gott reinzuschreiben bzw. von dort
zu lesen
Gut, in diesem Beispiel soll jetzt passieren,
dass der Wert, der dann da übergeben wird,
auf den Stack gespeichert wird und der Zeiger hier drauf,
auf diesen Wert, kann dann eben im Prinzip berechnet werden,
in diesem Fall jetzt hier relativ zu dem Basepointer,
also basepointerminus 4 wäre dann also quasi jetzt hier dieser Zeiger und an Start jetzt für den Aufruf der Funktion eben irgendeinen Wert,
wie er sonst immer dies X zu übergeben wird,
jetzt also quasi jetzt hier diese entsprechende Zeigerwert auf den Stack gelegt,
was dann im Prinzip mit dieser Anweisung dann passiert,
wo dann eben entsprechend Nee, Entschuldigung,
da war ich jetzt also falsch.
Also hier wird erstmal natürlich nur das Argument X erstmal gelesen.
Entsprechend jetzt also hier dieser Wert mit 1 initialisiert.
Man müsste es jetzt hier also auf der nächsten Folie weitergehen. So,
und hier wird jetzt also im Prinzip genau diese Zaga-Adresse genommen
in X gespeichert und dann hier mit dem Push,
EIX, also auf den Stack getan. Das heißt,
dann habe ich also hier an dieser Stelle eben anschließend die Adresse,
die mir hier drauf verweist,
und anschließend wird dann eben nochmal das X selber auf den Stack gepackt,
also jetzt hier als zweites Argument,
also die Funktion hat hier, hatte er zwei Argumente,
nämlich X als erstes und eben diesen Zeiger als zweites.
Das wird immer in umgekehrter Reihenfolge gemacht, das heißt als erstes kommt das hintere Argument,
anschließend dann eben das vordere Argument auf den Stack,
nicht und dann kann eben entsprechend der eigentliche Unterprogrammaufruf stattfinden.
Gut, hier in dem Beispiel würde man dann eben sehen,
wie jetzt also diese Funktion selber jetzt dieses, diesen Pointer letztendlich benutzt. Das heißt,
das, was also im C-Code quasi jetzt hier diese Referenzierung von diesem Erkumulator multipliziert mit X ist,
und das Ergebnis wird dann eben wieder auf diese Adresse zurückgeschrieben,
würde dann eben genau diesen drei Assembler-Befehlen entsprechen,
wo ich dann eben quasi jetzt den Inhalt des Registers ECX,
na X-Beicher, dann eben die Multiplikation, nicht und dadurch,
dass ich jetzt also den Inhalt von EDX als Adresse nehme, habe ich dann eben darüber diese der Referenzierung.
Jetzt habe ich das Ergebnis in EIX und das wird dann
eben wieder auf diese Adresse gespeichert, was dann eben Seekrug quasi jetzt genau dieser Zeile entspricht.
Gut, wie man da jetzt schon gesehen hat,
generell jetzt bei diesem Aufrufen, also mit Hilfe des Stacks,
lassen sich dann also sowohl Unterprogrammaufrufe, aber eben auch noch etwas allgemeinern, eben auch Rekursionen realisieren.
Nicht dadurch, dass eben jetzt wirklich für jeden Aufruf dieser
lokale Speicher jetzt zwischen Basepointer und Stackpointer zur Verfügung gestellt wird.
Aufgrund dieser Mechanismen,
die man da jetzt quasi per Konvention in diesem Fall irgendwo festgelegt hat,
wie jetzt Register entweder durch das aufgerufene Programm Also die Callers safe oder durch das aufrufende Programm,
also die Cordy-Safe-Register, gespeichert werden müssen.
Kann man dann eben davon ausgehen, dass man dann
dieser Register dann eben auch beliebig benutzen kann.
Und wie gesagt, Initialisierung mit Call, mit Repturnern eben der entsprechende Rücksporn.
Gut, und damit lassen sich dann eben jetzt wirklich
alle Hochsprachenprogrammkonstrukte dann eben auch passend,
also jetzt ein Unterprogrammaufrufe umsetzen
Ist Sansa im Einzelnen, also gerade,
wie man jetzt hier an einem vorigen Beispiel mit diesem Call bei Referenz daneben auch gesehen hat,
also teilweise vielleicht erst mal etwas umständlich,
wo man dann also erst mal Speicheradressen berechnet,
das dann also irgendwo auf dem Stack hinterlegt,
die dann woanders dann wieder zurückholt und darüber dann dir referenziert.
Ich habe im Prinzip lassen sich dann also mit diesen entsprechend einfachen Sachen
dann eben auch sämtliche Unterprogramme auf Hof realisieren. Gut,
abschließenden Teil, und damit sind wir dann also auch mit der Assembler-Programmierung oder diesem Teil das,
was Den ich jetzt also hier kurz motivierend vorstellen will, wäre dann also auch fertig.
Wollte ich noch mal ein bisschen was zu den Datentypen vorstellen. Nicht,
wenn man sich jetzt also irgendwie Datentypen in C oder Hochsprachen,
also in diesem Fall ist jetzt also immer hier als Beispiel C genommen,
ansieht, nicht, dann kann man jetzt erst mal sagen,
so der einfachen Datentypen,
also sprich jetzt irgendwelche ganz zahligen Datentypen, also sprich Indiger,
kann man natürlich also in den normalen 32 Pit-Registern speichern,
beziehungsweise bei aktuellen Architekturen,
wo man jetzt also ein 64-Bit-Rechner hätte,
sind da natürlich dann also
auch die Register
entsprechend breit
Nämlich, und je nachdem, mit welchen Anweisungen gerechnet wird, gibt es dann eben die Unterscheidung,
ob die zur Runde liegen oder ob der zugrunde liegende Bits dringt, dann eben entweder als Science oder als Unscience-Zahl interpretiert wird. Während die Standarddatentypen wären dann eben sowas wie Charakter,
Short in, optional dann immer noch entsprechend ansigned,
also jetzt in der Hochsprache, nicht zugehörig,
dann entweder eins, zwei oder vier Byte,
und je nachdem, mit welcher Art von Assembler man jetzt arbeitet,
nicht wäre das dann also quasi beim NTL-Sammler dann im Byteward,
Double Word, beziehungsweise bei dem Glue-Accembler, dann nimmt man das,
was in den Befehlen dann hinten quasi über dieses BW oder L kodiert ist,
nicht wo, dann, nicht,
also bislang hatte ich immer alles jetzt nur mit normalen Entagers gezeigt,
auch in den Beispielen wurde nichts anderes vorkommen, bin ich froh, dann immer im Prinzip sowas wie ein MoveL passiert, was dann eben genau ein Vierbyte-Enager letztendlich behandelt.
Gut, bei Gleitkomma gibt es dann eben entsprechendes mit Boardlängen und entsprechend lang Typen.
Wenn man jetzt nicht nur einfache Datentypen hat,
sondern jetzt irgendwelche zusammengesetzten Datentypen als Allererstes, erstmal sowas wie ein Aree.
Wie wird das gespeichert oder abgelegt? Also in der Hochsprache
würde ich jetzt ja mal sagen,
ich habe ein Aree von einem bestimmten Basistyp und von
diesem Aree habe ich jetzt eben eine Anzahl von N-Elementen
Welchen? Das, was die Compiler daraus machen, ist,
dass also entsprechend diesem Array dann eben ein fortlaufender Speicherbereich von,
also in Season Darkson, also N-Mal,
Size of Tea byte quasi generiert, da aluziiert wird.
Und dann eben ein entsprechender Zeiger quasi auf das erste Element
dieses Typs letztendlich verwahr ist. Und je nachdem,
von welchem Basistyp das ist, wer das Ganze dann eben spricht,
war ich ja mäßig entsprechend groß.
Das heißt, wenn ich also ein RAE von zwölf Charakteren habe,
dann wären das eben hier quasi zwölf Byte.
Wenn ich eine Rehe von fünf Intager-Werten habe,
jeweils 32 Bitbreit, wären das dann eben jetzt insgesamt 20 Byte.
Das heißt, die Basisadressen würden dann eben immer entsprechend diesem
Byte-of-Set quasi verändert werden müssten.
Gut, bei Double entsprechend das Gleiche bei Zeigern,
in diesem Fall jetzt hier auch bei einer 32-Bit-Maschine
Weit lang. Gut, das war's, die Compiler dann eben machen,
wenn ich jetzt in meiner Hochsprache irgendwelche Race beschreibe und irgendwelche Race-Zugriffe,
ist das dann natürlich das,
was letztendlich hier an irgendwelchen White-of-Sets zu dieser Basisadresse gelten wird,
dass das dann eben vom Compiler schon passend berechnet werden muss.
Das heißt, wenn ich jetzt also hier so ein Arey hätte,
mit Entsprechenden Entager-Werten und meinetwegen jetzt das vierte Element,
also sprich hier in diesem Feuer der WR3. Nicht dann sieht man hier jetzt
immer bei diesen Referenzierungen auf welche Speicheradressen beziehungsweise wie dann,
also die Basisiadressen, sich da dann eben entsprechend berechnen würden.
Was dann auch immer noch passiert, beziehungsweise was die meisten
von Ihnen ja auch schon aus der Programmierung kennen.
Nicht häufig, in der Regel ist es da dann ja auch so, werden dann also irgendwelche Drehgrenzen nicht abgefragt. Das heißt,
wenn ich also jetzt hier irgendwo von der Deklaration her mal also irgendeine reale bestimmten Größe detailliert habe,
ich aber dann also in meinem Code irgendwo drauf zugreife,
ich war, wie gesagt,
Initials gibt es hier eben von 0 bis 4 und Value von 5 wäre dann eben eigentlich irgendwo hier dann schon ein Speicherbereich,
der dann also nicht mehr zu dem RE gehört,
dass da dann letztendlich, also nicht klar ist, was dann rauskommt,
beziehungsweise dass dann eben auch häufig dann eben zu irgendwelchen Speicherschutzverletzung führt,
war dann also Betriebssystemseitig quasi geguckt wird,
darf der Code also auf dieser Adresse da jetzt überhaupt lesen,
beziehungsweise irgendwas machen.
Gut, hier nochmal, Beispiele jetzt für einfache Rätselgriffe.
Also ich habe in diesem Fall jetzt immer irgendwelche Zahlen,
fünf Stück an der Zahl, dann drei solche RAs,
die jetzt hier mit irgendwelchen konstanten Werten initialisiert werden sein sollen.
Und die beginnen eben irgendwo bei der Speicheradresse 16. Und wenn
man das jetzt also so in seinem C-Code letztendlich deklarieren würde,
würde das Ganze dann eben im Speicher quasi so angeordnet werden,
dass quasi für das erste Räder das Ganze eben von 16 bis 35 von Adressierung her letztendlich geht und das Nächste Rehe,
was dann irgendwo untergebracht wird dann,
also quasi bei der nachfolgenden Speicheradresse dann beginnt und so weiter und so fort.
Gut, das ist eigentlich alles relativ straightforward, da ist also nichts Neues mit dabei.
Gut, wenn man jetzt Reh-Zugriff macht,
Würde man dann eben in der Regel das immer über diesen Adressierungsmodus im Assembler-Code letztendlich machen,
wo ich eben immer mit Basisadresse beziehungsweise mit Index, dann quasi auf die zugehörige Adresse verweisen kann.
Das heißt, wenn ich jetzt also eine Funktion habe,
die jetzt also beispielsweise von einem Array quasi ihm dieses Element auslesen soll,
würde das, wenn, also sie haben jetzt die zugehörigen Werte,
also in den Register EDX bzw. EAX stehen,
das eben durch diesen Aufruf des entsprechenden Assembler-Codes passieren,
wo dann eben genau dieser Offset-Adressierungsmechanismus genutzt wird, wo ich dann eben sage,
EDX ist die Basisadresse, also das heißt genau der Teil,
wo also das zugehörige Aree vorne beginnt, mit EAX würde ich dann letztendlich den Index beschreiben.
Das heißt also, welches Element von dem Erreg haben möchte und mit diesem Skalierungsfaktor,
sagt man eben quasi abhängig vom Datentyp,
wie groß also ein einzelnes Element ist, also eins,
zwei, vier oder acht breit.
Dadurch erklärt sich dann eben auch letztendlich dieses komische Adressierungsformat des X-86 Assemblers.
Gut, wie gesagt, in der Regel hat man da dann natürlich eben keine Bereichsprüfung. Das heißt,
wenn ich also jetzt hier irgendeine Adresse habe,
die außerhalb des AAs ist, kann das dann eben dazu führen,
wenn man in seinem eigenen Speicherbereich ist, dass man da also irgendwelchen Schrott liest,
wenn man dann den verlässt, sollte das dann eben hoffentlich das Betriebssystem anfangen.
Gut, hier nochmal, kurzer Beispiel, Kota nur,
wie eben so ein Rehzugriff dann eben auch mit so einer Schleife letztendlich stattfinden könnte,
wo dann eben im Prinzip jetzt für den GCC quasi jetzt auch,
wie ich das vorhin vorgestellt habe, quasi diese Vorschleife entsprechend umgesetzt wird, jetzt eben hier irgendwie so ein Code mit Du.
Why, beziehungsweise dann entsprechenden passenden Go-To. Und interessant ist jetzt eigentlich nur folgendes,
das, was sich im C-Code letztendlich habe,
also sprich jetzt hier irgendwie so ein Array-Zugriff,
wo ich berechnen möchte, quasi jetzt in dieser Schleife über alle Elemente,
zehnmal ZI und das Ganze dann eben entsprechend aufsummiere, nicht,
dass das dann eben in dem Assembler-Code so umgesetzt wird,
dass dieser Laufindex, der jetzt also quasi jetzt hier bei der Referenzierung drin ist,
im Prinzip rausgenommen würden und da dann eben durch einen entsprechenden Zeiger ersetzt wird,
in dieser Zeiger, wird dann eben einfach nur durch entsprechendes Inkrementieren,
wo ich dann also zum nächsten Array-Index springe,
weitergerechnet und dann immer nur quasi mit dem Ende des Arrays verglichen und Wenn man diesen Code jetzt nimmt,
würde der dann eben entsprechend im Assembler umgesetzt dann zu dem hier führen,
wo dann im Prinzip jetzt hier oben dann eben noch kurz beschrieben ist,
also in welchen Registern welche Werte letztendlich stehen.
Da hatte ich also bis vorhin noch einen Fehler im Skript,
also falls ich das also irgendwie runtergeladen ausgedruckt haben und das
jetzt also deutlich vor heute Mittag passiert ist,
dann steht an dieser Stelle noch EAX,
das ist also natürlich definitiv falsch
Gut, also wenn man sich diesen Code jetzt hier im Einzelnen mal ansieht,
dann würde ich jetzt also auch aus Zeitgründen,
also jetzt nicht noch lang und breit, runterbeten,
würde man dann eben wirklich sehen, wie jetzt,
also quasi über diese Ausdrücke, dann eben der entsprechende Egerei-Zugriff passiert.
Nicht das, was dann hier auch zu sehen ist,
aber dafür hatte ich ja auch schon Beispiele am Mittwoch gebracht,
ist dann eben nochmal so ein Mechanismus,
wo quasi diese Funktionalität zehnmal ZI übersetzt ist in Fünfmal ZI,
in dem ich also quasi ZI hier in diesem Register habe,
dass hier mal vier plus eins,
das heißt in EDX habe ich dann nachher fünfmal ZI gespeichert.
Und hier wird das Ganze dann eben nochmal wieder mit zwei skaliert.
Und dadurch habe ich dann eben dieses zehnmal ZI. Gut,
das wäre dann eben so ein Beispiel für einen entsprechenden Rätselgriff mit Schleifen.
Gut, im folgenden wären dann nochmal ganz kurz
auch über ähnliche Mechanismen, aber auch nur entsprechend oberflächlich nochmal
Rekords vorgestellt
Wird dann aber nächste Woche passieren. Und damit wäre dann
eigentlich eben auch entsprechend dieser Tal zur Assembler-Programmierung soweit fertig.
Und dann kommen wir entsprechend wieder zu den etwas praktischeren Beispielen zur Rechnerarchitektur.
Wir rechnen jetzt wirklich auf oberster Ebene arbeiten. Heute sind
ja dann die letzten Übungsaufgaben rausgekommen. Da geht es dann
eben auch nochmal so ein bisschen um diesen Tal Assembler-Programmierung,
ist aber eben vergleichsweise sehr einfach gehalten. Das heißt,
da muss man jetzt also irgendwie nicht der Crack sein,
der da jetzt also irgendwie Betriebssysteme auf unterster Ebene schreibt,
sondern mit dem, was also an Beispielen hier in der Vorlesung gebracht wurde,
lassen sich da die Übungsaufgaben entsprechend machen.
Ist dann eben, wie gesagt, der letzte Zettel,
nächsten Freitag dann die Abgabe und am Montag danach
wäre dann eben nochmal eine entsprechende Besprechung,
beziehungsweise dann gibt es eben auch nochmal zu den Aufgaben 9 bis 12 die kompletten vollbeschriebenen Musterlösungen.
Gut, damit dann Schluss für heute,
ein schönes Wochenende