Zum Inhalt springen

Assembler-Programmierung für x86-Prozessoren/ Rechnen mit dem Assembler

Aus Wikibooks



Die Addition

[Bearbeiten]

Fü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

[Bearbeiten]

Die 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

[Bearbeiten]

Nicht 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 Flag
  • clc (Clear Carry Flag) – löscht das Carry Flag
  • cmc (Complement Carry Flag) – dreht den Zustand des Carry Flag um

Die Befehle INC und DEC

[Bearbeiten]

Der 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

[Bearbeiten]

Wir 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

[Bearbeiten]

Die 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

[Bearbeiten]

Die 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

[Bearbeiten]

In 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

[Bearbeiten]

Schiebebefehle 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 Linksverschiebung
  • sar (Shift Arithmetic Right): arithmetische Rechtsverschiebung
  • shl (Shift Logical Left): logische Linksverschiebung
  • shr (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) 

Abb 1 – Die Schiebebefehle. Der Ausgangszustand ist in der ersten Zeile dargestellt.

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
Abb 2 – Links- und Rechtsshift mit SHLD und SHRD

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

[Bearbeiten]

Bei 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: Linksrotation
  • ror: Rechtsrotation
  • rcl: Linksrotation mit Carry Flag
  • rcr: Rechtsrotation mit Carry Flag

Die Rotationsbefehle haben die folgende Syntax:

 rol / ror /rcl /rcr Zieloperand, Zähleroperand
Abb. 3 – Die Rotationsbefehle. Der Ausgangszustand ist in der ersten Zeile dargestellt.

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.