Assembler-Programmierung für x86-Prozessoren/ Sprünge und Schleifen



Unbedingte Sprünge Bearbeiten

Unbedingte Sprünge entsprechen im Prinzip einem GOTO in höheren Programmiersprachen. Im Assembler heißt der Befehl allerdings jmp (Abkürzung für jump; dt. „springen“).

Das Prinzip des Sprungbefehls ist sehr einfach: Um die Bearbeitung des Programms an einer anderen Stelle fortzusetzen, wird das IP-Register mit der Adresse des Sprungziels geladen. Da die Adresse des IP-Registers zusammen mit dem CS-Register immer auf den nächsten Befehl zeigt, den der Prozessor bearbeiten wird, setzt er die Bearbeitung des Programms am Sprungziel fort.

Der Befehl hat die folgende Syntax:

  jmp Zieloperand

Je nach Sprungart kann der Zieloperand ein Wert und/oder ein Register bzw. eine Speicherstelle sein. Im Real Mode gibt es drei verschiedene Sprungarten:

  • Short Jump: Der Sprung erfolgt innerhalb einer Grenze von -128 bis +127 Byte.
  • Near Jump: Der Sprung erfolgt innerhalb der Grenzen des Code-Segments.
  • Far Jump: Der Sprung erfolgt innerhalb des gesamten im Real Mode adressierbaren Speichers.

Bevor wir noch genauer auf die Syntax eingehen, sehen wir uns ein Beispiel an, das einen Short Jump benutzt. Bei einem Short Jump darf der Zieloperand nur als Wert angegeben werden. Ein Register oder eine Speicherstelle ist nicht erlaubt.

Der Wert wird als relative Adresse (bezogen auf die Adresse des nächsten Befehls) interpretiert. Das heißt, dem Inhalt des IP-Registers wird der Wert des Zieloperanden hinzuaddiert. Ist der Wert des Zieloperanden positiv, so liegt das Sprungziel hinter dem jmp-Befehl, ist der Wert negativ, so befindet sich das Sprungziel vor dem jmp-Befehl.

Das folgende Programm führt einen Short Jump aus:

  org 100h
  start:
         jmp short ziel
         ;ein paar unsinnige Befehle:
         mov ax, 05h
         inc ax
         mov bx, 07h
         xchg ax, bx
  ziel:  mov cx, 05h
         mov ah,4Ch
         int 21h

Das Sprungziel ist durch ein Label gekennzeichnet (hier: ziel). Damit kann der Assembler die (absolute oder relative) Adresse für das Sprungziel ermitteln und in den Opcode einsetzen.

Wenn Sie das Programm mit dem Debugger ausführen, bekommen Sie die folgende Ausgabe:

-r
AX=0000  BX=0000  CX=0011  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=0D3E  ES=0D3E  SS=0D3E  CS=0D3E  IP=0100   NV UP EI PL NZ NA PO NC
0D3E:0100 EB08          JMP     010A
-t

Lassen Sie sich nicht von der Ausgabe JMP 010A irritieren. Wie Sie am Opcode EB08 sehen können, besitzt der Zieloperand tatsächlich den Wert 08h; EBh ist der Opcode des (short) jmp-Befehls.

Die nächste Adresse nach dem jmp-Befehle beginnt mit 0102h. Addiert man hierzu den Wert 08h, so erhält man die Adresse des Sprungziels: 010Ah. Mit dieser Adresse lädt der Prozessor das IP-Register:

AX=0000  BX=0000  CX=0011  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=0D3E  ES=0D3E  SS=0D3E  CS=0D3E  IP=010A   NV UP EI PL NZ NA PO NC
0D3E:010A B90500        MOV     CX,0005
-t

Wie Sie nun sehen, zeigt nun die Registerkombination CS:IP auf den Befehl mov cx,0005h, wo der Prozessor mit der Bearbeitung fortfährt.

Beachten Sie, dass NASM nicht automatisch erkennt, ob es sich um einen Short oder Near Jump handelt (im Gegensatz zum Turbo Assembler und zum Macro Assembler ab Version 6.0). Fehlt das Schlüsselwort short, so übersetzt dies der Assembler in einen Near Jump (was zur Folge hat, dass der Opcode um ein Byte länger ist).

Beim Short Jump darf der Zieloperand nur eine relative Adresse besitzen, wohingegen beim Near Jump sowohl eine relative wie auch eine absolute Adresse möglich ist. Dies ist auch leicht einzusehen, wenn Sie sich nochmals den Opcode ansehen: Da nur ein Byte für die Adressinformation zu Verfügung steht, kann der Befehl keine 16bittige absolute Adresse erhalten.

Wenn der Near Jump-Befehl aber sowohl ein relative als auch eine absolute Adresse erhalten kann, wie kann der Assembler dann unterscheiden, in welchen Opcode er den Befehl übersetzen muss? Nun, wenn der Zieloperand ein Wert ist, handelt es sich um eine relative Adresse, ist der Zieloperand in einem Register oder einer Speicherstelle abgelegt, so handelt es sich um eine absolute Adresse.

Beim Far Jump wird sowohl das CS Register wie auch das IP Register neu geladen. Damit kann sich das Sprungziel auch in einem anderen Segment befinden. Die Adresse kann entweder ein Wert sein oder ein Registerpaar. In beiden Fällen handelt es sich um eine absolute Adresse. Eine relative Adresse existiert beim Far Jump nicht.

Das folgende Beispiel zeigt ein Programm mit zwei Codesegmenten, bei dem ein Far Jump in das zweite Segment erfolgt:

 segment code1 
   start:
         jmp word far ziel

  segment code2 
  ziel:  mov ah, 4Ch
         int 21h

Bitte vergessen Sie nicht, dass com-Dateien nur ein Codesegment besitzen dürfen und deshalb das Programm als exe-Datei übersetzt werden muss (siehe Kapitel Das erste Assemblerprogramm / EXE-Dateien).

Das Schlüsselwort word wird benutzt, da es sich um eine 16-Bit-Adresse handelt. Ab der 80386 CPU sind auch 32-Bit-Adressen möglich, da das IP Register 32 Bit breit ist (die Segmentregister sind auch bei der 80386 CPU nur 16 Bit groß).

Bedingte Sprünge Bearbeiten

Mit bedingten Sprüngen lassen sich in Assembler Kontrollstrukturen vergleichbar mit IF … ELSE Anweisungen in Hochsprachen ausdrücken. Im Gegensatz zum unbedingten Sprung wird dazu nicht in jedem Fall ein Sprung durchgeführt, sondern in Abhängigkeit der Flags.

Natürlich müssen dazu erst die Flags gesetzt werden. Dies erfolgt mit dem cmp- oder test-Befehl.

Der cmp-Befehl hat die folgende Syntax:

 cmp operand1, operand2

Wie Sie sehen, existiert weder ein Quell- noch ein Zieloperand. Dies liegt daran, dass keiner der beiden Operanden verändert wird. Vielmehr entspricht der cmp- einem sub-Befehl, lediglich mit dem Unterschied, dass nur die Flags verändert werden.

Der test-Befehl führt im Gegensatz zum cmp-Befehl keine Subtraktion, sondern eine UND Verknüpfung durch. Der test-Befehl hat die folgende Syntax:

 test operand1, operand2

Mit dem test-Befehl kann der bedingte Sprung davon abhängig gemacht werden, ob ein bestimmtes Bit gesetzt ist oder nicht.

Für Vorzeichenbehaftete Zahlen müssen andere Sprungbefehle benutzt werden, da die Flags entsprechend anders gesetzt sind. Die folgende Tabelle zeigt sämtliche bedingte Sprünge für vorzeichenlose Zahlen:

Sprungbefehl Abkürzung für Zustand der Flags Bedeutung
ja jump if above CF = 0 und ZF = 0 Operand1 > Operand2
jae jump if above or equal CF = 0 Operand1 >= Operand2
jb jump if below CF = 1 Operand1 < Operand2
jbe jump if below or equal CF = 1 oder ZF = 1 Operand1 <= Operand2
je jump if equal ZF = 1 Operand1 = Operand2
jne jump if not equal ZF = 0 Operand1 <> Operand2, also Operand1 != Operand2

Wie Sie an der Tabelle erkennen können, ist jeder Vergleich an eine charakteristische Kombination der Carry Flags und des Zero Flags geknüpft. Mit dem Zero Flag werden die Operanden auf Gleichheit überprüft. Sind beide Operanden gleich, so ergibt eine Subtraktion 0 und das Zero Flag ist gesetzt. Sind beide Operanden dagegen ungleich, so ist das Ergebnis einer Subtraktion bei vorzeichenlosen Zahlen einen Wert ungleich 0 und das Zero Flag ist nicht gesetzt.

Kleiner und größer werden durch das Carry Flag angezeigt: Wenn Zieloperand kleiner als der Quelloperand ist, so wird das Most Significant Bit „herausgeschoben“. Dieses Herausschieben wird durch das Carry Flag angezeigt. Das folgende Beispiel zeigt eine solche Subtraktion:

    0000 1101 (Operand1)
-   0001 1010 (Operand2)
Ü 1 111   1
-------------
    1111 0011

Der fett markierte Übertrag stellt den Übertrag dar, der das Carry Flag setzt. Wie Sie an diesem Beispiel sehen, lässt sich das Carryflag also auch dazu benutzen um festzustellen, ob der Zieloperand kleiner oder größer als der Quelloperand ist.

Mit diesem Wissen können wir uns nun herleiten, warum die Flags so gesetzt sein müssen und nicht anders:

  • ja: Hier testen wir mit dem Carry Flag, ob die Bedingung Operand1 > Operand2 erfüllt ist. Da die beiden Operanden nicht gleich sein sollen, testen wir außerdem noch das Zero Flag.
  • jae: Der Befehl entspricht dem ja-Befehl, mit dem Unterschied, dass wir nicht das Zero Flag testen, da beide Operanden gleich sein dürfen.
  • jb : Hier testen wir mit dem Carry Flag, ob die Bedingung Operand1 < Operand2 erfüllt ist. Aber wieso testet jb nicht auch das Zero Flag? Sehen wir uns dies an einem Beispiel an: Angenommen der erste und zweite Operand haben jeweils den Wert 5. Wenn wir beide voneinander subtrahieren, so erhalten wir als Ergebnis 0. Damit ist das Zero Flag gesetzt und das Carry Flag ist 0. Folge: Es reicht aus zu prüfen, ob das Carry Flag gesetzt ist oder nicht. Die beiden Operanden müssen in diesem Fall ungleich sein.
  • jbe: Mit dem Carry Flag testen wir wieder die Bedingung Operand1 < Operand2 und mit dem Zero Flag auf Gleichheit. Beachten Sie, dass entweder das Carry Flag oder das Zero Flag gesetzt werden müssen und nicht beide gleichzeitig gesetzt sein müssen.

Die Befehle je und jne dürften nun selbsterklärend sein.

Wie bereits erwähnt, unterscheiden sich die bedingten Sprünge mit vorzeichenbehafteten Zahlen von Sprüngen mit vorzeichenlosen Zahlen, da hier die Flags anders gesetzt werden:

Sprungbefehl Abkürzung für Zustand der Flags Bedeutung
jg jump if greater ZF = 0 und SF = OF Operand1 > Operand2
jge jump if greater or equal SF = OF Operand1 >= Operand2
jl jump if less ZF = 0 und SF <> OF Operand1 < Operand2
jle jump if less or equal ZF = 1 oder SF <> OF Operand1 <= Operand2

Die Befehle je und jne sind bei vorzeichenlosen und vorzeichenbehafteten Operatoren identisch und tauchen deshalb in der Tabelle nicht noch einmal auf.

Darüber hinaus existieren noch Sprungbefehle, mit denen sich die einzelnen Flags abprüfen lassen:

Sprungbefehl Abkürzung für Zustand der Flags
jc jump if carry CF = 1
jnc jump if not carry CF = 0
jo jump if overflow OF = 1
jno jump if not overflow OF = 0
jp jump if parity PF = 1
jpe jump if parity even PF = 1
jpo jump if parity odd PF = 0

Es fällt auf, dass sowohl der jb und jae wie auch jc und jnc testen, ob das Carry Flag gesetzt bzw. gelöscht ist. Wie kann der Prozessor also beide Befehle unterscheiden? Die Antwort lautet: Beide Befehle sind Synonyme. Die Befehle jb und jc besitzen beide den Opcode 72h, jae und jnc den Opcode 73h. Das gleiche gilt auch für jp und jpe, die ebenfalls Synonyme sind.

Das folgende Beispiel zeigt eine einfache IF-THEN-Anweisung:

 org 100h
 start:
   mov dx,meldung1
   mov ah,9h
   int 021h
   mov ah, 01h    ;Wert über die Tastatur einlesen
   int 021h
   cmp al, '5'
   jne ende
   mov dx,meldung2
   mov ah,9h
   int 021h
 ende:
   mov ah,4Ch
   int 21h
 section .data
   meldung1: db 'Bitte geben Sie eine Zahl ein:', 13, 10, '$'
   meldung2: db 13, 10, 'Sie haben die Zahl 5 eingegeben.', 13, 10, '$'

Nachdem Sie das Programm gestartet haben, werden Sie aufgefordert, eine Zahl einzugeben. Wenn Sie die Zahl 5 eingeben, erscheint die Meldung Sie haben die Zahl 5 eingeben.

Wie Sie sehen, prüfen wir nicht Gleichheit, sondern auf Ungleichheit. Der Grund hierfür ist, dass wir die nachfolgende Codesequenz ausführen wollen, wenn der Inhalt des AL Registers 5 ist. Da wir aber nur die Möglichkeit eines bedingten Sprungs haben, müssen wir überprüfen ob AL ungleich 5 ist.

Das folgende Programm zeigt die Implementierung einer IF-THEN-ELSE-Anweisung im Assembler:

 org 100h
 start:
     mov dx,meldung1
     mov ah,9h
     int 021h
     mov ah, 01h    ;Wert über die Tastatur einlesen
     int 021h
     cmp al, '5'

     ja l1
     mov dx,meldung2
     mov ah,9h
     int 021h
     jmp ende

 l1: mov dx,meldung3
     mov ah,9h
     int 021h
 ende:
     mov ah,4Ch
     int 21h
 section .data
 meldung1: db 'Bitte geben Sie eine Zahl ein:', 13, 10, '$'
 meldung2: db 13, 10, 'Sie haben eine Zahl kleiner oder gleich 5 eingegeben', 13, 10, '$' 
 meldung3: db 13, 10, 'Sie haben eine Zahl groesser 5 eingegeben' , 13, 10, '$'

Auch hier besitzt der bedingte Sprung eine „umgekehrte Logik“. Beachten Sie den unbedingten Sprung. Er verhindert, dass auch der ELSE-Zweig des Programms ausgeführt wird und darf deshalb nicht weggelassen werden.

Schleifen Bearbeiten

Wie in jeder anderen Sprache auch hat der Assembler Schleifenbefehle. Eine mit FOR vergleichbare Schleife lässt sich mit loop realisieren. Der loop-Befehl hat die folgende Syntax:

  loop adresse

Bei der Adresse handelt es sich um eine relative Adresse, die 8 Bit groß ist. Da das Offset deshalb nur einen Wert von –128 bis +127 einnehmen darf, kann eine Schleife maximal 128 Byte Code erhalten – dies gilt auch für eine 32-Bit-CPU wie beispielsweise den Pentium IV. Umgehen lässt sich diese Beschränkung, wenn man die Schleife mit den Befehlen der bedingten Sprünge konstruiert, beispielsweise mit einem Near Jump.

Die Anzahl der Schleifendurchgänge befindet sich immer im CX-Register (daher auch der Name „Counter Register“). Bei jedem Durchgang wird der Inhalt um 1 verringert. Ist der Inhalt des CX-Registers 0 wird die Schleife verlassen und der nächste Befehl bearbeitet. Der Befehl ließe sich also auch mit der folgenden Befehlsfolge nachbilden:

dec cx
jne adresse

Wenn der Schleifenzähler um mehr als 1 verringert werden soll, kann der sub-Befehl verwendet werden. Wenn der Schleifenzähler um 2 oder 3 verringert wird, werden die meisten Assemblerprogrammierer stattdessen auf den dec-Befehl zurückgreifen, da dieser schneller als der sub-Befehl ist.

Das nachfolgende Programm berechnet 7!, die Fakultät von 7:

 org 100h
 start:
   mov cx, 7
   mov ax, 1
   mov bx, 1
 schleife:
   mul bx
   inc bx
   loop schleife
   mov ah, 4Ch
   int 21h

Mit dem Befehl mov cx, 7 wird der Schleifenzähler initialisiert. Insgesammt wird die Schleife 7-mal durchlaufen, im ersten Durchlauf mit CX=7 und BX=1, im letzten mit CX=1 und BX=7.

Das Ergebnis kann mit jedem Debugger überprüft werden (AX=13B0).

Neben den Normalen FOR-Schleifen existieren in den meisten Hochsprachen auch noch WHILE- oder „REPEAT ... UNTIL“-Schleifen. Im Assembler kann dazu die loope (loop if equal) und loopne (loop if not equal) als Befehl benutzt werden. Beide Befehle prüfen nicht nur, ob das CX-Register den Wert 0 erreicht hat, sondern zusätzlich noch das Zeroflag: Der loope durchläuft die Schleife solange, wie der Zeroflag gesetzt ist, der loopne-Befehl dagegen, solange das Zeroflag ungleich 0 ist.