   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 2 - Pamiec, czyli gdzie upychac cos, co sie nie miesci w procesorze

   Poznalismy juz rejestry procesora. Jak widac, jest ich ograniczona
   liczba i nie maja one zbyt duzego rozmiaru. Rejestry ogolnego
   przeznaczenia sa co najwyzej 32-bitowe (czterobajtowe). Dlatego czesto
   programista musi niektore zmienne umieszczac w pamieci. Przykladem
   tego byl napis, ktory wyswietlalismy w poprzedniej czesci artykulu.
   Byl on zadeklarowany dyrektywa DB, co oznacza "declare byte". Ta
   dyrektywa niekoniecznie musi deklarowac dokladnie 1 bajt. Tak jak
   widzielismy, mozna nia deklarowac napisy lub kilka bajtow pod rzad.
   Teraz omowimy rodzine dyrektyw sluzacych wlasnie do rezerwowania
   pamieci.

   Ogolnie, zmienne mozna deklarowac jako bajty (dyrektywa DB, cos jak
   char w jezyku C), slowa (word = 16 bitow = 2 bajty, cos jak short w C)
   dyrektywa DW, podwojne slowa DD (double word = dword = 32bity = 4
   bajty, jak long w C), potrojne slowa pword = 6 bajtow - PW, poczworne
   slowa DQ (quad word = qword = 8 bajtow, typ long long), tbyte = 10
   bajtow - DT (typ long double w C).

   Sekcja kodu jest tylko do odczytu, wiec zmienne, ktore chcemy moc
   rzeczywiscie zmienic, musimy umiescic w sekcji danych. Od tej pory
   umawiamy sie wiec, ze kazda zmienna znajduje sie w obszarze "section
   .data" (dla NASMa) lub "segment readable writeable" (dla FASMa).

   Przyklady:
   (przeskocz przyklady)
        section .data
        ; FASM: segment readable writeable

        dwa             db 2
        szesc_dwojek    db 2, 2, 2, 2, 2, 2 ; tablica szesciu bajtow
        litera_g        db "g"
        _ax             dw 4c00h        ; dwubajtowa liczba calkowita
        alfa            dd 12348765h    ; czterobajtowa liczba calkowita

        ;liczba_a       dq 1125         ; osmiobajtowa liczba calkowita.
                                        ; FASM przyjmie, NASM
                                        ; starszy niz wersja 2.00 nie.

                                        ; dla NASMa zamienimy to na
                                        ; postac rownowazna: 2 razy
                                        ; po 4 bajty:
        liczba_a        dd 1125, 0

        liczba_e        dq 2.71         ; liczba zmiennoprzecinkowa
                                        ; podwojnej precyzji (8 bajtow),

                                ; dziesieciobajtowa liczba calkowita:
        ;duza_liczba    dt 6af4aD8b4a43ac4d33h
                                ; FASM ani NASM tego nie przyjmie.
                                ; Zrobimy to tak:
        duza_liczba     dd 43ac4d33h, 0f4aD8b4ah; czemu z zerem z przodu?
                                                ; Czytaj dalej
                        db 6ah

        pi              dt 3.141592     ; FASM i NASM

        ;nie_init       db ?    ; nie zainicjalizowany bajt.
                                ; Wartosc nieznana. NASM ani FASM tak
                                ; tego nie przyjmie. Nalezy uzyc:

        nie_init        resb 1          ; NASM
        ;nie_init       rb 1            ; FASM

        napis1          db "NaPis1."
        xxx             db 1
                        db 2
                        db 3
                        db 4

   Zwroccie uwage na sposob rozbijania duzych liczb na poszczegolne
   bajty: najpierw deklarowane sa mlodsze bajty, a potem starsze (na
   przyklad "dd 11223344h" = "db 44h, 33h, 22h, 11h"). To dziala, gdyz
   procesory Intela i AMD (i wszystkie inne klasy x86) sa procesorami
   typu "little-endian", co znaczy, ze najmlodsze bajty danego ciagu
   bajtow sa umieszczane przez procesor w najnizszych adresach pamieci.
   Dlatego my tez tak deklarujemy nasze zmienne.

   Ale z kolei takie cos:
        beta    db aah

   nie podziala. Dlaczego? KAZDA liczba musi zaczynac sie od cyfry. Jak
   to obejsc? Tak:
        beta    db 0aah

   czyli poprzedzic zerem.

   Nie podziala rowniez to:
        0gamma  db      9

   Dlaczego? Etykiety (dotyczy to tak danych, jak i kodu programu) nie
   moga zaczynac sie od cyfr.

   Zapisanie kilku wartosci po dyrektywie Dx (DB, DW, DD, i tak dalej)
   automatycznie tworzy tablice elementow odpowiedniego rozmiaru o tych
   wartosciach, z ktorych kazda nastepna jest tuz po poprzedniej w
   pamieci. Na przyklad, nastepujaca dyrektywa tworzy tak naprawde
   tablice szesciu bajtow o wartosci 2, a nie probuje z szesciu dwojek
   utworzyc wartosc, ktora potem umiesci w pojedynczym bajcie:
        szesc_dwojek    db 2, 2, 2, 2, 2, 2

   A co, jesli chcemy zadeklarowac zmienna, powiedzmy, skladajaca sie z
   234 bajtow rownych zero? Trzeba je wszystkie napisac?
   Alez skad! Nalezy uzyc operatora TIMES. Odpowiedz na pytanie brzmi:
        section .data

        zmienna         TIMES    234    db      0
        nazwa                   liczba   typ    co zduplikowac

   lub, w skladni FASMa:
        segment readable writeable

        ; 234 razy zarezerwuj bajt wartosci 0:
        zmienna2:       times    234    db      0

   Rezerwacja obszaru bez okreslania jego wartosci wygladalaby mniej
   wiecej tak:
        section .data
        ; FASM: segment readable writeable

        zmienna         resb    234             ; NASM
        zmienna2        rb      234             ; FASM

   A co, jesli chcemy miec dwuwymiarowa tablice podwojnych slow o
   wymiarach 25 na 34?
   Robimy dla NASMa na przyklad tak:
        section .data

        Tablica         times   25*34   dd      0

   a dla FASMa:
        segment readable writeable

        ; 25*34 razy zarezerwuj dword wartosci 0:
        Tablica2:       times   25*34   dd      0

   Do obslugi takich tablic przydadza sie bardziej skomplikowane sposoby
   adresowania zmiennych. O tym za moment.

   Zmiennych trzeba tez umiec uzywac.
   Do uzyskania adresu danej zmiennej uzywa sie nazwy tej zmiennej, tak
   jak widzielismy wczesniej. Zawartosc zmiennej otrzymuje sie poprzez
   umieszczenie jej nazwy w nawiasach kwadratowych. Oto przyklad:
        section .data
        ; FASM: segment readable writeable

        rejestr_eax     dd      1
        rejestr_bx      dw      0
        rejestr_cl      db      0
        ...
                mov     [rejestr_bx], bx
                mov     cl, [rejestr_cl]
                mov     eax, [rejestr_eax]
                int     80h

   Zauwazcie zgodnosc rozmiarow zmiennych i rejestrow.
   Mozemy jednak miec problem w skompilowaniu czegos takiego:
        mov     [jakas_zmienna], 2

   Dlaczego? Kompilator wie, ze gdzies zadeklarowalismy "jakas_zmienna",
   ale nie wie, czy bylo to
        jakas_zmienna   db      0

   czy
        jakas_zmienna   dw      22

   czy moze
        jakas_zmienna   dd      "g"

   Chodzi o to, aby pokazac, jaki rozmiar ma obiekt docelowy. Nie bedzie
   problemow, gdy napiszemy:
        mov     word [jakas_zmienna], 2

   I to obojetnie, czy zmienna byla bajtem (wtedy nastepny bajt bedzie
   rowny 0), czy slowem (wtedy bedzie ono mialo wartosc 2) czy moze
   podwojnym slowem lub czyms wiekszym (wtedy 2 pierwsze bajty zostana
   zmienione, a pozostale nie). Dzieje sie tak dlatego, ze zmienne
   zajmuja kolejne bajty w pamieci, najmlodszy bajt w komorce o
   najmniejszym adresie. Na przyklad:
        xxx     dd      8

   jest rownowazne:
        xxx     db      8,0,0,0

   oraz:
        xxx     db      8
                db      0
                db      0
                db      0

   Te przyklady nie sa jedynymi sposobami adresowania zmiennych (poprzez
   nazwe). Na procesorach 32-bitowych (od 386) odnoszenie sie do pamieci
   moze odbywac sie wg schematu:
   [ zmienna + rej_baz + rej_ind * skala +- liczba ]

   gdzie:
     * "zmienna" oznacza nazwe zmiennej i jest to liczba obliczana przez
       kompilator lub linker
     * rej_baz (rejestr bazowy) = jeden z rejestrow EAX, EBX, ECX, EDX,
       ESI, EDI, EBP, ESP
     * rej_ind (rejestr indeksowy) = jeden z rejestrow EAX, EBX, ECX,
       EDX, ESI, EDI, EBP (bez ESP)
     * mnoznik (scale) = 1, 2, 4 lub 8 (gdy nie jest podany, przyjmuje
       sie 1)

   Przyklady:
        mov     al, [ nazwa_zmiennej+2 ]
        mov     [ edi-23 ], cl
        mov     dl, [ ebx + esi*2 + nazwa_zmiennej+18 ]

   Na procesorach 64-bitowych odnoszenie sie do pamieci moze odbywac sie
   wg schematu:
   [ zmienna + rej_baz + rej_ind * skala +- liczba ]

   gdzie:
     * "zmienna" oznacza nazwe zmiennej i jest to liczba obliczana przez
       kompilator lub linker
     * rej_baz (rejestr bazowy) = jeden z rejestrow RAX, RBX, RCX, RDX,
       RSI, RDI, RBP, RSP, R8, ..., R15, a nawet RIP (ale wtedy nie mozna
       uzyc zadnego rejestru indeksowego)
     * rej_ind (rejestr indeksowy) = jeden z rejestrow RAX, RBX, RCX,
       RDX, RSI, RDI, RBP, R8, ..., R15 (bez RSP i RIP)
     * mnoznik (scale) = 1, 2, 4 lub 8 (gdy nie jest podany, przyjmuje
       sie 1)

   Dwie zasady:
     * miedzy nawiasami kwadratowymi nie mozna mieszac rejestrow roznych
       rozmiarow
     * w trybie 64-bitowym nie mozna do adresowania uzywac rejestrow
       czastkowych: R*D, R*W, R*B.

   Przyklady:
        mov     al, [ nazwa_zmiennej+2 ]
        mov     [ rdi-23 ], cl
        mov     dl, [ rbx + rsi*2 + nazwa_zmiennej+18 ]
        mov     rax, [rax+rbx*8-34]
        mov     rax, [ebx]
        mov     r8d, [ecx-11223344]
        mov     cx, [r8]

   A teraz inny przyklad: sprobujemy wczytac 5 elementow o numerach 1, 3,
   78, 25, i 200 (pamietajmy, ze liczymy od zera) z tablicy "zmienna"
   (tej o 234 bajtach, zadeklarowanej wczesniej) do kilku rejestrow
   8-bitowych. Operacja nie jest trudna i wyglada po prostu tak:
        mov     al, [ zmienna + 1 ]
        mov     ah, [ zmienna + 3 ]
        mov     cl, [ zmienna + 78 ]
        mov     ch, [ zmienna + 25 ]
        mov     dl, [ zmienna + 200 ]

   Oczywiscie, kompilator nie sprawdzi za Was, czy takie elementy tablicy
   rzeczywiscie istnieja - o to musicie zadbac sami.

   W powyzszym przykladzie rzuca sie w oczy, ze ciagle uzywamy slowa
   "zmienna", bo wiemy, gdzie jest nasza tablica. Jesli tego nie wiemy
   (dynamiczne przydzielanie pamieci), lub z innych przyczyn nie chcemy
   ciagle pisac "zmienna", mozemy posluzyc sie bardziej zlozonymi
   sposobami adresowania. Po chwili zastanowienia bez problemu
   stwierdzicie, ze powyzszy kod mozna bez problemu zastapic czyms takim
   i tez bedzie dzialac:
        mov     ebx, zmienna
        mov     al, [ ebx + 1 ]
        mov     ah, [ ebx + 3 ]
        mov     cl, [ ebx + 78 ]
        mov     ch, [ ebx + 25 ]
        mov     dl, [ ebx + 200 ]

   Teraz trudniejszy przyklad: sprobujmy dobrac sie do kilku elementow
   dwuwymiarowej tablicy dwordow zadeklarowanej wczesniej (tej o
   rozmiarze 25 na 34). Mamy 25 wierszy po 34 elementy kazdy. Aby do EAX
   wpisac pierwszy element pierwszego wiersza, piszemy oczywiscie tylko:
        mov     eax, [Tablica]

   Ale jak odczytac 23 element 17 wiersza? Otoz, sprawa nie jest taka
   trudna, jakby sie moglo wydawac. Ogolny schemat wyglada tak (zakladam,
   ze ostatni wskaznik zmienia sie najszybciej, potem przedostatni itd. -
   pamietamy, ze rozmiar elementu wynosi 4):
        Tablica[17][23] = [ Tablica + (17*dlugosc wiersza + 23)*4 ]

   No to piszemy:
        mov     ebx, Tablica
        mov     esi, 17
   jakas_petla:
        imul    esi, 34         ; ESI=ESI*34=17 * dlugosc wiersza
        add     esi, 23         ; ESI=ESI+23=17 * dlugosc wiersza + 23
        mov     eax, [ ebx + esi*4 ]    ; mnozymy numer elementu przez
                                        ; rozmiar elementu
        ...

   Mozna bylo to zrobic po prostu tak:
        mov     eax, [ Tablica + (17*34 + 23)*4 ]

   ale poprzednie rozwiazanie (na rejestrach) jest wprost idealne do
   petli, w ktorej robimy cos z coraz to innym elementem tablicy.

   Podobnie ("(numer_wiersza*dlugosc_wiersza1 +
   numer_wiersza*dlugosc_wiersza2 + ... )*rozmiar_elementu") adresuje sie
   tablice wielowymiarowe. Schemat jest nastepujacy:
        Tablica[d1][d2][d3][d4]    - 4 wymiary o dlugosciach wierszy
                                                 d1, d2, d3 i d4

        Tablica[i][j][k][m] = [ Tablica + (i*d2*d3*d4+j*d3*d4+k*d4+m)*
                                *rozmiar_elementu ]

   Teraz powiedzmy, ze mamy taka tablice:
        dword tab1[24][78][13][93]

   Aby dobrac sie do elementu tab1[5][38][9][55], piszemy:
        mov     eax, [ tab1 + (5*78*13*93 + 38*13*93 + 9*93 + 55)*4 ]

   Pytanie: do jakich segmentow sie to odnosi? Przeciez mamy kilka
   rejestrow segmentowych, ktore moga wskazywac na zupelnie co innego.
   Odpowiedz:
   Na rejestrach 32-bitowych mamy:
    1. jesli pierwszym w kolejnosci rejestrem jest EBP lub ESP, uzywany
       jest SS
    2. w pozostalych przypadkach uzywany jest DS

   W systemach 64-bitowych segmenty odchodza w zapomnienie.
   Domyslne ustawianie mozna zawsze obejsc uzywajac przedrostkow, na
   przyklad
        mov     ax, [ss:si]
        mov     [gs:eax+ebx*2-8], cx
     _________________________________________________________________

Organizacja pamieci w Linuksie

   W systemie Linux kazdy program dostaje swoja wlasna przestrzen, nie
   jest mozliwe zapisywanie zmiennych lub kodu innych programow (z
   wyjatkami, na przyklad debugery). Teoretycznie rozmiar owej
   przestrzeni wynosi tyle, ile mozna zaadresowac w ogole calym
   procesorem, czyli 2^32 = 4 GB na procesorach 32-bitowych. Obszar ten
   jest jednak od gory troche ograniczony przez sam system, ale nie
   bedziemy sie tym zajmowac.

   Struktura programu po uruchomieniu jest dosc prosta: caly kod, dane i
   stos (o tym za chwile) znajduja sie w jednym segmencie, rozciagajacym
   sie na cala wspomniana przestrzen. Na moim systemie wykonywanie
   zaczyna sie pod adresem 08048080h w tej przestrzeni.
   (przeskocz ilustracje pamieci programu w Linuksie)
                +-----------------------+
                |       BFFFFFFF        |
                |    Stos, argumenty    |
                +-     zm. lokalne     -+
                |        .....          |
                +-       .....         -+
                |  Dane, zm. globalne   |
                |      (statyczne)      |
                +-       .....         -+
                |        kod            |
                +-       .....         -+
                |       08048080h       |
       CS=DS=SS +-----------------------+

   Najnizej w pamieci znajduje sie kod, za nim dane, a na koncu - stos.

   Jak w takim razie realizowana jest ochrona kodu przed zapisem?
   W samym procesorze istnieje mechanizm stronicowania, ktory umozliwia
   przyznanie odpowiednich praw do danych stron pamieci (zwykle strona ma
   4kB). Tak wiec, nasz duzy segment jest podzielony na strony z kodem,
   danymi i stosem.
     _________________________________________________________________

Stos

   Przyszla pora na omowienie, czym jest stos.
   Otoz, stos jest po prostu kolejnym segmentem pamieci. Sa na nim
   umieszczane dane tymczasowe, na przyklad adres powrotny z funkcji, jej
   parametry wywolania, jej zmienne lokalne. Sluzy tez do zachowywania
   zawartosci rejestrow.
   Obsluga stosu jest jednak zupelnie inna.

   Po pierwsze, stos jest "budowany" od gory na dol! Rysunek bedzie
   bardzo pomocny:
   (przeskocz rysunek stosu)

        Adres
                        SS
                +-------------------+
        100h    |                   |
                +-------------------+   <----- ESP = 100h
        0FEh    |                   |
                +-------------------+
        0FCh    |                   |
                +-------------------+
        0FAh    |                   |
                +-------------------+
        0F8h    |                   |
                +-------------------+
        0F6h    |                   |
        ...             ....

   Na tym rysunku ESP=100h, czyli ESP wskazuje na komorke o adresie 100h
   w segmencie SS.

   Dane na stosie umieszcza sie instrukcja PUSH a zdejmuje instrukcja
   POP. PUSH jest rownowazne parze instrukcji:
        sub     esp, ..    ; odejmowana liczba zalezy od
                           ; rozmiaru obiektu w bajtach
        mov     [ss:esp], ..

   a POP:
        mov     .., [ss:esp]
        add     esp, ..

   Tak wiec, po wykonaniu instrukcji PUSH AX i PUSH DX powyzszy stos
   bedzie wygladal tak:
   (przeskocz ilustracje dzialania PUSH)
        Stos po wykonaniu  PUSH AX i PUSH DX, czyli
                sub     esp, 2
                mov     [ss:esp], ax
                sub     esp, 2
                mov     [ss:esp], dx

                        SS
                +-------------------+
        100h    |                   |
                +-------------------+
        0FEh    |       AX          |
                +-------------------+
        0FCh    |       DX          |
                +-------------------+   <----- ESP = 0FCh
        ...             ....

   SP=0FCh, pod [SP] znajduje sie wartosc DX, a pod [SP+2] - wartosc AX.
   A po wykonaniu instrukcji POP EBX (tak, mozna zdjac dane do innego
   rejestru, niz ten, z ktorego pochodzily):
   (przeskocz ilustracje dzialania POP)
        Stos po wykonaniu POP EBX, czyli
                mov     ebx, [ss:esp]
                add     esp, 4

                        SS
                +-------------------+
        100h    |                   |
                +-------------------+   <----- ESP = 100h
        0FEh    |       AX          |
                +-------------------+
        0FCh    |       DX          |
                +-------------------+
        ...             ....

   Teraz ponownie SP=100h. Zauwazcie, ze dane sa tylko kopiowane ze
   stosu, a nie z niego usuwane. Ale w zadnym przypadku nie mozna na nich
   juz polegac. Dlaczego? Zobaczycie zaraz.
   Najpierw bardzo wazna uwaga, ktora jest wnioskiem z powyzszych
   rysunkow.
   Dane (ktore chcemy z powrotem odzyskac w niezmienionej postaci)
   polozone na stosie instrukcja PUSH nalezy zdejmowac kolejnymi
   instrukcjami POP W ODWROTNEJ KOLEJNOSCI niz byly kladzione. Zrobienie
   czegos takiego:
        push    eax
        push    edx
        pop     eax
        pop     edx

   nie przywroci rejestrom ich dawnych wartosci!
     _________________________________________________________________

Przerwania i procedury a stos

   Uzywalismy juz instrukcji przerwania, czyli INT. Przy okazji omawiania
   stosu nadeszla pora, aby powiedziec, co ta instrukcja w ogole robi.
   Otoz, INT jest (w przyblizeniu) rownowazne temu pseudo-kodowi:
        pushfd                  ; wloz na stos rejestr stanu procesora
                                ; czyli flagi
        push    cs              ; segment, w ktorym aktualnie pracujemy
        push    eip_next        ; adres instrukcji po INT
        jmp     procedura_obslugi_przerwania

   Kazda procedura obslugi przerwania (Interrupt Service Routine, ISR)
   konczy sie instrukcja IRET (interrupt return), ktora odwraca powyzszy
   kod, czyli z ISR procesor wraca do dalszej obslugi naszego programu.

   Jednak oprocz instrukcji INT przerwania moga byc wywolane w inny
   sposob - przez sprzet. Tutaj wlasnie pojawiaja sie IRQ. Do urzadzen
   wywolujacych przerwania IRQ naleza miedzy innymi karta dzwiekowa,
   modem, zegar, kontroler dysku twardego, itd...

   Bardzo istotna role gra zegar, utrzymujacy aktualny czas w systemie.
   Jak napisalem w jednym z artykulow, tyka on z czestotliwoscia ok. 18,2
   Hz. Czyli ok. 18 razy na sekunde wykonywane sa 3 PUSHe a po nich 3
   POPy. Nie zapominajmy o push i pop wykonywanych w samej ISR tylko po
   to, aby zachowac modyfikowane rejestry. Kazdy PUSH zmieni to, co jest
   ponizej ESP.

   Dlatego wlasnie zadne dane ponizej ESP nie moga byc uznawane za
   wiarygodne.

   Gdzie zas znajduja sie adresy procedur obslugi przerwan?
   W pamieci, w Tabeli Deskryptorow Przerwan (Interrupt Descriptor Table,
   IDT), do ktorej dostep ma wylacznie system operacyjny. Na pojedynczy
   deskryptor przerwania sklada sie oczywiscie adres procedury obslugi
   przerwania, jej deskryptor, prawa dostepu do niej i kilka innych
   informacji, ktore z punktu widzenia programisty nie sa (na razie)
   istotne.

   Mniej skomplikowana jest instrukcja CALL, ktora sluzy do wywolywania
   zwyklych procedur, na przyklad:
        call proc1              ; wywolanie proste
        call [adres_proc1]      ; wywolanie procedury, ktorej adres
                                ; jest w zmiennej adres_proc1
        ...
proc1:
        ...
        ret

   W zaleznosci od rodzaju procedury (near - zwykle w tym samym
   pliku/programie, far - na przyklad w innym pliku/segmencie),
   instrukcja CALL wykonuje takie cos:
        push    cs i kilka innych rzeczy ; tylko jesli FAR
        push    eip_next        ; adres instrukcji po CALL

   Procedura moze zawierac dowolne (nawet niesymetryczne ilosci
   instrukcji PUSH i POP), ale pod koniec ESP musi byc taki sam, jak byl
   na poczatku, czyli wskazywac na prawidlowy adres powrotu, ktory ze
   stosu jest zdejmowany instrukcja RET (lub RETF). Dlatego nieprawidlowe
   jest takie cos:
        zla_procedura:
                push    eax
                push    ebx
                add     eax, ebx
                ret

   gdyz w chwili wykonania instrukcji RET na wierzchu stosu jest EBX, a
   nie adres powrotny! Blad stosu jest przyczyna wielu trudnych do
   znalezienia usterek w programie.
   Jak to poprawic bez zmiany sensu? Na przyklad tak:
        moja_procedura:
                push    eax
                push    ebx
                add     eax, ebx
                add     esp, 8
                ret

   Teraz juz wszystko powinno byc dobrze. ESP wskazuje na dobry adres
   powrotny. Dopuszczalne jest tez takie cos:
        proc1:
                push    eax
                cmp     eax, 0          ; czy EAX jest zerem?
                je      koniec1         ; jesli tak, to koniec1

                pop     ebx
                ret
        koniec1:
                pop     ecx
                ret

   ESP ciagle jest dobrze ustawiony przy wyjsciu z procedury mimo, iz
   jest 1 PUSH a 2 POPy.
   Po prostu ZAWSZE nalezy robic tak, aby ESP wskazywal na poprawny adres
   powrotny, niezaleznie od sposobu. W sklad tego wchodzi definiowanie
   procedur pod glownym programem (po ostatnich instrukcjach zamykajacych
   program). Dlaczego? Niektore (najprostsze) formaty plikow
   wykonywalnych nie pozwalaja na okreslenie poczatku programu i takie
   programy sa wykonywane po prostu z gory na dol. Jesli u gory kodu
   umiesci sie procedury, zostana one wykonane, po czym instrukcja RET
   (lub RETF) spowoduje zamkniecie programu (w najlepszym przypadku) lub
   wejscie procesora na nieprawidlowe lub losowe instrukcje w pamieci.
     _________________________________________________________________

Alokacja zmiennych lokalnych procedury

   Nie musi sie to Wam od razu przydac, ale przy okazji stosu omowie,
   gdzie znajduja sie zmienne lokalne funkcji (na przyklad takich w
   jezyku C) oraz jak rezerwowac na nie miejsce.

   Gdy program wykonuje instrukcje CALL, na stosie umieszczany jest adres
   powrotny (o czym juz wspomnialem). Jako ze nad nim moga byc jakies
   dane wazne dla programu (na przyklad zachowane rejestry, inne adresy
   powrotne), nie wolno tam nic zapisywac. Ale pod adresem powrotnym jest
   duzo miejsca i to tam wlasnie programy umieszczaja swoje zmienne
   lokalne.

   Samo rezerwowanie miejsca jest dosc proste: liczymy, ile lacznie
   bajtow nam potrzeba na wlasne zmienne i tyle wlasnie odejmujemy od
   rejestru ESP, robiac tym samym miejsce na stosie, ktore nie bedzie
   zamazane przez instrukcje INT i CALL (gdyz one zamazuja tylko to, co
   jest pod ESP).

   Na przyklad, jesli nasze zmienne zajmuja 8 bajtow (np.dwa DWORDy lub
   dwie 32-bitowe zmienne typu "int" w jezyku C), to odejmujemy te 8 od
   ESP i nasz nowy stos wyglada tak:
                        SS
                +-------------------+
        100h    |  adres powrotny   |
                +-------------------+   <----- stary ESP = 100h
        0FEh    |       wolne       |
                +-------------------+
        0FCh    |       wolne       |
                +-------------------+
        0FAh    |       wolne       |
                +-------------------+
        0F8h    |       wolne       |
                +-------------------+   <----- ESP = 0F8h

   ESP wynosi 0F8h, nad nim jest 8 bajtow wolnego miejsca, po czym adres
   powrotny i inne stare dane.

   Nie trzeba podawac typow zmiennych lokalnych, ich liczby ani ich
   nazywac - wystarczy obliczyc ich laczny rozmiar i ten rozmiar odjac od
   ESP. To, gdzie ktora zmienna faktycznie w pamieci sie znajdzie (lub
   inaczej: ktory obszar pamieci bedzie przypisany ktorej zmiennej),
   zalezy calkowicie od programisty - na przyklad [ESP] moze przechowywac
   pierwsza zmienna, a [ESP+4] - druga, ale moze byc tez calkiem na
   odwrot.

   Miejsce juz mamy, korzystanie z niego jest proste - wystarczy
   odwolywac sie do [ESP], [ESP+2], [ESP+4], [ESP+6]. Ale stanowi to
   pewien problem, bo po kazdym wykonaniu instrukcji PUSH, te cyferki sie
   zmieniaja (bo przeciez adresy sie nie zmieniaja, ale ESP sie zmienia).
   Dlatego wlasnie do adresowania zmiennych lokalnych czesto uzywa sie
   innego rejestru niz ESP. Jako ze domyslnym segmentem dla EBP jest
   segment stosu, wybor padl wlasnie na ten rejestr (oczywiscie, mozna
   uzywac dowolnego innego, tylko trzeba dostawiac SS: z przodu, co
   kosztuje za kazdym razem 1 bajt).

   Aby moc najlatwiej dostac sie do swoich zmiennych lokalnych, wiekszosc
   funkcji na poczatku zrownuje EBP z ESP, potem wykonuje rezerwacje
   miejsca na zmienne lokalne, a dopiero potem - zachowywanie rejestrow
   itp. (czyli swoje PUSHe). Wyglada to tak:
        push    ebp             ; zachowanie starego EBP
        mov     ebp, esp        ; EBP = ESP

        sub     esp, xxx        ; rezerwacja miejsca na zmienne lokalne
        push    rej1            ; tu ESP sie zmienia, ale EBP juz nie
        push    rej2
        ...

        ...
        pop     rej2            ; tu ESP znow sie zmienia, a EBP - nie
        pop     rej1

        mov     esp, ebp        ; zwalnianie zmiennych lokalnych
                                ;   mozna tez (ADD ESP,xxx)
        pop     ebp

        ret

   Niektore kompilatory umozliwiaja deklaracje procedury z parametrami,
   zmiennymi lokalnymi i ich typami:
        proc2 proc a:DWORD,b:DWORD
                LOCAL c:DWORD
                LOCAL d:DWORD
                LOCAL e:DWORD
                ...
                ret
        proc2 endp

   Mozna wtedy odwolywac sie do parametrow i zmiennych lokalnych przez
   ich nazwy, zamiast przez wyrazenia typu [ESP+nnn] i [ESP-nnn].

   Przy instrukcji MOV ESP, EBP napisalem, ze zwalnia ona zmienne
   lokalne. Zmienne te oczywiscie dalej sa na stosie, ale teraz sa juz
   ponizej ESP, a niedawno napisalem: zadne dane ponizej ESP nie moga byc
   uznawane za wiarygodne.

   Po pieciu pierwszych instrukcjach nasz stos wyglada tak:
                           SS
                +-----------------------+
                |    adres powrotny     |
                +-----------------------+
                |       stary EBP       |
                +-----------------------+       <----- EBP
                |      xxx bajtow       |
                |                       |
                |                       |
                +-----------------------+
                |         rej1          |
                +-----------------------+
                |         rej2          |
                +-----------------------+       <----- ESP

   Rejestr EBP wskazuje na stara wartosc EBP, zas ESP - na ostatni
   element wlozony na stos.
   I widac teraz, ze zamiast odwolywac sie do zmiennych lokalnych poprzez
   [ESP+liczba] przy ciagle zmieniajacym sie ESP, o wiele wygodniej
   odwolywac sie do nich przez [EBP-liczba] (zauwazcie: minus), bo EBP
   pozostaje niezmienione.

   Czesto na przyklad w disasemblowanych programach widac instrukcje typu
   AND ESP, NOT 16 (lub AND ESP, ~16 w skladni NASM). Jedynym celem
   takich instrukcji jest wyrownanie ESP do pewnej pozadanej granicy, na
   przyklad 16 bajtow (wtedy AND z wartoscia NOT 16, czyli FFFFFFF0h),
   zeby dostep do zmiennych lokalnych trwal krocej. Gdy adres zmiennej na
   przyklad czterobajtowej jest nieparzysty, to potrzeba dwoch dostepow
   do pamieci, zeby ja cala pobrac (bo mozna pobrac 32 bity z na raz w
   procesorze 32-bitowym i tylko z adresu podzielnego przez 4).

   Ogol danych: adres powrotny, parametry funkcji, zmienne lokalne i
   zachowane rejestry nazywany jest czasem ramka stosu (ang. stack
   frame).
   Rejestr EBP jest czasem nazywany wskaznikiem ramki, gdyz umozliwia od
   dostep do wszystkich istotnych danych poprzez stale przesuniecia
   (offsety, czyli te liczby dodawane i odejmowane od EBP): zmienne
   lokalne sa pod [EBP-liczba], parametry funkcji przekazane z zewnatrz -
   pod [EBP+liczba], zas pod [EBP] jest stara wartosc EBP. Jesli
   wszystkie funkcje w programie zaczynaja sie tym samym prologiem: PUSH
   EBP / MOV EBP, ESP, to po wykonaniu instrukcji MOV EBP, [EBP] w EBP
   znajdzie sie wskaznik ramki ... procedury wywolujacej. Jesli znamy jej
   strukture, mozna w ten sposob dostac sie do jej zmiennych lokalnych.
     _________________________________________________________________

   Zainteresowanych szczegolami adresowania lub instrukcjami odsylam do
   Intela i AMD

   Nastepnym razem o podstawowych instrukcjach jezyka asembler.

     - Ilu programistow potrzeba, aby wymienic zarowke?
     - Ani jednego. To wyglada na problem sprzetowy.

   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. Zadeklaruj tablice 12 zmiennych majacych po 10 bajtow:
         1. zainicjalizowana na zera (pamietaj o ograniczeniach
            kompilatora)
         2. niezainicjalizowana
    2. Zadeklaruj tablice 12 slow (16-bitowych) o wartosci BB
       (szesnastkowo), po czym do kazdego z tych slow wpisz wartosc FF
       szesnastkowo (bez zadnych petli). Mozna (a nawet trzeba) uzyc
       wiecej niz 1 instrukcji. Pamietaj o odleglosciach miedzy
       poszczegolnymi elementami tablicy. Naucz sie roznych sposobow
       adresowania: liczba (nazwa zmiennej + numer), baza (rejestr bazowy
       + liczba), baza + indeks (rejestr bazowy + rejestr indeksowy).
    3. Zadeklaruj dwuwymiarowa tablice bajtow o wartosci 0 o wymiarach 13
       wierszy na 5 kolumn, po czym do elementu numer 3 (przedostatni) w
       wierszu o numerze 12 (ostatni) wpisz wartosc FF. Sprobuj uzyc
       roznych sposobow adresowania.
