Wyświetlanie liczb

Autor: Wojciech Muła
Dodany:3.05.2002
Aktualizacja:30.04.2008

Treść

1. Wyświetlanie liczb w systemie dwójkowym

Poniższe programy zamieniają liczbę zapisaną w rejestrze AL, na reprezentację binarną w łańcuchu ASCII-Z.

1.1. Kod x86

; wejście: al
; wyjście: _string (zakończony zerem)

segment .data

_string db "????????", 0

segment .text

        mov cx, 8       ; 8-bitów do przetworzenia
        mov di, _string ; offset łańcucha
_conv:
        mov bl, 0       ; bl = 0
        rol al, 1       ; CF = MSB(al)
        adc bl, '0'     ; bl = ascii(CF)

        mov [di], bl    ; [di++] = bl
        inc di          ;
        loop _conv

1.2. Kod z wykorzystaniem MMX

; wejście: al
; wyjście: mm0

segment .data

mmx_bits db 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01 ; maska dla kolejnych bitów
mmx_asc0 db 0x30,0x30,0x30,0x30,0x30,0x30,0x30,0x30 ; 0x30 = ascii('0')

_string  db "????????",0

segment .text

                          ; np. a = 0xe9 (11101001b)
movd      mm1, eax        ; mm1 = |00|00|00|00|xx|xx|xx|e9|
punpcklbw mm1, mm1        ; mm1 = |00|00|00|00|xx|xx|e9|e9|
punpcklbw mm1, mm1        ; mm1 = |xx|xx|xx|xx|e9|e9|e9|e9|
punpcklbw mm1, mm1        ; mm1 = |e9|e9|e9|e9|e9|e9|e9|e9|

                          ; zostaw pojedyncze bity w każdym bajcie
pand      mm1, [mmx_bits] ; mm1 = |01|00|00|01|00|01|01|01|
pcmpeqb   mm1, [mmx_bits] ; mm1 = |ff|00|00|ff|00|ff|ff|ff|

movq      mm0, [mmx_asc0] ; mm0 = |30|30|30|30|30|30|30|30|
psubb     mm0, mm1        ; mm0 = |31|30|30|31|30|31|31|31| = "11101001"

movq      [_string], mm0

2. Wyświetlanie liczb w systemie dziesiętnym

By wyświetlić liczbę, należy odczytać poszczególne cyfry. Najprościej jest pobrać najmłodszą cyfrę — jest to reszta z dzielenia przez 10 (ogólnie: przez podstawę systemu). Z kolei wynikiem dzielenia liczby przez wagę najstarszej cyfry jest właśnie najstarsza cyfra. Generalnie, odczyt dowolnej cyfry przedstawia się następująco.

// x         - liczba                    (np. 10506)
// digit_pos - pozycja cyfry (>0)        (        2)
// base      - podstawa systemu liczenia (       10)
int get_digit(int x, int digit_pos, int base=10) {
                           // x = 10506
 x /= pow(base, digit_pos) // x = 105
 x %= base                 // x = 5
 return x;
}

2.1. Pobieranie najmłodszej cyfry

Wadą tego rozwiązania jest uzyskiwanie cyfr w odwrotnej kolejności (od najmłodszej do najstarszej), ale można to rozwiązać na co najmniej dwa sposoby:

  • odwracając wynikowy łańcuch;
  • zapisując od końca łańcucha — trzeba znać maksymalną ilość cyfr.

2.1.1. Odwracanie łańcucha

Zwykłe odwracanie łańcucha jest oczywiście dość proste, ale ja proponuję inne podejście. Otóż można użyć stosu do niejawnego odwrócenia łańcucha, a właściwie do odwrócenia kolejności zapisów do łańcucha.

void display_uint(unsigned int x) {
 if (!x) return
 display_uint(x/10);
 // wyświetlenie cyfry
 putchar(x % 10 + '0');
}

Przy rekursji stos jest wykorzystywany zupełnie naturalnie, ale można również bezpośrednio użyć struktury stosowej.

void push(char);
char pop();
int  stack_empty();

void display_uint(unsigned int x)
{
 do {
     push(x%10 + '0');
     x /= 10;
 }
 while (x)

 while (!stack_empty())
   putchar(pop());
}

W asemblerze użycie powyższego sposobu jest nadzwyczaj proste, ponieważ procesor wspiera operacje na stosie poprzez instrukcje push i pop.

; edi - wskaźnik na łańcuch - minimum 11 bajtów
; eax - zamieniana wartość (bez znaku!)
dword2dec_ascii:

pushad

mov ecx, 10

mov ebp, esp                            ; zapisz wierzchołek stosu
.conv:
        xor     edx, edx                ; edx = eax % 10
        div     ecx                     ;

        lea     ebx, [edx + '0']        ; ebx = ASCII(edx)
        push    bx

        or      eax, eax                ; do ... while (eax)
        jnz     .conv                   ; /

cld
.create_string:
        cmp     ebp, esp                ; while(!stack_empty()) - dopóki
        je      .end                    ; oryginalna zawartość stosu
                                        ; nie zostanie odtworzona

        pop     ax                      ; stos jest opróżniany
        stosb                           ; a zapisane na nim uprzednio litery
                                        ; zapisywane do łańcucha [edi]
        jmp     .create_string
.end:

mov [edi], byte 0  ; zapisz kończący symbol
popad
ret

2.1.2. Liczby o znanej ilości cyfr

W tym przypadku łańcuch wypełnia się od końca, a funkcja zwraca wskaźnik do ostatnio zapisanego znaku.

#define size 20 /* ilość cyfr */

char string[size+1] = {0};

char* uint2ascii(unsigned int x)
{
 char *c = &string[size-1]; // przedostatni znak

 do
   {
   *c-- = x % 10 + '0';
    x  /= 10;
   }
 while (x)

 return c;
}

W asemblerze kod jest równie prosty, a nawet bardziej, bowiem rozkaz div zarówno dzieli jak i liczy resztę. W C zdefiniowano funkcję div, która działa analogicznie do rozkazu div.

; esi = c > string[size]
; eax = x
uint2ascii:
        mov ebx, 10
  .conv:
        div   eax, ebx ; eax = eax/10
                       ; edx = eax%10
        add   dl , '0' ;  dl = ASCII(dl)
        mov [esi], dl
        dec  esi

        test  eax, eax ; if (x != 0) goto .conv
        jnz   .conv
        ret

2.2. Pobieranie najstarszej cyfry

Jest to czasochłonna metoda, jednak dość ciekawa. Za darmo można uzyskać zera wiodące (ang. leading zeros), co być może w niektórych zastosowaniach się przyda, aczkolwiek wymaga większej liczby obliczeń.

void display_uint(unsigned int x)
{                    // 4294967295 -- maksymalna wartość dword'a
 unsigned int weight =  1000000000;

 do {
     putchar(x/weight + '0'); // wyświetlenie najstarszej cyfry

     x      %= weight;        // "wycięcie" najstarszej cyfry
     weight /= 10;            // następna cyfra
    }
 while (x);
}

3. Wyświetlanie liczb w systemie szesnastkowym

Funkcje konwertujące liczby na system szesnastkowy.

3.1. Tablicowanie (kod x86)

Najprostszym sposobem jest tablicowanie cyfr szesnastkowych:

segment .data

hax_digits db "0123456789abcdef"
string     db "????????", 0      ; null terminated string

segment .text

dword2hex:
; eax - liczba do wyświetlania

mov edi, string+8 ; offset ostatniego znaku

mov ecx, 8        ; ilość tetrad (ang. nibbles)
                  ; tetrada to 4 kolejne bity, czyli tyle ile wymaga do
                  ; zapisania 1 cyfra szesnastkowa

xor ebx, ebx
_conv:
        mov bl, al               ; eax = |hg|fe|dc|ba|
        and bl, 0x0f             ; ebx = |00|00|00|0a|

        mov bl, [hex_digits+ebx] ; bl = 'a';

        mov [edi], bl            ; zapisz znak
        dec edi

        shr eax, 4               ; eax = |0h|gf|ed|cb| -- następna tetrada
        loop _conv

3.2. Bezpośrednie obliczenia

Można obyć się bez tablicy; proszę przyjrzeć się kodom ASCII poszczególnych liter:

0 1 2 3 4 5 6 7 8 9 a b c d e f
0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x61 0x62 0x63 0x64 0x65 0x66

Wybór litery jest prosty:

  • Jeśli tetrada t jest w zakresie 0..9 to kod litery uzyskuje się dodając 0x30 do t.
  • Jeśli tetrada jest większa od 9 to kod litery uzyskuje się prawie tak samo łatwo: t-10 + 0x61.
char hex_digit(unsigned int nibble)
{
 if (nibble > 0xa) return = nibble + '0';
              else return = (nibble-10) + 'a';
}

3.2.1. Kod x86 (bez rozgałęzień!)

;  2-02-02

; wejście: al - tetrada
; wyjście: bl - bl='0' + (al >= 9) ? al : al+('a'-10-'0');
; niszczy: cl

mov bl, '0'
mov cl, 9

cmp cl, al ; CF = al>cl
sbb cl, cl ; cl = -1 jeśli CF==1

and cl, 'a'-10-'0'

add bl, al
add bl, cl

3.2.2. Kod MMX

segment .text

_word2ascii_mmx:
; eax - liczba           ; eax: hgfedcba

; Pierwszym krokiem jest odwrócenie kolejność tetrad,
; ponieważ pierwszy jest wyświetlany bajt o młodszym adresie, a
; na pierwszej pozycji jest zawsze najstarsza cyfra.
; Stąd całe zamieszanie

bswap     eax            ; eax: badcfehg

; Kolejnym krokiem jest "rozpakowanie" tetrad do
; bajtów

movd      mm0, eax       ; mm0: 00000000badcfehg
movd      mm1, eax       ; mm1: 00000000badcfehg
psrlq     mm0, 4         ; mm0: 000000000badcfeh

punpcklbw mm0, mm1       ; mm0: ba 0b dc ad fe cf hg eh
pand      mm0, [mm_mask] ; mm0: 0a 0b 0c 0d 0e 0f 0g 0h

; Na końcu właściwa konwersja

                         ; np.
movq      mm1, mm0       ; mm1: 0a 01 07 0f 0d 00 08 0c
pcmpgtb   mm1, [mm_nine] ; mm1: ff 00 00 ff ff 00 00 ff ; mm1 > 9
pand      mm1, [mm_corr] ; mm2: 27 00 00 27 27 00 00 27

paddb     mm0, [mm_zero] ;
paddb     mm0, mm1       ;

movq [string], mm0

ret;urn

segment .data

mm_mask dd 0x0f0f0f0f, 0x0f0f0f0f  ; maska dla młodszych tetrad
mm_nine dd 0x09090909, 0x09090909  ; packed_byte(0x9)
mm_zero dd 0x30303030, 0x30303030  ; packed_byte('0')
mm_corr dd 0x27272727, 0x27272727  ; packed_byte('a' - 10)

string db '????????',0

3.2.3. Kod SSSE3 update

Zobacz wpis w osobnym artykule.