Der x86-64-Track bei Exercism verwendet NASM (The Netwide Assembler) als Assembler.
In NASM werden Kommentare durch ein Semikolon (;) eingeleitet.
Kommentare können überall im Programm stehen, und alles, was nach einem ; kommt, wird vom Assembler ignoriert.
Eine Konstante zur Assemblerzeit lässt sich in NASM mit equ definieren.
Zum Beispiel definiert dies eine Konstante namens UNIVERSE mit dem Wert 42:
UNIVERSE equ 42
Konstanten werden einmal ausgewertet, wenn sie definiert werden, und können später nicht neu definiert werden.
In Assembler verwenden wir zum Speichern von Werten üblicherweise Register statt Variablen. Ein CPU-Register ist ein schneller Speicher im Prozessor eines Computers. Die meisten Berechnungen finden in den Registern der CPU statt.
Einige der Register werden für ganz verschiedene Berechnungen genutzt und heißen Allzweckregister (GPRs). Andere haben spezielle oder fest zugewiesene Aufgaben.
In x86-64 gibt es 16 64-Bit-Allzweckregister (GPRs), auf die auch als 32-Bit-, 16-Bit- oder 8-Bit-Register zugegriffen werden kann.
Die GPRs sind unten beschrieben, wobei n in rn von 8 bis 15 reicht: r8, r9, r10, r11, r12, r13, r14 und r15.
| 64-bit | 32-bit | 16-bit | 8-bit |
|---|---|---|---|
| rax | eax | ax | ah/al |
| rbx | ebx | bx | bh/bl |
| rcx | ecx | cx | ch/cl |
| rdx | edx | dx | dh/dl |
| rsi | esi | si | sil |
| rdi | edi | di | dil |
| rbp | ebp | bp | bpl |
| rsp | esp | sp | spl |
| rn | rnd | rnw | rnb |
Wenn weniger als 64 Bit verwendet werden, stammen die angesprochenen Bits normalerweise aus dem unteren Teil des Registers.
Ausnahmen von dieser Regel sind ah, bh, ch und dh, die auf die oberen 8 Bit des 16-Bit-Teils des Registers zugreifen.
Darstellung, wie auf die Bits des Registers rax zugegriffen wird:
+--------+---------------------------------------+
| 64-bit | rax |
+--------+-------------------+-------------------+
| 32-bit | | eax |
+--------+-------------------+---------+---------+
| 16-bit | | ax |
+--------+-----------------------------+----+----+
| 8-bit | | ah | al |
+--------+-----------------------------+----+----+
Einige dieser Register müssen über Funktionsaufrufe hinweg erhalten bleiben: rbp, rsp, rbx, r12, r13, r14 und r15.
Werden sie nicht erhalten, kann das zu einem Fehler oder zu undefiniertem Verhalten führen.
Die übrigen müssen nicht erhalten bleiben und dürfen frei verwendet werden: rax, rcx, rdx, rdi, rsi, r8, r9, r10 und r11.
Register werden üblicherweise mit einer push-Anweisung auf dem Stack gesichert.
Vor dem Ende der Funktion werden sie dann mit einer pop-Anweisung wiederhergestellt.
Anweisungen sind Berechnungsschritte, die eine CPU ausführen kann. Sie haben meist die folgende Form:
name destination, source
Zuerst steht also der Name der Anweisung, dann mindestens ein Leerzeichen, gefolgt vom Zieloperanden, einem Komma (,) und schließlich einem Quelloperanden.
Der Quelloperand wird von einer Anweisung normalerweise nicht verändert, nur der Zieloperand.
Beide Operanden müssen dieselbe Größe haben, sofern nicht anders angegeben. Wenn der Quelloperand zum Beispiel 16 Bit hat, muss auch der Zieloperand 16 Bit haben.
Um einen Wert in einem Register zu speichern, können wir zum Beispiel die Anweisung mov verwenden:
mov rax, rdx ; rax = rdx
; mov is the name, rax is the destination operand and rdx is the source operand
Das obige Beispiel kopiert den Inhalt aller 64 Bit von rdx, dem Quelloperanden, in alle 64 Bit von rax, den Zieloperanden der Anweisung.
In den meisten Anweisungen können konstante Ganzzahlen als Quelloperanden verwendet werden (aber nicht als Zieloperanden):
mov rcx, 42 ; rcx = 42
; rcx is the destination operand and 42 is the source operand
Beachte, dass ein Schreibzugriff auf ein 32-Bit-Register auch die oberen Bits löscht, mov eax, 42 ist also dasselbe wie mov rax, 42.
Für 16-Bit- und 8-Bit-Register gilt das nicht.
Für die Rechenoperationen Addition, Subtraktion und Multiplikation können wir die Anweisungen add, sub und imul verwenden:
add rax, rsi ; rax = rax + rsi
imul rax, rdi ; rax = rax * rdi
sub rax, r8 ; rax = rax - r8
Anweisungen sind in Funktionen organisiert. Alle Funktionen stehen im Abschnitt section .text der Quelldatei.
Eine Funktionsdeklaration besteht aus:
:.Es gibt bestimmte Konventionen dafür, wie Argumente an eine Funktion übergeben und von ihr zurückgegeben werden. Die beiden wichtigsten Aufrufkonventionen sind die System V AMD64 ABI und Microsoft x64.
Dieser Track verwendet die Aufrufkonvention System V AMD64 ABI, bei der die ersten sechs ganzzahligen Argumente in Registern an eine Funktion übergeben werden.
Sie werden in dieser Reihenfolge übergeben: rdi, rsi, rdx, rcx, r8 und r9.
Ein ganzzahliger Wert wird im Register rax von der Funktion zurückgegeben.
Um eine Funktion aufzurufen, verwenden wir die Anweisung call.
Eine Funktion kann überall in derselben Quelldatei aufgerufen werden.
Damit eine Funktion für andere Quelldateien sichtbar ist, muss die Direktive global verwendet werden.
Zum Beispiel deklariert dies eine Funktion sum:
section .text ; functions are placed here
global sum ; sum is visible to other source files
sum:
; first argument is passed in rdi
; second argument is passed in rsi
; return value is placed in rax
mov rax, rdi ; rax is now equal to rdi
add rax, rsi ; rax = rax + rsi
ret ; function return
Und dies ruft unsere Funktion sum mit den Argumenten 3 und 5 auf:
mov rdi, 3 ; First argument in rdi
mov rsi, 5 ; Second argument in rsi
call sum
; The rax register now contains the value 8 (3 + 5), after sum returns