Operacje na łańcuchach ASCII-Z w asemblerze

Autor: Wojciech Muła
Dodany:28.02.2002
Aktualizacja:9.08.2007

Treść

Wprowadzenie

W artykule zostaną pokazane asemblerowe implementacje niektórych funkcji zadeklarowanych w pliku nagłówkowym string.h.

UWAGA: na początku września 2007 opublikowałem bibliotekę see2string, która zawiera niektóre funkcje ze string.h zaimplementowane z użyciem rozkazów SSE2. Oczywiście zastosowałem w nich metody opisane w tym artykule.

Porównywanie łańcuchów

memcmp — porównywanie łańcuchów o tej samej długości (MMX)

Funkcja stwierdza czy dwa łańcuchy znaków, o tej samej długości są takie same.

segment .text
; edi, esi - wskaźniki na łańcuchy
; ecx      - długość łańcuchów
; eax      - wynik
memcmp:

        mov ebx, ecx       ; liczba bajtów które nie zmieszczą się
        and ebx, 0x7       ; w rejestrze MMX

        shr ecx, 3         ; ecx/=3;
        jz  .compare_rest  ; jeśli zero porównanie MMX zostaje pominięte

.compare_mmx:

        movq    mm0, [edi] ; takie same                 różne (przykład)
        pcmpeqd mm0, [esi] ; |ffffffff|ffffffff|        |ffffffff|00000000|
        psrlq   mm0, 16    ; |0000ffff|ffffffff|        |0000ffff|ffff0000|

        movd    eax, mm0   ;          |ffffffff|                 |ffff0000|
        cmp     eax, -1    ;
        jne     .diff

        add     edi, 8
        add     esi, 8
        dec     ecx
        jnz     .compare_mmx

.compare_rest:
or   ebx, ebx  ; jeśli nie jest potrzebne dodatkowe porównanie
jz  .equal     ; łańcuchy są identyczne

movq    mm0, [edi]
movq    mm1, [edi]
pand    mm0, [mask + ebx*8 - 8] ; wyzeruj dane spoza końca łańcucha
pand    mm1, [mask + ebx*8 - 8]

pcmpeqd mm0, mm1
psrlq   mm0, 16
movd    eax, mm0
cmp     eax, -1
sete    eax                     ; eax = 1 gdy równe
ret

segment .data

mask db 0xff,0x00,0x00,0x00, 0x00,0x00,0x00,0x00 ; 1
     db 0xff,0xff,0x00,0x00, 0x00,0x00,0x00,0x00 ; 2
     db 0xff,0xff,0xff,0x00, 0x00,0x00,0x00,0x00 ; 3
     db 0xff,0xff,0xff,0xff, 0x00,0x00,0x00,0x00 ; 4
     db 0xff,0xff,0xff,0xff, 0xff,0x00,0x00,0x00 ; 5
     db 0xff,0xff,0xff,0xff, 0xff,0xff,0x00,0x00 ; 6
     db 0xff,0xff,0xff,0xff, 0xff,0xff,0xff,0x00 ; 7

memcmp (x86)

segment .text
; edi, esi - wskaźniki na łańcuchy
; ecx      - długość łańcuchów
; eax      - wynik
strncmp:
        mov ebx, ecx
        shr ecx, 2      ; ecx/=4 - ilość dword'ów
        jz  .compare_rest

.compare4b:
        mov edx, [edi]
        cmp edx, [esi]
        jne .diff

        add edi, 4
        add esi, 4

        dec ecx
        jnz .compare4b

.compare_rest:
and ebx, 0x3    ; ebx    - pozostałe bajty
jz  .equal

mov ebx, [mask + ebx*4 - 4]

mov eax, [edi]  ; pobierz 4 bajty
mov ecx, [esi]  ;

and eax, ebx    ; zamaskuj bajty pobrane
and ecx, ebx    ; spoza łańcuchów

cmp  eax, ecx    ; porównanie
sete eax

segment .data

mask db 0xff,0x00,0x00,0x00
     db 0xff,0xff,0x00,0x00
     db 0xff,0xff,0xff,0x00

strcmp (MMX) [9.08.2007]

Poniższa funkcja porównuje dwa łańcuchy. Zwracany wynik jest zgodny z konwencją funkcji strcmp znanej z C.

; wejście: esi (A), edi (B) - porównywane łańcuchy
; wyjście: eax
; modyfikowane:
;       MMX: mm0, mm1, mm2, mm6, mm7
;   x86: ebx

strcmp:
pxor    mm7, mm7 ; mm7 = packed_byte(0x00)
xor     ecx, ecx ; ecx = 0

.compare:
        movq mm0, [esi+ecx]
        movq mm1, [edi+ecx]
        add  ecx, 8

        pcmpeqb  mm1, mm0 ; 0x00 na pozycji gdzie różnią się oba łańcuchy
        pcmpeqb  mm0, mm7 ; 0xff na pozycji '\0' w łańcuchu A
        pandn    mm0, mm1 ; połącz wyniki - 0x00 gdzie łańcuchy różne/zero w A

        pmovmskb eax, mm0 ; i zamień na maskę bitową

        cmp      al, 0xff ; gdy którykolwiek bit jest wyzerowany, świadczy o
        jz       .compare ; napotkaniu końca A lub różniącego je bajtu


not eax
bsf eax, eax              ; pozycja pierwszego ustawionego bitu
add ecx, eax

movzx ebx, byte [esi+ecx] ; pobierz
movzx eax, byte [edi+ecx] ; znaki
sub   eax, ebx            ; i oblicz różnicę

ret

strlen — długość łańcucha (MMX) [23.06.2007]

Funkcja oblicza długość łańcucha znaków zakończonego zerem, z powodzeniem może być wykorzystana jako strchr, wystarczy wypełnić mm1 szukanym znakiem.

; edi - pierwszy znak
; ecx - wynik: długość łańcucha

mmx_strlen:

        pxor mm1, mm1 ; packed_byte(0x00)
        xor  ecx, ecx

.count:                     ; np.
        movq     mm0, [edi] ; mm0 = | 0 |'e'| 0 |'b'|'m'|'e'|'s'|'a'|
        pcmpeqb  mm0, mm1   ; mm0 = | ff| 0 | ff| 0 | 0 | 0 | 0 | 0 |

        pmovmskb eax, mm0   ; eax = 10100000b (0xa0)
        test      al, al
        jnz      .founded   ; w wyniku są ustawione bity, znaleziono kończące zero
                            ; w przeciwny przypadku...

        add      edi, 8     ; następne 8 bajtów
        add      ecx, 8     ; zwiększ o 8 długość

        jmp .count

.founded:

bsf eax, eax    ; znajdź pierwszy ustawiony bit
add ecx, eax
ret

strchr — wyszukiwanie pierwszego wystąpienia znaku (MMX) [24.06.2007]

; edi - pierwszy znak
;  al - szukany znak
; edi - wynik (wskaźnik, NULL gdy znak nie znaleziony)

mmx_strlen:

        pxor      mm7, mm7  ; packed_byte(0x00)

        movzx     eax, al
        imul      eax, 0x01010101
        movd      mm6, eax
        punpcklbw mm6, mm6  ; |AL|AL|AL|AL|AL|AL|AL|AL|

.count:                 ; np. AL = 'e'
        movq     mm0, [edi] ; mm0 = | 0 |'e'| 0 |'b'|'m'|'e'|'s'|'a'|
        movq     mm1, mm0
        pcmpeqb  mm0, mm7   ; mm0 = | ff| 0 | ff| 0 | 0 | 0 | 0 | 0 | -- \0
        pcmpeqb  mm1, mm6   ; mm1 = | 0 | ff| 0 | 0 | 0 | ff| 0 | 0 | -- AL
        por      mm0, mm1   ; mm0 = | ff| ff| ff| 0 | 0 | ff| 0 | 0 | -- AL & \0

        pmovmskb eax, mm0   ; eax = 11100100b (0xa0)
        test      al, al
        jnz      .founded   ; w wyniku są ustawione bity, znaleziono
                            ; kończące zero lub szukany znak;
                            ; w przeciwny przypadku...

        add      edi, 8     ; następne 8 bajtów
        jmp .count

.founded:

bsf eax, eax    ; znajdź pierwszy ustawiony bit
add edi, eax    ; edi - adres albo \0 albo szukany znak

cmp [edi], 0    ; edi == 0?
jz  .NULL
ret             ; nie, edi wskazuje na szukany znak

.NULL
xor edi, edi    ; tak, nie znaleziono znaku, edi = NULL
ret

Uwagi do funkcji MMX-owych strcmp, strchr i strlen [24.06.2007]

Pierwsze procesory (Pentium MMX, Pentium II) posiadające rozszerzenie MMX nie miały wielu obecnie dostępnych instrukcji, w tym intensywnie wykorzystywaną w wymienionych funkcjach PMOVMSKB — można ją z powodzeniem zastąpić czteroma dodatkowymi rozkazami:

; mm0 -- maska bajtowa                   ; |aaaaaaaa|bbbbbbbb|cccccccc|dddddddd|eeeeeeee|ffffffff|gggggggg|hhhhhhhh|
psrlq    mm0, 4                          ; |0000aaaa|aaaabbbb|bbbbcccc|ccccdddd|ddddeeee|eeeeffff|ffffgggg|gggghhhh|
pand     mm0, [maska_0x0011001100110011] ; |00000000|000a000b|00000000|000c000d|00000000|000e000f|00000000|000g000h|
packsswb mm0, mm0                        ; |000a000b|000c000d|000e000f|000g000h|000a000b|000c000d|000e000f|000g000h|
                        ; ustawione bity są umieszczone
                        ; na początku każdej tetrady
movd     eax, mm0
bsf/bsr  eax, eax       ; czyli wynikiem jest index*4, a stąd:
shr      eax, 2         ; eax = index

Funkcje wykorzystujące zarówno PMOVMSKB jak i opisany wyżej zamiennik są dostępne w pliku mmx_string.c (kompilacja w GCC).

Zamiana wielkości liter

Kody małych liter ASCII różnią się od dużych tylko jednym, piątym bitem.

A..Z 0x41..0x5a
a..z 0x61..0x7a

tolower/toupper (x86)

; wejście/wyjście - al litera
tolower:
        cmp al, 0x41 ;
        jb  .skip    ;
        cmp al, 0x5a ;
        ja  .skip    ; sprawdź zakres

        xor al, 0x20 ; zaneguj 5-ty bit, jeśli to duża litera
.skip:
        ret

Kod bez rozgałęzień:

tolower:
        cmp   al, 0x41
        setnb bl       ; (al >= 0x41) ? bl=1 : bl=0;
        cmp   al, 0x5a
        setna bh       ; (al <= 0x5a) ? bh=1 : bh=0;

        and   bl, bh
        shl   bl, 5
        xor   al, bl   ; wyzeruj 5-ty bit jeśli (al>=41h) & (al<=5ah)

W przypadku funkcji toupper wystarczy zmienić sprawdzany zakres.

strupr (MMX)

Interesujące może być sprawdzenie czy bajt należy do przedziału (w kodzie wydzielone ***). Poniższa funkcja asemblerowa jest odpowiednikiem funkcji strupr, ale tak jak wcześniej wystarczy zmienić sprawdzany zakres aby uzyskać strlwr.

segment .text

; wejście: edi - wskaźnik na łańcuch 8 znaków
;          mm3 = packed_byte(0x00)
;          mm4 = packed_byte(0x20)
simple_strupr:

        mov mm0, [edi]          ; wprowadź 8 znaków

        ;***
        mov mm1, mm0            ; kopiuj
        mov mm2, mm0            ;

        psubusb mm1, [mmx_0x40] ; wszystkie bajty mniejsze od 0x41
        psubusb mm2, [mmx_0x5a] ; i 0x5a zostaną wyzerowane

        pcmpeqb mm1, mm3        ; mm1 = (bajt <  'A') ? 0xff : 0x00
        pcmpeqb mm2, mm3        ; mm2 = (bajt <= 'Z') ? 0xff : 0x00

        pandn   mm1, mm2        ; mm1 = (bajt >= 'A' && bajt <= 'Z') ? 0xff : 0x00
        pand    mm1, mm4        ; dla odpowiednich bajtów przypisz maskę
        ;***

        pxor    mm0, mm1        ; zanegowanie 5-go bitu - małe litery
        movq  [edi], mm0
        ret

segment .data

mmx_0x40 dd 0x40404040, 0x40404040
mmx_0x5a dd 0x5a5a5a5a, 0x5a5a5a5a

Szukanie łańcuchów

Oto używany algorytm (dzięki uprzejmości Dale Moora).

char * strstr (const char *s1, const char *s2) {
        int j = 0;
        while (1) {

                if (!s2[j])
                        return (char *)s1;
                if (!s1[j])
                        return NULL;

        if (s1[j] == s2[j])
                j++;
        else
                j = 0,
                s1++;
     }
}

strstr (386+)

_strstr:
        push esi                        ; s1
        push edi                        ; s2
        push ecx                        ; j

        xor ecx, 0                      ; j = 0

  .loop:
        mov  al, byte ptr [esi+ecx]     ; al = s1[j]
        mov  ah, byte ptr [edi+ecx]     ; ah = s2[j]

        or   ah, ah                     ; if (!s2[j]) return (char *)s1;
        jnz  .1
                lea eax, [esi+ecx]
                jmp .exit
     .1:

        or   al, al                     ; if (!s1[j]) return NULL
        jnz  .2
                mov eax, NULL
                jmp .exit
     .2:

        sub  al, ah                     ; if (s1[j] == s2[j])
        jnz  @else
   .then:
        inc ecx                         ; j++
        jmp @loop
   .else:
        xor ecx, ecx                    ; j=0, s1++
        inc esi
        jmp .loop

   .exit:
        pop ecx
        pop edi
        pop esi
        ret

strstr (686+)

_strstr:
        push esi                        ; s1
        push edi                        ; s2
        push ecx                        ; j
        push ebx
        push edx

        xor ecx, 0                      ; j = 0

  .loop:
        mov  al, byte ptr [esi+ecx]     ; al = s1[j]
        mov  ah, byte ptr [edi+ecx]     ; ah = s2[j]

        or   ah, ah                     ; if (!s2[j]) return (char *)s1;
        jz   .exit

        or   al, al                     ; if (!s1[j]) return NULL
        jz   .exit

        mov  edx, esi
        mov  ebx, ecx
        inc  edx                        ; edx = s1+1
        inc  ebx                        ; ebx = j+1

        cmp  al, ah                     ; eq = (s1[j] == s2[j])

        mov    ecx, 0                   ; j = 0
        cmove  ecx, ebx                 ; if  (eq) j = ebx
        cmovne esi, edx                 ; if (!eq) s1++

        jmp .loop

   .exit:
        add    esi, ecx                 ; esi = &s1[j]
        mov    edi, NULL                ; edi = NULL

        or      al, al                  ; ZF  = al==0
        cmovz  eax, edi
        cmovnz eax, esi

        pop edx
        pop ebx
        pop ecx
        pop edi
        pop esi
        ret