FAQ der Newsgroup de.comp.lang.assembler (d.c.l.a.)

Wie wandle ich ein Integer (Byte, Word etc.) in eine Dezimalzahl (String) um?

Wenn der Inhalt eines Registers eine Zahl repräsentiert, dann spricht das Intel-Manual allgemein von 'Integer', wobei die Größe und das Vorzeichen der Zahl keine Rolle spielen. Integer sind immer eine lückenlose Folgen Nullen und Einsen (Bits), die als Binärzahl dargestellt werden können:

DezimalBinärAnmerkungen
00 0 0 0 - 0 0 0 0 Eine einzelne Binärziffer (0 oder 1) nennt man Bit.

Sofern man die Bits wie in der nebenstehenden Tabelle in einer Gruppe zu 8 Stück zusammenfasst, spricht man von einem Byte, seltener wird der Begriff Octet benutzt. Eine Gruppe von 16 Bits sind ein Word, 32 Bits ein Double Word (DWord), 64 Bits ein Quadword (QWord).

Die Einteilung in 8, 16, 32, 64 Bits hat geschichtliche Wurzeln. Die allermeisten Prozessoren verarbeiten Daten in Blöcken mit dieser Größe. Sie ist aber nicht zwingend. Die FPU z. B. verarbeitet Gleitkommazahlen intern mit einer Breite von 80 Bits.

Ein Sonderfall ist das Nibble, eine Gruppe mit 4 Bits, da es nur zur Veranschaulichung benutzt wird. In der nebenstehenden Tabelle bräuchte man zum Beispiel nur die letzten vier Bits darstellen, also ein Nibble. Das vordere Nibble ist durchgehend 0000. Nebenstehend wurde zwischen den beiden Nibbles der dargestellten Bytes zur besseren Lesbarkeit ein Bindestrich eingefügt.
10 0 0 0 - 0 0 0 1
20 0 0 0 - 0 0 1 0
30 0 0 0 - 0 0 1 1
40 0 0 0 - 0 1 0 0
50 0 0 0 - 0 1 0 1
60 0 0 0 - 0 1 1 0
70 0 0 0 - 0 1 1 1
80 0 0 0 - 1 0 0 0
90 0 0 0 - 1 0 0 1

In Strings werden die einzelnen Buchstaben durch Bytes (manchmal auch Words) dargestellt. Die Methode, wie diese Bytes zu interpretieren sind, nennt man Zeichensatztabelle. In den verschiedenen, heute gebräuchlichen Zeichensatztabellen stehen die Dezimalziffern immer an derselben Stelle. Allerdings finden sich die Buchstaben '0' bis '9' nicht am Anfang sondern weiter hinten:

BuchstabeBinärAnmerkungen
'0'0 0 1 1 - 0 0 0 0 Diese Codierung nennt man "American Standard Code for Information Interchange", kurz: ASCII. Bei Zahlen gibt es sonst keine Besonderheiten zu beachten. Stößt man auf ein System, das Strings als Aneinanderreihung von Words verlangt, klebt man einfach eine 0 vorne hin.
'1'0 0 1 1 - 0 0 0 1
'2'0 0 1 1 - 0 0 1 0
'3'0 0 1 1 - 0 0 1 1
'4'0 0 1 1 - 0 1 0 0
'5'0 0 1 1 - 0 1 0 1
'6'0 0 1 1 - 0 1 1 0
'7'0 0 1 1 - 0 1 1 1
'8'0 0 1 1 - 1 0 0 0
'9'0 0 1 1 - 1 0 0 1

Wie man sieht, unterscheiden sich die Bytes in den Tabellen nur im vorderen Nibble. Während das Integer dort eine '0000' aufweist, steht beim Buchstaben eine '0011'. Die Umwandlung einer Integerzahl von 0 bis 9 in einen Buchstaben von 0 bis 9 besteht also darin, das vordere Nibble mit '0011' zu füllen. Hierfür ist der Assembler-Befehl "OR" geeignet.

Aufgabe: Umwandlung der Zahl 5 in den Buchstaben '5'

Assembler (8 Bit):
    mov al, 5
    or  al, 00110000b

Danach steht im Register AL der ASCII-Wert für 5.

Wenn die Integerzahl größer 10 ist, kann sie nicht mehr in einen einzelnen Buchstaben umgewandelt werden. Ein oderieren des vorderen Nibbles mit '0011' führt zu einem verwirrenden Ergebnis.

Aufgabe: Umwandlung der Zahl 10 in ASCII

FALSCH:
    mov al, 10                  ; 10 dezimal = 0000-1010 binär
    or  al, 00110000b           ; ergibt 0011-1010 binär = ASCII ':'

Noch schlimmer wird es bei einer Integerzahl größer 15. Diese sprengt sogar die Nibble-Grenzen, sodass eine Manipulation des vorderen Nibble auch die eigentliche Zahl in Mitleidenschaft zieht.

Aufgabe: Umwandlung der Zahl 26 in ASCII

FALSCH:
    mov al, 26                  ; 26 dezimal = 0001-1010 binär
    or  al, 00110000b           ; ergibt 0011-1010 binär = ASCII ':'

Danach steht im Register AL der Wert 0011-1011, was wieder dem ASCII-Wert für den Doppelpunkt entspricht. Es gibt keine Möglichkeit mehr herauszufinden, ob der Doppelpunkt nun für 10, 26, 42 oder 58 steht.

Man muss also zunächst die Integerzahl in mehrere kleine Einheiten aufteilen, die dann einzeln mit 0011 oderiert werden können. Dies erreicht man durch eine oder mehrere Divisionen durch 10. Die jeweiligen Reste entsprechen dann den einzelnen Ziffern der Dezimalzahl.

29282: 10= 2928 Rest2
 2928 : 10= 292 Rest8
 292 : 10= 29 Rest2
 29 : 10= 2 Rest9
 2 : 10= 0 Rest2
 0 EndeDezimalziffern

Auf diese Weise erhält man die Dezimalziffern von rechts nach links, also in umgekehrter Reihenfolge. In Assembler benötigt man somit einen LIFO-Speicher (LIFO = Last in, first out), damit man die Ziffern später in der richtige Reihenfolge aus dem Speicher holen kann. Es bietet sich dafür ein Stackzugriff mit PUSH an, da ein POP den jeweils letzten per PUSH gespeicherten Wert holt.

Aufgabe: Isolieren und Speichern der einzelnen Ziffern der Integerzahl 29282

Assembler:

    mov ax, 29282
    mov bx, 10               ; Divisor
    xor cx, cx               ; CX=0 (Anzahl der Ziffern)
  Schleife_1:
    xor dx, dx               ; DX wird in die Division miteinbezogen
    div bx                   ; DX:AX / BX = AX Rest DX
    push dx                  ; LIFO
    add cl, 1                ; ADD soll schneller sein als INC
    or  ax, ax               ; AX = 0?
    jnz Schleife_1           ; nein: nochmal

Danach stehen im Stack die Dezimalziffern und in CX die Anzahl der Ziffern, d. h. eine Angabe, wieviel POP's notwendig sind, um alle Ziffern zu erhalten. Nun werden die Ziffern vom Stack geholt, in ASCII umgewandelt und in einen String gespeichert.

Aufgabe: Zurückholen der Ziffern in richtiger Reihenfolge, in ASCII umwandeln und speichern

Assembler:

  Schleife_2:
    pop ax                   ; gepushte Ziffern zurückholen
    or ax, 00110000b         ; Umwandlung in ASCII
    stosb                    ; Nur AL nach [ES:DI] (DI ist ein Zeiger auf den String)
    loop Schleife_2          ; bis keine Ziffern mehr da sind
    xor al, al
    stosb                    ; ASCIIZ-Abschlussnull nach [ES:DI]

DI bzw. EDI wurde irgendwann vorher mit der Startadresse eines Puffers geladen. Dort steht jetzt ein ASCIIZ-String mit der Dezimalzahl - also eine Reihe von ASCII-Buchstaben mit einer Null als Abschluss.

ACHTUNG: Der Stack ist ein empfindliches Medium, in das auch der Prozessor und das Betriebssystem wichtige Daten speichern. Es muss also auf jedes PUSH auch ein POP folgen (nicht mehr und nicht weniger), sonst stürzt das System ab. Schleife_2 darf also erst beendet werden, wenn alle in Schleife_1 gepushten Daten wieder gepopt sind.

Hier das Ganze für den 32-Bit-Flat-Modus:

    mov eax, 29282
    mov ebx, 10              ; Divisor
    xor ecx, ecx             ; ECX=0 (Anzahl der Ziffern)
  Schleife_1:
    xor edx, edx
    div ebx                  ; EDX:EAX / EBX = EAX Rest EDX
    push dx                  ; LIFO
    add cl,1                 ; ADD soll schneller sein als INC
    or  eax, eax             ; AX = 0?
    jnz Schleife_1           ; nein: nochmal
  Schleife_2:
    pop ax                   ; gepushte Ziffern zurückholen
    or al, 00110000b         ; Umwandlung in ASCII
    stosb                    ; Nur AL nach [EDI] (EDI ist ein Zeiger auf den String)
    loop Schleife_2          ; bis keine Ziffern mehr da sind
    mov byte ptr [edi], 0    ; ASCIIZ-Abschlussnull
                             ; (im Flat-Modus wird ES und DS nicht unterschieden)

Ralph 'rkhb' Bauer Aug 2008