Adok's Way to Assembler
Folge 4
Begleit-Dateien:
+++ Flag-Register +++
Das Flag-Register, die Steuerzentrale des Computers, ist das einzige
Prozessorregister, das nicht über den MOV-Befehl angesprochen werden kann. Wie
man es anspricht, werden wir noch später erfahren.
Das Flag-Register ist 16 Bit breit, wobei jedes einzelne Bit eine eigene
Funktion erfüllt. In dieser Kursfolge werden wir folgende zwei Flags
benötigen:
- Bit 0 - Carryflag: Dieses Flag wird u.a. dann automatisch gesetzt, wenn bei
einer mathematischen Operation ein Über- oder ein
Unterlauf entsteht. -> Das Carryflag wird gesetzt, wenn
die Quelle eines CMP-Befehls - wie wir ihn bald
kennenlernen werden - größer als das Ziel ist.
- Bit 6 - Zeroflag: Ist das Ergebnis eines Befehls gleich 0, so wird dieses
Bit automatisch gesetzt. -> Dieses Flag ist wichtig, um
die Funktionsweise des JE-Befehls, welchen wir ebenfalls
bald kennenlernen werden, zu verstehen!
Weitere Flags, die wir nicht unmittelbar brauchen werden, aber ganz nützlich
sind:
- Bit 8 - Trapflag: Ist dieses Flag gesetzt, wird nach jedem ausgeführten
Befehl INT 1 ausgelöst. (Das machen sich auch Debugger
zunutze, um nach jedem Befehl das Programm zu unter-
brechen und dem Benutzer/Hacker den aktuellen Status der
Register und des Programms anzuzeigen.)
- Bit 9 - Interrupt-Enable-Flag: Dieses Flag läßt sich mit dem Befehl STI
setzen und mit CLI löschen. Ist es nicht gesetzt (also
gleich 0), sind alle Interrupts außer den sogenannten
NMIs (Non-Maskable-Interrupts, zu deutsch: nichtmaskier-
bare Unterbrechungen) deaktiviert.
- Bit 10 - Directionflag: Bei sogenannten String-Befehlen wird angezeigt, ob
ein Block in aufsteigender (Flag=0) oder absteigender
(Flag=1) Reihenfolge abgearbeitet werden soll, also das
Register, das auf das aktuelle Element zeigt, jedesmal
erhöht oder erniedrigt werden soll. Zu diesem Zweck läßt
sich das Directionflag auch vom Programmierer selbst
direkt setzen & löschen, und zwar mit den Befehlen STD
und CLD.
Der Vollständigkeit halber erwähne ich auch die weniger interessanten Flags:
- Bit 2 - Parityflag: Hat das Ergebnis einer Operation eine gerade Anzahl von
gesetzten Bits, so ist dieses Flag gesetzt. Wird von
vielen DFÜ-Programmen bei der CRC-Prüfung der seriellen
Schnittstelle verwendet.
- Bit 4 - Auxiliary-Carryflag: Entspricht dem Carryflag, wird aber nur dann
gesetzt, wenn man mit sogenannten BCD-(Binary-Coded-
Decimals)-Zahlen arbeitet. BCD-Zahlen werden allerdings
kaum verwendet, weil sie ziemlich speicherintensiv sind.
- Bit 7 - Signflag: Entspricht dem höchstwertigen Bit des Ergebnisses der
letzten Operation. (Das Signflag wird vor allem bei vor-
zeichenbehafteten Zahlen benötigt. Dort gibt nämlich das
höchste Bit das Vorzeichen an (1=minus, 2=plus). Dies
ist auch der Grund, warum in allen Programmiersprachen
signed-Datentypen einen kleineren Höchstwert haben als
unsigned-Datentypen. In diesem Kurs werden wir aber
wahrscheinlich nicht näher auf vorzeichenbehaftete
Zahlen eingehen.)
- Bit 8 - Overflowflag: Ist ebenfalls nur dann interessant, wenn mit
vorzeichenbehafteten Zahlen gearbeitet wird. (Rechnet
man bspw. 60 + 70, so ergibt dies 130. Damit wird aber
das höchste Bit des Ergebnis-Bytes gesetzt, das als
Vorzeichen betrachtet zu einem falschen Ergebnis führen
würde. Deshalb wird in solchen Fällen automatisch das
Overflowflag gesetzt.)
+++ Vergleiche in Assembler +++
Wozu nun das Ganze? Nun, in ASM gibt es keine IF/THEN-Konstrukte wie in den
Hochsprachen. Stattdessen existiert ein ganz besonderer Befehl, CMP (nicht zu
verwechseln mit CP/M :-)) ). Syntax:
CMP Ziel,Quelle
Ziel und Quelle sind die zu vergleichenden Werte, also Zahlen, Register oder
Speicherstellen. Wie arbeitet CMP? Ganz einfach: CMP zieht die Quelle vom Ziel
ab, wobei im Gegensatz zum "richtigen" Subtraktionsbefehl, SUB, die Regs bzw.
Speicherstellen nicht verändert werden. Das Geniale an der Sache ist nun: Je
nachdem, welches Ergebnis bei der Subtraktion herauskommt, werden bestimmte
Flags gesetzt oder gelöscht!
- Sind Ziel und Quelle identisch, ist das Ergebnis gleich 0 - also wird das
Zeroflag gesetzt. Andernfalls wird das Zeroflag gelöscht.
- Ist die Quelle größer als das Ziel, entsteht ein Unterlauf - also wird das
Carryflag gesetzt. Andernfalls wird das Carryflag gelöscht.
Und jetzt kommt's: Es gibt verschiedene bedingte Sprungbefehle, die auf
bestimmte Register reagieren.
- JE und JZ: Der Sprung zum angegebenen Label wird nur dann durchgeführt,
wenn das Zeroflag gesetzt ist.
-> Dieser Sprungbefehl wird verwendet, wenn man überprüfen
will, ob zwei Werte identisch sind.
- JNE und JNZ: ...wenn das Zeroflag nicht gesetzt ist.
-> ...ob zwei Werte ungleich sind.
- JA: ...wenn weder das Carry- noch das Zeroflag gesetzt ist.
-> ...ob das Ziel größer als die Quelle ist.
- JB: ...wenn das Carryflag gesetzt ist.
-> ...ob die Quelle größer als das Ziel ist.
Hängt man an JA bzw. JB noch ein E dran (JAE, JBE), so wird aus "größer"
"größer oder gleich".
Nun habe ich noch zwei HOT TIPS für euch! :-)
- Wenn man prüfen will, ob das CX-Register gleich 0 ist, kann man sich CMP
ersparen! Der Befehl JCXZ führt einen Sprung aus, wenn CX=0.
- Will man prüfen, ob ein Wert gleich 0 ist, so müßte man nach dem, was wir
gelernt haben, schreiben:
CMP Wert,0
JE Label
Es geht aber auch so:
OR Wert,Wert
JE Label
Hiermit wird der angegebene Wert mit sich selbst OR-verknüpft. Dadurch wird
der Wert nicht geändert, aber, wenn der Wert 0 ist, das Zeroflag gesetzt.
Statt OR kann man auch AND oder TEST schreiben. Alle drei Möglichkeiten sind
um einige Taktzyklen schneller als CMP.
Folgendes Beispielprogramm demonstriert die Verwendung des CMP-Befehls.
MODEL TINY ;Für COM-Files
CODE SEGMENT ;Beginn Code-Seg
ASSUME CS:CODE,DS:CODE ;CS und DS zeigen auf Code-Seg
ORG 100h ;Startadresse COM
start: ;Startlabel
JMP begin ;Sprung zu Label begin
wert1 DB 10 ;Variable wert1
wert2 DB 0 ;Variable wert2
text1 DB "Werte gleich$";Meldung 1
text2 DB "Wert1 größer$";Meldung 2
text3 DB "Wert2 größer$";Meldung 3
begin: ;Beginn des Proggys
MOV BH,BYTE PTR wert2 ;Wert 2 auf BH
CMP wert1,BH ;Werte vergleichen
JNE ungleich ;Wenn ungleich -> Label ungleich
MOV DX,OFFSET text1 ;Ansonsten Meldung 1
JMP ausgabe ;Sprung zu Label ausgabe
ungleich: ;Wenn ungleich...
JB w2groesser ;Wenn W2 größer -> Label w2groesser
MOV DX,OFFSET text2 ;Ansonsten Meldung 2
JMP ausgabe ;Sprung zu Label ausgabe
w2groesser: ;Wenn Wert 2 größer...
MOV DX,OFFSET text3 ;Meldung 3
ausgabe: ;Meldung ausgeben
MOV AX,0900h ;Funkt. 9
INT 21h ;String ausgeben
MOV AX,4C00h ;Funkt. 4Ch
INT 21h ;Programm beenden
CODE ENDS ;Ende Code-Seg
END start ;Ende des Proggys
Um deutlich zu machen, daß CMP sowohl mit Registern als auch mit Speicher-
stellen arbeiten kann, ist in diesem Programm der eine Parameter ein Register,
der andere eine Speicherstelle. Das Programm vergleicht nun die beiden Werte
und gibt aus, ob sie gleich sind oder welcher der beiden größer ist. Setzt
einfach in die Variablendefinitionen andere Zahlen ein und compiliert das
Programm neu, um zu sehen, welche Auswirkungen dies hat. Spielt euch auch
herum, probiert, andere Sprungbefehle zu verwenden - solange, bis ihr die
Funktionsweise eines jeden Sprungbefehls versteht.
+++ Stack +++
Der Stack (Stapel, "Kellerspeicher") ist eine besondere Art von Datensegment,
das mit den Befehlen PUSH und POP angesprochen wird. Das Register SS zeigt
immer auf das Segment des Stacks und das Register SP (Stackpointer) auf die
aktuelle Position im Stapel.
Nehmen wir an, SP zeige auf den Offset 1000. Nun schreiben wir PUSH AX. Damit
wird der Inhalt von AX auf den Offset 1000 im Stacksegment geschrieben.
Gleichzeitig wird dabei der Inhalt von SP um zwei - denn AX ist ein Word, also
2 Bytes groß - erniedrigt. Jetzt schreiben wir PUSH BX. Nun wird der Inhalt
von BX auf den Offset 998 im Stacksegment geschrieben und der Stackpointer um
zwei weitere Bytes erniedrigt. Er zeigt nun also auf den Offset 996.
Mit dem Befehl POP lassen sich Werte vom Stapel zurückholen. Dabei muß berück-
sichtigt werden: Das Ganze arbeitet nach dem sogenannten LIFO-Prinzip - Last
In, First Out. Das bedeutet: Der zuletzt gePUSHte Wert wird als erster gePOPt.
Wenn man den Stack mit einem schmalen Keller vergleicht, wird man erkennen,
daß es ja ganz logisch ist: Nehmen wir an, ich werfe einen Fernseher in den
Keller, danach eine Waschmaschine. Wenn ich wieder einen Gegenstand zu mir
nehmen will, muß ich zuerst die Waschmaschine - das als letztes hinein-
geworfene Objekt - nehmen, dann den Fernseher (abgesehen davon, daß der Fern-
seher sowieso schon beschädigt sein wird :-) ). Genauso verhält es sich mit
den Werten am Stack. Schreibt man nun z.B. POP AX, so wird der letzte Wert vom
Stack geholt, in AX geschrieben und der Stackpointer um zwei erhöht. Dabei muß
betont werden, daß der Wert nicht vom Stack gelöscht wird! Er bleibt noch auf
der Speicherstelle, auf die er gePUSHt wurde - lediglich zeigt jetzt der
Stackpointer auf ein anderes Element. Und wenn wir ein Word POPen, z.B. POP
CX, wird SP wiederum um zwei erhöht. Somit zeigt er in unserem Beispiel wieder
auf den Offset 1000.
Drei wichtige Dinge, die beim Arbeiten mit dem Stack berücksichtigt werden
müssen:
- PUSH und POP funktionieren nur 16-bittig! PUSH BL bspw. würde also nicht
funktionieren.
- Die Anzahl der PUSHs und der POPs müssen einander ausgleichen, so daß der
Stackpointer am Schluß wieder auf den Offset zeigt, auf den er vorher
gezeigt hat.
- Verwendet man den Stack in COM-Dateien, so zeigt das SS-Register natürlich
auf das Codesegment und der Stackpointer auf das letzte Byte im Codesegment,
nämlich CS:FFFFh. Normalerweise ist dies unproblematisch. Problematisch
wird es nur dann, wenn das Programm so groß ist, daß beim PUSHen die letzten
Befehle überschrieben werden. Also Vorsicht!
So, das war's für heute! In der nächsten Folge geht's weiter, und natürlich
wünsche ich euch auch diesmal bis dahin viel Spaß! Adok!