Vorheriges Kapitel (Speichermodelle) | Nächstes Kapitel (Strukturiertes Programmieren) |
In diesem Kapitel werden die wichtigsten Befehle erläutert, die es Ihnen ermöglichen, erste kleine Programme zu schreiben. Dieses Kapitel kann allerdings keine genaue Befehlsreferenz ersetzen, die an vielen Stellen wesentlich detailliertere Auskünfte geben kann.
Ein Prozessorbefehl hat eine Länge von mindestens einem bis zu 15 Bytes. Die Befehle (der Maschinencode, den die Mnemonics repräsentieren) sind so codiert, daß der Prozessor die Befehlslänge aus den ersten Bits auslesen kann. Ein übersetzter Befehl wird auch als OpCode (Operation Code) bezeichnet.
Da die meisten Befehle mindestens einen Parameter erwarten, dieser aber
unterschiedlichen Typs sein kann, verwende ich folgende Kennzeichnung:
memxx
für Variablen und Speicherstellen
regxx
für Register
conxx
für Konstanten
das 'xx' steht in jedem Fall für die Datengröße des Parameters in Bits. Diese wird in
aller Regel entweder 8, 16 oder 32 sein.
Wenn ein Assemblerbefehl Parameter erwartet, dann zumeist nach folgendem
Muster:
Befehl Ziel [,Quelle] |
Viele Befehle verändern bei ihrer Ausführung die Bits im Flag-Register und
zeigen so an, wie das Ergebnis der Ausführung aussieht.
In den entsprechenden Fällen werden Angaben gemacht, in welcher Weise die Flags
verändert wurden.
mov Ziel, Quelle |
DATASEG MyByte db 7 MyWord dw 0C3Fh MyWord2 dw 8657 MyConst = 249 CODESEG ... mov ax,34Fh mov bl,[MyByte] mov di,[MyWord] mov [MyWord],ax mov dh,34 mov [MyByte],dh mov [MyByte],128 mov cl,MyConst mov [MyWord],MyConst ... mov [MyWord],[MyWord2] ;verursacht einen Fehler mov [MyWord],al ;verursacht einen Fehler mov al,[MyWord2] ;verursacht einen Fehler mov al,cx ;verursacht einen Fehler |
Leider ist das Kopieren eines Speicherwertes in einen anderen nicht gestattet,
deshalb führt der Befehl mov [MyWord],[MyWord2]
zu einem Fehler.
Wie schon angesprochen, müssen die Operanden die gleiche Größe aufweisen. In den
letzten drei Anweisungen ist dies nicht der Fall; es wird immer ein Byte mit
einem Word konfrontiert (s. dazu nächstes Kapitel).
Beachten Sie, daß es nicht möglich ist, ein Segmentregister direkt mit einer
Konstanten zu laden. Sie müssen den Wert immer erst in ein allgemeines Register
schreiben und von dort in das Segmentregister schreiben.
Dafür ist es durchaus möglich, ein Segmentregister direkt mit einem
Speicherwert zu beschreiben.
mov es.0B800h ;verursacht einen Fehler mov ax,0B800h ;erst in ein allg. Register mov es,ax ;dann ins Segmentregister mov [MyWord],0B800h mov es,[MyWord] ;kein Problem |
xchg Operand1, Operand2 |
mov
dürfen beide
Operanden nicht zugleich Speicherstellen sein.
Beispiele:
xchg dx,cx xchg [MyByte],dh xchg bx,[MyWord] |
xchg ax,reg16 ;benötigt nur 1 Byte xchg eax,reg32 |
Das ist einer der wenigen Befehle, wo die Operandenreihenfolge keine Rolle spielt, schließlich werden sie nur gegeneinander ausgetauscht.
Die folgenden Befehle erlauben es, eine Kombination aus einem Segment- und
einem Nicht-Segmentregister mit einem einzigen Befehl mit Werten zu füllen.
Dabei wird das Segmentregister durch den Befehl selbst bestimmt (LDS füllt also
das DS-Register). Das Nicht-Segmentregister ist der erste Operand des Befehls.
Der zweite Operand ist ein 32-Bit-Speicherwert, dessen obere 16 Bit ins
Segmentregister geschrieben werden. Folglich werden die unteren 16 Bits in das
als ersten Operanden übergebene Nicht-Segmentregister geschrieben.
DATASEG MyDWord dw 012345h CODESEG ... lds reg16,mem32 ;allg. Form am Beispiel von LDS lds ax,[MyDWord] ;konkrete Form |
LFS
und LGS
stehen erst ab dem
80386 zur Verfügung.
lea Ziel, Quelle |
Der erste Operand muß ein 16- bzw. 32-Bit-Register sein, der zweite Operand muß ein Speicherwert sein. Der Befehl lädt die effektive Adresse (LEA = Load Effective Address) des Speicherwertes nach Abschluß der Adressberechnungen
Im allgemeinen wird der Befehl verwendet, um Register für Interruptfunktionen mit Werten von Speicheroperanden zu füllen. Es gibt für diesen Zweck eine Assemblerdirektive, die das gleiche erledigt.
DATASEG MyText db "Adresse gesucht" CODESEG ... lea ax,MyText ;Version mit LEA mov ax, OFFSET MyText ;mit Assemblerdirektive |
Die Variante mit der Assemblerdirektive ergibt nur Sinn, wenn die Adresse des
Speicherwertes zur Übersetzungszeit bekannt ist. In anderen Fällen (z. B. als
Parameter bei einem Unterprogramm) muß lea
verwendet werden.
Die Intel-Prozessoren verfügen über eine Vielzahl von Adressierungsarten bei
denen eine vielfältige Kombination von Registern und Konstanten über Addition
und Multiplikation möglich ist. Mit lea
ist es möglich, eine
Adresse in einem Schritt zu errechnen, statt die Berechnung über mehrere
Zwischenschritte mit den entsprechenden Operationen auszurechnen. In diesen
Fällen spielt lea
seine Stärke aus, da die Berechnung wesentlich
schneller ausgeführt wird als über einzelne Operationen.
Der Befehl in
ist als eine Sonderform des mov-Befehles zu
verstehen. Allerdings ist die Quelle der Daten kein Register oder eine
Speicherstelle, sondern einer der I/O-Ports, über die der Prozessor mit der
Computerperipherie verbunden ist.
Die möglichen Erscheinungsformen dieses Befehls lauten:
in Ziel, Quelle ;generelle Form in eax/ax/al, Port ;1. spezifische Form in eax/ax/al, dx ;2. spezifische Form |
Als Ziel ist natürlich nur jeweils eines der drei angegebenen Register
zugelassen. Dabei ist das Register EAX erst ab dem 80386 möglich.
Der 80x86 kann theoretisch mit 65536 Ports kommunizieren. Um alle Ports
erreichen zu können muß die gewünschte Portnummer als Word übergeben werden.
Der Parameter Port in der 1. spezifischen Form ist aber ein Byte-Wert. Aus
diesem Grund lassen sich mit der 1. Form nur die ersten 256 Ports erreichen.
Statt Port wird also eine konkrete Zahl im Bereich 0..255 angegeben.
Für Ports mit höheren Portnummern müssen Sie die Nummer ins Register DX
schreiben und mit der 2. spezifischen Form arbeiten.
Hier ein paar Beispiele
in al,060h ;liest ein Byte vom Port 60h nach AL in eax,0A0h ;ein DWord von Port 0A0h nach EAX mov dx,04F3Ch ;Portnummer > 255 nach DX in ax,dx ;vom Port in DX ein Word nach AX |
Dieser Befehl ist die Umkehrung des Befehls in
. Es gelten die
gleichen Bedingungen.
out Ziel, Quelle ;allgemeine Form out Port, al/ax/eax ;1. spezifische Form out dx, al/ax/eax ;2. spezifische Form ; out 34h,ax ;den Wert in AX an Port 34h senden mov dx,04573h ;Portnummer > 255 nach DX out dx,al ;an diesen Port den Inhalt von AL senden |
Die Befehle in und out werden hauptsächlich in der hardwarenahen Programmierung
eingesetzt. Überall da , wo externe Hardwarekomponenten angesprochen werden
sollen (extern=außerhalb der CPU), kann man entweder auf ein BIOS für diese
Komponente zurückgreifen (so vorhanden), oder direkt über Ports auf die
Hardware zugreifen. Beispiele sind die Programmierung von Grafikkarten oder
Netzwerkkarten.
Ports stellen die unterste und damit hardwarenächste Ebene der Programmierung
dar. Das bedeutet aber auch, daß Sie sich ein Gebiet begeben, das nicht in
jeder Hinsicht standardisiert ist. Sie könnten also bei intensiver
Portprogrammierung Software erstellen, die nur auf einer Hardwarebasis läuft,
da zum Beispiel der gleiche Port auf unterschiedlichen Grafikkarten zu
verschiedenen Reaktionen führt.
Als letzten Befehl in dieser Kategorie besprechen wir xlatb
. Dieser
Befehl ist in der Lage, über einen Index, der im Register AL liegen muß, einen
Wert aus einer Tabelle zu entnehmen, deren Anfangsadresse im Register BX
steht. Der entnommene Wert wird seinerseits in das Register AL geschrieben.
Dieser Befehl wird häufig in Umsetzungstabellen verwendet, z. B. um verschiedene
Zeichensätze umzuwandeln.
Dadurch, daß der Index im Register AL liegt, kann die Tabelle nicht größer als
256 Bytes sein. Ebenso ist immer nur der Zugriff auf Byte-Elemente möglich,
eine Tabelle mit Word-Elementen macht also keinen richtigen Sinn.
DATASEG MyTable db 'Hallo, wie geht es Dir' CODESEG ... mov bx,OFFSET MyTable ;Adresse der Tabelle mov al,12 ;der Index in die Tabelle xlatb ;Wert holen und in AL schreiben |
Da die Zählung bei Null beginnt, wird hier das 'e' aus dem Wort 'geht' ins Register Al geschrieben.
Xlatb
ist eine verkürzte Form von xlat Tabelle
. Bei
diesem Befehl wird durch den Operanden Tabelle lediglich festgelegt, in welchem
Segment sich die Tabelle befindet, der Offset wird trotzdem in BX erwartet. Bei
xlatb
wird grundsätzlich davon ausgegangen, daß die Adresse der
Tabelle über DS:BX zu ermitteln ist.
Es gibt noch weitere Befehle, die Daten bewegen können, diese gehören aber in die noch andere Befehle umfassende Klasse der Stringbefehle, die in einem eigenen Kapitel behandelt werden.
Es kann, wie auch im vorherigen Abschnitt, vorkommen, daß Sie einen bestimmten
Datentyp vor sich haben, die nachfolgende Aktion aber mit einem anderen Datentyp
durchgeführt werden muß. Nehmen wir das Beispiel mov [MyWord],al
.
In diesem Fall wird versucht, eine Variable vom Typ Word mit einem Wert vom Typ
Byte zu beschreiben. TASM meldet sich in solchen Fällen mit dem Fehler
"Operand types do not match".
mov ah,0 mov [MyWord],ax |
Da nur das untere Byte (AL) von Wichtigkeit ist, wird mit dem ersten Befehl das High-Byte mit Null überschrieben. Wenn auch das High-Byte vernachlässigbar ist (in den folgenden Befehlen nicht verwendet wird), kann dieser Schritt auch entfallen. Danach wird das komplette Register AX in MyWord kopiert.
Hätte das Beispielmov ah,[MyWord]
gelautet, bestünde folgender
Ausweg:
mov ax,[MyWord] ;AX:=MyWord xchg al,ah ;AH:=LowByte(MyWord) |
In beiden Fällen wäre jeweils der Inhalt des Registers AH verloren gewesen. Wichtige Daten hätten vorher also in andere Register, auf den Stack oder in Variablen kopiert werden müssen.
Das sogenannte Type-Casting bietet die Möglichkeit, diese Umwege zu vermeiden.
Im ersten Beispiel ist es den Umständen entsprechend egal, was im High-Byte
steht, deswegen ist für diese Fälle das Typ-Casting nicht so wichtig.
Möchte man dagegen im zweiten Beispiel tatsächlich das Low-Byte von MyWord
haben, dann kann man einfach folgendes schreiben:
mov ah,[byte MyWord] |
byte
in der eckigen Klammer werden die oberen 8
Bits von MyWord abgeschnitten; sie fallen einfach aus dem Betrachtungsrahmen.
Auf diese Weise erhält man das Low-Byte von MyWord, ohne zusätzlich
Registerinhalte zu überschreiben oder zusätzliche Befehle verwenden zu müssen.
Natürlich läuft es ähnlich ab, wenn Sie ein Word erhalten wollen, z. B.
mov ax,[word MyByte]
oder ein DWord (mov edx,[dword
MyWord]
Wenn Sie von einem größeren Datentyp in einen kleineren casten, dann wird der überschüssige Teil abgetrennt. Im umgekehrten Falle ist der hinzukommende Teil undefiniert. Es macht also keinen Sinn, mit Befehlen zu arbeiten, die diesen undefinierten Teil verwenden.
cbw |
Dieser Befehl erweitert das Byte im Register auf ein Word (cbw=convert byte to
word). Dabei wird eine Vorzeichenerweiterung durchgeführt. Dies bedeutet, daß
Bit 7 des Quellbytes (in AL) in die Bits 8-15 kopiert wird. Ist im Register AL
das 7. Bit gesetzt, dann wären nach Ausführung dieses Befehls die Bits 8-15 im
Register AX ebenfalls gesetzt. Da implizit immer mit AL und AX gearbeitet wird,
existieren keine Parameter.
Den Befehl gibt es bereits im 8086. Insbesondere wird er bei 8-Bit-Divisionen
wichtig.
cwd |
Dieser Befehl führt eine Vorzeichenerweiterung des Words im Register AX auf ein
DWord durch. Das 32-Bit-Ergebnis wird im Registerpaar DX:AX abgelegt. Der
Befehl existiert bereits seit dem 8086, daher wird keine Erweiterung in das
Register EAX durchgeführt. Bit 15 des Registers AX wird in alle Bits des
Registers DX kopiert.
Es werden keine Parameter benötigt. Wie cbw
hat der Befehl eine
wichtige Funktion bei der Division.
cwde |
Dieser Befehl macht im Grunde das gleiche wie cwd
, allerdings wird
das 32-Bit-Ergebnis in das Register EAX geschrieben.
Der Befehl ist erst ab dem 80386 verfügbar.
cdq |
CDQ
führt eine Vorzeichenerweiterung des 32-Bit-Wertes im Register
EAX auf einen 64-Bit-Wert durch. Das Ergebnis wird in den Registern EDX:EAX
abgelegt. Bit 31 des Registers EAX wird in alle Bits des Registers EDX
kopiert.
Wie cwde
ist dieser Befehl erst ab dem 80386 verfügbar.
;AL auf 32 Bit nach DX:AX erweitern cbw cwd ;AL auf 32 Bit nach EAX erweitern cbw cwde ;AL auf 64 Bit nach EDX:EAX erweitern cbw cwde cdq |
movsx Ziel, Quelle |
cbw
und cwde
. Er steht für MOVe with Sign eXtension.
movsx reg16, reg8 movsx reg16, mem8 movsx reg32, reg8 movsx reg32, mem8 movsx reg32, reg16 movsx reg32, mem16 |
movsx ax, al ;CBW movsx eax, ax ;CWDE movsx eax,al ;Kombination von CBW und CWD |
MOVSX
ist ab dem 80386 verfügbar. Es gibt keine
movsx
-Äquivalente für cwd
und cdq
.
movzx Ziel, Quelle |
MOVZX
(MOVe with Zero eXtension) funktioniert eigentlich genauso
wie movsx
, allerdings mit dem Unterschied, daß hier nicht der Wert
des höchstwertigen Bits des Quelloperanden kopiert wird sondern in jedem Falle
eine Null.
Die Syntax entspricht der von movsx
. Der Befehl ist ab dem 80386
verfügbar.
Beachten Sie, daß sowohl movsx
als auch movzx
auch
Datentransportbefehle sind. Es ist also durchaus möglich, daß bei zwei
Registeroperanden das Quellregister nicht Bestandteil des Zielregisters ist.
So ist bei den ersten vier Befehlen dieses Unterkapitels
(cbw,cwd,cwde,cdq
) die implizite Quelle immer Teil des ebenso
impliziten Ziels.
Bei movsx
und movzx
kann das Ziel beispielsweise AX
sein und die Quelle in BL liegen.
Sollte eine zero extension von 8 auf 16 Bit in einem allgemeinen
16-Bit-Register (AX, BX, CX, DX) durchgeführt werden, so ist die Verwendung
eines einfachen mov
schneller und kürzer.
statt: movzx ax,al lieber: mov ah,0h |
movzx
vorzuziehen.
bswap reg32 |
Es gibt zwei weitverbreitete Formen des Speicherorganisation. Sie nennen sich
Little Endian und Big Endian. Little Endian findet vor allem bei
x86-Prozessoren Anwendung, Big Endian wird dagegen bei den 68000 von Motorola
(verwendet in Apples Macintosh) sowie bei vielen RISC.Prozessoren
angetroffen.
Little Endian zeichnet sich dadurch aus, daß der niederwertigste Teil eines
Datums im Speicher an der niedrigsten Adresse steht. Bei einem 32-Bit-Wert
stehen also die Bits 0-7 an der niedrigsten Adresse, dann folgen die Bits 8-15,
16-23 und 24-31.
Bei Big Endian verhält es sich genau umgekehrt: die Bits 24-31 stehen an der
niedrigsten Adresse, dann folgen die Bits 16-23, 8-16 und an der höchsten
Adresse stehen die Bits 0-7.
BSWAP
nimmt eine Konvertierung zwischen diesen beiden Formaten
vor, indem das erste und vierte Byte sowie das zweite und dritte Byte
gegeneinander ausgetauscht werden.
Die berechtigte Frage, warum man sich um diese Organisationsfragen kümmern
soll, da x86-Programme sowieso nicht auf 68000-Prozessoren laufen, ist leicht
beantwortet.
Die eben selbst gegebene Antwort ist natürlich korrekt. Es ist allerdings
durchaus üblich, Daten zwischen Rechnern auszutauschenen, die eine
unterschiedliche Speicherorganisation verwenden. Um bei der Auswertung dieser
Daten auf korrekte Ergebnisse zu kommen, ist eine Umwandlung nötig.
bswap
auf dem gleichen Register
die ursprüngliche Datenorganisation wiederherstellt.BSWAP
ist erst ab dem 80486 verfügbar.
Die arithmetischen Befehle des 8086 arbeiten alle mit ganzen Zahlen. Um mit Fließkommazahlen zu arbeiten, müssen Sie den mathematischen Koprozessor (z. B. 8087) bemühen.
Es werden Befehle für die normalen Grundrechenarten zur Verfügung gestellt, Befehle zum einfachen Addieren und Subtrahieren sowie zur Modulo-Rechnung, Befehle für die Arbeit mit BCDs (Binary Coded Decimals) sowie Befehle, die das Carry-Flag verwenden.
add Ziel, Quelle |
Zum ersten Operanden wird der zweite Operand addiert und im ersten Operanden gespeichert. Beide Operanden müssen den gleichen Datentyp besitzen. Als Operanden kommen Register, Konstanten und Speicherstellen in Byte-, Word-, und DWord-Größe in Frage. Das Ziel darf keine Konstante sein und eine Kombination von zwei Speicherstellen ist ebenfalls nicht gestattet. Ansonsten ist eine beliebige Reihenfolge von Register, Konstanten und Speicherstellen möglich
Beispiele:
add ax,24 add [MyByte],15647 add bx,[MyWord] add [MyWord],cx add [MyWord],[AddWord] ;Fehler: zwei Speicheroperanden add 54676,dx ;Fehler: erster Operand ist Konstante |
sub Ziel, Quelle |
Vom ersten Operanden wird der Wert des zweiten Operanden abgezogen. Beide Operanden müssen den gleichen Datentyp besitzen. Als Operanden kommen Register, Konstanten und Speicherstellen in Byte-, Word-, und DWord-Größe in Frage. Das Ziel darf keine Konstante sein und eine Kombination von zwei Speicherstellen ist ebenfalls nicht gestattet. Ansonsten ist eine beliebige Reihenfolge von Register, Konstanten und Speicherstellen möglich
Beispiele:
sub ax,24 sub [MyByte],15647 sub bx,[MyWord] sub [MyWord],cx sub [MyWord],[SubWord] ;Fehler: zwei Speicherstellen sub 234,bl ;Fehler: erster Operand eine Konstante |
Ist der zweite Operand größer als der erste, dann ist das Ergebnis logischerweise negativ. Allein aus den Bits läßt sich aber nicht ablesen, ob das Ergebnis nun eine negative Zahl oder eine hohe positive Zahl ist. Tritt dieser Fall ein, dann wird das Carry-Flag gesetzt.
mul Faktor |
Mit mul
wird eine vorzeichenlose Multiplikation durchgeführt.
Der Faktor kann ein Register oder eine Variable mit einer Größe von 8, 16 oder
32 Bit sein. Abhängig von der Größe des Operanden unterscheidet sich auch das
Verhalten des Befehles.
Bei einer Operandengröße von 8 Bit wird der Inhalt des
Registers AL mit dem Faktor multipliziert, das Ergebnis liegt im gesamten
Register AX. Das Carry- und das Overflow-Flag werden auf Null gesetzt, wenn im
Register AH Null steht, ansonsten werden beide auf 1 gesetzt.
Bei einem Operanden mit Word-Größe wird der Inhalt von AX mit dem Faktor
multipliziert, das Resultat steht hinterher im Registerpaar DX:AX, wobei DX die
höherwertigen 16 Bits enthält. Carry- und Overflow-Flag werden auf Null
gesetzt, wenn DX Null ist, ansonsten werden beide auf 1 gesetzt.
Bei einem DWord-Operanden wird das Register EAX mit dem Faktor multipliziert
und das Resultat im Registerpaar EDX:EAX abgelegt. EDX enthält die
höherwertigen 32 Bits des Resultats. Carry- und Overflow-Flag werden auf Null
gesetzt, wenn EDX Null ist, ansonsten werden beide auf 1 gesetzt.
mul bl ;8-Bit-Multiplikation mul [MyWord] ;16-Bit-Multiplikation mul edx ;32-Bit-Multiplikation |
imul Faktor imul Ziel, Faktor1, Faktor2 ;ab 80186 imul Ziel, Faktor ;ab 80186 (ab 80386 erweitert) |
IMUL
führt eine Integer-Multiplikation unter Berücksichtigung des
Vorzeichens durch.
Wie Sie sehen können, gibt es drei verschiedene Formen. Bei den Formen mit mehr
als einem Operator müssen die Operatoren die gleiche Größe haben. Es sind
Byte-, Word- und DWord-Operanden erlaubt.
Wird nur ein Operand verwendet (die erste Variante), dann wird implizit
abhängig von der Operandengröße das Register AL, AX oder EAX verwendet.
Bei der Variante mit drei Operanden werden Operand 2 und 3 miteinander
multipliziert und das Ergebnis in den ersten Operanden geschrieben.
Die folgende Tabelle zeigt, wie bei den unterschiedlichen Varianten die Ablage
des Ergebnisses erfolgt und welche Operandenkombinationen möglich sind. Da als
Operanden sowohl Register als auch Konstanten sowie Speicherstellen möglich
sind, sind an den Stellen, an denen für einen Operanden verschiedene Typen
möglich sind, die Typen durch einen Schrägstrich getrennt aufgeführt.
Befehl | Ergebnis |
---|---|
IMUL reg8/mem8 | implizite Verwendung von AL, Ergebnis in AX |
IMUL reg16/mem16 | implizite Verwendung von AX, Ergebnis in DX:AX |
IMUL reg8/mem8 | implizite Verwendung von EAX, Ergebnis in EDX:EAX |
IMUL reg16,reg16/mem16 | Ergebnis im ersten Operanden |
IMUL reg32,reg32/mem32 | Ergebnis im ersten Operanden |
IMUL reg16,reg16/mem16,con8 | Ergebnis im ersten Operanden |
IMUL reg32,reg32/mem32,con8 | Ergebnis im ersten Operanden |
IMUL reg16,con8 | Ergebnis im ersten Operanden |
IMUL reg32,con8 | Ergebnis im ersten Operanden |
IMUL reg16,reg16/mem16,con16 | Ergebnis im ersten Operanden |
IMUL reg32,reg32/mem32,con32 | Ergebnis im ersten Operanden |
IMUL reg16,con16 | Ergebnis im ersten Operanden |
IMUL reg32,con32 | Ergebnis im ersten Operanden |
div Divisor |
Div
führt eine vorzeichenlose Division durch. Der Dividend ist
implizit immer gegeben, nur der Divisor wird als Operand übergeben.
Die Division erfolgt so, daß immer ein ganzzahliger Rest zurückbleibt (der
natürlich auch den Wert Null haben kann). Wenn man nur den Rest betrachet, dann
kann man auf diese Weise auch gleich die Modulo-Operation durchführen.
Die folgende Tabelle zeigt, bei welchen Datentypen des Divisors welche
Register wie belegt werden.
Größe | Dividend | Divisor | Quotient | Rest |
---|---|---|---|---|
Byte | AX | reg8/mem8 | AL | AH |
Word | DX:AX | reg16/mem16 | AX | DX |
DWord | EDX:EAX | reg32/mem32 | EAX | EDX |
div dl ;8-Bit-Multiplikation div bx,[MyWord] ;16-Bit-Multiplikation div ecx ;32-Bit-Multiplikation |
idiv Divisor |
IDIV
führt eine Integer-Division unter Beachtung des Vorzeichens
durch. Sowohl der Dividend als auch der Ergebnisspeicher sind implizit
festgelegt und von der Operandengröße abhängig. Als Operanden kommen Register
und Speicherstellen in Byte-, Word- und DWord-Größe in Frage.
Die verwendeten Register stimmen mit denen des Befehls div
überein.
inc Ziel |
add Ziel, 1
, wird aber schneller
ausgeführt. Als Operanden kommen Register und Speicherstellen in Byte- Word-
und DWord-Größe in Frage.
Beispiele:
inc ax, inc ecx ;ab 80386 inc [MyByte] |
Wenn der Operand bereits den höchsten im Wertebereich erreichbaren Wert besitzt
und dann inkrementiert wird, dann wird als Reaktion das Overflow-Flag gesetzt
und der Operand erhält den Wert am unteren Ende des Wertebereichs, bei einem
Wertebereich von 0..255 (bei Byte-Operanden) also die Null.
Der Befehl inc
beeinflußt nicht das Carry-Flag. Wenn das ein Ziel
sein sollte, dann müssen Sie tatsächlich mit dem Befehl inc
mit
einer 1 als zweiten Operanden arbeiten.
dec Ziel |
sub Ziel, 1
, wird aber
schneller ausgeführt und benötigt weniger Platz. Als Operanden kommen Register
und Speicherstellen in Byte- Word- und DWord-Größe in Frage.
Beispiele:
dec ax dec ecx ;ab 80386 dec [MyByte] |
Dieser Befehl wird häufig in Schleifen mit abzählbaren Wiederholungen
verwendet. Sollte sich beim Dekrementieren der Wert Null ergeben, dann wird das
Zero-Flag auf 1 gesetzt. Im folgenden Kapitel über Sprungbefehle wird es für
diesen Fall ein Code-Beispiel geben.
Wenn der Operand bereits den niedrigsten im Wertebereich erreichbaren Wert
besitzt und dann inkrementiert wird, dann wird als Reaktion das Overflow-Flag
gesetzt und der Operand erhält den Wert am oberen Ende des Wertebereichs, bei
einem Wertebereich von 0..255 (bei Byte-Operanden) also die 255.
Der Befehl dec
beeinflußt nicht wie der Befehl sub
das
Carry-Flag. Wenn das ein Ziel sein sollte, dann müssen Sie tatsächlich mit dem
Befehl sub
mit einer 1 als zweiten Operanden arbeiten.
Die Sprungbefehle werden unterschieden in bedingte und unbedingte Sprünge.
Beiden gemeinsam ist, daß sie ein Sprungziel benötigen. Dieses ist hinter dem
eigentlichen Sprungbefehl anzugeben und sieht folgendermaßen aus:
Sprungzielname:
Man beachte den Doppelpunkt hinter dem Namen.
Das Sprungziel wird auch als Sprungmarke bezeichnet.
Man steht in jedem längeren Programm vor dem Fall, daß man abhängig von einer bestimmten Situation eine bestimmte Handlung auszuführen hat.
Egal in welcher Programmiersprache wir uns bewegen, Entscheidungen werden erst nach einem vorherigen Vergleich getroffen. Egal, ob wir mit IF..THEN..ELSE, WHILE oder FOR arbeiten, immer wird etwas verglichen und abhängig von diesem Vergleich eine Handlung ausgeführt.
Programmcode wird normalerweise linear, so wie er im Hauptspeicher steht, ausgeführt. Bei einer Entscheidung gabelt sich dieser Weg jedoch in mindestens zwei Wege. Um einen dieser Wege zu erreichen, ist ein Sprung zu diesem nötig. Um herauszufinden, zu welchem Weg das Programm springen muß, muß ein Vergleich durchgeführt werden. In fast allen Fällen wird zur Auswertung des Vergleiches das Flagregister verwendet, in zwei anderen Fällen das Register CX bzw. ECX.
Den bedingten Sprungbefehlen geht in jedem Falle ein Befehl voraus, der die
Flags bzw. (E)CX verändert. Die bedingten Sprungbefehle werten nun die Register
aus und springen dann an eine bestimmte Stelle im Code (Sprungmarke).
In der folgenden Tabelle sind einige Sprungbefehle aufgeführt.
Befehl | Bedeutung |
---|---|
ja | jump if above (ohne Vorzeichen) |
jae | jump if above or equal |
jb | jump if below (ohne Vorzeichen) |
jbe | jump if below or equal |
jc | jump if Carry Flag = 1 |
jcxz | jump if CX = 0 |
jecxz | jump if ECX = 0 |
je | jump if equal |
jg | jump if greater (mit Vorzeichen) |
jge | jump if greater or equal (mit Vorzeichen) |
jl | jump if lower (mit Vorzeichen) |
jle | jump if lower or equal (mit Vorzeichen) |
jna | jump if not above |
jnae | jump if not above or equal |
jnb | jump if not below |
jnbe | jump if not below or equal |
jnc | jump if not Carry = 1 |
jne | jump if not equal |
jng | jump if not greater |
jnge | jump if not greater or equal |
jnl | jump if not lower |
jnle | jump if not lower or equal |
jno | jump if not Overflow = 1 |
jnp | jump if not Parity = 1 |
jns | jump if not Sign = 1 |
jnz | jump if not Zero = 1 |
jo | jump if Overflow Flag = 1 |
jp | jump if Parity Flag = 1 |
jpe | jump if parity even (Parity = 1) |
jpo | jump if paritiy odd (Parity = 0) |
js | jump if Sign = 1 |
jz | jump if Zero Flag = 1 |
In einem Programm würde sich ein bedingter Sprung zum Beispiel folgendermaßen
realisieren lassen:
... mov cx,1 dec cx jz CXIstNull ... CXIstNull: ... ;Die Einrückung dient nur der Lesbarkeit ... ... |
Dieses zugegebenermaßen recht sinnlose Programmfragment lädt in das Register CX
den Wert 1 und dekrementiert dieses dann. Da dann in CX der Wert 0 (Null) steht,
wird durch den Befehl DEC
das Zero-Flag im Flag-Register auf 1
gesetzt. Der nachfolgende Befehl JZ
überprüft nun, ob denn im
Flag-Register das Zero-Flag auf 1 gesetzt ist und führt, sollte dies der Fall
sein, einen Sprung an die durch die Sprungmarke CXIstNull gekennzeichnete Stelle
durch.
Wenn Sie die obige Tabelle eingehend durchgeschaut haben, dann wird Ihnen
vielleicht der Befehl JCXZ
aufgefallen sein. Diesen hätten Sie in
diesem speziellen Fall anstelle des Befehls JZ
verwenden können.
Hätten wir allerdings mit dem Register BX statt CX gearbeitet, dann bliebe Ihnen
nur ein Code wie der obige.
Mit den bedingten Sprungbefehlen lassen sich Konstrukte im Stile einer
FOR-Schleife aufbauen:
mov cx,400 LoopStart: ... ;Code, der eine Handlung ausführt dec cx jnz LoopStart ... |
In das Register CX wird erst der Wert 400 geladen. Dann führt der Prozessor den
Code aus, der hinter der Sprungmarke steht. Gehen wir einfach davon aus, daß
dieser Code die Zahl, die in Register CX steht, auf dem Bildschirm ausgibt, im
ersten Durchlauf also 400.
Nachdem dies erledigt ist, wird das Register CX um 1 dekrementiert. Beim ersten
Durchlauf steht jetzt also 399 in CX. Danach wird geprüft, ob das Zero-Flag
gesetzt ist. Da das vorherige Dekrementieren von CX nicht Null ergab, das
Zero-Flag also nicht auf 1 gesetzt wurde, bringt uns JNZ
(Jump if
Not Zero) wieder zur Sprungmarke LoopStart
. Ab da wird dann wieder
der Inhalt von CX ausgegeben (noch immer 399), dekrementiert (jetzt ist CX=398),
das Zero-Flag ausgewertet und gesprungen. Das geht solange, bis in CX nur noch
eine 1 steht, diese dekrementiert wird, dadurch das Zero-Flag gesetzt wird, die
Sprungbedingung nicht mehr zutrifft und schlußendlich die Schleife verlassen
wird.
Befindet sich die Laufvariable wie eben im Register CX, dann hält der Prozessor
noch den Befehl LOOP bereit. Mit diesem Befehl würde unser Programm
folgendermaßen aussehen:
mov cx,400 LoopStart: ... ;Code, der eine Handlung ausführt loop LoopStart ... |
LOOP
dekrementiert selbständig das Register CX und springt, falls
es nicht Null enthält, zu der Sprungmarke.
Die Verwendung von LOOP
soll sich bis zum 80286 noch lohnen, in
neueren Prozessoren ist die Kombination DEC...JXX
schneller.
Ein unbedingter Sprung wertet keine Flags oder andere Register aus, sondern
springt sofort an die durch die Sprungmarke bezeichnete Stelle:
... mov cx,1 jmp EinfachSo ... EinfachSo: ... ... ... |
Dem JMP
-Befehl ist es total egal, was in CX oder im Flag-Register
steht, er springt einfach nur an die Sprungmarke EinfachSo.
Die bedingten Sprünge haben eine Reichweite von 128 Byte, es kann also maximal
128 Bytes vor- oder zurückgesprungen werden. Diese Einschränkung
gilt jedoch nur bei Prozessoren, die älter als der 80386 sind. Ab dem 80386
haben die bedingten Sprünge eine Reichweite von 32 KByte. Sollten Sie für
einen Prozessor ab dem 80386 programmieren, dann müssen Sie allerdings mittels
der geeigneten Direktive die Codeerzeugung für diesen Prozessor einschalten,
also beispielsweise mit P386N
.
Im Gegensatz dazu kann der unbedingte Sprung mit jmp
auf jedem
Prozessor auch Segmentgrenzen überwinden, kurz gesagt also weiter als 64 KByte
springen.
Unterhalb des 80386 müssen Sie sich eines kleinen Umweges bedienen, um größere
(bedingte) Sprünge zu realisieren.
Das folgende Beispiel zeigt zuerst eine Schleifenkonstruktion, die einen Fehler
hervorruft und danach die korrigierte Version.
;es gilt die Annahme, daß kein 80386 oder höher eingestellt ist ... EntferntesZiel: ... ;Code mit einer Größe von ... ;mehr als 128 Byte dec cx jnz EntferntesZiel ;falls CX <> 0 dann neuer Durchlauf ... |
;gleiche Annahme wie oben ... EntferntesZiel: ... ;Code mit einer Größe von ... ;mehr als 128 Byte dec cx jz Ende ;falls CX=0 dann Sprung zum Schleifenende jmp EntferntesZiel ;ansonsten neuer Durchlauf Ende: ... |
Zwischen der Sprungmarke EntferntesZiel
und dem bedingten Sprungbefehl
liegt Code, der mehr als 128 Bytes beansprucht. Der Sprung soll durchgeführt werden,
wenn nach dem dekrementieren von CX ein anderer Wert als Null in diesem Register
steht. Wenn TASM beim Kompilieren zu diesem Befehl kommt, dann gibt er den
Fehler "Relative jump out of range by .. bytes", wobei die beiden
Punkte durch einen entsprechenden Zahlenwert ersetzt werden.
Bei der korrigierten Variante fallen zwei Dinge auf: es gibt einen weiteren
Befehl und es gibt eine weitere Sprungmarke.
Wieder wird das Register CX dekrementiert. Der nachfolgende Befehl prüft aber
diesmal, ob nach dem Dekrementieren der Wert Null in CX steht. Sollte dies der
Fall sein, dann wird die Schleife mit einem Sprung zur Sprungmarke Ende
verlassen. Sollte dies nicht der Fall sein, dann wird einfach beim nächsten
Befehl weitergemacht, der ohne Umschweife zum Schleifenanfang (EntferntesZiel)
springt.
Um es dem Programmierer zu ersparen, über Sprungweiten nachzugrübeln, existiert die Assemblerdirektive JUMPS, die automatisch Code erzeugt, wenn Sprungweiten überschritten werden. Das funktioniert aber nur dann in beide Richtungen, wenn mittels des Kommandozeilenschalters /m2 die Mehr-Pass-Assemblierung eingeschaltet wurde.
Sollte als Ausführungsplattform ein 80386 oder höher eingestellt sein, so ist auch die Direktive JUMPS unnötig, da ab dem 80386 innerhalb eines Segments bedingte Sprünge mit einer Weite bis zu 64 KB durchgeführt werden können.
Ein String im Zusammenhang mit den im folgenden behandelten Befehlen kann am
ehesten als ein Array von Bytes, Words und, ab dem 80386, auch DWords sein.
Natürlich können auch mit Prozessoren vor dem 80386 DWord-Strings bearbeitet
werden, aber erst ab dem 80386 gibt es dafür spezielle Befehle.
Verwechseln Sie diese Art von Strings nicht mit denen, die Sie vielleicht von
Pascal oder C her kennen.
lodsb lodsw lodsd ;ab 80386 |
Diese Befehle greifen alle auf die Adresse zu, die durch die Register DS:SI
bestimmt wird. Abhängig von der Endung des Befehls (b, w oder d) wird von
dieser Adresse ein Byte (b), Word (w) oder ein DWord (d) geladen und in das
Register Al, AX oder EAX geschrieben.
Ist das Direction Flag im Flagregister gelöscht, dann wird gleichzeitig zum
Inhalt des Registers SI die Operandengröße addiert, also entweder 1, 2 oder 4.
In einem String würde das Register damit auf das nächste Element zeigen. Ist
das Direction Flag gesetzt, dann wird von SI die Operandengröße abgezogen.
Es gibt zwei Befehle, die das Direction Flag explizit löschen oder setzen,
nämlich cld und std
Dieser Befehl wird hauptsächlich in Schleifen verwendet, um ein Stringelement herauszugreifen, zu bearbeiten und dann mit dem nächsten fortzufahren.
stosb stosw stosd ;ab 80386 |
Diese Befehle schreiben den Inhalt des Registers AL (bei Endung b), AX (Endung
w) oder EAX (Endung d) an die Adresse, die durch ES:DI bestimmt wird.
Ist das Direction Flag gelöscht, dann wird zu DI wieder die Operandengröße
addiert, ist es gesetzt, wird die Operandengröße abgezogen.
Durch Kombination von lods
und stos
können Sie einen
String in einer Schleife elementweise bearbeiten. Voraussetzung ist, daß sowohl
DS:SI als auch ES:DI auf die gleiche Adresse verweisen.
Möglicher Code dafür könnte folgendermaßen aussehen.
TITLE Ein ganz einfacher Codieralgorithmus IDEAL ;IDEAL-Mode einschalten MODEL SMALL ;Speichermodell SMALL STACK 100 ;Stack reservieren DATASEG TextToEncode db "Diese Text soll codiert werden$",13,10 len = $ - TextToEncode - 3 ;Länge des Strings - 3 als Konstante CODESEG start: STARTUPCODE mov ah,09h ;Funktionsnummer für Textausgabe mov dx,OFFSET StringToEncode ;DX mit Adresse des Strings laden int 21h ;String auf Bildschirm ausgeben mov ax,ds ;DS und ES müssen gleichen Inhalt haben mov es,ax ;DS und ES zeigen aufs gleiche Segment mov si,dx ;SI mit Adresse des Strings laden mov di,dx ;SI und DI haben gleichen Inhalt mov cx,len ;nach CX die Stringlänge cld ;Direction Flag löschen encodeLoop: lodsb ;lädt ein Zeichen, erhöht SI xor al,45 ;das Zeichen codieren stosb ;Zeichen in String schreiben dec cx ;Schleifenzähler dekrementieren jnz encodeLoop ;falls nicht Null dann neuer Durchlauf mov ah,09h ;Funktionsnummer mov dx,OFFSET StringToEncode ;DX mit Adresse des Strings laden int 21h ;String erneut ausgeben, diemal codiert EXITCODE END start |
Gehen wir dieses Programm Schritt für Schritt durch.
Zuerst kommen die üblichen Einstellungen für Speichermodell und
Programmiermodus sowie die Stackgröße. Ganz am Anfang steht eine
identifizierende Kurzbeschreibung hinter TITLE. Auf eine Einstellung für den
Prozessor habe ich verzichtet, da nur Code verwendet wird, der auch auf einem
8086 lauffähig ist.
Im Datensegment, das mit DATASEG eröffnet wird, definieren wir als erstes
unseren String. Ich habe mich hier tatsächlich für eine Zeichenkette
entschieden, da das Ergebnis deutlich aufgezeigt werden kann. Ich hätte mich
auch für eine Kette von Zahlen im Word-Format entscheiden können.
Dieser String endet mit einem Dollarzeichen und den ASCII-Codes für Carriage
Return und Line Feed. Das Dollarzeichen ist als Endemarkierung für die später
verwendete DOS-Funktion nötig, die beiden letzen Zeichen für den Zeilenumbruch
bei der Stringausgabe.
In der nächsten Zeile wird die Länge der Zeichenkette ermittelt. Dazu wird der
sogenannte Location Counter verwendet, der im Kapitel über
Strukturierte Programmierung ein eigenes
Unterkapitel einnimmt.
Da die letzten drei Zeichen nicht verändert werden sollen (wir brauchen sie für
die Ausgabe), werden abschließend noch drei Bytes von der ermittelten Länge
abgezogen.
Im Codesegment wird der Programmeintrittspunkt definiert (start:) und dann
mittels STARTUPCODE der korrekte Startcode eingefügt.
Als erstes wird der noch unveränderte String mittels einer von DOS
bereitgestellten Funktion auf dem Bildschirm ausgegeben. Dazu wird in AH die
Funktionsnummer geladen (09h), in DX die Adresse des ersten Bytes des Strings
hinterlegt (OFFSET StringToEncode) und schließlich der DOS-Interrupt aufgerufen
(int 21h).
Da DS:SI und ES:DI auf die gleiche Adresse verweisen müssen, wird zuerst der
Inhalt des Registers DS (dessen Wert wird durch STARTUPCODE eingestellt) nach
AX kopiert und von dort ins Register ES geschrieben. Sie erinnern sich:
Segmentregister können nicht direkt beschrieben werden, wenn die Quelle auch
ein Segmentregister ist; daher der Umweg.
Dann müssen noch die beiden Index-Register geladen werden. Da in DX noch die
Offsetadresse des Strings steht (von der vorherigen Ausgabe), kann dieser Wert
gleich in die Register SI und DI kopiert werden.
Nach Abschluß dieser Arbeiten wird noch die ermittelte Stringlänge abzüglich
der letzten drei Zeichen nach CX geschrieben und das Direction Flag explizit
gelöscht (üblicherweise ist dieses Flag immer gelöscht, aber man weiß ja
nie).
Da nun alle Register gesetzt sind, wird in einer Schleife in sequentieller
Reihenfolge ein Zeichen aus dem String ins Register AL kopiert, dort mittels
xor mit dem Wert 45 verknüpft und dann wieder an seine alte
Position zurückgeschrieben. Der vorherige Wert wird durch diese Maßnahmen
überschrieben. Dann wird das CX, das hier als Schleifenzähler fungiert,
dekrementiert und anschließend geprüft, ob diese Dekrementierung das Ergebnis
Null hatte. Sollte dies nicht der Fall sein, wird an die durch encodingLoop
bezeichnete Stelle (resp. Adresse) zurückgesprungen und die Codierungssequenz
erneut durchlaufen. Beachten Sie dabei, daß durch die automatische Erhöhung
der Register SI und DI diese schon auf die nächsten Elemente des Strings
zeigen.
Ist der String codiert, wird er mit der bekannten Funktion auf dem Bildschirm
ausgegeben.
movsb movsw movsd ;ab 80386 |
MOVS
ermöglicht den Speicher-zu-Speicher-Transfer von Daten. Dabei
wird das Byte, Word oder DWord an der Adresse DS:SI in die Speicherstelle an
der Adresse ES:DI kopiert.
Abhängig vom Direction Flag werden die Index-Register danach um die
Operandengröße erhöht (DF = 0) oder verkleinert (DF = 1).
scasb scasw scasd ;ab 80386 |
SCAS
vergleicht den Inhalt des Registers AL (scasb
),
AX scasw
oder EAX scasd
mit dem Inhalt der
Speicherstelle an der Adresse ES:DI und setzt danach entsprechend dem Befehl
cmp
das Flagregister. Abhängig von der Operandengröße und dem
Direction Flag wird ebenfalls das Register DI in bekannter Weise verändert.
Diese Operation eignet sich sehr gut, innerhalb einer Schleife in einem String
einen bestimmten Wert aufzufinden.
cmpsb cmpsw cmpsd ;ab 80386 |
CMPS
vergleicht die Inhalte der Speicherstellen DS:SI und ES:DI
miteinander und setzt dann entsprechend dem Befehl cmp
das
Flagregister. Abhängig von der Operandengröße und dem Direction Flag werden
ebenfalls die Register SI und DI in bekannter Weise verändert.
ins{b,w,d} |
INS
arbeitet implizit mit dem Register DX, in dem die Portnummer
erwartet wird. Von diesem Port holt der Befehl ein Byte, Word oder DWord (je
nachdem, ob der letzte Buchstabe ein b,w oder d ist). Dieses Byte, Word, Dword
wird an die Speicherstelle geschrieben, auf die ES:DI zeigt. Ist das
Direction-Flag gelöscht, dann wird danach das Register DI um die Operandengröße
erhöht, im anderen Fall um die Operandengröße verringert.
outs{b,w,d} |
ins
. Je
nach Suffix wird ein Byte, Word oder DWord an den Port im Register DX gesendet.
Die Daten werden von der Adresse DS:SI geholt. Abhängig vom Direction-Flag wird
dann SI erhöht oder verringert.
rep ins{b,w,d} rep outs{b,w,d} rep lods{b,w,d} rep stos{b,w,d} rep movs{b,w,d} |
Bei REP
handelt es sich um ein sogenanntes Befehlspräfix, das den
angegebenen Stringbefehlen vorangestellt werden kann.
Implizit wird bei REP
und seinen Varianten immer das Register (E)CX verwendet.
Der Stringbefehl wird sooft wiederholt, wie in (E)CX angegeben. Um Speicherblöcke zu
kopieren, ist ein rep movs{b,w,d}
eine sehr schnelle Möglichkeit. Um einen
Speicherblock mit einem Wert zu versehen, kann rep stos{b,w,d}
verwendet
werden.
Zugegebenermaßen ist die Verwendung von REP in Verbindung mit LODS nicht sonderlich
sinnvoll, da das Stringelement (E)CX-mal ins Register EAX/AX/AL geschaufelt wird, man aber
keine Möglichkeit erhält, diese Zeichen anderweitig zu bearbeiten.
Wie bei den normalen Stringbefehlen wirkt sich auch hier der Wert des Direction-Flags
aus.
Bei rep outs{b,w,d}
ist zu beachten, dass die Hardware am entsprechenden
Port die Daten vielleicht nicht so schnell verarbeiten kann wie sie hereinkommen.
Die Verwendung von REP
mit anderen Befehlen führt zu undefinierten
Ergebnissen.
repe cmps{b,w,d} repe scas{b,w,d} |
Der angegebene Stringbefehl wird so lange ausgeführt, wie das Register CX ungleich Null
sowie das Zero-Flag gesetzt ist. Damit kann im String nach Elementen gesucht werden, die
mit dem Referenzwert nicht übereinstimmen (wenn ein Vergleich ergibt, dass die beiden
Operanden identisch sind, dann wird das Zero-Flag gesetzt, ansonsten gelöscht).
Bei REPZ
handelt es sich um ein Synonym für REPE
.
repne cmps{b,w,d} repne scas{b,w,d} |
Hier wird der String-Befehl solange ausgeführt, wie das Register CX ungleich Null sowie
das Zero-Flag gelöscht ist. Auf diese Weise wird das erste Stringelement gefunden, das
mit dem Referenzwert übereinstimmt.
Auch hier ist REPNZ
ein Synonym für REPNE
.
and Ziel, Quelle ;allgemeine Form and reg, reg ;mögliche Operandenkombinationen and mem, reg and mem, reg and reg, con and mem, con and al/ax/eax,con |
Dieser Befehl führt eine AND-Verknüpfung zwischen den Bits der beiden Operanden durch. Die Operandengröße kann 8, 16 oder 32 Bit betragen, muß aber bei beiden Operanden gleich groß sein. Das Ergebnis der Verknüpfung wird im ersten Operanden hinterlegt.
Eine AND-Verknüpfung verläuft nach folgendem Schema: Werden zwei Bits miteinander verknüpft, dann ist das resultierende Bit nur dann gesetzt, wenn beide Bits gesetzt waren. Ist eines oder sind beide Bits gelöscht, dann ist auch das Ergebnisbit gelöscht.
10010110 11110000 10101010 10011101 and 01011011 and 00001111 and 01010101 and 10001111 ============ ============ ============ ============ 00010010 00000000 00000000 10001101 |
or Ziel, Quelle ;allgemeine Form |
10010110 11110000 10101010 10011101 or 01011011 or 00001111 or 01010101 or 10001111 =========== =========== =========== =========== 11011111 11111111 11111111 10011111 |
OR
wird häufig verwendet, um bestimmte Bits zu setzen. Wenn Sie
sich eine ASCII-Tabelle zur Hand nehmen, dann können Sie nicht nur erkennen,
dass die Großbuchstaben kleinere ASCII-Codes als die Kleinbuchstaben haben,
sondern auch, daß die Differenz zwischen ASCII-Codes korrespondierender Klein-
und Großbuchstaben genau 32 beträgt. Um nun einen Großbuchstaben in einen
Kleinbuchstaben zu konvertieren, kann folgender Code verwendet werden:
mov al,[char] ;Zeichen nach AL cmp al,65 ;ist der Buchstabe kleiner als 'A' jb NoChange ;ja --> keine Umwandlung cmp al,90 ;ist der Buchstabe größer als 'Z' ja NoChange ;ja --> keine Umwandlung or al,32 ;den Buchstaben hier auf klein stellen NoChange: ;Sprungmarke, falls keine Umwandlung ... |
Dieser Code berücksichtigt noch keine Sonderzeichen wie z. B. Umlaute.
Das zu bearbeitende Zeichen befindet sich in der Variablen char, die ins
Register AL kopiert wird. Danach wird geprüft, ob sich das Zeichen überhaupt im
Bereich der Großbuchstaben befindet. Sollte dies der Fall sein, dann wird durch
eine OR-Verknüpfung zwischen AL und dem Wert 32 das Bit 5 in AL gesetzt. Dies
ist die schon besprochene Differenz zwischen Groß- und Kleinbuchstaben.
Die obige Konvertierung hätte auch komplett mit der Variablen char durchgeführt
werden können (ohne Einbeziehung von AL); die Entscheidung für die Verwendung
von AL fiel aus Gründen der Geschwindigkeit: der Prozessor kann schneller auf
Werte in seinen Registern als auf den Speicher zugreifen. Das mag bei einem
Zeichen nicht viel ausmachen, aber wenn man diese Befehlsfolge verwendet, um
einen längeren Text in Kleinbuchstaben zu konvertieren, dann könnte es schon zu
einer meßbaren Verlangsamung kommen.
xor Ziel, Quelle ;allgemeine Form |
10010110 11110000 10101010 10011101 xor 01011011 xor 00001111 xor 01010101 xor 10001111 ============ ============ ============ ============ 11001101 11111111 11111111 00010010 |
XOR
wird häufig verwendet, um Bits zwischen zwei Zuständen
umzuschalten. Der Vorteil hierbei ist, dass man den aktuellen Zustand der Bits
nicht kennen muß, die Umschaltung erfolgt auf jeden Fall. Nehmen wir an, daß
die Zustände true and false durch ein Bit repräsentiert werden, wobei true=1
(gesetzt) und false=0 (gelöscht) bedeuten. Für die Umschaltung zwischen true
und false kann man dieses Bit einer XOR-Verknüpfung mit 1 unterziehen:
Fall 1: Wert ist true (Bit=1) xor [Wert],1 ;Ergebnis (Wert) ist 0 Fall 2: Wert ist false (Bit=0) xor [Wert],1 ;Ergebnis (Wert) ist 1
Da die Ergebnisse in den ersten Operanden geschrieben werden, ist Wert nach Ausführung der Befehle umgeschaltet. Die XOR-Verknüpfung mit Null wäre in diesem Fall nur wirksam, wenn Wert=true ist (1 xor 0 = 1). Im anderen Fall gilt: 0 xor 0 = 0 (keine Umschaltung).
Eine Besonderheit der XOR-Verknüpfung, die auch im letzten Beispiel ersichtlich wurde, besteht darin, daß bei einer Verknüpfung zweier Werte die Verknüpfung des Ergebnisses mit dem 2. Wert den ersten Wert ergibt. Ein Beispiel soll dies verdeutlichen:
Wert 1: 10010110b Wert 2: 01011011b 1. Verknüpfung 10010110 xor 01011011 ============ 11001101 ;Ergebnis der 1. Verknüpung 2. Verknüpfung zw. Erg. und 2. Wert 11001101 ;Ergebnis der 1. Verknüpung xor 01011011 ;2. Wert ============ 10010110 ;Ergebnis = 1.Wert
not Ziel ;allgemeine Form not reg ;mögliche Operanden not mem |
not 10010101=01101010 not 11110000=00001111 not 10101010=01010101 |
shl Ziel, Anzahl ;shift left shl reg,CL shl reg,con8 shl mem,CL shl mem,con8 |
Für die folgenden Bitschiebebefehle gilt die Annahme, dass das höchstwertige Bit am weitesten links in einem Byte/Word etc steht und die Zählung der Bits mit Null beginnt. Dementsprechend steht in einem Byte das höchstwertige Bit an Position 7, das Bit mit dem geringsten Wert an Position 0.
Beim Befehl SHL
werden aus dem ersten Operanden so viele Bits nach links
herausgeschoben, wie im zweiten Operanden angegeben. Am rechten Ende werden ebenso viele
Bits nachgeschoben, die alle nicht gesetzt sind. Das zuletzt herausgeschobene Bit läßt sich im Carry-Flag finden.
mov dx,0000010011101010b ;1285d ins Register DX schreiben shl dx, 3 ;3 Bits nach links herausschieben |
shr Ziel, Anzahl ;shift right shr reg,CL shr reg,con8 shr mem,CL shr mem,con8 |
SHL
, nur werden hier die Bits nach rechts geschoben.
sal Ziel, Anzahl ;shift arithmetic left sal reg,CL sal reg,con8 sal mem,CL sal mem,con8 |
SHL
.
sar Ziel, Anzahl ;shift arithmetic right sar reg,CL sar reg,con8 sar mem,CL sar mem,con8 |
shld Ziel, Quelle, Anzahl ;shift left with double precision shld reg16,reg16,con8 shld reg32,reg32,con8 shld mem16,reg16,con8 shld mem32,reg32,con8 shld reg16,reg16,CL shld reg32,reg32,CL shld mem16,reg16,CL shld mem32,reg32,CL |
SHLD
schiebt die Bits in Ziel um die angegebene Anzahl nach links und fügt an den freiwerdenden Positionen die höchstwertigsten Bits von Quelle ein.
shrd Ziel, Quelle, Anzahl ;shift right with double precision shrd reg16,reg16,con8 shrd reg32,reg32,con8 shrd mem16,reg16,con8 shrd mem32,reg32,con8 shrd reg16,reg16,CL shrd reg32,reg32,CL shrd mem16,reg16,CL shrd mem32,reg32,CL |
SHRD
schiebt die Bits in Ziel um die angegebene Anzahl nach rechts und fügt an den freiwerdenden Positionen die geringwertigsten Bits von Quelle ein.
Es werden nur die unteren 5 Bits von Anzahl verwendet (max 32). Erst ab 80386.
rcl Ziel, Anzahl ;rotate through carry left rcl reg,CL rcl reg,con8 rcl mem,CL rcl mem,con8 |
rcr Ziel, Anzahl ;rotate through carry right rcr reg,CL rcr reg,con8 rcr mem,CL rcr mem,con8 |
rol Ziel, Anzahl ;rotate left rol reg,CL rol reg,con8 rol mem,CL rol mem,con8 |
RCL
, nur das die Bits hier nicht den Weg durchs Carry-Flag nehmen, sondern sofort am rechten Ende wieder eingefügt werden.
Das zuletzt herausgeschobene Bit befindet sich im Carry-Flag.
ror Ziel, Anzahl ;rotate right ror reg,CL ror reg,con ror mem,CL ror mem,con |
RCR
, nur das die Bits hier nicht den Weg durchs Carry-Flag nehmen, sondern sofort am rechten Ende wieder eingefügt werden.
Das zuletzt herausgeschobene Bit befindet sich im Carry-Flag.
test Ziel, Quelle |
bt Quelle, Index ;Bit test |
Der Index im 2. Operanden zeigt auf ein Bit im ersten Operanden. Der Wert
dieses Bits wird im Carry-Flag gespeichert.
Der Befehl existiert seit dem 80386.
btc Quelle, Index ;Bit test and complement |
Der Index im 2. Operanden zeigt auf ein Bit im ersten Operanden. Der Wert
dieses Bits wird im Carry-Flag gespeichert. Das Bit, auf das der Index
verweist, wird komplementiert, also umgekehrt.
Der Befehl existiert seit dem 80386.
btr Quelle, Index ;Bit test and reset |
Der Index im 2. Operanden zeigt auf ein Bit im ersten Operanden. Der Wert
dieses Bits wird im Carry-Flag gespeichert. Das Bit, auf das der Index
verweist, wird gelöscht.
Der Befehl existiert seit dem 80386.
bts Quelle, Index ;Bit test and set |
Der Index im 2. Operanden zeigt auf ein Bit im ersten Operanden. Der Wert
dieses Bits wird im Carry-Flag gespeichert. Das Bit, auf das der Index
verweist, wird gesetzt.
Der Befehl existiert seit dem 80386.
bsf Ziel, Quelle ;Bit scan forward bsf reg, mem |
Der Befehl testet die Bits im zweiten Operanden von Bit 0 beginnend durch ob
sie gesetzt sind. Sind alle Bits gelöscht (0), dann wird das Zero-Flag gelöscht,
anderenfalls wird das Zero-Flag gesetzt und in den ersten Operanden wird der
Index des ersten gesetzten Bits geschrieben.
Beide Operanden müssen die gleiche Größe haben, die 16 oder 32 Bit betragen
kann. Der Befehl existiert seit dem 80386.
bsr Ziel, Quelle ;Bit scan reverse bsr reg, mem |
Worum es sich beim Stack handelt, wurde bereits besprochen. Die Prozessoren der x86-Familie stellen einige Befehle zur Verfügung, mit denen speziell der Stack bearbeitet wird.
Wie Sie wissen, ist der Stack immer im Stacksegment gelagert (das Segment, dessen Adresse im Register SS steht). Das bedeutet, daß alle Stackbefehle implizit auf das Register SS sowie auf das Register SP zugreifen. Im Register SP steht die Adresse der aktuellen Stackspitze (TOS = Top Of Stack).
Bei allen nachfolgenden Befehlen handelt es sich um 16- bzw. 32-Bit-Operationen. Es ist mit diesen Befehlen nicht möglich, Bytes auf den Stack zu kopieren oder von dort zu holen. Es ist natürlich grundsätzlich möglich, auch mit Bytes auf dem Stack zu arbeiten, allerdings kann man sich dabei mehr Probleme einhandeln als einem lieb ist.
Die folgenden Befehle kopieren einen Wert auf den Stack. Die Konsequenz aus
diesem Kopiervorgang besteht u. a. darin, daß das Register SP abhängig von der
Operandengröße verändert wird.
Bei 16-Bit-Operanden wird vom Inhalt des Registers SP der Wert 2 abgezogen, bei
32-Bit-Operanden der Wert 4. Über die abzuziehenden Werte muß nicht weiter
gesprochen werden, es handelt sich um die Größe der Operanden in Bytes. Was
vielleicht mehr Verwirrung hervorruft ist die Tatsache, daß die Werte abgezogen
und nicht addiert werden, zumal die Redewendung "auf den Stack
kopieren/pushen/schieben" berechtigterweise eine Addition vermuten läßt.
Allerdings dürfen Sie nicht vergessen, daß der Stack von oben nach unten
wächst, also in Richtung kleiner werdender Adressen. Und da SP die Adresse
der Stackspitze enthält, muß diese Adresse nach dem kopieren korrigiert werden.
Dieser Service wird vom Befehl push
gleich mit erledigt.
push reg16 push reg32 push mem16 push mem32 push con16 ;direktes Kopieren erst ab 80286 push con32 |
Die OpCodes für die Befehle ermittelt der Assemblierer in aller Regel selbst und mit großer Treffsicherheit. Wird allerdings mit Speicherwerten gearbeitet, dann kann bei bestimmten Adressierungsarten ein Type-Casting nötig werden, das dem Assemblierer den Datentyp mitteilt. Bei Verwendung von Konstanten hängt die Wahl des OpCodes von der Größe der Konstanten ab. Wird also beispielsweise eine Zahl größer als 65536 gepusht, dann wird automatisch der OpCode für 32-Bit-Konstanten verwendet.
Die beiden verbleibenden Befehle pushw
und pushd
sind
Alternativen zum Type-Casting. Die Befehle teilen dem Assemblierer direkt mit,
welche Operandengröße verwendet werden soll. Es wäre zum Beispiel möglich, daß
Sie den Wert 400 auf den Stack kopieren wollen. Mit einem Befehl wie
push 400
wird automatisch der 16-Bit-OpCode verwendet und es gilt:
SP := SP - 2. Allerdings ist es möglich, daß eine nachfolgende Operation, z. B.
ein Unterprogramm, ein DWord auf dem Stack erwartet und aus genau diesem Grunde
ein DWord vom Stack holt (s. nächster Abschnitt über den Befehl
pop
). Damit würde die gesamte bisherige Struktur des Stack
durcheinandergebracht werden. Da auch die nachfolgenden Befehle, die Werte vom
Stack holen, mit falschen Resultaten arbeiten, kann ein solcher Vorgang auch
zum Systemabsturz führen.
push 400 ;kopiert 16-Bit-Wert pushd 400 ;erweitert den Wert auf 32 Bit pushw [bx] |
Der letzte Befehl bedarf vielleicht einer Erklärung, die ich Ihnen nicht
vorenthalten möchte.
Es handelt sich um die sogenannte indirekte Adressierung. Hierbei wird nicht
direkt auf eine Adresse zugegriffen, sondern der Umweg über ein Register
gegangen, dessen Inhalt als Adresse interpretiert wird. Mit der letzten
Anweisung wird auf einen Wert zugegriffen, dessen Offsetadresse durch den Wert
im Register BX repräsentiert wird. Als Segmentadresse wird implizit der Wert im
Register DS angenommen.
Zur Verdeutlichung:
Mit dem Befehl push bx
würde der Inhalt des Registers BX auf den
Stack kopiert werden. Mit dem Befehl pushw [bx]
wird der Inhalt
der Speicherstelle kopiert, auf die die Adresse im Register BX verweist.
Da pushw
verwendet wurde, wird dieser Wert im Word-Format (16 Bit)
auf den Stack gelegt.
TASM ermöglicht es Ihnen, mit nur einem Befehl mehrere Operanden zu pushen, vorausgesetzt sie haben die gleiche Größe.
statt: push ax push bx push si push di lieber: push ax bx si di ;durch Leerzeichen getrennt |
Dieser Befehl ist die Umkehrung des Befehls push
. Mit ihm werden
Werte vom Stack geholt und in ein als Operanden zu übergebendes Ziel
geschrieben.
Abhängig von der Operandengröße wird wieder das Register SP verändert. Bei Word-Operanden gilt SP := SP + 2, bei DWord-Operanden gilt SP := SP + 4.
pop reg16 ;außer CS pop reg32 pop mem16 pop mem32 |
Werte vom Stack in das Register CS zu kopieren, hätte starke Auswirkungen auf den weiteren Programmverlauf. Da CS die Adresse des Codesegments enthält und über die Registerkombination CS:IP die Adresse des nächsten auszuführenden Befehls festgelegt wird, würde nach dem Kopieren eines Wertes vom Stack ins Register CS an einer u. U. komplett anderen Stelle des Speichers nach Befehlen gesucht werden bzw. würden an anderer Stelle, die durch CS:IP referenziert wird, Befehle ausgeführt werden, die den Rechner mit hoher Wahrscheinlichkeit abstürzen lassen. Riskieren Sie also möglichst nicht, durch übermütiges poppen der Werte Ihren Rechner ins Nirvana zu schicken, selbst dann nicht, wenn Sie wissen, daß eigentlich ein korrekter Wert nach CS kopiert wird.
POPW
und popd
sind wieder Befehle, die explizit
festlegen, daß ihr Operand mit einer bestimmten Größe behandelt werden soll.
Wie bei den Befehlen push(w|d)
machen diese Befehle nur bei
bestimmten Adressierungsarten Sinn.
TASM ermöglicht es Ihnen, mit nur einem Befehl mehrere Werte vom Stack zu holen, vorausgesetzt sie haben die gleiche Größe.
statt: pop ax pop bx pop si pop di lieber: pop ax bx si di ;durch Leerzeichen getrennt |
pusha pushad |
Diese Befehle pushen die allgemeinen Register sowie die Index- und
Pointerregister in der folgenden Reihenfolge auf den Stack: AX, CX, DX, BX, SP,
BP, SI, DI. PUSHAD
führt die gleiche Operation in der gleichen
Reihenfolge für die 32-Bit-Varianten der Register durch.
popa popad |
pusha
und
pushad
. Vom Stack werden Werte entnommen und in umgekehrter
Reihenfolge von pusha
in die Register geschrieben, also in
folgender Reihenfolge: DI, SI, BP, BX, DX, CX, AX. Der Wert für SP wird zwar
vom Stack geholt, aber nicht in das Register SP geschrieben. POPAD
führt wieder die gleiche Funktion in gleicher Reihenfolge für die
32-Bit-Versionen der Register durch.
pushf pusfd |
Diese Befehle pushen entweder das 16-Bit-Flagregister (pushf
) oder
ab dem 80386 das EFlags-Register (pushfd
) auf den Stack. Von dort
muß es, um es bearbeiten zu können, mit einem pop
bzw.
popd
(bei vorherigem pushfd
) in ein Register oder
eine Variable geschrieben werden.
popf popfd |
Diese Befehle nehmen 16 bzw. 32 Bit (bei popfd
) von der
Stackspitze und schreiben sie in das Flagregister bzw ins EFlags-Register.
Sollten Sie das EFlags-Register bearbeitet haben, dann beachten Sie, daß die
Bits 16 und 17 nicht verändert werden. Sollten Sie diese Bits also manipuliert
haben, so haben die neuen Werte nach einem popfd
keine
Auswirkung.
setcc |
lahf |
Bitnummer | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Flag | Sign | Zero | nicht definiert | Auxiliary Carry | nicht definiert | Parity | nicht definiert | Carry |
sahf |
call addr |
clc ;clear carry flag |
stc ;set carry flag |
cld ;clear direction flag |
std ;set direction flag |
Setzt das Direction-Flag auf Eins. Bei den Stringbefehlen führt das dazu, daß nach Ausführung des Stringbefehls die von diesem Befehl verwendeten Indexregister um die Operandengröße verringert werden.
cli ;clear interrupt-enable flag |
Setzt das Interrupt-Enable-Flag auf Null, löscht es also. Ab jetzt werden nur noch NMIs (Non-Maskable Interrupts) zugelassen. Der Befehl wird vor allem verwendet, um in ISRs eine ungestörte Abarbeitung zu gewährleisten.
sti ;clear interrupt-enable flag |
Vorheriges Kapitel (Speichermodelle) | Nach oben | Nächstes Kapitel (Strukturiertes Programmieren) |
Zum Inhaltsverzeichnis | ||
Zur Startseite |