2.4 Skoki warunkowe

Instrukcje skoków nie są szczególnie lubiane przez programistów języków wysokiego poziomu dlatego, że nieodpowiednie ich użycie może doprowadzić do zawieszenia się programu po przez wykonywanie cały czas tych samych partii kodu. W przypadku nauki języków wyższego poziomu bardzo często zabrania się stosowania skoków lub nawet nie implementuje się ich w składni języka. Oczywistym jest, że każdy język korzysta ze skoków mimo iż jawnie nie oferuje ich użycia programistom. W asemblerze możemy się posługiwać skokami do woli i stanowią one dość znaczącą część kodu każdego programu. Skoki stosuje się gdy pewna część kodu ma być wykonana przy spełnieniu określonego warunku. Co charakterystyczne dla procesorów warunek ten jest postaci działania arytmetycznego lub logicznego na liczbach binarnych.

Skoki warunkowe opierają się w swoim działaniu na stanie rejestru flag. Stany poszczególnych jego bitów określają bezpośrednio czy dany skok ma być wykonany czy też nie. Pierwszym prostym przykładem może być bit określający zerowy wynik czyli bit ZF (Zero Flag). Bit ten ustawiany jest jeżeli wynik ostatniej instrukcji obliczeniowej wynosił 0. Oczywiście w innym wypadku stan tego bitu ma wartość 0.

Przykład 1:
label:
	mov	ah,	5
	sub	ah,	5
	jz	label

Jest to jednocześnie przykład błędnie napisanego programu ponieważ wpędzi on procesor w nieskończoną pętlę, ale nie to jest tutaj najważniejsze. Na początku wpisujemy do rejestru ah wartość 5, a następnie odejmujemy wartość 5 co w wyniku da nam zerową wartość rejestru ah. Jednocześnie po wykonaniu drugiej instrukcji procesor automatycznie ustawi flagę ZF w rejestrze flag. Dzięki temu instrukcja skoku znajdująca się w kolejnej linii wykona skok programu do etykiety label, czyli program wykonał by się od początku co prowadzi jak wcześniej wspomniałem do nieskończonej pętli. Teraz przedstawię trochę poprawiony kod.

Przykład 2:
	...
	mov	al,	14
	sub	ah,	5
	jz	label
	mov	al,	15
label:
	...

Załóżmy, że ten kod jest częścią jakiegoś programu. W tym programie wykonywany jest również warunkowy skok oparty na fladze ZF. W tym przypadku nie mamy podanej jawnie początkowej wartości rejestru ah. Jeżeli ta wartość wynosiła 5 to po odjęciu od niej 5 wyzerujemy rejestr co w wyniku da nam wykonanie przez procesor skoku do etykiety label. W wyniku oczywiście nie zostanie wykonana instrukcja wpisująca do rejestru al wartości 15 i zostanie tam poprzednio wpisana wartość 14. Jeżeli wartość rejestru ah na początku miała by jakąkolwiek inną wartość niż 5 to otrzymamy w rejestrze al wartość 15. Jest to najprostszy przykład kodu wykonywanego warunkowo.

Instrukcje skoku warunkowego działają na różnych flagach rejestru flag i czasami nawet na kilku jednocześnie. Programista nie musi praktycznie wiedzieć na jakich flagach procesor wykonuje testowanie wartości ponieważ często najistotniejszym jest relacja jaka wynika z porównywania liczb. Można oczywiście porównywać liczby po przez odejmowanie i sprawdzanie np. stanu flagi ZF jak to opisałem powyżej jednak nie jest to najlepsza instrukcja. Do porównywania została wydzielona oddzielna instrukcja o nazwie cmp (compare) działająca na dwóch argumentach. Asembler dodatkowo serwuje nam szereg skoków warunkowych wykonujących skok jeżeli spełniona jest odpowiednia relacja między argumentami instrukcji cmp. Trzeba pamiętać iż praktycznie każda instrukcja asemblera zmienia flagi procesora dlatego porównując liczby powinniśmy używać instrukcji cmp tuż przed odpowiadającymi im instrukcjami skoków warunkowych.

Przykład 3:
	...
	mov	al,	14
	cmp	ah,	5
	jz	label
	mov	al,	15
label:
	...

Kod z przykładu 3 działał by identycznie jak poprzedni z tą zaletą iż przy sprawdzaniu wartości rejestru ah nie zmienia jego wartości. Dzięki temu można na przykład wykonać na nim kolejne porównanie z innymi wartościami. Instrukcja cmp jako argumenty może przyjmować rejestr-rejestr, rejestr-bezpośrednia wartość, rejestr-pamięć lub pamięć-bezpośrednia wartość. Instrukcje skoku natomiast mogą przyjmować dużo bardziej skompilowane testy na rejestrze flag niż tylko test zera. To oczywiście owocuje całą masą różnych form skoków warunkowych, które wypisane są w tabeli poniżej. Składnia instrukcji skoku warunkowego przedstawia się następująco:

j + [negacja] + [warunek nierówności] + [warunek równania] {etykieta}

Nie wszystkie człony składające się na nazwę instrukcji muszą w niej wystąpić. W przypadku sprawdzania czy oba argumenty są sobie równe instrukcja przyjmuje w nazwie tylko literkę j + warunek równości co daje w wyniku instrukcję:

je    label

W rzeczywistości jest to ta sama instrukcja, którą przedstawiłem wcześniej testująca stan flagi ZF. Różnica polega na tym, że przedstawiona w tej formie bardziej intuicyjnie informuje nazwą swoje działanie. Można ją mianowicie rozszyfrować jako je = jump if eual (skocz jeżeli równe). je należy do pewnego zestawu instrukcji, które swoimi skrótowymi nazwami określają relację między argumentami. Cały zestaw tych instrukcji znajduje się poniżej.

warunek działanie negacja warunku
je (lub jz) = jump if equal jne (lub jnz) ! =
bez znaku
ja > jump if above jna ! >
jae >= jump if above or equal jnae ! >=
jb < jump if below jnb ! <
jbe <= jump if below or equal jnbe ! <=
ze znakiem
jg > jump if greater jng ! >
jge >= jump if greater or equal jnge ! >=
jl < jump if less jnl ! <
jle <= jump if less or equal jnle ! <=

W przypadku nierówności argumentów rozróżnia się oddzielne instrukcje dla liczb ze znakiem i bez znaku.

Użycie instrukcji cmp powoduje sprawdzenie relacji pierwszego argumentu względem drugiego.

Przykład 4:
...
cmp	ax,	bx
jae	label
...

W tym kodzie skok do instrukcji label zostanie wykonany jeżeli ax >= bx.

org 0x100
 
	cmp	ax,	bx
	ja	wieksze
	cmp	ax,	bx
	jb	mniejsze
 
	mov	dx,	msg_rowne
	mov	ah,	9
	int	0x21
 
	mov	ax,	0x4C00
	int	0x21
 
wieksze:
	mov	dx,	msg_wieksze
	mov	ah,	9
	int	0x21
 
	mov	ax,	0x4C00
	int	0x21
 
mniejsze:
	mov	dx,	msg_mniejsze
	mov	ah,	9
	int	0x21
 
	mov	ax,	0x4C00
	int	0x21
 
msg_wieksze		db	"ax > bx$"
msg_mniejsze	db	"ax < bx$"
msg_rowne		db	"ax = bx$"

Powyższy program sprawdzi jaka jest relacja między wartościami rejestrów ax i bx i wypisze na ekran odpowiednią informację. Program oczywiście sam w sobie nie ma większego sensu i ma na celu pokazanie jak wykonuje się warunkowo pewne partie kodu. Jeżeli chodzi o bardziej sensowne wykorzystanie możemy napisać program, który czeka aż użytkownik naciśnie klawisz, a następnie wypiszę na ekran informację czy wciśniętym klawiszem był klawisz "r". Do tego oczywiście będzie potrzebna kolejna procedura systemowa pobierająca klawisz z urządzenia klawiatury. Jest to funkcja 7 przerwania 0x21 i zwraca ona w rejestrze al kod ASCII znaku "r".

org 0x100
 
	mov	ah,	7
	int	0x21
 
	mov	dx,	msg_no
	cmp	al,	'r'
	jnz	no_r
	mov	dx,	msg_yes
no_r:
	mov	ah,	9
	int	0x21
 
	mov	ax,	0x4C00
	int	0x21
 
msg_no	db	"Wcisnieto inny klawisz niz 'r'$"
msg_yes	db	"Wcisnieto klawisz 'r'$"

Poza porównywaniem liczb skoki warunkowe mogą być wykonywane bezpośrednio testując jedną z flag. Mogą to być flagi CF, SF, OF i PF. Instrukcje im odpowiadające to:

instruckja działanie instrukcja z negacją warunku
jc jump if CF=1 jnc
jz jump if ZF=1 jnz
js jump if SF=1 jns
jo jump if OF=1 jno
jp jump if PF=1 jnp

Te instrukcje nie są zbyt często stosowane ponieważ dotyczą dość niepraktycznych warunków w odniesieniu do porównywania liczb. Z instrukcji testujących pojedyncze flagi najczęściej stosuje się jz/jnz (działanie identyczne jak je/jne) i jc/jnc. Flaga CF bardzo często zostaje użyta do informowania programu czy poprzednio wywołana procedura lub przerwanie wykonało się prawidłowo. Przyjęła się konwencja ustawiania flagi CF jeżeli podprogram lub przerwanie zakończyło swoje działanie z błędem, a zerowanie CF jeżeli zakończyło się prawidłowym wynikiem.