Handgeschriebenes Assembly ist voller Wiederholungen. Dieselbe kurze Folge von Anweisungen taucht immer wieder auf, mit kaum Variation, und es gibt nur wenige Abstraktionen, um Wiederholungen zu reduzieren. Hinzu kommt, dass man beim Programmieren in Assembly Werte verwalten muss, deren Bedeutung nicht offensichtlich ist. Es gibt keine benannten Parameter, und die meisten Details der darunterliegenden Maschinerie muss der Programmierer von Hand erledigen.
NASM, der Assembler, den dieser Track verwendet, bietet einige Werkzeuge, die dem Programmierer bei beiden Problemen helfen können. Diese Werkzeuge laufen zur Assemblierzeit, bevor eine Anweisung den Prozessor erreicht. Sie sagen dem Assembler, wie er den Quelltext umschreiben soll, noch bevor dieser überhaupt in Maschinencode übersetzt wird.
Das wichtigste dieser Werkzeuge sind Makros, die man sich als ein Rezept für den Assembler vorstellen kann. Sie werden an der Stelle, an der sie verwendet werden, entfaltet, als wäre der Code dort von Hand geschrieben worden.
Makros können eine Reihe von wörtlichen Token als Parameter entgegennehmen, wodurch sie in verschiedenen Kontexten wiederverwendbar sind.
Sie sind jedoch keine Funktionen: Es gibt kein call, kein ret und keine Übergabe der Kontrolle an eine andere Stelle im Code.
Die einfachste Definition gibt einem Wert oder einem kurzen Ausdruck mit %define einen Namen:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Jedes spätere Vorkommen von WIDTH wird vor der Assemblierung durch 8 ersetzt.
Die Ersetzung ist textuell, der Name wird also überall eingesetzt, wo er auftaucht, egal ob in einem Operanden oder innerhalb einer anderen Definition.
Für numerische Konstanten verwende lieber equ statt %define.
Ein einzeiliges Makro, das mit %define deklariert wird, wird nur beim Verwenden entfaltet und kann neu zugewiesen oder undefiniert werden.
Eine equ-Konstante hingegen wird am Punkt der Definition ausgewertet und nie verändert.
Ein %define kann auch benannte Parameter entgegennehmen und verhält sich dann wie eine kleine Inline-Funktion:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
Setze sowohl den gesamten Rumpf als auch jeden Parameter in runde Klammern.
Mit %define square(x) x*x entfaltet der Aufruf square(1+1) zu 1+1*1+1, was 3 ergibt.
Geschrieben als %define square(x) ((x)*(x)) entfaltet derselbe Aufruf zu ((1+1)*(1+1)), was 4 ergibt.
Bei mit %define definierten Namen wird zwischen Groß- und Kleinschreibung unterschieden.
Die Variante %idefine macht den Namen unabhängig von Groß- und Kleinschreibung, falls das gewünscht ist.
Einzeilige Makros sind nützlich, um verschiedenen Teilen des Codes eine Bedeutung zu geben. Mit Sorgfalt verwendet verbessern sie die Lesbarkeit und verringern die Wahrscheinlichkeit von Tippfehlern. Zum Beispiel kann ein Register, das innerhalb einer Funktion einen bestimmten Zweck erfüllt, nach seinem Zweck benannt werden:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
Eine ganze Codefolge mit beliebig vielen Zeilen wird mit %macro erfasst und mit %endmacro abgeschlossen.
Eine Zahl nach dem Namen gibt an, wie viele Argumente das Makro entgegennimmt, was auch null sein kann.
Werden Argumente übergeben, werden sie durch ein Komma (,) getrennt:
%macro example_macro 0
...
%endmacro
Argumente in mehrzeiligen Makros sind nicht benannt.
Sie werden als %1, %2 und so weiter bezeichnet, in der Reihenfolge, in der sie übergeben werden:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Beachte, dass die Argumente gültige Syntax für die im Rumpf des Makros ausgeführten Operationen sein müssen.
Hier lassen sich mult_two eax, ecx und mult_two r8, r9 beide assemblieren, mult_two eax, cl aber nicht, da imul eax, cl nicht definiert ist.
Ein Makro, das ein gewöhnliches Label definiert, geht kaputt, sobald es zweimal verwendet wird, weil das Label dann ebenfalls zweimal definiert wäre.
Um Labels in einem Makro zu verwenden, das mehr als einmal aufgerufen werden kann, muss man einem solchen Label %% voranstellen.
Das weist den Assembler an, diesem Label bei jeder Entfaltung einen anderen, eindeutigen Namen zu geben:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
Mit times kann man einen einzeiligen Block wiederholen.
Das ist besonders nützlich, wenn man wiederholte Werte im Speicher deklariert:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
Für die Wiederholung von mehrzeiligem Code können wir stattdessen %rep verwenden:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Beachte, dass die Anzahl der Wiederholungen direkt nach %rep angegeben wird und ein %endrep den wiederholten Block abschließt.
Häufig verwendet man einen Assemblierzeit-Zähler in einem %rep-Block, ähnlich wie eine Laufzeitschleife einen Zähler verwendet.
Während eine ganzzahlige Konstante zur Assemblierzeit mit equ deklariert werden kann, verwenden wir für ganzzahlige Variablen zur Assemblierzeit stattdessen %assign.
Anders als %define wird %assign sofort zu einer Zahl ausgewertet und kann neu zugewiesen werden:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
Nachdem der Präprozessor gelaufen ist, ist die Schleife verschwunden und vier add-Anweisungen bleiben übrig, wobei die Werte 0, 1, 2 und 3 direkt eingebaut sind.
Das ist Schleifenabrollen, das der Assembler erledigt: kein Zählerregister, keine Verzweigung, nur geradliniger Code.
Dem Präprozessor stehen verschiedene Bedingungsanweisungen zur Verfügung. Sie wählen zwischen mehreren verschiedenen Pfaden und entfalten ihren Rumpf an Ort und Stelle, aber nur, wenn ihre Bedingung zutrifft.
%if ... %elif ... %else ... %endif wählen einen Zweig anhand eines numerischen Tests aus:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
Makros werden oft in einer eigenen Datei gehalten und mit %include in jede Quelle eingebunden, die sie braucht:
%include "patterns.mac"
Die genannte Datei wird an dieser Stelle vor der Assemblierung wörtlich eingefügt, genau so, als wäre ihr Inhalt dort eingetippt worden. So bleiben die Definitionen an einem Ort und mehrere Quelldateien können sie gemeinsam nutzen.
Wenn die Makrodatei eigenständig assembliert werden kann, sollte sie die Dateiendung .asm haben.
Beachte, dass Makros nicht verlinkt werden.
Selbst wenn die Makrodatei assembliert wird, sind ihre Makros nur für eine andere Datei sichtbar, die sie mit %include einbindet.
Ansonsten kann eine reine %include-Datei jede Dateiendung haben.
Die Verwendung von entweder .asm oder .mac ist eine gängige Konvention.
Du pflegst die Makrobibliothek, auf der die Satz-Engine einer Zeitschrift aufbaut. Die Engine selbst, also die Funktionen, die jede Seite layouten, ist fest vorgegeben, und du schreibst sie nicht. Was du schreibst, ist die Maschinerie zur Assemblierungszeit, aus der sie besteht.
Für diese Übung musst du keine einzige Funktion schreiben, nur Makros. Diese Makros werden dann in Funktionen eingebunden und aufgerufen, die bereits in einer anderen Datei vordefiniert sind.
Du hast vier Aufgaben.
Jedes Makro wird mit genau dem Namen und der Argumentreihenfolge aufgerufen, die seine Aufgabe vorgibt, also schreibe sie exakt so. Sonst lässt sich die Datei nicht assemblieren.
Das Format der Zeitschrift besteht aus einer Handvoll fester Zahlen, und der Rest des Layouts verweist nur über den Namen auf sie. Du solltest jeder zur Assemblierungszeit einen lesbaren Namen geben:
COLUMN_COUNT ist 4
COLUMN_STRIDE ist 18
SPACE ist 0x20, das Leerzeichen-ByteRULE ist 0x3D, das Byte =
RULE_WIDTH ist 4
PLAIN ist 0x2E, das Byte .
RULED ist 0x2D, das Byte -
BOXED ist 0x23, das Byte #
Die Engine gibt diese in der oben genannten Reihenfolge aus.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run
Die Engine leert eine Setzfahne und zieht Linien für den Zeitungskopf, indem sie einen Lauf aus einem einzelnen Byte stempelt. Fasse diesen Schritt als mehrzeiliges Makro zusammen, das die Engine überall aufrufen kann, wo ein Lauf gefüllt werden muss.
Definiere zuerst einen lesbaren Namen COUNTER für das Register rcx.
Definiere dann das Makro fill_run, das ein Array mit N Kopien eines Bytes füllen soll.
Es nimmt drei Argumente in dieser Reihenfolge entgegen:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
Das Makro fill_run darf COUNTER beliebig überschreiben, aber kein anderes Register.
Beachte, dass die Anzahl der Bytes in einem 64-Bit-Allzweckregister wie rax oder rsi oder als Immediate wie 10 oder 4 übergeben werden kann.
Du kannst davon ausgehen, dass diese Anzahl nie negativ ist.
Dieses Makro wird dreimal aufgerufen, in zwei verschiedenen Funktionen.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns
Das Raster ist für das Format fest, sodass die Spaltenanfänge zur Assemblierungszeit berechnet werden können.
Definiere das Makro lay_columns, das COLUMN_COUNT Offsets schreibt, jedes eine 64-Bit-Ganzzahl.
Es nimmt zwei Argumente in dieser Reihenfolge entgegen:
lay_columns rsi, rax
Die Anzahl der an die Zieladresse zu schreibenden Offsets ist fest und gleich COLUMN_COUNT.
Jedes Offset ist ein 64-Bit-Wert, berechnet mit offset[i] = margin + i * COLUMN_STRIDE, wobei i der Index des Offsets ist und bei 0 beginnt und margin das zweite Argument ist.
Dieses Makro darf seine beiden Registerargumente überschreiben, aber keine anderen. Es wird einmal aufgerufen.
margin = 3
out = {3, 21, 39, 57}
set_header
Jeder der drei festen Abschnitte hat einen Header-Stil, der durch das Format festgelegt ist, sodass das Byte zur Assemblierungszeit bekannt ist.
Definiere set_header, das zwei Argumente in dieser Reihenfolge entgegennimmt:
0 für schlicht, 1 für liniert und 2 für gerahmt steht.Die Zieladresse ist der Ausdruck einer effektiven Speicheradresse.
Sie besteht aus einem Basisregister, das mit einem Indexregister, skaliert mit 1, 2, 4 oder 8, summiert werden kann, und einem Immediate:
set_header rdx+8*rcx+2, 1
Wähle das Header-Byte (PLAIN, RULED oder BOXED) mit bedingter Assemblierung (%if / %elif / %else) statt mit einer Laufzeitverzweigung.
Dieses Makro sollte kein Register überschreiben. Es wird dreimal in derselben Funktion aufgerufen.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Melde dich bei Exercism an, um x86-64 Assembly mit 22 Konzepte130 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.