Ja. Heute, wen will ich also diesen Teil zum Befehl
setzen abschließen? Also fehlen jetzt noch ein paar Folien,
würde ich noch ein paar Sachen kurz vorstellen.
Einiges vielleicht auch noch überspringen. Und dann mit dem Tal
zur Assembler-Programmierung anfangen. Ansonsten nur noch als Ergänzung zum letzten
Mittwoch. Als ich hier also die Befehlsätze vorgestellt habe,
bin ich also hier irgendwie bei dem Maps-Befehlsatz drüber gestolpert,
dass also hier an dieser Zeile, also quasi für dieses BNE,
das steht dann eben für Not-Eak in den Folien,
die ich da vorgestellt hatte, stand also hier im Prinzip FR2gleich null.
Das muss dann logischerweise natürlich Ungleich null heißen.
Wie gesagt, das nur als Ergänzung. Wie gesagt,
das ist also nichts, was wir jetzt also irgendwie gelernt
haben müssten oder was in der Klausur irgendwann immer abgefragt wird.
Nicht nur, wenn man da nochmal irgendwie reinguckt,
wollte ich das jetzt nochmal gebracht haben. Ja,
ansonsten hatte ich ja im Prinzip aufgehört,
nachdem ich jetzt also vorgestellt hatte, die unterschiedlichen Arten von Befehlssetzen,
die unterschiedlichen Arten von Adressierungen, so ein bisschen diese Kriterien,
wonach man jetzt eigentlich befehlt,
setze irgendwie,
bewerten oder beurteilen kann
Und in dem Zusammenhang wollte ich jetzt noch mal so ein
bisschen was zu den Begriffen. Risk und Sisk erläutern,
hatte ich schon mal kurz jeweils mal irgendwie so anklingen lassen.
Nicht nur, dass man das da eigentlich eben auch noch
mal weiß, was dann eigentlich so die Unterschiede sind.
Oder die prinzipiellen Unterschiede so rum. Also es geht jetzt nicht darum,
also jetzt irgendwie einen bestimmten Befehlssatz genau auswendig zu kennen.
Also dafür gibt es eine Literatur, irgendwelche Online-Quellen. Und
wenn man sich also mit einem Prozess XY jetzt wirklich auf dieser Ebene beschäftigt,
dann würde man sich dort also entsprechend aus der Dokumentation irgendwie was rausholen.
Ansonsten muss er das nicht zwingend machen. Gut, ja,
so ein bisschen noch mal was zur Motivation von diesem,
was immer als Riss quasi bezeichnet wird. Wenn man sich so ansieht,
jetzt ein ganz stinknormalen Befehlssatz, den wir also quasi alle tagtäglich benutzen,
nämlich X86, also sprich alles, was so intel bzw. AMD-Prozessoren in Notebooks,
Desktops bzw. seit einigen Jahren, also auch in irgendwelchen Mac-Rechnern ist,
Und mal guckt, was also der Compiler letztendlich an Befehlen,
wenn er jetzt Assembler-Code erzeugt, daraus macht.
Dann findet man eigentlich immer irgendwie solche Statistiken,
wo man sagt, für so einen typischen Benchmark,
also dies hier ist eigentlich ein Auszug aus einer etwas länglicheren Folie,
wo er im Prinzip so ein klassischer Spec-in-Suite,
also das sind also im Prinzip Benchmark-Tests,
mit sich hier unterschiedlichen Algorithmen und Programmen,
die da also irgendwie mal gemacht bzw. übersetzt und dann eben auch ausgemessen werden.
Und wenn man das jetzt nimmt und sich dann also wirklich mal anguckt,
die zehn häufigsten Befehle, Dass man da dann über die Benchmarks mit den zehn häufigsten Befehlen im Prinzip 96 Prozent des gesamten Codes erschlägt.
So, jetzt habe ich ja gesagt,
oder schon mal kurz einfließen lassen, also x,
86 Befehlssatz umfasst, also letztendlich mehr als 300 Befehle.
Nicht also allein deswegen ist also eben dieses Referenzmanual,
was man sich ja bei Intel mal irgendwie runterladen kann,
also wirklich ein dicker Schenken bzw. ein entsprechend großes PDF in diesem Fall.
Aber von diesen sehr vielen sehr unterschiedlichen Befehlen werden wirklich in der Praxis,
wenn man sich also Programmcode anguckt, also nur sehr wenige benutzt.
Das sind eben vergleichsweise einfache Befehle, also sowas wie Load and Store,
also sprich vom Speicher in Register, beziehungsweise vom Register in Speicher,
bedingte Sprünge, Vergleichsoperation und dann ebenso einfacher arathmetische, beziehungsweise mitweise Operation und unter Programmaufrufe. So,
das ist jetzt also nicht nur also auf diesen Intel-Befehlsatz beschränkt,
sondern es ist also auch bei anderen Prozessoren,
also zum Beispiel bei dem,
von mir also auch schon kurz angesprochenen Mips-Prozess,
also im typischen Riss-Prozess, wo man auch sagen kann,
nicht also mit einen kleinen Anteil an Instruktionen,
also ungefähr 20 Prozent der Instruktionen, beziehungsweise hier sind jetzt,
eigentlich sind es elf, wenn ich die hier noch unterteile,
nicht also im Prinzip mit elf Befehlen,
decke ich also hier irgendwie 98 Prozent oder 96 Prozent eben auch von so einer Benchmark-Feed letztendlich ab.
Und dass man dann eben wirklich mit sehr einfachen Load-Store-Ad-Befehlen eigentlich alles letztendlich erschlagen kann. So,
und das ist eben dann auch die Motivation, um zu sagen,
also diese Reduced Instruction Set, also dieser Risk-Befehlssätze,
Haben sich heute eigentlich durchgesetzt und sind eben vergleichsweise erfolgreich,
weil das, was eben beispielsweise bei Intel,
also mit diesen sehr umständlichen, komplexen Befehlen möglich ist,
wird eigentlich gar nicht mehr benötigt ist,
dort eigentlich nur noch wegen Abwärtskompatibilität zu älteren Programmen und so weiter mit drin.
Gut, ansonsten eben nochmal zu den Begriffen. Also hier ist nochmal aufgeschlüsselt,
Disc steht für Komplex Instruction Set Computer. Nicht,
das sind eben diese Rechnerstrukturen oder Rechnerarchitekturen mit sehr irregulären Befehlssatz,
eben wie gesagt, Befehlswortlänge von einem weit bis zu 36
Mit allen möglichen, sehr komplizierten Adressierungsmodi. Und wen wir dann also auch noch kurz sehen,
auch noch heute, wenn ich also mit diesen Adressierungen jetzt beim X86-Befehlsatz anfange. So,
und da muss man sagen,
also diese Befehle stammen eben eigentlich noch oder Befehlsätze stammen eben eigentlich nur aus der Zeit,
als es noch keine großen oder vernünftigen Compiler und Hochsprachen gab,
sondern eben wirklich so, als es mit der Computertechnik losging,
die ersten wirklich Großrechner irgendwie gebaut wurden,
die dann aber eben auch wirklich mit Assembler programmiert wurden.
Und da war es also vergleichsweise Vorteilhaft,
also Assembler-Befehle zu haben, wo ich mit einem einzigen Befehl
so was wie eine gesamte Vorschleife eigentlich abbilden kann,
weil das dann quasi das Äquivalent zu dem ist,
was wir heutzutage eigentlich mit Hochsprachen und mit Compiler machen.
Gut, wie gesagt, hatte ich ja schon gesagt,
sehr viele Adressierungsarten, quasi alle Befehle können auch immer jeweils
auf den Speicher zugreifen.
Ich habe beliebige Mischung von oder eine Adressierung von Register oder
Speicher oder Ammediat
Werde ich also gleich noch ein bisschen genauer dann auch vorstellen.
Typischerweise unter Programme werden dann also auch über den Hauptspeicher,
quasi über den Stack mit angestoßen und gerade die Parameter Übergabe unter Programmen
passiert bei X86 Prozessoren dann eben auch über den Stack.
Auch das sehen wir in der nächsten Woche dann noch genauer kennenlernen.
Bei Riskrechnern im Gegensatz dazu werden Parameter für unter Programme dann typischerweise in Registern übergeben.
Ja, Vor- und Nachteile. Wie gesagt,
Man hat vergleichsweise mächtige Befehle, kann dann eben dort eigentlich Äquivalent zu dem,
was wir heutzutage so als Programmiersprachen kennen, eben sehr viel mit ausdrücken.
Hat dann eben auch vergleichsweise kompakten Code, kompakt in dem Sinne,
dass ich mit wenig Essembler befehlen viel machen kann. Nachteil ist,
wenn ich jetzt einen Compiler schreiben muss, beziehungsweise einen Compiler baue,
der dann also letztendlich, also das erzeugen soll,
ist das also nicht ganz trivial umzusetzen und einer der Gründe,
weswegen es eben auch so relativ wenig benutzte Befehle in einem Assembler-Code gibt,
ist dann eben, dass die Compiler dann eben letztendlich,
dass dann eher in riskartige Anweisungen umsetzen und davon dann eben einfach ein paar mehr erzeugen.
Andererseits, wenn ich den Code nicht selber schreiben muss,
soll mir das egal sein,
er soll letztendlich effizient auf der Hardware ausgeführt werden.
Das heißt also, möglichst schnell irgendwie mein Algorithmus berechnen und
da ist mir das egal,
wie das Ding intern eigentlich dann aufgebaut ist
Ja, und was dann eh mag ich,
der große Nachteil ist, was ja auch noch nachher später
noch ein bisschen vorstellen werde, ist eben dieses Pipelining.
Auch das für bietet sich Barces Computern dann eigentlich,
beziehungsweise einer der Gründe,
weswegen die Compiler dann eben so riskartigen Code erzeugen,
liegt dann eben auch daran,
dass heutzutage in den Prozessoren ja eben auch alles mit Pipeline gemacht wird.
Ja, Konsequenz von dem Ganzen ist eben auch ein Befehl,
wird in der Regel dann eben in mehreren Teilschritten auch abgearbeitet und
die Anzahl dieser Teilschritte ist dann eben auch nicht fix.
Also auch das ist dann so ein bisschen die Sache,
weswegen dann diese Abarbeitung von Befehlen, nicht je nach Art des Befehls,
also teilweise weniger, teilweise eben mehr Takte dauern kann.
Gut, zur Mikroprogrammierung, die nächsten drei Folien werde ich
jetzt also hier mal kurz überspringen.
Jetzt nur noch im Gegensatz da zu Risk,
steht dann eben für Reduce Instruction Set Computer.
Wie gesagt, die Grundidee basiert eigentlich auf diesen Überlegungen,
dass man mit verglagsweise wenig Befehlen,
dafür dann aber eben ein paar mehr eigentlich auch alles
letztendlich ausdrücken kann
Wurde, wie gesagt, Anfang der 80er-Jahre entwickelt,
basiert eigentlich auf einem internen Projekt, also von EBM,
wurde dann eben populär durch die Herren Hennessee und Patterson,
die also auch als Hennessee Petterson,
beziehungsweise Patterson Hennessee,
also diverse sehr gute Lehrbücher geschrieben haben,
die sich also mit Rechnerarchitektur auseinandersetzen,
beziehungsweise die dann da eben auch genau die MIPS-Architektur in dem Fall dann beschreiben,
findet sich dann also hier auch in meinen Folienunterlagen auch immer
relativ häufig in der Literatur lässt und gerade bei diesen Kapiteln immer
Wie gesagt, Grundidee ist, ich habe meine Programmiersprachen,
also sprich dazu, mit wir heutzutage arbeiten, ich habe einen Compiler,
der übersetzt es dann eben entsprechend in Assembler-Code und wie gesagt,
das ist eigentlich das Konzept, womit wir heute arbeiten,
was entsprechend gut funktioniert.
Typisch dabei ist eben, ich habe diese reguläre Struktur,
also sprich, wenn ich zum Beispiel 32 Bittenmaschine habe,
dann also wirklich 32 Bit-Wort-Breite für ein Speicherwort,
für die jeweiligen Datentypen für die Befehle.
Es gibt eben nur einen Wortbefehle und man kann dort dann eben sehr schön dieses Pipelining-Prinzip letztendlich realisieren.
Speicherzugriffe passieren eben wirklich nur über die explizit benutzten Load-and-Store-Befehle und alles,
was also letztendlich Rechenoperationen auf der Alu ist, geht eben nur mit lokalen Registern.
Dafür hat man dann eben teilweise sehr viele Register, nicht also typischerweise,
also irgendwie sowas schon mal wie mindestens 16 oder 32 Register,
wenn ich ein paar einigen Architekturen dann eben auch deutlich mehr.
Damit kann man mich teilweise also auch relativ effizient,
so was wie Kontextwechsel im Betriebssystem machen,
also zum Beispiel die Spark-Prozessoren von San Microsystems,
die jetzt von Orga gekauft worden sind,
Spark gibt es ja in dem Sinne auch nicht mehr,
hatten das, also die hatten, glaube ich,
irgendwie 128 Register, wo tern in der CPU verbaut.
Der Programmierer hat davon aber immer nur 32 gesehen,
was aber letztendlich passiert ist, wenn jetzt also sowas wie ein Kontextwechsel gekommen ist.
Das heißt, das Betriebssystem rechnet an einem Programm oder einem Task.
Irgendwann, weil irgendein Datum fehlt oder auf irgendeine Eingabe gewartet wird,
kommt es zum Interrap. Dieser Task wird beendet,
irgendeine andere, oder erstmal unterbrochen, irgendein anderer Task soll rechnen,
dann sieht das per Definition eigentlich bei X86-Prozessoren so aus,
dass also intern sämtliche Register irgendwo in den Hauptspeicher,
in den Heat-Bereich quasi gesichert werden müssen.
Sämtliche Informationen von dem Task wird dann also quasi dort abgelegt.
Der neue Task, dessen Informationen werden in ihre Gister und
in den Programmzähler geladen, läuft dann weiter.
Und wenn wir jetzt wieder Kontextwechsel zurück stattfindet, passiert wieder genau das Gleiche.
Und bei diesen Registerorientierten Befehlssetzen kann man eben sagen, ich habe 128 Register,
32 sind aber eigentlich nur in dem Sinne in Benutzung.
Und wenn anderer Task dran ist,
dann wird einfach quasi dieser 32er Satz auf irgendwas anderes abgebildet.
Also sprich einfach von dem anderen Prozess der entsprechende Registersatz quasi aktiv geschaltet.
Und ohne diese zusätzlichen Speicherzugriffe kann dann also weiter gerechnet werden.
Das ist dann eben auch entsprechend effizient. Ja, wie gesagt,
unter Programmaufrufe passieren dann eben auch typischerweise nicht über Parameterübergabe über den Hauptspeicher,
also in Form eines Stacks,
was wir jetzt also beim Mix86 noch kennenlernen werden,
sondern eben auch typischerweise über Register
Ja, Vor- und Nachteile. Wie gesagt,
ich habe dann vergleichsweise einfacher Mikroprogrammierung. Das kann dann also
auch in dem Sinne über so eine festverdrahtete Logik passieren.
Da brauche ich jetzt also nicht mehr irgendwelche größeren, endlichen Automaten,
die dann also entweder als Mikroprogramm oder eben als jetzt von
Hand entworfene Moor- oder Miliemaschine letztendlich die Ablaufsteuerung für die Befehle machen.
Ich habe eben sehr gut die Möglichkeit, dieses Pipelining zu machen,
da dann eben auch dann vergleichsweise einfache Instruktionen mit wenigen in Adressierungsarten.
Das kann dann eben auch sehr effizient in Hardware umgesetzt werden
Nachteil, ich brauche dann, also quasi um den Code auszudrücken, mehr Befehle.
Also jetzt zählst du von der Anzahl und die Hardware muss eben entsprechend viele Register bereitstellen. Gut,
wenn man das jetzt vergleicht, sagen wir mal so,
in den 90er, 80er, 90er Jahren, als Risk aufgekommen ist,
gab es da also zwischen den Anbietern beziehungsweise zwischen den Hochschulen dann,
also quasi im akademischen Bereich, dann also auch relativ viel an Diskussionen,
wo man gesagt hatte, ja,
das ist da besser oder das ist da besser.
Inzwischen muss man sagen, die Grenzen zwischen den Architekturen verschwimmen.
Also das, was früher so ganz einfache Riskprozessoren waren, wird eben immer komplexer.
Und wenn man sich also heutzutage so 64-Bit-Arm-Prozessor ansieht,
wie er also beispielsweise jetzt in Smartphones typischerweise verbaut wird,
haben die jetzt von einem ursprünglich sehr einfachen Befehlssatz,
also jetzt eben auch schon entsprechend komplexere Befehle,
wie sie jetzt also eigentlich eher bei X86-Prozessoren der Fall sind.
Auf der anderen Seite sieht es eben so aus,
dass bei den ganzen aktuellen Prozessoren im Prinzip die Befehlsabarbeitung intern
Auch in riskartige Strukturen eher umgesetzt wird.
Deswegen verschwimmen da die Grenzen dann eigentlich so ein bisschen.
Gut, ja, das eigentlich zu den Befehlssätzen. Wie gesagt,
heutzutage hat man immer so als typisches Beispiel X86-Befehlssätze seit einigen Jahren,
seit fünf Jahren, sechs Jahren, eigentlich das,
was dann so typischerweise als IA64, also Intel Architecture 64-Bit kennt,
wo ich dann eben entsprechenden sehr mächtigen Befehlssatz habe,
64 mit Wortgröße und mit entsprechenden Compilern da eben auch entsprechend gut mitarbeiten kann.
Gut, also da ist nur so ein bisschen als Hintergrund noch mal zu dieser Diskussion,
was ist mit Risk, was ist mit Sisk gemeint.
Ansonsten werden wir jetzt vom Befehlssatz her noch mal ein bisschen was zur Assembler-Programmierung X86 kennenlernen,
was eigentlich zwar Ein Cisk-Befehlsatz ist das,
was sie jetzt hier in den Beispielen vorstelle,
sind wirklich nur sehr eingeschränkt, sehr wenige Befehle.
Und wie gesagt, von der Benutzung her eigentlich auch dementsprechend,
was also ein Compiler daraus machen würde. Das heißt,
dann auch eher im Sinne so eines Rösk-Befehlsatzes, das heißt also
Vom Speicher wird dann eben relativ viel in irgendwelche Register gemacht und dann dort gerechnet.
Gut, wie sieht das jetzt generell erstmal mit der Abarbeitung aus?
Ich hatte ja also auch schon mehrfach vorgestellt und diese Folie ist also quasi jetzt auch nochmal irgendwo relativ am Anfang davor lese, naja, oder mit der Vorlesung irgendwo mit aufgetaucht. Also im Prinzip dieses Omplierungssystem,
nicht das, wo wir jetzt also typischerweise arbeiten,
ist, dass wir sagen,
wir haben jetzt also hier irgendwie unser Programm in irgendeiner Hochsprache,
in diesem Fall soll es jetzt also mal beidentwegen mal C sein,
nicht ich habe dann meinen Compiler, der erzeugt daraus,
dann, beziehungsweise es läuft erst durch den Pri-Prozess,
sondern durch den Compiler, der erzeugt daraus dann eben letztendlich Assembler-Code,
also sprich den Code,
den wir jetzt also im Rahmen dieses Kapitels auch noch so ein bisschen kennenlernen werden und das,
was man dann als Assembler bezeichnet,
ist eben ein Programm,
was quasi diese Befehle auf dieser Ebene dann eben wirklich in irgendwelche Bits-Drinks umsetzt,
die dann eben vom Prozessor quasi jetzt in den Speicher geladen werden,
dort eben entsprechend interpretiert werden
als so ein Befehl
Und wie gesagt, die Abarbeitung der Befehle folgt dann eben diesem typischen von Neumann-Zyklus-Befehl, holen, wie viel dekodieren, Befehle ausführen und so weiter. Gut,
hier auch noch mal aus einem anderen Foliensatz,
also dann auch noch mal so die Veranschaulichung,
also einmal die hochsprachliche Beschreibung, dann also irgendwie ein,
wie auch immer, gearteter Assembler-Code,
in diesem Fall Mips und das,
was dann eben quasi von der Maschine ausgeführt wird,
sind dann eben letztendlich irgendwelche Bitstrinks, die dann dazu führen,
dass man Hardware das dann eben interpretiert und sagt,
okay, dieser Bitstring, also entsprechend irgendwie so ein Befehlsformat,
steht dann eben für irgendeine bestimmte Allo-Operation,
wo dann eben im Prinzip jetzt also hier irgendwie ein Tal seines Befehlswortes aus dem und dem Register kommt und das jetzt noch irgendwie maskiert wird.
Gut, warum beschäftigt man sich heutzutage überhaupt noch mit Assembler-Code?
Kann man sich ja auch fragen. Da muss man sagen,
okay, also typischerweise und vielleicht auch hoffentlich werden wir also nie in die Verlegenheit kommen,
also jetzt wirklich selber Assembler-Code schreiben zu müssen.
Ich war, wie gesagt, das macht also,
wenn man jetzt also irgendwie vernünftig programmieren gelernt hat, also auch nur bedingt viel Spaß.
Und gerade in Bezug auf die Komplexität von Programmen muss man eben sagen,
einfach aus Produktivitätsgründen, wenn ich nicht in Hochsprachen programmiere,
sondern in Assembler-Code und ich habe jetzt also irgendein Programm,
wo ich dann sage, also da soll irgendwie was gemacht werden,
da soll eine grafische Oberfläche dahinter stehen,
das möchte man also nicht in Assembler-Code programmieren,
sondern da hat man dann eben entsprechend da dann also irgendwo seine Programmiersprachen.
Dann ist es eben auch so inzwischen, also wenn man sich so anguckt,
was also im Compilerbau möglich ist und was dort passiert,
dass, sagen wir jetzt, wirklich vernünftig gut gemachte Compiler,
also sehr effizienten Code erzeugen.
Das heißt, da kann man dann eben schon, oder das,
was ich Compiler machen, ist, dass man eben schon gucken kann,
auf welche Hardware das läuft, mit welchem Befehlssatz.
Ich bin zum Beispiel bei dem GCC, kann ich eben sagen,
okay, das soll jetzt generischer X86 Code sein,
ich kann aber eben auch sagen,
der soll für die letzte Prozessor,
Architektur von Intel mit den und den Eigenschaften sein oder für meinetwegen jetzt ein AMD mit den und den Eigenschaften.
Und dann wird also speziell für die Hardware und für diesen Befehlssatz, also der Code optimiert.
Der läuft da natürlich nicht mehr auf jedem Gerät,
sondern eben wirklich nur auf den Geräten mit den entsprechenden Prozessoren.
Aber man kann da dann eben sehr viel an Optimierung und Sachen mit reinbauen.
Und das, was dann dort teilweise passiert,
also muss man auch sagen, das ist also wirklich erstaunlich,
was die Compiler alles machen und können,
ist eben auch teilweise deutlich besser,
als wenn ich jetzt mich hinsetze und anfangen würde,
also irgendwie Assembler-Code zu schreiben
Nicht, weil da passieren eben schon so Sachen,
dass man sagt, naja gut,
also zum Beispiel sowas wie irgendwelche Schleifen oder so,
können unter Umständen automatisch aufgelöst werden, werden also dann durch irgendwie einzelne Anweisungen ersetzen.
Die Schleife wird einfach quasi entrollt,
hat eben den Vorteil, man erzeugt zwar mehr Code,
aber ich brauche dann also keinerlei Abfragen mehr auf irgendwelche Schleifenrandbedingungen,
braucht dafür dann auch keine bedingten Sprünge mehr.
Das führt wieder dazu, dass der Code dann
also Pipelining mäßig, also deutlich effizienter abgearbeitet werden kann
Nicht. Und wie gesagt, unter bestimmten Rahmenbedingungen machen, also die Compardern,
also solche Optimierungen, also sehr effizient teilweise werden Unterprogramme oder Makros auch automatisch ersetzt. Also da muss man sagen, das ist also wirklich sehr, sehr gut, was da passiert. Gut,
nichtsdestotrotz, warum Assembler zumindest ein gewisses Grundwissen oder so eine Grundidee,
was sich dahinter verbirgt,
möchte ich hier also doch noch so ein bisschen vermitteln,
weil also zum einen Wir haben ja vorher quasi die Hardware kennengelernt,
auf der das Ganze dann irgendwo ausgeführt wird,
dass man da dann nochmal so ein bisschen sieht oder nachvollziehen kann,
warum ich jetzt eigentlich mit diesen Hardware-Strukturen,
also mit den Alus und den Multiplexern und Speicher hier und das da,
letztendlich diese Befehle abarbeiten kann, wo man das dann eben auch braucht,
ist, wenn man das letztendlich Debugging betreibt,
das heißt also irgendwelchen Programmierfehler aufspürt,
die sich da also irgendwo angeschlichen haben,
auch da kann es teilweise notwendig sein,
dass man sich dann eben auch mal so ein bisschen
den Assembler-Code anguckt
Wenn man jetzt in den Bereich geht,
wo ich also jetzt wirklich ein Programm sehr stark optimieren möchte und
da noch irgendwie das Letzte rauskitzeln möchte, ist es auch notwendig,
noch ein bisschen mehr über die Hardware-Strukturen und um die Umsetzung mit der Assembler kennenzulernen.
Klassisches Beispiel sind eben, wenn ich jetzt bestimmte Datenstrukturen mache und die anlege,
ich hatte ja ganz am Anfang,
also mal irgendwie dieses Beispiel gebracht und gesagt,
Es war dimensionales Array, was entweder zahlenweise oder spaltenweise durchlaufen wird.
Und in dem einen Fall davon ab, gut,
da ging es also dann um Fließkommazahlen und irgendwie die, die aufzusummieren.
Und da gab es dann auch noch unterschiedliche Ergebnisse.
Gut, das liegt jetzt wieder an der Rundung von Fließkommazahlen.
Das haben wir irgendwie mal festgestellt,
dass ich da dann natürlich wegen der begrenzten Darstellung,
die ich jetzt also quasi für die Mantise habe,
es sein kann, dass also irgendwelche Stellen wegfallen.
Was aber dort dann eben auch noch mit aufgefallen ist,
dass ich also irgendwie teilweise Faktor 2 oder 3 unterschiedliche Laufzeiten habe.
Das heißt, wenn ich zahlenweise durchgehe, ist es also
dreimal so schnell, als wenn ich spaltenweise durchgehe.
Obwohl ich ja die gleichen Elemente, aber eben einfach nur in andere Reihenfolge instanziere. Das sind klassische Beispiele,
da muss man eben wissen, wie sieht der Prozessor aus,
wie sieht die Speicherhierarchie aus,
sondern im einen Fall funktioniert das eben sehr gut mit den Cashing-Algorithmen,
dass ich quasi, wenn ich also irgendwie auf ein Element zugreife,
die nächsten, ich sage jetzt mal 15 Elemente automatisch gleich mitgeladen werden und die,
die dann eben beim zweiten Zugriff auch wieder sehr schnell im Zugriff habe oder der Worst Case ist,
dass ich wirklich jedes Mal Mein Element, die aus dem Hauptspeicher holen muss, was dann eben entsprechend langsam ist.
Und wie gesagt, das sind dann eben diese Beispiele,
wo man also schon so ein bisschen auch was über die Hardware-Architektur wissen sollte,
um dann eben entsprechend effizient die Programme schreiben zu wollen.
Gut, und letztendlich,
wo man sich also dann leider auch mit Assembler auseinandersetzen müsste,
ist häufig, wenn man also entweder Komparlabau betreibt oder eben,
wenn man, also auf unterster Ebene,
also sowas wie irgendwelche Low-Level-Treiber beschreiben möchte,
also sprich da nah auf der Betriebssystemebene irgendwie
Gerätetreiber schreibt oder dort irgendwo was macht
Gut, das, was ich jetzt eigentlich nur so ein
bisschen vorstellen werde jetzt, also im Laufe dieses Kapitels,
sind also wirklich nur so ein paar ganz einfache Grundlagen.
Also da geht es jetzt also nicht darum,
also deswegen also nicht verzweifeln, wenn es hier in den Beispielen gleich,
also irgendwelcher Code erscheint, den müssen sie nicht selber so runterschreiben.
Geht es also nicht darum,
also jetzt hier ein Kurs in eine Semler-Programmierung zu machen,
sondern nur, dass man einfach so ein bisschen sieht,
wie die Operation in Assembler umgesetzt werden, wie die Datenstrukturen quasi abgebildet werden in den Speicher
Und was dann eben auch noch interessant ist, wie letztendlich Funktionsaufrufe funktionieren,
wo ich dann eben beispielsweise sowas wie irgendwelche lokalen Variablen habe,
wo ich Rekursionen mit drin habe,
nicht wo man sich dann auch fragt, ja,
wie kriege ich das dann letztendlich?
Also irgendwo in der Hardware abgebildet. Gut, ganz am Ende
des Capitels werde ich noch ein bisschen was zur Speicherverwaltung und Funktionsbibliotheken sagen.
Gut, grundsätzlich kann man immer sagen, jetzt,
also auf Assembler, also der eigentliche Speicher oder Hauptspeicher,
ich habe ja gesagt, eigentlich muss man sich mal vorstellen,
einfach von null bis Zwei hochennen, minus eins, byte, byteweise adressierbar.
Wird typischerweise von den Betriebssystemen, egal,
also ob das nun irgendein Linux-Derivat ist oder ob das in irgendeinem Windows ist,
eigentlich immer unterteilt, in Speicherbereiche,
wo zum einen Programmcode des Benutzers liegt, dann Speicherbereiche,
wo sowas wie Funktionsbibliotheken liegen, dann Teile,
wo jetzt von den laufenden Programmen quasi ein Stack abgebildet wird,
den ich eben brauche, um Funktionsaufrufe der Programme zu machen.
Nicht dann gibt es natürlich den Teil, wo ich jetzt
also eigentlich meine eigentlichen Datenstrukturen ablege. Die können entweder statisch,
also sprich durch Deklaration, erzeugt werden oder dynamisch eben,
indem ich dann also beispielsweise in C,
also irgendwie sowas wie ein Malock oder sowas benutze,
um mir dann also quasi jetzt dynamisch irgendwelche irgendwie Speicher zu holen.
Gut, dann Muss man immer sagen,
gerade die Speicherverwaltung ist immer der klassische Teil,
wo man also relativ schnell Programmierfehler sich einbaut.
Bei einigen Programmiersprachen ist das also vergleichsweise gut und sauber gelöst.
Bei anderen Programmiersprachen, wie gesagt, die Bartspiele jetzt hier werden im MNC stattfinden.
C ist also eine Programmiersprache, wo man dann immer sagen muss,
ist in der Hinsicht also wirklich sehr scheußlich,
weil ich wirklich letztendlich jede Zahl oder jedes Objekt beliebig sagen kann,
okay, das ist ein Objekt oder es ist also irgendwie ein Zugriff auf einen Speicher.
Und wenn ich mich da mal irgendwo vertippe oder so,
dann habe ich also aus irgendeiner x-beliebigen Unsigned oder Science-Zahl,
also auf einmal irgendwie eine Speicheradresse gemacht und im Regelfall kriegt man dann immer irgendwie während des Programmlaufs dann quasi vom Betriebssystem dann also irgendwie eine Fehlermeldung, nicht Segmentation Violation, dann steigt das Programm aus. So,
das ist Wenn man sich da jetzt also irgendwie ververprogrammiert hat,
also dann noch der harmlose Fall, der nicht so harmlose Fall ist,
dass dann eben solche Speicherlücken oder eben solche Sachen ein bisschen intelligenter ausgenutzt werden,
nämlich in einer Art und Weise,
dass es also jetzt nicht direkt vom Betriebssystem gemerkt wird,
sondern dass man dann, und das ist eben die Art und Weise,
wie eigentlich alle Schadsoftware arbeitet,
dass man dann eben versucht quasi jetzt aus seinem Programmcode in irgendwelche Speicherbereiche darauf zuzugreifen,
entweder von anderen Prozessen,
in denen man nichts zu suchen hat oder am besten gleich,
also vom Betriebssystem,
was dann also noch mit anderen Prioritäten und Zugriffsrechten liegt,
wo man dann sagen kann nicht,
also dann kann ich von da aus versuchen,
also quasi so eine Privilegierungs-Privilege-Escalation, also im Prinzip so ein Eskalation,
also eine höhere Privilegierung zu bekommen,
um dann eben dafür zu sorgen,
dass sich also von dem Prozess aus dann also auf einmal alles Mögliche machen darf,
was eigentlich nicht vorkommen sollte.
Gut, da muss man sagen, es gibt je nach Betriebssystem,
da also unterschiedliche Möglichkeiten, das zu handhaben. Andererseits muss man auch sagen,
das ist jetzt also gerade das,
was jetzt also im Prinzip jetzt gerade mit diesen beiden aktuellen Sicherheitslücken,
also Meltdown und Spectre, also passiert ist,
ist es eben so,
dass also aus Performance oder aus Effizienzgründen letztendlich nicht jeder Speicherzugriff eins
zu eins quasi über irgendwelche Status-Bits geprüft wird,
ob das System bzw. dieser Prozess da dann eben auch
darauf zugreifen darf
Kann man so was machen. Das nennt sich dann also Capabilities,
dass man dann also wirklich, also den Prozessen, also sehr umfangreiche Sachen mitgibt.
Ich bloß, wie gesagt, wenn man das macht, funktionieren eben einige Performance-Optimierungen nicht mehr so gut. Und gerade das,
was dort jetzt also ausgenutzt worden ist, ist, dass man sagt,
abfragen, ob Prozesse auf bestimmte Sachen drauf zugreifen können,
passieren eben erst, wenn diese Zugriffe quasi in der Abarbeitung abgeschlossen werden,
wenn also feststeht, der Prozess arbeite da jetzt richtig,
hat irgendwie was gemacht und um das jetzt also letztendlich abzuschließen,
dort wo dann eben drauf geprüft darf der Prozess das.
So und das für diese Sicherheitslücken jetzt also letztendlich gerade arbeiten oder gearbeitet haben,
ist eben das an dieser Stelle quasi die Information oder andersrum,
dass also quasi Schon vorher, bevor diese letzte Abfrage stattfindet,
also irgendwelche Informationen, auf die man eigentlich nicht zugreifen kann,
woanders hin gesichert wird und über diesen Mechanismus, beziehungsweise woanders hin gesichert,
stimmt auch nicht, aber im Prinzip diese Information,
also irgendwo so gespeichert oder abgelegt wird,
dass man sich später hinterher dann noch darauf zugreifen kann,
obwohl man eigentlich dann also auf diesen Speichel begreifen nicht hätte,
aufzuwerfen.
Gut, also wie gesagt, da gibt es eben im
Prinzip Sachen, wo dann einiges letztendlich im Argen ist.
Gut, muss man sagen, die Betriebssysteme werden da also
auch besser, bauen auch immer mehr Sicherheitsmechanismen ein. Das,
was ich hier jetzt auch vorstelle,
also ich werde dann teilweise jeder noch so ein bisschen was zu Pufferüberläufen sagen,
sind Sachen, also die Beispiele, wie sie hier sind,
würden zum Großteil, also heutzutage auch so nicht mehr funktionieren,
weil also auch gerade bei den Speicheradressen sich da jetzt also auch einiges noch wieder geändert hat.
Gut, ja, also Wie gesagt,
jetzt geht es gleich ein bisschen mit Sam Larcode los.
Wird jetzt allerdings eben sehr stark eingeschränkt. In diesem Fall jetzt insofern,
als ich das hier sage, das ist jetzt also auch
nicht mehr so ganz praxisrelevant, eben quasi auf Assembler-Code mit dem 32-Bit-Befehlssatz.
Das heißt, als Datentyp werde ich jetzt also auch nur sowas wie 32-Bit-Inager benutzen. Und von einem Befehlssatz,
wie gesagt, von diesem 300-Befehlen,
also vielleicht irgendwie Größe ordnungsmäßig 20 oder 30, hier mal irgendwo aufgelistet haben,
in den Beispielen kommen dann also vielleicht immer irgendwie so 10,
10, 15 verschiedene Befehle vor,
das dann eben auch nur ein sehr kleiner Teil.
Die nehmen relativ viel an Themen,
was dann eigentlich in die Richtung Assembler-Programmierung, Compilerbau geht,
fehlt hier, müsste man dann eigentlich auch in irgendwie anderen Vorlesungen dann wirklich mal einzeln genauer behandeln.
Ja, aus Sicht des Programmierers, was sieht der eigentlich jetzt vom Assembler,
da kann ich mal sagen, gut, also ich habe meine CPU,
dort befinden sich Register, acht Stück an der Zahl, also jetzt zumindest in diesem 32-Bit eingeschränkten Befehlssatz.
Ich habe sowas wie so ein Flag-Register,
da stehen eben irgendwie solche Bedingungen von irgendwelchen narrenmetischen Operaktionen mit drin,
sprich ist das Ergebnis gleich, null, es ist kleiner,
null, es ist ein Overflow aufgetaucht,
es ist ein Carry aufgetaucht und es gibt eben den Programmzähler,
EEP steht für, also von diesem Extended-Befehlsatz,
EEP ist dann immer Instruction-Pointer, das heißt,
dort steht die Adresse des Befehls,
der dann quasi aus dem Hauptspeicher geladen und in das Befehlsregister und dann dort geladen wird und dort dann eben ausgeführt wird und dementsprechend interpretiert wird.
So, und dann gibt es eben einen Speicher beidweise adressierbar.
Der wird jetzt irgendwie adressiert. Entweder,
wenn ich jetzt eine Instruktion lade mit dem Inhalt von diesem Befehlsregister
oder wenn ich jetzt also irgendwelche Daten lade mit irgendwelchen Adressen,
die mir dann also quasi hier in dem Bereich gehen,
wo ich dann also quasi jetzt meine Programmdaten liegen habe.
Und vom Betriebssystem her wird das dann eben irgendwie abgebildet in eben die Teile,
wo also quasi das Programm rumliegt und die Daten des Programms.
Und dann gibt es eben noch einen Bereich Stack,
den ich dann also letztendlich auch über zwei bestimmte Register verwalte,
wo dann im Prinzip für unser Programmaufrufe die Parameter übergeben werden,
beziehungsweise irgendwie sowas wie ein lokaler Kontext gesichert und gespeichert wird.
Gut, hier ist das Ganze noch mal kurz aufgezählt.
Also wie gesagt, Programmzähler ist dieses EEP-Register. Da dann
habe ich eben jeweils die Adresse der nächsten oder jeweils auszuführenden Anweisungen,
dann eine Registerbank mit irgendwie acht Registern, wo dann eben die häufig benutzten Daten rumliegen.
Also sprich, darauf rechne ich dann quasi jetzt mit meinen Arranmetisch logischen Operationen rum.
Und dann eben die Zustandscodes in diesen Flex-Registern.
Der Speicher ist bydrider addressierbar. Und ich habe eben den
eigentlichen Programmcode und die Daten aus dem Programmcode und eben noch
diesen Bereich Stack, der dann quasi für unter Programmaufrufe benötigt wird
Gut, wie sieht das Ganze jetzt in dieser Übersetzungshierarchie aus?
Ich habe eben hier mein C-Programm,
erzeugt man mit Pre-Compiler und Compiler an eben das Assembler-Programm.
Das war dann also ja das, was dann auch hier
in seinen Übungsaufgaben gefordert war. Wenn man sich das anguckt,
hat man dann eben irgendwie diese ganzen komischen Assembler-Befehle,
die ich also hier zumindest teilweise so ein bisschen versuchen werde,
ihn nahe zu bringen.
Und der eigentliche Assembler erzeugt daraus dann eben quasi jetzt also irgendwie Objektdateien und die werden dann,
wenn ich jetzt also quasi mein fertiges Programm habe,
dann eben noch über einen Linker mit Systembibliotheken entweder statisch oder dynamisch zusammengelingt.
Und dann habe ich also letztendlich mein Executable,
was sich dann eben über Kommandozeile oder über einen Doppelklick von irgendwas
entsprechend dann also ausführen kann.
Gut, für ein einfaches Beispiel, wie sieht so was aus?
Also ich habe jetzt also hier dieses hochkomplizierte Programm,
der Programm namens Sam, was zwei Argumente,
Integert-Zahlen X und Y hat, die Summe berechnet und das als Ergebnis zurückliefert.
Der Assembler-Code besteht dann also in diesem Fall aus irgendwie solchen
Befehlen oder sowas ähnlichem. Und wobei die eigentliche Berechnung von
dem Ganzen im Prinzip diese beiden Zahlen hier sind,
alles da drüber und da drunter ist eigentlich nichts anderes als die Verwaltung dieses Unterprogrammaufrufs.
Auch das werde ich dann Heute beziehungsweise in der nächsten Woche
dann noch ein bisschen erläutern, was sich da dahinter verbirgt.
Also hier dieses Push, dieses Move Long und hier
entsprechend so ein Move Long und so ein Pop.
Das bezieht sich dann eben immer auf den Stack und letztendlich das,
was hier einmal irgendwie gemacht wird, wird hier dann letztendlich wieder rückgängig gemacht.
Und mit dem Return wird dann also dieses Unterprogramm beendet.
Wie gesagt, wird gleich noch,
oder im Laufe dieses Kapitels dann noch ein bisschen klarer,
was dann gemeint ist
Gut, ja, das, was der Assembler eben macht,
wenn man sich jetzt diesen Code ansieht, ist,
dass er da dann eben entsprechend Hardwanner natürlich auf den,
die Register zugreift, Befehlsatz macht, typischerweise,
also dieser Assembler-Code ist immer so aufgebaut, ein Befehl pro Zeile,
dann hat man die Befehlskodierung oder diesen Assembler-Code dann eben in Form so einer Memonic-Kodierung,
das heißt mit irgendwie so mehr oder minder besprechenden Abkürzungen,
also wenn ich hier zum Beispiel Push L steht daneben für Push Long,
das heißt also,
es würden 32 Bitwert auf den Stack gepusht.
Nicht, dass es dann eben so ein Beispiel und gut,
dann habe ich eben in dem Code selber Konstanten,
die ich drin habe, also sprich das,
was ich jetzt vorher bei dem Befehlssätzen immer als Emediate-Werte bezeichnet hatte.
Nicht, das können eben entweder dezimare X-Werte sein in der Codierung,
nicht, dann habe ich dann eben die Register,
also in diesem Fall jetzt hier immer die acht Register letztendlich im Zugriff,
gut, über die eingängigen Namen kann man sich streiten,
also so eingängig sind die also auch nicht,
aber okay
Es gibt dann eben für die verschiedenen Arrasierungsmodi, die ich ja jetzt gleich noch vorstelle,
dann eben die entsprechenden Arten und Weisen, wie dann also die Adressberechnung stattfindet.
So war es ja auch immer noch wichtig ist,
dass es also das eigentlich, was Sie hiervon noch mal mitnehmen sollten,
ist, also hier ist als Konvention bei dem GCC bzw. Gnu Assembler so,
dass das Ziel in der Operation rechts steht.
Das heißt, wenn ich hier sowas habe wie so ein Move-Lorng,
in diesem Fall sind dieses jetzt hier Register, steht das dafür,
dass der Inhalt des Registers ESP nach EBP kopiert wird.
Auch dazu folgen gleich noch Beispiele, wo das also auch nochmal explizit daneben steht,
nicht, aber wie gesagt, Ziel der Operation steht hier dann immer recht.
So, was es dann eben auch gibt, ist dann irgendwie unmotiviert auf das Touchpad gekommen. So,
was dann eben auch immer noch gibt,
sind im Prinzip sowas wie Label,
also sprich jetzt irgendwelche Bezeichnungen und Doppelpunkt dahinter, die man braucht,
um in diesem Fall jetzt entweder sowas wie einen Einsprungpunkt für ein Unterprogramm zu haben,
dass es dann in diesem Fall jetzt hier ein globales Label oder was ich brauche,
um jetzt quasi bei irgendwelchen Sprüngen zu sagen,
ich möchte an die und die Stelle in meinem Code reinspringen.
Auch da kann man dann einfach so ein Label und einen Doppelpunkt,
also mit irgendeinem sprechenden Namen und irgendeinem String und einen Doppelpunkt dahinter nehmen,
dass wir dann eben Label,
was dann letztendlich bei Sprüngen quasi angesprungen wird.
Das, was dann auch nur nochmal ergänzend hier zu dieser dieser Hierarchie, was daneben passiert,
was der Assembler dann eben auch noch macht, ist,
dass er, wie gesagt, hier steht noch irgendwie ein lesbarer Name
. Wenn das in Binärcode umgesetzt wird, wird,
also hier dann also quasi irgendeine willkürlich gewählte Speicheradresse genommen.
Und wenn ich quasi jetzt diese Prozedur aufrufe,
wird dann eben der Programmzähler dann eben auch typischerweise natürlich mit
genau dieser Speicheradresse initialisiert, um dann quasi diesen Code hier anzuspringen.
Also das ist das, was sich dann bei diesen Unterprogrammen
aufrufen dahinter verbirgt
Gut, als Datentypen gibt es im Assembler-Code eben wirklich nur primitive Daten.
Das heißt also, alles, was wir ahnen,
komplexen Datentypen haben, also Race, irgendwelche Records,
also ein C, dieses Structs, gibt es nicht.
Das hat der Compiler vorher alles aufgelöst.
Es gibt wirklich nur beidweise adressiert, ganz zahlige Werte,
die dann eben letztendlich entweder sowas wie Signed, Unsigned,
Zahlen darstellen mit unterschiedlichen Wortlängen oder eben als Weightvisor als Zeichen
in einer Kodierung dann also letztendlich interpretiert werden.
Gut, Glattkommadaten gibt es dann natürlich auch noch in dem beiden A-Triple-E-Simulator 54 Formaten,
wird jetzt hier in den Beispielen aber auch nicht weiter vertieft.
Also Glattkommaberechnung lassen wir jetzt hier auch gleich außen vor.
Und was man dann eben noch hat,
dass dann eben jetzt irgendwelche Daten,
irgendwelche Zahlen letztendlich verweise sein können,
also sprich Pointer auf irgendwelche Adressen,
um jetzt unter Programme aufzurufen oder eben letztendlich um sowas
wie indirekte Adressierung
zu machen
Gut an, Operation an Befehlen hat man dann eben die typischen Arten metisch-logischen Befehlen,
Befehle, die können sowohl mit immediat werden als auch mit Register,
als auch mit Speicherzugriffen arbeiten, wird gleich noch folgen.
Dann habe ich eben Befehle,
mit denen ich also quasi Datentransfer zwischen Speicher und Register machen kann,
also sprich Load and Store,
in unterschiedlichen Varianten und dann gibt es eben noch Befehle,
die man also jetzt quasi für den Programmablauf braucht,
also sprich Sprungbefehle, bedingt oder unbedingt,
unter Programmaufrufe und eben dann bestimmte Befehle oder Sachen im Wort,
also im Prinzip Interraps zu behandeln.
Ja, wie geht es also von diesem Beispiel,
Summe der Objektcode aus, also sprich, wenn ich dann diesen Assembler-Code,
also sprich dieses hier nehme und das dann also durch den Assembler schicke,
dann kriegt man also im Prinzip irgendwie Programmcode raus,
in diesem Fall 13 weit lang,
startet hier mit irgendeiner willkürlich gewählten Start-Adresse.
Also das ist dann also irgendeine Speicheradresse,
wo dann das Programm dann also quasi abgelegt wird.
Nicht erfolgen, also hier irgendwie 13 bald,
also hier jetzt quasi einfach Hexer-Dezimal runtergeschrieben.
Und die entsprechen dann eben letztendlich den Befehlen aus diesem Assembler-Code.
So, wie gesagt, der Assembler übersetzt Assembler-Code eben in
dieses Binärformat
Damit habe ich dann eben auch entsprechend wirklich eins zu eins mein Programm dann abgebildet,
was dann dort dabei eben noch passiert ist, dass das eben letztendlich so was wie Label durch Speicheradressen ersetzt werden. Das heißt,
wenn ich in meinem Code irgendwie sowas stehen hätte,
also an dieser Stelle springe zu diesem Label zurück,
dann macht der Assembler, dass er,
wenn er jetzt also beispielsweise diesen Code eben irgendwie an eine feste Startadresse lädt,
dass sich dann eben ein Rücksprung habe und an der Stelle dann eben die entsprechende Adresse,
die der Assembler dort gewählt hat, dann eben auch entsprechend dort eingetragen wird.
Ja, und am Ende des Ganzen,
um jetzt also da zum fertigen Programm zu kommen,
gibt es dann eben noch den Linker, beziehungsweise Binder,
damit werden dann im Prinzip diese Referenzen zwischen unterschiedlichen Objektdateien dann eben noch aufgelöst, nicht also typischerweise,
wenn ich einen C jetzt irgendwie ein etwas größeres Programm habe,
baue ich das jetzt nicht alles in eine einzige Quellcode-Datei,
sondern gruppiere die jetzt noch irgendwie so ein bisschen,
um zu sagen, also da habe ich jetzt irgendwie die Funktionen,
die das und das machen und da habe ich die,
die das und das machen.
Und dann habe ich eben irgendwo noch einen Quellcode,
wo dann eigentlich mein eigentlicher Main, also das Hauptprogramm stattfindet,
nicht die werden alle einzeln übersetzt vom Compiler beziehungsweise vom Assembler und vom Linker
dann eben letztendlich zu einem einzigen Programm zusammengelingt und gegebenenfalls dann eben noch Bibliotheksaufrufe aus jetzt irgendwelchen Funktionsbibliotheken,
die ich benutzt habe.
Also nicht typischerweise, um jetzt überhaupt erstmal irgendwie so eine Stringbehandlung zu machen,
um Speicherverwaltung zu machen, werden dann eben mit hinzugelingt.
Entweder statisch oder heutzutage typischerweise eigentlich eher dynamisch. Das heißt,
ich habe diese Bibliotheken irgendwo auch an einer bestimmten Stelle im Speicher.
Und wenn das Programm zur Ausführung kommt und ich also irgendwo
einen Aufruf auf die Funktion Print erfarbe in meinem Programm,
wird dann eben entsprechend der Code aus dieser Stelle,
aus dem Speicher dann eben entsprechend angesprochen
Gut, wie sieht das jetzt also hier bei diesem Beispiel aus?
Also wie gesagt, ich hatte ja diese hochkomplizierte Funktion,
die also zwei ganze Zahlen X und Y addiert.
Irgendwo im Assembler-Code taucht dann eben auf als entsprechende Äquivalent AdLong.
Und dann eben diese Zeile hier,
die er macht im Prinzip eben genau das,
dass der Inhalt von EIX genommen wird und da noch
was hinzuaddiert wird
Dieses, was hinzu addiert wird, liegt in diesem Fall
jetzt irgendwie dann als Argument auf ein Stack.
Wie gesagt, warum da acht? Und dann in Klammern,
die es EBP ist, wird später noch genauer erklärt.
Nicht aber letztendlich entspricht diese Zeile sowas wie so einem Ausdruck in C. Das heißt,
ich hätte dann also als meine Operanten so bearbeitet,
dass also quasi in Register IAX,
also der Wert von X liegt und in Register Oder der Operant Y,
dann irgendwo im Speicher unter dieser Adresse zu finden ist.
Und dieses ist dann nichts anderes,
als diese Speicheradresse zu holen, hier hinzu zu addieren.
Und das Ergebnis landet dann auch wieder in EAX und dass
wir dann eben ansprechen, mein Ergebnis. So,
und wenn ich das jetzt durch eine Samra schiebe,
taucht dann also irgendwo als Äquivalenter zu auf im Objektcode ein Drei-Byte-Befehl,
der dann also quasi 0-3,
4-508 ist
Findet sich dann also irgendwo hier zwischendurch innen drin, in dem hier. So,
wenn ich jetzt im Prinzip so ein Objektcode habe,
das, was man machen kann,
um daraus jetzt wieder so ein bisschen zu sowas wie Assemblercode zu gelangen,
gibt es dann sogenannten Disassembler,
kann man beispielsweise bei den GCC-Tools mit dem Object-Tank machen,
wo ich dann also wirklich jetzt irgendeinen ausführbares Programm oder auch jetzt also meinen assemblierten Objektcode nehme,
den dort reinschiebe und kriege dann also quasi irgendwie so ein Listing raus,
wo ich also letztendlich einmal hier Nichts anderes habe als diese Abfolge,
nur jetzt eben so formatiert,
dass das also befehlsweise zusammengefasst ist und hier auf der rechten Seite habe ich dann eigentlich die zugehörigen Assembler-Befehle.
Das heißt, also hiermit kann ich aus dem sowas
erzeugen und dann eben so ein Reverse-Engineering machen.
So, und das ist etwas, weil Befehlsatz,
also sprich die Zuordnung zwischen Befehl und dementsprechenden Bits dringen,
das ist ja für die Prozessearchitektur
fest vorgegeben
Das heißt, für X86-Prozessoren sieht ein Return eben immer so aus,
dass das mit C3 kodiert wird. So, das heißt,
so ein Desassemblieren ist unabhängig vom Betriebssystem und unabhängig davon,
als ob das mein Code oder was weiß ich was ist.
Kann man sich folgendermaßen machen. Also ich habe hier Ein
Programm namens Notepad plus plus.excel. Das ist ein 32-Bit Windows-Programm
, Binärcode. Also im Prinzip kann ich das so mit
jedem x-beliebigen Programm machen, also sprich Code für x86-Rechner.
Hier unter Linux, wenn ich das ausführen wollte,
würde sagen, ist nicht, mag ich nicht.
Wenn ich das unter Windows mache,
wurde dann also im Prinzip nichts anderes als ein Notepad mit
ein bisschen erweiterten Funktionen aufpoppen und ich kann damit jetzt also
irgendwie einfache Texte editieren
So, und dieses Programm kann ich jetzt also
auch hier unter Linux, ohne Probleme.
Durch den Dessertsembler schicken und kriege dann im Prinzip also hier komplett raus,
was sich dort irgendwie theoretisch und so weiter alles tut.
Gut, davon jetzt von irgendeinem halbwegs komplexen Programm,
also wirklich ein Reverse-Engineering zu machen, will man nicht zwingend,
aber im Prinzip ist es möglich.
Ich muss ja auch nochmal darauf hinweisen,
das ist jetzt ein Public Domain-Programm
Da darf ich das vielleicht noch gerade machen. Bei originalen Microsoft-Programmen
ist das aufgrund der Lizenzrechte, die man irgendwann abgenickt hat,
natürlich verboten. Nichtsdestotrotz würde es funktionieren. Also das heißt,
ich kann es in den Internet-Explorer nehmen oder von Disassembling machen.
Gut, jedenfalls, also das jetzt zu der Sache,
was im Prinzip Disassembler macht, ist,
dass er dann eben letztendlich aus ausführbarem Programm oder Teilen
von diesem Programmobjektcode dann eben wieder so ein Assemblercode zurückmacht.
Ob dieser Assemblercode dann wirklich auf der Hardware ausgeführt werden kann,
hängt dann natürlich wieder davon ab, was das Betriebssystem dazu passen muss,
dass da noch diverse andere an Bedingungen passen müssen,
sprich werden da irgendwelche Sachen aufgerufen,
für die ich besondere Rechte brauche und,
und, und.
Aber im Prinzip ist das eben auch vor allem nur eine eins
zu eins Textersetzung von für uns unleserlich und maschinenlesbar bis zu theoretisch und bedingt leserlich,
auch wenn man das nicht zwingend machen möchte.
Gut, neben dem Obi-Jod-Darm geht das also im Prinzip auch mit anderen Programmen,
also im Prinzip bei den GCC gibt es dann also auch so einen Deburger,
auch damit kann ich im Prinzip sowie hier angedeutet letztendlich
genau das Gleiche erreichen
Gut, also hier ist das Ganze auch noch mal kurz beschrieben,
was Sie gerade vorgeführt haben und nicht was kann,
deshalb aszembliert werden, eben letztendlich, was alles,
was ausführbarer Code ist,
weil eben wirklich nur so eine eins zu eins Übersetzung der Bytes zu den jeweiligen zugehörigen Befehlen letztendlich stattfindet.
Gut, ja, also jetzt kommen wir zu der eigentlichen Semmler-Programmierung.
Dazu werde ich jetzt also heute jetzt mal anfangen,
noch ein bisschen die verschiedenen Adressierungsarten vorzustellen.
Ja, sollte eigentlich hinhauen, auch noch ein bisschen was
zur atmetischen Operation und zu Statusregistern zu sagen.
Und quasi dann abschließend zu dem Teil,
jetzt zu dem oder ersten Teil der Semmler-Programmierung,
wird es dann noch so ein bisschen darum gehen,
wie jetzt also quasi Programmstrukturen,
also sprich if Verzweigung oder irgendwelche Schleifen
Im SM-Lakkot letztendlich eingesetzt werden. Wie gesagt,
hier werde ich jetzt sehr stark eingeschränkten Assembler-Code nur benutzen.
Das heißt, wirklich nur mit Long-Datentypen, also 32 Bittbreiten-Datentypen.
Und mit den acht Registern, die man jetzt also quasi beim,
dem 32-Bit-Befehlssatz dort dann eben letztendlich benutzt. Wie gesagt,
das, was eigentlich dann im Befehlssatz möglich ist,
wird jetzt also hier im Großen und Ganzen
erst mal wecken lassen
Die Beispiele hier haben jetzt also alle die Syntax von dem GCC,
beziehungsweise von dem Gnur-Assembler, also sprich,
wenn ich jetzt hier mal einen Befehl habe, Quelle,
Ziel und so weiter, wird das also dort immer so kodiert.
Bei anderen Assemblern, also beispielsweise, wenn ich jetzt den von denen teile,
also irgendwie das Visual Studio benutzen würde und dort also den entsprechenden Assembler irgendwie aufrufe,
um mir das ansehe, kann das sein,
dass das beispielsweise dann eben anders aussieht nicht,
weil das ist dann eben letztendlich immer von der Compilers wieder abhängig,
also ob ich einen Intel oder einen Microsoft-Compiler oder irgendwelche anderen Compiler nehme.
Ja, ich habe hier jetzt nachfolgend,
also bei den Grafiken nicht immer die Quellenangabe mit dran,
sondern da soll jetzt allgemein gelten, also alles,
was es in Kapitel 15 noch irgendwie an Grafiken folgt,
wenn das nicht gerade durch irgendwie so eine Referenz entsprechend nochmal gekennzeichnet ist,
ist aus dem Buch von Briant und O'Halleran,
Computersystems,
Programmers Perspective bzw. aus dem Foliensatz,
wo ich dann also auch schon diese vorherigen Beispiele
quasi rausgezogen habe
Gut, ja, wie sieht das jetzt, Assembler-Code erstmal rein praktisch aus?
Dazu fangen wir jetzt also erstmal mit ganz einfachen Befehlen an.
Also dieses hier sind, nicht so viel zu den entsprechenden Namen,
die acht Register, die man zur Verfügung hat,
rein praktisch werden davon jetzt hier immer sechs benutzt und diese
beiden ESP und E-BP werden dann nämlich für die Verwaltung des Decks benutzt.
Das steht dann nämlich für Stackpoint da und BP für Basepointer,
wird uns dann, wenn ich Nächste Woche dann Unterprogrammaufrufe vorstelle,
dann auch genauer begegnen, was damit dann gemeint ist.
Gut, ansonsten habe ich hier eigentlich erstmal nur sowas wie sechs Register.
Einen der einfachsten Befehle ist einfach nur ein Datentransfer-Move, beziehungsweise Move Long.
Das heißt, der transferiert dann eben eins,
32 Bit Long Ward, ist eine Instruktion,
die vergleichsweise häufig benutzt wird.
Sprich kopiert irgendwas von Source
nach Destination
So, und ganz Operanten für Songs beziehungsweise für Destination
kann ich dann eben entweder immediate-Werte haben. Okay,
beim Destination macht ein Emidiate keinen Sinn, aber zumindest als Source.
Also sprich, einfach eine Zahl, also eine Konstante.
Im Assembler-Code wird das immer mit einem Dollar davor gekennzeichnet.
Und wenn da nichts weiter folgt, ist das ein Entager-Wert,
das heißt, dieses wäre dann eben eine minus 533 oder ansonsten,
wie in der C-Sintax,
mit einem 0X für Hexadezimal,
dann eben Hexadezimal
Damit kann man dann eben seine konstanten schreiben mit diesem Briefix.
Ich kann sonst als Operanten irgendeins von diesen Registern benutzen.
Wie gesagt, typischerweise wird das eins von diesen sechs hier sein.
Oder Source oder Destination können eben Speicheradressen sein.
Das heißt, sich dann auf den Hauptspeicher beziehen.
So, das ist hier nochmal so
ein bisschen weiter
Aufgeschlüsselt mit Beispielen. Also ich möchte einen Emediate-Wert in einen Register schreiben,
würde dann eben aussehen, Move Flong, Dollar 04X,
nein, 0X4, also im Prinzip Hexer dezimal 4 jetzt
als Emediate-Wert und soll gespeichert werden in das Register EAX.
Ich möchte Animediate-Wert in den Hauptspeicher schreiben.
Dies wäre Emediate-Wert,
dezimal minus
Und die Hauptspeicheradresse wird in diesem Fall aus dem Register EAX gelesen.
Das ist dann nämlich diese Situation, wenn das Register in Klammern steht.
Das heißt, dieses ist dann eine Speicheradresse,
die sich dann eben auf den Hauptspeicher bezieht.
So, ich möchte Register in Register kopieren,
also beispielsweise EAX nach Edex, wäre dann sowas.
Ich möchte Register in den Hauptspeicher kopieren
Entsprechend das Äquivalent dann dazu,
Hauptspeicher in Register wäre sowas und was hier unten eigentlich noch fehlt,
Hauptspeicher in Hauptspeicher, würde auch gehen,
dann hätte ich also hier quasi beide Argumente mit diesen Geklammerten ausdrucken.
Gut, hier sind noch mal grob diese Adressierungsarten skizziert,
also Emidiat immer mit dem Dollar davor.
Register ist dann immer Prozent und dann eben irgendwie der Name
des Registers und das sind dann eben typischerweise eins von diesen
sechs Registern
Willst du mal sagen, eigentlich acht, da habe ich gesagt,
da waren praktisch 500, 6 benutzen. So,
und bei den Hauptspeicheradressen hat man dann das eben so,
dass ich also letztendlich aus einem Register immer die Adresse nehme und
dann eben mit dieser Klammerung die eigentliche Speicheradresse draus mache.
Das heißt, in ICX habe ich eine Adresse,
die mir irgendwo auf den Hauptspeicher zeigt und von da aus hole ich dann eben ein Argument.
Das wäre jetzt also nur mit Klammern, dann gibt es
dann noch so eine Art, Aber es gibt eigentlich noch zwei Erweiterungen.
Eine vorkommt hier noch die andere an zwei Folien später,
wo ich dann also im Prinzip noch mit dem Offset arbeiten kann.
Das heißt, dann kommt vor diese Klammer noch ein Displacement,
das sind letztendlich nichts anderes als ein Indija-Wert,
den ich dann also dort noch davor schreibe.
Das heißt, das, was hier passiert ist,
den Inhalt von eBPP zu nehmen,
plus 8 zu rechnen und das ist dann die Speicheradresse,
von der gelesen wird und hier reingeschrieben wird.
Gut, Hier sieht man jetzt also nochmal so Beispiele,
wie sowas dann also bereit und praktisch aussieht.
Und in diesem Fall ist das in Form eines Unterprogramms namens Swap.
Das macht nichts anderes, als dass es quasi hier zwei Pointer übergeben bekommt.
Das heißt, dieses XP und YP sind jetzt jeweils Speicheradressen.
Und das, was es machen soll, ist,
von diesen beiden Speicheradressen, die Inhalte vertauschen. Wie macht es das,
den Inhalt von YP irgendwie ein Hilfsregister Tenol speichern oder ein
Hilfserreable Tenol speichern bei YP genauso und dann eben quasi in umgekehrter Reihenfolge dann eben auf diese Adressen hier zurückschreiben.
Dass wir ja dann, also jeder Zugehörige,
jetzt C-Code, das, was der Assembler daraus macht,
ist dann eben etwas, was einmal hier oben bzw. hier unten,
also irgendwie diese Mimik beinhaltet, die man braucht,
um jetzt also quasi Unterprogramme zur Hand haben,
wirklich,
also später
noch genauer vorstellen
Und hier in dem eigentlichen interessanten Teil, in dem Body,
das entspricht dann, also quasi im Assembler-Code,
diesem Tal des C-Codes, wo ich dann nämlich nichts anderes mache,
als in EDX und ECX, mir hier,
also die Argumente vom Stack zu holen.
Stack wird eben hier durch diesen Basepointer beschrieben, wird,
also wenn ich später dann noch ein bisschen was zu den Unterprogrammen sage,
dann noch entsprechend genauer klar, was damit gemeint ist.
Nicht, also hier hole ich dann aus dem Hauptspeicher
etwas mit so einem Displacement, was ich also quasi,
bevor ich dieses Programm aufgerufen habe, dort reingespeichert habe.
Das wird dann also nach E-CX beziehungsweise nach EDX kopiert.
Wie gesagt, das war eine SV Pointer.
Hier würden dann also quasi die entsprechenden Inhalte in zwei Register
geladen und anschließend das dann eben hier wieder in umgekehrt zurückgeschrieben.
Gut, zu diesen Adressierungen bzw. Adressierungsformen, also hier nochmal
die ganz allgemeine Form dieser Indizierten Adressierung. Also hier hatte ich ja schon gesagt,
also mit den Klammern Ist dann immer eine Speicheradresse,
gegebenenfalls noch mit so einem Emediate-Wert hinzugefügt,
die allgemeine Form ist dann eben nicht nur die Speicheradresse,
sondern im Mediate, sondern da gibt es dann noch irgendwie,
wie die Unterscheidung, dass diese Speicheradresse eigentlich erstmal nur ein Basisregister ist.
Dann gibt es dazu noch einen Index und optional noch ein Skalierungsfaktor,
das heißt, die ganz allgemeine Form von so einem Speicherzugriff ist,
ein Emediate-Wert, dann in Klammern, Ein Register,
noch ein Register und eben ein Skalierungsfaktor 1, 2,
4 oder 8.
Und es wird dieser Ausdruck hier berechnet, das heißt,
der Inhalt des Registers RB, plus der Inhalt des Registers RI,
multipliziert mit einem Skalierungsfaktor, plus diesem Emediate.
Und das ist dann eine Adresse, auf die dann quasi im Hauptspeicher drauf zugegriffen wird.
Nicht so ist das Ganze dann letztendlich zu sehen. Und wie gesagt,
damit kann man Deswegen, ich hatte ja gesagt,
also X 86 Prozesse mit komischen, umständlichen Adressierungsarten.
Also hier ist also die Art und Weise, wie man
also auf eine Hauptspeicher darauf zugreifen kann, mit unterschiedlichsten Varianten rein.
Praktisch wird man dieses alles nicht so oft oder so häufig finden,
sondern eben meistens eben entweder nur so eine Registeradresse direkt haben,
gegebenenfalls noch mit irgendwie so einem Emediate-Quert.
Das ist also das, was ja bislang hier auch immer
schon in den Beispielen, also hier,
hier oder hier eben entsprechend aufgetaucht ist
Was man dann schon eher seltener hat, ist,
dann eben noch mit dieser Index-Adressierung,
wo dann eben noch ein zweiter Inhalts- und des zweiten Registers
als Index bzw. als Skalierter Index damit hinzukommt.
Gut, das ist also die Art und Weise,
ich hatte ja letzte Woche diese unterschiedlichen Adressierungsarten mal so kurz vorgestellt oder einiges skizziert.
Also das ist die Art und Weise, wie das also
jetzt beim X86 Assemblern beispielsweise Fall ist. So,
um das jetzt mal ein bisschen mit Leben zu füllen bzw. veranschaulichen,
habe ich jetzt hier mal so ein Beispiel für so eine Adressberechnung,
also angenommen in dem Register EDX, steht hexadezimal F-0-0.
In dem Register ECX steht 1-0-0,
dann würde dieser Ausdruck 0X8, Klammer auf, EDX,
Klammer zu, eben nichts anderes machen als den Inhalt von EDX zu nehmen,
diesen Offset 8 hinzuzuaddieren.
Das heißt, ich habe dann die Adresse F-0-08
und auf dieser Adresse im Hauptspeicher würde dann darauf zugegriffen werden
Hier würden dann EDX plus ECX berechnet, das heißt, der Zugriff wurde dann auf die Adresse F100 erfolgen.
Hier hätte ich das dann eben nochmal mit so einem Skalierungsfaktor,
also F000 plus viermal, exalentimal,
100 wäre dann eben als effektive Adresse F400 und hier hätte ich dann zweimal EDX plus Herr Exartizimal 8-0,
wäre 1E 08-0. Gut,
also da muss er jetzt einfach nur stumpf diese vorne,
beziehungsweise diese Berechnungsvorschrift nehmen, nicht und das,
was da jetzt eben stand, war jetzt also noch nicht der Speicherzugriff,
sondern also quasi diese Berechnung,
also das, was hier innen drin letztendlich stattfindet.
Gut, also das erst mal zu den Arten und Weisen,
was jetzt also hier letztendlich für Source oder für Destination stehen kann.
Welche Befehle hat man jetzt noch? Also wie gesagt,
also das Move Long haben wir jetzt ja schon kennengelernt. Mich,
dann habe ich noch im Prinzip die eigentlichen Rechenbefehle,
also klassische Alu-Befehle, also Addition, Subtraktion, Multiplikation,
sowas wie ein Linksschieben, entweder erst SA-LL oder SHL,
weil links schieben gibt es zwar einen Befehlssatz,
hier einmal arathmetisch und einmal logisch,
rein praktisch gibt es da aber keinen Unterschied.
Ich war beim links schieben wird rechts eben immer
eine Null reingeschoben
Deswegen machen die beiden Befehle eigentlich das Gleiche. Also auch das ist wieder so
eine Sache für so ein etwas imkonsistent oder Beispiel für so einen etwas inkonsistenten Befehlssatz.
Beim Rechtsschieben gibt es dann eben arenthmetisches und logisches Schieben,
die sich eben dadurch unterscheiden beim arenthmetischen Schieben wird,
das am weitesten linksstehende Bit, das Mood-Significant-Bild dann quasi immer vervielfacht,
nicht um dafür zu sorgen, wenn es eine positive Zahl ist, also eine Ansign.
Nein, also bei einer Zahlenzahl,
also quasi positiv mit einer Null vorne,
dass die dann eben entsprechend vervielfacht wird,
wenn es eine negative Zahl ist,
die eben immer dadurch gekennzeichnet ist beim Zweierkompliment,
was das am weitesten links stehende Bit an der Eins ist,
dann wird das eben entsprechend vervielfach,
sodass das Ergebnis dann eben logischerweise auch quasi in einer negativen Zahl entspricht,
die ja quasi durch Zweigeteilt und zunächst kleineren negativen Zahl abgerundet wird.
Gut, dann habe ich noch sowas wie Exklusiv oder,
oder jeweils dann als Bitweiser-Operation Das heißt also,
bei 32 Bit-Worten hätte ich dann eben die entsprechende Bitweise-Verknüpfung.
Nicht das ist ja alles das,
was wir sogar relativ am Anfang,
bevor er also mit den Schaltnetzen angefangen habe,
mal so grob als Funktion alles irgendwie schon kennengelernt haben.
Findet sich dann eben auch entsprechend eins zu eins,
wie man dann hier sieht, eben auch in dem Befehlssatz wieder.
Gut, dann gibt es noch unnähere Operatoren,
also sprich inkrementieren,
dekrementieren,
negieren,
beziehungsweise Nord
Was ist da der Unterschied? Zwischen diesen beiden Befehlen.
Also die Antwort war, dass einer und das Zweier-Kompliment
ist in dem Sinne auch völlig so richtig.
Also Negieren ist wirklich ein arrethenmetisches Negieren, also sprich letztendlich die Rechnung im Zweier-Kompliment.
Das Nord ist einfach ein Bidvises Invertieren, was aratometisch gesehen quasi mal einer Kompliment entspricht.
Insgesamt bei der Rechner-Aratometik muss man eben immer sagen,
einer Kompliment taucht dort eigentlich nicht auf,
sondern also bei allen Rechnern wird typischerweise immer mit einem Zweier-Kompliment gearbeitet,
bei negativen Zahlen. Und wie gesagt,
dieses Nord würde ich dann eben immer interpretieren,
einfach nur als ein Bitvises invertieren,
wo ich dann also wirklich ähnlich wie jetzt,
also bei irgendwelchen Betweisen-Operationen dann eben wirklich sowas mache,
um jetzt irgendwelche Maskierungen zu machen oder um es
bestimmte Sachen zu machen
Gut, dann gibt es noch einen Befehl.
LEAL, Load Effective Adress. Auch das findet man häufiger
dann mal im Assembler-Code wieder. Letztendlich ist das ein Befehl,
der quasi von dieser Adressberechnung diesen inneren Teil hier macht.
Das heißt, der macht jetzt nicht den Speicherzugriff selber,
sondern er berechnet eigentlich nur diesen Ausdruck.
Und da muss man auch sagen, das ist jetzt Auch wieder so eine Sache.
Eigentlich eben Adressberechnung gedacht. Und der Inhalt landet dann in einem Register.
Das wird aber ganz gerne von den Compilern benutzt.
Für arathmetische Operationen. Das heißt,
also da wird eben letztendlich dieser Ausdruck berechnet,
wo ich dann eben quasi Inhalt eines Registers plus Inhalt eines zweiten Registers plus Emediate,
beziehungsweise das hier noch multiplizieren, mit einem Skalierungsfaktor rechne.
Wo man dann sagen kann,
das wird dann eigentlich für irgendwelche,
oder anstatt irgendwelche Additionsbefehle teilweise dann genommen oder missbraucht
Gut, als Beispiel für solche arethenmetischen Operationen,
also jetzt mal ein Unterprogramm namens Arith,
hat drei Argumente, X, Y und Set,
berechnet dann als erstes X plus Y,
da geht dann quasi in so eine Hilfsvariable,
die wird dann eben noch Z hinzuaddiert,
dann gibt es eben hier noch diesen Ausdruck X plus 4,
dann hier Y mal 48,
dann neben hier T5 ist gleich T3 plus T4 und der Rückgabewert soll dann eben T2 mal T5 sein.
So, wofür auch immer das sinnvoll sein mag,
aber also irgendwo hat sich der,
also jetzt der Programmierer was dabei gedacht,
also im Prinzip also diese Funktion hier gemacht und implementiert.
Im Assembler Code findet sich dieses dann letztendlich an dieser Stelle
wieder. Was passiert da bzw. wie funktioniert das?
Das wird jetzt ein bisschen aus dieser Zeichnung deutlich
Und zwar macht man da jetzt also folgendes,
dass man sagt, bevor diese Funktion aufgerufen wird,
also sprich, dieser Assembler-Code, passiert folgendes,
dass auf dem Stack das Übergebende oder das Übergeordnete Hauptprogramm,
also dass das im Prinzip Arit aufruft, die Argumente X,
Y und Set auf dem Stack ablegt,
und zwar Umgekehrter Reihenfolge.
Das heißt also, ich habe hier mein Set,
hätte dann hier mal ein Y, hätte dann hier das X,
dann wird also quasi passiert der eigentliche Call von diesem Montagprogramm.
Dazu wird dann eben quasi eine Rücksprungadresse auf ein Stack gesichert.
Und anschließend, das ist nämlich das,
was er sich hier oben eigentlich passiert mit diesem Push und mit diesem Move,
wird der alte Basepointer auch auf ein Stack gesichert und der
neue Basepointer zeigt dann also quasi hier auf dieses Element vom Steak
So, das ist also erstmal die Voraussetzung,
das hat, da gibt es eben bestimmte Konventionen,
wie im Assembler-Code unter Programme aufgerufen werden.
Da müssen sich quasi alle Programme daran halten. Und die haben halt eben dafür gesorgt,
dass bevor dieses hier aufgerufen wird, dass also hier quasi stattfindet,
nicht anschließend wird das also hier so umgesetzt.
Und wenn jetzt also quasi hier der innere Teil von diesem
Programm ausgeführt wird, findet man also quasi auf dem Stack
diese Situation vor
So, und das, was hier jetzt passiert,
ist, dass jetzt also hier quasi gerechnet wird,
EAX bekommt den Inhalt von EBP mit dem Offset 8. EBP sagt ihr,
das ist jetzt quasi hier so auf dieser Adresse.
Offset 8 verweist auf das erste Argument. Das heißt,
hiermit kopiere ich den Inhalt von X in das Register EAX.
Anschließend wird mit dem Offset 12 Zum Basepointer. Ein etwas
vom Stack geholt und nach EDX kopiert. Nämlich das zweite Argument Y. Das heißt,
nach diesen beiden Befehlen ist ein ERX gleich X,
sondern EDX ist das Y. So,
und jetzt wird quasi dieser erste Ausdruck berechnet,
nämlich TA1, das gleiche X plus Y.
Das Ganze jetzt eben mit diesem nicht mit Maditionsbefehl,
sondern mit diesem Load ERL,
wo ich dann eben jetzt hier einfach rechne,
Plus EAX, das Ergebnis landet in ECX. Und damit
habe ich dort dann also quasi das T1 berechnet. So,
und jetzt sieht man eben auch schon, ich habe gesagt,
also Compiler sind also relativ schlau und machen also irgendwie alle möglichen fancy Optimierungen,
wo man, sagen wir mal, jetzt so als,
wenn man jetzt von Han selber Assembler programmieren würde,
vielleicht immer gar nicht so drauf kommt oder es relativ umständlich ist.
Und das behandelt häufig eben die Handhabung von irgendwelchen Konstanten. Also ganz am Anfang,
als es mal so ein bisschen um die Zahlendarstellung ging,
hatte ich ja auch schon mal so was vorgestellt,
wo ich also im Prinzip gesagt habe, okay,
konstanten werden häufig durch irgendwelche Bit-Masken und Schiebebefehle irgendwie dargestellt.
Was ähnliches passiert hier jetzt nämlich auch, wo man jetzt sagt,
okay, also hier mit diesem Ausdruck, EDX,
EDX 2 wird dreimal Y berechnet. Nämlich nämlich der Inhalt,
also in EDX habe ich Y plus und das ist jetzt,
also hier bei dieser Adressierung, nicht furchtbar,
dass es quasi mit diesem Skalierungsfaktor berechne,
mache ich also im Prinzip an dieser Stelle also nichts anderes als Y plus zweimal Y.
Und das Ergebnis wird dann auch wieder in EDX gespeichert.
Das heißt, dort habe ich dann dreimal Y. So,
und dann wird dieses dreimal Y um vier Bit nach links geschoben.
So um vier Bit nach links schieben ist,
mal zwei, mal vier, mal acht mal 16.
Das heißt, damit habe ich dreimal Y, mal 16,
Und das ist dann nichts anderes, als Y48.
Also das war so ein Beispiel, wie jetzt also im Prinzip,
anstatt Multiplikation mit 48 zu machen,
das jetzt also quasi einfach in entsprechende Additionen bzw. Schiebebefehlers übersetzt wurde.
Gut, dann werden jetzt eben hier noch entsprechend dann also
hier diese anderen Teilausdrücke, also X plus 4,
nicht an dieser Stelle
Nee, das passiert hier dann also entsprechend schon implizit mit drin.
Also hier ist jetzt Bereich noch von T2, also Z plus T1.
Da wird jetzt also hier jetzt quasi nochmal das Z geholt und T1 hinzu addiert. Hier wird dann also im Prinzip T5 berechnet,
nämlich die 48 Y,
die ich also quasi hier in T4 gespeichert habe,
plus T3, also plus X plus 4,
nicht was in diesem Fall dann auch hier einfach durch diesen Offset plus 4,
das sind die 48 Y und das ist dann eben das
Plus X und das Ergebnis landet dann wieder ein EAX berechnet wird und dann habe ich ihm hier noch am Ende diese abschließenden Multiplikationen.
Also das ist jetzt die Art und Weise,
wie der Compiler letztendlich dann diese Ausdrücke umgesetzt und realisiert hat
Gut, wie man sieht, teilweise ein bisschen tricky,
also gerade mit dieser Optimierung. Andererseits, wie gesagt,
man muss das dann einfach von den Befehlen mal einzeln nachvollziehen,
beziehungsweise sich dann eben angucken, was sich da dann einzeln hinter verbirgt,
dann kann man das auch einigermaßen verstehen, nachvollziehen.
Wichtig ist dabei dann eben auch, dass man letztendlich,
bevor ich dieses hier richtig interpretieren kann,
ich dann eben auch wissen muss,
wie der Stack aufgebaut ist bzw. wie da die Parameter dann aussehen,
weil sonst kriege ich das natürlich dann von der Funktion
so auch nicht umgesetzt
Gut, wie gesagt, zu dem Stack und den Sachen
werde ich dann also auch nächste Woche noch was genauer vorstellen.
Ansonsten werde ich jetzt also hier an dieser Stelle für heute Schluss machen.
Wie gesagt, das war jetzt erst mal so ein Beispiel,
wie im Prinzip Assembler-Code jetzt im Fall von solchen aratmatischen Operationen aussieht.
Nächste Woche geht es dann eben entsprechend noch ein bisschen weiter mit Programmkonstrukten,
also sprich Verzweigung und dann eben unter Programm aufrufen als Beispiel,
wie man dort dann eben entsprechend arbeitet.
Gut, damit soll es das vor heute gewesen sein.
Ich wünsche allen ein schönes Wochenende und dann bis nächste Woche