O ile wyświetlanie i pobieranie od użytkownika tekstów jest łatwe do wykonania - wystarczy uruchomić tylko jedną funkcję systemową (eax=3 lub 4 przerwania 80h) - to pobieranie i wyświetlanie na przykład liczb wcale nie jest takie proste i każdemu może przysporzyć problemów. W tej części podam parę algorytmów, dzięki którym każdy powinien sobie z tym poradzić.
Co prawda wszyscy już to umieją, ale dla porządku też o tym wspomnę.
Wszyscy znają funkcję EAX=4 przerwania Linuksa - w EBX podajemy deskryptor, na który wyświetlamy
(1 oznacza standardowe wyjście, najczęściej ekran), w ECX - adres bufora z napisem do wyświetlenia, a
w EDX - liczba bajtów do wyświetlenia. Po wywołaniu int 80h
w EAX dostajemy liczba zapisanych bajtów (jeśli EAX jest ujemny, to wystąpił błąd).
Zawsze można też wyświetlać tekst ręcznie.
Do pobierania tekstów od użytkownika służy funkcja EAX=3 przerwania Linuksa -
w EBX podajemy deskryptor, z którego czytamy
(0 oznacza standardowe wejście, najczęściej klawiaturę), w ECX - adres bufora na dane, a
w EDX - liczba bajtów do przeczytania. Po wywołaniu int 80h
w buforze dostajemy
dane, a w EAX - liczba przeczytanych bajtów (jeśli EAX jest ujemny, to wystąpił błąd).
Są generalnie dwa podejścia do tego problemu:
Podejście pierwsze jest zilustrowane takim kodem dla liczb 16-bitowych (0-65535):
mov ax, [liczba] xor dx, dx mov cx, 10000 div cx or al, '0' ; wyświetl AL jako znak mov ax, dx xor dx, dx mov cx, 1000 div cx or al, '0' ; wyświetl AL jako znak mov ax, dx mov cl, 100 div cl or al, '0' ; wyświetl AL jako znak mov al, ah xor ah, ah mov cl, 10 div cl or ax, '00' ; wyświetl AL jako znak ; potem wyświetl AH jako znak
Jak widać, im więcej cyfr może mieć liczba, tym więcej będzie takich bloków. Trzeba zacząć od najwyższej możliwej potęgi liczby 10, bo inaczej może dojść do przepełnienia. W każdym kroku dzielnik musi mieć o jedno zero mniej, gdyż inaczej nie uda się wyświetlić prawidłowego wyniku (może być dwucyfrowy i wyświetli się tylko jakiś znaczek). Ponadto, jeśli liczba wynosi na przykład 9, to wyświetli się 00009, czyli wiodące zera nie będą skasowane. Można to oczywiście ominąć.
Podejście drugie jest o tyle wygodniejsze, że można je zapisać za pomocą pętli. Jest to zilustrowane procedurą _pisz_ld z części czwartej oraz kodem z mojej biblioteki:
mov ax, [liczba] xor si, si ; indeks do bufora mov cx, 10 ; dzielnik _pisz_l_petla: ; wpisujemy do bufora reszty z ; dzielenia liczby przez 10, xor dx, dx ; czyli cyfry wspak div cx ; dziel przez 10 or dl, '0' ; dodaj kod ASCII cyfry zero mov [_pisz_bufor+si], dl ; zapisz cyfrę do bufora inc si ; zwiększ indeks test ax, ax ; dopóki liczba jest różna od 0 jnz _pisz_l_petla _pisz_l_wypis: mov al, [_pisz_bufor+si-1] ; pobierz znak z bufora call far _pisz_z ; wyświetla znak dec si ; przejdź na poprzedni znak jnz _pisz_l_wypis
Zmienna _pisz_bufor to bufor odpowiedniej liczby bajtów.
Do tego zagadnienia algorytm jest następujący:
sub al, '0'
Przykładową ilustrację można znaleźć także w mojej bibliotece:
xor bx, bx ; miejsce na liczbę l_petla: call far _we_z ; pobierz znak z klawiatury cmp al, lf ; czy Enter? je l_juz ; jeśli tak, to wychodzimy cmp al, cr je l_juz ; przepuszczamy Spacje: cmp al, spc je l_petla cmp al, '0' ; jeśli nie cyfra, to błąd jb l_blad cmp al, '9' ja l_blad and al, 0fh ; izolujemy wartość (sub al, '0') mov cl, al mov ax, bx shl bx, 1 ; zrobimy miejsce na nową cyfrę jc l_blad shl ax, 1 jc l_blad shl ax, 1 jc l_blad shl ax, 1 jc l_blad add bx, ax ; BX=BX*10 - bieżącą liczbę mnożymy przez 10 jc l_blad add bl, cl ; dodajemy cyfrę adc bh, 0 jc l_blad ; jeśli przekroczony limit, to błąd jmp short l_petla l_juz: ; wynik w AX
Powiedzmy, że użytkownik naszego programu wpisał nam jakieś znaki (tekst, liczby). Jak teraz sprawdzić, co dokładnie otrzymaliśmy? Sprawa nie jest trudna, lecz wymaga czasem zastanowienia i tablicy ASCII pod ręką.
Cyfry w kodzie ASCII zajmują miejsca od 30h (zero) do 39h (dziewiątka). Wystarczy więc sprawdzić, czy wczytany znak mieści się w tym zakresie:
cmp al, '0' jb nie_cyfra cmp al, '9' ja nie_cyfra ; tu wiemy, że AL reprezentuje cyfrę. ; Pobranie wartości tej cyfry: and al, 0fh ; skasuj wysokie 4 bity, zostaw 0-9
Litery, podobnie jak cyfry, są uporządkowane w kolejności w dwóch osobnych grupach (najpierw wielkie, potem małe). Aby sprawdzić, czy znak w AL jest literą, wystarczy kod
cmp al, 'A' jb nie_litera ; na pewno nie litera cmp al, 'Z' ja sprawdz_male ; na pewno nie wielka, ; sprawdź małe ; tu wiemy, że AL reprezentuje wielką literę. ; ... sprawdz_male: cmp al, 'a' jb nie_litera ; na pewno nie litera cmp al, 'z' ja nie_litera ; tu wiemy, że AL reprezentuje małą literę.
Tu sprawa jest łatwa: należy najpierw sprawdzić, czy dany znak jest cyfrą. Jeśli nie, to sprawdzamy, czy jest wielką literą z zakresu od A do F. Jeśli nie, to sprawdzamy, czy jest małą literą z zakresu od a do f. Wystarczy połączyć powyższe fragmenty kodu. Wyciągnięcie wartości wymaga jednak więcej kroków:
; jeśli AL to cyfra '0'-'9' and al, 0fh ; jeśli AL to litera 'A'-'F' sub al, 'A' - 10 ; jeśli AL to litera 'a'-'f' sub al, 'a' - 10
Jeśli AL jest literą, to najpierw odejmujemy od niego kod odpowiedniej (małej lub wielkiej) litery A. Dostajemy wtedy wartość od 0 do 5. Aby dostać realną wartość danej litery w kodzie szesnastkowym, wystarczy teraz dodać 10. A skoro AL-'A'+10 to to samo, co AL-('A'-10), to już wiecie, skąd się wzięły powyższe instrukcje.
Oczywistym sposobem jest odjęcie od litery kodu odpowiedniej litery A (małej lub wielkiej), po czym dodanie kodu tej drugiej, czyli:
; z małej na wielką sub al, 'a' add al, 'A' ; z wielkiej na małą sub al, 'A' add al, 'a'
lub nieco szybciej:
; z małej na wielką sub al, 'a' - 'A' ; z wielkiej na małą sub al, 'A' - 'a'
Ale jest lepszy sposób: patrząc w tabelę kodów ASCII widać, że litery małe od wielkich różnią się tylko jednym bitem - bitem numer 5. Teraz widać, że wystarczy
; z małej na wielką and al, 5fh ; z wielkiej na małą or al, 20h
To zagadnienie można rozbić na dwa etapy:
Do wyświetlenia części całkowitej może nam posłużyć procedura wyświetlania liczb całkowitych, wystarczy z danej liczby wyciągnąć część całkowitą. W tym celu najpierw ustawiamy tryb zaokrąglania na obcinanie (gdyż inaczej na przykład część całkowita z liczby 1,9 wyniosłaby 2):
fnstcw [status] ; status to 16-bitowe słowo or word [status], (0Ch << 8) ; zaokrąglanie: obcinaj ;or word [status], (0Ch shl 8) ; dla FASMa fldcw [status]
W trakcie całej procedury wyświetlania będziemy korzystać z tego właśnie trybu zaokrąglania.
Pamiętajcie, aby przy wyjściu z procedury przywrócić poprzedni stan
słowa kontrolnego koprocesora (na przykład poprzez skopiowanie wartości zmiennej status
przed jej zmianą do innej zmiennej, po czym załadowanie słowa kontrolnego z tej drugiej zmiennej).
Teraz wyciągamy część całkowitą liczby następującym kodem:
frndint ; jeśli liczba była w ST0 fistp qword [cz_calkowita]
Pojawia się jednak problem, gdy część całkowita nie zmieści się nawet w 64 bitach. Wtedy trzeba
skorzystać z tego samego sposobu, który był podany dla liczb całkowitych: ciągłe dzielenie przez 10
i wyświetlenie reszt z dzielenia wspak.
W tym celu ładujemy na stos FPU część całkowitą
z naszej liczby oraz liczbę 10:
frndint ; jeśli liczba była w ST0 fild word [dziesiec] ; zmienna zawierająca wartość 10 fxch st1 ; stos: ST0=część całkowita, ST1=10
Stos koprocesora zawiera teraz część całkowitą naszej liczby w ST0 i wartość 10 w ST1. Po wykonaniu
fprem ; stos: ST0=mod (część całkowita,10), ST1=10
w ST0 dostajemy resztę z dzielenia naszej liczby przez 10 (czyli cyfrę jedności, do wyświetlenia jako ostatnią). Resztę tę zachowujemy do bufora na cyfry. Teraz dzielimy liczbę przez 10:
; ST0=część całkowita, ST1=10 fdiv st0, st1 ; ST0=część całkowita/10, ST1=10 frndint ; ST0=część całkowita z poprzedniej ; podzielonej przez 10, ST1=10
i powtarzamy całą procedurę do chwili, w której część całkowita stanie się zerem, co sprawdzamy takim na przykład kodem:
ftst ; zbadaj liczbę w ST0 i ustaw flagi FPU fstsw [status] ; zachowaj flagi FPU do zmiennej mov ax, [status] sahf ; zapisz AH do flag procesora jnz powtarzamy_dzielenie
Po wyświetleniu części całkowitej należy wyświetlić separator (czyli przecinek), po czym zabrać się do wyświetlania części ułamkowej. To jest o tyle prostsze, że uzyskane cyfry można od razu wyświetlić, bez korzystania z żadnego bufora.
Algorytm jest podobny jak dla liczb całkowitych, z tą różnicą, że teraz liczba jest na każdym kroku mnożona przez 10:
; ST0=część ułamkowa, ST1=10 fmul st0, st1 ; ST0=część ułamkowa * 10, ST1=10 fist word [liczba] ; cyfra (część ułamkowa*10) do zmiennej
Po wyświetleniu wartości znajdującej się we wskazanej zmiennej, należy odjąć ją od bieżącej liczby, dzięki czemu na stosie znów będzie liczba mniejsza od jeden i będzie można powtórzyć procedurę:
fild word [liczba] ; ST0=część całkowita, ; ST1=część całkowita + część ułamkowa, ; ST2=10 fsubp st1, st0 ; ST0=nowa część ułamkowa, ST1=10
Po każdej iteracji sprawdzamy, czy liczba jeszcze nie jest zerem (podobnie jak powyżej).
Procedurę wczytywania liczb niecałkowitych można podzielić na dwa etapy:
Wczytywanie części całkowitej odbywa się podobnie, jak dla liczb całkowitych: bieżącą liczbę pomnóż przez 10, po czym dodaj aktualnie wprowadzoną cyfrę. Kluczowa część kodu wyglądać może więc podobnie do tego fragmentu:
; kod wczytujący cyfrę ładuje ją do zmiennej WORD [cyfra] ; ST0=10, ST1=aktualna liczba fmul st1, st0 ; ST0=10, ST1=liczba*10 fild word [cyfra] ; ładujemy ostatnią cyfrę, ; ST0=cyfra, ST1=10, ST2=10 * liczba faddp st2, st0 ; ST0=10, ST1=liczba*10 + cyfra
Procedurę tę powtarza się do chwili napotkania separatora części ułamkowej (czyli przecinka, ale można akceptować też kropkę). Od chwili napotkania separatora następuje przejście do wczytywania części ułamkowej.
Aby wczytać część ułamkową, najlepiej powrócić do algorytmu z dzieleniem. Wszystkie wprowadzane cyfry najpierw ładujemy do bufora, potem odczytujemy wspak, dodajemy do naszej liczby i dzielimy ją przez 10. Zasadnicza część pętli mogłaby wyglądać podobnie do tego:
fild word [cyfra] ; ST0=cyfra, ST0=bieżąca część ułamkowa, ST2=10 faddp st1, st0 ; ST0=cyfra+bieżąca część ułamkowa, ST1=10 fdiv st0, st1 ; ST0=nowa liczba/10 = nowy ułamek, ST1=10
Po wczytaniu całej części ułamkowej pozostaje tylko dodać ją do uprzednio wczytanej części całkowitej i wynik gotowy.
Pamiętajcie o dobrym wykorzystaniu stosu koprocesora: nigdy nie przekraczajcie ośmiu elementów i nie zostawiajcie więcej, niż otrzymaliście jako parametry.