Jak pisać programy w języku asembler?

Część 17 - Pobieranie i wyświetlanie, czyli jak komunikować się ze światem

O ile wyświetlanie i pobieranie od użytkownika tekstów jest łatwe do wykonania - wystarczy uruchomić tylko jedną funkcję systemową (ah=9 i ah=0A przerwania 21h) - 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ć.




Wyświetlanie tekstu


(przeskocz wyświetlanie tekstu)

Co prawda wszyscy już to umieją, ale dla porządku też o tym wspomnę.
Wszyscy znają funkcję ah=9 przerwania DOSa - wystarczy podać jej łańcuch znaków zakończony znakiem dolara, a ona wszystko sama wyświetli:

		mov	ah, 9
		mov	dx, offset tekst	; NASM/FASM: bez "offset"
		int	21h
		...
		tekst	db	"Tekst$"

Ale co, jeśli chcemy wyświetlić znak dolara? Albo nie mamy DOSa do dyspozycji?

Otóż, są jeszcze inne funkcje służące do wyświetlania tekstu na ekranie - na przykład funkcja ah=0E przerwania 10h (wyświetla po jednym znaku):

		mov	ah, 0eh
		mov	al, 'a'
		int	10h

Funkcja ah=2 przerwania DOSa także wyświetla po jednym znaku:

		mov	ah, 2
		mov	dl, 'a'
		int	21h

Funkcja ah=13h przerwania 10h jest bardziej złożona - wyświetla całe napisy, można podać pozycję napisu i kolor każdego znaku:

	mov	ax, 1301h		; funkcja pisania ciągu znaków
	mov	bx, 12			; atrybut (kolor)
	mov	cx, ds
	mov	es, cx			; es = ds
	mov	bp, offset info1	; adres ciągu, NASM/FASM: bez "offset"
	mov	cx, info1_dl		; długość ciągu
	mov	dx, 1122h		; wiersz i kolumna
	int	10h			; piszemy napis
	...
	info1		db	"Informacja"
	info1_dl	equ	$ - info1

Zawsze można też wyświetlać tekst ręcznie.




Pobieranie tekstu


(przeskocz pobieranie tekstu)

Do pobierania tekstów od użytkownika służyć może funkcja AH=0A przerwania DOSa. Wystarczy podać jej adres bufora takiej konstrukcji:

	bufor	db 20		; maksymalna liczba znaków do pobrania
		db 0		; tu dostaniemy, ile znaków pobrano
	dane:	times 22 db "$"	; miejsce na dane

DOS wczyta dane z klawiatury (co najwyżej tyle bajtów, ile podaliśmy), w drugim bajcie zwróci nam, ile faktycznie przeczytano (Enter kończy), a od trzeciego bajtu zaczynają się same dane. Można się do nich odwoływać albo poprzez [dane], albo poprzez [bufor+2].

Jeśli nie ma DOSa do dyspozycji, można korzystać z funkcji ah=0 przerwania klawiatury int 16h.



Wyświetlanie liczb całkowitych


(przeskocz wyświetlanie liczb całkowitych)

Są generalnie dwa podejścia do tego problemu:

  1. dzielenie przez coraz mniejsze potęgi liczby 10 (zaczynając od najwyższej odpowiedniej) i wyświetlanie ilorazów
  2. dzielenie przez 10 i wyświetlanie reszt wspak

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.



Pobieranie liczb całkowitych


(przeskocz pobieranie liczb całkowitych)

Do tego zagadnienia algorytm jest następujący:

  1. wczytaj łańcuch znaków od razu w całości lub wczytuj znak po znaku w kroku 3
  2. wstępnie ustaw wynik na 0
  3. weź kolejny znak z wczytanego łańcucha znaków (jeśli już nie ma, to koniec)
  4. zamień go na jego wartość binarną. Jeśli znak jest w AL, to wystarczy:
    sub al, '0'
  5. przemnóż bieżący wynik przez 10
  6. dodaj do niego wartość AL otrzymaną z kroku 4
  7. skacz do 3

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



Sprawdzanie rodzaju znaku


(przeskocz sprawdzanie rodzaju znaku)

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ą.

  1. Cyfry

    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
  2. Litery

    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ę.
  3. Cyfry szesnastkowe

    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.

  4. Przerabianie wielkich liter na małe i odwrotnie

    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



Wyświetlanie liczb niecałkowitych


(przeskocz wyświetlanie liczb niecałkowitych)

To zagadnienie można rozbić na dwa etapy:

  1. wyświetlenie części całkowitej liczby
  2. wyświetlenie części ułamkowej liczby

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).



Pobieranie liczb niecałkowitych

Procedurę wczytywania liczb niecałkowitych można podzielić na dwa etapy:

  1. wczytanie części całkowitej
  2. wczytanie części ułamkowej

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.



Poprzednia część kursu (klawisz dostępu 3)
Spis treści off-line (klawisz dostępu 1)
Spis treści on-line (klawisz dostępu 2)
Ułatwienia dla niepełnosprawnych (klawisz dostępu 0)

Ćwiczenia

  1. Korzystając z przedstawionych tu algorytmów, napisz algorytmy wczytujące i wyświetlające liczby dziesiętne 8-bitowe.
  2. Korzystając z przedstawionych tu algorytmów, napisz algorytmy wczytujące i wyświetlające liczby szesnastkowe 16-bitowe (wystarczy zmienić liczby, przez które mnożysz i dzielisz oraz to, jakie znaki są dozwolone i wyświetlane - dochodzą litery od A do F).