2.7 Skoki bezwarunkowe

Instrukcje zaliczane do skoków bezwarunkowych to instrukcje, które dokonują modyfikacji rejestru IP lub CS:IP bez testowania stanu rejestru flag. Czyli różnica względem skoków warunkowych jest taka, że skok bezwarunkowy wykonywany jest zawsze. Dodatkowo można za pomocą niektórych instrukcji skoczyć do innego segmentu zmieniając wartość rejestru CS co w przypadku skoków warunkowych jest niemożliwe. Podstawowe instrukcje zaliczające się do takich skoków to JMP, CALL, RET, INT.

JMP

Instrukcja powodująca bezpośredni skok do pamięci o wskazanym adresie jako argument. Skoki JMP (Jump) rozróżnia się ze względu na to czy skok modyfikuje zawartość obu rejestrów CS:IP (skok daleki FAR) czy tylko rejestru IP (skok bliski NEAR). Skoki bliskie można podzielić jeszcze ze względu na to czy adres docelowy podany jest bezpośrednio czy też jako przesunięcie względem obecnej wartości rejestru IP. Wszystkie te rodzaje skoków postaram się teraz opisać.

Skok daleki, "Far"

Skoki dalekie charakteryzują się tym, że zmieniają jednocześnie wartości rejestrów CS i IP. W kodzie można stosować dwa warianty tego typu instrukcji różniące się tym w jaki sposób przekazujemy nowe wartości rejestrów. Najczęściej wartości określające miejsce skoku przekazuje się w postaci bezpośredniej (direct) np.:

far direct:
jmp 12:101

Podajemy w pierwszej kolejności numer segmentu, a po dwukropku przesunięcie względem jego początku. Oczywiście numer segmentu jest zawsze liczbą 16 bitową, a przesunięcie w zależności od trybu pracy procesora może być 16,32 lub 64 bitowe. Drugą możliwością jest przekazanie adresu w sposób pośredni (indirect) przez pamięć. W tym przypadku adres docelowy skoku zostaje umieszczony gdzieś w pamięci, a jako parametr dla instrukcji przekazujemy adres tej pamięci np.:

far indirect:
jmp far [update_cs] ... update_cs dw 101 dw 12

Skok bliski bezwzględny, "Near absolute"

Skok ten należy do grupy skoków bliskich (Near), a więc modyfikuje jedynie zawartość rejestru wskaźnika IP. Pierwszą przedstawioną wersją jest skok pośredni, oznacza to iż adres podawany przy skoku nie jest kodowany w instrukcji tylko znajduje się w pamięci lub w rejestrze. W przypadku podawania adresu po przez pamięć należy podać w wywołaniu instrukcji adres pamięci np.:

near absolute indirect:
jmp near [update] ... update dw 101

W przypadku pominięcia dyrektywy near skok i tak zostanie standardowo zinterpretowany przez kompilator jako bliski jednak lepiej samemu zadbać o poprawny kod. Po wykonaniu przedstawionego skoku do rejestru IP zostanie wpisana wartość znajdująca się w pamięci pod etykietą update.

Adres docelowy skoku można również przekazać pośrednio przez rejestr np. bx.

near absolute direct:
mov bx, 101 jmp bx

W tym przypadku do rejestru IP zostanie skopiowana zawartość rejestru bx.

Skok bliski względny, "Near retative"

Różnica w stosunku do skoku bezwzględnego polega jedynie na formie podawanego adresu, który jest zapisany jako względne przesunięcie. Instrukcja skoku przyjmuje argument bezpośredni, który zostaje zapisany w kodzie instrukcji. Docelowa wartość rejestru IP po skoku będzie wynosiła IP = adres_względny + IP

org 0x100
 
	mov	ax,	0
	jmp	near skip1
skip0:
	mov	ax,	0x4C00
	int	0x21
skip1:
	xor	bx,	bx
	jmp	skip0

Po przekompilowaniu wygląda następująco:

0747:0100 B80000 MOV AX,0000 0747:0103 E90500 JMP 010B 0747:0106 B8004C MOV AX,4C00 0747:0109 CD21 INT 21 0747:010B 31DB XOR BX,BX 0747:010D EBF7 JMP 0106

Pod adresem 0x747:0x103 mamy instrukcję skoku bezwarunkowego. Jego kod składa się z trzech kolejnych bajtów 0xE9, 0x05 i 0x00. Pierwszy bajt jest kodem informującym o tym iż jest to instrukcja skoku zawierająca w dwóch kolejnych bajtach wartość względnego przesunięcia. W komputerach klasy PC wartości danych składających się z więcej niż z jednego bajta zapisywane są w pamięci w konwencji little endian. Oznacza to iż bajty odpowiadające najmniej znaczącym częścią liczby zapisywane są w mniejszych adresach. Tak więc aby złożyć wartości 0x05 i 0x00 w wartość o rozmiarze dwóch bajtów należy przestawić je w odwrotnej kolejności czyli otrzymujemy 0x0005. Jest to przesunięcie względne dla danego skoku. Liczone jest ono względem adresu instrukcji następującej po instrukcji skoku. Adres instrukcji następującej to 0x747:0x106 aby otrzymać adres docelowy skoku należy do części offsetowej dodać 0x0005 czyli otrzymujemy adres 0x747:0x10B.

Pod adresem 0x747:0x10D mamy kolejny skok, który jest tym samym typem skoku czyli bliskim względnym. Różnica polega jedynie na tym iż ten skok względne przesunięcie zapisuje w jednym bajcie. Kod instrukcji to 0xEB, a przesunięcie to 0xF7. W tym przypadku przeliczając adres docelowy skoku na początek bierzemy adres następnej instrukcji czyli 0x747:0x10F. Przesunięcie jest zapisywane w kodzie U2 ponieważ może przyjmować zarówno wartości ujemne jak i dodatnie. Wartość 0xF7 dekodujemy zgodnie z U2 na -9, w związku z tym musimy odjąć od części offsetowej -9 co daje w wyniku adres 0x747:0x106.

W poprzednim programie jak widać wprowadziłem dodatkowo kolejny podział skoków na krótkie i długie. Jak widać w programie prog12 instrukcje bliskie o adresie względnym standardowo przez NASM kompilowane są do kodu z przesunięciem względnym w jednym bajcie. Gdyby było inaczej to możemy wymusić takie przesunięcie po przez prefiks short. Jeżeli chcemy aby została użyta instrukcja z adresem o rozmiarze dwóch bajtów trzeba użyć dyrektywy near. To jakiej standardowo formy użyje kompilator NASM zależy od tego czy włączyliśmy optymalizowanie kodu. Dla zainteresowanych odsyłam do dokumentacji NASM:The -O Option: Specifying Multipass Optimization

Jako, że ilość typów skoków może być trudna do ogarnięcia postaram się je przedstawić w tabeli.

Typ skoku Przykład kodu
Far Direct
jmp 12:101
Far Indirect
jmp far [update_cs] ... update_cs dw 101 dw 12
Near Absolute Direct
mov bx, 101 jmp bx
Near Absolute Indirect
jmp near [update] ... update dw 101
Near Relatve
jmp near skok1 ... skok1:
Short Relative (1B)
jmp short skok2 ... skok2:

Dogłębna wiedza na temat soków bezwarunkowych w formie opisanej przeze mnie jest potrzebna jeżeli chcemy bardzo dobrze poznać język. Dla osób chcących jedynie pisać programy bez szczególnej wiedzy jak one są później zapisywane w postaci kodu maszynowego wystarczy zapamiętać podstawowy podział skoków. Skoki bliskie zmieniają tylko zawartość rejestru IP i te są najczęściej używane. Jeżeli z jakiś względów będziemy musieli dodatkowo zmienić zawartość rejestru CS to wtedy zastosujemy skok daleki FAR. Cały powyższy tekst miał jedynie na celu skatalogowanie wszystkich możliwych form zapisu adresu docelowego w tych dwóch rodzajach skoków.