Gut, mit entsprechender Verspätung hoffe ich dann, dass wir trotzdem noch mal ein bisschen Inhalte bringen kann.
Also, ich hatte ja letzte Woche im Prinzip aufgehört,
also bei den Beispielen oder bei der Vorstellung der verschiedenen Adressierungsarten von Rechnern,
wo ich dann jetzt eben als Beispiel jetzt eben diese unterschiedlichen Nulladress bis drei Adressenmaschinen vorgestellt habe.
Dazu noch nur noch mal ein Hinweis, ein,
findiger Student hat also festgestellt,
dass da in Folien noch einen entsprechenden Fehler gab und zwar bei diesem Beispiel,
wo ich also hier diese Null-Adressmaschinen beschrieben hatte,
da war zumindest also an dieser Stelle hier dann noch ein Fehler drin,
das ist inzwischen korrigiert,
auf dem Web-Server ist so eine neue Version,
ist wie gesagt sonst nicht weiter relevant,
aber wenn man sich das jetzt also quasi ansieht und dann eben als Vorlage für die entsprechende Übungsaufgabe nimmt,
ist natürlich ärgerlich und sagen wir sowas soll da nicht sein,
deswegen ist inzwischen also geändert,
also auf diese unter vorhergehenden Folie
Gut, am Ende dieses Kapitels gibt es jetzt also noch
einige Beschreibungen oder einige Folien zur Intel-Architektur, beziehungsweise zu dem entsprechenden Befehlssatz.
Aus Zeitgründen werde ich da jetzt also relativ schnell rübergehen,
beziehungsweise ein paar Folien überspringen,
weil das sagen wir jetzt also auch nicht weiter relevant ist,
beziehungsweise im nächsten Kapitel,
wo es dann also wirklich um die X86 Assembler-Programmierung geht,
werden also dann die entsprechend relevanten Schale,
die sie dann auch für Übungsaufgaben Weise dann eben vielleicht auch für die Klausur brauchen,
dann also nochmal im Einzelnen vorgestellt.
Hier sind jetzt eigentlich nur so einige Allgemeine einleitende Sachen.
Deswegen werde ich da jetzt also relativ schnell rübergehen. Wie gesagt,
X686 Architektur ist im Prinzip ein Sisk-Rechner, historisch gewachsen mit entsprechenden Erweiterungen.
Das heißt also, da kamen dann irgendwann später diese Multimedia-Extensions hinzu,
dann als Erweiterung von dem Multimedia dann die SSE-Befehlsätze und so weiter und so fort.
Inzwischen hat man eben eine 64-Bit-Architektur die dann,
aber im Prinzip auch immer noch als 32-Bit,
beziehungsweise mit kleineren Befehlsätzen, programmiert werden kann auf Assembler-Ebene.
Ich also hier nochmal eine Auflistung der entsprechenden Beispiele, was dann alles da jetzt hinzugekommen ist, werde ich aber jetzt mal einzeln nicht weiter vorstellen. Speichermodell,
das hatte ich ja auch schon mehrfach gesagt,
beziehungsweise das war ja dann also auch in der einen Übungsaufgabe,
die jetzt also quasi über Weihnachten und Neujahr drin war,
auch noch dort zu sehen,
ist im Prinzip auf der Intel-Architektur Little Andien, das heißt,
Wenn ich ein 32 Bitwort aus dem Speicher raushole,
also sprich ein ganzes Wort, wie jetzt er sich hier oben angedeutet,
zählen die Bytes mit der Adresse 0, 1,
2, 3, oder sind sie entsprechend so sortiert,
dass an der niedrigsten Adresse dann also quasi das LSB des entsprechenden Befehlswortes liegt.
Gut, der Speicher selber ist im Prinzip weit adressierbar,
hatte ich ja auch
schon mehrfach vorgestellt
Je nachdem, mit welchen Datentypen man dort arbeitet,
werden dann aber eben entsprechende Worte, Double Word,
Crowdword, was auch immer da entsprechend rausgeholt.
Hier auf dieser Folie auch nur kurz gezeigt, weil das,
was dann nachher davon relevant ist, wird dann eben auch nochmal genau vorgestellt.
Sind also jetzt nochmal die X86-Register zu sehen. Einmal 8,
Fließkomma-Register, jeweils 80 Bitboyd. Die werden wir jetzt aber,
sagen wir jetzt, nicht weiter betrachten, beziehungsweise in den Beispielen würde ich jetzt also nichts vorstellen.
Und dann eben hier jetzt erstmal als 32-Bit-Beispiele, also jetzt die 32-Bit-Breitenregister. Und hier sieht man eben,
dass also einige von diesen Registern,
weil sie jetzt eben auch gerade historisch eigentlich noch von den alten Chips oder beziehungsweise von den alten Intelship-Sätzen übergeblieben sind,
also das, was jetzt hier so mit hellgrau markiert ist,
entspricht dann quasi in allerersten 80, 86 Prozessor,
sind dann eben entsprechend teilweise byte adressierbar,
teilweise dann quasi als 16-Bit-Register adressierbar.
Das sind dann also hier diese Bezeichner und das,
was also nachher in den Beispielen kommt,
beziehungsweise was sie dann also auch weiter vorstellen werde,
ist dann also quasi diese 32-Bit-Adressierung,
wo dann also letztendlich diese Bezeichner gelten.
So, das sind dann eigentlich auch die durch den Benutzer direkt ansprechbaren Register.
Nicht zusätzlich gibt es dann eben noch diverse andere Register,
die dann also beispielsweise jetzt vom Betriebssystem aus benutzt werden,
beziehungsweise dann für die Prozessverwaltung genutzt werden
Gut, alles Weitere folgt dann eigentlich dort bei den entsprechenden Beispielen.
Hier auch nur noch mal kurz als Überblick als Referenz aus dem Intel Programmer Manual quasi rauskopiert,
was dann eben auch noch mit interessant ist,
was man also beispielsweise dann für bedingte Sprünge braucht, beziehungsweise für entsprechende Programmabfragen.
Es gibt dann eben auch ein Flag-Register,
was dann eben auch hier explizit durch bestimmte Befehle gesetzt wird,
beziehungsweise von anderen Befehlen abgefragt wird und in diesem 32-Bit-breiten Register sieht man,
sie sind dann also hier einige Sachen noch nicht weiter benutzt.
Viele andere sind dann eben diverse spezielle Flex und das,
was uns, sagen wir mal,
als Programmierer dann eben vielleicht noch interessiert,
sind dann eben sowas wie Overflow,
Sign,
Zero oder eben Carrie,
werde ich dann also nachher im Einzelnen
auch noch mit den entsprechenden Beispielen vorstellen
Gut, hier nur noch mal kurz aufgelistet, also die Datentypen,
die, also die X686-Prozessoren kennen, wie gesagt,
wer sich das dann also noch mal genauer ansehen will,
beziehungsweise dann eben auch die einzelnen Befehle ansehen will,
da dann eben eigentlich nur die entsprechende Referenz,
also hier springe ich mal schnell drüber,
wäre dann also im Prinzip,
dass Intel Architecture Software Developers manual,
was man also im Prinzip dann direkt bei Ende auf den entsprechenden Seiten runterladen kann,
wenn man sich da also jetzt zu der Assembler-Programmierung beziehungsweise dann zu den einzelnen Eigenheiten dann nochmal genau was ansehen will.
Gut, das, womit ich jetzt eigentlich weitermachen will, ist dann eben Aufbauen darauf.
Im Prinzip nochmal eben jetzt wirklich eine genauere Vorstellung wie ein Assembler-Programm abgearbeitet wird.
Ich hatte ja schon auf den Folien vorher vorgestellt, Schreibfehler,
dass Kompilierungssystem wo dann im Prinzip ausgehend von einem C-Programm,
das dann über entsprechend mehrere Schritte dann eben wirklich bis zum Ausführbaren.
Code umgesetzt wird. Nicht hier, also die entsprechende Folie,
wo man dann eben wirklich sieht, wie, also beispielsweise jetzt C-Programm hier Ende.
Assembler-Code, dann eben in entsprechenden Binärworte umgesetzt wird, die dann eben letztendlich nachher auch von der Hardware ausgeführt werden. So,
jetzt kann man sich natürlich fragen, gut,
warum muss man sich heute jetzt überhaupt noch mit Assemblerprogrammierung beschäftigen,
beziehungsweise rumärgern, weil letztendlich ist es natürlich schon so und das denken wir,
wenn sie also auch im Studium bislang so erlebt haben,
wird man natürlich jetzt nicht direkt irgendwie direkt Assembler programmieren,
sondern sinnigerweise irgendeine vernünftige Hochsprache verwenden und hat dann eben entsprechend seine Compiler beziehungsweise ein Entwicklungswerkzeug,
die das dann eben wirklich auf Maschinencode übersetzen und ausführen.
In der Regel sind diese Programme eben auch so gut,
dass Und das, was dort dann also wirklich an Assembler-Code rauskommt,
also auch schon so viel an entsprechenden Optimierungen und gerade eben auch an Prozessor,
spezifischen Optimierungen enthält, beziehungsweise dort entsprechend über Compiler-Switches eingebaut werden kann,
dass da der Code also in der doch deutlich besser ist als handkodierte Assembler,
es sei denn, man ist jetzt also wirklich entsprechender Spezialist.
Nichtsdestotrotz braucht man da doch noch immer die Information einmal,
und das ist das, worum es mir jetzt hier darum geht,
dass man eben nochmal wirklich sieht,
wie wird jetzt eben wirklich Programmcode durch irgendwie drunter liegender Hardware abgearbeitet.
Zum anderen ist es aber eben auch so,
dass bei Fehlern, die im Programm auftreten,
beziehungsweise wenn man dann eben wirklich mit einem D-Burger da mal entsprechend genauer reingehen muss,
ein gewisses Grundwissen um die Abarbeitung, um die Assembler-Ebene da also nicht schadet.
Zum anderen, wenn man sich jetzt eben wirklich überlegt,
wie bestimmte Hochsprachenkonstrukte beziehungsweise wie bestimmte Programmstrukturen, also gerade auch Datenstrukturen letztendlich im Speicher abgelegt sind und durch das Programm abgearbeitet werden. Kann man damit dann eben auch relativ häufig,
also irgendwelchen Problemen auf den Zahn fühlen,
wo es also wirklich darum geht, dass also irgendwelche Programme ineffizient arbeiten,
weil man also beispielsweise jetzt also in seinen Datenstrukturen bzw. in dem Code,
der dann darauf arbeitet, jetzt eben irgendwelche beispielsweise Speicherzugriffe hat,
die also relativ in effizient sich umsetzen lassen, beziehungsweise dazu führen,
dass sie dann also langsam durch die Maschine nur entsprechend abgearbeitet werden können,
sodass wenn man jetzt also wirklich daran geht, entsprechend weit zu optimieren,
man da dann eben schon ein bisschen nachsehen sollte.
Gut, die andere Sache,
wo man dann also auch wirklich direkt noch Assembler-Code braucht,
ist natürlich, wenn man jetzt einen Compiler entwickelt,
weil da muss man sich natürlich jetzt irgendwie überlegen,
wie kann ich jetzt aus meinem hochsprachigen Code entrechnen,
Assembler-Befehle für einen Prozessor XY erzeugen,
beziehungsweise wenn man dann eben wirklich auf unterster Ebene So war es wie Gerätetreiber schreibt,
wo man dann also wirklich direkt das auch irgendwelche Hardware noch ansprechen muss,
beziehungsweise eben auch mit speziellen Modi dann eben auf Betriebssystemeebene arbeiten muss.
Gut, das, was der Programmierer im Prinzip vom Assembler eigentlich sieht,
also jetzt für eine X-86-Architektur, ist,
dass man sagt, ich habe hier meine CPU,
ich habe hier einen Programmzähler, einen Satz von Registern,
irgendwelche Condition-Codes, also im Prinzip das,
was in diesem Fleckregister letztendlich drinsteht und ich habe ihm einen großen Speicherbereich,
in dem sich eben mein Code für irgendwelche Datenstrukturen befindet.
Mein Programm selber und eben noch Sachen,
die also vom Betriebssystem bereitgestellt werden, beziehungsweise vom Betriebssystem verwaltet werden und das,
was da dann eben jetzt noch interessant ist,
was ich dann also jetzt auch in den Beispielen immer noch im Einzelnen behandeln werde,
ist dann im Prinzip der Stack.
Das heißt, der Mechanismus, den man braucht,
um jetzt eben Unterprogramme aufzurufen für dieses Unterprogramm entsprechende Parameter irgendwie zu übergeben,
wie man letztendlich das,
was man also tagtäglich jetzt bei der vernünftigen Programmierung ja irgendwo einsetzt,
wie das dann also durch irgendwelche Betriebssystemmmittel dann also wirklich auf der Hardware-Ebene auf dem Prozessor abgearbeitet werden kann.
Gut, das, was man da dann eben letztendlich sieht,
Programmzähler, also sprich die Adresse der nächsten Anweisung,
ist dann eben ein ausgewähltes Register, dann eben sowas wie eine Registerbank,
wo ich dann also jetzt direkt meine Register auswählen kann und eben,
um darauf zu rechnen, also häufig benutzt die Programmdaten, hinterlege.
Und dann eben die Flag-Register,
wo ich dann eben diese Zustandscodes drin habe,
wo dann also im Prinzip von arathmetischen Operationen Statusinformationen abgelegt werden,
beziehungsweise was ich dann eben bei bedingten Sprüngen abfragen kann.
Gut, der Speicher selber, beidweise adressiert,
indem dann eben die entsprechenden Daten drin liegen und dann eben
der Stack, den ich dann eben für Programmaufrufe brauche
Gut, Übersetzung, Übersetzungshierarchie hatte ja schon mehrfach gebracht.
Hier nur noch mal die entsprechende Folie,
also jetzt von den Unterlagen von Brian und so, Heilerin.
Das war auch das, was dann also in der einen Übungsaufgabe dann mal gefragt war.
Das heißt, ich habe hier irgendwo mein C-Programm,
kann das also mit einem Compiler in Assembler-Code übersetzen.
Das ist dann im Prinzip auch erst noch mal lesbarer. Text,
die man sich ansehen kann, jetzt mit einem ganz normalen Text,
Editor anschließen würde, dann eben der eigentliche Assembler,
das dann eben umsetzen und jetzt irgendwie entsprechenden Binär-Code,
also sprich eine Folge aus Nun und Einsen.
Und für ein ausführbares Programm würde ich das in der Regel natürlich dann,
also jetzt nicht nur meinen Code in einer einzigen Datei,
sondern irgendwie ein bisschen modular entwickeln, habe ich dann eben jetzt mehrere solche Module,
die dann letztendlich noch zusammengelingt werden müssen.
Und bei diesem linken kann es dann eben auch noch sein,
dass ich dann eben auch nur irgendwelchen Code für irgendwelche Programmbibliotheken,
wo ich dann also jetzt irgendwelche Funktionen mir aus diesen Bibliotheken hole,
die ich dann nicht selber schreiben will und muss,
dass das
dann auch hinzugelingt wird
Gut,
wenn man sich jetzt ansieht,
was bei dieser Kompilation von C-Code zu Assembler-Code passiert,
also hier hätte ich jetzt also irgendwo eine Funktion Summe mit zwei Indige-Argumenten,
liefert eine Indige zurück,
nämlich genau die Summe von X plus Y,
wenn ich das entsprechend übersetze,
würde da dann eben entsprechend jetzt also irgendwie sowas
wie so ein vergleichbarer Assembler-Code bei rauskommen
Ist dadurch gecancelt, dass er also hier erstmal so ein Label hat,
was dann im Großen und Ganzen hier dem Funktionsnamen entspricht und
dann eben hier diverse Assembler-Befehle, was sich dahinter verbirgt,
wie die im Einzelnen funktionieren.
Nicht warum hier jetzt also irgendwelche Push und Pop-Stack-Operationen stattfinden,
werde ich dann also gleich an den einzelnen Beispielen letztendlich noch genauer bezeichnen.
Im Prinzip geht es darum,
also einmal diesen kurz zu initialisieren,
sowas wie die Parameter-Übergabe zu machen,
das ist im Prinzip dann
genau dieser Teil hier
Nicht hier ist dann die eigentliche arrethmetische Operation, nämlich das Adieren.
Und hiermit wird dann quasi dieser Unterprogrammaufruf,
der ja quasi letztendlich hier dahinter liegt, dann wieder beendet.
Wie gesagt, im Einzelnen wird das dann gleich noch genauer vorgestellt. Gut,
was man hier aber an diesem Beispiel schon mal sieht,
charakteristisch von Assesbler-Code ist eben,
dass ich als Datentypen eigentlich nur ganz teilige Daten habe,
das heißt eins,
zwei oder vier
Bald jetzt in diesem Beispiel, beziehungsweise in diesem Beispiel,
die ich insgesamt jetzt also diese Woche vorstellen werde, mich und ihm
entsprechend große oder kleine Gleitkommadaten. Dazu haben wir jetzt aber hier Auszeit von dann eigentlich auch keine weiteren Beispiele mehr. Was dann eben auch noch entscheidend ist,
alles, was man eigentlich so in Hochsprachenprogrammierung dann eben hat,
sprich irgendwelche Datenstrukturen,
quasi in Äquivalenz zu irgendwelchen Records oder eben sowas wie Arays,
gibt es auf Assembler-Ebene nicht, sondern Alles,
was ich dort dann dahinter verbirgt,
wird letztendlich durch den Compiler entsprechend aufgelöst,
sodass also irgendwelche der Referenzierung von Pointern beziehungsweise also Ray-Zugriffe oder eben bei entsprechenden Datenstrukturen dort dann passieren,
weil auf Assembler-Ebene habe ich wirklich nur Fordler,
auf den adressierbare Beit.
Gut, was man dann an diesem Beispiel auch schon gesehen hat,
es gibt eben nur entsprechend einfache, primitive Operationen,
das sind eben im Prinzip sowas wie irgendwelche Alu-Funktionen,
die entweder auf Registern oder auch direkt auf dem Speicheradressen arbeiten.
Wie gesagt, X86 ist eine SISK-Maschine. Das heißt,
ich kann ihm bei arathmetischen Operationen auch auf irgendwelche Speicheradressen
direkt drauf zugreifen. Dann hat man eben,
typischerweise aber auch noch Datentransfer eben zwischen dem Speicher und irgendwelchen Registern,
wo ich dann eben Sachen,
die ich häufiger benutze oder mit denen man häufiger rechnet,
dann eben direkt in Registerlade,
beziehungsweise dann eben
auch wieder Sachen
Zurückbringe. Gut, und dann braucht man eben noch entsprechend mehr Befehle,
um jetzt den Programmablauf zu steuern, also sprich Sprünge beziehungsweise das,
was man eben braucht, um unter Programmaufrufe auszuführen.
So für so ein Beispiel, wie jetzt also diese sehr einfache Summenfunktion,
würde dann also ein Objektcode letztendlich so aussehen,
dass das, was also jetzt hier diese Assembler-Code ist,
also jetzt bei Jahr 32 Architektur in 13 weit umgesetzt wird.
Wie gesagt, der Befehlssatz der X86-Prozessoren ist ja,
sagen wir mal, sehr inhomogen, also jetzt anders als bei Rissgerechnern,
wo alle Befehle genau ein Maschinenwort lang sind.
Und hier gibt es eben Befehle, die nur ein oder zwei bald lang sind, aber eben auch welche, die entsprechend länger sind. Und hier,
das wäre dann eben jetzt so eine Form der Folge der entsprechenden Wörter,
die dann eben diesem Code entsprechen, entscheiden,
ist da dann eigentlich nur oder nachher für die Ausführung,
dass man eben einfach nur sagt, okay,
die beginnen eben ab einer bestimmten Speicheradresse und nicht,
die entspricht dann quasi diesem,
was also vorher in einem lesbaren Assembler-Code noch dieses Label war und dieses hier ist dann eben einfach jetzt nur kodiert in dem entsprechenden Assembler-Code,
dann das, was quasi diesen Anweisungen entspricht.
Die Schritte dazu, hier nochmal explizit beschrieben,
also der Assembler übersetzt, dass jetzt quasi diesen Assembler-Code zur Objektcode,
wo dann im Prinzip jetzt eben jede dieser Anweisungen einfach nur
eins zu eins irgendwie benekodiert werden Damit hat man dann eben letztendlich schon ein fast vollständiges Bild des ausführbaren Kurz.
Was jetzt nur noch fehlt, ist,
dass ich in der Regel nicht quote, jetzt in der während der Feinde habe.
Das, was der Linker dann macht,
dass der dann eben mehrere dieser Objektdateien eben zusammennimmt, gegenseitige Referenzen,
das heißt Programmaufrufe beziehungsweise unter Programmaufrufe entsprechend auflöst und zusätzlich dann eben noch Funktionen aus irgendwelchen Bibliotheken,
die mir dann also letztendlich das Betriebssystem bereitstellt,
mit hinzu, Linked, sofern diese statisch gelenkt sind.
Das heißt, da kommt das direkt in den Code mit rein. In der Regel ist es natürlich so,
dadurch, dass, sag mal, sowas wie irgendwie Ausgabeanweisung,
also sowas wie eine Printfunktion, natürlich von allen möglichen Programmen,
beziehungsweise laufenden Prozessen benutzt wird, das heißt,
sowas wird man dann also eigentlich nicht statisch hinzulenken,
sondern das wird dann dynamisch verknüpft,
das heißt,
diese Bibliotheken oder der entsprechende Code liegt dann also Hauptspeicher und wird dann also zur Laufzeit des Programmes dann entsprechend mit eingebunden,
beziehungsweise die entsprechenden Funktionalitäten dann angesprochen.
Gut, wie sieht da so ein Beispiel dann also für
eine Umsetzung jetzt, also in Maschinenfehler aus? Wenn ich jetzt,
also hier mal einen C-Code nehme,
erstmal jetzt nur diese Zuweisung, TS gleich X plus Y,
dafür habe ich dann also als Assembler-Befehl beispielsweise was wie so ein Ad Long,
wo ich dann im Prinzip zwei Zahlen oder zwei Adressen addiere und das Ergebnis landet dann eben auch hier in einer Adresse.
Das heißt, also dieses Adlong ist im Prinzip ein,
zwei Adressbefehl, wo dann hier quasi als Operanten benutzt werden,
der eine Operant ist in einem Register,
der andere Operant wird in diesem Beispiel jetzt,
also hier von einer bestimmten Speicheradresse geladen, zusätzlich noch mit einem Offset, warum das so ist.
Wie gesagt, dazu komme ich dann,
wenn ich dann nochmal die Funktionsaufrufe vorstelle,
weil im Prinzip ist dieses jetzt hier so ein Argument,
was, wenn ich hier zurückspringe,
also ich bin jetzt hier bei dieser Zeile,
was ich dann im Prinzip jetzt hier auf ein Parameter dieser Funktion bezieht und deswegen wird da so eine etwas umständliche,
ein umständlicher Zugriff jetzt über so eine Speicheradresse gemacht.
Letztendlich wird aber im Prinzip mit dieser Funktion etwas gerechnet,
wo ich dann eben sage, X und das,
was hier mit der referenziert wird,
Das heißt, da habe ich dann eben meinen Eingangsregister,
mein Ergebnis und das Ergebnis der gesamten Funktion ist dann letztendlich auch dieses.
Das heißt, dieses Wert, die Zuordnung zu dem C-Code. So,
und übersetzt nach dem Assemblerlauf hätte man dann eben irgendwo als Objektcode irgendwo ein,
drei Byte-Befehl,
der dann also letztendlich sowas ist,
was dann eben irgendwann an einer bestimmten Speicheradresse dann im Hauptspeicher
abgelegt wird und
dann abgearbeitet wird
So, was sie in den,
ein Beispiel dann ja auch schon gesehen hatten,
war dann im Prinzip so ein bisschen die Benutzung des Dis-Assemblers,
wo man dann im Prinzip aus dem Objektcode oder aus diesem Objektdateien rückwärts übersetzt und dann eben quasi aus diesen entsprechenden Bike-Folgen,
die dort hinterlegt sind,
versucht dann eben die entsprechenden Assembler-Befehle wieder rückwärts herauszuholen,
bis zum Beispiel lesbar zu machen,
was in dem Podcast für diesen Befehl Object-Dump-D,
wenn es bei den GCC-Compilern passiert,
wo man sich dann eben wirklich die Quote dann auch mal entsprechend ansehen kann.
Das Ganze kann man eben sowohl auf dem ausführbaren Programm passieren,
kann aber eben auch wirklich auf einzelnen Dateien passieren,
die jetzt noch nicht zum Programm zusammengelenkt worden sind.
Alternativ hier auch noch mal kurz angesprochen, man kann diese Funktionalität,
dieses Disressembling auch mit einem Debugger erreichen,
indem ich also jetzt quasi das Programm jetzt mit einem Debugger starte und dann eben eine Kommandozeile eingebe,
wo ich dann eben sage, this assemble, diese Funktion.
Und dann kriegt man dann eben den entsprechenden Code zu dieser Funktion mit angezeigt.
Gut, was kann alles disassembliert werden, hatte ich eben schon gesagt.
Also letztendlich alles, was man als ausführbaren Code interpretieren kann,
wo dann letztendlich eigentlich nur eine 1 zu 1 Rückübersetzung passiert von dem entsprechenden Bits drinkst,
die da im Speicher liegen, zu diesen Bezeichnern.
So, jetzt sind dahinten zwei Fragen. Genau,
also die Frage war jetzt, also warum,
hier habe ich eben einmal diesen Ausdruck jetzt aus meinem C-Programm und hier steht jetzt also irgendwie,
also in der Folie von Brandon Rohellerin und City Miller to Expression X plus gleich Y.
So, das ist, wenn man das jetzt als Programmierer sieht,
da haben sie natürlich völlig recht, Natürlich war es völlig unterschiedliches,
weil, wie gesagt, dieses ist ein,
wenn ich jetzt also auf die Terminologie von letzter Woche zurückgehe,
dieses ist ein Drei-Adress-Befehl, dieses ist ein Zwei-Adress-Befehl.
Dieses ist aber das, was also der Compiler aus diesem C-Code
heraus macht, weil im X86-Befehlsatz gibt es keine drei Adressbefehle.
Und das, was dann passiert, ist eigentlich immer so was.
Und es wird da eben nur schon explizit benutzt, dass man sagt,
ich hole mir jetzt hier diese Sachen von diesem Programmaufruf quasi vom Stack.
Das heißt, da wird nichts in der Aufruf oder aufrufenden
Funktion überschrieben. Und ich weiß dann eben auch schon,
in diesem Register habe ich dann meinen Rückgabewert nachher.
Und deswegen kann ich das jetzt benutzen,
um da quasi was reinzutun,
das nachher dann mit der Fuß bei der Benutzung,
aber jetzt selber
wieder zu überschreiben
Das ist aber jetzt wirklich immer das, was das Compader pro,
oder, ja, also was der Compiler dann also wirklich daraus macht,
wie jetzt also irgendwelche Hochsprachen, kurze Assembler-Code umgesetzt wird.
Wenn ich jetzt irgendwo eine Datenabhängigkeit hätte,
wo in meinem C-Code quasi hinterher X und Y noch weiter benutzt werden würden,
dann müsste der Compiler das entsprechend anders machen und das also
erst nochmal temporär irgendwo anders hin kopieren und dann damit rechnen,
weil natürlich, also einfach aufgrund der Art und Weise,
welche Befehle es dort gibt,
eben immer dieses Überschreiben stattfindet
Gut, im folgenden beziehungsweise ich hoffentlich dann also bis zum Ende dieser Woche
werde ich jetzt also noch mal ein bisschen genauer auf die Assembler-Programmierung eingehen,
also sprich die Adressierungsarten vorstellen,
die arathmetischen Operationen beziehungsweise die entsprechend einfachen arathmetischen Operationen,
das was wir jetzt hier brauchen und dann eben auch zeigen,
wie man also jetzt beispielsweise was wie Sprünge,
Programmaufrufe abarbeitet,
beziehungsweise wie dann eben auch entsprechende Datenstrukturen umgesetzt werden
Bislang hatte ich hier immer irgendwie entsprechende Referenzen,
also auch bei diesen Grafiken ist das so und bei allem war es jetzt also Nachfolgen kommt,
die sind eben direkt aus dem Buch von Brian und Hellerin, was sich ja also quasi so als Primärliteratur immer beschrieben habe, direkt herausgenommen.
Gut, jetzt kommen wir mal so ein bisschen auf die einzelnen Befehle zu sprechen. Das allererste oder das Einfachste,
was man jetzt eigentlich so bräuchte bzw. kann man sich mal vorstellen,
ist was wie ein Move vor einem Cobby-Befehl,
also jetzt bei diesem GCC, Assembler beziehungsweise bei den entsprechenden Assembler-Befehlen,
wäre das dann also was wie so ein Move L,
also Move Long, wie man also hier an diesem Suffix sieht,
es gibt dann also auch noch andere Formen,
die dann eben auf anderen Datenstrukturen beziehungsweise entsprechenden Argumenten arbeiten,
werde ich jetzt aber hier nicht weiter vorstellen,
da gibt es zwei Argumente, Quelle und Ziel,
wo dann eben wirklich jetzt ein Vierbyte Wort entsprechend einfach von A nach B bewegt wird,
ist, wenn man sich den Assembler-Code jetzt quasi ansieht und da eben dann auch die Häufigkeiten untersucht,
also mit einer der häufigsten Funktionen, weil das,
was man also hier an Quelle und Ziel angeben kann,
das ist dann gleich auf der nächsten Folie gezeigt,
sind das jetzt eben nicht nur Transfers zwischen Registern,
sondern beliebige Transfers, wo ich dann im Prinzip von Hauptspeicher im Register,
aber auch von Register,
in Hauptspeicher oder eben auch direkt mit Imediate-Werten arbeiten kann.
Hier rechts aufgelistet, auch dazu komme ich dann nachher gleich nochmal genauer,
sind also, sag mal, so die acht Register,
die man, sagen wir jetzt,
als normaler Programmierer im X86 Assemblercode direkt zugänglich hat,
beziehungsweise direkt ansprechen kann,
wobei von diesen acht Registern letztendlich auch nur sechs im Sinne wirklich frei verfügbar sind.
Und zwar sind dann also für die Behandlung von Unterprogrammaufrufen,
beziehungsweise der Verwaltung des Decks belegt,
werde ich ja beim Arzt ineinander noch genauer vorstellen.
So, bei so einem Move-Befehl,
wie gesagt, kopieren von Quelle nach Destination,
als Operant kann ich eben sowas wie eine, an Emediate-Wert haben.
Das heißt also jetzt wirklich eine beliebige Konstante ist im XX80-Assembler-Code quasi dann immer nur dadurch gekennzeichnet,
dass das Ganze mit einem Dollar beginnt,
kann also wie hier jetzt sowas wie eine Hexzahl sein,
aber eben auch ein ganz normaler Intager-Wert und wird dann eben entsprechend
Je nachdem, was für ein Move das
dann ist mit der passenden Anzahl von Byte, kodiert.
Die Operanten können eben jetzt irgendwelche von diesen acht Registern sein,
die dann eben einfach jetzt über entsprechende Bezeichner mit angegeben sind.
Es können aber eben auch Speicherzugriffe für Source oder für Destination letztendlich dastehen,
wobei es da dann eben auch unterschiedliche Adressmodi gibt.
Als Beispiel Ist das hier jetzt also mal so grob aufgezeichnet? Das heißt,
ich kann jetzt hier Zugriffe haben,
wo ich also einen Emediate-Wert in einem Register oder einen Speicher schreibe,
wo ich ihm einen Transfer von Registern,
ein anderes Register oder in den Speicher mache oder vom Speicher,
Wasser auslese und inneren Register schreibe.
Ich und hier wären dann immer entsprechende Beispiele.
Und was man jetzt hier einfach jetzt nur sieht,
wie gesagt, auf die einzelnen Avaresierungsmodi werde ich dann also
gleich nochmal genauer
Zu sprechen kommen, was man hier also sieht,
also mit dem Dollar vorneweg, das sind dann immer Emediate-Werte.
Das wäre dann eben jetzt direkt der Bezeichner eines Registers.
Und wenn irgendwas in Klammern steht,
ist das dann eben ein Speicherzugriff. Das heißt,
hiermit würde ich jetzt eben in die Speicheradresse,
die in dem Register eher X enthalten ist,
den Wert oder das entsprechende Äquivalent jetzt zu dem Dezimalwert von minus 147 reinschreiben.
Hier kopiere ich zwei Register.
Hier schreibe ich eben den Inhalt eines Registers in den Speicher
Beziehungsweise hier lese ich aus dem Speicher in ein Register. Gut,
diese Adressierung, haben wir ja eben schon gesagt,
wenn das jetzt also um Speicheradressierung geht, dadurch gekennzeichnet,
dass sich quasi immer diese runden Klammern drum habe und steht dann letztendlich dafür,
dass man hier drin ein Register angibt,
der Inhalt dieses Registers ist dann quasi die Speicheradresse,
auf die dann passend zugetreffen wird.
Zusätzlich gibt es da dann noch die Möglichkeit, vorneweg ein Offset anzugeben,
also hier jetzt im Englischen immer als Displacement bezeichnet,
wo ich dann quasi,
oder wo sich dann also diese Speicheradresse errechnet,
als der Inhalt dieses Registers plus diesem Wert,
der wir jetzt selber hier kann, dann eben auch negativ sein.
Das heißt, man kann dann entsprechend dann also auch irgendwas abziehen.
Und Angabe des Wertes hier wäre dann eben auch
ganz normal als Integer oder Hexadezimalzahl
Gut, als Beispiel für diese Adressions-Modi.
Wäre jetzt also hier ein Aufruf einer Funktion. Swap,
wo ich jetzt also im Prinzip ein Call bei Referenz mache.
Das heißt, ich übergebe hier jetzt nicht einen Wert,
sondern einen Zeiger auf einen Wert. Das heißt,
also ich habe jetzt hier letztendlich sowas wie Adressen oder Zeige
auf Inager und ich möchte im Prinzip diese beiden Argumente nachher vertauscht haben,
nicht und das, was man dann also im Seekot dafür beispielsweise schreiben könnte,
wäre dann eben, dass ich eben den Inhalt hier von Register schreibe,
dieses Jahr auch in einem Register schreibe und dann dementsprechend das Ganze dann wieder zurückschreibe.
So, bei dem Aufruf dieses Unterprogramms sieht man,
ist das Ganze hier jetzt eben schon so
ein bisschen unterteilt in drei typische Teile,
die sich also auch immer bei Unterprogrammen aufrufen
Auf Assembler-Ebene letztendlich finden. Einmal so ein Teil,
den man quasi als Initialisierung oder im Englischen dann eben Setup bezeichnen kann,
den man in dieser Form eigentlich bei allen Unterprogrammen aufrufen,
immer so eins zu eins findet.
Das ist einmal ein Push-Befehl auf ein Register,
das heißt,
das Register wird auf eins Deck geschrieben,
ein Kopieren des Stackpointers auf Innern in des dann neuen Stackpointers
und dieses Register und dann eben hier noch
ein Push eines anderen Registers auf den Stack
Warum man das braucht, beziehungsweise was das dann mit dem Stack
auf sich hat, werde ich dann also beim Unterprogrammaufruf,
beziehungsweise bei der Handhabung von Unterprogrammen nochmal genau zeigen.
Da geht es eben letztendlich darum, dass ich über
diesen Mechanismus des Stacks diese Argumente übergebe. Das heißt,
ein aufrufendes Programm legt die Argumente bzw. die Inhalte,
da hast du dann eben entsprechend auf den,
also in diesem Fall also dann die Adressen auf den Stack,
die hole ich mir dann also quasi hier hinterher,
wenn das Programm aufgerufen ist, irgendwann ab.
Nicht und das, was hier dann passiert,
ist dann eben diese Übergabe dessen des Kontrollflusses,
auf das Unterprogramm, wo dann im Prinzip jetzt also der Pointer,
der auf irgendeinen Bereich des Decks zeigt,
dann eben entsprechend gesichert wird.
Anschließend wird dann eben im Stack ein neuer Stackpointer aufgemacht und
dieses Programm selber kann dann quasi auf seinem Stack arbeiten,
ohne dass es jetzt irgendwelche Teile des Aufrufenden Kurs dabei dann überschreiben kann.
Dann kommt eben die eigentliche Berechnung und hinterher,
das ist quasi genau das
Äquivalent. Dazu wird alles, was hier gemacht wurde,
dann eben wieder rückgängig gemacht. Das heißt, das,
was ich hier mit diesem Pursche,
also wo ich den Stack jetzt verändert habe,
wird also hier diese alte Werte zurückgeholt.
Das ist genau dieses, worum da minus vier steht und so weiter.
Und wie gesagt, das kommt dann gleich in dem Beispiel
noch genauer raus. Und dann eben auch dies zurücksetzen,
also hier quasi das Äquivalent zu diesem Move und mit dem Pop,
das ist dann das Äquivalent zu diesem Push.
Und nach dem Return ist das Unterprogramm dann letztendlich fertig.
So,
das war es
Ich jetzt eigentlich nur motivieren wollt. Jetzt habe ich
also hier fünf Minuten lang über die Folie gesammelt.
Ist natürlich nur jetzt einfach dieser innere Teil,
weil es ging jetzt erstmal nur um den Move-Befehl.
Hier mache ich nichts anderes,
als dass ich jetzt die beiden Argumente,
die sich quasi jetzt hier als Offset zu dem Stack herauslesen lassen,
die beiden Argumente hier in zwei Register speiche.
Das heißt also, die Adresse von XP wird in ECX.
Nee, das ist das zweite Argument.
Also die Adresse von YP wird in ECX geladen,
die Adresse
Von XP wird in Edex geladen. Anschließend werden diese dann eben als Speicheradressen genommen.
Das heißt, da passiert dann eben genau die Referenzierung,
die ja dann also hier quasi über diese Pointe-Operator-NC angedeutet ist.
Das heißt also, da greife ich hierüber jetzt auf Speicherinhalte zu,
gebe das dann also in ein entsprechendes Hilfsregister bei dem zweiten hier auch.
Und hier werden dann eben die Inhalte dieser Register dann eben in
die entsprechend vertauschten oder unter dem entsprechen vertauschten Adressen dann wieder abgelegt.
Und hiermit das Ganze dann beendet. Das,
was ich jetzt eigentlich im Einzelnen immer noch vorstelle,
beziehungsweise was dann also auch in den Beispielen immer vorkommt.
Ich hatte eben vorhin schon gesagt, also die Adressierungsmodi,
da hat man eben normale Adressierungen in Speicher, beziehungsweise Displacement,
ganz allgemein gesprochen, sieht das dann immer so aus,
dass sich bei diesen Speicheradressierungen Nicht immer angedeutet,
durch die Runden klammern, bei diesen Speicheradressierungen eben sage,
ich habe Basisregister, also eins dieser acht Indiger-Register.
Und zusätzlich kann ich dann eben optional noch Index-Register und einen Skalierungsfaktor,
was dann also eins, zwei, vier oder acht sein können,
stehen haben und eben diesen zusätzlichen Offset,
also dieses Displacement und die ganz allgemeine Form Zoom-Zugriff wäre dann eben Speicherzugriff auf eine Adresse,
die sich dann eben dadurch berechnet,
dass ich dem den Inhalt dieses Basisregister nehme,
plus den Inhalt des Index-Registers mit diesem Faktor es skaliert,
plus diesem Offset.
Nicht das, was man, das ist der ganz allgemeine
Fall, das, was man meistens hat.
Deswegen jetzt hier geborsigste Fälle eben einfach nur direkt, also Inhalt eines Registers,
beziehungsweise dann mit so einem Displacement versehen oder eben irgendwas, wo ich dann also teilweise noch sowas wie ein Index-Register mitbenutze. Das wird eigentlich genau dann sinnvoll,
nicht also nach dem Motto,
warum brauche ich die überhaupt jetzt, also zwei komische Register,
mit denen ich da also irgendwelche Speicheradressen berechne,
wird eben genau dann sinnvoll,
wenn ich zum Beispiel auf solche Strukturen wie Arrays zugreifen möchte,
wo ich dann also im Prinzip in dem einen Register als Basis
den Verweis auf das Element null habe,
nicht und hier dann in dem anderen Register quasi immer raufzählen
kann und entsprechend der Größe der Datenstruktur dann eben quasi jetzt
hier mein Offset in diesem zweiten Register inkrementiere und dann mit
diesem Adressierungsmodus dann der Reihe nach auf die Elemente des Arrays
0123 und so weiter zugreifen könnte
So, hier nochmal praktisch dargestellt,
wie also jetzt wirklich so eine Adressberechnung aussieht,
beziehungsweise was dann also als Beispiel dabei passieren würde,
also wenn ich jetzt also in den Register
EDX 0X 1100 habe und in dem Register ECX 0X 0X 100,
nicht dann würde ihm als Ausdruck bei hier rauskommen,
Inhalt von EDX plus dem Off-Set von 8,
wäre dann eben die Adresse Hex F008.
In diesem Beispiel würden dann eben diese beiden oder die Inhalte
dieser beiden Register zusammengezählt und dann resultierende Adresse wäre dann eben F 100.
Hier dann mal ein Beispiel, wo dann dieser Skalierungsfaktor eingesetzt wird.
Das heißt, da wurde dann eben der Inhalt von
dem ECX mit 4 nochmal zusätzlich skaliert werden.
Ich hätte dann dies als resultierende Adresse,
in die es dann also jetzt hier für irgendeinen Code,
wo ich dann also im Prinzip jetzt den Inhalt eines Registers
nehme,
mit zwei skaliere
Und dann eben noch diesen Index dazureichende. Und das erste
Argument, das fehlt jetzt eben hier entsprechend dann leer.
Gut, also das jetzt nur als Beispiele,
wie jetzt also im X86 Assembler da die Adressberechnung aussieht.
Gut, was gibt es jetzt an arathmetischen Operationen, mit denen letztendlich jetzt,
also die eigentliche Funktion des Programms, also irgendwie beschrieben werden kann.
Einfache minäre Operationen, at Sub in der Jamultiplikation Dann sowas
wie ein Linksschieben, rechts schieben, sowohl arrithmetisch als auch logisch.
Und dann eben sowas wie ein Exklusiv-Oder und, und, oder.
Hier in dem Beispiel, beziehungsweise noch in allen folgenden Beispielen,
werde ich hier immer die Befehle aus dem GCC-Assembler benutzen.
Das heißt also immer in dieser Form AdL,
wo dann also beispielsweise das Ganze jetzt eben für einen 32-Bit Word steht,
wo dann das erste Argument, die Quelle, und das zweite Argument,
das Ziel des Befehls ist. Das Gemeine ist jetzt,
also das ist mir nämlich heute auch gerade passiert,
asy noch mal woanders, nachgeguckt hatte zum X86 Assembler,
das Gemeine ist, also je nachdem,
welchen Assembler man hat, beziehungsweise womit dieser Code erzeugt wurde,
der dann also quasi im zweiten Schritt erst in den Objektcode oder den Maschinencode übersetzt wird,
sind diese Bezeichner eben nicht einheitlich,
sondern man findet auch oft was,
wo also quasi gerade bei diesem Ad oder bei diesem binären Befehlen Quelle und Ziel vertauscht sind.
Das heißt, da ist es Ziel links und die Quelle
rechts. Also falls sie dann auf sowas stoßen,
also davon nicht verwirren lassen,
also hier beziehungsweise auch in den Beispielen,
die dann also noch weitergebracht werden,
ist das Ziel der Operation dann quasi mal das zweite Argument,
beziehungsweise das war es dann bei so einem Zwei-Adress-Befehl
entsprechend überschrieben wird
Und berechnet wird, also bei der Subtraktion ist es dann
natürlich interessant, immer Ziel, Minusquelle. Das heißt,
ich rechne dann eben immer genau diesen Ausdruck da letztendlich aus. Gut,
dann gibt es natürlich noch irgendwie sowas wie unnähere Operationen,
also Inkrement, Dekrement, beziehungsweise als Unäre arathmetische Operation,
eine Negation,
beziehungsweise als Unäre logische Operation dann eben noch wo dann also
im Prinzip das entsprechende Wort dann oder der entsprechende Inhalt dann eben bittweise invertiert wird.
Gut, als Beispiel für arathmetische Operation ist jetzt also hier eine Funktion.
Are, die also hier drei Argumente hat,
innen drin jetzt also irgendwie diverse Ausdrücke ausrechnet und dann
eben hier irgendwie immer ein Ergebnis zurück liefert.
Und beispielsweise würde jetzt eben ein Assembler-Programm aus diesem Code oder nein, Blödsinn.
Also ein Compiler aus diesem Code, quasi jetzt also diesen, dieses Assembler-Programm machen.
Was jetzt also eben auch wieder gekennzeichnet ist, also hier diese Initialisierung,
hier das Ende von diesem Unterprogrammaufruf und dieses hier innen drin entspricht jetzt also irgendwo der Berechnung dieser Ausdrücke. Wenn man das jetzt wirklich im Einzelnen analysiert,
tut man sich also mit der nächsten Folie etwas einfacher,
weil dann ist nämlich dort dann auch hinterlegt,
Was das also mit dieser Adressberechnung teilweise auf sich hat,
die hier stattfindet, wo ich dann eben sage,
wo ich jetzt welche Argumente vom Stack lade und wo dann eben auch beispielhaft beschrieben ist,
was also der Compiler aus dieser arathmetischen Operation,
wo ich also hier zum Beispiel sage, Y mal 48,
gemacht hat, wo dann also quasi diese konstante 48,
also hier nirgendwo im Code auftaucht, dieses Mal 48,
aber dann also relativ trickreich durch Addition beziehungsweise nachher dann durch Schieberoperationen erzeugt wird,
ähnlich dem was, also dann vor schon etwas längerer Zeit,
dann also auch meinen Übungsaufgaben gekommen ist,
wo es also um diese Schieberoperationen geht.
Wenn man sich jetzt also hier im Einzelnen jetzt mal ansieht,
mit diesen ersten beiden Move-Befehlen hole ich mir jetzt quasi vom Stack,
auf dem meine Argumente von der aufrufenden Funktion abgelegt worden sind,
Inhalte von X und Y und Lade,
die dann eben in das Register EAX beziehungsweise in das Register EDX.
Das sind also hier quasi jetzt zwei freie Register,
über die ich jetzt als Programmierer, wenn ich jetzt also
diesen Assembler-Code jetzt selber schreiben würde, also beliebig verfügen kann.
Entsprechend dieser Reihenfolge sind die Argumente jetzt hier auf dem Stack,
hier in umgekehrter Reihenfolge abgelegt. Das,
was das aufrufende Programm letztendlich sieht, nach dieser Initialisierungsphase ist,
dass es also quasi ein Zeiger gibt,
der in diesem EBP abgelegt wird,
was dann also quasi als Basepointer hier auf die letzte Stelle der Stacks quasi verweist.
Und dadurch, dass ich also jetzt den Inhalt von EBP nehme,
mit dem Offset 8, bin ich eben genau also entsprechend zwei Wörter weiter.
Das nächste Wort wäre jetzt also quasi die Rücksprungadresse,
also sprich der Programmzähler, der also dort gesichert worden ist.
Und dann habe ich dann eben Das erste Argument,
was mir also dann mein aufrufendes Programm dort hoffentlich hinterlegt hat.
Nicht, und das heißt, mit dem Inhalt von EBP,
mit dem Offset 8, so hatte ich dann dementsprechend dieses X hier vom Stack lesen.
Ich entspreche das mit dem Offset 4Byte weiter,
demzufolge dann eben auf Set 12,
hätte ich dann eben das nächste Wort, das Argument Y vom Stack gelesen.
Und hier unten, da würde ich jetzt also zum Beispiel mal
das Set benutzt und logischerweise sieht man da dann eben EBP mit Offset 16.
Dafür braucht man also diese komischen Adressierungsmodi, wo dann also beispielsweise jetzt mit diesen Offsets dann, also auch gerechnet wird. So,
dieses hier sind jetzt also im Prinzip Befehle, die eigentlich benutzt werden,
also dieses LEA-L, Load Effective Adress, Long,
sind eigentlich Befehle, die benutzt werden für die Adressberechnung,
die die Compiler, also ich muss ja zugeben,
das ist ja dann immer irgendwelcher Code, der und dem einen,
also jetzt irgendein mehr oder minder schlauer Compiler generiert hat,
die die Compiler aber immer ganz gerne verwenden,
um dann auch irgendwelche arathmetischen Ausdrücke zu bereichnen
Und das, was hier eigentlich nur passiert, ist,
dass also jetzt hier quasi kein Ad-Befehl genommen wird,
was im Prinzip genauso möglich wäre,
wo ich dann quasi den Inhalt von EDX und EAX addiere und das Ergebnis jetzt eben irgendwo abspeicher,
weil dieser Ad-Befehl hatte ich ja eben schon gezeigt,
beziehungsweise wurde er dann auch nachgefragt, er hatte nur zwei Adressen,
ich bräuchte aber irgendwie sowas wie drei Adressen,
wenn ich das würde, also jetzt hier trickreich dadurch gemacht,
dass sie einfach hier einen anderen Befehl benutzt,
beziehungsweise eigentlich dessen Adressberechnungsmechanismus,
um die Addition dieser beiden Inhalte zu machen und das Ergebnis dann eben in ein anderes Register zu speichern.
Das Ganze eben deswegen, damit ich mir natürlich X und Y
nicht überschreibe, weil ich die später irgendwann nochmal brauche.
So, hier wird dann eben auch dieser Befehl genommen,
um jetzt relativ trickreich, auch über diesen Adressierungsmodus,
dreimal Y zu berechnen.
Das ist dann einfach EDX mit diesem Skalierungsfaktor 2,
ist zweimal der Inhalt von EDX,
ein EDX war ja Y gespeichert,
plus EDX habe ich dreimal
Den Inhalt von EDX, also sprich dreimal Y. So,
dann gibt es hier ein Shift, Left, mit einem Offset 4.
So, und wenn ich jetzt dreimal Y habe,
mit einem Offset 4 nach links schiebe,
jedes Mal nach links schieben, ist eine Multiplikation mit zwei.
Also, dreimal zwei ist sechs, mal zwei ist zwölf, 24, 48.
Das heißt, mit diesen beiden Befehlen ist eigentlich relativ trickreich, die ist Y mal 48 implementiert. Sag mal,
das sind dann wirklich so die Tricks,
die eben optimierende Compiler einem da eigentlich einbauen,
die dann ihm genau wissen,
also ich habe jetzt irgendwie den Befehlssatz und dann vieles,
was man jetzt, sagen wir mal so,
als Programmierer nativ erstmal anders umsetzen würde,
aber eben letztendlich vielleicht auch deutlich umständlicher umsetzen würde,
eben so trickreich dann eben auf entsprechende Hardware-Ressourcen umsetzen können.
Gut,
also dass wir jetzt eben hier dieses 48 Y und gut,
hier entsprechend werden jetzt eben die anderen Teilausdrücke berechnet und dann
eben letztendlich dann auch nie nachher irgendwie sowas
wie dieses Ergebnis berechnet
Gut, also da ist jetzt eben nun mal so,
also zum Beispiel, wie dann also ein compiler-generierter Code aussieht, beziehungsweise was da dann eben teilweise noch so ein Tricks da rein mit passiert. Gut,
für logische Operationen, also hier auch nochmal ein kleines Beispiel,
wo dann also so einige von den logischen Operationen ausgeführt werden und also auch da sieht der Code dann eben letztendlich hier so aus,
dass ich also jetzt hier in das Register EAX,
also quasi den Inhalt von X, letztendlich lader eben auch,
also über diesen Offset von dem Stack heraus,
anschließend Soll er jetzt hier gerechnet werden, X,
exklusiv oder Y,
was dann eben mit dieser X-Off-Funktion und Y wird dann quasi C direkt vom Stack herausgelesen über diese Adressierung gerechnet wird.
Und dann soll jetzt hier noch die 2,
also das heißt die 1 nach rechts schieben, also aratmetisches Schieben,
das ist dann eben direkt dieser Befehl mit dem Offset Oder mit der Constanten 17.
Und dann habe ich hier zusätzlich dann noch eine Berechnung,
wo also im Prinzip jetzt im C-Code sowas steht,
wie, ich erzeuge mir jetzt also irgendeine Bitmaske,
das heißt also eine eins wird um 13 Stellen nach links geschoben.
Anschließend wird sieben davon abgezogen, das ist dann meine Bitmaske
und damit soll es also hier irgendwie der Inhalt,
der hier berechnet wurde, maskiert werden, wofür immer das auch gut sein mag.
Und was man hier dann eben auch sieht, ist,
dass also der Compiler im Prinzip diesen konstanten Ausdruck der ja
quasi zur Compile-Zeit dann letztendlich schon feststeht und ausgewertet werden kann, dann eben schon übersetzt hat.
Das heißt also, hier fängt man jetzt nicht an,
irgendwie eine konstante Einzelne, irgendein Register zu laden,
mit Schiebebefehlen, also das jetzt hier da rein und nach nachzubauen,
sondern das hat der Compiler schon quasi komplett aufgelöst in die konstante 810085,
die dann im Prinzip hier explizit dann einfach an den Code reinkompiliert wird.
Gut, das war jetzt Äh. Schnell vorgestellt. So
Beispiele für arathmetische und für logische Operationen. Dann würde ich
jetzt mal die letzte Viertelstunde noch ein bisschen was sagen zur Umsetzung von Kontrollfluss.
Wo es also darum geht, wie kann ich jetzt also
im Prinzip sowas wie irgendwelche Zustandscodes setzen bzw. wodurch werden die gesetzt?
Wie kann ich die abfragen? Und daraus natürlich abgeleitet.
Also wie kann ich jetzt also im Prinzip irgendwelche Verzweigungen in meinem Programm im sein,
dass also im Prinzip also nur einfache ist,
denn Els-Verzweigung oder eben auch irgendwie etwas kompliziertere Schleifen oder mehrfach Verzweigung.
Gut, das, was man, sagen wir mal,
als einfacher Programmierer aus diesem Statusregister letztendlich braucht,
sind eigentlich erstmal nur vier einzelne Bits.
Carry Fleck, sein Zero und Overflow.
Diese Bits können jetzt gesetzt werden. Zum einen implizit,
und zwar wird bei jeder atmetischen Operation, die ausgeführt wird,
Also wenn ich zum Beispiel jetzt so einen Ad Long habe,
werden für das Ergebnis dieser Berechnung die entsprechenden Bits gesetzt.
Das heißt, wenn ich bei dieser Addition meinen Zahlenbereich verlasse,
das heißt, ansigned Überlauf habe,
dann ist quasi das Carryflag, das Carry Out,
das entsprechenden Adiernetzes.
Zero-Flag würde ihm gesetzt werden,
wenn das Ergebnis gleich Null ist
Das Signfleck würde gesetzt werden, wenn das Ergebnis,
wenn ich das jetzt also als Signzahl interpretiere,
kleiner Null ist, ist bei der üblichen Zahlendarstellung,
die wir haben, natürlich relativ einfach.
Das ist nämlich einfach das höchstwertigste Bittes-Ergebnisses. Ich vor allem zweier Kompliment,
kleiner Null, muss das eben, oder ist das eins und alles,
was größer gleich Null ist, ist eben das höchstwertigste Bitt Null.
Und dieses Overflow-Fleck, das wird dann eben quasi über diese Bedingungen gesetzt.
Das heißt, wenn ich die Argument jetzt eben als Zweier-Kompliment interpretiere.
Und das eben bei der Berechnung überläuft, also sprich den Zahlenbereich verlässt.
Nicht was dann also letztendlich diesem adatometischen Ausdruck entspricht,
dass ich eben sage, beide Argumente sind größer null,
aber das Ergebnis ist kleiner null oder beide Argumente sind quasi kleiner null,
aber das Ergebnis ist dann eben größer gleich null und das wäre dann Indize für,
dass sich da dann eben ein Overflow jetzt für eine Zahlendarstellung hätte.
Gut, neben diesem Impliziten setzen durch alle arathmetischen Operationen,
gibt es dann eben noch Befehle im X86-Befehlsatz,
die dann eben diese Flex also auch explizit setzen.
Zum einen gibt es da so sowas wie Vergleichsoperationen,
also so ein Compare-Long mit zwei Argumenten,
entspricht eigentlich genau der Berechnung dieses Ausdrucks Source 1 minus Source 2, also sprich das,
was dann eben auch diese entsprechende so Fraktionsbefehlen machen würde.
Der einzige Unterschied ist beim Subfraktionsbefehl,
habe ich hinterher das Ergebnis in Source 1 stehen,
bei dieser Compare-Anweisung wären nur die Flex gesetzt,
aber die Inhalte dieser beiden Argumente würden dann eben nicht verändert werden.
Das heißt, da wird eben kein Resultat gespeichert. Gut,
und wie gesagt, die Berechnung Reflex beziehungsweise das, was dann letztendlich dahinter steht,
ist dann eigentlich genau das Gleiche wie bei dem entsprechenden Subfraktionsbefehl.
Da beziehe ich das jetzt dann auf das
Ergebnis, nicht und wenn ich das jetzt hier umsetze,
dann hätte ich dann eben quasi das Zero Flag gesetzt,
wenn die beiden Werte identisch sind.
Also logischerweise, wenn das Ergebnis so einer Subpraktion dann eben
gleich null wäre und so weiter und so fort. So,
dann gibt es für arathmetische Operationen quasi dann eben auch noch
ein explizites Setzen von Zero on Science durch die Anweisung Test Long,
entspricht dann nichts anderem,
als dass ich quasi sowas mache wie beim Endbefehl,
wo dann im Prinzip mitweise diese beiden Argumente verknüpft werden.
Das macht man typischerweise, dass man sagt,
zumindest eins von diesen Argumenten ist dann sowas wie eine Bitmaske.
Damit kann ich dann eben ausseuchen, ganzen Registerworten zum Beispiel,
dann eben wirklich einzelne Flex heraus abfragen.
Nicht, dass wir dann etwas,
was man mit so einer Testanweisung macht,
die setzen dann also letztendlich hin und die zwei Flex,
nämlich einmal Zero
Also sprich, alle Ergebnisse sind oder alle Bits sind null oder signed,
wenn dann im Prinzip ist also das höchstwertigste Bit und leicht null ist.
So, und dann gibt es jetzt eben von,
wenn ich jetzt diese Flex gesetzt habe durch eine dieser drei Mechanismen,
also sprich entweder durch die entsprechenden Operationen oder eben diese expliziten Anweisungen,
die Möglichkeit, die Inhalte dieser Flex über entsprechende Setanweisungen auszulesen.
Und das, was die Set-Anweisung macht,
ist, dass sie jetzt noch ein zusätzliches Argument hat,
dass es jetzt also hier noch nicht drauf abgebildet,
was dann eben entsprechend diesem Ergebnis, das heißt,
je nachdem, was hier abgefragt wird, dann eben gesetzt wird,
als null oder als eins.
Ja, und da kann ich dann dementsprechend abfragen,
Set, Equal Zero, not equal Zero,
negative,
non negative
Greater, Greater, all equal. Also je nachdem,
welche Flex dort gesetzt werden sind,
habe ich dann eben entsprechend die zugehörigen, logischen Verknüpfungen.
Ich und beispielsweise aus negativ und Equalizierung kann ich dann also eigentlich alle Kombinationen,
Negative, Non-Negative, Greater, Greator, Equal Less,
Less, Orequel, entsprechend passend ableihen.
Gut, zusätzlich hat er ja schon gesagt,
hat so eine Set-Anweisung, dann eben noch ein Argument,
dass es dann beim XX86 Assembler Co dann erst mal nur ein einzelnes Bite,
das heißt ein Operant, entweder Speicheradresse oder eben ein Teil dieses Registers.
Hier sieht man dann eben nochmal das Teil dieser Registern eben auch entsprechend beidweise, adressierbar sind.
Nicht daran eben auch nochmal unterschieden in Low und Highbyte.
Nicht anschließend hat man meistens noch irgendwie einen entsprechenden Befehl,
der dann dafür sorgt,
dass nicht nur jetzt also über die Set-Anweisung hier irgendwie ein Byte gesetzt wird,
sondern dass dann eben hoffentlich auch irgendwie der Rest des Registers,
der dann hiervon nicht berührt wird, also irgendwie zurückgesetzt oder irgendwie weiter gelöscht wird.
Nicht und als Beispiel so ein Funktionsaufruf,
wo ich also jetzt hier zwei Argumente habe,
X und Y und der soll mir eben zurückliefern,
X ist größer Y, würde dann eben übersetzt werden in, ich lade mir
Im das Argument Y jetzt eben hier in das Register EAX.
Anschließend habe ich eben so einen Komperbefehl. Das heißt,
damit wird jetzt eben dieses entsprechende Fleck erstmal gesetzt,
wo ich dann eben EAX mit,
also sprich Y mit X, also in diesem Argument dann Vergleiche,
anschließend habe ich dann eben einen Setbefehl,
also Set Greater,
und das Ergebnis wurde dann eben in diesem unteren Byte des
EAX-Registers abgelegt
Und anschließend habe ich dann eben noch diesen Befehl,
der dafür sorgt, dass in dem ERX-Register, bis auf dieses Byte,
dann eben der Rest, entsprechend mit Null zurückgesetzt wird.
Gut, analog zu diesen Set-Befehlen gibt es dann eben
auch noch einen ganzen Satz von Sprungbefehlen,
die dann eben als Jump-Befehle jetzt unbedingt oder bedingte Sprünge realisieren.
Und auch da kann ich dann eben sagen,
ich habe natürlich also erstmal sowas wie einen unbedingten Sprung.
Das heißt, da brauche ich jetzt keine Bedingungen abzufragen.
Ansonsten habe ich eben Jump-Equel,
Jump-Not-Equel, Jump
Negative, jump not negative, wo dann eben die entsprechenden Flags
in passenden Kombinationen eben auch wieder miteinander kombiniert werden und dich
dann eben über diese unterschiedlichen Befehle letztendlich dann damit verzweigen könnte.
Wir haben diese Sprungbefehle, das ist dann natürlich nachher genau das,
was man also zum Beispiel noch braucht,
um sowas wie irgendwelche Verzweigungen beziehungsweise dann eben auch irgendwelche Schleifen realisieren zu können.
Ein Beispiel, das ist jetzt das Letzte, was ich noch bringen werde,
also jetzt nochmal für bedingten Sprung. Also ich habe hier eine Funktion, die soll mir eben zurückliefern von zwei Argumenten, das Maximum. Im C-Code würde ich also schreiben,
if X größer als Y, dann Return X,
ansonsten Return Y, wird vom Assembler,
äh Blitzen vom Campala, also letztendlich so umgesetzt,
in Assembler-Code, dass ich eben auch hier Initialisierung,
Ende von dem Funktionsaufruf habe und hier in dem inneren Teil passiert dann im Prinzip Folgendes,
dass das, was hier als Code erzeugt wird,
das ist jetzt also hier Dieser Code,
der jetzt also hier nochmal entsprechend mit Kommentaren versehen ist,
das, was dort als Code erzeugt wird,
entspricht jetzt eigentlich nicht direkt dem Assembler, dem C-Code,
so wie ich ihn jetzt hier geschrieben habe,
sondern das entspricht eigentlich einem Code,
der dann mit dem allgemein beliebten Goto geschrieben ist.
Das heißt, ich habe, was dann also ein schlechter Programmierstudi ist,
aber ich habe eben die Verzweigung dadurch gemacht,
dass ich quasi hier sage,
Returnwert wäre erstmal explizit einer von den beiden,
in diesem Fall jetzt Y. Ich mache eine Abfrage,
x kleiner gleich Y. Das speichere ich ab.
Und dann frage ich eben ab, wenn das eins ist,
dann springe ich quasi hier raus und gebe diesen Wert zurück,
den ich dann eben hier entsprechend schon mit Y vorbesetzt habe.
Wenn nicht, wird diese Anweisung noch ausgeführt. Das heißt,
der Returnwirt wird dann mit dem anderen Wert initialisiert und
anschließend laufe ich quasi über dieses Label zum Rücksprung.
Und das ist im Prinzip genau das,
was der Assembler eben aus diesem Code gemacht hat,
wo dann im Prinzip also X und Y hier vom Stack
in diese beiden Register geladen werden.
Anschließend passiert dann eben hier diese entsprechende Comperbefehl und es gibt dann eben einen Jump,
der dann eben zu diesem Label hier hinten springt.
Und wenn die Bedingung wahr ist,
habe ich den IAX schon meinen Wert Y,
wenn die Bedingung nicht wahr ist,
ich also nicht springe,
dann piere ich eben
den Inhalt von
In das Rückgaberegister EAX und springe dann entsprechend zurück. Das heißt also,
dieses entspricht dann wirklich so eins zu eins diesem C-Code.
Gut, das waren jetzt erstmal so Sprungbefehle und Schleifen. Erster Teil,
also sprich jetzt also erstmal Bedingungen und Sprünge und nächste Woche
würde ich dann also im Prinzip mit der Implementation von Schleifen
beziehungsweise dann eben dem Aufruf von unter Programm
entsprechend weitermachen.
Gut, dann