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 demja
-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 testetjb
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.