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] |
Erwartet der Befehl also einen Parameter, dann wird das Ergebnis der Operation meist auch in diesen Parameter geschrieben. Wird noch ein zweiter Operand erwartet, dann ist das zumeist eine Quellangabe, die abhängig vom Befehl nur in den ersten Parameter geschrieben wird oder erst über eine Berechnung mit dem ersten Parameter in diesen geschrieben wird.
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.
Der mit Abstand wichtigste Befehl hat folgende symbolische Syntax:
mov Ziel, Quelle |
Der Befehl deutet bereits recht deutlich das englische Wort move (bewegen) an,
was in diesem Fall aber nicht ganz den Tatsachen entspricht. Tatsächlich
wird der Quelloperand in Ziel kopiert, bleibt also erhalten.
In jedem Fall müssen Ziel und Quelle die gleiche Größe haben.
Als Quelle kommen Register, Speicherstellen (Variablen) und Konstanten in Frage,
als Ziel sind nur Register und Speicherstellen möglich.
Beispiele:
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 |
Ein weiterer Befehl ist in der Lage, den Inhalt seiner zwei Operanden gegeneinander auszutauschen:
xchg Operand1, Operand2 |
Die Operanden müssen die gleiche Größe haben. Es kann sich
jeweils um Register oder Speicherstellen handeln. Wie beim Befehl
mov
dürfen beide Operanden nicht zugleich Speicherstellen
sein.
Beispiele:
xchg dx,cx xchg [MyByte],dh xchg bx,[MyWord] |
Die OpCodes der dargestellten Beispiele sind mindestens 2 Bytes lang, meistens länger. Es gibt zwei Sonderformen, die kürzer sind:
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 dd 012345h CODESEG ... lds reg16,mem32 ;allg. Form am Beispiel von LDS lds ax,[MyDWord] ;konkrete Form |
Die beiden Befehle 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, Const8 ;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 Const8 (direkte Port-Angabe) 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 in 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.
Beispiel:
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".
In diesem Fall ist folgender Ausweg möglich:
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 Beispiel mov 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] |
Mit dem Wörtchen 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.
Um einen 8-Bit-Wert auf einen 32- oder 64-Bit-Wert zu erweitern, können Sie
folgende Sequenzen verwenden
;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 |
Dieser Befehl ist im Grunde eine Verallgemeinerung der Befehle cbw
und cwde
. Er steht für MOVe with Sign eXtension.
Die möglichen Erscheinungsformen sind:
movsx reg16, reg8 movsx reg16, mem8 movsx reg32, reg8 movsx reg32, mem8 movsx reg32, reg16 movsx reg32, mem16 |
Die oben genannten Befehle können Sie mit folgenden Instruktionen simulieren:
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 |
Sollten Ziel und Quelle allerdings unterschiedlichen Registern angehören,
so ist 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.
Beachten Sie, daß ein zweimaliges 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 |
Nach Ausführung des Befehls werden einige Flags gesetzt. Da der Prozessor nicht wissen kann, ob es sich um vorzeichenbehaftete Zahlen handelt oder nicht werden die Flags so gesetzt, dass sowohl bei vorzeichenbehafteter als auch vorzeichenloser Interpretation eine korrekte Beurteilung des Ergebnisses vorgenommen werden kann. Folgende Flags sind betroffen:
adc Ziel, Quelle |
ADC
macht nichts anderes als ADD
, addiert aber noch
eine Eins zum Ergebnis hinzu, wenn bei Befehlsausführung das Carry-Flag
gesetzt war.
Bei den Prozessoren vor dem 80386 musste ein LongInt aufgrund der
Registergröße in zwei 16-Bit-Registern dargestellt werden.
Hierfür sind die Registerkombinationen DX:AX und CX:BX vorgesehen. Die
Addition kann allerdings nur registerweise, also für jeweils 16 Bit
durchgeführt werden. Dazu werden erst die beiden niederwertigen Register
der Operanden miteinander addiert (mit ADD
). Sollte hier ein
Überlauf auftreten, so wird das Carry-Flag gesetzt. Anschließend
werden die beiden höherwertigen Register miteinander addiert (mit
ADC
). Durch Einbeziehung des Carry-Flags in diese Addition wird das
korrekte Ergebnis erzeugt.
Anwendungsbeispiel:
mov ax,45278 mov dx,0 mov bx,36498 mov cx,0 add ax,bx ;81776 passt nicht in AX - CF wird gesetzt ;in AX steht jetzt 16241 (81776-65535) adc dx,cx ;addieren mit Carry, DX:AX = 81776 ;weil CF=1 und CF wird zu DX dazuaddiert |
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. Die Bedeutung der
restlichen Flagstellungen ist mit denen nach ADD
identisch.
sbb Ziel, Quelle |
Ebenso wie bei ADD
gibt es auch bei SUB
eine Variante,
die das Carry-Flag in die Berechnung einbezieht. Die Verwendung läuft dann
auch analog.
Anwendungsbeispiel:
sub ax,bx sbb dx,cx |
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. Konstanten sind als Operanden nicht zugelassen.
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. Da
das/die Zielregister immer ausreichend groß sind, kann es keinen
Überlauf mehr geben.
Beispiele:
mul bl ;8-Bit-Multiplikation, Ergebnis in BX mul [MyWord] ;16-Bit-Multiplikation, Ergebnis in DX:AX mul edx ;32-Bit-Multiplikation, Ergebnis in EDX:EAX |
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 Operanden müssen die Operanden 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 reg32/mem32 | 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 |
Ist das Ergebnis von IMUL für das Ziel zu groß, dann werden das Overflow- und das Carry-Flag gesetzt, ansonsten werden beide gelöscht.
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 |
Das Ziel ist gleichzeitig die Quelle. Der Operand wird genau um den Wert 1
erhöht, entspricht also 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
add
mit einer 1 als zweiten Operanden arbeiten.
dec Ziel |
Das Ziel ist gleichzeitig die Quelle. Vom Operanden wird genau der Wert 1
abgezogen, der Befehl entspricht also 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 muss in jedem Falle ein Befehl vorausgehen, der die
Flags bzw. (E)CX verändert. Die bedingten Sprungbefehle werten nun die
Register aus (Flags bzw. ECX) 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 schlussendlich 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 bis zu
64 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 Sinne der im folgenden behandelten Befehlen kann am ehesten als
ein Array von Bytes, Words und, ab dem 80386, auch DWords betrachtet werden.
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 StringToEncode db "Dieser Text soll codiert werden",13,10,"$" len = $ - StringToEncode - 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, diesmal 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 den ASCII-Codes für Carriage Return und Line Feed
und einem Dollarzeichen. Das Dollarzeichen ist als Endemarkierung für die
später verwendete DOS-Funktion nötig, die beiden anderen 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 Register 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} |
Hier gelten die gleichen Voraussetzungen wie beim Befehl 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.
Der Ablauf bei REP Stringbefehl
lässt sich grundsätzlich
wie folgt beschreiben: zuerst wird das Register (E)CX daraufhin
überprüft, ob es den Wert Null enthält. Sollte dies der Fall
sein, so wird der Befehl nicht ausgeführt. Anschließend wird das
Register (E)CX um den Wert 1 dekrementiert. Danach werden abhägig vom
Direction Flag die Register SI und DI um die Operandengröße
inkrementiert (DF = 0) bzw. dekrementiert (DF = 1).
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 reg, mem 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 |
Dieser Befehl führt eine OR-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. Die möglichen Operandenkombinationen stimmen mit denen des Befehls AND überein.
Eine OR-Verknüpfung verläuft nach folgendem Schema: Werden zwei Bits miteinander verknüpft, dann ist das resultierende Bit nur dann gesetzt, wenn mindestens eines der beiden Bits gesetzt war. Sind beide Bits gelöscht, dann ist auch das Ergebnisbit gelöscht.
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 messbaren Verlangsamung kommen.
xor Ziel, Quelle ;allgemeine Form |
Dieser Befehl führt eine XOR-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. Die möglichen Operandenkombinationen stimmen mit denen des Befehls AND überein.
Eine XOR-Verknüpfung verläuft nach folgendem Schema: Werden zwei Bits miteinander verknüpft, dann ist das resultierende Bit nur dann gesetzt, wenn genau eines der Bits gesetzt war. Sind beide Bits gesetzt oder gelöscht, dann ist auch das Ergebnisbit gelöscht.
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üpfung 2. Verknüpfung zw. Erg. und 2. Wert 11001101 ;Ergebnis der 1. Verknüpfung xor 01011011 ;2. Wert ============ 10010110 ;Ergebnis = 1.Wert
not Ziel ;allgemeine Form not reg ;mögliche Operanden not mem |
Der Operand dieses Befehls kann 8, 16 oder 32 Bit groß sein. Der Befehl kehrt den Wert jedes Bits im Operanden um, ein gesetztes Bits ist nach Ausführung also gelöscht, ein gelöschtes Bit ist nach Ausführung gesetzt.
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 ;1258d ins Register DX schreiben shl dx, 3 ;3 Bits nach links herausschieben |
Das Ergebnis dieser Operation besteht darin, dass jetzt in DX der Binärwert
0010011101010000b bzw. 10064d steht. Nicht ohne Zufall ist dieser Wert genau
8mal höher als der Wert vor SHL.
Es gilt allgemein: Erg = Quellwert * 2^Anzahl.
shr Ziel, Anzahl ;shift right shr reg,CL shr reg,con8 shr mem,CL shr mem,con8 |
Verhält sich wie SHL
, nur werden hier die Bits nach rechts
geschoben.
Es gilt allgemein: Erg = Quellwert / 2^Anzahl.
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 |
Verschieben der Bits in Ziel um die angegebene Anzahl nach rechts. Dabei wird
das höchstwertige Bit (das Vorzeichen) immer erhalten. Also wenn das
Vorzeichen-Bit gesetzt war, dann ist es auch nach der Verschiebung noch
gesetzt.
Das zuletzt herausgeschobene Bit befindet sich im Carry-Flag.
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.
Es werden nur die unteren 5 Bits von Anzahl verwendet (max 32).
Erst ab 80386.
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 |
Hier werden die Bits eins nach dem anderen nach links herausgeschoben, ins Carry-Flag bugsiert und von dort am rechten Ende wieder eingefügt. Das zuletzt herausgeschobene Bit befindet sich im Carry-Flag.
rcr Ziel, Anzahl ;rotate through carry right rcr reg,CL rcr reg,con8 rcr mem,CL rcr mem,con8 |
Hier werden die Bits eins nach dem anderen nach rechts herausgeschoben, ins Carry-Flag bugsiert und von dort am linken Ende wieder eingefügt. Das zuletzt herausgeschobene Bit befindet sich im Carry-Flag.
rol Ziel, Anzahl ;rotate left rol reg,CL rol reg,con8 rol mem,CL rol mem,con8 |
Verhält sich ähnlich wie 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 |
Verhält sich ähnlich wie 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 |
Dieser Befehl führt eine logische UND-Verknüpfung der beiden Operanden durch, die die Flags verändern. Das Ergebnis der Verknüpfung wird nicht gespeichert. Dient häufig dazu herauszufinden, ob bestimmte Bits eines Operanden gesetzt sind oder nicht.
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 ;Ziel kann nur ein Register sein |
Der Befehl sucht im zweiten Operanden von Bit 0 beginnend nach dem ersten
gesetzten Bit. Wird kein gesetztes Bit gefunden (Quelle = Null), dann wird das
Zero-Flag gesetzt und das Ziel ist undefiniert, anderenfalls wird das Zero-Flag
gelöscht und in das Ziel wird der Index des ersten gesetzten Bits
geschrieben.
Beide Operanden müssen die gleiche Größe haben, die 16 oder 32
Bit betragen kann, die Quelle kann nur ein Register oder eine Speicherstelle
sein. Der Befehl existiert seit dem 80386.
bsr Ziel, Quelle ;Bit scan reverse bsr reg, mem ;Ziel kann nur ein Register sein |
Der Befehl sucht im zweiten Operanden vom höchstwertigen Bit beginnend nach
dem ersten gesetzten Bit. Wird kein gesetztes Bit gefunden (Quelle = Null), dann
wird das Zero-Flag gesetzt und das Ziel ist undefiniert, anderenfalls wird das
Zero-Flag gelöscht und in das Ziel wird der Index des ersten gesetzten Bits
geschrieben.
Beide Operanden müssen die gleiche Größe haben,
die 16 oder 32 Bit betragen kann, die Quelle kann nur ein Register oder eine
Speicherstelle sein. Der Befehl existiert seit dem 80386.
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.
PUSH
lässt sich am ehesten mit schieben oder stoßen
übersetzen. 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 Operand 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 |
Diese Befehle sind die Umkehrung der Befehle 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 reg8/mem8 ;set byte on condition |
Der Befehl ist im Grunde eine Variation der bedingten Sprungbefehle. Die/das als Parameter übergebene Speicherstelle/Register wird auf 1 gesetzt, wenn die statt cc angegebene Bedingung zutrifft.
Befehl | Bedeutung |
---|---|
seta | set if above (ohne Vorzeichen) |
setae | set if above or equal |
setb | set if below (ohne Vorzeichen) |
setbe | set if below or equal |
setc | set if Carry Flag = 1 |
sete | set if equal |
setg | set if greater (mit Vorzeichen) |
setge | set if greater or equal (mit Vorzeichen) |
setl | set if lower (mit Vorzeichen) |
setle | set if lower or equal (mit Vorzeichen) |
setna | set if not above |
setnae | set if not above or equal |
setnb | set if not below |
setnbe | set if not below or equal |
setnc | set if not Carry = 1 |
setne | set if not equal |
setng | set if not greater |
setnge | set if not greater or equal |
setnl | set if not lower |
setnle | set if not lower or equal |
setno | set if not Overflow = 1 |
setnp | set if not Parity = 1 |
setns | set if not Sign = 1 |
setnz | set if not Zero = 1 |
seto | set if Overflow Flag = 1 |
setp | set if Parity Flag = 1 |
setpe | set if parity even (Parity = 1) |
setpo | set if paritiy odd (Parity = 0) |
sets | set if Sign Flag = 1 |
setz | set if Zero Flag = 1 |
lahf |
Dieser Befehl überträgt die unteren 8 Bit des Flagregisters in das Register AH. Folgende Bits sind davon betroffen:
Bitnummer | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Flag | Sign | Zero | nicht definiert | Auxiliary Carry | nicht definiert | Parity | nicht definiert | Carry |
sahf |
Dieser Befehl lädt den Inhalt des Registers AH in die unteren 8 Bit des Flagregisters. Zur Belegung siehe LAHF
.
call addr |
Für nähere Informationen zu diesem Befehl verweise ich auf das Kapitel Unterprogramme - Prozeduren.
clc ;clear carry flag |
stc ;set carry flag |
cld ;clear direction flag |
Setzt das Direction-Flag auf Null, löscht es also. Bei den Stringbefehlen führt das dazu, daß nach Ausführung des Stringbefehls die von diesem Befehl verwendeten Indexregister um die Operandengröße erhöht werden.
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 (Interrupt-Handler) eine ungestörte Abarbeitung zu gewährleisten.
sti ;clear interrupt-enable flag |
Setzt das Interrupt-Enable-Flag auf Eins. Die Bearbeitung kann ab jetzt durch Interrupts unterbrochen werden.
Der Befehl CPUID
wurde erst mit dem Pentium eingeführt,
wurde aber dann auch in die späteren Versionen der 80486 eingebaut.
Ob ein Prozessor über diesen Befehl verfügt kann man über das
ID-Flag (Bit 21) im EFlags-Register ermitteln. Wenn ein Programm dieses Flag
verändern kann dann existiert der Befehl auf diesem Prozessor. Dazu muss
aber ebenfalls ermittelt werden, ob es überhaupt das erweiterte Flag-
Register gibt (sprich: ob man einen 80386 oder höher vor sich hat).
Mit diesem Befehl wurde endlich ein definierter Weg geschaffen, um etwas über die Fähigkeiten des Prozessors zu erfahren. Vorher mussten mehr oder weniger komplizierte Tricks angewandt werden, die teilweise bekannte Prozessorbugs als Informationsquelle ausnutzten. Mit diesem Befehl kann man in der Tat sehr viele Informationen ermitteln. Der Befehl wurde zwar bei allen Pentium-Klonen implementiert, allerdings in unterschiedlichem Umfang und mit unterschiedlichen Ergebnissen.
CPUID
ist sozusagen themenorientiert. Das Thema wird dabei vor dem
Aufruf ins Register EAX geschrieben.
Mit dieser Funktion wird der sogenannte Vendor Identification String sowie die
höchste gültige Funktionsnummer für diesen Prozessor ausgegeben.
Der Identifikationsstring für Intel-Prozessoren lautet
"GenuineIntel", bei AMD "AuthenticAMD". Das EAX-Register
enthält die höchste Funktionsnummer, die vor dem Aufruf von
CPUID
in EAX eingegeben werden kann. Beim reinen Pentium sowie den
neueren 80486ern wird 01H zurückgegeben, beim Pentium Pro, Pentium II sowie
den Celerons eine 02H, der P III kennt 3 Funktionen und der Pentium 4 sowie der
Intel XEON werden 02H zurückgeben.
Funktion 1 gibt in EAX Versionsinformationen über den Prozessor zurück. Die Bits 0-3 enthalten die Stepping ID, Bits 4-7 das Modell, Bits 8-11 die Familie und Bits 12-13 den Prozessortyp. Der Prozessortyp kann die Werte 00B (Original OEM Processor), 01B (Intel Overdrive ® Processor), 10B (Dual Processor) und 11B (Intel reserved) annehmen.
In den Bits 0-7 des Registers EBX wird ein Index in eine Tabelle mit Markenstrings hinterlegt. Die Indizes 4-7 und 9-255 sind reserviert, Index 1 steht für einen Celeron Prozessor, Index 2 für den Pentium III Prozessor, Index 3 für den Pentium III Xeon und und Index 8 für den Pentium 4. Index 0 besagt, dass das Feature der Markenstrings nicht unterstützt wird. Dieses Feld gibt es seit dem Pentium III Xeon.
Bits 8-15 (das 2. Byte) geben die Größe einer Cache Line an, die mit
CLFLUSH
geschrieben wird. Der Wert muss dazu mit 8 multipliziert
werden. Dieses Feld wurde mit dem Pentium 4 eingeführt.
Bits 16-23 geben die Anzahl der logischen Prozessoren je physikalischem Prozessor an. Dies steht im Zusammenhang mit Technologien wie Hyperthreading.
Bits 24-31 ist die ID, die dem lokalen APIC (Advanced Programmable Interrupt Controller; auf dem Prozessor untergebrachter Interrupt Controller) beim Booten zugewiesen wird. Gibt es auch erst seit dem Pentium 4.
Register ECX ist derzeit reserviert.
Register EDX enthält sehr umfangreiche Informationen über
Prozessorfeatures.
Bit | Beschreibung |
---|---|
0 | Der Prozessor beinhaltet eine On-Chip-FPU |
1 | Erweiterungen für den Virtual 8086 Mode sind vorhanden |
2 | Erweiterungen fü prozessorgestütztes Debugging sind vorhanden |
3 | gibt an ob die Pages auch 4 MByte groß werden können (sonst 4 KByte) |
4 | gibt an, ob der Befehl RDTSC (Read Time Stamp Counter) unterstützt wird |
5 | die Befehle RDMSR und WRMSR werden unterstützt. MSR = Model Specific Register |
6 | physikalische Adressen oberhalb von 32 Bit sind möglich; die Anzahl der Zusatzbits ist nicht definiert und prozessorabhängig |
7 | Machine Check Exceptions sind verfügbar |
8 | die Instruktion CMPXCHG8B wird unterstützt |
9 | es befindet sich ein APIC auf dem Prozessor |
10 | reserviert |
11 | die Befehle SYSENTER und SYSEXIT sind verfügbar |
12 | Memory Type Range Register sind vorhanden |
13 | in Page Directory Entries sowie Page Table Entries ist das Global Bit vorhanden, das angibt, dass die Page global verfügbar ist |
14 | die Machine Check Architecture wird unterstützt, die ein einheitliches Verfahren für Fehlersuche und -report darstellt. Dazu werden MSRs verwendet. |
15 | die CMOVXX-Befehle sind verfügbar; ähnlich wie die Sprungbefehle und die Varianten des SET-Befehls bedingen Flagstellungen ein MOV |
16 | Page Attribute Tables sind vorhanden |
17 | der Prozessor unterstützt eine weitere Möglichkeit Speicher jenseits der 32 Bit zu adressieren |
18 | das Feature der Prozessorseriennummer wird unterstützt |
19 | der Befehl CLFLUSH ist vorhanden |
20 | reserviert |
21 | Debug Informationen können in einen Speicherbereich geschrieben werden |
22 | ACPI wird unterstützt; der Prozessor kann seine eigene Temperatur verfolgen und davon abhängig die Performance anpassen |
23 | MMX wird unterstützt |
24 | FXSAVE und FXRSTOR zum Bearbeiten des FPU-Contextes werden unterstützt |
25 | SSE ist verfügbar |
26 | SSE2 ist verfügbar |
27 | Self Snoop; damit kann der Prozessor auf kollidierende Speichertypen reagieren indem der Cache überprüft wird |
28 | Hyper-Threading ist implementiert |
29 | im Prozessor ist eine Temperatur-Verfolgungs-Schaltung eingebaut |
30 | reserviert |
31 | zeigt an, dass ein bestimmter Pin vorhanden ist, der besagt, dass ein Interrupt auf Verarbeitung wartet |
Diese Funktion gibt Informationen über die Caches sowie die TLBs (Translation Lookaside Buffer) in den Registern EAX, EBX, ECX und EDX aus. Das unterste Byte in EAX gibt an, wie oft die Funktion aufgerufen werden muss, um alle Informationen über Caches und TLBs zu erhalten. Das Bit 31 aller 4 Register gibt an, ob das Register auswertbare Informationen (Bit 31 = 0) enthält oder nicht (Bit 31 = 1). Sobald ein Register auswertbare Informationen beinhaltet, werden die Registerinhalte byteweise betrachtet. Jedes Byte kann einen bestimmten Wert aus der folgenden Tabelle annehmen. Die Reihenfolge der Werte in den Registern ist nicht definiert.
Wert | Beschreibung |
---|---|
00H | Nulldeskriptor |
01H | Instruction TLB: 4 KByte Pages, 4-Wege-assoziativ, 32 Einträge |
02H | Instruction TLB: 4 MByte Pages, 4-Wege-assoziativ, 2 Einträge |
03H | Data TLB: 4 KByte Pages, 4-Wege-assoziativ, 64 Einträge |
04H | Data TLB: 4 MByte Pages, 4-Wege-assoziativ, 8 Einträge |
06H | 1st Level Instruction Cache: 8 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
08H | 1st Level Instruction Cache: 16 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
0AH | 1st Level Data Cache: 8 KByte, 2-Wege-assoziativ, 32 Byte Line-Größe |
0CH | 1st Level Data Cache: 16 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
22H | 3rd Level Cache: 512 KByte, 4-Wege-assoziativ,Dual-sectored line, 64 Byte Sektor-Größe |
23H | 3rd Level Cache: 1 MByte, 8-Wege-assoziativ,Dual-sectored line, 64 Byte Sektor-Größe |
40H | Kein 2nd Level Cache, oder, falls der Prozessor einen gültigen 2nd Level Cache besitzt, dann kein 3rd Level Cache |
41H | 2nd Level Cache: 128 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
42H | 2nd Level Cache: 256 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
43H | 2nd Level Cache: 512 KByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
44H | 2nd Level Cache: 1 MByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
45H | 2nd Level Cache: 2 MByte, 4-Wege-assoziativ, 32 Byte Line-Größe |
50H | Instruction TLB: 4 KByte und 2 MByte oder 4 MByte Pages, 64 Einträge |
51H | Instruction TLB: 4 KByte und 2 MByte oder 4 MByte Pages, 4-Wege-assoziativ, 128 Einträge |
52H | Instruction TLB: 4 KByte und 2 MByte oder 4 MByte Pages, 4-Wege-assoziativ, 256 Einträge |
5BH | Data TLB: 4 KByte und 4 MByte Pages, 4-Wege-assoziativ, 64 Einträge |
5CH | Data TLB: 4 KByte und 4 MByte Pages, 4-Wege-assoziativ, 128 Einträge |
5DH | Data TLB: 4 KByte und 4 MByte Pages, 4-Wege-assoziativ, 256 Einträge |
66H | 1st Level Data Cache: 8 KByte Pages, 4-Wege-assoziativ, 64 Byte Line-Größe |
67H | 1st Level Data Cache: 16 KByte Pages, 4-Wege-assoziativ, 64 Byte Line-Größe |
68H | 1st Level Data Cache: 32 KByte Pages, 4-Wege-assoziativ, 64 Byte Line-Größe |
70H | Trace Cache: 12K-microOP, 8-Wege-assoziativ |
71H | Trace Cache: 16K-microOP, 8-Wege-assoziativ |
72H | Trace Cache: 32K-microOP, 8-Wege-assoziativ |
79H | 2nd-level cache: 128 KB, 8-Wege-assoziativ, Dual-sectored Line, 64 Byte Sektor-Größe |
7AH | 2nd-level cache: 256 KB, 8-Wege-assoziativ, Dual-sectored Line, 64 Byte Sektor-Größe |
7BH | 2nd-level cache: 512 KB, 8-Wege-assoziativ, Dual-sectored Line, 64 Byte Sektor-Größe |
7CH | 2nd-level cache: 1 MB, 8-Wege-assoziativ, Dual-sectored Line, 64 Byte Sektor-Größe |
82H | 2nd-level cache: 256 KB, 8-Wege-assoziativ, 32 Byte Line-Größe |
83H | 2nd-level cache: 512 KB, 8-Wege-assoziativ, 32 Byte Line-Größe |
84H | 2nd-level cache: 1 MByte, 8-Wege-assoziativ, 32 Byte Line-Größe |
85H | 2nd level cache: 2 MByte, 8-Wege-assoziativ, 32 Byte Line-Größe |
Seit dem Pentium 4 kennt Intel auch die Erweiterten Funktionen, mit denen AMD schon länger arbeitet. Die maximale Anzahl an erweiterten Funktionen ermitteln Sie, indem Sie CPUID mit dem Wert 80000000H in EAX aufrufen. Sollte der Prozessor keine erweiterten Funktionen unterstützen so wird automatisch die höchste verfügbare Basisfunktion ausgeführt. Ansonsten wird in EAX sowohl für den Pentium 4 als auch den Intel Xeon 80000004H ausgegeben. Die Register EBX, ECX und EDX sind reserviert.
Diese Funktion gibt erweiterte Informationen zur Prozessorsignatur sowie zu Feature Bits zurück. Alle Register sind zur Zeit reserviert (nicht definiert).
Wenn diese Funktionen in aufsteigender Reihenfolge nacheinander aufgerufen werden, dann werden in den Registern EAX, EBX, ECX und EDX 16 Byte (je Aufruf) hinterlegt, die, miteinander verbunden, den sogenannten Brand String ergeben (nicht zu verwechseln mit der Funktion 01H). Insgesamt kann dieser Brand String 48 Bytes lang werden, wobei das Byte 48 per Definition den Wert Null hat. Der Brand String ist rechts ausgerichtet, notfalls werden Leerzeichen vorangestellt. Sollte der Brand String eine MHz-Angabe enthalten, dann wird dadurch die von Intel sichergestellte maximale Taktfrequenz angegeben, nicht die aktuell geltende Taktfrequenz.
Vorheriges Kapitel (Speichermodelle) | Nach oben | Nächstes Kapitel (Strukturiertes Programmieren) |
Zum Inhaltsverzeichnis | ||
Zur Startseite |