Az ugrás olyan
tevékenység, amellyel a processzor utasításszámlálóját nem a programban soron
következő utasításra, hanem egy távolabbi pontra állítjuk. A hívás egy olyan ugrás,
amely lehetőséget ad arra, hogy visszatérjünk a kiindulópontba. Nézzünk erre
egy példát.
egy:
jmp negy
ketto:
mov ecx, 14h
harom:
ret
negy:
call ketto
ot:
mov eax, ecx
A "jmp" (jump,
ugrás) utasításnak megmondjuk, hogy milyen címet kell beállítani az EIP-be, vagyis az utasításszámlálóba. Ez a cím a "negy" címke címe. Ezáltal a vezérlés a "negy" címke után található utasításra adódik. Azok az
utasítások, amelyek az "egy" és a "negy"
címke közt találhatóak, nem kerültek végrehajtásra.
A "negy" címke után egy call (hívás) utasítás szerepel, amelynek a "ketto" címke címét adjuk át. Ezáltal az EIP regiszter
a "ketto" címke címére áll, de még valami
más is történik. A verembe mentődik a következő utasítás címe, esetünkben ez az
"ot" címke. Minden program részére fenn
kell tartani egy veremterületet. Ez maximálisan egy szegmensnyi terület lehet és mint korábban is említettem, arra használható, hogy
visszatérési értékeket, paramétereket, stb. helyezzünk el benne, vagyis olyan
értékeket, melyeket menteni szeretnénk, hogy később felhasználjuk azokat. A
vermet egy olyan zsákként képzelhetjük el, amibe néha
bedobálunk kacatokat, amik jelenleg nem kellenek, majd később kiszedjük azokat.
Van itt egy fontos szabály: amit utoljára raktunk a verembe, azt vehetjük ki
legelőször. A vermet ezért LIFO-nak is hívják: "Last In, First
Out". Két művelettel érhetjük el a vermet közvetlenül: "push" (tol) és "pop" (felbukkan). A "push" hatására egy értéket tolhatunk a verembe, ez a
verem tetejére kerül, a többi pedig egy egységgel
lejjebb. Az egység a processzor valós (REAL) módjában 16 bit. A "pop"
utasítással a verem tetején található adat
“felbukkan”, ezáltal a többi érték is egységnyivel feljebb kerül. Elmondhatjuk
ezek alapján, hogy a "push" és
"pop" utasítások az ESP értékét is változtatják - hiszen mint tudjuk,
az ESP regiszter (veremmutató: Stack Pointer) mutatja
meg, hol tartunk a veremben. Az ESP értéke jelzi, hogy a veremben lévő értékek
látszólag felemelkedtek, vagy lejjebb csúsztak - ugyanis ezek az értékek -
természetesen - fizikailag ugyanazon a helyen maradnak, nem másolódnak sehova,
csak a verem teteje mozog - ezt a "magasságot" jelzi az ESP. Itt
azonban még egy trükk is be lett vetve: a verem a memória fizikai kezelése
szempontjából nézve felülről lefelé telítődik, vagyis ami fizikai memóriacím
szerint veremterület teteje (ESP=0xFFFFFFFF), az a verem alja
és ami a fizikai 0x1 cím, az a verem legteteje. Ha a fizikai 0x1 cím alá, vagy
a 0xFFFFFFFF cím fölé megyünk, akkor a verem túlcsordul, logikailag rendre
felül-, illetve alulcsordul. Ha egy üres verembe "push"
utasítással elhelyezünk egy 16 bites értéket (ez ugye 2 byte), akkor az ESP
mutató értéke 0xFFFFFFFD lesz, vagyis kettővel kevesebb, mint ami a verem
tetejét jelezné, ha pedig ezt a 16 bites értéket egy "pop"
utasítással kiemeljük, akkor az ESP értéke 0xFFFFFFFF lesz.
A veremszegmens címét a processzor SS regisztere tartalmazza.
Láthatjuk, hogy az ESP egy 32 bites regiszter, vagyis a verem elvileg 2^32 byte
(4.3 gigabyte) méretű lehet,
valós módban azonban ebből csak a 16 bites SP rész használható. Az SS regiszter
azonban mindig 16 bites, hiszen valós módban 65536 szegmenscím létezik, mivel
16 bájtonként haladunk 1MB-ig. A processzor védett módja teljesen másképp
működik, ott deszkriptorok írnak le egy memóriacímet és mivel egy deszkriptor
8 bájtos, összesen 65536 / 8 = 8092 deszkriptor
lehet.
Térjünk vissza a példához. Tegyük fel, hogy a vezérlés átadódott a
"ketto" címkére. Itt egy "mov ecx 0x14" utasítás
található, vagyis legyen ECX regiszter értéke 0x14. A következő a "harom" címke, itt egy "ret"
utasítás áll, ez a RETURN (visszatérés) rövidítése. Ezen utasítás hatására a
processzor kiolvassa a verem tetején lévő számot, és azt, mint visszatérési
címet betölti az EIP regiszterbe. A verem tetejére korábban a "call" utasítás helyezett el egy címet, az "ot" címke címét, vagyis most a vezérlés az "ot" címkére fog átadódni.
Itt egy "mov eax
ecx" utasítás található, ennek hatására most az
EAX regiszter tartalma 0x14 lesz. Ha nem hívtuk volna meg a "ketto" címkét, akkor mi lett volna most EAX tartalma?
A választ nem tudjuk megadni, ECX értéke ez esetben határozatlan - vagyis
ismeretlen - pont annyi, mint amennyit valamelyik korábbi program beállított
neki.
Mikor állítunk magunk elé feltételeket? Ha esik az eső, viszek
esernyőt, ha nem, hát nem. Láthatóan ez egy feltétel, arra használjuk, hogy
eldöntsük, mi az optimális viselkedésforma.
cmp byte ptr [idojaras], ESO
je kell
mov eax, 0
jmp tovabb
kell:
mov eax, ESERNYO
tovabb:
Tegyük fel, hogy ez egy olyan program részlete, amelyben az "idojaras" címkénél tárolt érték a következő lehet:
NAP, ESO, HO, SZEL. Korábban megismerkedtünk a * operátorral, ami arra
használatos, hogy egy címkénél található értéket kivegyen (indirekció).
A "cmp" utasítás két értéket hasonlít össze
úgy, hogy az első értékből kivonja a második értéket, majd beállítja a flag regiszter bizonyos bitjeit. Állítja a ZERO flag-et, akkor lesz IGAZ értékű, ha a kivonás eredménye
nullát ad, vagyis a két érték egyezett. Állítja a CARRY (átvitel) flag értékét is, akkor lesz IGAZ, ha az első érték kisebb, mint
a második, vagyis a kivonásnak negatív eredménye lesz: kell még értékátvitel
(CARRY), hogy nulla legyen.
Tegyük fel, hogy az "idojaras"
címkénél található érték ESO volt, vagyis esik az eső. Ebben az esetben az
összehasonlítás eredménye: "egyenlő". A "je"
utasítás használható ilyenkor, "JUMP IF EQUAL" (ugrás, ha egyenlő). A
feltételes ugróutasításokkal vigyázni kell, ugyanis maximum 127 bájtot
ugorhatunk velük előre-hátra! Az az oka ennek a
megkötésnek, hogy a feltételes ugróutasítások egy 8 bites relatív címet kapnak,
ami a jelenlegi EIP és a megadott cím különbsége. Nyolc biten 2^8, vagyis 256 féle értéket lehet ábrázolni. A processzor megengedi az
előre és hátra ugrást, vagyis ezen a 8 biten negatív (előjeles egész) számokat
kell ábrázolni. A nyolc bites előjeles egészt előjeles karakter adattípusnak (signed
char) hívjuk, értéktartománya [-128,+127].
Mivel a hasonlítás eredménye "egyenlő" volt, a vezérlés a
"kell" címkére adódik át. Itt az EAX regiszter egy
"ESERNYO" értéket kap. Vagyis viszünk esernyőt. Ezek után a vezérlés
a "tovabb" címkére kerül.
Amennyiben az időjárás nem esős, nem ugrunk a "kell" címkére,
és EAX regiszter nulla értéket kap, majd elugrunk a "tovabb"
címkére.
Hogyan lehetséges, hogy ábrázolni tudjuk az ESO, NAP, HO, SZEL, ESERNYO
fogalmakat? A gép számokat ismer csupán, vagyis ezek is számértékként vannak
meghatározva. Azokat az értékeket, amelyeket elnevezünk, vagyis később már egy
egyezményes névvel tudunk elérni, konstans literáloknak nevezzük.
A következőkben egy olyan programot fogunk megvizsgálni, amely egy
négyszögről eldönti, hogy "a" oldala nagyobb-e, mint 2, és
"b" oldala kisebb-e, mint négy. Ha igaz az első feltétel, akkor EAX
értéke igaz lesz (nem hamis, vagyis nem nulla), ha a második feltétel igaz,
akkor EBX értéke igaz lesz. Ha tehát mindkét feltétel igaz, akkor EAX=EBX=1, ha
pedig egyik sem, akkor EAX=EBX=0.
a_oldal:
cmp byte ptr [a], 2
ja atobb
mov eax, 0
jmp b_oldal
atobb:
mov eax, 1
b_oldal:
cmp byte ptr [b], 4
jl bkevesebb
mov ebx, 0
jmp vege
bkevesebb:
mov ebx, 1
vege:
A feltételes
ugrásoknál meg kell különböztetni előjeles és előjel nélküli (unsigned) feltételt. Előjeles feltételek a "nagyobb, mint"
(Greater than),
"nagyobb, vagy egyenlő" (Greater or Equal), "alacsonyabb,
mint" (Below than),
"alacsonyabb, vagy egyenlő" (Below or Equal). Az Assembly nyelvekben
ezek tagadása is használható: például "nem nagyobb, mint" (Not Greater than)
ugyanazt jelenti, mint az "alacsonyabb, vagy egyenlő".
Példánk az előjel nélküli összehasonlításokat mutatja be, "ugrás,
ha több, mint kettő" (Jump
if Above than) és "ugrás, ha kevesebb, mint négy" (Jump if Less than).
Az Assembly alacsony szintű nyelvcsalád. Magas szintű nyelvekben,
melyekben "emberi jellegű szavakkal és kifejezésekkel" tudjuk
megfogalmazni a programban végrehajtandó tevékenységeket, a feltételek
kiértékelését az "if" utasítás végzi.
Például C nyelven az első program így hangzik:
if( idojaras == ESO )
{ eax = ESERNYO; }
else
{ eax = 0; }
A második példaprogram ennek megfelelően így
néz ki C nyelven:
if( a > 2 )
{ eax = 1; }
else
{ eax = 0; }
if( b < 4 )
{ ebx = 1; }
else
{ ebx = 0; }
Előfordul, hogy ismerünk több esetet, melyből egy bizonyos időpontban
egyszerre csak egy áll elő. Az esetek kiválasztására a magas szintű nyelvekben
a "case" (eset) kulcsszó használatos:
switch( idojaras ){
case ESO : { eax = ESERNYO; }
case HO : { eax = KABAT+SAL+SAPKA+KESZTYU; }
case SZEL : { eax = KABAT; }
case NAP : { eax = NAPSZEMUVEG; }
default : { eax =
0; } // eldonthetetlen idojaras
:))
// inkabb nem veszunk fel semmit
}
Készítsük el ezt a programot is Assembly nyelven.
cmp byte ptr [idojaras], ESO
je esernyo
cmp byte ptr [idojaras], HO
je telicucc
cmp byte ptr [idojaras], SZEL
je fujaszel
cmp byte ptr [idojaras], NAP
je sutanap
default:
mov eax, 0
jmp vege
esernyo:
mov eax, ESERNYO
jmp vege
telicucc:
mov eax, KABAT+SAL+SAPKA+KESZTYU
jmp vege
fujaszel:
mov eax, KABAT
jmp vege
sutanap:
mov eax, NAPSZEMUVEG
vege:
Akkor szoktunk ciklusokat szervezni, ha van egy tevékenységünk és az sokszor kell elvégezni. Beszélünk determinisztikus és nem
determinisztikus ciklusokról. A determinisztikus ciklus esetében tudjuk, hogy
egy adott tevékenységet hányszor kell elvégezni. Magas szintű nyelvekben ezt
"for" ciklusnak hívják. Például egy
memóriaterület első három bájtját fel kell tölteni 0xAA értékkel. Az Intel Assembly-ben ez úgy
kényelmes, hogy megadjuk a feltöltendő memóriacím szegmenscímét és az azon
belüli indexet, majd egy tároló utasítást ciklikusan végrehajtunk.
mov al, 0AAh ;
feltöltendő érték AL-be
mov ecx, 0x3 ; ciklusszámláló értéke ECX-be
mov edi, Offset terulet ; célterület címe a címregiszterbe
cld ;
irány flag törlése
push cs
pop es ;
célterület szegmenscíme ES regiszterbe
rep ; repeat
stosb ; Strore String
by Byte
; itt még
lehetnek utasítások
terulet:
db 0
db 0
db 0
Néhány trükk látható: mivel ez egy rövid program, az ES (extra
szegmens) regiszter értéke a CS regiszter értékét kapja, ugyanabban a
szegmensben van a célterület, mint a programunk kódja. A regiszterértékek
másolását most a vermen keresztül a Push-Pop utasításpárral oldottuk meg. A "rep"
(REPEAT) ismétlő utasítással lehet egy műveletet ismételni, pontosan annyiszor,
amennyit korábban az ECX regiszterben beállítottunk. A "stosb" utasítás ES:EDI címre
beírja AL regiszter értékét és az irány (Direction) flag értékének megfelelően növeli, vagy csökkenti EDI-t. Ha a Direction flag értéke igaz, akkor az irány fordított, vagyis EDI
értékét csökkenti, ha hamis, akkor az irány normális, növekvő, az EDI értéke
nő. Esetünkben az irány flag-et törölni, nullázni
kell ("cld" utasítás: CLear
Direction flag). A "stosb" utasítással és párjaival később még találkozni
fogunk.
Tudjuk, hogy a "rep" segítségével
egy utasítást tudunk ciklikusan végezni. Hogy oldjuk akkor meg azt, hogy
mégiscsak többet ismételjünk egyszerre, vagyis a ciklusmag egy utasításblokkból
álljon? Megoldás lehet persze az is, hogy egy külső részben írunk egy sok
utasításból álló rutint, s "ret"-tel
(visszatérés) fejezzük be és ezt a rutint hívjuk egy
utasítással, a "call"-lal ciklikusan. Ezt
azonban már függvényhívásnak nevezik, és annak ciklikus formáját kultúrkörökben
nem tartják teljesen etikusnak - ha csak nem direkt ez a cél...
A megoldás a "loop" utasítás: ezt a
ciklusmag végére kell tenni és annyiszor ugrunk el
vele a ciklus elejére, amennyit az ECX regiszterben beállítottunk.
mov al, 0AAh
mov ecx, 3
mov edi, Offset terulet
cld
push cs
pop es
feltoltes:
stosb
loop feltoltes
; itt még
lehetnek utasítások
terulet:
db 0
db 0
db 0
Következzenek hát a nem determinisztikus ciklusok. Ezeknek két fajtája
van: az elöltesztelő (while)
és a hátultesztelő (do while). Az elöltesztelő ciklusnál
a ciklusfejben van egy teszt, ha ez egy feltételt igaznak talál, akkor
végrehajtódik a ciklusmag és visszatérünk a ciklusfejhez. Ezért hívjuk ezt while (amíg) ciklusnak. A hátultesztelő
ciklusnál először lefut a ciklusmag, majd a cikluslábban egy teszt eldönti,
hogy szükséges-e a ciklusmagot újra végrehajtani. Ez más néven a do while (csináld,
amíg) ciklus.
Nos, íme, egy while ciklus:
Ciklusfej:
cmp al, 0
je vege
ciklusmag:
dec al
jmp ciklusfej
vege:
Ha ezt C-nyelven
írjuk le, az így néz ki:
while( al != 0x0 ) { --al; }
Amíg "al" nem egyenlő 0x0, addig dekrementáld (eggyel csökkentsd) "al"
értékét. Ha a program indulásakor "al"
értéke nulla volt, akkor be sem lépünk a ciklusba. Ellentétben áll ezzel a
következő, do while példa:
ciklusmag:
dec al
cikluslab:
cmp al, 0
je vege
jmp ciklusmag
vege:
vagyis
do { --al; } while(
al != 0x0 );
Először dekrementáljuk "al"
értékét, végül megnézzük, hogy nulla-e. Ha annyi, kilépünk, ha nem, akkor visszaugrunk
a ciklusmagra.
A következő részben a tömbök kezelésével fogunk megismerkedni.
Németh Róbert - nrobcsi@freemail.hu