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!