Lekce 10 - Assembler - Podmíněné a nepodmíněné skoky
V minulé lekci, Assembler - Další instrukce pro práci s čísly, jsme si ukázali násobení, dělení a také sčítání a odčítání velkých čísel.
V dnešním ASM tutoriálu se budeme věnovat podmíněným a nepodmíněným skokům.
Skoky se používají při programování podmínek nebo cyklů. V
kompilátoru MASM můžeme pro podmínky a cykly používat příkazy
.if
, .repeat
, .while
. Kompilátor MASM za
nás automaticky vygeneruje instrukce skoků. Naopak v kompilátoru NASM nejsou
žádné podmínky nebo cykly a tuto funkcionalitu musíme simulovat pomocí
skoků na různá návěstí v programu a samozřejmě pomocí porovnávacích
instrukcí. Právě této problematice se budeme dnes věnovat.
Skoky v ASM jsou podmíněné nebo nepodmíněné.
Nepodmíněné skoky
Nepodmíněné skoky složí k tomu, abychom mohli neomezeně skákat z
jedné části kódu do druhé. V našem Hello world
programu jsme takto např. skákali na návěstí Return
.
V praxi takto můžeme třeba vytvořit nekonečný cyklus nebo prostě přeskočit nějakou část kódu.
Instrukce JMP
(JuMP)
Instrukci JMP
již také známe, slouží právě k
nepodmíněnému skoku. Má pouze jeden operand a tím je návěstí, které
udává, kam máme skočit.
Návěstí deklarujeme přidáním :
za vybraný název, např.
takto:
sem_skoc:
Ve strojovém kódu samozřejmě nejsou žádná návěští,
ale číselné adresy. Ty za nás vypočítává kompilátor. Pro krátké skoky
vloží do instrukce 8, 16 nebo 32-bitovou relativní adresu. Ta je kladná pro
skoky směrem dopředu a záporná pro skoky zpět. Pro dlouhé skoky se
použije absolutní 16 nebo 32-bitovou adresa. Ve strojovém kódu proto
existuje několik instrukcí skoku: E9
, EA
,
EB
, FF
. V ASM jednoduše ve všech případech
používáme JMP
a kompilátor se podle délky skoku rozhodne,
jaký kód instrukce použije.
Někdy je cíl skoku potřeba určit až za běhu programu. V takových případech se používají nepřímé skoky, u kterých je adresa skoku buď v registru, nebo někde v paměti.
Příklad
Nyní si ukážeme příklad nekonečného cyklu:
znovu: mov si, hello_world_message call print_string jmp znovu hello_world_message db 'Hello, World!', 13, 10, 0
Kód předpokládá, že máme někde deklarovanou i funkci
print_string
, kterou jsme si deklarovali již dříve v našem
Hello world programu.
Deklarujeme si návěstí znovu:
a do registru SI
přesuneme text, který pomocí funkce print_string
vypíšeme.
Dále skočíme na návěstí znovu:
a celé se to bude opakovat
znovu, až do nekonečna. Samozřejmě nemusíme skočit dozadu, abychom takto
skákali stále dokola, ale můžeme skočit i někam dopředu, dále do
programu.
Výsledkem programu tedy bude:
Hello, World! Hello, World! Hello, World! Hello, World! ...
Z tohoto nekonečného cyklu bychom za nějaké podmínky mohli vyskočit pryč. K tomu ale již potřebujeme skok nějak opodmínkovat.
Podmíněné skoky
Častěji budeme ale samozřejmě potřebovat skočit podmíněně. To znamená, že skočíme pouze tehdy, pokud bude splněna nějaká podmínka.
Instrukce CMP
(CoMPare)
Před provedením samotného skoku je potřeba použít instrukci
CMP
(CoMPare). Ta má dva operandy (A
a
B
) a defakto supluje za příkaz if
z vyšších
programovacích jazyků. CMP
operandy porovná a na výsledek pak
můžeme reagovat pomocí dalších instrukcí.
Celá podmínka se zapisuje takto:
cmp [první operand], [druhý operand] j[x] [návěstí]
Pojďme si ukázat tabulku, na jaké výsledky porovnání můžeme pomocí kterých instrukcí reagovat:
Porovnání | A == B | A != B | A > B | A < B | A >= B | A <= B |
---|---|---|---|---|---|---|
Instrukce | JE (JZ) | JNE (JNZ) | JA (JNBE) | JB (JNAE) | JNB (JAE) | JNA (JBE) |
Instrukce pro každou možnost jsou hned 2. Nenechte se zmást,
důvod je prostý. Procesor reálně obě čísla porovnává tak, že je
odečte. Můžeme se tedy ptát, zda jsou např. 2 čísla rovna nebo zda je
jejich rozdíl 0
. Obojí je to samé, jen má instrukce pro
přehlednost jiný název, aby bylo z kódu jasné, co v dané chvíli
myslíme.
Instrukce JE
(Jump if Equal) a JZ
(Jump if Zero)
Skočí na dané návěstí, pokud jsou porovnávaná čísla
stejná (A == B
).
Instrukce
JNE
(Jump if Not Equal) a JNZ
(Jump if Not Zero)
Skočí na dané návěstí, pokud jsou porovnávaná čísla
různá (A != B
).
Instrukce
JA
(Jump if Above) a JNBE
(Jump if Not Below or
Equal)
Skočí na dané návěstí, pokud je první porovnávané číslo
větší než druhé (A > B
).
Instrukce
JB
(Jump if Below) a JNAE
(Jump if Not Above or
Equal)
Skočí na dané návěstí, pokud je první porovnávané číslo
menší než druhé (A > B
).
Instrukce
JNB
(Jump if Not Below) a JAE
(Jump if Above or
Equal)
Skočí na dané návěstí, pokud je první porovnávané číslo
větší nebo rovno než druhé (A >= B
).
Instrukce
JNA
(Jump if Not Above) a JBE
(Jump if Below or
Equal)
Skočí na dané návěstí, pokud je první porovnávané číslo
menší nebo rovno než druhé (A <= B
).
Pokud víte něco o reprezentaci čísel v počítači, tak čísla, která mohou mít i zápornou hodnotu, jsou v paměti uložena jinak (protože musí mít vymezenou paměť i pro záporné hodnoty, jsou posunutá o polovinu rozsahu). Toto má vliv na porovnávání čísel a ASM za nás problém neřeší. Operace pro skoky, které jsme si ukázali, platí pouze pro unsigned čísla (tedy ta, která mohou být jen nezáporná). Jak se s tím vypořádat u signed čísel si ukážeme příště.
Příklad
Určitě si teď zasloužíme příklad. Tady je:
mov ax, 1 mov bx, 2 cmp ax, bx jb bx_je_vetsi ;v této části programu bx není větší jmp konec bx_je_vetsi: ;v této části programu je bx větší konec:
Program skočí na dané návěstí podle toho, zda je hodnota 1
> 2
.
Instrukce TEST
Nejčastěji se instrukce TEST
používá pro zjištění,
jestli je hodnota v registru kladná, záporná, nebo nulová.
Porovnání | A == 0 | A != 0 | A < 0 | A >= 0 |
---|---|---|---|---|
Instrukce | JZ | JNZ | JS | JNS |
Použití pak vypadá následovně:
test ax,ax jz ax_je_nulove js ax_je_zaporne
Pokud jsou operandy instrukce TEST
různé, provede se logická
operace AND. Tím můžeme testovat jednotlivé bity prvního operandu.
Nejčastěji se testuje poslední bit, čímž se zjistí, jestli je číslo
sudé nebo liché:
test ax,1 jz ax_je_sude jnz ax_je_liche
Cykly
U všech typů cyklů můžeme používat kterékoli obecné registry. U for cyklů ale musíme dávat pozor, abychom nechtěně uvnitř cyklu nezměnili hodnotu registru, ve kterém máme řídící proměnnou cyklu.
Ukažme si, jak pomocí skoků nahradit různé druhy cyklů, které určitě znáte z vyšších programovacích jazyků.
Cyklus s podmínkou na konci (do
-while
):
muj_cyklus: ; Vnitřek cyklu cmp ax,bx ja muj_cyklus
Cyklus s podmínkou na začátku (while
):
jmp porovnani muj_cyklus: ; Vnitřek cyklu porovnani: cmp ax,bx ja muj_cyklus
Cyklus od 10
do 1
(for
):
mov cx, 10 muj_cyklus: ; Vnitřek cyklu dec cx jnz muj_cyklus
Cyklus od 1
do 10
(for
):
mov cx, 1 muj_cyklus: ; Vnitřek cyklu inc cx cmp cx,10 jbe muj_cyklus
Ve starších programech se také často používala instrukce
loop
. Ta dělá úplně to samé jako dvě instrukce
dec cx
a jnz návěstí
. Jediný rozdíl je v tom, že
je asi čtyřikrát pomalejší. Druhá nevýhoda je v tom, že ji nelze
použít s jiným registrem než cx
.
V příští lekci, Assembler - Kombinace skoků a příznaky, se naučíme kombinovat podmíněné a nepodmíněné skoky, zjistíme, jak skokové instrukce získají výsledek z CMP a jak porovnávat signed čísla.