Eigenheiten der ASM/XS-Syntax
1. Allgemein
Die Syntax des Assembler XS wurde vor allem von NASM übernommen. Allerdings habe ich auf einige (ev. nützliche) Fähigkeiten verzichtet, um das Programm möglichst klein zu halten. Vor allem beschränkt sich mein Assembler auf "flache" Binärdateien ohne Linking-Informationen. Deshalb entfallen die SECTION- oder SEGMENT- Direktiven und externe Labels.
Der Assembler XS benutzt immer 2 Passes (Wiederholungen) für die Übersetzung eines Quelltextes. In beiden Passes wird der Quelltext ganz bearbeitet. Der erste Pass dient dazu, Label-/Konstantennamen zu sammeln, und im 2. Pass, in dem diese Namen bekannt sind, wird das Programm in die Ausgabedatei geschrieben. Werden im 1. Pass Fehler entdeckt, hört der Assembler schon nach dem 1. Pass auf; werden im 2. Pass Fehler entdeckt, wird die Ausgabedatei sofort geschlossen und gelöscht. Der Assembler arbeitet dann weiter, ohne in die Datei zu schreiben. Bei kritischen Fehlern (Datei-I/O, Labelcache voll) bricht der Assembler sofort ab.
Der Assembler XS unterscheidet nicht zwischen Gross- und Kleinbuchstaben, ausser innerhalb von Stringkonstanten.
2. Zahlen, Konstanten und Labels
Diese Ausdrücke werden im Folgenden alle als "Zahlkonstanten" bezeichnet.
2.1. Zahlen
Der Assembler XS unterstützt folgende Zahlenformate:
- Dezimalzahlen, diese werden ohne Zusatz angegeben, z.B.
104. - Sedezimalzahlen (auch bekannt als Hex(adezimal)zahlen, dies ist
aber ein lateinisch/griechisches Mischwort) können in folgenden
Formen geschrieben werden:
$F1,0xF1,0F1h. An diesen Beispielen erkennt man auch, wo eine führende 0 vor Buchstabenziffern gesetzt werden muss. - Binärzahlen, z. B.
1010010011110000bund001b - Strings mit max. 2 Zeichen = 1 Word. Die Strings dürfen (wie
üblich) mit Hochkommata (') oder Gänsefüsschen (")
eingeschlossen werden, nur muss am Anfang und am Ende dasselbe stehen.
Bei Strings mit 2 Zeichen ist das erste das Low-Byte, d. h. wenn z. B.
der String "LH" in AX gelegt wird, ist nachher AL "L" und AH "H". Damit
kann man bequem per Word-Transfer (z. B.
mov ax, [stringvar]oderlodsw) eingelesene Strings mit diesen Konstanten vergleichen. Allerdings entstehen dadurch beim DW-Befehl Probleme (siehe 3.2.).
Wie schon bei 1. erwähnt, werden Gross- und Kleinbuchstaben nicht unterschieden, deshalb darf man auch die Zahlenpräfixe und -suffixe beliebig schreiben.
2.2. Konstanten und Labels
Konstanten und Labels sind Ausdrücke, die für eine Zahl stehen. Sie werden vom Assembler XS beim Lesen (Verwenden in einem Befehl) genau gleich behandelt, zum Schreiben (Definieren) benutzt man diese Befehle:
label: mov ax,
konstante ;Labeldefinition mit
Doppelpunkt (wie gewohnt), Befehle in der gleichen Zeile möglich
konstante=
$1234
;Konstantendefinition mit Gleichheitszeichen (man beachte die
Abstände!)
Bei Konstantendefinitionen dürfen auf der rechten Seite alle möglichen Zahlkonstanten stehen, nur sind Ausdrücke mit weiter unten definierten Label-/Konstantennamen nicht möglich und führen zu einer Fehlermeldung. Neudefinitionen von Konstanten und Labels sind erlaubt, es wird jedoch eine Warnung ausgegeben. Dabei wird die Ausgabedatei immer noch weiter erstellt.
Der Ausdruck @ bezeichnet immer die aktuelle Stelle im
Code (den angenommenen IP). In anderen Assemblern wird diese Funktion
meistens durch $ dargestellt.
Die vordefinierten speziellen Labels SBYTE und SBYTE2 werden im Kapitel 4.4 (Optimierung) näher erklärt.
2.2.1. Lokale Labels
Ähnlich wie bei NASM kann man mit dem Assembler XS lokale Labels definieren. Der "Name" beschränkt sich dabei auf eine Dezimalzahl (Index), der wie bei den lokalen Labels von NASM ein Punkt vorangeht. Der Index muss grösser als 0 sein. Diese Labels habe ich vor allem deshalb eingeführt, damit man nicht mehr nichtssagende Namen (Loop1 etc.) für Ziele von Schleifen und kurzen Sprüngen definieren und erst noch aufpassen muss, dass man den Namen nicht schon vergeben hat. Diese lokalen Labels funktionieren folgendermassen:
PROC1:
mov ax, $0E30
mov cx, 5 ;5 Nullen
ausgeben
.1:
int $10
loop .1
or bl, bl ;(sinnlos)
jnz .2
jmp WEIT_WEG
.2:
(...)
PROC2:
;Bei der Definition eines (überall
gültigen) Labels beginnt die Zählung der lokalen Labels
xor ax,
ax ;wieder
von vorne bei .1
mov di, PUFFER
mov cx, 5
.3:
;Eine Reihenfolge der Indizes muss nicht eigehalten werden.
stosb
add di, 5
loop .3
;3 Zeilen zurück
or bl, bl
jz .1
jmp WEIT_WEG
.1:
jmp
.2
;dies gibt einen Fehler, da .2 noch nicht definiert ist
Im Assembler wird der "Neubeginn" durch eine Variable realisiert, die vor dem Zugriff auf den Wertespeicher zur eigentlichen Zahl addiert wird. Wenn ein "richtiges" Label definiert wird, addiert das Programm den höchsten seit dem letzten "richtigen" Label definierten Index zu dieser Variable, wodurch die vorher definierten lokalen Labels "nach unten", also in negative Indizes bis .0 geschoben werden und damit nicht mehr auf sie zugegriffen werden kann. Da diese Labels aber dennoch gespeichert sind, sind sie dem Assembler im 2. Pass noch bekannt.
2.3. Kombinierte Ausdrücke in Klammern
Aus mehreren solche Zahlkonstanten kann man durch verschiedene Operatoren einen neuen Wert berechnen. Für den Assembler XS muss dieser ganze Ausdruck in runden Klammern stehen, ausserdem dürfen keine Leerzeichen in der Klammer stehen, da sie sonst als mehrere Operanden interpretiert wird. Es sind folgende Operatoren möglich:
- + (Addieren)
- - (Subtrahieren)
- * (Multiplizieren)
- / (Dividieren)
- % (Modulus: Rest der Division)
Wichtig: Aus Spargründen wird der Ausdruck ohne Beachtung der
Vorrangregeln der Operationen von links nach rechts durchgegangen, wie es ein einfacher
Taschenrechner tut. Der Ausdruck (align-position%align) wird also nicht
als (align−(position%align)) berechnet, sondern
als ((align−position)%align). Solche weitere Klammern können nicht gesetzt werden,
die Berechnung solcher Werte ist aber durch zusätzliche Hilfskonstanten möglich
(siehe Beispiel unten).
Beispiele:
cmp ax, bx
jne (@+5)
;Nächsten Sprungbefehl überspringen
jmp ziel
align= 16
position=
@
;für bessere Übersichtlichkeit, da @ wie ein Operator aussieht
lowbits= (position%align)
rb (align-lowbits) ;Nächsten Befehl
an einer durch 16 teilbaren Adresse ausrichten
3. Variablen, Anlegen von Daten
3.1. ORG und RES(B,W)
Wie bei NASM ist die ORG-Direktive nur dazu da, den vom Assembler angenommenen Wert des Programmzählers (IP) zu setzen. Man kann damit nicht in der Datei vor- und zurückspringen, wie es mit MASM möglich ist. Allerdings kann mit ORG mehrmals der Wert geändert werden, was vor allem für unterschiedliche Bereiche wichtig ist. zum Beispiel für einen Bootsektor, der vom Programm auf die Diskette geschrieben wird:
org
$100 ;DOS .COM-Datei
mov ax, $0301
mov bx, BOOTSECT
mov cx, $0001
xor dx, dx
int $13
;Bootsektor auf Diskette schreiben
int $20
;und beenden
BOOTSECT:
org
$7C00
;hier würde NASM "Error: Program origin redefined!" ausgeben
mov ah, $0E
mov si, BOOTMSG
;funktioniert ohne ORG nicht!
mov cx, 22
AUSGABE:
lodsb
int $10
loop AUSGABE
(...)
BOOTMSG:
db 'Ich bin der Bootsektor'
(...)Auch die Befehle RESW und RESB dienen dazu, den angenommenen IP-Wert zu ändern, vor allem bei der Definition von Labels, die nicht auf Codeteile weisen. Sie funktionieren genau wie bei NASM, nämlich indem sie den (bei RESW verdoppelten) Operanden zum IP-Wert addieren.
3.2. Anlegen von Variablen, Bezug im Code (Adressoperanden mit eckigen Klammern)
Die von anderen Assemblern bekannten Befehle DB und DW und die XS-spezifischen D oder DATA, RB und RW dienen dazu, Daten zu definieren, die in die Programmdatei geschrieben werden. Die Befehle haben folgende Funktion:
db $15, 'Hallo
Welt' ;bekannte Funktion
db
'CX'
;hier steht "CX" in der Ausgabedatei
dw $1234,
$1235
;bekannte Funktion
dw
'CX'
;Achtung: Aufgrund des inneren Aufbaus des Assemblers (DW ruft immer
den Zahlenparser auf,
;DB benutzt für Strings eine eigene Prozedur) wird hier "XC" in
die Datei geschrieben.
d 4, 'Hallo',
$FFFF ;D erlaubt es, Words und
Bytes, vor allem auch Strings, zu mischen. Strings werden hier
;mit einer eigenen Prozedur behandelt. Bei Zahlen entscheidet der Wert
über die Breite
;(0-255: Byte; 256-65535: Word)
data $9999, 0, 'Hallo'
;DATA hat genau dieselbe Funktion und wird BASIC-Programmierern bekannt
vorkommen.
rb 4,
'a'
;wiederholt den 2. Operanden mit im 1. Operanden angegebener Anzahl.
Dieses Beispiel liefert also "aaaa".
rw 3,
0x1234
;entspricht dw 0x1234, 0x1234, 0x1234
rw 2,
'ah'
;ergibt "haha" (Grund siehe dw)
rb
4
;reserviert Null-Bytes oder -Words. Dieses Beispiel ergibt dasselbe wie
db 0,0,0,0
rb 3, "Langer Text"
;UNMÖGLICH!!! (RB behandelt Strings nicht mit
einer eigenen Prozedur, deshalb sind nur Bytes erlaubt.)
rb
(3+@%16*VAR)
;Es können beliebige Zahlkonstanten verwendet werden.
Bei der Definition von Variablen mit Namen und deren Verwendung im Code muss folgendes beachtet werden:
- Es wird keine Information über den Typ der Variablen (Word, Byte) gespeichert. Der Name ist ein normales Label, deshalb braucht es auch einen Doppelpunkt. Das im Folgenden über Variablennamen Gesagte gilt natürlich gleichermassen für Labels.
- Wenn im Code auf den Inhalt der Variablen zugegriffen wird, muss
der Variablenname in eckigen Klammern stehen, wie bei anderen Zahlen
oder bei Registern, z. B. entspricht
mov ax, [variable]dem Befehlmov ax, [$1234]. Um die Adresse der Variablen zu erhalten, wird der Name ohne Klammern geschrieben (mov ax, variableentsprichtmov ax, 0x1234). - Wenn weitere Konstanten oder Register verwendet werden,
müssen diese in der selben Klammer stehen, nicht in der
"Array-Form" von MASM. Beispiel:
mov cx, [es:variable+bx+di-5](oder auches mov cx, [variable+bx+di-5]). Dies ergibt sich wieder daraus, dass Variablennamen wie Zahlen behandelt werden. Mit diesen Namen kann auch genauso gerechnet werden, z. B.add bx, [0-variable+label-5]. Dabei ist nur Addition und Subtraktion möglich, Register können nur addiert werden (Subtraktion wegen des 8086-Befehlssatzes unmöglich). - Wichtig: Es dürfen keine Leerzeichen in der Klammer stehen, da der Assembler sie sonst als mehrere Operanden interpretiert und meldet, die Klammer sei nicht abgeschlossen ("Adressierung fehlerhaft").
Dies entspricht der Vorgehensweise von NASM, ausser der Beschränkung auf Addition und Subtraktion von Konstanten.
4. Eigenheiten bei Prozessorbefehlen
4.1. Präfixe
Das Segment-Override-Präfix kann auf folgende Arten angegeben werden:
es
lodsb
;hier die einzige Möglichkeit
es mov ax,
[$1234] ;oder:
mov ax, [ES:$1234]
es mov ax, [ES:$1234]
;Hier wird das ES-Präfix zweimal in den Code geschrieben!
Die restlichen Präfixe sind REP, REPE, REPNE und LOCK.
4.2. Zusätzliche Angaben (Operationsbreite etc.)
Um bei Operationen die Breite festzulegen, verwendet der Assembler XS Suffixe, die analog zum Fall LODSB/LODSW an den Befehls-Mnemonic angehängt werden:
mov [VARIABLE],
$525 ;Hier ist die
Breite definiert, denn $525 hat nur in einem Word Platz
mov [VARIABLE], 1
;Hier ist Word- oder
Byte-Breite möglich, der Assembler wird reklamieren.
movw [VARIABLE], 1
;Word-Breite
festgelegt
movb [VARIABLE], 1
;Byte-Breite
festgelegt
movw ax,
bx
;Diese Suffixe sind bei den meisten Operationen auch dann erlaubt, wenn
sie
;nicht nötig wären, aber:
pushw
[VARIABLE]
;UNMÖGLICH!!! Der Assembler wird "Operand fehlerhaft" ausgeben.
inw ax,
<dx>
;ebenfalls UNMÖGLICH!!!
jmps
ShortZiel
;Auch das funktioniert nach dem gleichen Schema
callf [bx]
;FAR-Call (Intersegment)
call
[bx]
;NEAR-Call (Innerhalb Segment)
4.3. Port-I/O-Befehle
In Analogie zu den eckigen Klammern bei Adressen für RAM-Zugriffe werden bei IN und OUT spitze Klammern (Kleiner/Grösser-als-Zeichen) für die Portnummer verwendet. Ein Vorteil davon ist auch, dass diese Klammern schnell ins Auge fallen und so die oft massenhaft auftretenden Port-Transfer-Befehle in Gerätetreibern gut sichtbar (ev. auch durch Editor-Highlighting) machen. So sieht man sofort, an welchen Ports das Programm allenfalls kritische Ein- und Ausgaben macht:
mov al, [$61]
;analog zu:
in al, <$61>
out <(2*BASEPORT+1)>, ax
;Wieder sind alle Arten von Zahlkonstanten erlaubt, aber
bei
;solchen kombinierten Werten müssen die runden Klammern stehen.
out <dx>, ax
4.4. Optimierung, Byte-Zahlkonstanten
Der Assembler XS ist ein 2-Pass-Assembler und zusätzlich möglichst klein und sparsam konzipiert. Deshalb muss man normalerweise selbst den Code optimieren, der erzeugt wird, vor allem muss für einen kurzen Sprung der Befehl JMPS verwendet werden, sonst wird immer ein langer Sprung generiert. Einige Dinge regelt der Assembler aber automatisch:
- Wenn es für einen Befehl mehrere Darstellungen gibt, die
nicht von der Grösse von (oft erst im Nachhinein bekannten)
Operanden abhängig sind, wird die kürzeste gewählt, so
z. B. die 3 Byte lange Form für
mov ax, [variable]undmov [variable], ax. - Bei Adressangaben mit kleinen Displacements (Konstantanteilen)
wird automatisch die platzsparendere Byte-Form gewählt, so z. B.
bei
mov ax, [bx-5]. - Bei einigen arithmetischen Befehlen (ADD etc.) ist es möglich, Word-Operanden mit Byte-Zahlkonstanten zu verrechnen (Beispiel: add ax, 5). Diese Möglichkeit wird vom Assembler XS automatisch gewählt, spezielle Befehlssuffixe dafür existieren nicht. Man kann den Assembler aber mit SBYTE dazu "zwingen", siehe unten.
Die letzten beiden Punkte sind vom Wert der Zahlkonstanten abhängig. Da diese unter Umständen im 1. Pass noch nicht bekannt sind, aber trotzdem schon die Länge des Opcodes für Label-Berechnungen benötigt wird und deshalb im 2. Pass nicht mehr geändert werden darf, gelten alle Labels, ob definiert oder undefiniert, als Word-Konstanten.
Um trotzdem ein Programm optimieren zu können, ohne die genauen Adressen selbst zu berechnen, stellt der Assembler XS zwei spezielle Labels zur Verfügung, SBYTE und SBYTE2. Bei ihrer Definition wird geprüft, ob der Wert tatsächlich als vorzeichenbehaftetes (signed) Byte dargestellt werden kann; wenn nicht, wird eine Fehlermeldung ausgegeben. Beim Einsetzen in einen Maschinenbefehl kann – wie bei einer Zahl – die kürzere Opcode-Form verwendet werden. Wenn SBYTE aber zusammen mit weiteren Konstanten auftaucht (z. B. [SBYTE+bx+5] oder (SBYTE+5)), kann die Byte-Grösse nicht mehr gewährleistet werden, und es wird wieder immer die Word-Form verwendet.
