   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 4 - Pierwsze programy, czyli przelamywanie pierwszych lodow

   Znamy juz rejestry, troche instrukcji i zasad. No ale teoria jest
   niczym bez praktyki. Dlatego w tej czesci przedstawie kilka wzglednie
   prostych programow, ktore powinny rozbudzic wyobraznie tworzenia.

   Ten program spyta sie uzytkownika o imie i przywita sie z nim:
   (przeskocz program pytajacy o imie)
        ; Program witajacy sie z uzytkownikiem po imieniu
        ;
        ; Autor: Bogdan D.
        ; kontakt: bogdandr (at) op (dot) pl
        ;
        ; kompilacja:
        ; nasm -f elf czesc.asm
        ; ld -s -o czesc czesc.o
        ;
        ; kompilacja FASM:
        ; fasm czesc.asm czesc

        ; dla FASMa:
        ;format ELF executable
        ;entry _start

        ;segment readable executable            ; poczatek sekcji kodu

        ; dla NASMa:
        section .text           ; poczatek sekcji kodu
        global _start   ; _start bedzie symbolem globalnym,
                        ; od ktorego zacznie sie wykonywanie programu

        _start:
                mov     eax, 4          ; zapis do pliku
                mov     ebx, 1          ; na ekran
                mov     ecx, jak_masz   ; napis do wyswietlenia: pytanie
                mov     edx, jak_masz_dl        ; dlugosc napisu
                int     80h             ; wyswietlamy

                mov     eax, 3          ; czytanie z pliku
                mov     ebx, 0          ; z klawiatury
                mov     ecx, imie       ; dokad czytac?
                mov     edx, imie_dl    ; ile bajtow czytac?
                int     80h             ; wczytujemy


                mov     eax, 4          ; zapis do pliku
                mov     ebx, 1          ; na ekran
                mov     ecx, czesc      ; napis do wyswietlenia: "czesc"
                mov     edx, czesc_dl   ; dlugosc napisu
                int     80h             ; wyswietlamy

                mov     eax, 4          ; zapis do pliku
                mov     ebx, 1          ; na ekran
                mov     ecx, imie       ; napis do wyswietlenia: imie
                mov     edx, imie_dl    ; dlugosc napisu
                int     80h             ; wyswietlamy

                mov     eax, 1
                xor     ebx, ebx
                int     80h

        ; dla FASMa:
        ;segment readable writeable             ; poczatek sekcji danych

        section .data           ; poczatek sekcji danych

        jak_masz        db      "Jak masz na imie? "
        ; FASM: znak rownosci zamiast EQU
        jak_masz_dl     equ     $ - jak_masz

        ; rezerwuj 20 bajtow o wartosci poczatkowej zero, na imie
        imie:           times 20 db 0
        ; FASM: znak rownosci zamiast EQU
        imie_dl         equ     $ - imie

        czesc           db      "Czesc "
        ; FASM: znak rownosci zamiast EQU
        czesc_dl        equ     $ - czesc
     _________________________________________________________________

   Nastepny program wypisuje na ekranie rejestr flag w postaci dwojkowej.
   (przeskocz program wypisujacy flagi)
; Program wypisujacy flagi w postaci dwojkowej
;
; Autor: Bogdan D.
; kontakt: bogdandr (at) op (dot) pl
;
; kompilacja:
; nasm -f elf flagi.asm
; ld -s -o flagi flagi.o
;
; kompilacja FASM:
; fasm flagi.asm flagi


;format ELF executable          ; dla FASMa
; entry _start                  ; dla FASMa

; segment readable executable   ; dla FASMa

section .text           ; tu zaczyna sie segment kodu,
                        ; nie jest to potrzebne

global _start           ; nazwa punktu rozpoczecia programu.
                        ; FASM: usunac te linijke

;CPU 386                ; bedziemy tu uzywac rejestrow 32-bitowych.
                        ; Nie jest to potrzebne, gdyz
                        ; NASM domyslnie wlacza wszystkie
                        ; mozliwe instrukcje.

_start:                         ; etykieta poczatku programu

        pushfd                  ; 32 bity flag ida na stos

        pop     esi             ; flagi ze stosu do ESI

        mov     eax, "0"
        mov     ebx, nasze_flagi; EBX = adres bufora dla wartosci flag
        xor     edi, edi        ; EDI = 0

        mov     cx, 32          ; tyle bitow i tyle razy trzeba
                                ; przejsc przez petle

petla:                          ; etykieta oznaczajaca poczatek petli.

        and     al, "0" ; upewniamy sie, ze AL zawiera tylko
                                ; 30h="0", co zaraz sie
                                ; moze zmienic. Dokladniej,
                                ; czyscimy bity 0-3,
                                ; z ktorych bit 0 moze sie zaraz zmienic

        shl     esi, 1          ; Przesuwamy bity w ESI o 1 w lewo.
                                ; 31 bit ESI idzie do CF

        adc     al, 0           ; ADC - add with carry. Do AL dodaj
                                ; 0 + wartosc CF.
                                ; jesli CF (czyli 31 bit ESI) = 1,
                                ; to AL := AL+1,
                                ; inaczej AL bez zmian

        mov     [ebx+edi], al   ; zapisz AL w buforze
        add     edi, 1

        loop    petla           ; przejdz na poczatek petli,
                                ; jesli nie skonczylismy

        mov     eax, 4          ; funkcja zapisywania do pliku/na ekran
        mov     ebx, 1          ; 1 = ekran
        mov     ecx, nasze_flagi
        mov     edx, 32         ; dlugosc tekstu
        int     80h             ; wypisz na ekran

        mov     byte [nasze_flagi],0ah
        mov     eax, 4          ; funkcja zapisywania do pliku/na ekran
        mov     ebx, 1          ; 1 = ekran
        mov     ecx, nasze_flagi
        mov     edx, 1          ; dlugosc tekstu
        int     80h             ; wypisz na ekran przejscie do nowej linii

        mov     eax, 1
        int     80h             ; wyjscie z programu

; FASM: segment readable writeable
section .data                   ; dane juz nie moga byc w sekcji kodu, gdyz
                                ; w Linuksie sekcja kodu programu jest
                                ; chroniona przed zapisem

nasze_flagi:    times   32      db "0"          ; "0" = 30h

   Kompilujemy go nastepujaco (wszystkie programy bedziemy tak
   kompilowac, chyba ze powiem inaczej):
        nasm -f elf flagi.asm
        ld -s -o flagi flagi.o

   lub:
        fasm flagi.asm flagi

   Nie ma w tym programie wielkiej filozofii. Nie powinno byc trudno go
   zrozumiec.
     _________________________________________________________________

   Teraz krotki programik, ktorego jedynym celem jest wyswietlenie na
   ekranie cyfr od 0 do 9, kazda w osobnej linii:
   (przeskocz program wypisujacy cyfry)
; Program wypisuje na ekranie cyfry od 0 do 9
;
; kompilacja NASM:
; nasm -f elf cyfry.asm
; ld -s -o cyfry cyfry.o


section .text

global _start

; definiujemy stale (NASM):

%define         lf      10              ; Line Feed
%define stdout  1       ; standardowe urzadzenie wyjscia (zwykle ekran)
%define         sys_write 4             ; funkcja pisania do pliku

;
; kompilacja FASM:
; fasm cyfry.asm cyfry
;
; format ELF executable         ; dla FASMa
; entry _start
; segment readable executable

; definiujemy stale (FASM):
; lf = 10
; stdout = 1
; sys_write = 4



_start:

        mov     eax, 0                  ; pierwsza wypisywana cyfra

wyswietlaj:
        call    _pisz_ld                ; uruchom procedure wyswietlania
                                        ; liczby bedacej w EAX
        call    _nwln                   ; uruchom procedure, ktora
                                        ; przechodzi do nowej linii
        add     eax, 1                  ; zwiekszamy cyfre
        cmp     eax, 10                 ; sprawdzamy, czy ciagle EAX < 10
        jb      wyswietlaj              ; jesli EAX < 10, to
                                        ; wyswietlamy cyfre

        mov     eax, 1                  ; funkcja wyjscia z programu
        xor     ebx, ebx                ; kod wyjscia = 0
        int     80h                     ; wychodzimy



; nastepujace procedury (wyswietlanie liczby i przechodzenie
; do nowego wiersza) nie sa az tak istotne, aby omawiac je
; szczegolowo, gdyz w przyszlosci bedziemy uzywac tych samych
; procedur, ale z biblioteki, a te wstawilem tutaj dla
; uproszczenia kompilacji programu.

; Ogolny schemat dzialania tej procedury wyglada tak:
; wezmy liczbe EAX=12345. Robimy tak:
; 1. dzielimy EAX przez 10. reszta = EDX = DL = 5.
; Zapisz do bufora. EAX = 1234 (iloraz)
; 2. dzielimy EAX przez 10. reszta = DL = 4.
; Zapisz do bufora. EAX=123 (iloraz)
; 3. dzielimy EAX przez 10. reszta = DL = 3.
; Zapisz do bufora. EAX=12
; 4. dziel EAX przez 10. DL = 2. zapisz. iloraz = EAX = 1
; 5. dziel EAX przez 10. DL = 1. zapisz. iloraz = EAX = 0.
; Przerywamy petle.
; Teraz w buforze sa znaki: "54321". Wystarczy wypisac wspak
; i oryginalna liczba pojawia sie na ekranie.

_pisz_ld:

; pisz32e
; we: EAX=liczba bez znaku do wypisania

        pushfd
        push    ecx
        push    edx
        push    eax
        push    esi

        xor     esi, esi
        mov     ecx, 10

._pisz_ld_petla:
        xor     edx, edx
        div     ecx

        or      dl, "0"
        mov     [_pisz_bufor+esi], dl   ; do bufora ida reszty z
                                        ; dzielenia przez 10,
        inc     esi                     ; czyli cyfry wspak

        test    eax, eax
        jnz     ._pisz_ld_petla

._pisz_ld_wypis:
        mov     al, [_pisz_bufor+esi-1] ; wypisujemy reszty wspak
        call    _pisz_z

        dec     esi
        jnz     ._pisz_ld_wypis

        pop     esi
        pop     eax
        pop     edx
        pop     ecx
        popfd

        ret


_pisz_z:

; pisz_z
; we: AL=znak do wypisania

        push    eax
        push    ebx
        push    ecx
        push    edx

        mov     [_pisz_bufor+39], al

        mov     eax, sys_write          ; funkcja zapisu do pliku
        mov     ebx, stdout             ; kierujemy na
                                        ; standardowe wyjscie
        lea     ecx, [_pisz_bufor+39]
        mov     edx, 1
        int     80h

        pop     edx
        pop     ecx
        pop     ebx
        pop     eax

        ret


_nwln:

;wyswietla znak konca linii (Linux)

        push    eax

        mov     al, lf
        call    _pisz_z

        pop     eax
        ret


section .data
; FASM: segment readable writeable

_pisz_bufor:    times   40      db 0    ; miejsce na 40 cyferek

   Nastepny "twor" nie jest wolnostojacym programem, ale pewna procedura.
   Pobiera ona informacje z rejestru AL i wypisuje, co trzeba. Oto ona:
   (przeskocz procedure _pisz_ch)
; FASM: segment readable executable
section .text

_pisz_ch:

;we: AL=cyfra heksadecymalna do wypisania 0...15
; CF=1 jesli blad

        push eax                ; zachowaj modyfikowane rejestry: AX, Flagi
        pushfd

        cmp al,9                ; Sprawdzamy dane wejsciowe :
                                ; AL jest w 0-9 czy w 10-15?
        ja _ch_hex              ; AL < 9. Skok do "_ch_hex"
        or al,"0"               ; 0 < AL < 9. Or ustawia 2 bity,
                                ; czyniac z AL liczbe z
                                ; przedzialu 30h - 39h, czyli od "0" do "9".
                                ; Mozna bylo napisac
                                ; "add al,30h", ale zdecydowalem sie
                                ; na "or", bo jest
                                ; szybsze a efekt ten sam.

        jmp short _ch_pz        ; AL juz poprawione. Skacz do miejsca,
                                ; gdzie wypisujemy znak.

_ch_hex:                        ; AL > 9. Moze bedzie to cyfra hex,
                                ; moze nie.
        cmp al,15               ; AL > 15?
        ja _blad_ch             ; jesli tak, to mamy blad
        add al,"A"-10           ; Duzy skok myslowy. Ale wystarczy to
                                ; rozbic na 2 kroki i
                                ; wszystko staje sie jasne. Najpierw
                                ; odejmujemy 10 od AL.
                                ; Zamiast liczby od 10 do 15 mamy juz
                                ; liczbe od 0 do 5. Teraz te liczbe
                                ; dodajemy do "A", czyli kodu ASCII litery
                                ; A, otrzymujac znak od "A" do "F"

_ch_pz:                         ; miejsce wypisywania znakow.
        mov     [znak], al

        mov     eax, 4          ; funkcja wypisywania
        mov     ebx, 1          ; ekran
        mov     ecx, znak
        mov     edx, 1
        int     80h


        popfd                   ; zdejmij ze stosu flagi
        clc                     ; CF := 0 dla zaznaczenia braku bledu
                                ; (patrz opis procedury)
        jmp short _ch_ok        ; skok do wyjscia

_blad_ch:                       ; sekcja obslugi bledu (AL > 15)
        popfd                   ; zdejmij ze stosu flagi
        stc                     ; CF := 1 na znak bledu

_ch_ok:                         ; miejsce wyjscia z procedury
        pop eax                 ; zdejmij modyfikowane rejestry

        ret                     ; return, powrot

; FASM: segment readable writeable
section .data

znak    db      0

   To chyba nie bylo zbyt trudne, co?
   Szczegoly dotyczace pisania procedur (i bibliotek) znajduja sie w moim
   artykule o pisaniu bibliotek.

   Teraz pokaze pewien program, ktory wybralem ze wzgledu na duza liczbe
   roznych instrukcji i sztuczek. Niestety, nie jest on krotki, ale
   wspolnie sprobujemy przez niego przejsc.
   (przeskocz program zliczajacy liczby pierwsze)
; Program liczy liczby pierwsze w przedzialach
;   2-10, 2-100, 2-1000,... 2-100.000
;
; Autor: Bogdan D.
; kontakt: bogdandr (at) op (dot) pl
;
; kompilacja:
;
; nasm -f elf ile_pier.asm
; ld -s -o ile_pier ile_pier.o
;
; kompilacja FASM:
; fasm ile_pier.asm ile_pier


; format ELF executable         ; tylko dla FASMa
; entry _start

; FASM: segment readable executable
section .text

global _start                   ; FASM: usunac

_start:                         ; poczatek...

        xor ebx,ebx             ; EBX = liczba, ktora sprawdzamy,
                                ; czy jest pierwsza. Zaczniemy od 3.
                                ; Ponizej jest 3 razy "inc"
                                ; (zwieksz o 1). Najpierw
                                ; EBX = 0, bo "xor rej,rej" zeruje dany
                                ; rejestr

        xor edi,edi             ; EDI = biezacy licznik liczb pierwszych

        xor ecx,ecx             ; ECX = stary licznik liczb
                                ; (z poprzedniego przedzialu)
                                ; Chwilowo, oczywiscie 0.

        inc ebx                 ; EBX = 1
        mov esi,10              ; ESI = biezacy koniec przedzialu.
        inc edi                 ; EDI=1. uwzgledniamy dwojke, ktora
                                ; jest liczba pierwsza
        inc ebx                 ; EBX=2, pierwsza liczba bedzie = 3

petla:                          ; petla przedzialu

        cmp ebx,esi             ; czy koniec przedzialu?
                                ; (ebx=liczba, esi=koniec przedzialu)
        jae pisz                ; EBX >= ESI - idz do sekcji
                                ; wypisywania wynikow

        mov ebp,2               ; EBP - liczby, przez ktore
                                ; bedziemy dzielic.
                                ; pierwszy dzielnik = 2

        inc ebx                 ; zwiekszamy liczbe. EBX=3. Bedzie
                                ; to pierwsza sprawdzana

spr:                            ; petla sprawdzania pojedynczej liczby

        mov eax,ebx             ; EAX = sprawdzana liczba
        xor edx,edx             ; EDX = 0
        div ebp                 ; EAX = EAX/EBP (EDX bylo=0),
                                ; EDX=reszta z dzielenia

        or edx,edx              ; instrukcja OR tak jak wiele innych,
                                ; ustawi flage zera ZF na 1, gdy jej
                                ; wynik byl zerem. W tym przypadku
                                ; pytamy: czy EDX jest zerem?

        jz petla                ; jezeli dzieli sie bez reszty
                                ; (reszta=EDX=0),
                                ; to nie jest liczba pierwsza
                                ; i nalezy zwiekszyc liczbe
                                ; sprawdzana (inc ebx)

        inc ebp                 ; zwiekszamy dzielnik

        cmp ebp,ebx             ; dzielniki az do liczby
        jb spr                  ; liczba > dzielnik - sprawdzaj
                                ; dalej te liczbe. Wiem, ze
                                ; mozna bylo sprawdzac tylko do
                                ; SQRT(liczba) lub LICZBA/2, ale
                                ; wydluzyloby to program i brakowalo
                                ; mi juz rejestrow...

juz:                            ; przerobilismy wszystkie dzielniki,
                                ; zawsze wychodzila reszta
                                ; wiec liczba badana jest pierwsza

        inc edi                 ; zwiekszamy licznik liczb znalezionych
        jmp petla               ; sprawdzaj kolejna liczbe az
                                ; do konca przedzialu

                                ; sekcja wypisywania informacji

pisz:

        push ebx                ; zachowujemy modyfikowane
                                ; a wazne rejestry
        push ecx

        mov     eax, 4
        mov     ebx, 1
        mov     ecx, przedzial
        mov     edx, dlugosc_przedzial
        int     80h             ; wypisujemy informacje o przedziale

        mov     eax,esi         ; EAX=ESI=koniec przedzialu
        call    _pisz_ld        ; wypisz ten koniec (EAX)

        mov     eax, 4
        mov     ebx, 1
        mov     ecx, dwuk
        mov     edx, 1
        int     80h             ; wypisujemy dwukropek

        pop ecx

        add ecx,edi             ; dodajemy poprzednia liczbe
                                ; znalezionych liczb pierwszych
        mov eax,ecx             ; EAX = liczba liczb pierwszych
                                ; od 2 do konca biezacego przedzialu

        call _pisz_ld           ; wypisujemy te liczbe.

        pop ebx

        cmp esi,100000          ; 10^5
        jb dalej                ; ESI > 100.000? Tak - koniec,
                                ; bo dalej liczy zbyt dlugo
koniec:
        mov     eax, 4
        mov     ebx, 1
        mov     ecx, przedzial
        mov     edx, 1
        int     80h             ; wypisujemy znak nowej linii


        xor     ebx, ebx        ; kod wyjscia = 0
        mov     eax, 1
        int     80h             ; wyjscie z programu

dalej:

        mov eax,esi             ; EAX=ESI
        shl eax,3               ; EAX = EAX*8
        shl esi,1               ; ESI=ESI*2
        add esi,eax             ; ESI=ESI*2+EAX*8=ESI*2+ESI*8=ESI*10.
                                ; Znacznie szybciej niz MUL

        xor edi,edi             ; biezacy licznik liczb

        jmp petla               ; robimy od poczatku...

_pisz_ld:

;we: EAX=liczba bez znaku do wypisania

        push ebx
        push ecx                ; zachowujemy modyfikowane rejestry
        push edx
        push eax
        push esi

        xor esi,esi             ; SI=0. Bedzie wskaznikiem w
                                ; powyzszy bufor.

        mov ecx,10              ; bedziemy dzielic przez 10,
                                ; aby uzyskiwac kolejne cyfry
                                ; Reszty z dzielenia pojda do
                                ; bufora, potem beda wypisane
                                ; wspak, bo pierwsza reszta
                                ; jest przeciez cyfra jednosci
_pisz_ld_petla:
        xor edx,edx             ; EDX=0

        div ecx                 ; EAX = EAX/ECX, EDX = reszta,
                                ; ktora miesci sie w DL, bo to
                                ; jest tylko 1 cyfra dziesietna.

        or  dl, "0"

        mov [_pisz_bufor+esi],dl        ; Cyfra do bufora.

        inc esi                 ; Zwieksz numer komorki w buforze,
                                ; do ktorej bedziemy teraz pisac

        or eax,eax              ; EAX = 0 ?

        jnz _pisz_ld_petla      ; Jesli nie (JNZ), to skok do
                                ; poczatku petli

_pisz_ld_wypis:

        mov     eax, 4
        mov     ebx, 1
        lea     ecx, [_pisz_bufor+esi-1]
        mov     edx, 1
        int     80h

        dec esi                 ; zmniejsz wskaznik do bufora.

        jnz _pisz_ld_wypis      ; Jesli ten wskaznik (ESI) nie
                                ; jest zerem, wypisuj dalej

        pop esi                 ; odzyskaj zachowane rejestry
        pop eax
        pop edx
        pop ecx
        pop ebx

        ret                     ; powrot z procedury

; FASM: segment readable writeable
 section .data

 _pisz_bufor: times 20 db 0       ; miejsce na cyfry dla procedury

 przedzial       db      10,"Przedzial 2-"

; FASM:  dlugosc_przedzial       =     $ - przedzial
 dlugosc_przedzial       equ     $ - przedzial

 dwuk            db      ":"
     _________________________________________________________________

   Kilka uwag o tym programie:
     * Czemu nie zrobilem mov ebx,2 a potem inc ebx, ktore musialo byc w
       petli?
       Bo xor ebx,ebx jest krotsze i szybsze.
     * Dobra. Wiec czemu nie:
        xor ebx,ebx
        inc ebx
        inc ebx
       Te instrukcje operuja na tym samym rejestrze i kazda musi
       poczekac, az poprzednia sie zakonczy. Wspolczesne procesory
       potrafia wykonywac niezalezne czynnosci rownolegle, dlatego
       wcisnalem w srodek jeszcze kilka niezaleznych instrukcji.
     * Ten program sprawdza za duzo dzielnikow. Nie mozna bylo sprawic,
       by sprawdzal tylko do na przyklad polowy sprawdzanej liczby?
       Mozna bylo. Uzywajac zmiennych w pamieci. Niechetnie to robie, bo
       w porownaniu z predkoscia operacji procesora, pamiec jest wprost
       NIEWIARYGODNIE wolna. Zalezalo mi na szybkosci.
     * Czy nie prosciej zamiast tych wszystkich SHL zapisac jedno MUL lub
       IMUL?
       Jasne, ze prosciej. Przy okazji dobre kilka[nascie] razy wolniej.
     * Dlaczego ciagle xor rej,rej?
       Szybsze niz mov rej,0, gdzie to zero musi byc czesto zapisane 4
       bajtami zerowymi. Tak wiec i krotsze. Oprocz tego, dzieki
       instrukcji XOR lub SUB wykonanej na tym samym rejestrze, procesor
       wie, ze ten rejestr juz jest pusty. Moze to przyspieszyc niektore
       operacje.
     * Dlaczego na niektorych etykietach sa jakies znaki podkreslenia z
       przodu?
       Niektore procedury sa zywcem wyjete z mojej biblioteki, piszac
       ktora musialem zadbac, by przypadkowo nazwa jakies mojej procedury
       nie byla identyczna z nazwa jakiejs innej napisanej w programie
       korzystajacym z biblioteki.
       Czy nie moglem tego potem zmienic?
       Jasne, ze moglem. Ale nie bylo takiej potrzeby.
     * Czemu or rej,rej a nie cmp rej,0?
       OR jest krotsze i szybsze. Mozna tez uzywac test rej,rej, ktore
       nie zmienia zawartosci rejestru.
     * Czemu or dl, "0"?
       Bardziej czytelne niz add/or dl,30h. Chodzi o to, aby dodac kod
       ASCII zera. I mozna to zrobic bardziej lub mniej czytelnie.
     * Co to w ogole za instrukcja to lea edx, [_pisz_bufor+si-1] ?
       LEA - Load Effective Address: do rejestru EDX wpisz adres
       (elementu, ktorego) adres wynosi "_pisz_bufor+SI-1". Tak wiec od
       tej pory EDX = _pisz_bufor+SI-1, czyli wskazuje na ostatnia cyfre
       w naszym buforze. Czemu odjalem 1? Jak widac w kodzie, po wpisaniu
       cyfry do bufora, zwiekszamy SI. Gdy nasza liczba juz sie skonczy
       to SI pokazuje na nastepne wolne miejsce po ostatniej cyfrze, a
       chcemy, aby pokazywal na ostatnia. Stad to minus jeden.
     * Czemu FASM nie akceptuje EQU?
       FASM akceptuje EQU, tylko tutaj symbol byl zdefiniowany po uzyciu,
       co najwyrazniej przeszkadza FASMowi. Postawienie znaku rownosci
       zamiast EQU naprawilo sprawe.
     * Czemu jest dwukropek po etykiecie zmiennej?
       Po to, aby FASM przyjal dyrektywe "times". Bez dwukropka nie
       chcial skompilowac.

   Wiem, ze ten program nie jest doskonaly. Ale taki juz po prostu
   napisalem...
   Nie martwcie sie, jesli czegos od razu nie zrozumiecie. Naprawde, z
   czasem samo przyjdzie. Ja tez przeciez nie umialem wszystkiego od
   razu.

   Inny program do liczb pierwszych znajdziecie tu: prime.txt.

   Nastepnym razem cos o ulamkach i koprocesorze.

     Podstawowe prawo logiki:
     Jezeli wiesz, ze nic nie wiesz, to nic nie wiesz.
     Jezeli wiesz, ze nic nie wiesz, to cos wiesz.
     Wiec nie wiesz, ze nic nie wiesz.

   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

   (mozna korzystac z zamieszczonych tu procedur)
    1. Napisz program, ktory na ekranie wyswietli liczby od 90 do 100.
    2. Napisz program sprawdzajacy, czy dana liczba (umiescisz ja w
       kodzie, nie musi byc wczytywana znikad) jest liczba pierwsza.
    3. Napisz program wypisujacy dzielniki danej liczby (liczba tez w
       kodzie).
