Um zu verstehen, wie Computer organisiert sind, wie sie auf einem sehr niedrigen Niveau zu arbeiten scheinen, ist ein Verständnis dafür erforderlich, wie ein Assemblerprogramm funktioniert. Auf der einfachsten Ebene haben Computer drei Hauptteile:
- Hauptspeicher oder RAM, der Daten und Befehle enthält,
- einem Prozessor, der die Daten verarbeitet, indem er die Anweisungen ausführt, und
- Input und Output (manchmal verkürzt auf I/O), die es dem Computer ermöglichen, mit der Außenwelt zu kommunizieren und Daten außerhalb des Hauptspeichers zu speichern, so dass er die Daten später wieder zurückholen kann.
Hauptspeicher
In den meisten Computern ist der Speicher in Bytes aufgeteilt. Jedes Byte enthält 8 Bit. Jedes Byte im Speicher hat auch eine Adresse, die eine Zahl ist, die angibt, wo sich das Byte im Speicher befindet. Das erste Byte im Speicher hat eine Adresse von 0, das nächste eine Adresse von 1 und so weiter. Die Unterteilung des Speichers in Bytes macht ihn byteadressierbar, da jedes Byte eine eindeutige Adresse erhält. Die Adressen von Byte-Speichern können nicht verwendet werden, um auf ein einzelnes Bit eines Bytes zu verweisen. Ein Byte ist der kleinste Teil des Speichers, der adressiert werden kann.
Auch wenn sich eine Adresse auf ein bestimmtes Byte im Speicher bezieht, können Prozessoren mehrere Bytes Speicher in einer Reihe verwenden. Die häufigste Verwendung dieser Funktion besteht darin, entweder 2 oder 4 Bytes in einer Reihe zu verwenden, um eine Zahl, normalerweise eine ganze Zahl, darzustellen. Einzelne Bytes werden manchmal auch zur Darstellung von ganzen Zahlen verwendet, aber da sie nur 8 Bit lang sind, können sie nur 28 oder 256 verschiedene mögliche Werte enthalten. Bei Verwendung von 2 oder 4 Bytes in einer Reihe erhöht sich die Anzahl der verschiedenen möglichen Werte auf 216, 65536 bzw. 232, 4294967296.
Wenn ein Programm ein Byte oder eine Anzahl von Bytes in einer Reihe verwendet, um etwas wie einen Buchstaben, eine Zahl oder etwas anderes darzustellen, werden diese Bytes als Objekt bezeichnet, weil sie alle Teil derselben Sache sind. Auch wenn alle Objekte in identischen Speicherbytes gespeichert sind, werden sie so behandelt, als hätten sie einen "Typ", der angibt, wie die Bytes zu verstehen sind: entweder als ganze Zahl oder als Zeichen oder als ein anderer Typ (wie ein nicht ganzzahliger Wert). Man kann sich Maschinencode auch als einen Typ vorstellen, der als Anweisungen interpretiert wird. Der Begriff "Typ" ist sehr, sehr wichtig, weil er definiert, was mit dem Objekt gemacht werden kann und was nicht und wie die Bytes des Objekts zu interpretieren sind. Zum Beispiel ist es nicht zulässig, eine negative Zahl in einem Objekt mit positiver Zahl zu speichern, und es ist nicht zulässig, einen Bruch in einer ganzen Zahl zu speichern.
Eine Adresse, die auf ein Multi-Byte-Objekt zeigt (ist die Adresse eines Multi-Byte-Objekts), ist die Adresse des ersten Bytes dieses Objekts - das Byte, das die niedrigste Adresse hat. Nebenbei bemerkt, eine wichtige Sache, die man beachten sollte, ist, dass man den Typ eines Objekts - oder auch nur seine Größe - nicht an seiner Adresse erkennen kann. Tatsächlich kann man nicht einmal anhand des Typs eines Objekts sagen, um welchen Typ es sich handelt, wenn man es betrachtet. Ein Assemblerprogramm muss im Auge behalten, welche Speicheradressen welche Objekte enthalten und wie groß diese Objekte sind. Ein Programm, das dies tut, ist typsicher, weil es nur Dinge mit Objekten macht, die auf ihrem Typ sicher sind. Ein Programm, das das nicht tut, wird wahrscheinlich nicht richtig funktionieren. Beachten Sie, dass die meisten Programme eigentlich nicht explizit speichern, was der Typ eines Objekts ist, sondern dass sie nur konsistent auf Objekte zugreifen - dasselbe Objekt wird immer als derselbe Typ behandelt.
Der Verarbeiter
Der Prozessor führt Instruktionen aus (führt sie aus), die als Maschinencode im Hauptspeicher gespeichert werden. Die meisten Prozessoren können nicht nur zur Speicherung auf den Speicher zugreifen, sondern verfügen auch über einige kleine, schnelle Speicherplätze fester Größe zur Aufnahme von Objekten, mit denen gerade gearbeitet wird. Diese Räume werden als Register bezeichnet. Prozessoren führen in der Regel drei Arten von Befehlen aus, obwohl einige Befehle eine Kombination dieser Typen sein können. Nachstehend finden Sie einige Beispiele für jeden Typ in der Assemblersprache x86.
Anweisungen, die Speicher lesen oder schreiben
Die folgende x86-Assembleranweisung liest (lädt) ein 2-Byte-Objekt aus dem Byte an Adresse 4096 (0x1000 in hexadezimaler Schreibweise) in ein 16-Bit-Register namens "ax":
mov ax, [1000h]
In dieser Assemblersprache bedeuten eckige Klammern um eine Nummer (oder einen Registernamen), dass die Nummer als Adresse zu den zu verwendenden Daten verwendet werden soll. Die Verwendung einer Adresse, um auf Daten zu verweisen, wird als Indirektion bezeichnet. In diesem nächsten Beispiel, ohne die eckigen Klammern, erhält ein anderes Register, bx, tatsächlich den Wert 20 geladen.
mov bx, 20
Da keine Indirektion verwendet wurde, wurde der tatsächliche Wert selbst in das Register eingetragen.
Wenn die Operanden (die Dinge, die nach der Eselsbrücke kommen), in umgekehrter Reihenfolge erscheinen, wird eine Anweisung, die etwas aus dem Speicher lädt, es stattdessen in den Speicher schreibt:
mov [1000h], ax
Hier erhält der Speicher an Adresse 1000h den Wert von ax. Wenn dieses Beispiel direkt nach dem vorherigen Beispiel ausgeführt wird, sind die 2 Bytes an 1000h und 1001h eine 2-Byte-Ganzzahl mit dem Wert 20.
Anweisungen, die mathematische oder logische Operationen ausführen
Einige Anweisungen machen Dinge wie Subtraktion oder logische Operationen wie nicht:
Das Maschinencode-Beispiel weiter oben in diesem Artikel wäre dies in Assemblersprache:
Axt hinzufügen, 42
Hier werden 42 und ax zusammengezählt und das Ergebnis wieder in ax gespeichert. In der x86-Assembly ist es auch möglich, einen Speicherzugriff und eine mathematische Operation wie diese zu kombinieren:
ax hinzufügen, [1000h]
Diese Anweisung addiert den Wert der bei 1000h gespeicherten 2-Byte-Ganzzahl zu ax und speichert die Antwort in ax.
oder ax, bx
Diese Anweisung berechnet den oder der Inhalte der Register ax und bx und speichert das Ergebnis zurück in ax.
Anweisungen, die darüber entscheiden, was die nächste Anweisung sein wird
Normalerweise werden die Befehle in der Reihenfolge ausgeführt, in der sie im Speicher erscheinen, d.h. in der Reihenfolge, in der sie in den Assemblercode eingegeben werden. Der Prozessor führt sie einfach eine nach der anderen aus. Damit Prozessoren jedoch komplizierte Dinge tun können, müssen sie unterschiedliche Anweisungen ausführen, je nachdem, welche Daten ihnen gegeben wurden. Die Fähigkeit von Prozessoren, je nach dem Ergebnis eines Vorgangs unterschiedliche Anweisungen auszuführen, wird als Verzweigung bezeichnet. Anweisungen, die darüber entscheiden, was die nächste Anweisung sein soll, werden Verzweigungsanweisungen genannt.
Nehmen wir in diesem Beispiel an, jemand möchte die Farbmenge berechnen, die er benötigt, um ein Quadrat mit einer bestimmten Seitenlänge zu bemalen. Aufgrund von Größenvorteilen verkauft das Farbengeschäft jedoch nicht weniger als die Farbmenge, die zum Bemalen eines Quadrats von 100 x 100 benötigt wird.
Um herauszufinden, welche Farbmenge sie aufgrund der Länge des Quadrats, das sie malen wollen, benötigen, haben sie sich diesen Satz von Schritten ausgedacht:
- 100 von der Seitenlänge subtrahieren
- wenn die Antwort kleiner als Null ist, setzen Sie die Seitenlänge auf 100
- die Seitenlänge mit sich selbst multiplizieren
Dieser Algorithmus kann im folgenden Code ausgedrückt werden, wobei ax die Seitenlänge ist.
mov bx, ax sub bx, 100 jge weiter mov ax, 100 weiter: mul ax
Dieses Beispiel führt einige neue Dinge ein, aber die ersten beiden Anweisungen sind vertraut. Sie kopieren den Wert von ax in bx und subtrahieren dann 100 von bx.
Eines der neuen Dinge in diesem Beispiel heißt Label, ein Konzept, das in Versammlungssprachen im Allgemeinen zu finden ist. Labels können alles sein, was der Programmierer will (es sei denn, es ist der Name einer Anweisung, was den Assembler verwirren würde). In diesem Beispiel ist das Label "continue". Es wird vom Assembler als die Adresse einer Anweisung interpretiert. In diesem Fall ist es die Adresse von mult ax.
Ein weiteres neues Konzept ist das der Flaggen. Auf x86-Prozessoren setzen viele Befehle 'Flags' im Prozessor, die vom nächsten Befehl verwendet werden können, um zu entscheiden, was zu tun ist. In diesem Fall, wenn bx kleiner als 100 war, wird sub ein Flag setzen, das besagt, dass das Ergebnis kleiner als Null war.
Die nächste Anweisung ist jge, was die Abkürzung für "Springen, wenn größer als oder gleich" ist. Es ist eine Verzweigungsanweisung. Wenn die Flags im Prozessor angeben, dass das Ergebnis größer oder gleich null war, springt der Prozessor nicht einfach zur nächsten Anweisung, sondern zur Anweisung mit dem continue label, das mul ax ist.
Dieses Beispiel funktioniert gut, aber es ist nicht das, was die meisten Programmierer schreiben würden. Der Subtraktionsbefehl setzt das Flag korrekt, aber er ändert auch den Wert, mit dem er arbeitet, was erforderte, dass die Achse nach bx kopiert werden musste. Die meisten Assemblersprachen erlauben Vergleichsbefehle, die keines der übergebenen Argumente ändern, aber trotzdem die Flags richtig setzen, und x86-Assembler sind keine Ausnahme.
cmp ax, 100 jge weiter mov ax, 100 weiter: mul ax
Anstatt nun 100 von ax abzuziehen, zu sehen, ob diese Zahl kleiner als Null ist, und sie wieder ax zuzuordnen, wird ax unverändert gelassen. Die Fahnen werden immer noch auf die gleiche Weise gesetzt, und der Sprung wird in den gleichen Situationen immer noch durchgeführt.
Eingabe und Ausgabe
Obwohl Eingabe und Ausgabe ein grundlegender Teil der Datenverarbeitung sind, gibt es keine einzige Möglichkeit, sie in Assemblersprache durchzuführen. Das liegt daran, dass die Art und Weise, wie E/A funktioniert, von der Einrichtung des Computers und dem Betriebssystem abhängt, auf dem er läuft, und nicht nur davon, welche Art von Prozessor er hat. Im Beispielabschnitt verwendet das Hello World-Beispiel MS-DOS-Betriebssystemaufrufe und das Beispiel danach BIOS-Aufrufe.
Es ist möglich, E/A in Assemblersprache durchzuführen. Tatsächlich kann Assembler im Allgemeinen alles ausdrücken, wozu ein Computer in der Lage ist. Aber obwohl es Anweisungen zum Hinzufügen und Verzweigen in Assemblersprache gibt, die immer das Gleiche tun, gibt es keine Anweisungen in Assemblersprache, die immer E/A ausführen.
Es ist wichtig zu beachten, dass die Art und Weise, wie E/A funktioniert, nicht Teil irgendeiner Assemblersprache ist, weil sie nicht Teil der Arbeitsweise des Prozessors ist.