Assembler-Programmierung für x86-Prozessoren/ Rechnen mit dem Assembler
Die Addition
BearbeitenFür die Addition stehen zwei Befehle zu Verfügung: add
und adc
. Der adc
(Add with Carry) berücksichtigt das Carry Flag. Wir werden weiter unten noch genauer auf die Unterschiede eingehen.
Die Syntax von add
und adc
ist identisch:
add Zieloperand, Quelloperand
adc Zieloperand, Quelloperand
Beide Befehle addieren den Quell- und Zieloperanden und speichern das Resultat im Zieloperanden ab. Ziel- und Quelloperand können entweder ein Register oder eine Speicherstelle sein (natürlich darf nur entweder der Ziel- oder der Quelloperand eine Speicherstelle sein, niemals aber beide zugleich).
Das folgende Programm addiert zwei Zahlen miteinander und speichert das Ergebnis in einer Speicherstelle ab:
org 100h
start:
mov bx, 500h
add bx, [summand1]
mov [ergebnis], bx
mov ah, 4Ch
int 21h
section .data
summand1 DW 900h
ergebnis DW 0h
Unter Linux gibt es dabei - bis auf die differierenden Interrupts und die erweiterten Register - kaum Unterschiede:
section .text
global _start
_start:
mov ebx,500h
add ebx,[summand1]
mov [ergebnis],ebx
; Programm ordnungsgemäß beenden
mov eax,1
mov ebx,0
int 80h
section .data
summand1 dd 900h
ergebnis dd 0h
Es fällt auf, dass summand1
und ergebnis
in eckigen Klammern geschrieben sind. Der Grund hierfür ist, dass wir nicht die Adresse benötigen, sondern den Inhalt der Speicherzelle.
Fehlen die eckigen Klammern, interpretiert der Assembler das Label summand1
und ergebnis
als Adresse. Im Falle des add
-Befehls würde das BX Register folglich mit der Adresse von summand1
addiert. Beim mov
-Befehl hingegen würde dies der Assembler mit einer Fehlermeldung quittieren, da es nicht möglich ist, den Inhalt des BX Registers in eine Adresse zu kopieren und es folglich keinen Opcode gibt.
Wir verfolgen nun wieder mit Hilfe des Debuggers die Arbeit unseres Programms:
-r AX=0000 BX=0000 CX=0014 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0100 NV UP EI PL NZ NA PO NC 0CDC:0100 BB0005 MOV BX,0500 -t AX=0000 BX=0500 CX=0014 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0103 NV UP EI PL NZ NA PO NC 0CDC:0103 031E1001 ADD BX,[0110] DS:0110=0900
Wir sehen an DS:0110=0900, dass ein Zugriff auf die Speicherstelle 0110 im Datensegment erfolgt ist. Wie wir außerdem erkennen können, ist der Inhalt der Speicherzelle 0900.
Abschließend speichern wir das Ergebnis unserer Berechnung wieder in den Arbeitsspeicher zurück:
AX=0000 BX=0E00 CX=0014 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0107 NV UP EI PL NZ NA PE NC 0CDC:0107 891E1201 MOV [0112],BX DS:0112=0000 -t
Wir lassen uns nun die Speicherstelle 0112, in der das Ergebnis gespeichert ist, über den d
-Befehl ausgeben:
-d ds:0112 L2 0CDC:0110 00 0E
Die Ausgabe überrascht ein wenig, denn der Inhalt der Speicherstelle 0112 unterscheidet sich vom (richtigen) Ergebnis 0E00 im BX Register. Der Grund hierfür ist eine Eigenart der 80x86 CPU: Es werden High und Low Byte vertauscht. Als High Byte bezeichnet man die höherwertige Hälfte, als Low Byte die niederwertige Hälfte eines 16 Bit Wortes. Um das richtige Ergebnis zu erhalten, muss entsprechend wieder Low und High Byte vertauscht werden.
Unter Linux sieht der Debugging-Ablauf folgendermaßen aus:
(gdb) break 6 Breakpoint 1 at 0x8048085: file add.asm, line 6. (gdb) run Starting program: /(...)/(...)/add Breakpoint 1, _start () at add.asm:6 6 add ebx,[summand1] (gdb) info registers eax 0x0 0 ecx 0x0 0 edx 0x0 0 ebx 0x500 1280 esp 0xbffe0df0 0xbffe0df0 ebp 0x0 0x0 esi 0x0 0 edi 0x0 0 eip 0x8048085 0x8048085 <_start+5> eflags 0x200392 [ AF SF TF IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x0 0 (gdb) stepi _start () at add.asm:7 7 mov [ergebnis],ebx (gdb) info registers eax 0x0 0 ecx 0x0 0 edx 0x0 0 ebx 0xe00 3584 esp 0xbffe0df0 0xbffe0df0 ebp 0x0 0x0 esi 0x0 0 edi 0x0 0 eip 0x804808b 0x804808b <_start+11> eflags 0x200306 [ PF TF IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x0 0
Wie Sie sehen, lässt sich der GDB etwas anders bedienen. Nachdem wir einen Breakpoint auf die Zeile 6 gesetzt haben (Achtung: Das bedeutet, dass die sechste Zeile nicht mehr mit ausgeführt wird!), führen wir das Programm aus und lassen uns den Inhalt der Register ausgeben. Das ebx-Register enthält die Zahl 500h (dezimal 1280), die wir mit dem mov-Befehl hineingeschoben haben. Mit dem GDB-Kommando stepi rücken wir in die nächste Zeile (die den add-Befehl enthält). Erneut lassen wir uns die Register auflisten. Das ebx-Register enthält nun die Summe der Addition: 0xe00 (dezimal 3584).
Im Folgenden wollen wir eine 32-Bit-Zahl addieren. Bei einer 32-Bit-Zahl vergrößert sich der darstellbare Bereich für vorzeichenlose Zahlen von 0 bis 65.535 auf 0 bis 4.294.967.295 und für vorzeichenbehafteten Zahlen von –32.768 bis +32.767 auf –2.147.483.648 bis +2.147.483.647. Die einfachste Möglichkeit bestünde darin, ein 32-Bit-Register zu benutzen, was ab der 80386-CPU problemlos möglich ist. Wir wollen hier allerdings die Verwendung des adc
-Befehls zeigen, weshalb wir davon nicht Gebrauch machen werden.
Für unser Beispiel benutzen wir die Zahlen 188.866 und 103.644 (Dezimal). Wir schauen uns die Rechnung im Dualsystem an:
10 11100001 11000010 (188.866) + 1 10010100 11011100 (103.644) Ü 11 11 1 --------------------------------- 100 01110110 10011110 (292.510)
Wie man an der Rechnung erkennt, findet vom 15ten nach dem 16ten Bit ein Übertrag statt (fett hervorgehoben). Der Additionsbefehl, der die oberen 16 Bit addiert, muss dies berücksichtigen.
Die Frage, die damit aufgeworfen wird ist, wie wird der zweite Additionsbefehl davon in Kenntnis gesetzt, dass ein Übertrag stattgefunden hat oder nicht. Die Antwort lautet: Der erste Additionsbefehl, der die unteren 16 Bit addiert, setzt das Carry Flag, wenn ein Übertrag stattfindet, andernfalls löscht er es. Der zweite Additionsbefehl, der die oberen 16 Bit addiert, muss nun eins hinzuaddieren, wenn das Carry Flag gesetzt ist. Genau dies tut der adc
-Befehl (im Gegensatz zum add
-Befehl) auch.
Das folgende Programm addiert zwei 32-Bit-Zahlen miteinander:
org 100h
start:
mov ax, [summand1]
add ax, [summand2]
mov [ergebnis], ax
mov ax, [summand1+2]
adc ax, [summand2+2]
mov [ergebnis+2], ax
mov ah, 4Ch
int 21h
section .data
summand1 DD 2E1C2h
summand2 DD 194DCh
ergebnis DD 0h
Die ersten drei Befehle entsprechen unserem ersten Programm. Dort werden Bit 0 bis 15 addiert. Der add
-Befehl setzt außerdem das Carry Flag, wenn es einen Übertrag von der 15ten auf die 16te Stelle gab (was hier der Fall ist, wie wir auch gleich mit Hilfe des Debuggers nachprüfen werden).
Mit den nächsten drei Befehlen werden Bit 16 bis 31 addiert. Deshalb müssen wir dort zwei Byte zur Adresse hinzuaddieren und außerdem mit dem adc
-Befehl das Carry Flag berücksichtigen.
Wir sehen uns das Programm wieder mit dem Debugger an. Dabei sehen wir, dass der add
-Befehl das Carry Flag setzt:
AX=E1C2 BX=0000 CX=0024 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0103 NV UP EI PL NZ NA PO NC 0CDC:0103 03061C01 ADD AX,[011C] DS:011C=94DC -t AX=769E BX=0000 CX=0024 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0CDC ES=0CDC SS=0CDC CS=0CDC IP=0107 OV UP EI PL NZ NA PO CY 0CDC:0107 A32001 MOV [0120],AX DS:0120=0000
Mit NC zeigt der Debugger an, dass das Carry Flag nicht gesetzt ist, mit CY, dass es gesetzt ist. Das Carry Flag ist allerdings nicht das einzige Flag, das bei der Addition beeinflusst wird:
- Das Zero Flag ist gesetzt, wenn das Ergebnis 0 ist.
- Das Sign Flag ist gesetzt, wenn das Most Significant Bit den Wert 1 hat. Dies kann auch der Fall sein, wenn nicht mit vorzeichenbehafteten Zahlen gerechnet wird. In diesem Fall kann das Sign Flag ignoriert werden.
- Das Parity Bit ist gesetzt, wenn das Ergebnis eine gerade Anzahl von Bits erhält. Es dient dazu, Übertragungsfehler festzustellen. Wir gehen hier aber nicht näher darauf ein, da es nur noch selten benutzt wird.
- Das Auxiliary Carry Flag entspricht dem Carry Flag, wird allerdings benutzt, wenn mit BCD-Zahlen gerechnet werden soll.
- Das Overflow Flag wird gesetzt, wenn eine negative Zahl nicht mehr darstellbar ist, weil das Ergebnis zu groß geworden ist.
Subtraktion
BearbeitenDie Syntax von sub
(subtract) und sbb
(subtract with borrow) ist äquivalent mit dem add/adc
-Befehl:
sub/sbb Zieloperand, Quelloperand
Bei der Subtraktion muss die Reihenfolge von Ziel- und Quelloperand beachtet werden, da die Subtraktion im Gegensatz zur Addition nicht kommutativ ist. Der sub/sbb
-Befehl zieht vom Zieloperanden den Quelloperanden ab (Ziel = Ziel – Quelle):
Beispiel: 70 – 50 = 20
Diese Subtraktion kann durch die folgenden zwei Anweisungen in Assembler dargestellt werden:
mov ax,70h
sub ax,50h
Wie bei der Addition können sich auch bei der Subtraktion die Operanden in zwei oder mehr Registern befinden. Auch hier wird das Carry Flag verwendet. So verwundert es nicht, dass die Subtraktion einer 32-Bit-Zahl bei Verwendung von 16-Bit-Register fast genauso aussieht wie das entsprechende Additionsprogramm:
org 100h
start:
mov ax, [zahl1]
sub ax, [zahl2]
mov [ergebnis], ax
mov ax, [zahl1+2]
sbb ax, [zahl2+2]
mov [ergebnis+2], ax
mov ah, 4Ch
int 21h
section .data
zahl1 DD 70000h
zahl2 DD 50000h
ergebnis DD 0h
Der einzige Unterschied zum entsprechenden Additionsprogramm besteht tatsächlich darin, dass anstelle des add
-Befehls der sub
-Befehl und anstelle des adc
- der sbb
-Befehl verwendet wurde.
Wie beim add
- und adc
-Befehl werden die Flags Zero, Sign, Parity, Auxiliary Carry und Carry gesetzt.
Setzen und Löschen des Carryflags
BearbeitenNicht nur die CPU sondern auch der Programmierer kann das Carry Flag beeinflussen. Dazu existieren die folgenden drei Befehle:
stc
(Set Carry Flag) – setzt das Carry Flagclc
(Clear Carry Flag) – löscht das Carry Flagcmc
(Complement Carry Flag) – dreht den Zustand des Carry Flag um
Die Befehle INC und DEC
BearbeitenDer Befehl inc
erhöht den Operanden um eins, der Befehl dec
verringert den Operanden um eins. Die Befehle haben die folgende Syntax:
inc Operand
dec Operand
Der Operand kann entweder eine Speicherstelle oder ein Register sein. Beispielsweise wird über den Befehl
dec ax
der Inhalt des AX Registers um eins verringert. Der Befehl bewirkt damit im Grunde das Gleiche wie der Befehl sub ax, 1
. Es gibt lediglich einen Unterschied: inc
und dec
beeinflussen nicht das Carry Flag.
Zweierkomplement bilden
BearbeitenWir haben bereits mit dem Zweierkomplement gerechnet. Doch wie wird dieses zur Laufzeit gebildet? Die Intel-CPU hält einen speziellen Befehl dafür bereit, den neg
-Befehl. Er hat die folgende Syntax:
neg Operand
Der Operand kann entweder eine Speicherzelle oder ein allgemeines Register sein. Um das Zweierkomplement zu erhalten, zieht der neg
-Befehl den Operanden von 0 ab. Entsprechend wird das Carryflag gesetzt, wenn der Operand nicht null ist.
Die Multiplikation
BearbeitenDie Befehle mul
(Multiply unsigned) und imul
(Integer Multiply) sind für die Multiplikation zweier Zahlen zuständig. Mit mul
werden vorzeichenlose Ganzzahlen multipliziert, wohingegen mit imul
vorzeichenbehaftete Ganzzahlen multipliziert werden. Der mul
-Befehl besitzt nur einen Operanden:
mul Quelloperand
Der Zieloperand ist sowohl beim mul
- wie beim imul
-Befehl immer das AL- oder AX-Register. Der Quelloperand kann entweder ein allgemeines Register oder eine Speicherstelle sein.
Da bei einer 8-Bit-Multiplikation meistens eine 16-Bit-Zahl das Ergebnis ist (z. B.: 20 * 30 = 600) und bei einer 16-Bit-Multiplikation meistens ein 32-Bit-Ergebnis entsteht, wird das Ergebnis bei einer 8-Bit-Multiplikation immer im AX-Register gespeichert, bei einer 16-Bit-Multiplikation in den Registern DX:AX. Der höherwertige Teil wird dabei vom DX-Register aufgenommen, der niederwertige Teil vom AX-Register.
Beispiel:
org 100h
start:
mov ax, 350h
mul word[zahl]
mov ah, 4Ch
int 21h
section .data
zahl DW 750h
ergebnis DD 0h
Für Linux:
section .text
global _start
_start:
mov eax, 350h
mov ebx, 750h
mul ebx ; multiply eax with ebx, put the breakpoint here
; EXIT
mov eax, 1
mov ebx, 1
int 80h
Wie Sie sehen, besitzt der Zeiger zahl
das Präfix word
. Dies benötigt der Assembler, da sich der Opcode für den mul
-Befehl unterscheidet, je nachdem, ob es sich beim Quelloperand um einen 8-Bit- oder einen 16-Bit-Operand handelt. Wenn der Zieloperand 8 Bit groß ist, dann muss der Assembler den Befehl in Opcode F6h übersetzen, ist der Zieloperand dagegen 16 Bit groß, muss der Assembler den Befehl in den Opcode F7h übersetzen.
Ist der höherwertige Anteil des Ergebnisses 0, so werden Carry Flag und Overflow Flag gelöscht, ansonsten werden sie auf 1 gesetzt. Das Sign Flag, Zero Flag, Auxilary Flag und Parity Flag werden nicht verändert.
In der Literatur wird erstaunlicherweise oft „vergessen“, dass der imul
-Befehl im Gegensatz zum mul
-Befehl in drei Varianten existiert. Vielleicht liegt dies daran, dass diese Erweiterung erst mit der 80186-CPU eingeführt wurde (und stellt daher eine der wenigen Neuerungen der 80186-CPU dar). Aber wer programmiert heute noch für den 8086/8088?
Die erste Variante des imul
-Befehls entspricht der Syntax des mul
-Befehls:
imul Quelloperand
Eine Speicherstelle oder ein allgemeines Register wird entweder mit dem AX-Register oder dem Registerpaar DX:AX multipliziert.
Die zweite Variante des imul
-Befehls besitzt zwei Operanden und hat die folgende Syntax:
imul Zieloperand, Quelloperand
Der Zieloperand wird mit dem Quelloperand multipliziert und das Ergebnis im Zieloperanden abgelegt. Der Zieloperand muss ein allgemeines Register sein, der Quelloperand kann entweder ein Wert, ein allgemeines Register oder eine Speicherstelle sein.
Die dritte Variante des imul
-Befehls besitzt drei(!) Operanden. Damit widerlegt der Befehl die häufig geäußerte Aussage, dass die Befehle der Intel-80x86-Plattform entweder keinen, einen oder zwei Operanden besitzen können:
imul Zieloperand, Quelloperand1, Quelloperand2
Bei dieser Variante wird der erste Quelloperand mit dem zweiten Quelloperanden multipliziert und das Ergebnis im Zieloperanden gespeichert. Der Zieloperand und der erste Quelloperand müssen entweder ein allgemeines Register oder eine Speicherstelle sein, der zweite Quelloperand dagegen muss ein Wert sein.
Bitte beachten Sie, dass der DOS-Debugger nichts mit dem Opcode der zweiten und dritten Variante anfangen kann, da er nur Befehle des 8086/88er-Prozessors versteht. Weiterhin sollten Sie beachten, dass diese Varianten nur für den imul
-Befehl existieren – also auch nicht für den idiv
-Befehl.
Die Division
BearbeitenDie Befehle div
(Unsigned Divide) und idiv
(Integer Division) sind für die Division zuständig. Die Syntax der beiden Befehle entspricht der von mul
und imul
:
div Quelloperand
idiv Quelloperand
Der Zieloperand wird dabei durch den Quelloperand geteilt. Der Quelloperand muss entweder eine Speicherstelle oder ein allgemeines Register sein. Der Zieloperand befindet sich immer im AX-Register oder im DX:AX-Register. In diesen Registern wird auch das Ergebnis gespeichert: Bei einer 8-Bit-Division befindet sich das Ergebnis im AL-Register und der Rest im AH-Register, bei der 16-Bit-Division befindet sich das Ergebnis im AX-Register und der Rest im DX-Register. Da die Division nicht kommutativ ist, dürfen die Operanden nicht vertauscht werden.
Beispiel:
org 100h
start:
mov dx,0010h
mov ax,0A00h
mov cx,0100h
div cx ; DX:AX / CX
mov ah, 4Ch
int 21h
Das Programm teilt das DX:AX-Register durch das CX-Register und legt das Ergebnis im AX-Register ab.
Da das Ergebnis nur im 16 Bit breiten AX-Register gespeichert wird, kann es passieren, dass ein Überlauf stattfindet, weil das Ergebnis nicht mehr in das Register passt. Einen Überlauf erzeugt beispielsweise der folgende Programmausschnitt:
mov dx, 5000h
mov ax, 0h
mov cx, 2h
div cx
Das Ergebnis ist größer als 65535. Wenn die CPU auf einen solchen Überlauf stößt, löst sie eine Divide Error Exception aus. Dies führt dazu, dass der Interrupt 0 aufgerufen und eine Routine des Betriebssystems ausgeführt wird. Diese gibt die Fehlermeldung „Überlauf bei Division“ aus und beendet das Programm. Auch die Divison durch 0 führt zum Aufruf der Divide Error Exception.
Wenn der Prozessor bei einem Divisionsüberlauf und bei einer Division durch 0 eine Exception auslöst, welche Bedeutung haben dann die Statusflags? Die Antwort lautet, dass sie bei der Division tatsächlich keine Rolle spielen, da sie keinen definierten Zustand annehmen.
Logische Operationen
BearbeitenIn Allgemeinbildungsquiz findet man manchmal die folgende Frage: „Was ist die kleinste adressierbare Einheit eines Computers?“ Überlegen Sie einen Augenblick. Wenn Sie nun mit Bit geantwortet haben, so liegen Sie leider falsch. Der Assembler bietet tatsächlich keine Möglichkeit, ein einzelnes Bit im Arbeitsspeicher zu manipulieren. Die richtige Antwort ist deshalb, dass ein Byte die kleinste adressierbare Einheit eines Computers ist. Dies gilt im Großen und Ganzen auch für die Register. Nur das Flag-Register bildet hier eine Ausnahme, da hier einzelne Bits mit Befehlen wie sti
oder clc
gesetzt und zurückgesetzt werden können.
Um dennoch einzelne Bits anzusprechen, muss der Programmierer deshalb den Umweg über logische Operationen wie AND, OR, XOR und NOT gehen. Wie die meisten höheren Programmiersprachen sind auch dem Assembler diese Befehle bekannt.
Die nachfolgende Tabelle zeigt nochmals die Wahrheitstabelle der logischen Grundverknüpfungen:
A | B | AND | OR | XOR |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
Ein kleines Beispiel zur Verdeutlichung:
10110011 AND 01010001 ------------ 00010001
Die logischen Verknüpfungen haben die folgende Syntax:
and Zieloperand, Quelloperand
or Zieloperand, Quelloperand
xor Zieloperand, Quelloperand
Das Ergebnis der Verknüpfung wird jeweils im Zieloperanden gespeichert. Der Quelloperand kann ein Register, eine Speicherstelle oder ein Wert sein, der Zieloperand kann ein Register oder eine Speicherstelle sein (wie immer dürfen nicht sowohl Ziel- wie auch Quelloperand eine Speicherstelle sein).
Das Carry und Overflow Flag werden gelöscht, das Vorzeichen Flag, das Null Flag und das Parity Flag werden in Abhängigkeit des Ergebnisses gesetzt. Das Auxiliary Flag hat einen undefinierten Zustand.
Die NOT-Verknüpfung dreht den Wahrheitswert der Bits einfach um. Dies entspricht dem Einerkomplement einer Zahl. Der not
-Befehl hat deshalb die folgende Syntax:
not Zieloperand
Der Zieloperand kann eine Speicherstelle oder ein Register sein. Die Flags werden nicht verändert.
In vielen Assemblerprogrammen sieht man übrigens häufig eine Zeile wie die folgende:
xor ax, ax
Da hier Quell- und Zielregister identisch sind, können die Bits nur entweder den Wert 0, 0 oder 1, 1 besitzen. Wie aus der Wahrheitstabelle ersichtlich, ist das Ergebnis in beiden Fällen 0. Der Befehl entspricht damit
mov ax, 0
Der mov-Befehl ist drei Byte lang, während die Variante mit dem xor-Befehl nur zwei Byte Befehlscode benötigt.
Schiebebefehle
BearbeitenSchiebebefehle verschieben den Inhalt eines Registers oder einer Speicherstelle bitweise. Mit den Schiebebefehlen lässt sich eine Zahl mit 2n multiplizieren bzw. dividieren. Dies geschieht allerdings wesentlich schneller und damit effizienter als mit den Befehlen mul
und div
.
Der 8086 kennt vier verschiedene Schiebeoperationen: Links- und Rechtsverschiebungen sowie arithmetische und logische Schiebeoperationen. Bei arithmetischen Schiebeoperationen wird das Vorzeichen mit berücksichtigt:
sal
(Shift Arithmetic Left): arithmetische Linksverschiebungsar
(Shift Arithmetic Right): arithmetische Rechtsverschiebungshl
(Shift Logical Left): logische Linksverschiebungshr
(Shift Logical Right): logische Rechtsverschiebung
Die Schiebebefehle haben immer die folgende Syntax:
sal / sar / shl / shr Zieloperand, Zähloperand
Der Zieloperand gibt an, welcher Wert geschoben werden soll. Der Zähloperand gibt an, wie oft geschoben werden soll. Der Zieloperand kann eine Speicherstelle oder ein Register sein. Der Zähloperand kann ein Wert oder das CL-Register sein.
Da der Zähloperand 8 Bit groß ist, kann er damit Werte zwischen 0 und 255 annehmen. Da ein Register allerdings maximal 32 Bit breit sein kann, sind nur Werte von 1 bis 31 sinnvoll. Alles was darüber ist, würde bedeuten, dass sämtliche Bits an den Enden hinausgeschoben werden. Der 8086-CPU war dies noch egal: Sie verschob auch Werte die größer als 31 sind, was allerdings entsprechend lange Ausführungszeiten nach sich ziehen konnte. Ab der 80286 werden deshalb nur noch die ersten 5 Bit beachtet, so dass maximal 31 Verschiebungen durchgeführt werden.
Das zuletzt herausgeschobene Bit geht zunächst einmal nicht verloren: Vielmehr wird es zunächst als Carry Flag gespeichert. Bei der logischen Verschiebung wird eine 0 nachgeschoben, bei der arithmetischen Verschiebung das Most Significant Bit.
Dies erscheint zunächst nicht schlüssig, daher wollen wir und die arithmetische und logischen Verschiebung an der Zahl –10 klar anschauen. Wir wandeln die Zahl zunächst in die Binärdarstellung um und bilden dann das Zweierkomplement:
00001010 (10) 11110101 (Einerkomplement) 11110110 (Zweierkomplement)
Abbildung 1 zeigt nun eine logische und eine arithmetische Rechtsverschiebung. Bei der logischen Rechtsverschiebung geht das Vorzeichen verloren, da eine 0 nachgeschoben wird. Für eine negative Zahl muss deshalb eine arithmetische Verschiebung erfolgen, bei dem immer das Most Significant Bit erhalten bleibt (Abbildung 1a). Da in unserem Fall das Most Significant Bit eine 1 ist, wird eine 1 nachgeschoben (Abbildung 1b). Ist das Most Significant Bit hingegen eine 0, so wird eine 0 nachgeschoben.
Sehen wir uns nun die Linksverschiebung an: Wir gehen wieder von der Zahl –10 aus. Abbildung 1c veranschaulicht arithmetische und logische Linksverschiebung. Wie Sie erkennen können, haben logische und arithmetische Verschiebung keine Auswirkung auf das Vorzeichenbit. Aus diesem Grund gibt es auch zwischen SHL und SAL keinen Unterschied! Beide Befehle sind identisch!
Wie wir mit dem Debugger nachprüfen können, haben beide den selben Opcode:
…
sal al,1
shl al,1
…
Beim Debugger erhält man:
-r AX=0000 BX=0000 CX=0008 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0D36 ES=0D36 SS=0D36 CS=0D36 IP=0100 NV UP EI PL NZ NA PO NC 0D36:0100 D0E0 SHL AL,1 -t
AX=0000 BX=0000 CX=0008 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=0D36 ES=0D36 SS=0D36 CS=0D36 IP=0102 NV UP EI PL ZR NA PE NC 0D36:0102 D0E0 SHL AL,1
Seit der 80386-CPU gibt es auch die Möglichkeit den Inhalt von zwei Registern zu verschieben (Abbildung 2). Die Befehle SHLD und SHRD haben die folgende Syntax:
shld / shrd Zieloperand, Quelloperand, Zähleroperand
Der Zieloperand muss bei beiden Befehlen entweder ein 16- oder 32-Bit-Wert sein, der sich in einer Speicherstelle oder einem Register befindet. Der Quelloperand muss ein 16- oder 32-Bit-Wert sein und darf sich nur in einem Register befinden. Der Zähloperand muss ein Wert oder das CL-Register sein.
Rotationsbefehle
BearbeitenBei den Rotationsbefehlen werden die Bits eines Registers ebenfalls verschoben, fallen aber nicht wie bei den Schiebebefehlen an einem Ende heraus, sondern werden am anderen Ende wieder hinein geschoben. Es existieren vier Rotationsbefehle:
rol
: Linksrotationror
: Rechtsrotationrcl
: Linksrotation mit Carry Flagrcr
: Rechtsrotation mit Carry Flag
Die Rotationsbefehle haben die folgende Syntax:
rol / ror /rcl /rcr Zieloperand, Zähleroperand
Der Zähleroperand gibt an, um wie viele Bits der Zieloperand verschoben werden soll. Der Zieloperand kann entweder ein allgemeines Register oder eine Speicherstelle sein. Der Zähleroperand kann ein Wert sein oder das CL-Register.
Bei der Rotation mit rol
und ror
wird das Most bzw. Least Significant Bit, das an das andere Ende der Speicherstelle oder des Registers verschoben wird, im Carry Flag abgelegt (siehe Abbildung 3).
Die Befehle rcl
und rcr
nutzen das Carry Flag für die Verschiebung mit: Bei jeder Verschiebung mit dem rcl
-Befehl wird das Most Significant Bit zunächst in das Carry Bit verschoben und der Inhalt des Carry Flags in das Least Significant Bit. Beim rcr
ist es genau andersherum: Hier wird das Least Significant Bit zunächst in das Carry Flag geschoben und dessen Inhalt in das Most Significant Bit (siehe Abbildung 2).
Wie bei den Schiebebefehlen berücksichtigt der Prozessor (ab 80286) lediglich die oberen 5 Bit des CL-Registers und ermöglicht damit nur eine Verschiebung zwischen 0 und 31.