Assembly programozás - 2. rész

 

4. Ugrás és hívás

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.

 

5. Feltételek kiértékelése

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:

 

6. Ciklusok

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.

 

Előzetes

A következő részben a tömbök kezelésével fogunk megismerkedni.

Németh Róbert - nrobcsi@freemail.hu