   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 13 - Operacje na bitach, czyli to, w czym asembler blyszczy
najbardziej

   W tej czesci poznamy wazna grupe instrukcji - operacje na bitach. Te
   wlasnie instrukcje odrozniaja asemblera od innych jezykow, gdzie
   rzadko pojawia sie mozliwosc dzialania na tych najmniejszych
   jednostkach informacji (odpowiednie operatory istnieja w jezykach C i
   Pascal, ale inne jezyki, jak na przyklad Fortran 77, sa tego
   pozbawione).
   Mimo iz o wszystkich instrukcjach opisanych w tej czesci juz
   wspomnialem przy okazji omawiania podstawowych rozkazow procesora, to
   instrukcje bitowe sa wazne i zasluguja na oddzielny rozdzial,
   poswiecony w calosci tylko dla nich.

   Zdawac by sie moglo, ze z takim jednym, malenkim bitem niewiele da sie
   zrobic: mozna go wyczyscic (wyzerowac), ustawic (wstawic do niego 1)
   lub odwrocic jego biezaca wartosc. Ale te operacje maja duze
   zastosowania i dlatego ich poznanie jest niezbedne. Jesli sobie
   przypomnicie, to uzywalismy juz wielokrotnie takich instrukcji jak AND
   czy XOR. Teraz przyszedl czas, aby poznac je blizej.
     _________________________________________________________________

Instrukcja NOT

   (przeskocz NOT)

   Instrukcja NOT (logiczna negacja - to NIE jest to samo, co zmiana
   znaku liczby!) jest najprostsza z czterech podstawowych operacji
   logicznych i dlatego to od niej rozpoczne wstep do instrukcji
   bitowych.

   NOT jest instrukcja jednoargumentowa, a jej dzialanie wyglada tak:
        NOT 0 = 1
        NOT 1 = 0

   Uzywamy tej instrukcji wtedy, gdy chcemy naraz odwrocic wszystkie bity
   w zmiennej lub rejestrze. Na przyklad, jesli AX zawiera 0101 0011 0000
   1111 (530Fh), to po wykonaniu NOT AX w rejestrze tym znajdzie sie
   wartosc 1010 1100 1111 0000 (ACF0h). Dodanie obu wartosci powinno dac
   FFFFh.

   NOT moze miec zastosowanie tam, gdzie wartosc logiczna "falsz" ma
   przyporzadkowana wartosc zero, a "prawda" - wartosc FFFFh, gdyz NOT w
   tym przypadku dokladnie przeklada "prawde" na "falsz".
     _________________________________________________________________

Instrukcja AND

   (przeskocz AND)

   Instrukcji AND (logicznej koniunkcji) najprosciej uzywac do
   wyzerowania bitow. Tabelka dzialania AND wyglada tak:
        0 AND 0 = 0
        0 AND 1 = 0
        1 AND 0 = 0
        1 AND 1 = 1

   No ale jakie to moze miec zastosowanie?
   Powiedzmy teraz, ze chcemy sprawdzic, czy bit numer 4 (numeracje bede
   podawal od zera) rejestru AX jest rowny 1, czy 0. Tutaj nie wystarczy
   proste porownanie CMP, gdyz reszta rejestru moze zawierac nie wiadomo
   co. Z pomoca przychodzi nam wlasnie instrukcja AND. Ponizej
   pseudo-przyklad:
        and     ax, 0000 0000 0001 0000b        ; (and ax, 16)

   Teraz, jesli bit numer 4 (odpowiadajacy wartosci 2^4=16) byl rowny 1,
   to caly AX przyjmie wartosc 16, jesli zas byl rowny zero, to caly AX
   bedzie zerem. Na nasze szczescie, instrukcja AND ustawia odpowiednio
   flagi procesora, wiec rozwiazaniem naszego problemiku bedzie kod:
        and     ax, 16
        jz      bit_4_byl_zerem
        ;jnz    bit_4_nie_byl_zerem

   A jakies zastosowanie praktyczne?
   Juz podaje: zamiana malych liter na wielkie. W kodzie ASCII litery
   male od wielkich roznia sie tylko tym, ze maja ustawiony bit numer 5.
   Tak wiec po wykonaniu:
        mov     al, "a"
        and     al, 5fh         ; 5fh = 0101 1111 - czyscimy bit 5
                                ; (i 7 przy okazji)

   w rejestrze AL bedzie kod wielkiej litery A.
   Inne zastosowanie znajdziecie w moim kursie programowania glosniczka:
        in      al, 61h
        and     al, not 3               ; zerujemy bity 0 i 1
                                        ; NASM: and al,~3
        out     61h, al

   W tym kodzie instrukcja AND posluzyla nam do wyczyszczenia bitow 0 i 1
   (NOT 3 = NOT 0000 0011 = 1111 1100).

   Jak zauwazyliscie, instrukcja AND niszczy zawartosc rejestru, oprocz
   interesujacych nas bitow. Jesli zalezy Wam na zachowaniu rejestru,
   uzyjcie instrukcji TEST. Dziala ona identycznie jak AND, ale nie
   zapisuje wyniku dzialania. Po co nam wiec taka instrukcja? Otoz, wynik
   nie jest zapisywany, ale TEST ustawia dla nas flagi identycznie jak
   AND. Pierwszy kod przepisany z instrukcja TEST bedzie wygladal tak:
        test    ax, 16
        jz      bit_4_byl_zerem
        ;jnz    bit_4_nie_byl_zerem

   Teraz nasz program bedzie ciagle dzialac prawidlowo, ale tym razem
   zawartosc rejestru AX zostala zachowana.
   Jest jeszcze jedno ciekawe zastosowanie instrukcji TEST:
        test    ax, ax

   I co to ma niby robic? Wykonuje  AND AX, AX , nigdzie nie zapisuje
   wyniku i tylko ustawia flagi.
   No wlasnie! Ustawia flagi, w tym flage zera ZF. To, co widzicie
   powyzej to najwydajniejszy sposob na to, aby sprawdzic czy wartosc
   rejestru nie jest zerem.
     _________________________________________________________________

Instrukcja OR

   (przeskocz OR)

   Instrukcja OR (logiczna alternatywa) w prosty sposob sluzy do
   ustawiania bitow (wpisywania do nich 1).
   Tabelka dzialania wyglada nastepujaco:
        0 OR 0 = 0
        0 OR 1 = 1
        1 OR 0 = 1
        1 OR 1 = 1

   Jesli na przyklad chcemy, aby 2 najmlodsze bity rejestru BX byly sie
   rowne 1, a nie chcemy naruszac innych bitow (czyli MOV jest
   wykluczone), mozemy to zrobic tak:
        or      bx, 0000 0000 0000 0011         ; (or bx, 3)

   Zastosowanie tego jest proste. Podam 2 przyklady. Pierwszy z nich jest
   wyjety z mojej procedury wytwarzajacej dzwiek w glosniczku (i kursu
   poswieconego temu zagadnieniu):
        in      al, 61h
        or      al, 3                           ; ustawiamy bity 0 i 1
        out     61h, al

   Przyklad drugi jest odwroceniem operacji AND na znakach ASCII:
        mov     al, "A"
        or      al, 20h                 ; 20h = 0010 0000 - ustawiamy bit 5

   teraz w AL powinien byc kod malej literki a.
   Instrukcja OR nie ma swojego odpowiednika, jakim jest TEST dla AND.
   Ale za to ma inne ciekawe zastosowanie - mozna nia sprawdzic, czy 2
   rejestry naraz nie sa zerami (to jest najlepszy sposob - bez zadnych
   CMP, JNZ/JZ itp.):
        or      ax, bx

   Podobnie, jak w instrukcji AND, flaga zera bedzie ustawiona, gdy wynik
   operacji jest zerem - a to moze sie zdarzyc tylko wtedy, gdy AX i BX
   sa jednoczesnie zerami.
   Zauwazcie, ze nie mozna do tego celu uzyc instrukcji AND. Dlaczego?
   Podam przyklad: niech AX=1 i BX = 8. AX i BX nie sa oczywiscie rowne
   zero, ale:
                0000 0000 0000 0001     (=AX)
        AND     0000 0000 0000 1000     (=BX)
                =
                0000 0000 0000 0000

   Dlatego zawsze nalezy przemyslec efekt dzialania instrukcji.
     _________________________________________________________________

Instrukcja XOR

   (przeskocz XOR)

   Instrukcji XOR (eXclusive OR, logiczna alternatywa wykluczajaca) uzywa
   sie do zmiany stanu okreslonego bitu z 0 na 1 i odwrotnie.
   Dzialanie XOR jest okreslone tak:
        0 XOR 0 = 0
        0 XOR 1 = 1
        1 XOR 0 = 1
        1 XOR 1 = 0

   Zauwazmy takze, ze dla dowolnych a i b mamy:
   (a XOR b) XOR b = a
   a XOR 0 = a
   a XOR -1 = NOT a (-1 = FF w bajcie, FFFF w slowie i FFFFFFFF w
   dwordzie)
   a XOR a = 0
   Z tej ostatniej rownosci wynika natychmiast, ze wyXORorwanie rejestru
   z samym soba zawsze go wyzeruje. W ten sposob otrzymujemy jeden z
   dwoch najwydajniejszych sposobow na wyzerowanie rejestru:
        xor     rej, rej

   Drugi sposob to SUB rej,rej.

   Teraz przyklad: chcemy, aby wartosc rejestru AX stala sie rowna 1 gdy
   rejestr byl wyzerowany, a zerem, gdy byla w tym rejestrze jedynka.
   Oto, jak mozemy to zrobic:
                cmp     ax, 1
                je      wyzeruj
                mov     ax, 1
                jmp     koniec
        wyzeruj:
                mov     ax, 0
        koniec:

   Ale wersja optymalna wyglada tak:
        xor     ax, 1

   gdyz mamy:
        wartosc AX:     0000 0000 0000 0001             0000 0000 0000 0000
                XOR     0000 0000 0000 0001             0000 0000 0000 0001
                =
        nowy AX:        0000 0000 0000 0000             0000 0000 0000 0001

   Jak widac, jest to o wiele prostsze i wydajniejsze rozwiazanie.
   Dlatego wlasnie dobrze jest, gdy pozna sie instrukcje logiczne.
     _________________________________________________________________

Instrukcje przesuwania bitow

   (przeskocz instrukcje przesuwania)

   Instrukcje przesuwania bitow ("shift") przemieszczaja bity, nie
   zmieniajac ich wzajemnego polozenia (przesuwaja "grupowo"). To
   wyjasnienie moze sie wydawac bardzo pokretne, ale spokojnie - zaraz
   wszystko sie wyjasni.
   Na poczatek powiem, ze jest kilka takich instrukcji (ktore tez byly
   podane w rozdziale o podstawowych instrukcjach procesora):
     * SHL - shift left (shift logical left) = przesuniecie (logicznie) w
       lewo
     * SAL - shift arithmetic left = przesuniecie (arytmetycznie) w lewo
     * SHR - shift logical right = przesuniecie (logiczne) w prawo
     * SAR - shift arithmetic right = przesuniecie (arytmetyczne)
     * SHLD/SHRD = przesuniecia logiczne w lewo/prawo o podwojnej
       precyzji

   Dzialanie kazdej z tych instrukcji pokaze na przykladzie.
   Niech na poczatku AX = 1010 0101 1010 0101 (A5A5h).

   SHL i rownowazna SAL dziala tak (zakladajac, ze przesuwamy o jeden):
   najstarszy bit jest we fladze CF, kazdy inny bit wchodzi na miejsce
   bitu starszego o 1, a do bitu zerowego wkladane jest zero.
   Po wykonaniu SHL AX,3 wartosc AX bedzie wiec wynosic 0010 1101 0010
   1000 (2D28h), gdyz wszystkie bity przesunelismy o 3 miejsca w lewo,
   oraz CF=1 (bo jako ostatnia z rejestru wyleciala jedynka).

   Instrukcja SHR dziala w druga strone niz SHL: bit zerowy jest
   umieszczany we fladze CF, kazdy inny bit wchodzi na miejsce bitu
   mlodszego o 1, a do najstarszego bitu wkladane jest zero.
   Dlatego teraz po wykonaniu SHR AX,1 w rejestrze AX bedzie 0001 0110
   1001 0100 (1694h), bo poprzednie bity AX przesunelismy o 1 miejsce w
   prawo, oraz CF=0.

   SAR rozni sie od SHR nie tylko nazwa, ale tez dzialaniem. Slowo
   "arytmetyczne" w nazwie NIE jest tu bez znaczenia. Gdy SAR dziala na
   liczbach ze znakiem, to zachowuje ich znak (bit7), czyli wykonuje to
   samo, co SHR, ale zamiast wkladac zero do najstarszego bitu, wstawia
   tam jego biezaca wartosc.
   Z poprzedniego przykladu mamy, ze AL = 94h = 1001 0100. Gdy teraz
   wykonamy SAR AL,2 to jako wynik otrzymamy 1110 0101 (E5h), bo
   wszystkie bity poszly o 2 miejsca w prawo o bit 7 zostal zachowany, i
   CF=0.

   SHLD i SHRD wykonuja to samo, co SHL i SHR, ale na dwoch rejestrach
   naraz (no, prawie). Na przyklad wykonanie  SHLD EAX,EBX,3 spowoduje ze
   3 najstarsze bity EAX zostana wyrzucone (i CF=ostatni z wyrzuconych)
   oraz 3 najstarsze bity EBX przejda na nowo powstale miejsca w 3
   najmlodszych bitach EAX. Ale uwaga: EBX pozostaje niezmieniony ! I to
   jest wlasnie przyczyna uzycia slow "no prawie".

   Ale nie sposob powiedziec o SHL i SHR bez podania najbardziej
   popularnego zastosowania: szybkie mnozenie i dzielenie.
   Jak mozna mnozyc i dzielic tylko przesuwajac bity, pytacie?
   Otoz, sprawa jest bardzo prosta. Wpiszcie do AX jedynke i wykonajcie
   kilka razy SHL AX,1 za kazdym razem sprawdzajac zawartosc AX. Jak
   zauwazycie, w AX beda kolejno 1,2,4,8,16,... Czyli za kazdym razem
   zawartosc AX sie podwaja.
   Ogolnie,  SHL rej,n mnozy zawartosc rejestru przez 2^n. Na przyklad
   SHL AX,4 przemnozy AX przez 2^4 = 16.
   Ale co zrobic, gdy chcemy mnozyc przez cos innego niz 2^n?
   Odpowiedz jest rownie prosta, na przyklad AX * 10 = (AX*8) + (AX*2) -
   z tym sie chyba zgodzicie. A od tego juz tylko 1 krok do
        mov     bx, ax
        shl     ax, 3           ; AX = AX*8
        shl     bx, 1           ; BX = BX*2 = AX*2
        add     ax, bx          ; AX = AX*10

   Ale niekoniecznie musimy dodawac wyniki. Zauwazcie, ze AX * 15 =
   (AX*8) + (AX*4) + (AX*2) + AX. Trzeba byloby wykonac 3 SHL i 3 ADD.
   Ale my skorzystamy z innego rozwiazania: AX * 15 = (AX*16) - AX. Juz
   tylko 1 SHL i 1 SUB. Stad mamy:
        mov     bx, ax
        shl     ax, 4           ; AX = AX*16
        sub     ax, bx

   Dokladnie w ten sam sposob dziala dzielenie (tylko oczywiscie przy
   dzieleniu uzywamy SHR/SAR i niestety szybko mozemy dzielic tylko przez
   potegi dwojki). Pilnujcie tylko, aby uzywac tej wlasciwej instrukcji!
   Jak wiemy, 65534 = 0FFFEh = -2 . Teraz, oczywiscie FFFE SHR 1 = 7FFFh
   = 32767 (=65534/2) a FFFE SAR 1 = FFFF = -1 (= -2/2). Widac roznice,
   prawda? Pamietajcie, ze SAR patrzy na znak i go zachowuje.

   Uzywanie SHL dla mnozenia i (zwlaszcza) SHR dla dzielenia moze
   znacznie przyspieszyc nasze programy, gdyz instrukcje MUL i DIV sa
   dosc wolne.
     _________________________________________________________________

Instrukcje rotacji bitow

   (przeskocz instrukcje rotacji)

   Teraz przedstawie kolejna grupe instrukcji bitowych - instrukcje
   rotacji bitow. W tej grupie sa tylko 4 instrukcje:
     * ROL - rotate left = obrot w lewo.
       Ta instrukcja robi tyle, co SHL, lecz zamiast do bitu zerowego
       wkladac zero, wklada tam biezaca wartosc najstarszego bitu (przy
       okazji zachowujac go takze we fladze CF).
       bit7 = bit6, ... , bit1 = bit0, bit0 = stary bit7
     * RCL - rotate through carry left = obrot w lewo z uzyciem flagi CF.
       Ta instrukcja jest podobna do ROL z jedna roznica: wartosc
       wstawiana do najmlodszego bitu jest brana z flagi CF, a nie od
       razu z najstarszego bitu. Po wzieciu biezacej wartosci CF,
       najstarszy bit jest do niej zapisywany.
       carry flag CF = bit7, bit7 = bit6, ... , bit1 = bit0, bit0 = stara
       CF
     * ROR - rotate right = obrot w prawo.
       Ta instrukcja robi tyle, co SHR, lecz zamiast do najstarszego bitu
       wkladac zero, wklada tam biezaca wartosc najmlodszego bitu (przy
       okazji zachowujac go takze we fladze CF).
       bit0 = bit1, ... , bit6 = bit7, bit7 = stary bit0
     * RCR - rotate through carry right = obrot w prawo z uzyciem flagi
       CF.
       Ta instrukcja jest podobna do ROR z jedna roznica: wartosc
       wstawiana do najstarszego bitu jest brana z flagi CF, a nie od
       razu z najmlodszego bitu. Po wzieciu biezacej wartosci CF,
       najmlodszy bit jest do niej zapisywany.
       CF = bit0, bit0 = bit1, ... , bit6 = bit7, bit7 = stara CF

   Oczywiscie, te instrukcje moga dzialac na wartosciach wiekszych niz
   bajt - bit7 jako najstarszy jest tutaj tylko dla ilustracji.

   Schematyczne dzialanie tych instrukcji na bajtach widac na tych
   rysunkach:
   (przeskocz rysunki)
        ROL:
                        +-->------------->-------------->--+
                        |                                  |
                CF <-   7 <- 6 <- 5 <- 4 <- 3 <- 2 <- 1 <- 0

        RCL:
                        +-->-----------> CF >----------->--+
                        |                                  |
                        7 <- 6 <- 5 <- 4 <- 3 <- 2 <- 1 <- 0

        ROR:
                        +--<-------------<--------------<--+
                        |                                  |
                        7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0   -> CF

        RCR:
                        +--<-----------< CF <-----------<--+
                        |                                  |
                        7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0

   W przypadku ROL i ROR, to ostatni wyjety z jednej strony a wlozony z
   drugiej strony bit zostaje tez zapisany do flagi CF.
   RCR i RCL dzialaja tak, ze bit, ktory ma zostac wstawiony, jest
   pobierany z CF, a wypchniety bit laduje w CF, a nie od razu na nowym
   miejscu.

   No to kilka przykladow:
        0011 1100  ROL  2  =  1111 0000  (tak samo jak SHL)
        0011 1100  ROL  3  =  1110 0001

        1111 0000  ROR  1  =  0111 1000  (tak samo jak SHR)
        1010 0011  ROR  5  =  0001 1101

   Zastosowanie tych instrukcji znalazlem jedno: generowanie chaosu w
   rejestrach...
   Po co to mi? Na przyklad generatory liczb pseudo-losowych z mojej
   biblioteki korzystaja z tych wlasnie instrukcji (a takze z kilku
   poprzednich, na przyklad XOR).
     _________________________________________________________________

Instrukcje testowania i szukania bitow

   (przeskocz instrukcje BT*)

   Ostatnia juz grupa rozkazow procesora to instrukcje testowania i
   szukania bitow. W tej grupie znajduja sie:
     * BT - Bit Test - sprawdz wartosc bitu
     * BTC - Bit Test and Complement - sprawdz wartosc bitu i odwroc
     * BTR - Bit Test and Reset - sprawdz wartosc bitu i wyzeruj
     * BTS - Bit Test and Set - sprawdz wartosc bitu i ustaw
     * BSF - Bit Scan Forward - szukaj bitu rosnaco
     * BSR - Bit Scan Reverse - szukaj bitu malejaco

   Teraz po kolei omowie dzialanie kazdej z nich.

   Instrukcje BT* przyjmuja 2 argumenty: miejsce, gdzie maja znalezc dany
   bit i numer tego bitu, a zwracaja wartosc tego bitu we fladze CF.
   Ponadto, BTS ustawia znaleziony bit na 1, BTR czysci znaleziony bit, a
   BTC odwraca znaleziony bit.
   Kilka przykladow:
        bt      eax, 21         ; umiesc 21. bit EAX w CF
        jc      bit_jest_1
        ...
        bts     cl, 2           ; umiesc 2. bit CL w CF i ustaw go
        jnc     bit_2_byl_zerem
        ...
        btc     dh, 5           ; umiesc 5. bit DH w CF i odwroc go
        jc      bit_5_byl_jeden

   Instrukcje Bit Scan przyjmuja 2 argumenty: pierwszy z nich to rejestr,
   w ktorym bedzie umieszczona pozycja (numer od zera poczawszy)
   pierwszego bitu, ktorego wartosc jest rowna 1 znalezionego w drugim
   argumencie instrukcji. Dodatkowo, BSF szuka tego pierwszego bitu
   zaczynajac od bitu numer 0, a BSR od najstarszego (numer 7, 15, 31, i
   tak dalej, w zaleznosci od rozmiaru drugiego argumentu).

   Teraz szybki przykladzik:
        mov     ax, 1010000b
        bsf     bx, ax
        bsr     cx, ax

   Po wykonaniu powyzszych instrukcji w BX powinno byc 4, a w CX - 6
   (bity liczymy od zera).
     _________________________________________________________________

   Jak pewnie zauwazyliscie, w kilku miejscach w tym tekscie wyraznie
   podkreslilem slowa "najwydajniejszy" i im podobne. Chcialem w ten
   sposob uzmyslowic Wam, ze operacje logiczne / binarne sa bardzo wazna
   grupa instrukcji. Uzywanie ich, najlepiej wraz z instrukcja LEA
   sluzaca do szybkich rachunkow, moze kilkakrotnie (lub nawet
   kilkunastokrotnie) przyspieszyc najwazniejsze czesci Waszych programow
   (na przyklad intensywne obliczeniowo petle o milionach powtorzen -
   patrz na przyklad program "L_mag.asm" z 8. czesci tego kursu).

   Dlatego zachecam Was do dobrego opanowania instrukcji binarnych - po
   prostu umozliwia to pisanie programow o takiej wydajnosci, o ktorej
   inni moga tylko pomarzyc...

   Po szczegolowy opis wszystkich instrukcji odsylam, jak zwykle do :
   Intela i AMD

   Ciekawe operacje na bitach (w jezyku C).

   Poprzednia czesc kursu (klawisz dostepu 3)
   Kolejna czesc kursu (klawisz dostepu 4)
   Spis tresci off-line (klawisz dostepu 1)
   Spis tresci on-line (klawisz dostepu 2)
   Ulatwienia dla niepelnosprawnych (klawisz dostepu 0)
     _________________________________________________________________

Cwiczenia

    1. W jednej komendzie policz:
         1. iloraz z dzielenia EDI przez 4
         2. reszte z dzielenia EDI przez 4
         3. najwieksza liczbe mniejsza lub rowna EDI dzielaca sie przez 4
       Wskazowka: 4 = 2^2 oraz mozliwe reszty z dzielenia przez 4 to 0,
       1, 2 i 3 i zajmuja one co najwyzej 2 bity.
    2. W jednej komendzie:
         1. ustaw bity 0, 11, 4 i 7 rejestru CX, nie ruszajac pozostalych
         2. wyczysc bity 9, 2, 7 i 25 rejestru ESI, nie ruszajac
            pozostalych
         3. przelacz (zmien wartosc na odwrotna) bity 16, 4, 21, 1 i 10
            rejestru EAX, nie ruszajac pozostalych
         4. spraw, by wartosc rejestru AL=18h zmienila sie na 80h, bez
            instrukcji MOV
         5. spraw, by wartosc rejestru AL=18h zmienila sie na 81h, bez
            instrukcji MOV
         6. przelacz bit 23 rejestru EDX nie ruszajac pozostalych, a jego
            stara wartosc umiesc we fladze CF
