   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 15 - Petle i warunki - czyli o tym, jak uzywac blokow kontrolnych

   Wszystkie jezyki wysokiego poziomu maja pewne bloki kontrolne i petle,
   co moze w oczach niektorych osob stanowic przewage nad asemblerem.
   Dlatego teraz pokaze, jak przepisac te wszystkie struktury z
   wykorzystaniem asemblera, czesto uzyskujac kod lepszy niz ten
   wytworzony przez kompilatory jezykow wyzszego poziomu.
   Zanim zaczniemy, dodam, ze nie kazdy jezyk wysokiego poziomu posiada
   opcje kompilacji warunkowej (cos w stylu #ifdef w jezyku C), ale za to
   KAZDY dobry kompilator jezyka asembler ma taka opcje wbudowana! Po
   szczegoly odsylam do instrukcji posiadanego kompilatora.
     _________________________________________________________________

Bloki decyzyjne (warunkowe) "if/else if/else".

   (przeskocz bloki warunkowe)

   Przetlumaczenie czegos takiego na asembler nie jest trudne i opiera
   sie na instrukcjach CMP oraz odpowiednich skokach warunkowych. Pokaze
   to na przykladzie (bede uzywal skladni jezyka C, gdyz posiada
   wszystkie struktury, ktore chcialbym omowic):
   (przeskocz schemat bloku if/else)
        if (a == b)             /* czy a jest rowne b? */
        {
                /* czesc 1 */
        }
        else if (a == c)
        {
                /* czesc 2 */
        }
        else
        {
                /* czesc 3 */
        }

   Powyzszy kod mozna po prostu zastapic czyms takim (zakladam zmienne
   32-bitowe):
   (przeskocz asemblerowy schemat bloku if/else)
                mov     eax, [a]
                cmp     eax, [b]
                jne     elseif1

                ; czesc 1

                jmp     po_if1
        elseif1:
                cmp     eax, [c]        ; pamietajmy, ze [a] juz jest w EAX
                jne     else1

                ; czesc 2

                jmp     po_if1
        else1:

                ; czesc 3

        po_if1:

   Na szczegolna uwage zasluguje przypadek porownywania zmiennej do zera,
   gdzie zamiast instrukcji  CMP EAX, 0 uzyjemy instrukcji  TEST EAX,
   EAX.
   Jesli zas trafi sie Wam dosc prosty kod w stylu:
   (przeskocz przyklad if/else)
        if (a == b)             /* czy a jest rowne b? */
        {
                d = a;          /* wstaw wartosc a do d */
        }
        else if (a == c)
        {
                d = b;
        }
        else
        {
                d = 0;
        }

   lub wyrazenie warunkowe, czyli cos postaci:
                d = (a == b)? a : 0;

   To mozecie (a nawet powinniscie) uzyc instrukcji warunkowego
   kopiowania danych CMOV*. Instrukcje te powoduja znacznie wydajniejsza
   prace procesora (ktory juz nie musi co dwie instrukcje skakac i czytac
   nowych instrukcji). Pierwszy fragment kodu po przetlumaczeniu moglby
   wygladac tak:
   (przeskocz tlumaczenie przykladu if/else)
        xor     edx, edx        ; domyslna wartosc, ktora wstawimy
                                ; do zmiennej D wynosi zero

        mov     eax, [a]
        cmp     eax, [b]
        cmove   edx, eax        ; gdy a == b, to do EDX wstaw
                                ; wartosc A, czyli EAX

        cmp     eax, [c]
        cmove   edx, [b]        ; gdy a == c, to do EDX wstaw wartosc B

        mov     [d], edx        ; do D wstaw wynik operacji
                                ; (A, B lub domyslne 0)

   A drugi:
   (przeskocz tlumaczenie wyrazenia warunkowego)
        xor     edx, edx        ; domyslna wartosc to 0

        mov     eax, [a]
        cmp     eax, [b]        ; porownaj A z B

        cmove   edx, eax        ; gdy rowne, to EDX=[a]

        mov     [d], edx        ; do D wstaw wynik operacji

   Tylko nowoczesne kompilatory jezyka C potrafia wyczyniac takie
   sztuczki.
   Podobne instrukcje istnieja takze dla liczb i rejestrow
   zmiennoprzecinkowych: FCMOV*.
     _________________________________________________________________

Petle.

   (przeskocz petle)

   Z petlami jest troche gorzej, gdyz jest ich wiecej rodzajow.
   Zacznijmy od petli o znanej z gory ilosci przejsc (powtorzen,
   iteracji), czy petli typu
        for (wyrazenia poczatkowe; warunek wykonywania; wyrazenie koncowe)

   Na przyklad:
   (przeskocz przyklad petli for)
        for (i=1; i<10; i=i+1)
        {
                j=j+i;
        }

   zostaloby przetlumaczone na:
   (przeskocz tlumaczenie tego przykladu)
                mov     ecx, 1          ; ECX to zmienna I. i=1
        petla_for:
                cmp     ecx, 10
                jae     koniec_petli    ; wychodzimy, gdy i >= 10

                add     eax, ecx        ; EAX to zmienna J. j=j+i

                add     ecx, 1          ; i=i+1
                jmp     short petla_for
        koniec_petli:

   Jesli warunkiem zakonczenia petli jest to, ze pewna zmienna osiagnie
   zero, mozna stosowac instrukcje LOOP. Przyklad:
   (przeskocz druga petle for)
        for (i=10; i>0; i--)
        {
                j=j+i;
        }

   moze zostac przetlumaczony na 2 sposoby:
   (przeskocz sposoby tlumaczenia)
        ; sposob 1:
                mov     ecx, 10         ; ECX to zmienna I. i=10
        petla_for:
                cmp     ecx, 0          ; lub: test ecx, ecx
                jbe     koniec_petli    ; wychodzimy, gdy i <= 0

                add     eax, ecx        ; EAX to zmienna J. j=j+i

                sub     ecx, 1          ; i=i-1
                jmp     short petla_for
        koniec_petli:



        ; sposob 2:
                mov     ecx, 10         ; ECX to zmienna I. i=10
        petla_for:
                add     eax, ecx        ; EAX to zmienna J. j=j+i
                loop    petla_for       ; zmniejsz ECX o 1 i jesli rozny od
                                        ;    zera, skocz do: petla_for

   Pamietajmy jednak, ze instrukcja LOOP dziala tylko na rejestrze (E)CX,
   wiec jesli chcemy miec kilka zagniezdzonych petli, to przed kazda z
   nich (rozpoczynajaca sie zmiana rejestru ECX) musimy zachowac ten
   rejestr (na przyklad na stosie), a po zakonczeniu petli musimy
   przywrocic jego poprzednia wartosc.

   Sprawa z petlami o nieznanej ilosci powtorzen nie jest o wiele
   trudniejsza. Petla typu "for" jest calkowicie rownowazna petli
   "while". Wlasnie z tego skorzystamy, a kod niewiele bedzie sie roznic
   budowa od poprzedniego przykladu.
   Powiedzmy, ze mamy taka petle:
   (przeskocz ten przyklad)
        for (i=100; i+1<=n; i=i+2)
        {
                j=j+i+4;
        }

   Mozemy ja zastapic rownowazna konstrukcja:
   (przeskocz zamiane for na while)
        i=100;
        while (i+1 <= n)
        {
                j=j+i+4;
                i=i+2;
        }

   Otrzymujemy kod:
   (przeskocz tlumaczenie while)
                mov     ecx, 100        ; ECX to zmienna I. i=100
        nasza_petla:
                mov     ebx, ecx
                add     ebx, 1          ; EBX = i+1
                cmp     ebx, [n]        ; sprawdzamy, czy i+1 <= n
                ja      koniec_while    ; wychodzimy, gdy i+1 > n

                add     eax, ecx        ; EAX to zmienna J. j=j+i
                add     eax, 4          ; j=j+i+4

                add     ecx, 2          ; i=i+2
                jmp     short nasza_petla
        koniec_while:

   Ostatni rodzaj petli to petle typu "do-while" (repeat...until). Taka
   petla rozni sie tym od poprzedniczek, ze warunek jest sprawdzany po
   wykonaniu kodu petli (czyli taka petla zawsze bedzie wykonana co
   najmniej raz). Daje to pewne mozliwosci optymalizacji kodu.
   Popatrzmy na taki przyklad:
   (przeskocz przyklad "do-while")
        do
        {
                i=i+1;
                j=j-1;
        } while ((i < n) && (j > 1));

   Warunek wyjscia to: i wieksze badz rowne n LUB j mniejsze badz rowne
   1.
   A teraz popatrzcie, co mozna z tym zrobic:
   (przeskocz tlumaczenie "do-while")
        petla_do:
                add     ecx, 1          ; ECX to zmienna I. i=i+1
                sub     edx, 1          ; EDX to zmienna J. j=j-1

                cmp     ecx, [n]
                jae     koniec          ; i >= n jest jednym z warunkow
                                        ; wyjscia. Drugiego nie musimy
                                        ; sprawdzac, bo wynik i tak
                                        ; bedzie prawda
                cmp     edx, 1
                jbe     koniec          ; j <= 1 to drugi warunek wyjscia

                jmp     petla_do
        koniec:

   Mozna przepisac to w lepszy sposob:
   (przeskocz lepszy sposob)
        petla_do:
                add     ecx, 1          ; ECX to zmienna I. i=i+1
                sub     edx, 1          ; EDX to zmienna J. j=j-1

                cmp     ecx, [n]
                jae     koniec          ; i >= n jest jednym z warunkow
                                        ; wyjscia. Drugiego nie musimy
                                        ; sprawdzac, bo wynik i tak
                                        ; bedzie prawda

                                        ; jesli nadal tutaj jestesmy,
                                        ; to z pewnoscia i < n.
                cmp     edx, 1
                ja      petla_do        ; j <= 1 to drugi warunek
                                        ; wyjscia. Jesli j > 1,
                                        ; to kontynuuj wykonywanie petli.
                                        ; Jesli j < 1, to po prostu
                                        ; opuszczamy petle:
        koniec:

   Jesli warunek kontynuacji lub wyjscia z petli jest wyrazeniem
   zlozonym, to:
     * jesli sklada sie z alternatyw (dzialan typu OR, ||), to na
       pierwszym miejscu sprawdzajcie te warunki, ktore maja najwieksza
       szanse byc prawdziwe. Oszczedzicie w ten sposob czasu na
       bezsensowne sprawdzanie reszty warunkow (wynik i tak bedzie
       prawda).
     * jesli sklada sie z koniunkcji (dzialan typu AND, &&), to na
       pierwszym miejscu sprawdzajcie te warunki, ktore maja najwieksza
       szanse byc falszywe. Wynik calosci i tak bedzie falszem.

   Przyklady:
        1)   a == 0 || (b > 1 && c < 2)
        2)   (b < d || a == 1) && c > 0

   W przypadku 1 najpierw sprawdzamy, czy "a" jest rowne zero. Jesli
   jest, to caly warunek jest prawdziwy. Jesli nie jest, sprawdzamy
   najpierw ten z dwoch pozostalych, ktory ma najwieksza szanse bycia
   falszywym (jesli ktorys jest falszywy, to wynik jest falszem).
   W przypadku 2 najpierw sprawdzamy, czy "c" jest wieksze od zera. Jesli
   nie jest, to caly warunek jest falszywy. Jesli jest, to potem
   sprawdzamy ten z pozostalych warunkow, ktory ma wieksza szanse bycia
   prawdziwym (jesli ktorys jest prawdziwy, to wynik jest prawda).
     _________________________________________________________________

Decyzje wielowariantowe (wyrazenia typu switch/case)

   (przeskocz decyzje wielowariantowe)

   Fragment kodu:
   (przeskocz schemat switch/case)
        switch (a)
        {
                case 1: .....
                case 2: .....
                ....
                default: .....
        }

   w prosty sposob rozklada sie na serie wyrazen "if" i "else if" (oraz
   "else", o ile podano sekcje "default"). Te zas juz umiemy przedstawiac
   w asemblerze. Jest jednak jedna ciekawa sprawa: jesli wartosci
   poszczegolnych przypadkow case sa zblizone (cos w stylu 1, 2, 3 a nie
   1, 20, 45), to mozemy posluzyc sie tablica skokow (ang. jump table). W
   tej tablicy przechowywane sa adresy fragmentow kodu, ktory ma zostac
   wykonany, gdy zajdzie odpowiedni warunek. Brzmi to troche pokretnie,
   dlatego szybko pokaze przyklad.
   (przeskocz przyklad switch/case)
        switch (a)
        {
                case 1:
                        j=j+1;
                        break;
                case 2:
                        j=j+4;
                        break;
                case 4:
                        j=j+23;
                        break;
                default:
                        j=j-1;
        }

   A teraz tlumaczenie:
   (przeskocz tlumaczenie przykladu switch/case)
                mov     eax, [a]
                cmp     eax, 1                  ; jesli a < 1 lub a > 5,
                                                ; to na pewno default
                jb      sekcja_default

                cmp     eax, 5
                ja      sekcja_default

                jmp     [przypadki + eax*4 - 4]

        przyp1:
                add     dword ptr [j], 1        ; NASM/FASM: bez slowa PTR
                jmp     koniec

        przyp2:
                add     dword ptr [j], 4        ; NASM/FASM: bez slowa PTR
                jmp     koniec

        przyp4:
                add     dword ptr [j], 23       ; NASM/FASM: bez slowa PTR
                jmp     koniec

        sekcja_default:
                sub     dword ptr [j], 1

        koniec:

        ....
        przypadki:      dd przyp1, przyp2, sekcja_default, przyp4

   Kod najpierw sprawdza, czy "a" ma szanse byc w ktoryms z przypadkow
   (jesli nie jest, to oczywiscie wykonujemy sekcje "default"). Potem,
   jesli a=1, to skacze pod etykiete w
   zmienne [przypadki + 1*4 - 4 ] = [przypadki] = przyp1.
   Podobnie, jesli a=2, skoczymy do "przyp2". Jesli a=3, skoczymy do
   sekcji "default", a jesli a=4, skoczymy do sekcji "przyp4".

   Dla programow 64-bitowych, nalezy uzyc mnoznika wynoszacego 8 i
   takiego samego rozmiaru kazdego elementu w tablicy:
                jmp     [przypadki + eax*8 - 8]
        ....
        przypadki:      dq przyp1, przyp2, sekcja_default, przyp4

   Od razu widac wielka zalete takiego rozwiazania: w jednej jedynej
   instrukcji wiemy, gdzie musimy skoczyc. Jak liczba przypadkow bedzie
   wzrastac, zauwazymy tez wade tego rozwiazania: rozmiar tablicy szybko
   rosnie (wynosi on roznice miedzy wartoscia najwyzsza mozliwa a
   najnizsza mozliwa pomnozona przez 2 bajty). Dlatego to rozwiazanie
   jest nieprzydatne dla mozliwych wartosci: {1, 20, 45} (42 wartosci z
   45 bylyby nieuzywane, czyli wskazujace na sekcje "default" -
   zdecydowane marnotrawienie pamieci). W takim przypadku lepiej uzyc
   "tradycyjnej" metody if/else if/else.
     _________________________________________________________________

   Mam nadzieje, ze wiedza zawarta w tej czesci kursu umozliwi Wam
   pisanie lepszych i bardziej zlozonych programow niz to bylo do tej
   pory. Teraz bedziecie wiedziec, co tak wlasciwie robie kompilatory,
   tlumaczac niektore wyrazenia kontrolne. Ta wiedza pomoze Wam pisac
   lepsze programy w jezykach wyzszego poziomu (gdyz juz teraz wiecie,
   jak zapisywac wyrazenia logiczne tak, by dostac najbardziej wydajny
   kod).

   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. Zaimplementowac zdanie:
       Jesli EAX jest rowny EBX lub ECX nie jest rowny EBP, to do EDX
       wstaw EAX, inaczej do ECX wstaw 0.
    2. Zaimplementowac zdanie (uzyc instrukcji warunkowego przesuwania):
       Jesli EAX jest rowny EBX lub ECX nie jest rowny EBP, to do EDX
       wstaw EAX, inaczej do EDX wstaw 0.
    3. Napisac program, ktory liczy sume liczb od 10 do dowolnej liczby
       wpisanej w kodzie/czytanej z linii polecen.
    4. Zaimplementowac zdanie:
       Dopoki ECX jest wieksze od 1, zmniejsz ECX o 2.
    5. Zaimplementowac zdanie:
       Zwiekszaj EAX o 3, dopoki EAX jest mniejsze od 100.
