   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 5 - Koprocesor, czyli jak liczyc na ulamkach. Odwrotna Notacja Polska

   Jak zapewne wiekszosc wie, koprocesor (FPU = Floating Point Unit, NPX
   = Numerical Processor eXtension) sluzy do wykonywania dzialan
   matematycznych. Podstawowy procesor tez oczywiscie wykonuje dzialania
   matematyczne (dodawanie, mnozenie, ...) ale tylko na liczbach
   calkowitych. Z czasem jednak przyszla potrzeba wykonywania obliczen na
   liczbach niecalkowitych, czyli ulamkach (liczbach
   zmiennoprzecinkowych). Dlatego firmy produkujace procesory zaczely je
   wyposazac wlasnie w uklady wspomagajace prace na ulamkach. Do
   procesorow 8086, 80286, 80386 byly dolaczane jako osobne uklady
   koprocesory: 8087, 80287, 80387 (80187 nie wprowadzil zadnych
   istotnych nowosci. Byla to przerobka 8087, a moze nawet byl to po
   prostu ten sam uklad). Procesory 486SX mialy jeszcze oddzielny
   koprocesor (80387) a od 486DX (w kazdym razie u Intela) koprocesor byl
   juz na jednym chipie z procesorem. I tak jest do dzis.

   Ale dosc wstepu. Pora na szczegoly.
     _________________________________________________________________

Typy danych

   Zanim zaczniemy cokolwiek robic, trzeba wiedziec, na czym ten caly
   koprocesor operuje.
   Oprocz liczb calkowitych, FPU operuje na liczbach ulamkowych roznej
   precyzji:
     * Pojedyncza precyzja. Liczby takie zajmuja po 32 bity (4 bajty) i
       ich wartosc maksymalna wynosi ok. 10^39 (10^39). Znane sa
       programistom jezyka C jako "float".
     * Podwojna precyzja. 64 bity (8 bajtow), max = ok. 10^409 (10^409).
       W jezyku C sa znane jako "double"
     * Rozszerzona precyzja. 80 bitow (10 bajtow), max = ok. 10^4930
       (10^4930). W jezyku C sa to "long double"

   Jak widac, ilosci bitow sa oczywiscie skonczone. Wiec nie kazda liczbe
   rzeczywista da sie dokladnie zapisac w postaci binarnej. Na przyklad,
   jedna dziesiata (0.1) zapisana dwojkowo jest ulamkiem nieskonczonym
   okresowym! Poza tym, to, czego nas uczyli na matematyce, na przyklad
   oczywista rownosc: a+(b-a)=b nie musi byc prawda w swiecie ulamkow w
   procesorze ze wzgledu na brak precyzji!

   Poza ulamkami, FPU umie operowac na BCD (binary coded decimal). W
   takich liczbach 1 bajt dzieli sie na 2 czesci po 4 bity, z ktorych
   kazda moze miec wartosc 0-9. Caly bajt reprezentuje wiec liczby od 0
   do 99, w ktorych cyfra jednosci jest w mlodszych 4 bitach a cyfra
   dziesiatek - w starszych 4 bitach.

   Szczegolami zapisu liczb ulamkowych nie bedziemy sie tutaj zajmowac.
   Polecam, tak jak poprzednio, strony Intela i strony AMD, gdzie
   znajduje sie tez kompletny opis wszystkich instrukcji procesora i
   koprocesora.
     _________________________________________________________________

Rejestry koprocesora

   Po zapoznaniu sie z typami (a przede wszystkim z rozmiarami) liczb
   ulamkowych, powstaje pytanie: gdzie koprocesor przechowuje takie
   ilosci danych?

   FPU ma specjalnie do tego celu przeznaczonych 8 rejestrow, po 80 bitow
   kazdy. W operacjach wewnetrznych (czyli bez pobierania lub zapisywania
   danych do pamieci) FPU zawsze uzywa rozszerzonej precyzji.

   Rejestry danych nazwano st(0), st(1), ... , st(7) (NASM: st0 ... st7).
   Nie dzialaja jednak one tak, jak zwykle rejestry, lecz jak ... stos!
   To znaczy, ze dowolnie dostepna jest tylko wartosc ostatnio polozona
   na stosie czyli wierzcholek stosu, w tym przypadku: st(0). Znaczy to,
   ze do pamieci (FPU operuje tylko na wlasnych rejestrach lub pamieci -
   nie moze uzywac rejestrow ogolnego przeznaczenia na przyklad EAX itp.)
   moze byc zapisana tylko wartosc z st(0), a kazda wartosc pobierana z
   pamieci idzie do st(0) a stare st(0) przesuwa sie do st(1) itd. kazdy
   rejestr przesuwa sie o 1 dalej. Jezeli brakuje na to miejsca, to FPU
   moze wygenerowac przerwanie (wyjatek) a rejestry danych beda
   prawdopodobnie zawierac smieci.

   Operowanie na rejestrach FPU bedzie wymagalo nieco wiecej uwagi niz na
   zwyklych, ale i do tego mozna sie przyzwyczaic.
     _________________________________________________________________

Instrukcje koprocesora

   Zacznijmy wiec od omowienia kilku podstawowych instrukcji. Przez [mem]
   bede nazywal dane bedace w pamieci (32-, 64- lub 80-bitowe, "int"
   oznacza liczbe calkowita), "st" jest czestym skrotem od "st(0)".
   Jezeli komenda konczy sie na P to oznacza, ze zdejmuje dane raz ze
   stosu, PP oznacza, ze zdejmuje 2 razy, czyli st(0) i st(1).
    1. Instrukcje przemieszczenia danych:
          + FLD/FILD [mem] - zaladuj liczbe rzeczywista/calkowita z
            pamieci. Dla liczby rzeczywistej jest to 32, 64 lub 80 bitow.
            Dla calkowitej - 16, 32 lub 64 bity.
          + FST [mem32/64/80] - do pamieci idzie liczba ze st(0).
          + FSTP [mem32/64/80] - zapisz st(0) w pamieci i zdejmij je ze
            stosu. Znaczy to tyle, ze st(1) o ile istnieje, staje sie
            st(0) itd. kazdy rejestr cofa sie o 1.
          + FIST [mem16/32] - ewentualnie obcieta do calkowitej liczbe z
            st(0) zapisz do pamieci.
          + FISTP [mem16/32/64] - jak wyzej, tylko ze zdjeciem ze stosu.
          + FXCH st(i) - zamien st(0) z st(i).
    2. Instrukcje ladowania stalych
          + FLDZ - zaladuj zero. st(0) = 0.0
          + FLD1 - zaladuj 1. st(0) = 1.0
          + FLDPI - zaladuj pi.
          + FLDL2T - zaladuj log2(10)
          + FLDL2E - zaladuj log2(e)
          + FLDLG2 - zaladuj log(2)=log10(2)
          + FLDLN2 - zaladuj ln(2)
    3. Dzialania matematyczne:
          + dodawanie: FADD, skladnia identyczna jak w odejmowaniu
            prostym
          + odejmowanie:
               o FSUB [mem32/64]       st := st-[mem]
               o FSUB st(0),st(i)       st := st-st(i)
               o FSUB st(i),st(0)       st(i) := st(i)-st(0)
               o FSUBP st(i), st(0)       st(i) := st(i)-st(0) i zdejmij
               o FSUBP (bez argumentow) = FSUBP st(1),st(0)
               o FISUB [mem16/32int]       st := st-[mem]
          + odejmowanie odwrotne:
               o FSUBR [mem32/64]       st := [mem]-st(0)
               o FSUBR st(0),st(i)       st := st(i)-st(0)
               o FSUBR st(i),st(0)       st(i) := st(0)-st(i)
               o FSUBRP st(i),st(0)       st(i) := st(0)-st(i) i zdejmij
               o FSUBRP (bez argumentow) = FSUBRP st(1),st(0)
               o FISUBR [mem16/32int]       st := [mem]-st
          + mnozenie: FMUL, skladnia identyczna jak w odejmowaniu
            prostym.
          + dzielenie: FDIV, skladnia identyczna jak w odejmowaniu
            prostym.
          + dzielenie odwrotne: FDIVR, skladnia identyczna jak w
            odejmowaniu odwrotnym.
          + wartosc bezwzgledna: FABS (bez argumentow) zastepuje st(0)
            jego wartoscia bezwzgledna.
          + zmiana znaku: FCHS:       st(0) := -st(0).
          + pierwiastek kwadratowy: FSQRT:      st(0) := SQRT[ st(0) ]
          + reszty z dzielenia: FPREM, FPREM1       st(0) := st(0) mod
            st(1).
          + zaokraglanie do liczby calkowitej: FRNDINT:      st(0) :=
            (int)st(0).
    4. Komendy porownania:
       - FCOM/FCOMP/FCOMPP, FUCOM/FUCOMP/FUCOMPP, FICOM/FICOMP,
       FCOMI/FCOMIP, FUCOMI/FUCOMIP, FTST, FXAM.
       Tutaj trzeba troche omowic sytuacje. FPU oprocz rejestrow danych
       zawiera takze rejestr kontrolny (16 bitow) i rejestr stanu (16
       bitow).
       W rejestrze stanu sa 4 bity nazwane C0, C1, C2 i C3. To one
       wskazuja wynik ostatniego porownania, a uklad ich jest taki sam,
       jak flag procesora, co pozwala na ich szybkie przeniesienie do
       flag procesora. Aby odczytac wynik porownania, nalezy zrobic takie
       cos:
        fcom
        fstsw   ax      ; tylko od 386. Inaczej:
                        ; fstsw word ptr [zmienna] / mov ax,[zmienna]
        sahf            ; AH zapisane do flag
       i uzywac normalnych komend JE, JB itp.
          + FCOM st(n)/[mem] -       porownaj st(0) z st(n) (lub zmienna
            w pamieci) bez zdejmowania st(0) ze stosu FPU
          + FCOMP st(n)/[mem] -       porownaj st(0) z st(n) (lub zmienna
            w pamieci) i zdejmij st(0)
          + FCOMPP -       porownaj st(0) z st(1) i zdejmij oba ze stosu
          + FICOM [mem] -       porownaj st(0) ze zmienna calkowita 16-
            lub 32-bitowa w pamieci
          + FICOMP [mem] -       porownaj st(0) ze zmienna calkowita 16-
            lub 32-bitowa w pamieci, zdejmij st(0)
          + FCOMI st(0), st(n) -       porownaj st(0) z st(n) i ustaw
            flagi procesora, nie tylko FPU
          + FCOMIP st(0), st(n) -       porownaj st(0) z st(n) i ustaw
            flagi procesora, nie tylko FPU, zdejmij st(0)
       Komendy konczace sie na I lub IP zapisuja swoj wynik bezposrednio
       do flag procesora. Mozna tych flag od razu uzywac (JZ, JA, ...).
       Te komendy sa dostepne tylko od 386.
       FTST porownuje st(0) z zerem.
       FXAM bada, co jest w st(0) - prawidlowa liczba, blad (NaN = Not a
       Number), czy 0.
    5. Instrukcje trygonometryczne:
          + FSIN - st(0) := sinus [st(0)]
          + FCOS - st(0) := kosinus [st(0)]
          + FSINCOS - st(0) := kosinus [st(0)], st(1) := sinus [st(0)]
          + FPTAN - partial tangent = tangens st(0) := tg [st(0)]
          + FPATAN - arcus tangens st(0) := arctg [st(0)]
    6. Logarytmiczne, wykladnicze:
          + FYL2X       st(1) := st(1)*log2[st(0)] i zdejmij
          + FYL2XPI       st(1) := st(1)*log2[ st(0) + 1.0 ] i zdejmij
          + F2XM1       st(0) := 2^[st(0)] - 1
    7. Instrukcje kontrolne:
          + FINIT/FNINIT - inicjalizacja FPU. Litera N po F oznacza, aby
            nie brac po uwage potencjalnych niezalatwionych wyjatkow.
          + FLDCW, FSTCW/FNSTCW - Load/Store control word - zapisuje 16
            kontrolnych bitow do pamieci, gdzie mozna je zmieniac na
            przyklad aby zmienic sposob zaokraglania liczb.
          + FSTSW/FNSTSW - zapisz do pamieci (lub rejestru AX) slowo
            statusu, czyli stan FPU
          + FCLEX/FNCLEX - wyczysc wyjatki
          + FLDENV, FSTENV/FNSTENV - wczytaj/zapisz srodowisko (rejestry
            stanu, kontrolny i kilka innych, bez rejestrow danych).
            Wymaga 14 albo 28 bajtow pamieci, w zaleznosci od trybu pracy
            procesora (rzeczywisty - DOS lub chroniony - Windows/Linux).
          + FRSTOR, FSAVE/FNSAVE - jak wyzej, tylko ze z rejestrami
            danych. Wymaga 94 lub 108 bajtow w pamieci, zaleznie od trybu
            procesora.
          + FINCSTP, FDECSTP - zwieksz/zmniejsz wskaznik stosu - przesun
            st(0) na st(7), st(1) na st(0) itd. oraz w druga strone,
            odpowiednio.
          + FFREE - zwolnij podany rejestr danych
          + FNOP - no operation. Nic nie robi, ale zabiera czas.
          + WAIT/FWAIT - czekaj, az FPU skonczy prace. Uzywane do
            synchronizacji z CPU.
     _________________________________________________________________

Przyklady

   Dosc juz teorii, pora na przyklady. Programiki te wymyslilem piszac
   ten kurs.

   Przyklad 1 (NASM):
   (przeskocz program wyswietlajacy czestotliwosc zegara)
; z wyswietlaniem:
;   nasm -O999 -f elf -o fpu1.o fpu1.asm
;   ld -s -o fpu1 fpu1.o bibl/lib/libasmio.a
; bez wyswietlania:
;   nasm -O999 -f elf -o fpu1.o fpu1.asm
;   ld -s -o fpu1 fpu1.o

section .text

; jesli nie chcesz wyswietlania, usun te linijke nizej:
%include "bibl/incl/linuxbsd/nasm/std_bibl.inc"

global _start

_start:
        finit                   ; zawsze o tym pamietaj !

        fild    dword [dzielna] ; ladujemy dzielna. st(0) = 1234DD
        fild    dword [dzielnik]; ladujemy dzielnik. st(0) = 10000h,
                                ; st(1) = 1234DD
        fdivp   st1             ; dzielimy. st(1) := st(1)/st(0) i pop.
                                ; st(0) ~= 18.2
                                ; FASM: fdivp st(1)

        fstp    tword [iloraz]  ; zapisujemy st(0) do pamieci i
                                ; zdejmujemy ze stosu

; jesli nie chcesz wyswietlania, usun te 3 linijki nizej:
        mov     edi, iloraz
        piszd80                 ; wyswietl wynik
        nwln                    ; przejdz do nowego wiersza

        mov     eax, 1
        xor     ebx, ebx
        int     80h

section .data

align 8                         ; NASM w tym miejscu dorobi kilka
                                ; NOPow (instrukcji nic nie
                                ; robiacych, ale zabierajacych czas),
                                ; aby adres dzielil sie
                                ; przez 8 (patrz dalej).

dzielna         dd 1234ddh      ; 4013 91a6 e800 0000 0000
dzielnik        dd 10000h

section .bss                    ; dane w sekcji nazwanej .BSS nie
                                ; beda fizycznie umieszczone
                                ; w programie, a dopiero w pamieci.
                                ; Zaoszczedzi to
                                ; miejsce = dlugosc pliku.
                                ; Mozna tu umieszczac tylko
                                ; niezainicjalizowane dane.

iloraz          rest 1          ; rezerwacja 10 bajtow.
     _________________________________________________________________

   Teraz wersja dla FASMa:
   (przeskocz ten program w wersji FASMa)
; z wyswietlaniem:
;   fasm fpu1.asm
;   ld -s -o fpu1 fpu1.o bibl/lib/libasmio.a
; bez wyswietlania:
;   fasm fpu1.asm
;   ld -s -o fpu1 fpu1.o

format ELF

public _start

section ".text" executable
; jesli nie chcesz wyswietlania, usun te linijke nizej:
include "bibl/incl/linuxbsd/fasm/std_bibl.inc"

_start:
        finit                   ; zawsze o tym pamietaj !

        fild    dword [dzielna] ; ladujemy dzielna. st(0) = 1234DD
        fild    dword [dzielnik]; ladujemy dzielnik. st(0) = 10000h,
                                ; st(1) = 1234DD
        fdivp   st1, st0        ; dzielimy. st(1) := st(1)/st(0) i
                                ; pop. st(0) ~= 18.2
                                ; FASM: fdivp st(1)

        fstp    tword [iloraz]  ; zapisujemy st(0) do pamieci i
                                ; zdejmujemy ze stosu

; jesli nie chcesz wyswietlania, usun te 3 linijki nizej:
        mov     edi, iloraz
        piszd80                 ; wyswietl wynik
        nwln                    ; przejdz do nowego wiersza

        mov     eax, 1
        xor     ebx, ebx
        int     80h

section ".data" writeable align 8

dzielna         dd 1234ddh      ; 4013 91a6 e800 0000 0000
dzielnik        dd 10000h

section ".bss" writeable
iloraz          dt 0.0

   Ten przyklad do zmiennej "iloraz" wstawia czestotliwosc zegara
   komputerowego (ok. 18,2 Hz) i wyswietla ja przy uzyciu jednej z
   procedur z mojej biblioteki (mozna to usunac).
   Nalezy zwrocic uwage na zaznaczanie rozmiarow zmiennych (dword/tword).

   Dyrektywa "align 8" ustawia kolejna zmienna/etykiete tak, ze jej adres
   dzieli sie przez 8 (qword = 8 bajtow). Dzieki temu, operacje na
   pamieci sa szybsze (na przyklad pobranie 8 bajtow: zamiast 3 razy
   pobierac po 4 bajty, bo akurat tak sie zdarzylo, ze miala jakis
   nieparzysty adres, pobiera sie 2x4 bajty). Rzecz jasna, skoro zmienna
   "dzielna" (i "dzielnik") ma 4 bajty, to adresy zmiennych "dzielnik" i
   "iloraz" tez beda podzielne przez 4. Ciag cyfr po sredniku to ulamkowa
   reprezentacja dzielnej. Skomplikowane, prawda? Dlatego nie chcialem
   tego omawiac.
     _________________________________________________________________

   Przyklad 2: czy sinus liczby pi rzeczywiscie jest rowny 0 (w
   komputerze)?
   (przeskocz program z sinusem)
; format ELF executable         ; tylko dla FASMa
; entry _start

; FASM: segment readable executable
section .text

global _start                   ; FASM: usunac

_start:
        finit                   ; zawsze o tym pamietaj !

        fldpi                   ; wczytujemy PI
        fsin                    ; obliczamy sinus z PI
        ftst                    ; porownujemy st(0) z zerem.
        fstsw   ax              ; zapisujemy rejestr stanu
                                ; bezposrednio w AX.


        sahf                    ; AH idzie do flag
        mov     ah,9            ; AH=9, flagi niezmienione
        je      jest_zero       ; st(0) = 0?
                                ; Jesli tak, to wypisz, ze jest

        mov     ecx, nie_zero
        mov     edx, dlugosc_nie_zero

        jmp     pisz

jest_zero:
        mov     ecx, zero
        mov     edx, dlugosc_zero

pisz:
        mov     eax, 4
        mov     ebx, 1
        int     80h             ; wypisz jedna z wiadomosci.

        mov     eax, 1
        xor     ebx, ebx
        int     80h

; FASM: segment readable writeable
section .data

nie_zero        db      "Sin(PI) != 0",0ah
dlugosc_nie_zero        equ     $ - nie_zero
                ; FASM: "=" zamiast "equ"

zero            db      "Sin(PI) = 0",0ah
dlugosc_zero            equ     $ - zero
                ; FASM: "=" zamiast "equ"
     _________________________________________________________________

   Przyklad 3: czy pierwiastek z 256 rzeczywiscie jest rowny 16, czy 200
   jest kwadratem liczby calkowitej?
   (przeskocz ten program)
; format ELF executable         ; tylko dla FASMa
; entry _start

; FASM: segment readable executable
section .text

global _start                   ; FASM: usunac

_start:
        finit                   ; zawsze o tym pamietaj !

        fild word  [dwa_pie_sze]; st(0) = 256
        fsqrt                   ; st(0) = sqrt(256)
        fild word [szesnascie]  ; st(0) = 16, st(1) = sqrt(256)
        fcompp                  ; porownaj st(0) i st(1) i
                                ; zdejmij oba. st: [pusty]

        fstsw   ax
        sahf

        mov     ah,9
        je      tak256
        mov     ecx, nie_256
        mov     edx, dlugosc_nie_256

        jmp     short pisz_256

tak256:
        mov     ecx, tak_256
        mov     edx, dlugosc_tak_256

pisz_256:

        mov     eax, 4
        mov     ebx, 1
        int     80h             ; wypisz stosowna wiadomosc

                                ; do zapisu stanu stosu, czyli
                                ; rejestrow danych FPU mozna
                                ; uzywac takiego schematu zapisu,
                                ; ktory jest krotszy:
                                ; st:  (0),  (1),  (2),  ... , (7)

        fild word [dwiescie]    ; st: 200
        fsqrt                   ; st: sqrt(200)
        fld     st0             ; do st(0) wczytaj st(0).
                                ; st: sqrt(200), sqrt(200)
        frndint                 ; zaokraglij do liczby calkowitej.
                                ; st:  (int)sqrt(200), sqrt(200)
        fcompp                  ; porownaj i zdejmij oba.
        fstsw ax
        sahf

        mov ah,9
        je tak200

        mov     ecx, nie_200
        mov     edx, dlugosc_nie_200

        jmp short pisz_200

tak200:
        mov     ecx, tak_200
        mov     edx, dlugosc_tak_200

pisz_200:

        mov     eax, 4
        mov     ebx, 1
        int     80h             ; wypisz stosowna wiadomosc

        mov     eax, 1
        xor     ebx, ebx
        int     80h

; FASM: segment readable writeable
section .data

dwa_pie_sze     dw      256
dwiescie        dw      200
szesnascie      dw      16

nie_256         db      "SQRT(256) != 16", 0ah
dlugosc_nie_256         equ     $ - nie_256
                ; FASM: "=" zamiast "equ"

tak_256         db      "SQRT(256) = 16", 0ah
dlugosc_tak_256         equ     $ - tak_256
                ; FASM: "=" zamiast "equ"

nie_200 db "Liczba 200 nie jest kwadratem liczby calkowitej", 0ah
dlugosc_nie_200         equ     $ - nie_200
                ; FASM: "=" zamiast "equ"

tak_200 db "Liczba 200 jest kwadratem liczby calkowitej", 0ah
dlugosc_tak_200         equ     $ - tak_200
                ; FASM: "=" zamiast "equ"

   Dwa ostatnie programiki zbilem u siebie w jeden i przetestowalem.
   Wyszlo, ze sinus PI jest rozny od zera, reszta byla prawidlowa.

   Oczywiscie, w tych przykladach nie uzylem wszystkich instrukcji
   koprocesora (nawet sposrod tych, ktore wymienilem). Mam jednak
   nadzieje, ze te proste programy rozjasnia nieco sposob poslugiwania
   sie koprocesorem.
     _________________________________________________________________

Odwrotna Notacja Polska (Reverse Polish Notation, RPN)

   Ladnie brzmi, prawda? Ale co to takiego?

   Otoz, bardzo dawno temu pewien polski matematyk, Jan Lukasiewicz,
   wymyslil taki sposob zapisywania dzialan, ze nie trzeba w nim uzywac
   nawiasow. Byla to notacja polska. Sposob ten zostal potem dopracowany
   przez Charlesa Hamblina na potrzeby informatyki - w ten sposob
   powstala Odwrotna Notacja Polska.

   W zapisie tym argumenty dzialania zapisuje przed symbolem tego
   dzialania. Dla jasnosci podam teraz kilka przykladow:
   (przeskocz przyklady na ONP)
   Zapis tradycyjny                  ONP
        a+b                     a b +
        a+b+c                   a b + c + ; ab+ stanowi pierwszy argument
                                                ; drugiego dodawania
        c+b+a                   c b + a +
        (a+b)*c                 a b + c *
        c*(a+b)                 c a b + *
        (a+b)*c+d               a b + c * d +
        (a+b)*c+d*a             a b + c * d a * +
        (a+b)*c+d*(a+c)         a b + c * d a c + * +
        (a+b)*c+(a+c)*d         a b + c * a c + d * +
        (2+5)/7+3/5             2 5 + 7 / 3 5 / +

   Ale po co to komu i dlaczego mowie o tym akurat w tej czesci?
   Powod jest prosty: jak sie dobrze przyjrzec zapisowi dzialania w ONP,
   to mozna zobaczyc, ze mowi on o kolejnosci dzialan, jakie nalezy
   wykonac na koprocesorze. Omowimy to na przykladzie:
   (przeskocz ilustracje relacji miedzy ONP a koprocesorem)
        Zapis tradycyjny (jeden z powyzszych przykladow):
        (a+b)*c+(a+c)*d

        Zapis w ONP:
        a b + c * a c + d * +

        Uproszczony kod programu:

        fld     [a]
        fld     [b]
        faddp   st1, st0
        fld     [c]
        fmulp   st1, st0
        fld     [a]
        fld     [c]
        faddp   st1, st0
        fld     [d]
        fmulp   st1, st0
        faddp   st1, st0

        Teraz st0 jest rowne wartosci calego wyrazenia.

   Jak widac, ONP znacznie upraszcza przetlumaczenie wyrazenia na kod
   programu. Jednak, kod nie jest optymalny. Mozna byloby na przyklad
   zachowac wartosci zmiennych a i c na stosie i wtedy nie musielibysmy
   ciagle pobierac ich z pamieci. Dlatego w krytycznych sekcjach kodu
   stosowanie zasad ONP nie jest zalecane. Ale w wiekszosci przypadkow
   Odwrotna Notacja Polska sprawuje sie dobrze i uwalnia programistow od
   obowiazku zgadywania kiedy i jakie dzialanie wykonac.

   Pamietajcie tylko, ze stos koprocesora moze pomiescic tylko 8
   zmiennych!

   Nastepnym razem o SIMD.

   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. Napisz program, ktory sprawdzi (wyswietli stosowna informacje),
       czy liczba PI dzielona przez sama siebie daje dokladne 1.0
    2. Napisz program obliczajacy (nie wyswietlajacy) wartosc 10*PI.
       Potem sprawdz, czy sinus tej liczby jest zerem.
    3. Napisz program mowiacy, ktora z tych liczb jest wieksza: PI czy
       log2(10).
    4. Napisz program sprawdzajacy, czy 10*PI - PI - PI - PI - PI - PI =
       5*PI.
    5. Zamien na ONP:
       a/c/d + b/c/d
       a/(c*d) + b/(c*d)
       (a+b)/c/d
       (a+b)/(c*d)
    6. Zamien z ONP na zapis tradycyjny (daszek ^ oznacza potegowanie):
       ab*cd*e/-
       a5/c7/ed-9/*+
       a3+b/de+6^-
       dc-7b*2^/
