So, ja, also heute sollte, oder wollte ich eigentlich jetzt zu sehen,
dass durch dieses Kapitel zur Assembler-Programmierung beenden, also sprich,
ich werde jetzt also auch nochmal relativ schnell über die verbleibenden Folie oder die verlorenen Folien entsprechend vorstellen.
Wie gesagt, Verständnis oder das, was gelernt werden soll,
sind im Prinzip Grundprinzipien, also sprich grob,
dass man Idee hat, wie Assembler-Code aussieht,
welche Register ansprechbar sind, wie Parameter-Übergabe bei Prozeduren und Funktionen funktioniert.
Das ist das, was jetzt gleich kommen wird. Und wie Daten bzw. Datenstrukturen im Speicher abgelegt werden,
weil auch das ist etwas, wo man immer sagen kann, naja,
also braucht mich in Java oder in CR eigentlich nicht weiter zu interessieren,
ist aber dann teilweise in Bezug auf die Performance von Programmen dann doch nicht ganz unwichtig,
dass man zumindest eine Idee oder eine Vorstellung davon hat.
Gut, letztes Mal hatte ich ja aufgehört im Prinzip mit der Einführung von Kontrollstrukturen,
also sprich das und IF, übersetzt werden kann,
letztendlich dann einfach in ein Äquivalent war es dann also quasi ein Verzweigung und ein Goto ist,
also hier wäre jetzt also quasi der Assembler-Code,
dass ich dem quasi hier diese Abfrage dann durch einen Compare-Befehl mache,
der setzt dann das entsprechende Fleck,
anschließend wird dann abhängig von diesem Fleck,
entweder zu diesem Label gesprungen oder eben entsprechend dieser Code ausgeführt,
also das ist dann eigentlich soweit selbsterklärend.
Wie bei vielen Sachen, das war jetzt das,
womit ich also aufgehört hatte,
ist das Ganze dann immer so ein bisschen davon abhängig,
wie der Compiler arbeitet, welche Flex dort eingestellt werden,
das heißt also diese Art von Code kommt eben bei einer bestimmten Einstellung jetzt beim GCC beispielsweise genauso raus.
Mit anderen Einstellungen wird das dann quasi in etwas anders gehandhabt,
hat ja auch schon gesagt.
Dann ist das eine Art bedingte Überschreibung des Rückgabewerts.
Und letztendlich werden aber vorher schon mal beide Ausdrücke ausgerechnet.
Dann kann man sich immer darüber streiten, was wann eigentlich
sinnvoller ist. Meine Erfahrung ist eigentlich auch immer,
wenn ich jetzt ein System bis zum letzten ausreizen möchte,
High-Performance-Computing, dann kann es interessant sein,
sich mal so ein bisschen um diese Compiler-Flex zu kümmern.
Wenn ich Code schreibe, der jetzt für andere Systeme ist,
also quasi im Cross-Compility-Compilation mache, also sprich,
ich kann beispielsweise jetzt hier auf meinem Rechner,
beziehungsweise habe es sogar drauf, also irgendwie den GCC benutzen,
um Code für einen Aduino zu erstellen, was das im Prinzip ein ganz anderer Prozessorarchitektur ist und so weiter.
Nicht dann ist es auch interessant oder teilweise eben notwendig,
also beliebig viel von diesen Dingern einzustellen,
aber ansonsten ist die Erfahrung,
dass eigentlich die Voreinstellung in der Regel schon ganz gut funktionieren
und dass man sich da dann also erstmal noch nicht
so viele Gedanken drum machen sollte
Gut, ja, weitere Constrollstrukturen wären jetzt natürlich sowas wie irgendwelche Schleifen,
also sowas wie eine Schleife,
wo jetzt also quasi am Ende des Schleifenkörpers,
also ein Du-Wile-NC, abgefragt wird,
passiert im Assembler-Code eben so, dass quasi der C-Code,
der daraus oder der dann eigentlich nachher übersetzt wird,
muss man sich dann eben eigentlich vorstellen,
wie sowas wie so ein Botoo, das heißt,
es gibt dann eben künstliches Label hier zu Beginn der Schleife,
dann wird eben quasi einmal das Ganze ausgeführt am Ende dann eben die Abfrage gemacht.
Und wenn die Abbruchbedingungen eben noch nicht erreicht ist,
springe ich hier entsprechend wieder zurück. Und die Schleife iteriert dann
eben entsprechend durch. Und gut, das ist dann etwas,
was wir dann also ja vorher quasi schon mit dem IF gehabt haben,
lässt sich dann eben entsprechend in den Assembler-Code dann übersetzen.
Nicht, also hier im Folgenden ist dann immer irgendwie so ein Beispiel,
also ein C-Code, also für ein Programm namens P-Count,
hat ein Argument, ein Unsilent Long und liefert irgendein Resalt zurück.
Dazu vielleicht mal Quizfrage an diejenigen,
die sich also irgendwie mit C auskennen,
beziehungsweise sich dazu befähigt fühlen, also an den Code zu interpretieren,
weiß jemand,
was diese Funktion macht.
Bitte? Also
Genau. Also die Antwort war, also erzählt die Einzelnen in einem Argument.
Also im Prinzip ist das also sowas wie so ein Bitcoin,
was man ja für Paritäten oder für jetzt,
na gut, Parität würde man einen X-Raum machen,
aber eben teilweise dann also für irgendwie Kodierung,
Hemmingabstand und so weiter braucht.
Also nur mal kurz zur allgemeinen Erklärung,
was also im Prinzip passiert ist. Ergebnis wird mit null
initialisiert. Anschließend nehme ich also hier quasi mein Argument,
maskiere das mit einem Hexwert von 1. Was passiert dabei?
Ich habe hier meine Zahl. Also das Argument X. Irgendwie
64 Bit lang. Wird dann quasi Bittweise verwundet mit einem Vektor,
der genau so aussieht. Das heißt, das,
was sich auf der rechten Seite von diesem Flussgleichoperator habe,
ist im Prinzip, wenn hier eine 1 steht,
der Wert 1, wenn da keiner 1 steht,
der Wert null.
Das wird zu dem Ergebnis hinzuaddiert
Anschließend wird X um einen nach rechts geschoben.
Das heißt, das letzte Bit habe ich abgearbeitet.
Das ist in diesem Fall dann ein logisches Schieben und kein arrithmetisches Schieben.
Das heißt, vorne kommt eine Null rein und als Erbbruchbedingung
wird dann eben gefragt, ist X X ungleich null.
Das ist nämlich hier dieses Wild X, wenn es leich null wäre,
dann brauche ich auch nicht weiter zu zählen.
Dann bin ich dann entsprechend fertig und insgesamt habe ich dann,
also je nachdem, von, ja,
sag ich mal, 1 bis 64 durchläufen,
hier dann also irgendwo dann mein Ergebnis zusammengezählt und hätte da
dann also meine Rückgabe gewöhnt
Gut, also wie gesagt, das nur als Hintergrund zu dem C-Programm. Ja,
also wie gesagt, und umgesetzt wird das Ganze dann eben quasi,
dass dann hier also diese Einsprung ist,
hier dann eben die entsprechende Funktion hier dann mit dem Rücksprung und in dem Assembler-Code wäre das dann eben das,
was dann letztendlich da unten steht und das war es bei allen diesen Sachen dann immer der Fall ist,
was ich auch gleich noch mal kurz,
wenn es also um die Prozeduraufrufe geht, sagt,
ist, dass man immer sagen kann,
das erste Argument, was hier quasi übergeben wird,
wird, in dem Register RDI übergeben.
In diesem Fall gibt es jetzt hier nur das eine Argument und
das Register RAX ist dann immer typischerweise der Rückgabe wert.
Also das nur zur Erklärung, warum da wie, wo,
welche Register stehen, müssen sie aber jetzt nicht zwingend wissen,
welches Register wofür genommen wird, nicht nur zu erklären. Gut,
wenn ich jetzt klassische Wahlschleife habe, andersrum,
also sprich die Abfrage zu Beginn der Iteration,
dann wird das letztendlich in diesem Fall auch wieder compilerabhängig entweder so umgesetzt,
dass ich dass im Prinzip mache,
wie die Du-Wile-Schleife,
ich aber im Prinzip am Anfang hier gleich mit einem zweiten Label zu dieser Abfrage am Ende der Schleife springe und dann eben dadurch erreiche,
dass ich quasi direkt vor der ersten Iteration hier schon eine Abfrage machen kann.
Auch das wird dann einfach state for ward,
also hier auch nochmal C-Code bzw. ein Zeko
dann mit Sprüngen entsprechend
Umgesetzt und liefert dann eben den entsprechenden Assembler-Code. Eine andere Variante,
wie gesagt,
das Ganze ist immer davon abhängig,
welche Compiler-Optionen man wählt,
wäre das im Prinzip dieser C-Code so umgesetzt wird,
dass sich also am Anfang also auf die negierte Abbruchbedingung abfrage
und dann eben entweder zum Ende springe oder dann eben wieder
so eine Duo- und Wahlschleife habe und dadurch dann also
quasi das Ganze umsetze
Nicht auch das wird dann also hier nochmal im Beispiel C-Code umgebauter C
gut gemacht. So bei Vorvorschleifen sieht man hier schon,
wird das Prinzip dann einfach nur noch im Herrn entsprechend weiter ausgeweitet.
Also Grundidee ist eigentlich immer auf Assembler-Ebene habe ich nur Sprungbefehle und irgendwelche Sprungmarken.
Das heißt, es wird immer ein Code mit irgendwelchen Go-Toos.
Und das, was ich eben in Programmiersprachen habe,
also vor Why Duvall, wird quasi mehrstufig entsprechend umgesetzt,
bis ich bei sowas lande. Nicht bei einer Vorschleife passiert,
also folgendes, dass ich dann eben sage, okay,
ich mache eben Ziehe die Initialisierung raus, habe da eine ganz normale Wahlschleife.
Nicht, die wird dann in sowas hier umgesetzt und das
kann ich dann wieder in so einen Gut-Code umsetzen und dass wir dann also quasi mehr stufig letztendlich das Ganze.
Gut, wie gesagt, das ist jetzt interessant für Compilerbauer.
Hier eben jetzt nur der Vollständigkeithaber nochmal so kurz beschrieben.
Hier das Ganze dann also nochmal mit einem Beispiel,
da kann man dann jetzt auch noch ein bisschen optimieren,
dass ich dann eben sagen kann, also hier bestimmte Abfragen,
also diese erste kann ich im Prinzip rausnehmen,
wenn ich weiß,
dass die Bedingung nach der Initialisierung sowieso immer schon wahr
ist und hätte da dann entsprechend meinen Code
Gut, dann gibt es ja noch Mehrfachverzweigung. Also das,
was so Switch-Anweisungen sind. Wie wird das vom Compiler umgesetzt?
Die eine Methode ist, dass sich das einfach als quasi
geschachtete Ifs schreibe und das dann eben straightforward entsprechend umsetze.
Das funktioniert natürlich immer nur ganz gut,
wenn ich relativ wenige Alternativen oder Verzweigungen habe.
Wenn ich sehr viele Fälle habe,
habe ich dann natürlich unnötig viele Abfragen bzw. Vergleiche.
Demzufolge wird das dann also eigentlich immer sehr langsamer Code
Eine andere Variante ist etwas, was jetzt also noch kurz
vorgestellt ist, wo dann eine sogenannte Sprungtabelle benutzt wird.
Da geht es darum, dass also im Prinzip,
wie also hier auch in diesen Beispielern, dass also auch der Fall ist,
letztendlich die Verzweigungsbedingungen in irgendwie sowas wie numerischen Wert umgesetzt wird.
Und mit diesem Wert adressiere ich im Prinzip sowas wie
ein RA und in dem RA habe ich dann quasi,
weil es die Einsprungstellen oder Einsprungbedingungen den Code,
der quasi dann zu den entsprechenden Teilfällen letztendlich führt.
Dadurch kann man dann eben sagen, ich brauche an der Stelle
dann eben wirklich nur einen einzigen Sprung und nicht lauter abfragen, lauter Verzweigung und so weiter.
Gut, auch das etwas, dass wir die Anmerkung unten,
was in der Regel der Compiler entsprechend automatisch macht. Also hier ist nochmal dargestellt,
wie man sich das also im Prinzip mit so einer Sprungtabelle vorstellen muss,
also dass wir jetzt sehen, okay,
in zehn meine Switch-Anweisungen, nicht in die Sprungtabelle,
ist dann eben im Prinzip sowas wie ein Array,
wo ich dann einfach nur sagen muss,
ich habe jetzt hier für den Fall null,
für den Fall eins,
für den Fall zwei
Entsprechend die Adresse und unter der Adresse steht dann eben der
entsprechende Code, der dann hier jetzt jeweils auszuführen ist.
Und diese Mehrfachverzweigung ist dann im Prinzip nichts anderes als ein unbedingter Sprung,
wobei dann quasi das Sprungziel, nämlich diese Adresse,
dann über so ein Array-Zugriff mit diesem Index,
der dann also quasi also aus der Bedingung heraus erfolgt,
sich vergibt, ergibt.
Und das hat natürlich den Vorteil, dass sich im Prinzip
so eine Mehrfachverzweigung mit konstantem Zeitbedarf beziehungsweise Zeitaufwand abfrühstücken kann.
Quasi diese Onotation ist bekannt aus irgendwie anderen Veranstaltungen oder,
also wenn ich hier sowas habe wie Verzweigung in U von
1 Operationen
Ja. Ja, gut, also einige haben so ein bisschen Kopf geschüttelt.
Hier war schon eine richtige Antwort. Also da geht es im Prinzip darum,
dass man also bei Algorithmen die Komplexität der Algorithmen in Bezug
auf die Größe der Aufgabenstellung letztendlich beschreibt.
Nicht. Und sowas wie O von 1 heißt eben im Prinzip,
dass wenn ich eine Zweifachverzweigung habe und eine Vierfachverzweigung, dann ist beides gleich aufwendig.
Wenn ich O von N hätte, dann wäre eine Vierfachverzweigung
doppelt so aufwendig wie eine Zweifachverzweigung. Wenn ich entsprechend kompliziertere Algorithmen habe,
dann ist das dann eben nicht mehr konstant oder linear wachsen,
sondern dann habe ich also Wachstum, die also entweder polinomeal sind,
also sprich x oder andersrum K hoch irgendwas.
Nicht also quadratisch oder, also zum Beispiel sowas wie ein Array irgendwie durchlaufen,
wo dann also quasi der Aufwand, wenn ich das wahrdimensional habe,
dann entsprechend quadratisch ansteigen würde, also eine Ray von Bilddaten,
bei vielen anderen Algorithmen hat man eben leider das Problem,
dass es nicht mal mehr polynomeal, sondern exponentiell ansteigend ist.
Das ist eben genau der Grund, weswegen ich dann in der Theorie,
wird das dann noch entsprechend rankommen,
sogenannte NP vollständige Probleme habe, wo man sagen kann,
ich habe zwar einen Algorithmus, damit kann ich das berechnen,
aber der Aufwand steigt so stark,
dass das praktisch einfach irgendwie zumindest für sehr große Werte gar nicht sinnvoll möglich ist,
also Beispiel vorausberechnen von oder komplett vorausberechnen von irgendwie allen Zügen
in einem Schachspiel oder sowas
Da kann man sagen, okay, für zwei, drei,
vier Züge kann man das also noch immer ganz gut machen.
Irgendwann explodiert dann einfach quasi da dann der Suchraum. Gut, und wie gesagt, also hier ist dann also Vorteil letztendlich, das kostet mich also keinen größeren Aufwand, sondern skaliert konstant. Ja,
hier dann also quasi der Beispiel, oder das Beispiel,
wie das Ganze, also im Prinzip im Code aussehen würde,
also hier dann eben C-Code,
im Assembler-Code habe ich dann also einfach als konstant so eine Sprungtabelle,
die wird dann eben letztendlich vom Assembler her angelegt,
wo ich dann also hier und am Label drin stehen habe,
die Sprungziele für die unterschiedlichen Einsprungpunkte und dadurch,
dass dann also letztendlich die Bedingungen durch diese etwas komplizierte Adressierung,
die ja so im X86 da möglich ist, dann berechnet wird,
sieht das vorgendermaßen aus, dass man sagen kann,
ich habe eben mein Argument X, das ist RDI,
das wird hier mit dem Faktor 8 skaliert.
Damit erreiche ich, wenn RDI gleich Null ist,
habe ich hier quasi den nullten Eintrag,
wenn RDI gleich 1 ist, habe ich den ersten Eintrag,
wenn RDI gleich 2 ist, habe ich den zweiten Eintrag,
der Faktor 8 ergibt sich jetzt wieder beim 64-Bit-System, dadurch,
dass quasi jeder Eintrag hier 64-Bit bzw. 8-Bit groß ist,
nicht und demzufolge habe ich hier dann quasi immer die Adressen an dieser Stelle,
an dieser Stelle, an dieser Stelle,
die ich dann also letztendlich auslesen würde.
Und zu dem Ganzen wird als Offset dann eben noch 11.4 hinzuordiert und
das ist dann eben quasi diese Startadresse von dem Ganzen hier. Gut,
das Ganze macht Compiler, letztendlich, oder der Assembler,
wenn er das also quasi umsetzt oder der Kumpalern vielmehr,
also auch schon statisch,
muss man sich dann eben nicht drum kümmern und funktioniert dann eben entsprechend effizient gut nicht.
Also hier sieht man nochmal,
wie dann also letztendlich diese effektive Adresse berechnet wird,
RDI mal 8 plus diesem L14,
und damit hat man dann eben direkt die Einsprungstellen für den
jeweiligen Code,
der dann also zu
so einem Case gehört
Gut, dann kommen wir jetzt zu dem Teil unter Programme,
weil, gut, das Anlass ist,
wir sagen eigentlich ein bisschen allgemeines Verständnis,
weil das sind also noch ein paar Sachen,
die teilweise dann also auch in Bezug auf Speicherverwaltung und dann eben auch auf das übernächste Kapitel mit dem Betriebssystem dann noch ein bisschen wichtig sind,
beziehungsweise das auch etwas ist, worüber man sich,
sagen wir mal so,
selber bei seiner Programmierung vielleicht also auch ein bisschen noch Gedanken machen sollte.
Ja, was passiert im Prinzip beim Unterprogramm? Also ich habe
jetzt hier mal als Beispiel, also irgendwie eine Funktion P,
die ruft dann irgendwo eine Funktion Kuh auf.
Zum einen habe ich eine Kontrollübergabe, das heißt,
ich beim Aufruf muss dieser Code hier quasi angesprungen werden,
ausgeführt werden und wenn da fertig ausgeführt ist und ich also hier diese Return-Anweisung habe,
muss ich also quasi im Code-Ablauf wieder hier entsprechend hin zurückspringen,
um dann also quasi an dieser Stelle in meinem aufrufenden Programm weiterzuarbeiten.
Auf der anderen Seite habe ich natürlich irgendwelche Daten, die zu übergeben sind, also die Argumente,
beziehungsweise in diesem Fall das eine Argument und eben irgendwelche Rückgabewerte,
beziehungsweise in diesem Fall, also jetzt hier diese eine Rückgabewert.
Und was passieren muss, ist,
dass das Ganze dann eben auch noch bestimmte Eigenschaften,
sagen wir jetzt mit der Speicherverwaltung macht,
weil wenn ich also in meinem aufgerufenen Programm jetzt irgendwelche lokal deklarierten Objekte habe,
also in diesem Fall jetzt,
also irgendwie sowas wie so ein Array oder eben irgendwie diese konstante Tee,
dass das quasi zum, beim Aufruf entsprechender Speicherplatz hierfür bereitgestellt wird,
wenn ich das Programm verlasse,
soll das Ganze dann eben auch entsprechend wieder freigegeben werden.
Also sprich das, was man dann eben zum Beispiel in
anderen Programmiersprachen oder woanders dann eben auch richtig als Speicherbereinigung hat.
Hier jetzt aber eben auch für irgendwelche konstant bzw. Variablen,
wo man dann eben sagt, okay,
die Lebensdauer dieser Variablen ist jetzt eben letztendlich zur Laufzeit von diesem Unterprogramm anschließend,
da das jetzt ja keine globale Variable ist,
kann das eben entsprechend freigegeben werden
Gut, dazu dient jetzt eben genau dieser Stack oder Kellerspeicher.
Letztendlich ist das, also erstmal nichts anderes,
als nur irgendwie eine ganz normale Region im Hauptspeicher, also sprich beidweise adressierbarer Speicher.
Wo der liegt, wird im Prinzip vom Betriebssystem zur Laufzeit,
beziehungsweise wenn das Programm in den Hauptspeicher geladen wird,
um die Ausführung vorzubereiten oder zu machen, irgendwo vorgegeben.
Das heißt, es gibt dann also irgendwo so eine Startadresse,
wo dann also quasi der Teil zu meinem Programm irgendwo rumliegt und
In dem Bereich gibt es dann eben unter anderem auch diesen Stack.
Stack ist Kellerspeicher, haben wir also schon bei den N-Adressmaschinen oder Null-Adressmaschinen quasi.
Kurz kennengelernt, als es irgendwie so um diese Push-and-Porp-Operationen ging.
Hier ist das Ganze jetzt bei der Adressierung beim X-86-Code immer quasi so,
dass man sagen kann,
die Startadresse ist irgendwie eine relativ hohe Speicheradresse und das Deck wird,
wenn er größer wird, dann quasi und wächst dann in Richtung niedrigerer Speicheradressen.
Das ist einfach erstmal nur Konvention, willkürlich so vorgegeben,
gibt es also jetzt keinen logischen Grund,
warum das also jetzt irgendwie so und so ist.
Ist das einzige, was eben letztendlich immer klar sein muss,
ist, dass das also innerhalb eines Betriebssystems dann für alle Programme,
die dann dort laufen sollen, dann eben letztendlich dann einheitlich vorgegeben wird.
So, quasi das oberste Element des Stacks ist das einzige,
was eigentlich So rum gesehen jetzt immer nur zugreifbar ist.
Und dafür gibt es da jetzt eben dieses Register RSP.
Das war eins von diesen 16 Universalregistern. Das steht dann
eben als Stackpointer, da das also quasi in dem Register,
die Adresse steht des Ober-Des Top-Elements, von dem Steak.
Den brauche ich jetzt eben um meine Funktionen und Prozeduren zu verwalten,
wo ich da nämlich für die Parameter irgendwie Speicherplatz gegebenenfalls reservieren kann,
wo ich für lokale Variablen Speicherplatz reservieren kann,
wo Wenn ich jetzt funktionswerte habe,
die dann teilweise auch darüber zurückgegeben werden können.
Und wichtig, das Ganze funktioniert dann eben auch für rekursive Funktionen. Weil was kann,
also wenn ich jetzt das jetzt nicht irgendwie mit sowas wie irgendwelchen Stacks machen würde,
sondern ich sage, ich nehme jetzt für ein Unterprogrammaufruf jetzt einfach nur einen bestimmten Speicherbereich.
Da kommen jetzt also irgendwie die Argumente rein,
da können irgendwelche lokalen Variablen leben und irgendwann am Ende,
die Funktion quasi auch für Return läuft,
dann wird dieser Speicherbereich freigegeben und das war's.
Das würde nicht funktionieren, aus welchem Grund,
wenn die Funktion trekusiv ist, das heißt, sich selber aufruft,
dann würde sie sich quasi bei dieser Rekursion diesen Speicherbereich natürlich auch selber wieder irgendwie überschreiben.
Mit dem Stack passiert das eben nicht,
sondern dadurch wird dann dafür gesorgt, dass quasi dynamisch für jede Instanz,
für jeden Aufruf einer Funktion oder einer Prozedur dann eben getrennter Speicher verwaltet wird.
Das ist erstmal so grob als Idee, weswegen man das jetzt,
also über einen Stack macht und nicht sagt, okay,
der Compiler baut jetzt für jede Funktion P,
die ich jetzt irgendwo benutze, reserviert da einen bestimmten Speicherbereich,
da kann dann irgendwas mit passieren und hinterher wird das Ding dann eben weggeworfen, würde ihm bei Rekursion nicht mehr funktionieren. Gut,
wie das gehandhabt wird, da gibt es dann eben unterschiedliche Varianten oder Konventionen,
ist eben Betriebssystem abhängig, in der Regel kann man aber eben sagen,
dass zumindest ist es beim X-86-64-Assembler erst mal versucht wird,
die Parameter in den vorhandenen Registern zu übergeben,
wenn ich in einer Funktion oder einer Prozedur jetzt mehr Parameter habe,
als jetzt im Prinzip ich mit meinen 16 bzw. ein Stackpoint,
da muss ich dann schon rausnehmen mit meinen 15 Registern,
verwalten kann, dann wird über ein Stack dann eben auch,
werden auch die Parameter übergeben, dann brauche ich noch Mechanismen,
die jetzt sagen, was passiert mit den Inhalten der Register,
weil Das ist das,
was mit diesem Caller on Corley safe gemeint ist.
Werde ich also gleich noch mal ein Beispiel vorstellen,
weil da geht es letztendlich darum, dass man sagt,
okay, ich habe in dem Prozessor 16 beziehungsweise Stackbound abgezogen, 15 Register.
Die werden jetzt von irgendeiner Funktion, nicht nehmen wir dieses Beispiel,
hier von P für irgendwelche Variablen benutzt, beispielsweise für das X.
Anschließend wird eine Funktion Kuh aufgerufen, die arbeitet hier irgendwie,
Wird beendet und das Ganze läuft dann hier weiter. So,
und was da natürlich gegeben sein muss, ist,
dass dieses X, was vorher mal in irgendeinem Register von P gespeichert wurde,
nach Aufruf von Kuh, nicht durch irgendwas hier drin, überschrieben worden ist.
Das heißt, es muss irgendwo sichergestellt werden, dass Register,
die ich also hier oder dies ist jetzt C-Code,
aber die im Prinzip ist im Assembler-Code ja dann angesprochen werden,
dass die Vor- und Nachfunktionsaufruf, die gleichen Inhalte haben,
sprich, dass diese Funktionen jetzt nicht irgendwelche Registerinhalte,
weil die selber damit rechnet,
überschreibt und dann hierfür kaputt macht.
Demzufolge müssen dann diese Registerinhalte teilweise eben auch noch gesichert werden.
Und das passiert eben in der Regel dann auch über einen Stack.
Das ist dann das, was mit diesem Caller und Cordy
safe gemeint ist. Werde ich gleich nochmal am Beispiel,
beziehungsweise X86 Assembler unter Linux dann, also quasi kurz vorstellen.
Wie gesagt, das Ganze funktioniert,
solange sich letztendlich alle Funktionen oder Assemblercode
aller Funktionen
da dran hält
Und wie gesagt, das ist dann quasi immer als Betriebssystem konstant oder in Varianten irgendwo vorgegeben.
Und dann hat man damit dann auch kein Problem.
Gut, wie arbeitet man jetzt mit einem Stack?
Zum einen gibt es natürlich diese Funktion Push und Pop.
Haben wir also schon, sagen wir jetzt als Minimalbefehle für irgendwas kennengelernt. Nicht,
wo man sagt,
also wenn ich jetzt hier irgendwie so einen Push von Source mache,
soll, also im Prinzip Vorranges passieren,
dass also ein Wert wird von diesem von dieser Quelle geholt und als oberstes Element auf den Stack geschrieben.
Das heißt, in dem Fall wird der Stackpoint da,
da also quasi der Stack immer nach unten wächst,
beim X686er-Prozessor, der Stackpointer um 8 dekrementiert,
zeigt dann quasi auf ein neues neues Speicherwort und unter diesem Speicherwort
wird dann quasi dieser Inhalt reingeschrieben und dann eben dort gespeichert.
Das ist eben das,
was also
eine Push-Operation macht
Ist dann eben genau das Gegenteil. Der Stackpoint da
zeigt irgendwo auf das oberste Element vom Stack.
Und so ein Pop nach Destination würde dann eben folgendes machen.
Dieses hier wird gelesen. Der Stackpoint da wird um acht inkrementiert. Das heißt,
er zeigt dann, also hier auf das neuoberste Element von dem Stack,
dieses hier steht zwar noch der alte Wert drin,
der ist dann aber funktional gesehen vergessen.
Und das, was also hier vorgelesen wurde,
wird dann eben die es in diesem Fall wäre,
dass deine Mann Register dann entsprechend reingeschrieben.
Echt, das sind diese beiden Operationen,
mit denen dann im Prinzip der Stack bearbeitet wird.
So, im Prinzip lassen sich also damit auch Funktions- und Prozeduraufrufe implementieren.
Beim X686 gibt es dafür jetzt aber eigentlich noch Extrabefehle,
nämlich Call und Return. Die machen dann also sowohl Handhandhalbung von Stackpointer,
die bearbeiten dann aber auch gleich das Instruktionsregister, weil was muss jetzt in Bezug auf
Wenn ich jetzt diese Grafik hier nehme,
nicht was muss jetzt in Bezug auf die Kontrollübergabe passieren in einem Instruktionsregister,
habe ich ja die Adresse der Anweisungen, die gerade ausgeführt wird.
Wenn ich jetzt also quasi in einem Assembler-Code da bin,
dass also quasi die Scroll von Kuh gemacht wird, muss dafür gesorgt werden,
dass quasi der Befehlszähler dann mit der Startatresse hiervon geladen wird.
Nicht dann läuft in entsprechend dieser Code ab und beim Return
wird dann eben dafür gesorgt, dass Die nächste Adresse,
die quasi hier nach dem Call kommt,
dann eben entsprechend wieder in den Befehlszähler geladen wird und dann der Co. dann entsprechend dort weiterläuft.
Nicht, das ist also das, was im Prinzip mit Court und Return passiert.
Ja, diese Rücksprungadresse muss natürlich auch irgendwo gespeichert werden,
weil die aufgerufene Prozedur weiß ja nicht,
wo das im aufrufenden Teil im Programmcode weitergeht.
Auch das passiert eben auf dem Stack und passiert im Prinzip
automatisch über diese Funktion Call und Return,
dass das sowohl Rücksprung, Adressen speichert,
Programmzähler passend setzt bzw. dem Programmfehler dann entsprechend diese Rücksprungadresse dann auch wieder zurücklädt.
So, hier ist das Ganze jetzt mal für ein Beispiel.
Also hier links wäre jetzt also quasi entsprechend der C-Code.
Also es wird ja hier jetzt irgendwie so eine Funktion Mull
2 aufgerufen, die dann also hier irgendwo ist.
Das wäre dann also der Code, den also irgendwie
ein Assembler daraus jetzt gemacht hat. Das heißt,
hier unter bestimmten Adressen im Speicher habe ich jetzt also hier irgendwo meinen Assembler-Code.
Interessant ist jetzt also quasi dieses Ball von dem Moll 2.
Nicht, und da steht dann eben als Adresse drin,
die Startadresse von dem Code. Das ist also etwas,
was der Assembler dann ja so schon irgendwo willkürlich festlegen kann,
beziehungsweise dann eben sagt, an welcher Stelle,
welcher Code quasi jetzt hier nach der Übersetzung drinsteht.
Schreibt jetzt wieder in die 5-Zahlen des Samuel-Codes oder mit 2
-2 in die Funktion auf.
Das würde von meiner Paralerweise halten
Das würde ein Compiler normalerweise was? Nein, 2 Millionen,
2, 2. Gut, also kam jetzt die Anmerkung,
also das sieht jetzt hier natürlich insofern erstmal fürchterlich kompliziert aus,
dass man sagt, okay,
ich habe jetzt hierfür diesen einen Aufruf von dem Moll 2 relativ viel Overhead und einiges drumherum,
weil, also so umgesehen ist das Ersorten auch völlig richtig,
das, was für den Funktionsaufruf passiert,
ist eben eigentlich ja nicht nur dieses Call,
sondern das eben auch beispielsweise Parameterübergabe passieren muss und so weiter.
Und das ist eben das, was teilweise hierfür passiert.
Deswegen sieht man also hierfür zwei Zahlen C-Code. Dann also drei oder sechs
Zahlen, Assemblercode. Und da war dann also der einen Wahn,
was dann also völlig richtig ist, dass ein Compiler,
wenn er das entsprechend optimiert, das quasi inline ersetzt.
Das heißt, also da würde quasi diese aufgerufene Funktion,
Funktion Moll 2 sinnigerweise gar nicht mehr existieren,
sondern das, was sich dahinter an Code verbirgt,
wird einfach nur an dieser Stelle quasi hier letztendlich,
ich könnte es auch in zehn machen,
eingefügt und dann hätte ich hier eben entsprechend nur die entsprechenden Anweisungen,
die dazu gehören.
Ja, ist richtig, in der Regel würden die Compiler das also machen,
ist jetzt aber auch wieder davon abhängig,
bei dem Code passiert so ein Aufruf nur einmal,
aus einem Kontext heraus passiert er entsprechend öfter.
Weil wenn er entsprechend öfter passiert, kann man sich dann also drüber streiten,
ab wann es sinnvoller ist, quasi diese Textersetzung,
Code-Ersetzung zu machen oder beim Unterprogrammaufruf zu bleiben. Und die andere Sache ist,
muss man auch mal sagen, okay, das ist hier,
das sind natürlich immer irgendwelche akademischen Trivialbeispiele mit,
also irgendwie relativ wenig Code, wo einfach,
sagen wir mal,
das Verhältnis von Vorbereitung des Calls und Code nach dem Call zu dem eigentlichen Inhalt,
also das ist in diesem Fall der Funktion von Mull 2,
natürlich sehr ungünstig ist.
Nicht,
wenn da drin jetzt also richtig was gerechnet wird,
dann ist der Overheader jetzt natürlich
nicht mehr ganz so groß
Auch das ist eine Sache, ich hatte das schon gesagt,
also man kann beim GCC als Compiler mit Rahmen,
dann also alle möglichen Sachen letztendlich beeinflussen.
In der Regel wird man da jetzt also erstmal nicht groß dran drehen,
sondern liebt mit den voreingestellten Werten. Und da ist es dann eben so,
dass ein normaler auf die Menacompilern eben eigentlich von sich aus übersucht,
ist es sinnvoll, so oder so oder so vorzugehen und das dann in der Regel macht,
eigentlich also auch schon gut beziehungsweise zufriedenstellend mache.
Nicht das heißt also auch da sollte man da dann grob nicht dran selber dran.de. Gut,
ja, also hier nochmal, wie gesagt,
wie sieht das jetzt hier in dem Beispiel aus,
angenommen, ich habe jetzt also hier den Stack,
das sind jetzt also hier irgendwie die Adressen und ich würde dann eben hier mal entsprechend kleiner,
ich habe hier den Stackpointer, das zeigt,
hier ist also auf das oberste Element,
hat demzufolge also hier die Adresse von dem Stack und den Instruction Pointer,
also den Programmzähler, der in diesem Fall jetzt also hier quasi die Speicheradresse hat,
wo dann also dieser Call-Aufruf ist.
So, bei dem Prozeduraufruf mit dem Call Mull 2 würde dann eben im Prinzip Folgendes passieren,
dass der Programmzähler dann eben hier mit dieser Startadresse geladen wird,
dass diesem Aufruf folgende Adresse,
nämlich wo quasi nach dem Return weitergearbeitet wird,
auf den Stack geschrieben wird,
das ist also diese 400549 und dass das Stackzählern logischerweise hier auf den oder Stackpointern eben auf das Notten nächste Top entsprechend zeigt.
Gut anschließend würde dann eben Wo dann also hier quasi an dieser Sprung ausgeführt wird,
dann eben dieser Code ja abgearbeitet. Und wenn man am Ende ist,
also quasi jeder Programm zählt jetzt bei dem entsprechenden Abweisung von dem Return ist,
wird dann zurückgesprungen, was in dem Fall dann eben heißt,
der Programmzähler wird jetzt mit dem vorher auf dem Stack gesicherten und hierüber zugreifbaren Rücksprung,
Code quasi geladen.
Das heißt, die nächste Anweisung nach dem Call, aus meinem aufrufenden Programm,
wäre dann diejenige, die dann also hier ausgeführt wird.
Und der Stackzähler oder Stackpoint, da wird dann eben wieder so umgesetzt,
dass ich dann hier wieder in einem malten Kontext bin.
Das ist dann also letztendlich dieses hier. Gut,
dadurch lassen sich dann eben im Prinzip Kontrolle über oder lässt
sich dann eben diese Kontrollübergabe dann dementsprechend passend implementieren.
Was man hier auch immer schon gesehen hat, ist,
dass die Rücksprungadresse letztendlich dann also irgendwo ja auch ein Stack
gespeichert oder dazwischen gespeichert wird
Das war früher eigentlich immer so ein Mechanismus oder so ein Angriffspunkt,
worüber letztendlich bei irgendwelcher Schadsoftware beziehungsweise wenn irgendwie was vielleicht auch,
also auch nur unabsichtlich unsauer programmiert ist,
worüber man also dann versuchen kann,
quasi jetzt in der Ausführung aus dem entsprechenden Programmtext oder Programmkontext rauszukommen.
Sprich, wenn ich jetzt also beispielsweise angenommen, das Ganze wäre jetzt immer so implementiert.
Und ich habe jetzt also hier mein Code in dem Unterprogramm
und in dem Code kann ich jetzt über einen Stackpointer oder über andere Mechanismen hier diese Rücksprungadresse überschreiben.
Dann würde das letztendlich ja dazu führen,
dass wenn also jedes Return kommt, dies hier verlassen wird,
ich an diese Adresse springe und das ist dann eben nicht mehr diese Adresse,
sondern darauf haben wir irgendwie ein anderer Code oder Code aus dem Betriebssystem oder was auch immer.
Das ließ sich also letztendlich vor längerer Zeit für irgendwelche Viren
oder Schadsoftware so ausnutzen. Muss jetzt auch immer sagen,
heutzutage würde es nicht mehr funktionieren,
weil in der Regel mit diese Rücksprungadressen,
also auch noch durch bestimmte Mechanismen,
gesichert sind,
sodass im Prinzip beim Return noch geguckt wird,
wurde hier diese Rücksprungadresse verändert oder wurde sie nicht verändert nicht und
dadurch kann jetzt also im Programm nicht irgendwie absichtlich oder unabsichtlich,
also irgendwie woanders hin entsprechend zurückspringen
Kann sein, dass sowas dann also nochmal in,
ich glaube, dann zweiten, dritten Semester oder sowas,
dann mit drin ist bei Sicherheit, also SSV,
die Vorlesung von Herrn Federath,
wo es dann im Prinzip um verteilte Systeme,
aber eben auch um Sicherheitsmechanismen geht,
dass da dann sowas vielleicht dann also auch nochmal ein bisschen genauer mit angesprochen wird.
Wie gesagt, hier nur Anmerkungen, also Rücksprung, Adresse auf dem Steak,
angenommen, ich würde die jetzt mit was anderen überschreiben,
dann könnte ich da also im Prinzip beliebig hin und her springen.
Gut, ja, also das zu der Art und Weise, wie das passiert.
Gut, das war jetzt also Kontrollfluss, wie funktioniert die Datenübergabe? Da kann man jetzt eben sagen,
hier von den Registern ist X86,
64 Bit unter Linux, also immer folgende Konvention üblich,
dass man erst mal versucht,
die ersten sechs Argumente über Register zu übergeben, dass wir,
also das erste Argument einer Funktion wird immer in RDI stehen,
das zweite in RSI, das dritte vierte,
fünfte und sechste in diesen entsprechenden Registern und Die,
der Returnwert einer Funktion wird immer in RAX stehen.
Das war bislang bei allen Beispielen, also auch immer der Fall. Ist halt einfach irgendwo eine Konvention,
ist immer so festgelegt und solange diese Konvention von allen Funktionen, Prozeduren und so weiter dann eben passend eingehalten wird, funktioniert das Ganze. So,
wenn man jetzt mehr Argumente zu übergeben hat,
war es ja nun eigentlich theoretisch beliebig viel sein können,
dann werden die eben auch auf den Stack gelegt und zwar in der Reihenfolge,
dass also quasi das letzte Argument mit der höchsten Adresse und dann eben in top of stack ist ja quasi hier unten,
dann eben das siebte,
achte und so weiter Argument dann eben immer hier oberhalb von diesem dieser Adresse hier stehen würde,
in Bezug auf die Adressen,
wie das Ganze dann letztendlich beim Call gehandhabt wird,
wäre das also an der Stelle dann ja die Rücksprungadresse stehen würde und der Stack pointe dann eben wie Sorry.
Hier angedeutet dann ja also quasi auf diese Rücksprungadresse. Hier war das,
also die Rücksprungadresse zeigt und hier wären dann eben die Argumente 7,
8, 9 und so weiter unter den entsprechenden Speicheradressen.
Gut, was bei der Datenübergabe dann ja aber eben auch immer noch wichtig ist, ich hatte gesagt, was passiert jetzt mit diesen Registerinhalten? Nicht,
wenn ich diese Funktion aufrufe,
habe ich eben Dieses Argument X in RDI,
das Argument Y in RSI,
wenn diese Funktion jetzt quasi mal zwei aufruft, habe ich dann eben,
dass dort zugehörige erste Argument A in RDI und BNRSI,
jetzt müsste ich natürlich irgendwie dafür sorgen,
dass quasi hier nicht, frisch, wenn ich,
also hier erst nach dem Call zurückkomme,
dass hier jetzt also letztendlich wieder die ursprünglichen Werte stehen und
nicht das,
was ich also hier vorher reingeschrieben habe
Nicht dazu gibt es dann eben etwas,
dass man im Prinzip in dem spare ich ja von
dem Stack eben auch noch diese Register mitspeichert.
Das kann eben entweder durch das Unterprogramm passieren oder es kann durch das aufrufende Programm passieren.
Das Ganze nennt sich demzufolge dann eben entweder Corly safe,
dann ist das Unterprogramm dafür verantwortlich,
dass irgendwelche Register gesichert und vor dem Return wieder zurückgeschrieben werden.
Ich war also dieses X Y und hier noch irgendwelche anderen Sachen irgendwo hinschreiben,
kann ich natürlich, also an dieser Stelle machen durch das Mole Store.
Ich kann das aber eben auch durch das aufrufende Programm, durch das Mold machen.
Nicht, wenn das hier passiert, wäre das eben Caller-safe.
Das heißt, die Aufrufenfunktion muss es sichern. Hier wäre
das Cally safe, dann würde das eben die aufgerufene Funktion machen. Gut,
ansonsten mit dem Stack erreiche ich dann eben letztendlich das,
was also ganz oben steht, also dass man eben sogenannten Reendland-Code habe,
wo ich dann eben Code habe,
der den lokalen Kontext eben über den Stack sichert.
Und dadurch eben gewährleistet ist, dass der Code eben mit
mehreren Instanzen irgendwo laufen kann, beziehungsweise rekusiv aufgerufen werden kann.
Und für jede dieser Instanzen muss ich dann eben gegebenenfalls Argumente,
wenn ich jetzt mehr als sechs habe, lokale Variablen,
entsprechend diesen Sachen, also vielleicht irgendwelche alten Werte,
zwischengespeichert haben und die Rücksprungadresse geht dann eben letztendlich über
den Stack
So, das Ganze nennt sich dann eben im Mark Closure,
das sind also quasi die Daten, die es also für so einen Funktionsaufruf benötigt werden. Das heißt,
ich habe jetzt in meiner Prozedur, wenn die läuft,
hier irgendwo als oberstes Argument den Stackpointer und habe dann eben irgendwie so einen ganzen Speicherbereich,
wo hier entweder lokale Daten liegen,
wo vielleicht zwischengespeicherte Registerinhalte liegen,
also sprich alles,
was hier an Datenstrukturen irgendwo benötigt wird und den Stackpointer entsprechend inkrementiert,
wird dann also hier abgelegt und vor dem Return
wird das Ganze dann eben letztendlich wieder auch abgebaut
Oft hat man, beziehungsweise früher beim 32-Bit-Essembler,
hatte man dann eigentlich auch immer noch einen zweiten Pointer,
das ist dieser Base-Pointer, R-B-P, das Register.
Das verweist quasi auf den Beginn dieses Speicherbereichs, der aufgerufenen Prozedur. So,
ich hatte ja schon gesagt, vor dem Prozeduraufruf muss gegebenenfalls,
also für Argumente und so weiter,
beziehungsweise für das Speichern von Werten,
Der Stack irgendwie noch gefüllt werden, dann kommt der eigentliche Prozedurauufruf.
Und nach, beziehungsweise vor dem Return, je nachdem,
ob das Caller oder Quardy Safe ist,
wird dann eben hier alles wieder entsprechend freigegeben oder das Ganze wieder zurückgesetzt.
Kann man sich jetzt also folgendermaßen vorstellen. Ich habe jetzt
hier drei Funktionen, die sich jetzt also gegenseitig bzw. rekursiv
irgendwie noch aufrufen. Eine Funktion namens Jo,
die ruft einen Hu auf und Hu ruft MI auf,
zweimal und MI ruft sich jetzt,
das ist auch noch irgendwie selber reklusiv auf.
So, wie sieht das also jetzt in Bezug auf
den Stack aus? Also ich bin in der Ausführung,
habe hier irgendwie meinen Stack, habe hier die laufende Funktion.
Jo, die ist also bis hier ausgeführt wurde.
Ich dann soll da unter Programmaufruf von dem Hu passieren,
was dann eben passiert ist,
dass der Stack quasi entsprechend so weit wächst,
dass ich also hier diesem Kontext diese aufgerufenen Funktionen hier habe.
Die läuft dann eben entsprechend durch, ruft irgendwann dann dieses MI einmal auf.
Dadurch wird das Deck dann wieder entsprechend vergrößert.
MI kann sich jetzt eben selber rekursiv auch noch aufrufen.
Und da sieht man dann eben genau diesen Effekt,
dass dann jede Instanz quasi so eines regulsiven Aufrufs dann eben quasi den eigenen Speicherbereich letztendlich hat.
So, wenn diese Funktion dann also irgendwo am Ende ist,
Rekursion terminiert oder das Unterprogramm dann eben mit dem Return zurückgeht.
Ich würde dann eben entsprechend das Stack wieder abgebaut,
gegebenenfalls jetzt hier entsprechend wieder gemacht und irgendwann,
wenn ich in der Programmausführung hier bin, nach dem Aufruf von Wu,
habe ich dann eben wieder diesen einen ursprünglichen Stack.
Gut, wie gesagt, das Ganze passiert in der Regel automatisch,
beziehungsweise wird dann eben passend im Assembler-Quoten mit eingebaut.
So, hier ist nochmal kurz aufgezeichnet,
wie man sich das letztendlich immer vorstellen muss.
Ich habe hier Mein Aufrufenden Code von dem Steak-up würde vor dem Aufruf letztendlich die,
wenn ich jetzt mehr als sieben Argumente habe,
die dann eben auch noch auf den Stack mit irgendwelchen Push-Operationen raufbringen, dann würde der Call passieren.
Beim Call wird ja die alte Rücksprungadresse auf den Stack gespeichert und anschließend habe ich dann in meiner aufgerufenen Funktion dann eben den,
das, was dann eben hier immer so Blau hinterlegt ist,
den entsprechenden Kontext, wo dann er eben gegebenenfalls noch irgendwelche Register,
die hier also gefüllt worden sind, noch zwischengespeichert werden,
wenn beispielsweise hier drin jetzt auch mit diesen Registern gerechnet wird.
Und am Ende vor dem Return werden dann eben diese Werte auch wieder entsprechend wieder zurückgebracht. Gut,
ja, hier nochmal zu der Frage,
welche Register können jetzt temporär von welcher Funktion genutzt werden,
weil, wie gesagt, diese 16 Universalregister,
möchte ich hier natürlich in meinem Assembler-Code verwenden,
die möchte ich hier natürlich auch an meinem Assembler-Code verwenden.
Und da muss man sich jetzt eben letztendlich einig sein,
wer quasi die Werte entweder die aufrufende Funktion oder die aufgerufene
Funktion dann eben auf dem Stack speichert,
damit nach Beendigung des Funktionsaufrufs oder des Prozeduraufrufs
dann quasi diese aufgerufene Funktion dann auch wieder seine alten Werte
letztendlich lesen kann
So, und da ist also die Sicherungskonvention,
also das Register RAX, das war auch in den Beispielen
immer schon der Fall, ist typischerweise dann der Rückgabewert.
Das wird von der aufrufenden Funktion gesichert. Die Register RDI
bis R9 sind ja die Argumente für die ersten sechs Parameter.
Wenn ich jetzt also ein Unterprogramm aufrufe,
Das muss im Zweifelsfall dann auch die aufrufende Funktion sichern.
Das heißt, die wurde ja vielleicht auch schon mit irgendwelchen Werten hier drin aufgerufen.
Wenn diese Funktion jetzt eben eine andere Funktion aufruft,
muss die dann eben die alten Werte erstmal auf den Stack sichern,
schreibt dann einem quasi den eigenen Wert,
also sprich das Argument von den Funktionsaufruf rein.
Und anschließend passiert dann der Funktionsaufruf.
Das heißt, das Ganze ist dann eben auch Caller-safed.
Und die beiden Register R10 und R11 müssen eben
auch von der Aufrufenden Funktion
gesichert werden
Quali safe, also sprich,
da muss ich dann die aufgerufene Funktion drum kümmern,
sind dann eben die Register RBX und dann eben R12,
13 und 14 und Basepointer beziehungsweise Stackpointer werden letztendlich,
also direkt oder indirekt dann ebenentsprechend auch dort behandelt,
wobei der Stackpointer sollte dann natürlich nicht direkt manipuliert werden,
sondern wird dann in der Regel durch Call- und Return mit behandelt.
Gut, ja, das erstmal grob zu der Idee,
weswegen man den Stack braucht, welche Probleme es da geben kann,
also sprich diese Register irgendwo lokal noch mal zu sichern,
damit die sich nicht irgendwie gegenseitig überschreiben,
weil sonst würde die ganze Programmierung ja noch nicht richtig funktionieren.
Dann kommen wir jetzt noch mal kurz zu dem Teil,
wie im Prinzip Daten bzw. Datenstrukturen im Speicher abgelegt werden,
wenn ich jetzt also im Prinzip meine Datenstrukturen aus einer Programmiersprache,
in diesem Fall, also von C beispielsweise nehmen würde.
Und zwar, wenn ich jetzt also ganz normale Ganszahlen habe,
ist letztendlich dann auch immer Architektur-Compiler, Betriebssystem abhängig,
habe ich dann also sowas wie 1, 2,
4 oder 8 Byte, als für die Zahlendarstellung.
Nicht bei Fließkommazahlen dann eben auch mit entsprechenden Wortbreiten, je nachdem,
wie das Ganze letztendlich dann deklariert ist. Wie gesagt,
ist immer so ein bisschen vom Compiler abhängig,
das hatte ich also auch schon mal in dem Kapitel zu den Zahlendarstellungen,
wo ich dann also auch nochmal diese Anmerkung hatte, nicht,
dass das eben wirklich vom Betriebssystem habe ich ein 16,
32 oder 64 Bildbetriebssystem,
welchen Compiler benutze ich dann
entsprechend ist
Gut, wie sieht das jetzt bei irgendwelchen Strukturen aus?
Also sprichst du was wie Arrays? Da kann man sich jetzt immer vorstellen, dass ich,
weil der Spare ich ja selber, also sprich das,
was ich in Java oder in C als Array habe,
gibt es auf Assembler-Ebene nicht.
Also auf Assembler-Ebene gucke ich mir eigentlich wirklich immer nur Byte an.
Und wenn das jetzt also irgendwie größere Zahlen sind,
also sprich irgendwelche Inditscher-Werte, werden dann eben automatisch vom Assembler
entweder vier oder acht Byte als eine logische Einheit genommen,
gelesen,
Zufolge werden, dafür dann eben die entsprechenden Adressen aus dem Adressraum,
der ja aber baldweise adressiert ist, generiert. So,
wenn ich jetzt also ein normales A-dimensionales Aray habe,
mit irgendwie en Elementen, wird typischerweise eben dann ein Speicherbereich dafür assoziiert,
der dann eben entsprechend en-mal der Größe der Datenstruktur oder des Basistyps von Wahrheits entspricht.
Sprich, wenn ich sowas habe wie ein Aray von zwölf Zeichen,
Zeichendarstellung ist in diesem Fall jetzt also erstmal hier Ein Byte,
hätte ich dann eben quasi einmal die Startadresse und dann eben entsprechend die elf Folgeadressen,
nicht und mit x plus 12 würde ich dann eben quasi dann hinter das Ende dieses Arrays referenzieren.
Nicht, wenn ich das jetzt für einen Inager mache mit fünf Elementen,
werden, weil beim Entager 32 Bit breit, brauche ich vier Byte,
wird der Assembler demzufolge dann quasi auch wieder Startadresse x,
das erste Element wird dann mit x plus 4 referenziert,
Nicht, dass wir ein Element, null Element 1,
Element 2 und so weiter,
wo ich dann eben entsprechend jetzt immer mit diesem,
hier habe ich ein Offset von 1,
war es dann nämlich der Size-of-Charakter entspricht,
hier habe ich dann jeweils ein Offset von 4,
was dann dem entspricht.
Nicht, wenn ich ein 64-Bezahl-Darstellung habe,
würde ich dann eben entsprechend jeweils mit Offset 8 rechnen,
Maschinenadressen, das sind also hier diese Pointer,
würden dann eben auch entsprechend 64-Bit bereit sein
Gut, da ist es jetzt eben so, das macht der Compiler beziehungsweise der Assemblern eben automatisch. Das heißt,
wenn ich jetzt in meinem C-Code sowas habe,
dass ich jetzt also hier sage,
ich habe hier meine Startadresse, hier zum 5-Bit RA,
mit jetzt irgendwie irgendwelchen Werten, hier diese entsprechenden Offsets,
also in diesem Fall jetzt also immer jeweils 32-Bit-Zahn-Darstellung,
dass ich dann letztendlich, wenn ich das jetzt hier derreferenziere,
sehen kann, Value von 4, also dem vierten Element,
wäre dann drei, ich rede es bei NC,
fange immer mit 0, 1, 0, 1,
2, 3, 4. Na,
hier habe ich dann eben genau den an,
weil nicht wir dann eben Zeige auf ein Entager,
nämlich die Startadresse von dem Ganzen,
also hier diese Adresse X,
Value Plus 1 wäre dann da der Compiler jetzt eben weiß,
ich rechne jetzt hier mit diesem Offset dann
X Plus
Die Adresse von Value von 2 wäre dann also irgendwie X plus 8,
also sprich Wurf beginnt das zweite Element,
der Inhalt von Value von 1 wäre dann 5 und der Value plus I
wäre dann eben entsprechend jetzt immer hier die wahrige Adresse,
die sich dann hieraus ergibt.
So, was dann natürlich immer nicht passiert,
und das ist einfach eine allgemein beliebte Programmierfehler NC,
ist das, also jetzt irgendwelche Bereichsgrenzen, wirklich sicher abgecheckt werden.
Das heißt, wenn ich dann eben ein Element habe,
was also fünf oder ein RE, was eben fünf Elemente hat,
die jetzt also quasi von 0 bis vier hier zugegriffen werden können,
einen Zugriff auf Value von fünf,
wäre dann eben letztendlich das Element was also quasi ab hier im Speicher liegt,
was aber eigentlich gar nicht mehr zu diesem Erregel hört.
Auch das etwas,
was inzwischen in der Regel Betriebssysteme sicher abfangen sollten,
beziehungsweise wo dann also quasi durch den Compiler dann eben entsprechende Tests eingebaut werden,
dass eben solche Zugriffe dann eben zur Laufzeit,
zu entsprechenden Ausnahmebehandlungen führen und man dann eben irgendwie eine entsprechende Fehlermeldung bekommt.
Gut, damit lassen Sie sich dann im Prinzip also
einfache Race direkt
Referenzieren bzw. entsprechend zugreifen. Also hier nochmal so ein Beispiel dann eben so für drei HRs,
die jetzt ja hier irgendwie deklariert sind mit irgendwie entsprechenden Werten.
Und wenn ich jetzt also quasi an dieser Stelle beispielsweise mit Adresse 16 anfange,
wo man dann eben entsprechend sieht, wie die im Speichel
jetzt also auf nach der Deklaration dann eben hier auf entsprechend aufeinanderfolgenden Adressen laufen.
Da sieht man eben auch,
weswegen solche Zugriffe dann eben hoffentlich auch irgendwie abgefangen werden,
weil was da natürlich passiert ist,
wenn ich also jetzt quasi hier das als erstes Element hätte und
darauf jetzt eben auf mit dem Index 5 drauf zugreifen würde,
dann bin ich nämlich hier schon in der nächsten Datenstruktur und im nächsten Array und würde dann eigentlich versuchen,
dort also quasi das Nulltelement rauszulesen.
Gut, also Errätzugriff, dafür gibt es,
wird dann eben wirklich immer direkt über die Adressierung in Bezug auf diese Basisadresse umgesetzt.
Und dafür gibt es eben genau dieses eine Adressierungsschema,
was sich ja so, also in diesem Adressberechnung beim X86 ja schon vorgestellt,
hatte vorgestern, wo man eben sagt,
ich habe quasi hiermit RDI sowas wie meine Basisadresse,
sprich wo eine Datenstruktur startet im Speicher.
Ich habe dann mit RSI mein Laufindex,
der dann eben 0-1, 2, 3,
4 irgendwie durchläuft und mit diesem Skalierungsfaktor 4 habe ich dann eben die Size of T,
also sprich die Größe in Bite,
mit der dann Die Adressen entsprechend fortgeschaltet werden müssen.
Darüber kann man dann eben direkt solche Arrays zugreifen. Wie gesagt,
in der Programmierung, da ich dann also da erst mal
nicht zwingend solche Befehle habe, kann es dann eben sein,
dass es da dann also zu Problemen kommt.
Teilweise kann man sowas also auch innerhalb seines eigenen Programmes einbauen.
Ansonsten muss man sich dann eben nicht wundern,
wenn das Programm sie dann
irgendwelchen Schrott produziert
Mit dieser Bereichsüberprüfung, was inzwischen allerdings immer passiert ist,
wenn ich meinen eigenen Programmkontext verlasse, sprich also mit dem,
mit irgendwelchen Basisadressen und der dem RA jetzt irgendwas adressieren würde,
was dann nicht mit zu meinem Programm,
zum Betriebssystem oder irgendwelchen Datenstopp, anderen Datenstrukturen im Speicher gehört.
Nicht da würde es dann letztendlich Probleme geben können. Goodbye,
mehrdimensionalen RAs, wenn ich also eben zum Beispiel sowas wie irgendwelche Matrizen habe,
kann man im Prinzip jetzt unterschiedliche Varianten haben,
wie das Ganze implementiert ist,
die in Anführungszeichen neueste Variante ist eigentlich das,
wie das also beispielsweise ein Java der Fall ist.
Da wird so ein Errädern,
also über eigentlich ein Aray von anderen Arays gebildet,
wo ich dann also im Prinzip Zahlen habe und in diesen Zeilen dann,
die jetzt wieder innerhalb der zweiten Dimension dann eben wieder zusammenbauen kann.
Dadurch kann ich dafür sorgen,
dass diese Zahlen nicht alle unterschiedlich lang sind und dann eben
auch nicht rechteckige Layouts erzeugen
Und das Ganze passiert über Pointer,
dadurch lassen sich dann also auch noch ein bisschen oder Mehrfachbenutzung einbauen,
ist im Prinzip in diesem Beispiel jetzt hier angedeutet,
wenn ich also jetzt hier irgendwie so eine zweidimensionale Matrix habe,
die jetzt also hier mit irgendwelchen Werten für Row einfach mal,
um zu zeigen, wie groß sie sind,
dann eben für R und C initialisiert,
kann man sich eben vorstellen, die Basisadresse,
die jetzt also quasi für Matrix gilt,
würde dann also quasi über diesen Pointer realisiert werden.
Dieses wäre dann eben die erste Zeile,
also schon quasi das,
was in der Zero ist
Also Row Null, Row 1, Row 2, Row 3,
Euro 4 und die Calms wäre dann eben quasi immer der entsprechende Index dann innerhalb von dem Ganzen.
Ansonsten in anderen Programmiersprachen wird das in der Regel dann eigentlich
wirklich als zwardimensionales eins zu eins in den Speicher geschrieben.
Da kann man dann auch noch unterscheiden in einer Row Major-Anordnung. Also sprich,
wenn nicht ein Also hier irgendwie sind zweidimensionales Array habe,
laufe ich da,
wenn ich jetzt also quasi mit den Adressen fortschalte,
innerhalb der Zeile vorwärts und wenn ich hier am Ende bin,
gehe ich dann quasi in die nächste Zeile oder laufe ich innerhalb der Spalte vorwärts und gehe dadurch dann quasi Also sprich,
laufe hier durch die unterschiedlichen Zeilen innerhalb der ersten Spalte und gehe dann also erste Spalte,
zweite Spalte, dann eben entsprechend in dieser prahlen Folge durch.
Das Ganze nennt sich dann Role Major oder Calmmajor-Anordnung.
Auch da gibt es dann also irgendwie dann so bösartige Effekte,
wo man sagen kann, in C ist das halt so realisiert.
In Fordran ist es anders realisiert, in einigen Programmen,
also beispielsweise Mathematik, Software,
Matlab wird also auch immer so eine Calmmajor-Anordnung gemacht,
wenn ich jetzt Kot habe,
den ich jetzt also quasi aus unterschiedlichen Programmiersprachen mische und das
Zusammenbauzahlen Programm kann es in ungünstigen Fällen da dann also zu Problemen geben kommen.
Nicht, weil da einfach diese Adressierungsarten,
also sich so ein bisschen in die Quere kommen.
Nicht ansonsten, in C sind es zweidimensionales Area,
also eine Rohmetscher-Anordnung, wie also letztendlich hiermit angedeutet. Nicht,
ich habe hier mein Area jetzt so entsprechend deklariert und die
erste Zeile würde dann ihm quasi ihrem Speicher hier in aufeinanderfolgenden Adressen sein,
dann beginnt eben die zweite Zeile und so weiter,
nicht weil dieses zwardimensionale RA wird natürlich alles jetzt auf eine Dimension im Speicher runtergebrochen.
Hier dann einmal das Beispiel, wie das dann letztendlich aussehen würde,
wenn ich jetzt also hier diese Datenstruktur in den Speicher irgendwo reinlege und Wie gesagt,
das Ganze lässt sich dann eben natürlich jetzt nicht nur zweidimensional,
sondern mehrdimensional machen.
Auch da kann man immer sagen, okay,
bei so einer Rohmeter-Anordnung ist immer die letzte Dimension fortlaufend,
bei einer Carlo Major-Anordnung immer die erste Dimension fortlaufend.
Ansonsten, wie gesagt, ist das funktional, logischerweise Äquivalent.
Goodbye, anderen Datenstrukturen, also sprich sowas wie Records,
Wie sieht das da aus? Also angenommen,
ich habe jetzt den C, also hier irgendwie so eine Structure,
wo ich jetzt, also ein Arame von vier Entager-Werten habe,
wo ich ja hier irgendwie einen Index-I habe,
der mir also dann beispielsweise hier also irgendwie ein Array-Element dann zurückgeben kann.
Und ich habe ihn sage jetzt hier auf ein nächstes Element. Das,
was der Compiler daraus machen würde aus einer Structure, wäre halt,
dass also quasi der Rekord Air hier dann auch irgendeine Basisadresse bekommt.
Dann wird jetzt eben für jede dieses Element dieser Struktur dann
quasi ein zusammenhängender Bereich mit so und so viel Bytes aluziiert, beziehungsweise verwendet.
Also einmal dann eben für dieses Array mit den vier Elementen,
nicht dann einmal für das,
was ich jetzt also für diesen Index brauche und einmal das,
was ich also jetzt für diesen Zeiger brauche.
Und was dann auch im Prinzip jetzt passiert, ist,
dass dann das Programm letztendlich, also diese Adressumrechnung,
die sich daraus ergeben, also sprich,
wo die greife ich jetzt also auf das I zu,
wie greife ich auch das Next zu,
dann eben automatisch auf die entsprechenden Speicheradressen machen.
Gut, hier wäre jetzt also beispielsweise nur, dass ich
also in diesem Array beispielsweise dann jetzt also dieses IT-Element zurückgeben möchte.
Das heißt, ich habe also hier irgendwie diese Datenstruktur,
über die ich also mit dem Zwerger darauf zuweise,
sage dann eben,
dass wir dann eben ein Element,
was dann also letztendlich
davon zurückgegeben werden soll
Das wäre dann also der entsprechende C-Ausdruck,
wie man da jetzt diesen Rückgabewert letztendlich berechnet, nämlich,
dass sich also von dem, aus dieser Struktur,
er, A,
nehme und von A dann eben entsprechend das IDX-Element oder IDX-T-Element nehme und einen Zeiger darauf zurückgebe,
nicht im Assembler-Code wäre, das dann eben letztendlich, sowas,
dass dieser Adressausdruck berechnet wird und dann eben in dieses Rückgabewertregister,
in diesem Fall RAX,
beispielsweise gespeichert wird
Gut, hier dann eben ein Beispiel, wie man dann jetzt also irgendwie so eine verlinkte Liste durchlaufen würde,
wo ich dann immer irgendwie ein Element berechne,
dann zunächst gehe und dann quasi hier entsprechend, solange ich noch ein weiteres Element habe,
dann eben diese Liste der Reihe nach traversieren würde und das dann eben auch immer entsprechend hier dann der SM-Black-Code.
Also das war es ja eigentlich erstmal nur wichtig ist,
was so ein bisschen hängen bleiben soll,
also der beziehungsweise der Assembler baut, das ist nämlich das,
was man hier dann entsprechend sieht, dann immer entsprechend den Größen in Byte,
die diese Elemente jetzt in der Struktur benötigen,
sich dann quasi immer die passenden Adressen zurecht,
dass ich jetzt eben sagen kann,
wie kriege ich jetzt meine Werte dort entsprechend zurück,
beziehungsweise wie grauer greife ich dann darauf zu, sodass dann eben beispielsweise hier,
wenn ich jetzt also quasi auf diesem Pointer drauf zugreifen werde,
RDI wäre dann immer jeweils in diesem Fall beim Aufruf dann die Basisadresse,
das Argument gewesen, nicht mit Offset 24,
lande ich dann eben quasi bei dem Next.
Das wird dann eben wieder für den nächsten Aufruf dann,
also nach RDI kopiert und ich hätte dann eben quasi damit dann den Verweis auf das nächste Element,
was aber nur passiert, wenn das ungleich null ist.
Also sprich, wenn ich jetzt hier noch irgendwie nächstes Element hätte,
das wäre dann also hier quasi der dazu kommentierte C-Code.
Gut, ja, das erstmal grob zu den Datenstrukturen. Das,
was dann da dann auch immer noch häufig sieht bzw. beobachtet ist,
also dass diese Datenstrukturen dann eben typischerweise auch noch an Wortgrenzen ausgerechnet oder ausgerichtet werden. Das bezieht sich so ein bisschen auch darauf,
dass, was jetzt also in dem vorigen Kapitel,
als es also nochmal so grob um Speicher und diese Problematik mit Big Andien,
Little Andien ging, darum geht, dass man sagt,
naja, der Speicher oder der Hauptspeicher ist natürlich,
so wie das hier betrachtet wurde,
immer irgendwie so ein Beidweise adressierbares Reh,
wo ich dann also quasi hier immer irgendwie so einzelne Bytes drin habe,
rein praktisch ist es aber natürlich so,
dass also irgendwie jetzt beim 64-Bit-System ich dann also in dem Fall dann also entsprechend Ich dann,
also dementsprechend eigentlich acht Byte habe und dass es dann eben ein Speicherwort.
Und da ging es ja also letztendlich darum, dass man sagt,
naja, was man jetzt eigentlich aus Effizienzgründen nicht machen möchte,
ist, dass man sagt, okay, ich habe eins,
32-Bit-Indiger, nicht dafür brauche ich jetzt also irgendwie vier Aufeinanderfolgende Byte.
Und jetzt habe ich als nächstes beispielsweise als Datenstruktur in so einem Record ein 64-Bit-Indiger.
Wenn ich den jetzt also besonders speichereffizient speichere,
kann ich natürlich sagen, nehme ich also jetzt die nächsten
acht Byte
Und die wären dann also quasi jetzt hier in diesem Speicherwort.
Und in dem nächsten Speicherwort dann eben hier auch noch mit drin.
So, das ist natürlich dann also vergleichsweise ineffizient.
Deswegen sieht man dann eben zu, dass also Datenstrukturen so
im Speicher ausgerichtet werden, dass man also hier lieber was freilässt.
Und dieses 64-Bit, die sind 64-Bit-Enager dann also quasi in
dem nächsten Speicherwort unterbringt, sodass ich da dann eben auch
nur einen einzigen Speicherzugriff beibrauche
Nicht, das ist also das,
was jetzt also hier mit gemeint ist, mit diesen Datenstrukturen und Wortgrenzen ausrichten,
weil man da dann eben entsprechend ineffiziente Zugriff war,
nämlich also auf einmal zwei Speicherzugriffe,
war es dann also auch noch bei Konzepten wie virtuellem Speicher,
was, also beim übernächsten Kapitel beim Betriebssystem kennenlernen werden und Cashing,
was das im nächsten Kapitel dann ab nächster Woche dann folgen wird,
auftauchen wird, wo man dann sagen kann,
das würde damit nicht richtig funktionieren.
Nicht demzufolge erzeugt der Kompar dann eben typischerweise Lücken,
wo man dann eben sagt, je nachdem,
wie groß die Datenstrukturen sind, werden die dann eben entsprechend immer so ausgerichtet,
dass sie dann immer quasi komplett ganze Speicherworte belegen.
Also hier, also Nummer das, Beispiel nicht,
wo man sagen kann, okay,
wenn ich also sowas wie einen Charakter habe, hier irgendwelche Integra nicht,
dann wird das Ganze dann eben immer so umgebaut,
dass sich beispielsweise die Startadresse von so einer Struktur dann eben wirklich Beginn mit einem kompletten Speicherwort ist.
Das heißt, ein Vielfaches von acht. Nicht,
dann hätte ich jetzt also hier Charakter und dieses Entager dann
eben in einem Speicherwort diesen Entager in einem zweiten Speicherwort.
Dann wird das Ganze freigelassen. Da dann also quasi jetzt hier
der Double-Wert kommt und der dann auch entsprechend an so einer Wortgrenze ausgerichtet werden soll.
Gut, dann die letzten paar Folien, wo es jetzt
also nochmal um den Linker und den Loder geht.
Da geht es also im Prinzip jetzt nur darum,
dass man sagt, okay,
also ganz am Anfang hatte ich ja oder zu Beginn dieses Capit,
dass er so kurz vorgestellt, die Hierarchie,
wie jetzt also quasi der Compiler der Reihe nach jetzt,
also in diesem Fall, dass er den C-Code nimmt,
daraus Assembler-Code macht, dann läuft der Assembler,
der erzeugt daraus, ist die Benärdatei, die einen Objektcode.
In der Regel habe ich dann ja mein Programm,
nicht nur in einer, sondern in mehreren Quelldateien,
wo ich mehrere kleine Sachen zusammen habe und wie man das verwaltet,
dass es eben letztendlich das, was jetzt,
also hier auf die nächsten drei Folgen noch folgen wird.
Nicht, wo man sagt, okay,
ich habe typischerweise natürlich in dem Programm mehr oder mehrere Quelldateien,
wo ich jetzt eben beispielsweise hier irgendwie mein Hauptprogramm habe,
da rufe ich jetzt irgendeine Funktion auf.
Nicht, die ist jetzt also irgendwo in einer anderen Datei
letztendlich mit drin. Und das passiert dann eben letztendlich durch
diesen Mechanismus linker Loder und so weiter,
wo das dann also im Prinzip passiert
Linux-artigen Systemen, hat man dazu dann eben beispielsweise die GCC-Toolchain als Compiler-Driver.
Dabei ist dies GCC eigentlich nur eine Art Oberprogramm,
hinter der sich dann also verschiedene Teile noch verbergen,
nämlich also sowas wie ein Pre-Prozessor, der macht Textersetzung,
dann der eigentliche Compiler, dann der nächste Schritt Assembler,
der macht daraus dann eben quasi den Binärcode und dann eben der Linker,
der das dann eben gegebenenfalls auch schon zu einem ausführbaren Programm zusammenbaut.
Das heißt also, da kann man theoretisch mit einem Aufruf dann eben gleich mehrere Aufeinanderfolgende Schritte einstellen. So,
jeder dieser Schritte oder jedes dieses Talprogramm lässt sich dann also irgendwie über zig Parameter dann also irgendwie dann noch entsprechend einstellen,
wo man dann eben wirklich sagen kann, für den Compiler,
welche Art Code soll daraus gemacht werden, für welche Zielmaschine,
64 Bits, 32 Bit, diese Szene ist,
also da gibt es also beliebig viele Optionen,
ein paar von den Optionen sind jetzt eben schon bei diesen Umsetzung von If oder Wile beispielsweise mal mit angegeben worden.
Wie gesagt, wenn man sich davon mal abschreckend inspirieren lassen
soll oder will, kann man also gerne mal...
Also hier irgendwie sich die Manpage von dem GCC
mal irgendwie ansehen und dann geht das hier unter Option Summary
Also jedes von denen ist dann also quasi hier irgendwie so eine Compiler-Option,
die dann also abhängig für Programmiersprache,
welche Warnwarnungen oder Fehlermeldungen sollen ausgegeben werden,
möchte ich Debugging machen und so weiter, also beliebig viel.
Im Prinzip eingestellt werden können. Und dann kommen also noch
die ganzen Optionen für die einzelnen Teilprogramme.
Also wie gesagt, das lässt sich dann also noch
beliebig viel oder beliebig stark
für eintunen
Gut, was man aber im Prinzip dann damit erreicht,
also jetzt mit dieser Aufteilung von dem Code in mehrere Quellprogramme ist,
dass sie natürlich entsprechend modular arbeiten kann mit kleinen Teilen,
dass sie bestimmte Teile dann also natürlich auch noch sehr einfach wiederverwenden kann. Nicht,
womit man dann natürlich also deutlich effizienteren Code beziehungsweise Handhabung machen kann,
weil ich dann eben immer nur kleine Teile jeweils neu übersetzen muss,
wenn sich irgendwie was geändert hat,
wo ich dann also beispielsweise beim Mehrkernsystem oder mehr Prozessorsystem dann eben auch sagen kann,
ich kann parallel kompilieren und wo man dann eben auch den Vorteil hat,
dass man gegebenenfalls also auch noch Speicherplatz spart,
weil bestimmte Sachen dann eben nur einmal ja,
entweder dazu gelingt werden oder wenn das also zur Laufzeit dynamisch gelingt,
wird dann also auch nur einmal im Speicher sind.
Ja, was passiert dabei im Prinzip? Also zum einen habe ich also irgendwie den Linker,
wenn ich jetzt also hier quasi dieses Beispiel habe,
ich habe hier meine Funktion, Hauptprogramm und Unterprogramm,
das hier quasi geguckt werden muss, was sind irgendwelche Definitionen,
demzufolge werden dafür dann eben Symbole in so einer Symboltabelle angelegt.
Und wenn ich hier irgendwie was benutze als Referenz,
wird eben entsprechend vermerkt,
dass wenn ich das Ganze nachher zusammenbaue, mit diesem Zoom,
dann also quasi dieses dann also aus dem anderen Programm an hier herausgenommen werden kann.
Nicht wenn hier irgendwelche Bezeichner, identisch sind, sich Sachen entsprechend überschreiben,
wir müssen das dann eben auch entsprechend berücksichtigt werden.
Das, was der Linker dann macht, ist eben die sogenannte Relocation. Das heißt,
ich habe jetzt für jedes Teilprogramm mein Objektcode in der Regel
oder bei diesem GCC sieht das dann immer so aus,
dass der Code unterteilt ist in Text,
Section und Data-Section, Text bezeichnet quasi den Assembler-Code,
also sprich die binären Anweisungen für das Programm.
Data sind die Datenstrukturen, also sprich das,
was sie jetzt gerade als Letztes vorgestellt habe, wie das im Speicher abgelegt wird.
So, das habe ich jetzt einmal vom Hauptprogramm,
vom Unterprogramm und vielleicht noch irgendwelchen Systemcode, den ich brauche,
um das Ganze zu starten oder um jetzt irgendwelche Sachen dynamisch noch mit hinzuzulenken.
Und das, was da linker macht, ist,
Wir haben hier jetzt ja alle irgendwelche Adressen mit drin,
dass das Ganze dann eben zusammengebaut wird zu einem Blog,
wo ich dann eben einmal sowas wie so eine Header-Datei habe,
die jetzt erstmal sagt, das ist ein ausführbares Programm für ein 64-Bit-Linux oder ist es ein ausführbares Programm für ein 64-Bit-Windows?
Das war also das, was ich also bei diesem Object-Darm vorgestern also auch noch mal kurz gezeigt hat,
wo dann eben drin stand, also irgendwie bei dem ein Beispiel da,
sowas wie PEI-Win 32 oder so weiter,
dann habe ich Eine aggregierte Text-Section, das heißt,
die Sachen wurden dann eben hier alle entsprechend zusammengefasst.
Wenn ich hier drin ein Unterprogramm aufrufe von dem Summen habe,
werden dann eben entsprechend,
weil die jetzt also hier an bestimmte Adressen im Speicher oder denen
bestimmte Adressen zugeordnet werden,
kann ich dann eben an dieser Stelle hier auch schon sagen,
wo dann eben quasi der Funktionscode von dem Summ
hier letztendlich liegt und dann eben entsprechend für den Prozeduraufruf
dann eben diese Adressumrechnung quasi automatisch machen
Und die ganzen Datenstrukturen werden dann eben auch noch mal mit zusammengepackt.
Nicht das Ganze wäre dann eben so ein ausführbares Programm für den Loda.
Und anschließend wird das Ganze dann eben noch gelingt. Da geht es jetzt darum,
dass man eben jetzt noch sagt,
also irgendwie sowas wie externe Funktionen, also beispielsweise,
wenn ich in C irgendwie sowas wie ein Entklut von dem und
dem mache und da also jetzt irgendwie die Funktion Print F benutze,
aus Standard-IO, dass das Ganze dann eben Dazu gebaut wird,
das kann eben entweder statisch passieren.
Das heißt, der entsprechende Code wird in meinen Code mit reingebaut.
Auch wenn ich das jetzt also nicht als Programm selber geschrieben habe.
Nicht demzufolge passiert dann dieses Linken, während der Kompilierung.
Oder das Ganze kann eben dynamisch passieren,
also als Dynamic Lanking, wo dann also quasi,
wenn das Programm zur Ausführung in den Speicher geladen wird,
geguckt wird, okay, ich benutze die Funktion Print F
und der zugehörige Codelik liegt irgendwo an der und der Stelle im Speicher.
Und demzufolge muss dann für den Call die Adresse entsprechend passend gesetzt werden.
Dadurch ist es dann eben möglich, im Prinzip durch
mehrere Prozesse den Code gemeinsam zu benutzen. Gut,
ein dynamisches Binden, also das ist ein Dynamic-Lanking,
nicht das passiert eben bei Linux, also über diese Shared-Object-Dateien,
weil Windows ist das alles,
was man da irgendwo jetzt von irgendwelchen Programmen mal als DLL findet.
Das sind dann
eben diese Dynamic-Lank-Libraries
So, hier sieht man nochmal, wie das Ganze dann im Prinzip jetzt beim Laden mehr passiert.
Ich habe hier also irgendwie mein Hauptprogramm. Würde das jetzt also quasi mit einem GCC irgendwie übersetzen,
erzeuge daraus jetzt also was wie die Objektdatei in meinem Hauptprogramm
benutze ich jetzt eben sowas wie so eine Shared-Object-Datei vom Betriebssystem,
also sprich die allgemein für die Stadtbrache C verfügbar ist,
also sie ist lebt C. Oder eben auch eine,
die ich mir selber erzeugt habe,
also in diesem Fall habe ich dann irgendwo in der Datei,
die ich dann eben irgendwelche Vektor-Operationen meinetwegen drin habe.
Das Ganze wird dann eben zu dem Programm gelingt.
Dabei werden dann eben die Verweise auf diese und diese Bibliothek eben entsprechend hinzugefügt.
Das ist dann eben das, was mit dieser Kommandozeile passieren würde.
Und wenn das Ganze dann eben zur Ausführung kommt,
nicht dann wird im Prinzip während der Ausführung oder bei der Ausführungszeit
dann eben wirklich aus diesen Bibliotheken,
die irgendwo schon vorher im Speicher liegen oder durch das Betriebssystem
damit reingeladen werden,
dann eben der entsprechende Code
mit ausgeführt
So, damit habe ich jetzt mit zwei Minuten über zehn das Kapitel Assembler-Programmierungsweit fertig.
Hier stehen noch mal drauf, was ich hier alles nicht behandelt habe und es gibt also auch noch beliebig viel mehr, was dann eigentlich auch noch fehlt. Wie gesagt,
also das, worum es hier jetzt erstmal nur gehen sollte,
ist ein Grundverständnis, wie kommen Programme zur Ausführung,
wie sieht das auf und dass da eben ein Maschinencode aus,
wie werden Datenstrukturen abgespeichert und alles,
was dann also da noch wirklich darüber hinaus geht,
Kann ich immer nur sagen, folgt,
wenn man sich entsprechend spezialisiert, dann entweder später im Studium oder ja,
ein paar Sachen sind leider aus Zeitgründen bei uns im Studium,
also jetzt eigentlich auch gar nicht mehr so richtig drin,
aber das ist dann das generelle Problem der Informatik,
nicht dass sich dadurch der Stoff oder der Inhalt so schnell so viel weiterentwickelt,
dass wenn man sagt, naja gut, also alles,
was man heutzutage eigentlich theoretisch wissen können sollte,
wenn man das also wirklich sinnvoll vertieft,
nicht dann würde also ein Bachelorstudium,
also schon irgendwie sechs Jahre dauern und das ist dann also auch nicht zwingend Sinn der Sache.
Gut, damit Schluss für heute,
ein schönes Wochenende und nächste Woche
geht es dann weiter