2.6 Procedury

Bardzo często w dużych programach zdarzają się algorytmy wykonywane dość często. Aby nie trzeba było wklepywać kilkanaście razy tego samego kodu w różne miejsca w programie, zostały wprowadzone mechanizmy operowania na procedurach. Procedurą jest więc pewna część kodu (zwykle jeden kompletny algorytm), który istnieje w całym programie tylko w jednym miejscu, ale może być w nim wykonywany dowolną ilość razy i w dowolnym jego miejscu. To oczywiście nie jest ścisła definicja, ale przedstawia ideę powstania procedur.

W poprzedniej części przedstawiłem kod, który wypisywał na ekran ciąg znaków zakończony wartością 0. W większości programów wypisywanie na ekran jest bardzo często wykonywaną czynnością więc na pewno warto umieścić ten kod w procedurze. Tutaj sytuacja jest podobna do pętli programowych, a mianowicie nie ma ścisłych reguł ograniczających nas tak jak w językach wysokiego poziomu. Generalnie procedurę określa jej punkt startowy i punkt wyjściowy. Oczywiście swoboda asemblera pozwala na tworzenie dowolnej ilości wejść czy wyjść z procedury, ale na razie ograniczmy się do prostego schematycznego przykładu.

org 0x100
 
	call procedura0
 
	mov	ax,	0x4C00
	int	0x21
 
procedura0:
    ret

W przykładzie prog10 istnieje jedna pusta procedura. Jej początek określony jest przez adres w pamięci oznaczony w kodzie jako etykieta procedura0. Koniec procedury jest wyznaczony instrukcją RET. Do wykonywania kodu procedur służy instrukcja CALL. W momencie jej wywołania na stos odkładany jest adres następnej instrukcji po aktualnie wykonywanej. Następnie do rejestru IP ładowany jest adres wywoływanej procedury czyli procedura0. Potem wykonywana jest instrukcja adresowana rejestrami CS:IP, co w tym przypadku będzie akurat powrotem z procedury czyli instrukcją RET. Ta instrukcja pobiera ze stosu wartość i ładuje ją do rejestru IP. Tak więc przy pobieraniu następnej instrukcji znowu będziemy w głównym kodzie programu za instrukcją CALL. Przedstawię to w debugerze.

C:\>nasm -f bin -o prog10.com prog10.asm C:\>debug prog10.com -r AX=FFFF BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0100 NV UP EI PL NZ NA PO NC 075E:0100 E80500 CALL 0108 -d ss:fffe 075E:FFF0 00 00 ..
Po przekompilowaniu i uruchomieniu programu w debug.exe, wyświetlamy stany rejestrów komendą r. Widzimy, że następną wykonaną instrukcją będzie CALL, a obecny adres czyli wartość IP=0x100. Kod instrukcji CALL to 0xE8, a jej parametr to dwubajtowa wartość 0x0005. Ta wartość to względny adres procedury. Bezwzględny adres otrzymamy poprzez wykonanie:

IP + rozmiar(CALL) + 0x0005 = 0x100 + 3 + 5 = 0x108

Wyświetlając zawartość stosu widzimy, że znajduje się na nim zerowa wartość dwubajtowa. Została ona tam umieszczona najprawdopodobniej przez system operacyjny. Po wykonaniu instrukcji CALL powinniśmy przejść do wykonywania kodu procedury o wskazanym adresie (0x108), a na stos powinna zostać wrzucona wartość adresu następnej instrukcji po CALL. Wykonujemy więc jedną instrukcję komendą t, a następnie sprawdzamy stany rejestrów i stosu.

-t AX=FFFF BX=0000 CX=0009 DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0108 NV UP EI PL NZ NA PO NC 075E:0108 C3 RET -d ss:fffc 075E:FFF0 03 01 00 00 ....
Rejestr IP przechowuje teraz wartość początku procedury czyli 0x108 i następną wykonywaną instrukcja jest RET. To oznacza, że wykonujemy teraz kod procedury. Na stosie są odłożone 2 wartości dwubajtowe 0x0103 i 0x0000. Po wykonaniu instrukcji powrotu, ze stosu zostanie zdjęta wartość i wpisana do rejestru IP.

-t AX=FFFF BX=0000 CX=0009 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=075E ES=075E SS=075E CS=075E IP=0103 NV UP EI PL NZ NA PO NC 075E:0103 B8004C MOV AX,4C00

W wyniku wrócimy z wykonywanej procedury i zaczniemy wykonywać kod kończący program. Prog10 jest właściwie tylko szablonowym przedstawieniem procedury. W kolejnym przykładzie umieszczę w procedurze algorytm wypisywania na ekran ciągu znaków.

Procedura z parametami

org 0x100
 
	mov	bx,	prog_start
	call	Print
 
	mov	bx,	hello_world
	call	Print
 
	mov	bx,	prog_end
	call	Print
 
	mov	ax,	0x4C00
	int	0x21
 
prog_start		db	"Program start ",0
hello_world		db	"Hello world ! ",0
prog_end		db	"Program end ",0
 
;###############################################################################
;# Print string at current cursor position
;# IN:
;#	ds:bx = ASCIIZ input string address
;###############################################################################
Print:
	mov	ah,	2
	mov	dl,	[ds:bx]
	cmp	dl,	0
	jz	end
	int	0x21
	inc	bx
	jmp	Print
end:
	ret

Program 11 przedstawia przykład użycia procedury przyjmującej parametry. W tym konkretnym przypadku jest to adres DS:BX do ciągu znaków zakończonego zerem. Procedura wypisze ten ciąg znaków na ekran w miejsce gdzie aktualnie znajduje się kursor. Ten typ przekazywania parametrów jest nazywany przekazywaniem przez rejestry. Jako że wywołanie procedury przez instrukcję CALL nie zmienia żadnych innych rejestrów poza IP to można ich użyć do przekazania procedurze pewnych informacji wejściowych.

Program 11 korzysta z przerwania 0x21, które jest zestawem podprogramów wgrywanym do pamięci przez system DOS. Gdybyśmy mogli wypisywać na ekran znaki bez pomocy tego przerwania to uniezależnilibyśmy się od systemu operacyjnego. Z pomocą przychodzą tutaj inne przerwania, których kod umieszczany jest w pamięci przez mapowanie obszarów ROM (Read Only Memory) z zewnętrznych pamięci takich jak BIOS (Basic Input Output System) czy kart grafiki.