Assembler (1 / 3) - Grundlagen

    • [ASM]

    Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

    • Assembler (1 / 3) - Grundlagen

      Hi Leute,

      was dem Board meiner Meinung nach wirklich fehlt, ist ein umfangreiches Assembler Tutorial, mit dem auch Anfänger etwas anfangen können. Ich bemühe mich, eine anfängerfreundliche Sprache zu benutzen und alles möglichst Anschaulich zu erklären.
      Inhaltsverzeichnis:

      I: Grundlagen

      1. Zahlensysteme
      1.1. Das Dezimalsystem
      1.2. Das Hexadezimalsystem
      1.3. Das Binärsystem

      2. ROM und RAM, Offsets und Pointer

      1.1. Das ROM und das RAM
      2.2. Offsets und Pointer
      2.3. Datentypen
      2.3.1 Bits
      2.3.2 Bytes
      2.3.3 Das Hword
      2.3.4 Das word

      3. Aufbau des Memory

      II: Assembler
      III: Researching




      1. Zahlensysteme

      1.1.Das Dezimalsystem

      Im Laufe des Sturktur- und ASMhackings werdet ihr häufiger über verschiedene Zahlensystem stolpern. Normalerweise benutzen "normale" Menschen das Dezimalsystem, also das, was jedes Kind in der Grundschule gelernt hat. Dezimalsystem (Dezi = 10), weil wir 10 verschiedene Ziffern benutzen: 0,1,2,3,4,5,6,7,8,9. Sobald wir eine Zahl über 9 haben, erhöhen wir die nächsthöhere Stelle um 1, in dem Fall die Zehnerstelle. Aus 09 wird also 10. Ganz logisch eigentlich, sollte noch jedem einleuchten. Haben wir dann die höchste Ziffer an der Zehnerstelle erreicht, so würden wir die Hunderterstelle um 1 erhöhen. Nach 099 würde demnach 100 kommen. Das Prinzip sollte klar sein, oder?

      1.2. Das Hexadezimalsystem

      Andere Zahlensysteme arbeiten nach exakt dem gleichen Prinzip. Ist in in einer Stelle die höchste Ziffer erreicht, wird diese auf 0 zurückgesetzt und dafür die nächsthöhre Stelle erhöht. Ich stelle euch nun das System vor, das ihr am häufigsten brauchen werdet. Das Hexadezimalsystem (Hexa = 6, Dezi = 10 -> 16) umfasst 16 verschiedene Ziffern. Nun fragt man sich, wie diese neuen 6 Ziffern aussehen? Ganz einfach, wir verwenden nun auch Buchstaben als Zahlen. Im Hexadezimalsystem ist nich 9 die höchste Ziffer sondern F. Die Ziffernfolge sieht also so aus: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F.
      Nach der 9 würde also nicht 10 kommen, da 9 ja noch nicht die höchste Ziffer ist. Stattdessen geht es mit A weiter. Erst wenn wir F erreicht haben, also die größte Ziffer, setzen wir diese zurück und erhöhen die Zehnerstelle um 1. Nach F kommt also 10. Verstanden?
      Ein weiteres Beispiel: Wir haben die Zahl 19. Wer jetzt meint, dannach würde 20 kommen, der hat nicht aufgepasst! Nach 9 ist nicht Schluss, sondern es folgt A. Nach 19 kommt also 1A. Das letzte Beispiel: Der Wert 9F. Na, wer kommt drauf. F ist die höchste Ziffer, also erhöhen wir die nächste Stelle, in dem Fall die 9. Was kommt also nach 9F. 100? Nein, natürlich nicht! 9 ist nicht die höchste Ziffer. Nach 9F kommt A0.
      Das alles mag am Anfang sehr merkwürdig erscheinen, aber nach einiger Zeit gewöhnt man sich an dieses Zahlensystem. Dass wir 16 Ziffer haben ist zum Programmieren sehr viel geeigneter. Zur unterscheidung von Dezimalwerten und Hexadezimalwerten verwendet man immer dann, wenn man einen Wert in Hex (hexadezimal) angibt das Kürzel 0x. Also wollen wir den Wert 20 in hexadezimal angeben, so schreiben 0x20. Dies ist insofern wichtig, dass 20 und 0x20 nicht dem gleichen Wert entsprechen. Würde man 0x20 ins Dezimalsystem zurückrechnen, so erhielte man den Wert 32. Manchmal wird auch statt 0x das Kürzel &H verwendet. Generell macht es oft Sinn, Angaben in Hex zu machen, warum das so ist, werdet ihr später noch feststellen.

      1.3. Das Binärsystem

      Das weitere Zahlensystem, dass ich vorstelle, ist das Binärsystem (Bi = 2). Hier haben wir nur 2 Ziffern! Verdammt wenig, nicht? Ja, da habt ihr schon Recht, 2 Ziffern sind sehr wenig, und zum Rechnen ist dieses Zahlensystem herzlich ungünstig. Generell gilt aber, dass der Gameboy nur mit Binärinformationen arbeitet, auch wenn wir diese zusammengefasst bearbeiten. Die zwei Ziffern sind 0 und 1, und beim Zählen verfährt man so wie oben. Nach 0 kommt 1. Da 1 die höchste Stelle ist, wird nun schon die nächste Stelle erhöht, also kommt nach 1 gleich 10. Dannach 11, dann 100, dann 101, 110, 111, 1000, 1001, 1010, 1011, 1100, 1101, 1111, 10000.... Das Prinzip wiederholt sich und ist in jedem Zahlensystem gleich. Dennoch ist es äußerst unwahrscheinlich, dass ihr mit diesem Zahlensystem rechnet, brauchen tut ihr es aber fast immer! Binärwerte gibt man üblicherweise mit dem Kürzel &b oder nur b an. Auch hier unterscheiden sich 10 und b10, welches im Dezimalsystem dem Wert 2 entsprechen würde.

      Diese drei sind essentielle Zahlensysteme, die ihr im Bereich Researching und ASM immer wieder brauchen werdet. Ein anderer, sehr wichtiger Punkt, ist dabei natürlich die Umrechnung. Wie rechne ich einen Dezimalwert in einen Hexadezimalwert um? Darüber braucht ihr euch keine Gedanken machen, da der Windows Taschenrechner das für euch übernehmen kann.
      Unter Ansicht -> Programmierer stellt ihr den Taschenrechner um. Nun könnt ihr, indem ihr die Markierung bei entweder Hex, Dez oder Bin setzt, die Werte in ihrem jeweiligen System eintippen. Ändert ihr die Markierung dann, so werden die Werte automatisch umgerechnet.

      Was nehme ich aus diesem Kapitel mit?
      • Dezimalsystem: Ziffern 0-9
      • Hexadezimalsystem: Ziffern 0-F
      • Binärsystem: Ziffern 0-1

      2. ROM und RAM, Offsets und Pointer

      2.1. Das ROM und das RAM

      Ja, viele sind schon über den Begriff RAM gestolpert, doch was genau ist eigentlich dieser RAM und worin unterscheidet er sich von ROM. Der Begriff ROM (= Read Only Memory) bezeichnet einen Speicher, aus dem man Informationen ablesen kann, diese aber selbst nicht verändern kann. Deswegen ein Read Only (nur Auslesen) Memory. Wir können uns also auf Werte aus dem ROM beziehen, aber keinen ändern, während das Spiel läuft. Werte, die feststehen und sich nicht mehr ändern lassen, nennt man auch statische Werte. Werte die sich hingegen ändern lassen, werden als dynamisch Bezeichnet. Genrell bezichnet Dynamik etwas, das noch nicht feststeht, etwas variables und flexibles. Womit wir auch schon beim Begriff des RAMs währen.
      RAM (= Random Access Memory) bezeichnet einen Speicher, aus dem wir Informationen auslesen aber auch abändern können. Wollen wir also eine Information, die wir während des Spielverlaufes noch ändern können, so muss diese dynamische Information im RAM stehen. Generell baut sich der Speicher des Spiels, während es aktiv ist so auf, dass erst das RAM generiert wird, und dannach das ROM geladen wird.

      2.2. Offsets und Pointer


      Soweit so gut, wie beziehen wir uns aber auf eine Information? Jede einzelne Information sowohl im ROM als auch im RAM hat eine Adresse, eine Hausnummer gewissermaßen, mit der man sich auf eben diese Information beziehen kann. Will man also die Information auslesen, so muss man sich immer auf diese Adresse, die eine Information hat, beziehen. Diese Adresse bezeichnen wir fortan als Offset. Das Offset einer Information ist also seine Adresse, die entweder aufs RAM oder ROM verweist.
      Wenn wir uns jetzt also konkret auf eine Information beziehen, müssen wir seine Adresse angeben. Diese Angabe der Adresse nennen wir einen Pointer, da er auf die Information zeigt (eng. pointed). Wenn wir also auf eine Information verweisen, existiert immer ein Pointer auf diese Information, den wir auslesen müssen.
      Da jede Information, auch die die aus mehreren Bytes(siehe unten) bestehen, in Bytes aufgeteilt sind, gibt ein Offset immer die Adresse eines einzelnen Bytes an, während der Pointer zwar auch nur auf diesen Byte verweist, aber so ausgelesen werden kann, dass mehrere Bytes geladen werden. Offsets werden üblicherweise in Hex angegeben.

      2.3. Datentypen

      2.3.1 Bits

      Aber was für Informationen gibt es denn nun eigentlich? Die ganze Zeit rede ich hier von irgendwelchen Zahlensystemen, in denen wir Informationen angeben können, und Adressen und Pointern, die auf diese Informationen zeigen, aber von den Informationen selbst hab ich noch gar nichts gesagt. Das hat auch seine Gründe. Zuerst einmal müsst ihr nämlich verstanden haben, was hex, was dez und was bin bedeuten, und auch was ein Pointer ist. Aber nun genug der Reden. Generell liegt jede Information binär vor, also entweder als 0 oder als 1. Da wir mit derart kleinen Informationen unmöglich arbeiten können, fassen wir mehrere dieser Informationen, also mehrere Binärinformationen, sogenannte Bits zusammen. Dennoch - es gibt auch Informationen, die binär vorliegen, und welche dann Tatsächlich nur 0 oder 1 als Wert haben können. Diese Ja/Nein, oder auch gerne wahr/falsch (zu englisch true/false) Information wird oft auch als boolean bezeichnet. Derartiges interessiert uns aber eher weniger, wir merken uns, dass die kleinste Information ein 1-Bit Wert ist.

      2.3.2. Bytes

      Die nächstgrößere Information ist der Byte. Ein Byte fasst 8 Bits zusammen und bietet uns folglich wie viele verschiedene Möglichkeiten als Wert? Die, die ein wenig mathematisch bewandert sind, können sich denken, dass man einfach nur 2 hoch 8 rechnen muss, kurz gefasst - ein Byte hat 256 verschiedene Werte, die er annehmen kann. Diese sind logischerweise alle Zahlen im Bereich 0-255 bzw 0x0-0xFF. Ist jemandem etwas aufgefallen? Genau, 0xFF ist der höchste zweistellige Wert, der sich über hexadezimal ausdrücken lässt. Hier sehen wir wieder, dass es durchaus Sinn macht, 8 Bits als einen Byte zusammenzufassen. Dennoch - eine Informationsreichweite von 0-255 ist immer noch sehr mager. Wir schauen uns also die nächstgrößere Information an.

      2.3.3. Das Hword


      Da wäre noch das Hword, das seinen Namen daher hat, dass es ein halbes Word darstellt. Ein Hword fasst 2 Bytes bzw 16 Bits zusammen und hat somit eine Informationsreichweite von 0-65535 bzw 0x0-0xFFFF. Wichtig ist, dass generell alle Informationen, die mehr als einen Byte umfassen, immer byteverkehrt dargestellt sind. Dies hängt damit zusammen, dass die CPU diese Informationen schneller verarbeiten kann. Was heißt denn aber nun "byteverkehrt"? Wir betrachten uns das Hword 0xABCD. Im ROM finden wir jede Information als Byte vor, also sollten wir hier die beiden Bytes AB und CD erwarten, in der Reihenfolge AB CD. Dies ist aber nicht der Fall. Hier kommt der Trick -> Die Reihenfolge der Bytes wird umgedreht! Aus AB CD wird CD AB.

      2.3.4 Das Word

      Die größte Information, die wir behandeln werden, ist das Word. Wenn ein Hword 16 Bits umfasst, macht es Sinn, dass das Word 32 Bits bzw 2 Hwords, bzw 4 Bytes umfasst. Damit ergibt sich eine Informationsreichweite von 0-4294967295 bzw 0x0-0xFFFFFFFF. Und auch hier gilt wieder, dass Informationen dieser Größe byteverkehrt vorliegen. Aus 0x01020304 - in Bytes gefasst 01 02 03 04 - würde also 04 03 02 01 werden, und wenn wir diese Information hinterlegen wollen, müssen wir sie auch byteverkehrt darstellen. Übrigens: Diese Darstellung wird im allgemeinen auch als "Little Endian" bezeichnet.

      Was nehme ich aus diesem Kapitel mit?

      • ROM: Nicht beschreibbarer Speicher, aus dem Informationen ausgelesen werden können
      • RAM: Beschreibbarer Speicher, aus dem Informationen ausgelesen werden können
      • Offset: Adresse einer Information
      • Pointer: Verweis auf eine Adresse
      • Datengrößen:
      • 1-Bit
      • 1 Byte bzw 8-Bits
      • 1 Hword bzw 2 Bytes bzw 16-Bits
      • 1 Word bzw 4 Bytes bzw 32-Bits
      • Informationen liegen byteverkehrt vor
      3. Der Aufbau des Memory

      Ja, ich habe bereits von ROM und RAM gesprochen, auch von Adressen von Informationen und Pointern, aber bisher war das ganze doch eher allgemein gefasst und sehr abstrakt. Nun beziehe ich all diese Basics auf das Memory, also den Speicher des GBAs. Das Memory unterteilt sich ins RAM und das ROM, wobei das RAM direkt am Anfang steht, also die Adresse (das Offset) 0x0 hat. Das ROM hingegen beginnt bei 0x8000000, sprich alle Daten, die wir im Hex-Editor in unserem ROM vorfinden, werden an dieses Adresse (dieses Offset) geladen. Folglich müssen wir, wenn wir uns auf Informationen im ROM beziehen zu dem Offset der Information, wie wir sie im Hex Editor vorfinden, 0x8000000 addieren.

      Es folgt nun eine genaue Untergliederung des GBA Memory:
      [table='1,2,3']
      [*] Offset [*] Bezeichnung [*] Beschreibung
      [*] 0x00000000 [*] BIOS [*] Hier befinden sich Informationen für das GBA BIOS. Faustregel: Finger weg von diesem Bereich!
      [*] 0x02000000 [*] WRAM [*] Hier sind sehr viele dynamische Informationen gespeichert, unter anderem Variablen und Flags. Dieser Ort eignet sich hervorragend für eigene Codes, da sehr viele und große Bereiche ungenutzt sind.
      [*] 0x03000000 [*] IRAM [*] Dieser Bereich des RAMs verhält sich ähnlich wie der WRAM, nur kann man schneller auf ihn zugreifen. Ich rate dennoch davon ab, den IRAM zu benutzen, da sehr viele Systemfunktionen wie DMAs u.a. auch IO-Register hier ihre buffer haben, und man sehr schnell etwas kaputt machen kann.
      [*] 0x04000000 [*] IO-RAM [*] Hier sind die IO-Reigster gespeichert, die sehr stark geschützt sind - und das nicht ohne Grund. Wenn man nicht genau weiß, was man tut - Finger weg!
      [*] 0x05000000 [*] Palette-RAM [*] Hier sind sämtliche Paletten geladen, aktive sowie Sicherungskopien und andere. Teile des Palette-RAMs sind durch die DMA3 geschützt.
      [*] 0x06000000 [*] VRAM [*] Das Visual-RAM. Hier sind sämtliche aktive Bilddatein dekomprimiert und abgesichert, und großteile des VRAMs sind schreibgeschützt. Der VRAM wird für grafische Funtkionen eine Rolle spielen, da alles, was sich in
      [*] 0x07000000
      [*] OAM
      [*] Hier sind die Attribute der Objekte gespeichert. Auch das braucht uns vorerst nicht zu interessieren.
      [*] 0x08000000
      [*] ROM[*] An diese Adresse wird später das ROM geladen
      [/table]
      Das ist im Groben der Aufbau des Memorys, und wie ihr schon gesehen habt, haben die einzelnen Bereiche sehr viel Speicher. Dennoch bleibt für die Hacker selbst, die den RAM benutzen wollen, nicht all zu viel übrig. Meistens wird der WRAM genutzt, um selbst dnymiasche Daten abzuspeichern.

      Wollen wir dem ganzen ein bisschen mehr Praxisbezug geben? Angenommen ich will eine Information am Offset 0x02003000 auslesen, dann muss ich in meinem Code einen Pointer auf diese Information hinterlegen. Sprich ich gehe im Allgemeinen so vor:

      Quellcode

      1. lade : Label_0
      2. definiere Label_0, =0x02003000


      Wir laden also ein sogenanntes dynamisches Label, welches wir unten definieren. An diesem Offset wird dann ein Pointer auf unsere Information hinterlegt sein. Im Hex Editor würde uns diese beispielsweise so begegnen:

      00 30 00 02

      Ein anderes Beispiel: Ich will aus meinem ROM die Information am Offset 0x5EB auslesen. Also gehe ich vor wie oben:

      Quellcode

      1. lade : Label_1
      2. definiere Label_1, ...


      Na, was für einen Pointer hinterlege ich hier? Wer jetzt meint 0x5EB der hat nicht aufgepasst. Alle Informationen, die in unserem ROM liegen, werden später nach 0x8000000 geladen, was heißt, dass ich diesen Wert zu allen meinen Offsets addieren muss. Mein Pointer würde also so aussehen: 0x080005EB. Im Hex Editor würde uns dieser so begegnen: EB 05 00 08

      Was nehme ich aus diesem Kapitel mit?

      • Wie ist das Memory ungefähr aufgebaut
      • Wie bilde ich einen Pointer ins ROM bzw RAM
      Weiter gehts im Post unten!
      Wo war Gondor, als meine Klausurenphase begann?

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von Wodka ()


    • II: Assembler

      Nun sind wir auch schon beim Puntk Assembler angelangt. Assembler, oder
      kurz ASM ist für viele das große Mysterium des Hackings, das nur den
      allwissenden Profis alleine gehört, und das ein Normalsterblicher nicht
      verstehen kann. Dabei ist das ganze recht simpel und bedarf nur ein
      wenig Übung. Ich werde euch heute in die Sprache des Assemblers
      einweisen. Zu jedem Befehl werde ich immer ein keines Anwendungsbeispiel
      liefern, der richtige Praxisbezug, also wie man mit Assembler arbeitet,
      kommt aber erst in Kapitel III.

      1. Register und der Stack



      1.1. Was ist Assembler?

      Assembler ist die grundlegendste aller Programmiersprachen, der reine Maschienencode, mit dem Computer arbeitet. Grundsätzlich unterscheidet man beim Hacking zwischen zwei verschiedenen Befehlssätzen, die wir zur Verfügung haben: Einmal ARM und Thumb. Da gut 99% des Spiels in Thumb programmiert sind, werde ich mich auch auf diese Sprache beschränken.
      Assembler an sich arbeitet nach einem simplen Prinzip. Die ganze Sprache tut nichts anderes, als Werte auslesen, verändern oder neu berechnen und diese Werte dann an bestimmte Stellen im RAM zu schreiben. Ernsthaft, viel mehr macht und kann Assembler auch nicht. Einige mögen jetzt enttäuscht sein: Es heißt ja immer: "Mit Assembler ist alles möglich! Du kannst alles in dein Spiel einbauen, wenn du nur ASM kannst", und in der Theorie stimmt das vielleicht auch, aber ist vieles schlicht einfach zu kompliziert oder zu aufwändig, um es ins Spiel einzubauen. Dennoch ist Assembler sehr wichtig und man kann in der Tat sehr viele Dinge realisieren. Generell ist das Beherrschen der Sprache nicht einmal die halbe Miete - nur wer Assembler anwenden kann, ist in der Lage diese ganzen "Wunder und Features", von denen oftmals die Rede ist, hervorzubringen.
      Ich komme nun noch einmal zurück auf das obrige - Assembler tut nichts anderes, als Werte auslesen, verarbeiten und wieder ins RAM zu schreiben. Das könnte, in simpler Form so aussehen:

      Quellcode

      1. lade : Label_0
      2. lade die Information an Label_0 in einen Speicher
      3. addiere : Speicher, +1
      4. schreibe Inhalt von Speicher an Label_0
      5. definiere : Label_0, 0x02030000


      Dass diese Funktion nichts großartiges macht, sollte klar sein. Sie lädt zurest das Label_0, um dann einen Wert von diesem Offset, das im dynamischen Label definiert ist, in einen Speicher zu laden. Dann addiert sie zu diesem Speicher den Wert 1 dazu, und schreibt den Wert wieder an das Offset, das im dynamischen Label_0 definiert ist.
      Natürlich ist das noch kein Assembler-Code, aber zeigt dieser "Pseudo"-Code, wie Assembler funktioiniert.

      Was nehme ich aus diesem Kapitel mit?
      • Assembler leißt Werte aus, verarbeitet diese und schreibt sie in den RAM

      1.2. Die Register

      Gerade eben ist bereits der Begriff Speicher gefallen. Wann immer wir einen Wert, sei es von einem Offset oder einem dynamischen Label laden, so muss dieser Wert auch irgendwo gespeichert werden. Andernfalls könnten wir ja mit diesem Wert nicht arbeiten. Dazu bietet uns der Assembler 16 Speicher, von denen wir im thumb aber nur 8 (+2) verwenden. Diese Speicher werden als Register bezeichnet. Jedes Register kann dabei einen 32-Bit Wert, also ein Word speichern. Somit ist es logisch, dass es auch alle kleineren Informationen abspeichern kann. Das das funtkioiniert erläutere ich an einem Beispiel:

      Ich lade den Wert 0xFEFE (das ist ein 16-Bit Wert, ein Hword) in das Register #0. Jetzt sieht der Inhalt des Registers so aus: 0x0000FEFE. Dieser Wert ist ganz ohne Zweifel ein 32-Bit Wert, ein Word, enthält aber den selben Wert wie das Hword. Verstanden? Genauso würde es mit einem Byte funtkionieren.

      Die Register werden, zu ihrer Erkennung, über eine Nummerierung bezeichnet (Register 0, Register 1, Register 2, usw.). Wir im thumb-Befehlsset verwenden die Register 0-7.
      Anmerkung: Wie ihr gemerkt habt, beginnen die wir Nummerierungen nicht bei der Zahl 1, sondern 0! Diese Vorgehensweise ist im Bereich des Hackings (und auch Programmierens) üblich und ich rate euch, auch nach diesem Schema zu nummerieren.
      Die Register werden mit rX abgekürzt, wobei das X für die Nummer steht, die das Register hat. Register 0 heißt also r0, Register 1 heißt r1, usw. Weiterhin gibt es noch zwei besondere Register, die wir auf jeden Fall auch noch brauchen, die ich aber erst später erklären werde: r14, das sogenannte Link-Register (kurz lr) und r15, den sogenannten Programmcounter (kurz pc).
      Nun mögen sich einige von euch schon wundern: 8 Register für ein ganzes Spiel? Wie soll das denn funktionieren, wenn man nur Words abspeichern kann? Diese Frage ist durchaus berechtigt. Hätten wir tatsächlich nur 8 Register, so würden wir niemals ein ganzes Spiel auf die Reihe bekommen. Um mehr Platz zu generieren, gibt es eine Möglichkeit, die Register abzuspeichern, sodass wir sie verändern können und auch damit arbeiten, und nach getaner Arbeit dann wieder zurückladen. Der Ort, an dem wir die Register abspeichern können, nennt sich Stack.

      Was nehme ich aus diesem Kapitel mit?

      • 8 benutzbare Register, r0-r7 (&r14,r15)

      1.3. Der Stack

      Gerade eben ist er bereits gefallen, der Begriff des Stacks. Am Stack können wir Register, oder bessergesagt die Werte, die sie speichern, ablegen und sie dann später wieder "nehmen". Wenn wir also mit dem Register 0 arbeiten wollen, so legen wir erst den Inhalt des Registers auf dem Stack ab, verändern dann seinen Wert, und dann, wenn wir das Register nicht mehr brauchen, nehmen wir seinen Inhalt wieder von Stack und schreiben diesen Inhalt in das Register 0 zurück. Das ganze könnt ihr euch gewissermaßen wie einen Bücherstapel vorstellen.



      Diese von mir recht stümperhaft erstellte Grafik, zeigt auf, wie der Stack funtkioniert. Wir legen oben auf dem Stack einen Wert, und können diesen dann auch wieder herunternehmen. Wichtig ist, dass wir immer nur den obersten Wert wieder herunternehmen können, aber nie einen darunter. Wollen wir also mehrere Register abspeichern, so müsste das so ablaufen:

      Quellcode

      1. Lege Wert von r0 auf dem Stack ab @: Oben auf dem Stack liegt jetzt r0
      2. Lege Wert von r1 auf dem Stack ab @: Oben auf dem Stack liegt jetzt r1 -> Wir müssen r1 auch zuerst wieder herunternehmen
      3. ... Arbeite mit den Registern
      4. Nehme r1 vom Stack herunter @: Oben auf dem Stack liegt nun wieder r0, da wir r1 heruntergenommen haben
      5. Nehme r0 vom Stack herunter @: Nun können wir den aus r0 gespeicherten Wert zugreifen


      Wichtig ist, dass ihr das Prinzip verstanden habt. Ihr müsst immer das, was ihr zuletzt auf den Stack gepackt habt, auch wieder zuerst herunternehmen. Generell gilt auch für eure Routinen, dass ihr alles was ihr auf den Stack draufpackt, auch wieder herunternehmen müsst. Sonst können eventuell andere Codes des Spiels Probleme bekommen, wenn sie meinen einen Wert, den sie zuvor auf den Stack gepackt hatten, zu laden, während es in Wahrheit einer ist, den ihr dort hinterlegt habt.
      Außerdem ist es möglich, auch mehrere Register auf den Stack zu packen, ihr müsst sie dann, bzw solltet sie dann aber auch wieder in ihrer Reihenfolge von Stack laden. Hier ein Beispiel:

      Quellcode

      1. Lege die Werte der Register r0-r4 und außerdem r6 auf den Stack
      2. ... Arbeite mit den Registern
      3. Nehme die Werte der Register r0-r4 und außerdem r6 vom Stack herunter


      Anmerkung: Man kann auf die Elemente des Stacks auch unterhalb des obersten Elements zugreifen, doch ist das weitaus komplizierter und wird erst später erläutert.

      Was nehme ich aus diesem Kaptiel mit?
      • Stack speichert Werte der Register in Stapelform
      • Zugriff nur auf das oberste Element

      1.4. Besondere Register und Subroutinen

      Im Vorfeld habe ich ja bereits von den besonderen Registern r14, dem LinkRegister (lr) und r15, dem ProgrammCounter(pc) gesprochen. Auch wenn wir generell nur mit den Registern 0-7 arbeiten, so verwenden wir, oft indirekt auch diese beiden Register sehr häufig. Ich beginne mit r15, dem ProgrammCounter. Dieses Register gibt an, welcher Befehl als nächstes ausgeführt wird. Um das zu erläutern, gebe ich hier ein Beispiel. Vor dem Befehl führe ich immer auf, an welcher Adresse der ASM-Befehl im ROM steht. Ein Befehl umfasst hierbei immer 2 Bytes, also ein Hword.

      Quellcode

      1. ROM: 0x020 : Lade in r1 das dynamische Label X @: Im Programm Counter steht nun das Offset des nächsten Befehls, also 0x022
      2. ROM: 0x022 : Lade den Wert, der am Offset r1 steht, in r2 @: Im Programm Counter steht nun das Offset des nächsten Befehls, also 0x024
      3. ROM: 0x024: ...


      Der ProgrammCounter wird mit r15 oder öfter genutzt auch pc bezeichnet.

      Der ProgrammCounter wird im Zusammenhang mit den Subroutinen, die ich jetzt erkläre, noch sehr wichtig. Generell schreiben wir oftmals, fast immer, eine sogenannte Subroutine. Diese wird von einem übergeordneten Code angesteuert, und wenn sie ausgeführt wurde, wird der Rest des übergeordneten Codes ausgeführt. Hier wieder an einem Beispiel verdeutlicht. Ich gebe mit einer Zahl am Ende an, in welcher Reihenfolge die Befehle ausgeführt werden.

      Quellcode

      1. Lade in r1 das dynamische Label X @0
      2. rufe die Routine Z als Subroutine auf @1
      3. Addiere die Werte aus r1 und r2 @4
      4. Routine Z: @1
      5. verdopple r1 und speichere das Ergebnis in r2 @2
      6. kehre zurück zum Codeblock @3


      Wir sehen, dass die übergeordnete Routine die Subroutine aufruft. Dann wird diese komplett ausgeführt, bis der Rückkehr-Befehl erfolgt, und der Rest des übergeordneten Codeblocks ausgeführt wird. Hier kommt das Link-Register ins Spiel. Beim Aufruf eines Codes als Subroutine, wird automatisch im Link-Register die Adresse des nächsten Befehls, also dem Befehl, der nach dem Ausführen der Subroutine kommen würde, hinterlegt. Das Link-Register enthält also eine Referenz auf die Rückkehr-Adresse des Codes. Wenn wir diese Adresse also am Ende unserer Subroutine irgendwie in den ProgrammCounter schieben, dann würden wir automatisch zum übergeordneten Codeblock zurückkehren. Ich veranschauliche das ganze noch einmal an der obringen Routine. Das Link-Register wird mit r14, oder öfters genutzt auch lr bezeichnet.

      Quellcode

      1. ROM 0x20 : Lade in r1 das dynamische Label X @pc =0x22
      2. ROM 0x22 : rufe die Routine Z als Subroutine auf @ nun wird ins Link-Register das, was eigentlich ins pc kommen würde, gepackt
      3. @ lr=0x24, pc=0x30
      4. ROM 0x24 : Addiere die Werte aus r1 und r2 @ pc=0x26
      5. Routine Z: @1
      6. ROM 0x30 : verdopple r1 und speichere das Ergebnis in r2 @ pc=0x32
      7. ROM 0x32 : rückkehr (schiebe lr nach pc) @ pc = lr --> pc =0x24


      Auf diese Weise können Routinen als Unterroutinen, die sogenannten Subroutinen, aufgerufen werden. Oft werden diese Subroutinen mit sub abgekürzt. Außerdem kann eine Subroutine wiederum eine Subroutine aufrufen, solange sie nur davor das link-Register mit auf dem Stack packt, damit dieses beim Aufruf der neuen Subroutine, nicht beschädigt wird. Im Zuge dessen kann man auch einen netten Trick anwenden, mit dem man sich das verschieben des Link-Registers in den ProgrammCounter sparen kann.

      Ich sollte vorher klären, dass folgendes durchaus möglich ist:

      Quellcode

      1. Lege den Inhalt von r1 auf den Stack
      2. Nehme das oberste Element vom Stack und lege es nach r0


      Auf diese Weise sichern wir nicht nur r1, sondern verschieben auch seinen Inhalt nach r0. Selbiges können wir auch mit lr und pc machen.

      Quellcode

      1. Lege den Inhalt von lr auf den Stack
      2. ... Aufruf einer Subroutine
      3. Nehme das oberste Element des Stacks, und lege es nach pc


      Somit sichern wir nicht nur unser Link-Register ab und können so wieder neue Subroutinen aufrufen, sondern haben automatisch auch einen Rückkehrbefehl zur übergeordneten Routine. Wahlweise können wir, sofern wir keine neuen Subroutinen in unserer Subroutine aufrufen, auch einfach den Befehl "schiebe den Inhalt von lr nach pc" anwenden. Was ihr anwedet ist euch überlassen, wobei ich, einfach um Fehler zu vermeiden, zu der ersten Variante, die den Stack benutzt, tendiere.
      Anmerkung: Das ganze Spiel schlüsselt sich in Subroutinen, die über eine Subroutine aufgerufen werden auf.

      Was nehme ich aus diesem Kaptiel mit?
      • pc verweist auf den nächsten Befehl
      • Subroutinen als untergeordnete Routinen, die wie ein Befehl ausgeführt werden können
      • Subroutinen hinterlegen eine Referenz auf die Rückkehradresse in lr
      2. Die einzelnen Operatoren
      2.1 Was sind Operatoren?

      Gerade eben fiel der Begriff des Operators. Ein Operator führt immer eine bestimme Rechenoperation durch, wie zum Beispiel eine Addtion, eine Subtraktion oder ein logisches AND. Für jede dieser Operationen gibt es einen Operator, der eben genau diese Operation durchführt. Im Grunde kann man sagen, dass die einzelnen Befehle auf einen Operator zurückgreifen. Will ich also zwei Register addieren, so greift man beim Additionsbefehl auf den Additions-Operator zurück.

      2.2 Stack Operatoren: push und pop
      Im Vorfeld habe ich davon gesprochen, dass wir Register auf den Stack packen, genauer ihren Inhalt, und diesen dann wieder vom Stack herunternehmen. Dafür nutzen wir die Operatoren "push" und "pop"
      Während der push-Operator Register auf den Stack legt, nimmt sie der pop-Operator wieder herunter. Das ganze könnte dann so aussehen:
      Spoiler anzeigen

      Quellcode

      1. push {r1}pop {r1}

      In diesem Fall packen wir ein einzelnes Register auf den Stack, und nehmen es wieder herunter. Ferner ist auch:

      Quellcode

      1. push {r1-r6}
      2. pop {r1-r6}

      oder gar

      Quellcode

      1. push {r1-r6, lr}
      2. pop {r1-r6, pc}

      möglich.
      Viel mehr gibt es zu diesen Operatoren auch nicht zu sagen, außer der Tatsache, dass man nur die Register 0-7, bzw lr und pc als Parameter (innerhalb der geschweiften Klammern { }) angeben kann.

      2.2 Addition, Subtraktion und Multiplikation

      Die Operatoren für Addition, Subtraktion und Multiplikation sind nicht schwerer als die für push und pop. Im Folgelaufes des Tutorials werde ich die Parameter, also die Register, auf die die Operation durchgeführt wird, mit rM, rX und rY bezeichnen, wobei M, X und Y für eine Zahl zwischen 0 und 7 steht.

      2.2.1 Addition:
      Spoiler anzeigen
      Die Addition läuft über den add-Operator. Dieser addiert zwei Register, bzw ein Register und einen Wert und speichert das Ergebnis in einem anderen oder gleichen Register ab. Wichtig: Addiert man einen Wert, darf dieser nicht größer als #0x7 sein.

      Quellcode

      1. add rM, rX, rD

      oder

      Quellcode

      1. add rM, rX, #D


      macht folgendes: rM = rX+rD oder rM = rX+#D. Eine ganz simple Addition also. Das Ergenis steht in rM, wobei rM und rX auch das gleiche Register beschreiben dürfen. Sprich folgendes ist ebenfalls möglich:

      Quellcode

      1. add rM, rM, rD


      2.2.2. Subtraktion
      Spoiler anzeigen
      Selbes Prinzip wie beim Add-Operator. Nur verwenden wir hier den sub-Operator.

      Quellcode

      1. sub rM, rM, rD

      bzw

      Quellcode

      1. sub rM, rM, #D


      Was hier ins Auge sticht ist, dass rM sowohl das Ergebnis beinhaltet, als auch ein Teil der Operation ist. Dies muss so sein. Durchgeführt wird folgendes: rM = rM - rD, bzw rM = rM-#D

      2.2.3 Mutiplitkation
      Spoiler anzeigen
      Die Multiplikation läuft über den mul-Operator.

      Quellcode

      1. mul rM, rM, rD


      Hierbei kann NICHT mit einer Zahl multipliziert werden, sondern mit einem Register. Außerdem muss das Ergebnisregister Teil der Operation sein. Durchgeführt wird folgendes: rM = rM * rD


      2.3 Lade- und Schreiboperatoren und der Schiebeoperator

      Die Lade- und Schreiboperatoren lesen Informationen aus und laden diese in ein Register, bzw schreiben Informationen in einem Register an ein Offset im RAM. Dafür gibt es für jede Dateigröße ab dem Byte sowohl einen Lade- als auch einen Schreiboperator.

      2.3.1 Laden
      Spoiler anzeigen
      Der ldr-Operator lädt einen Wert in ein Register. Dabei kann dieser Wert entweder an einem Offset liegen, das in einem anderen Register liegt, oder als label definiert sein. Unterschieden wird zwischen dem ldr-Operator, der ein Word lädt, dem ldrh-Operator, der ein Hword lädt und dem ldrb-Operator, der einen Byte lädt.

      Quellcode

      1. ldr rM , =label


      oder

      Quellcode

      1. ldr rM, [rX]


      Im ersten Fall wird in rM der Wert eines Labels geladen, im zweiten Fall das Word, das am Offset, welches in rX gespeicher ist. Selbige Syntax (Befehls-und Parameteranordnung) gilt für ldrh und ldrb.

      2.3.2. Schreiben
      Spoiler anzeigen

      Der str-Operator schreibt einen Wert an ein Offset, welches in einem Register gespeichert ist. Dabei hat jeder Datengröße am dem Byte einen eigenen Schreiboperator. str schreibt ein Word, strh ein Hword und strb einen Byte. Die Syntax sieht hierbei so aus:

      str rM, [rX]

      Hierbei wird der Inhalt von rM an das Offset, das in rX gespeichert ist, geschrieben. Selbige Syntax gilt auch für strb und strh.


      ACHTUNG: Words können nur von Offsets, die durch 4 teilbar sind, also die auf die Ziffer 0,4,8 oder C enden ausgelesen oder geschrieben werden. Derartige Offsets nennt man 4-alinged. Weiterhin können Hwords nur von Offsets, die durch 2 teilbar sind, also die auf die Ziffer 0,2,4,6,8,A,C,E enden, ausgelesen oder geschrieben werden. Derartige Offsets nennt man 2-alinged.

      Spoiler anzeigen
      2.3.3 Der Schiebeoperator

      Schon oft war die Rede von Registern inenanderschieben, wobei diese Bezeichnung eigentlich falsch ist. Der mov-Operator schiebt den Inhalt eines Registers oder einen 8-Bit Wert, einen Byte, in ein anderes Register. Die Syntax sieht dabei wie folgt aus:

      Quellcode

      1. mov rM, rD


      oder

      Quellcode

      1. mov rM, #D


      Wobei der Inhalt des Registers rD bzw der Wert #D in das Register rM geschoben wird.


      Mit diesem Wissen können wir auch schon der ersten, sinnvollen Code schreiben:

      Spoiler anzeigen

      Quellcode

      1. .align 2 @Dieser Befehl am Beginn des Dokuments sorgt dafür, das unser Code später 2-alinged ist, denn jeder unserer Befehle ist ja ein Hword.
      2. .thumb @Dieser Befehl lädt das thumb-Befehlsset
      3. .equ Glücklichkeit, 0x02030A23 @Hier definiere ich das dynamische Label "Glücklichkeit", das ich unten verwenden will
      4. main: @das dynamische label für unsere Hauptroutine
      5. push {r0-r1, lr} @ich arbeite mit den Registern r0-r1, lr packe ich deswegen auf den Stack, weil mein Code als Subroutine fungiert
      6. ldr r0, =Glücklichkeit @in r0 lade ich das Offset, an dem die Information steht, die ich bearbeiten will
      7. ldrb r1, [r0] @nun lade ich den Byte, der an r0 steht nach r1
      8. add r1, #0x10 @jetzt verändere ich den Wert, ich addiere #0x10 dazu
      9. strb r1, [r0] @ich schreibe den modifizierten Wert wieder ans Offset, das in r0 steht zurück
      10. pop {r0-r1, pc} @meine routine ist beendet, ich pope meine Register und kehre zur übergeordneten Funktion (Code) zurück.
      Alles anzeigen


      Dieser Code macht zwar noch nicht viel mehr, als einen Byte im RAM um #0x10 zu erhöhen, aber ist das schon einmal ein erster Erfolg. Und das sogar ohne komplizierte Operatoren. Das nächste Kaptiel behandelt die Vergleichs- und Branchoperatoren, die auch nicht wirklich schwer zu verstehen sind.

      Was nehme ich aus diesem Kaptiel mit?
      • Additionsoperator add: add rM, rX, rD : rM = rX+rD
      • Subtraktionsoperator sub: sub rM, rM, rX : rM = rM - rX
      • Multiplikationsoperator mul: mul rM, rM , rX : rM = rM*rX
      • Ladeoperator ldr(h/b): ldr(h/b) rM, [rX]
      • Schreiboperator str(h/b): str(h/b) rM, [rX]
      • Schiebeoperator mov: mov rM, rX



      DIE WEITEREN BEFEHLE WERDEN ZEITNAH NACHGEREICHT!


      Wo war Gondor, als meine Klausurenphase begann?

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Wodka ()




    • 3. Branches




      3.Branches

      Von was rede ich denn nun schon wieder? Als Branch bezeichnet man einen "Link" gleichermaßen, der zu einem neuen Codeabschnitt führt, wobei der Begriff "link" nicht wirklich passt, mehr eine Weiterleitung zu einem anderen Codeabschnitt. Ein Branch verweist dabei aber nicht auf ein festes Offset, zu dem gesprungen wird, sondern eine relative Adresse, sprich er gibt an, wie weit der Codeabschnitt vom branch-Befehl selbst entfernt ist. Daraus resultiert auch, dass ein branch nicht beliebig weit gehen kann. Aber genug der Theorie. Es gibt verschiedene Arten von branches.

      3.1. Der normale Branch

      Der "normale" Branch tut schlicht und ergreifend nichts anderes, als auf einen anderen Codeabschnitt, relativ zum Offset des branches, zu verweisen. Das sieht normalerweise so aus:
      Spoiler anzeigen

      Quellcode

      1. @//... Irgendein Code
      2. b label @: An dieser Stelle wird zu dem dynamischen Label "label" gesprungen.
      3. @//... irgendein Code
      4. label:
      5. @// irgendein anderer Code

      Die Syntax für einen branch ergibt sich also aus:
      Spoiler anzeigen

      Quellcode

      1. b $Offset relativ zum branch



      3.2. Der Branch with exchange

      Der Branch with exchange klingt kompliziert, ist aber sehr einfach. Anstatt zu einer dynamischen Adresse relativ zum Standort des branch-Befehls zu springen, wird nun direkt zu einem Offset gesprungen, das ein Register enthählt. Wichtig ist, dass dieses Offset auch wieder | 1 gerechnet werden muss. Was das genau bedeutet, erkläre ich dann bei der praktischen Anwendung von ASM. Generell ergibt sich für den Branch with exchange folgende Syntax:

      Spoiler anzeigen

      Quellcode

      1. bx rM

      Dabei wird zu dem Offset in rM gesprungen. Sprich man muss (auf welche Weise auch immer) ein Offset nach rM laden. Manch einem möchte aufgefallen sein, dass man einen bx-Befehl auch durch:
      Spoiler anzeigen

      Quellcode

      1. mov pc, rM

      ersetzzen kann, was auch zutrifft. Doch muss man festhalten, dass dabei das Offset in rM nicht | 1 gerechnet werden darf, da es direkt in den Programmcounter wandert. Schöner, und häufiger verwendet ist aber der branch with exchange.

      3.3. Der Branch with link

      Manch einer mag sich fragen, wie man denn Subroutinen nun aufrufen kann. Dafür ist der Branch with link da. Auch er springt zu einer Adresse, die relativ zum Standort des branch-Befehls ist, also nicht feststeht, doch hinterlegt er einen link im linkRegister r14, sodass man später zu alten Codeabschnitt, der nach dem branch ausgeführt wird, zurückkehren kann. Die Syntax sieht dabei recht ähnlich aus, wie die des "normalen" branches:
      Spoiler anzeigen

      Quellcode

      1. bl $Offset relativ zum branch

      Wobei label ein dynamisches Label darstellt. Eine Subroutine muss über einen Branch with link aufgerufen werden, und wie das ganze aussehen kann, zeige ich euch an einem Beispiel. Beigefügt sind Nummern, die zeigen, in welcher Reihenfolge die einzelnen Codeblöcke ausgeführt werden.
      Spoiler anzeigen

      Quellcode

      1. @//... irgendein Code @1:
      2. bl label @2:Hier wird zum label "label" gesprungen, und gleichzeitig in r14 die Adresse des Befehls nach dem branch hinterlegt.
      3. @//... irgendein Code @6:
      4. label:
      5. push {lr} @3: Das Link register wird auf den Stack gepackt, sodass diese Subroutine selbst neue branches with links aufrufen kann
      6. @// ... irgendein Code @4:
      7. pop {pc} @5: nun wird das vorher auf den Stack gepackte linkRegister in den programm_counter gepopt -> Rückkehr zum
      8. übergeordneten Code




      3.4. Die conditional Branches

      Die einzlenen conditional Branches verhalten sich genauso wie ein gewöhnlicher branch, doch ob sie ausgeführt werden, ist immer von einer Bedingung abhängig. Generell werden bei einem conditional Branch immer genau 2 Register oder ein Register und ein Wert miteinander verglichen, und wenn die Bedignung zutrifft, wird der branch ausgeführt. Das klingt ersteinmal verwirrend, weshalb ich euch das ganze, nach dem Vorstellen der Syntax, an einem Beispiel erläutern werde. Die Syntax sieht dabei so aus:
      Spoiler anzeigen

      Quellcode

      1. cmp rM, rD
      2. b[Bedingung] $Offset relativ zum branchl

      oder
      Spoiler anzeigen

      Quellcode

      1. cmp rM, #D
      2. b[Bedingung] $Offset relativ zum branch

      Wobei #D einen 8-Bit Wert darstellt. Der cmp Befehl dient dazu, um einen Vergleich durchzuführen. Unmittelbar nach dem Vergleich muss dann der conditional-Branch erfolgen. Bevor man dann einen weiteren Conditional-branch ansetzt, muss erst wieder neu verglichen werden. Hier an einem Beispiel:
      Spoiler anzeigen

      Quellcode

      1. cmp r0, r1 @ Vergleiche den Wert von r0 mit dem aus r1
      2. bls kleiner @ b: branch ;; Bedingung: ls (kleiner), sprich ein Branch erfolgt, wenn der Wert von r0 kleiner als der von r1 ist
      3. cmp r0, r1 @ Vergleiche den Wert von r0 mit dem aus r1, ich kann nicht sofort einen weiteren cond. Branch durchführen, erst muss verglichen werden
      4. bgt größer @ b: branch ;; Bedingung gt (größer), sprich ein Branch erfolgt, wenn der Wert von r0 größer als der von r1 ist
      5. kleiner:
      6. @// ... Code der ausgeführt wird, wenn r0 < r1
      7. größer:
      8. @// ... Code der ausgeführt wird, wenn r0 > r1


      Generell gibt es verschiedene Bedingungen, an die ein branch geknüpft sein kann. Jede davon wird immer mit zwei Buchstaben abgekürzt. Hier eine Liste der gängisten und häufigst benutzten.
      Spoiler anzeigen

      Quellcode

      1. EQ : Gleich
      2. NE : Nicht gleich bzw ungleich
      3. LT : Kleiner
      4. LS : Kleiner oder gleich
      5. GT : Größer
      6. GS : Größer oder gleich
      7. HI : Größer ohne Berücksichtung des Vorzeichens des Wertes
      8. LO : Kleiner ohne Berücksichtung des Vorzeichen des Wertes

      Es existieren noch eine Reihe weitere Bedinungen, die jedoch recht kompliziert sind und daher selten genutzt werden.

      Was nehme ich aus diesem Kaptiel mit?

      • branches springen zu einem dynamischen Offset/Label
      • branches with exchange zu einem festen Offset, das in einem Reigster gespeichert ist
      • branches with link rufen dynamische Label als Subroutinen auf
      • Conditional Branches verhalten sich wie branches, sind aber an Bedingungen geknüpft

      4. Negative Werte

      4.1 Singed Betrachtung und Unsinged Betrachtung

      Bisher haben wir im Bereich Assembler nur positive Werte kennengelernt, und uns auch beim Rechnen auf diese beschränkt. Nun gibt es aber auch die Möglichkeit mit negativen Werten zu rechnen. Diese Werte haben dann entweder das Vorzeichen + oder das Vorzeichen -, weswegen wir sie fortan als "Singed" bezeichnen, sprich mit einem Vorzeichen versehen.
      Wichtig ist, dass kein Wert als "Singed" Wert oder nicht "singed" (unsinged) Wert vorliegt. Man kann einen Wert entweder als singed oder unsinged behandeln, die Rechenverfahren funktionieren aber gleich, sprich wenn ich zu einer Zahl den singed Wert addiere bekomme ich das gleiche Ergebnis, wie wenn ich den unsinged Wert addiere. Es kommt also immer darauf an, wie man einen Wert betrachtet. Will man einen Wert als singed Wert betrachten, so fällt dabei der letzte Bit eines Wertes weg, da dieser fortan angibt, ob der Wert positiv oder negativ ist. Sprich wenn der letzte Bit einer Information 0 ist, wird der singed Wert mit dem Vorzeichen + betrachtet, wenn der letzte Bit einer Information 1 ist, wird der singed Wert mit dem Vorzeichen - betrachtet.
      Ein 32-Bit Wert, also ein Word, hat also in der Betrachtung mit Vorzeichen nur noch 31 Bits und einen Singed Bit, der angibt, welches Vorzeichen der Wert hat. Damit das Rechnen auch weiterhin aufgeht, müssen die Werte immer größer werden, je näher sie sich dem Wert annähern, in dem alle Bits 1 sind. Somit wird aus dem Wert 0xFFFFFFFF (111111111 11111111 11111111 11111111b) der Wert -1, und aus 0xFFFFFFFE der Wert -2, da -1>-2.
      Es kommt bei den Werten also immer darauf an, wie man sie betrachtet. Man kann den Wert 0xFFFFFFFF entweder als den größtmöglichen Wert ohne Vorzeichen ansehen (irgendeine sehr hohe Zahl), oder auch als größtmögliche negative Zahl, nämlich -1. Fürs Rechnen aber ist es egal, wie wir unseren Wert ansehen, da 0xFFFFFFFE + 0x1 immer gleich 0xFFFFFFFF ist, egal ob ich das 0xFFFFFFFE nun als -2 oder als sehr hohe positive Zahl ansehe. Natürlich haben wir beim Ergebniss 0xFFFFFFFF natürlich wieder die Wahl, ob wir diesen Wert als -1 ansehen wollen (womit die Rechnung -2 + 1 = -1 aufgehen würde) oder als den höchsten Hexadezimalwert eines Words ansehen wollen.

      !!!Habt ihr nicht verstanden, wieso es zum Rechnen egal ist, ob ein Wert singed oder unsinged betrachtet wird, dann lest den Abschnitt noch einmal aufmerksam durch. Je nach Betrachtung des Ergebnisses stimmt die vorherige Rechnung!!!


      4.2. Singed Words, Hwords und Bytes

      4.2.1. Singed Words

      Wie man ein Word als singed betrachtet, habe ich ja bereits oben erläutert. Der letzte Bit (31) gibt immer das Vorzeichen an, womit 0x7FFFFFF die höchste positive darstellt, während 0xFFFFFFFF die höchste negative Zahl darstellt. Man hat also nun einen 31-Bit Wert, der entweder positiv oder negativ ist. Um ein Word als Singed zu betrachten, muss man im Grunde nichts tun, da sich fürs rechnen nichts ändert (wie ich bereits oben beschrieben habe).

      4.2.2. Singed Hwords

      Ein Hword als singed zu betrachten ist im Grunde nicht viel anders, als ein Word als singed zu betrachten. Der letzte Bit (15) gibt nun das vorzeichen an, womit Bit 0-14 die Größe des Werts beschreiben. 0x7FFF (irgendwas um 32000) ist somit die höchste positive Zahl darstellt, während 0xFFFF die höchste negative Zahl (-1) darstellt. Um ein Hword als singed zu betrachten, muss man nichts tun, um mit ihm zu rechen, muss man es erst in ein singed Word "umrechnen". Dafür kann man entweder Arithmetische Shifts (siehe unten) oder singed Ladeoperatoren(siehe unten) benutzen.

      4.2.3. Singed Bytes

      Ja, das Prinzip ist das Gleiche wie oben. Der letzte Bit (7) stellt das Vorzeichen dar, Bit 0-6 beschreiben die Größe des Wertes. 0x7F stellt den größten positiven Wert dar (127), während 0xFF den höchsten negativen Wert (-1) darstellt. Um einen Byte als singed Byte zu betrachten, muss man nichts tun, um mit ihm zu rechnen muss man auch ihn erst in ein singed Word "umrechnen".


      4.3. Lade- und Schreiboperatoren für Singed Werte


      Wie ich bereits sagte, benötigt man um ein singed Word auszulesen und
      mit ihm zu rechnen keinen extra Operator, da das Rechnen mit einem Word
      immer gleich funktioniert.
      Man kann also "singed" Words mit den
      gewohnten Operatoren ldr und str auslesen und schreiben.

      4.3.1. ldrsh

      Anders sieht es bei den Hwords aus. Diese muss man mit einem extra-Ladeoperator auslesen, damit man mit ihnen rechnen kann. Der Ladeoperator ldrsh ließt ein Hword von einem 2-alignedem Offset aus, und transformiert in direkt in ein Word. Die Syntax für ldrsh setzt sich fast genauso zu sammen wie die von ldrh.
      Spoiler anzeigen

      Quellcode

      1. ldrsh rM, [rX, rD]

      Was sofort auffällt, ist, dass man nun drei Register als Parameter hat. Ldrsh kann einen Wert von einem Register nur dann auslesen, wenn ein zweites Register angibt, wie weit dieser Wert vom Offset entfernt ist. Das ganze klingt verwirrend, einfach weiterlesen:
      Die oben angeführte Syntax würde ein singed Hword nach rM laden. Das ganze vom Offset, das in rX gespeicher ist, und dannach rD addiert auslesen. Während man bei ldrh statt dem Parameter rD einen festen Wert angeben konnte, benötigt ldrsh ein Register, welches diesen Wert beinhaltet, selbst wenn dieser 0x0 ist.
      Spoiler anzeigen

      Quellcode

      1. ldrsh rM, [rX, #0x0] bzw ldrsh rM, [rX]

      ist also nicht länger möglich. Den Wert 0x0 muss man vorher in ein gesondertes Register laden. Alternativ kann man auch mit Arithmetischen Shifts arbeiten, was ich unten erkläre.

      Wem jetzt die Frage kommt "Wie schreibe ich ein Singed Hword an ein Offset", der hat zwar ein gewisses Problembewusstsein, aber richtig mitgedacht hat er auch nicht. Nehmen wir an ich lade das Singed Hwords 0xFFFD (-3) nach rM über ldrsh. Ldrsh transformiert 0xFFFD zu 0xFFFFFFFD (-3), sodass ich nun ein Singed Word habe mit dem ich Rechnen kann. Wenn ich jetzt 0x1 subtrahiere erhalte ich den Wert 0xFFFFFFFC (-4). Will ich diesen Wert -4 nun zurück an das Offset schreiben, so kann ich einfach den strh-Operator nutzen, da dieser die Bits 16-31 schlichtweg ignoriert und lediglich 0xFFFC an das Offset schreibt. Lese ich diesen Wert 0xFFFC dann wieder über ldrsh aus, so erhalte ich den Wert 0xFFFFFFFC (-4).
      Dieses Beispiel soll verdeutlichen, dass es nur beim auslesen eines Wertes wichtig ist, ob man ihn als singed oder unsinged ansieht. Rechnen, schreiben und alles andere (abgesehen vom shiften(siehe unten))sind vom Vorzeichen unabhängig. Zwar gibt es den Schreibeoperator strsh, doch macht er in meinen Augen wenig Sinn.

      4.3.1. ldrsb

      Ähnlich sieht es bei ldrsb aus. Um einen Singed Byte als Word zu laden, muss man den lrdsb-Operator anwenden. Dieser "transformiert" beispielsweise den singed Byte 0xFE (-2) zu 0xFFFFFFFE (-2). Dann können wir mit dem Wert rechnen. Die Syntax sieht genauso aus wie die von ldrsh:
      Spoiler anzeigen

      Quellcode

      1. ldrsb rM, [rX, rD]

      Ein Singed Byte wird von rX+rD nach rM geladen. Zum Schreiben können wir wieder den strb Operator nehmen (die Erklärung ist die selbe wie oben).

      Was nehme ich aus diesem Kaptiel mit?
      • Werte können singed oder unsinged betrachten werden
      • Rechnet man mit Words, so spielt die Betrachtung keine Rolle
      • Auslesen erfolt entweder unsinged oder singed



      Wo war Gondor, als meine Klausurenphase begann?

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von Wodka ()

    • sprachlich sehr gutes tutorial, ein kenner wird alles vestehen, doch habe ich zweifel das ein neuling es so leicht verstehen wird.
      Zahlsysteme sind ordentlich erkärt, top auch die umrechnung von bits, bytes und words etc. dass sich dort ein deutliches muster erkennen lässt, du könntest dabei aber mehr auf den hexeditor oder die RAM-Anzeige des GBA´s eingehen, damit man versteht warum dies so unterteilt wird.

      Zudem würde ich nochmehr auf die tatsache einghen, dass offsets hexadezimal angegeben werden und ein offset immer nur einen byte enthält, aus deinen ladebeispielen lässt sich nämlich für einen der keine ahnung mit der materie hat nicht schließen, wie viele informationen du gerade lädst. ich zumindest finde es schwer herauszufinden dass du immer nur einen byte lädst, wenn ich es nicht wissen würde.

      gib vll noch an, dass ein pointer immer ein word groß ist

      fazit: finde die idee des tutorials gut, es hört sich jetzt vll so an, als ob ich es schlecht reden würde, oder das tutorial insgesamt total schlecht wäre, finde es aber echt klasse :D wollte nur punkte anführen, die für dich und viele andere zwar selbstverständlich sind, für anfänger, wie mich vor ein paar wochen noch, trotzdem kompliziert sind, weil einfach zu unausführlich drauf eingegangen wird

      greez roquo_
      wer kam nur auf die idee widerstände parallel zu schalten? ich hasse dich :D
    • @Oben
      Musst aber auch sehen, dass da ja noch viel kommt...Und gewisse Dinge sollten einfach klar sein. Du wirst mit dem Debugger kein Offset im Binärcode researchen. Wenn man sich das Tutorial ohne Vorwissen ansieht, mit dem Ziel ASM zu erlernen, so wird das garantiert niemals funktionieren.

      Finde es bisher eine riesengrosse Einleitung. Der Grossteil ist meiner meinung nach Grundbildung und muss man einfach Wissen wenn man mit dem Hex-Editor rumspielt oder mit ROMS fungiert.

      Trotzdem sehr schön und freue mich auf die Erklärung der jeweils eknzelnen Befehle. Warte schon ein weilchen auf eine Deutsche Erklärung. Wäre evt. die Möglichkeit das wissen von damals wieder aufzufrischen und zu erweitern.
      Das waren noch Zeiten...

      ...

      Wodka schrieb:

      Wodka - 3. Oktober 2012

      Ich spüre seinen Atem in meinem Nacken o.O!

      PEDODAD!


    • Wodka schrieb:


      II: Assembler

      2.2.1 Addition:
      Die Addition läuft über den add-Operator. Dieser addiert zwei Register, bzw ein Register und einen Wert und speichert das Ergebnis in einem anderen oder gleichen Register ab. Wichtig: Addiert man einen Wert, darf dieser nicht größer als #0x7 sein.

      Quellcode

      1. main: @das dynamische label für unsere Hauptroutine
      2. push {r0-r1, lr} @ich arbeite mit den Registern r0-r1, lr packe ich deswegen auf den Stack, weil mein Code als Subroutine fungiert
      3. ldr r0, =Glücklichkeit @in r0 lade ich das Offset, an dem die Information steht, die ich bearbeiten will
      4. ldrb r1, [r0] @nun lade ich den Byte, der an r0 steht nach r1
      5. add r1, #0x10 @jetzt verändere ich den Wert, ich addiere #0x10 dazu
      6. strb r1, [r0] @ich schreibe den modifizierten Wert wieder ans Offset, das in r0 steht zurück
      7. pop {r0-r1, pc} @meine routine ist beendet, ich pope meine Register und kehre zur übergeordneten Funktion (Code) zurück.


      Dieser Code macht zwar noch nicht viel mehr, als einen Byte im RAM um #0x10 zu erhöhen, aber ist das schon einmal ein erster Erfolg. Und das sogar ohne komplizierte Operatoren. Das nächste Kaptiel behandelt die Vergleichs- und Branchoperatoren, die auch nicht wirklich schwer zu verstehen sind.



      du sagst oben man darf nicht mehr als 0x7 addieren, später addierst du 0x10 da stimmt doch was nicht? :D
      wer kam nur auf die idee widerstände parallel zu schalten? ich hasse dich :D
    • ich bin da voll auf gfe`S seite, jedes ASM turorial redet halt über diese operatoren und was man damit machen kann, aber ein konkretes beispiel wäre immer besser, zB bei der addition, das man die kp eines pokemons um 10 erhöht etc., das man auch etwas sehen kann
      wer kam nur auf die idee widerstände parallel zu schalten? ich hasse dich :D
    • Das große Problem, welches viele mit Assembler haben ist der Glaube, sobald man die Sprache versteht kann man plötzlich den Ozean teilen. Tut mir leid einigen vielleicht die Illusion zu zerstören, aber so ist das eben nicht. Assembler an sich ist eine Sprache, die eben aus diesen paar OPCodes/Mnemonics oder wie auch immer man sie bezeichnen will besteht. Vom Aufbau her einfacher als alle höheren Programmiersprachen. Um etwas damit anfangen zu können muss man allerdings das Spiel oder seine "Umgebung" kennen(Weils egal ist, gehe ich von Pokémon und dem GBA aus, dass man in Assembly auch von Grund auf neue Dinge erstellen kann sollte jedem bewusst sein), man muss wissen wonach man sucht und wie man es findet(Ein Research Teil ist laut Wodka ja noch in Arbeit), man muss eigenständig Probleme lösen, sei das Problem jetzt "Wie kann ich die HP eines Pokémon erhöhen" muss man sich fragen: "Wo stehen diese denn?" - "Im RAM, an Offset XY", danach nimmt man seinen Werkzeugkasten, sofern man damit nicht vertraut ist kann man ja in einem Tutorial oder in einer Dokumentation nachschauen was man jetzt alles braucht um den Wert im RAM zu verändern, etc.
      Man könnte hierzu natürlich einen Beispielcode angeben, allerdings hat man 5 Minuten später vielleicht ein anderes Problem, welches auf eine völlig andere Weise zu lösen ist, z.B.: "Wie zeige ich ein Bild auf BG0 an?", da hilft einem die Lösung des letzten Problems nicht wirklich viel und man muss sich etwas neues ausdenken.
      Im übrigen ist der Beispielcode beim add Befehl schon in etwa das was man haben möchte, man ersetze einfach das Offset "Glücklichkeit" durch jenes, welches die KP eines Pokémon enthält...(Gut, so einfach ist es nicht, aber die Methodik sollte klar sein)

      tl;dr: Das schwere ist nicht die Sprache selbst, sondern wie man damit umgeht.

      ~Sturmvogel
    • @roquo_
      Auf PC gabs ein Tutorial. Weitaus nicht so ausführlich wie dieses hier. Dafür mit logischen und nachvollziehbaren Beispielen. So war es jeweils die Aufgabe die Werte im RAM für die einzelnen Statuswerte zu suchen, diese in einem ASM Script anzuwenden und dann daraus ein Script machen der die Statuswerte abfragt. Nach jedem Beispiel gab es eine Aufgabe zur Thematik. So soll man bei dem obrigen bspw. Die restlichen Werte selber herausfinden und den ASM-Code dann darauf anpassen. Ist wirklich hilfreich um zu lernen.

      @Sturmvogel
      Eine Problematik die viele nicht sehen. Es ist nunmal schwer den Durchblick zu finden und zu wissen wo man anfangen muss. Trotzdem, denke Wodka bringt es auf die Reihe die Hacker auf den Rechten Weg zu leiten.
      Das waren noch Zeiten...

      ...

      Wodka schrieb:

      Wodka - 3. Oktober 2012

      Ich spüre seinen Atem in meinem Nacken o.O!

      PEDODAD!


    • Ich finde es ist bisher ein sehr schönes Tutorial. Die Einleitung ist schön ausführlich, aber irgendwie denke ich auch etwas unnötig. Wenn man mit dem hacken anfängt, dann wird man meistens nicht als erstes mit ASM starten, sondern tastet sich wohl eher langsam an sowas ran, weswegen Leute wohl gar nicht erst in dieses Tutorial reinschauen und erst später reingucken und dann schon die Grundlagen beherrschen, aber das ist nur meine Meinung dazu.
      Die Operatoren hast du schön erklärt und habe ich auch bisher alle verstanden, denke ich zumindest mal. Ich freue mich auf den nächsten Teil. :D
    • yanbo schrieb:

      Ich finde es ist bisher ein sehr schönes Tutorial. Die Einleitung ist schön ausführlich, aber irgendwie denke ich auch etwas unnötig. Wenn man mit dem hacken anfängt, dann wird man meistens nicht als erstes mit ASM starten, sondern tastet sich wohl eher langsam an sowas ran, weswegen Leute wohl gar nicht erst in dieses Tutorial reinschauen und erst später reingucken und dann schon die Grundlagen beherrschen


      Puh, wenn du mich fragst, dann eher nicht. Ich sehe das so:

      Viele stürzen sich da hinein obwohl sie null Ahnung vom Aufbau haben, wundern sich dann warum Grafiken nicht dargestellt werden, wundern sich warum alles über 0x9 nicht klappt z.B. applymovement oder kopieren irgendetwas zusammen, was dann ein Script sein soll, wo man sich fragt, ob derjenige auch weiß, wozu z.B. compares da sind. Manche wissen wohl auch nicht, dass es mehrere Parameter bei if gibt. Oder auch komische ASM-Snippets, wo derjenige ein Hword lädt und ein Byte schreibt. Man kann so Sachen wie, wie bildet man Pointer, eigentlich immer wieder erklären, wenn man Beiträge schreibt. Ich sehe auch oft bei Anfänger Features im ersten Hack oder Anfragen, die schon einiges an Research- und ASM-Kenntnissen abverlangen. Oder bei Tilesets: Anstatt erst einmal ein Tile mit den Farben aus dem Spiel einzufügen müssen es gleich ganze Tilesets mit neuen Paletten sein. Das langsam Herantasten sehe ich nur bedingt, meistens aber überhaupt nicht. In allem halte ich ausführliche Einleitung mit Beispielen für sinnvoll, am besten überall mit einem Schild: Vorsicht, komplex und zeitintensiv. Ab hier mindestens Siebtklässler. ^^
      Gibt desöfteren auch Leute, die gar nichts verstehen, obwohl es zu Thema X viele Tutorials hier im Forum gibt. Teilweise entstehen in Fragethreads kleine Hybridtutorials xD. Je mehr verschiedene Erklärungen man zu einem Thema hat, desto besser. Gibt ja auch Tutorials, die vorrangig nur aus Bildern bestehen, während andere nur Fließtext sind.
      Je nach dem, wie der User am besten lernt, wird ein anderes Tutorial zum Thema interessanter sein.
      Wers wirklich schon kann, kann einfach den Part überspringen.

      P.S.: Von anderen Tutorials und der Abfolge her würde ich mov, ldr und str eigentlich noch vor add und co. schieben.
      Ich denke, wenn man das verstanden hat, kann man auch dazu übergehen, mit den Zahlen zu arbeiten und letztenendes Ergebnisse ins RAM übertragen.

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von Kairyon ()