Memóriatérkép BIOS (alaplapi) BIOS bővítések (csatolókártyán elhelyezkedő)
F000 : FFFF Felső memória C000 : 0000 B000 : FFFF A000 : 0000 9000 : FFFF
246 Kbyte (Videó RAM) képernyő megjelenítéséhez szükséges
640 Kbyte (konvencionális (hagyományos) memória) /az első 10 szegmens/ 1000 : 0000 0000 : FFFF egy szegmens maximális mérete 64 Kbyte 0000 : 0001 0000 : 0000 Maximum F000 lehet a szegmenscím, mert: F000 * 16 = F0000 + FFFF = FFFFF ami már 20 bites cím
Szegmens regiszterek (16 bitesek) CS(Code Segment) utasítások címzéséhez SS(Stack Segment) verem címzéséhez DS(Data Segment) (automatikus) adat terület címzéséhez ES(Extra Segment) másodlagos adat terület címzéséhez
Vezérlő regiszterek (16 bitesek) IP(Instruction Pointer) az éppen végrehajtandó utasítás logikai címét tartalmazza a CS által mutatott szegmensben SP (Stack Pointer) a stack-be (verembe) utolsónak beírt elem logika címét tartalmazza az SS által mutatott szegmensben STATUS (SR vagy FLAGS) a processzor állapotát jelző regiszter BP(Base Pointer) a stack indexelt címzéséhez használatos SI(Source Index) a kiindulási (forrás) adat terület indexelt címzéséhez használatos
DI(Destination Index) a cél adat terület indexelt címzéséhez használatos Szegmens (6.8. ábra) A programozó számára látható logikai egység. Minden szegmens címtartománya 0-tól valamilyen maximumig terjed. A szegmens tényleges mérete ennél kisebb lehet. A program számára a címtartomány két dimenziós: (szegmens, offset). Általában egy szegmensben csak egyféle dolgok vannak: vagy kód vagy konstans vagy … Különböző tárvédelmi lehetőségek: kód: csak végrehajtható, nem írható, nem olvasható, konstans: csak olvasható Szegmentált memóriacímzés: Operatív memória nélkül nem tudunk programokat futtatni egy Neumann-architektúrájú (tárolt programú) számítógépen - valamilyen módon tehát a memóriát el kell érnünk, hogy oda programokat töltsünk be. A memóriához való hozzáférés, mint tevékenység két fázisra osztható: 1; címzés: ekkor meghatározzuk, milyen memóriacímre (cellaként, rekeszként lehet elképzelni) vagyunk kíváncsiak 2; operáció: az adott címre beírunk vagy onnan kiolvasunk egy adatot. Ahhoz, hogy beírjunk egy értéket, tudnunk kell, mekkora helyet foglal a memóriában. Számértékekben kell gondolkodni, mást ugyanis a digitális gépek nem ismernek... A számokat az assembly nyelveken programozók szeretik hexadecimális formában ábrázolni - ez ugyanis kifejezőbb, mint a decimális forma, később látjuk is, miért lesz ez fontos. A hexadecimális számok mögé egy 'h' utótagot írok, vagyis a 12h egy hexa szám, értéke decimálisan tizennyolc. Vegyünk egy példát a memóriában való értékek tárolására. Tegyük fel, hogy három számot szeretnénk eltárolni. Az egyik szám értéke a [0,100] intervallumba tartozik, a második a [-200,+200] tartományba, a harmadik pedig a [-100.000,+100.000] tartományba. Az első intervallumot 7 biten tudjuk ábrázolni, mert 7 biten 128 féle adat fér el, ebbe belefér a [0,100] intervallum. Mi azonban csak kerek címekre, byte határokra írhatunk. Ezen intervallumon tárolt számérték tehát 8 bit helyet foglal a memóriában. Tegyük fel, hogy a hely memóriacíme "0000h". A számolás szerint a következő szám már a "0001h" memóriahelytől fér csak el. Vegyük a következő számot. Értéke a [-200,+200] tartományba eshet, vagyis 401 féle értéket vehet fel. A 401, mint szám tárolásához 9 bitre lesz szükség, mert 2^8 (ejtsd: 2 ad 8, vagyis kettő a nyolcadikon) 256, ez kicsi, 2^9 pedig 512, ez már nagyobb, mint 401. Tudunk 9 bitnyi helyet kicímezni? Nem, 8 bitenként tudunk címezni, vagy annak többszörösén. A "0001h"-s cím tehát már egy 16 bites számra fog mutatni. Ez kétszer 8 bit, vagyis 2 bájt, 0001h + 2 = 0003h, tehát ez lesz a következő szám címe.
A harmadik szám értéke a [-100.000,+100.000] tartományba fog esni. Ez összesen 200.001 lehetőség, ez a szám még 16 biten sem fér el: 2^16 = 65536. Ez kevés. Ennek kétszerese 131072, ez 2^17, ez is kevés, ennek kétszerese kell, 262144, vagyis 2^18. Ez elférhetne a 8 bit többszöröse szabály alapján 16+8=24 biten. A szám ugyan elfér, de a processzor csak háromféle számot tud kezelni: 8 bitest (byte), 16 bitest (short integer) és 32
bitest (integer). Ezek alapján már tudjuk, hogy egy 32 bites memóriahelyre lesz szükség. Ez 4 bájt, a következő szabad terület memóriacíme tehát 0003h + 4 = 0007h. Említettem, hogy valós módban mindössze 1MB memória érhető el. Tudni kell azonban azt is, hogy a processzor valós módjában a memóriacímzés 16 bites. Itt jön a bökkenő. 16 bittel csak 2^16, vagyis 65536 féle cím különböztethető meg. Ez sokkal kevesebb, mint egy megabájt. Erre vezették be a szegmenseket. A szegmensek egymás után 16 bájtonként következnek, mert 65536 = 64kB memóriaterület fedhető le, és 1MB osztva 64kB = 16. Vegyünk megint egy példát - három felvonásban. Az elsőben el szeretnénk érni a valós memória 18. bájtját, a másik esetben a 48. bájtot, a harmadikban pedig a 123.789-es memóriacímet. Tudjuk, hogy a szegmensek 16 bájtonként következnek. A 18-as memóriacímhez tartozó szegmenscím meghatározása a következőképpen történhet: 18-at elosztjuk 16-tal és kapunk 1-et. Egy lesz a szegmenscím: az első szegmensben van a 18-as cím. Mi az osztás maradéka? Kettő, ez lesz az index. Szegmenscím=1, Index=2. Ezt hexadecimálisan szokták jelölni és kettősponttal elválasztva, vagyis "0001h:0002h". Második esetben a 48. bájt kell, osszuk el 48-at 16-tal, 3-at kapunk és nulla a maradék, vagyis a cím 0003h:0000h lesz. Harmadik esetben 123.789 / 16 = 7.736, vagyis 1E38h, az osztás maradéka 13, vagyis az index 000Dh - tehát a cím 1E38h:000Dh. Most pedig jön a szépség - kicsit másképpen - csak tisztán matematikai alapon. Az index 16 bites, értéktartománya 65536, tehát egy szegmens mérete 64kB. Emiatt a 18. bájtot a 0. szegmens 18 bájtjaként is el lehet érni, a 48-as bájtot pedig a 0. szegmens 48. bájtjaként. Hogyan lehetne akkor a 123.789-ik bájtot is így, másképpen elérni? Tudjuk, hogy 1 < (123.789 / 65.536) < 2, ez azt jelenti, hogy mivel 65.536/16 = 4096, vagyis 4096 féle lehetséges szegmenscím van egy 64kB-os szegmens (memóriablokk) területén, a 4096-edik szegmenscímtől már a következő 64kB is elérhető. 123.789 - 65.536 = 58.253, vagyis 4096 = 1000, 58.253 = E38Dh, tehát a 1000h:E38Dh címen ugyanaz az adat van, mint a korábban kiszámolt 1E38h:000Dh címen. Javaslom a tisztelt olvasónak, fusson újra végig ezen a gondolatmeneten, mert ez a valós módú memóriacímzés módszere és alapvető fontosságú. Aztán nézze jól meg ezt a két párost (1000h:E38Dh) és (1E38h:000Dh), vajon miért is ugyanaz az értékük? Látható, hogy (1E00h:038Dh) és (1E30h:008Dh), de természetesen még (1E31h:007Dh) és (1E10h:028Dh) is ugyanarra a címre mutat. Aki ezt belátja, az érti a szegmentált memóriacímzést.
Regiszterek kezelése: Ismert, hogy a regiszterek a processzor futási környezetét alkotják. Hogyan tudjuk feltölteni a regisztereket értékekkel? Egy ilyen feltöltést (adat-)mozgatásnak hívunk, és a MOV utasítást használjuk rá. Először meg kell nevezni az adott regisztert és megmondani, hogy milyen értékkel akarjuk feltölteni. Említettem négy általános célú regisztert. Ezek a regiszterek az EAX, EBX, ECX és az EDX. A jelölés értelme a következő: hajdanában-danában az Intel regiszterek 16 bitesek voltak, de 80386 óta 32 bitesek, ez egy kiterjesztés (Extension), ezt jelzi az E betű a regiszterek előtt. Az általános célt a matematikában is használt X, mint változó jelöli. Nem teljesen igaz azonban, hogy mind a négy regiszter általános célú. Vegyük őket sorra.
EAX: A valóban általános célú regiszter, a legtöbb műveletet ezzel szokás végezni. EBX: A bázisregiszter, ezt címzéseknél fogjuk használni. ECX: A ciklusregiszter, vagyis ciklusváltozóként használt regiszter. EDX: Data, vagyis adatátvitelre (pl. osztás maradéka) használt regiszter. A régi programokkal és címzésekkel való kompatibilitás miatt megtartották a 16 bites elnevezéseket is, vagyis a valóságban (a hardware-ben) ugyan nem létezik, de nyelvi elemként megmaradt az AX, BX, CX, DX regiszter is - és a későbbiekben ez igaz lesz az indexregiszterekre is. Nézzünk végre példákat. Első pédánkban az EAX regiszterbe 1237ABCEh értéket töltünk, majd ezt átmozgatjuk az EDX regiszterbe, végül ennek alsó 16 bitjét az ECX regiszterbe. ; amit pontosvessző jel után írunk, az megjegyzés mov eax, 1237ABCEh mov edx, eax mov cx, dx
; mozgasd EAX-be a 1237ABCEh hexa értéket ; mozgasd EDX-be EAX értékét
; mozgasd CX-be DX értékét
A lényeg, hogy EnX és nX ugyanazt a regisztert jelöli, csak EnX esetében mind a 32 bitjét el tudjuk érni, nX esetében pedig csak az alsó 16 bitet (mert ugye a felső az Extension). Lehetőség van az alsó, 16 bites rész alsó és felső bájtjának elérésére is, ezt nL-nek (low, alacsony) és nH-nak (high, magas) jelölik. Például: EBX(32), BX(16), BL(8), BH(8). mov edx, 13550000h mov dl, 0E3h mov dh, 0D7h Remélem, belátható ezek után, hogy EDX értéke 1355D7E3h lesz. Vegyünk észre még valamit - ez már ugyan csupán a szintaktikára vonatkozik - ha a hexadecimális formájú szám betű karakterrel kezdődik, akkor előnullázzuk (például 0E3h). Nos, nézzünk egy kis címzést - immár a gyakorlatban. mov esi, Offset adatok ; "adatok" címke eltolási címe ESI-be mov al, ds:[esi]
; DS:ESI címen található érték AL regiszterbe
mov bl, Byte Ptr [adatok] ; Byte jellegű adat BL regiszterbe add esi, 2
; indexregiszer értékének növelése 2-vel
; ezáltal az indexmutató a memóriában 2 bájttal távolabbi címre fog mutatni mov ecx, ds:[esi] mov dx, Word Ptr [adatok + 4] add esi, 4 mov ebx, ds:[esi]
; egyéb programrészlet adatok: db 01h db 02h db 03h db 04h dw 0AACCh dd 22AACC88h Használtunk végre egy szegmensregisztert és egy indexregisztert is. Hat szegmensregiszterrel rendelkezik az i80386-os processzor, ezek a következők: CS, DS, ES, FS, GS, SS (gépi kód szerinti sorrendben ES, CS, SS, DS, FS, GS).
Kódszegmens (CS): az ezen regiszter által meghatározott szegmensben fut a felhasználói folyamat. Adatszegmens (DS): ez a folyamat alapértelmezett adatszegmensének címét tartalmazza. Rövid programoknál a kód és adatszegmens ugyanaz, hiszen felesleges lenne memóriát pazarolni. Az extraszegmens regiszter az ES, ugyanilyen célt szolgálnak az FS és GS regiszterek is, ezek általános céllal használható szegmensjelölők. A verem szegmens címét az SS (Stack Segment) regiszter tartalmazza, ide általában a programok paraméterei, hívási láncok címei, stb. kerülnek. Nézzük az indexregisztereket. Ezek meghatároznak egy memóriahelyet egy adott szegmenscímhez viszonyítva ezt hívjuk "offset"-nek, vagyis relatív címnek. Utóbbi példaprogramunkban az ESI (Extended Source Index, vagyis forrásindex) regiszter tartalma az "adatok" címke memóriabeli címe volt az adott szegmensen belül. A címke egy elnevezett memóriacím. Az ESI regisztert input (Source Index: forrás index) címzésére használják. Az EDI (D, mint Destination, vagyis cél) regisztert output (kimenet) célokra. Az EBP a bázismutató (Base Pointer), ami általános célokra használható címzésnél. Az ESP a veremmutató (Stack Pointer), ez jelzi, hogy milyen (hány bájt) mélységig jutottunk a program vermében. A Stack Pointer értéke a program futása során csökken, nem nő, mert a verem verem és nem cső... Láthatjuk, hogy a szegmens regiszterekhez tartoznak indexregiszterek, a DS-hez az ESI, az ES, FS és GS-hez az EDI, az SS-hez az ESP és például az EBP... Természetesen a kódszegmenshez is tartozik mutató, ez az EIP, vagyis utasításszámláló (Instruction Pointer), ez jelöli ki a kódszegmensben azt a címkét, ahol a program végrehajtása tart, vagyis EIP mindig a következő utasítás címét tartalmazza. Térjünk vissza a példaprogram elemzéséhez. Először tehát elhelyezi ESI-ben az "adatok" címke memóriacímét, majd AL regiszterbe mozgatja a DS:ESI által kijelölt memóriaterületen található értéket, vagyis 1-et. Azt a műveletet, amikor egy memóriacímről értéket olvasunk ki, vagy oda értéket írunk be, indirekciónak hívjuk. A tömbzárójelek (array brackets: [ és ] jelek) közé írt azonosító jelzi, hogy mivel indexelünk. Ha ez egy regiszter, akkor egyértelmű, hogy a regiszter által jelöljük ki a relatív memóriacímet. Ilyen zárójelben az indexregisztereken kívül az EBX regiszter is szerepelhet még. Amennyiben a tömbzárójelek közé egy elnevezett címke (ezt hívják magaszintű nyelvekben "változó"-nak) azonosítóját írjuk, akkor jelezni kell, hogy milyen típusú adatra mutatunk (PTR - pointer - kulcsszó). A lehetséges típusok:
BYTE - például AL, vagy CH esetén, amelyek 8 bites regiszterek WORD - 16 bites regiszetereknél, pl. AX, SI esetén DWORD - double word: duplaszó, vagyis 32 bites, pl. EBP regiszter esetén
Menjünk tovább a példában. Ha ESI értékéhez hozzáadunk kettőt ("add esi, 2"), akkor az "adatok" címke után 2 bájttal található memóiacímre fog mutatni ESI. Ott mi található? A db-vel jelölt helyek bájtos helyek, vagyis két bájttal arrébb a 3 érték található. Az ECX regiszter tartalma mi lészen? ECX tudjuk, 32 bites, vagyis ami az aktuális ESI címétől 32 bit, 4 bájt hosszon helyezkedik el, az egy számként bekerül ECX-be. Ez a szám az AACC0403h lesz. Hogy miért? A számok ábrázolása Intel processzoron LITTLE ENDIAN jellegű, vagyis legelöl van a legkisebb helyiérték. Legelöl áll 03, utána 04, majd pedig egy dw short int (16 bites egész) szám, ez bekerül a 32 bites regiszter tetejére, mert ez van leghátul: így épül fel ECX értéke AACC0403h-vá. A következő utasítással DX értéke is 0AACCh lesz, mivel a "mov dx, word ptr [adatok + 4]" az adatok címe után 4 bájttal található címről cseni el az adatot. Megint növeljük ESI értékét, most néggyel, majd az itt található értéket bemozgatjuk az EBX regiszterbe. Ezáltal az a 22AACC88h értékkel töltődik fel.
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.
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 mov ecx, 0x3
; feltöltendő érték AL-be ; 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.
Feltételek: 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őrehá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:
A CPU rendelkezésére álló memóriaterület olyan nagy, hogy egyetlen regiszter nem tudja végig címezni a teljes memóriát. Ezért egy memóriacím elôállításához mindig két regisztert használunk fel. Az egyiket szegmensregiszternek, a másikat
offszet regiszternek hívjuk. Az offszetregiszterek általá-ban a mutató- (BP, IP, SP), vagy az index- (SI, DI) regiszterek.
A tényleges memóriacím így a két regiszter segítségével áll elô úgy, hogy a szegmens regiszter értékének tizenhat-szorosához hozzáadódik az offszetregiszter értéke. Például ha a CS regiszter értéke 1A2Ch, az IP regiszter értéke pedig 2B17h, akkor a CS:IP regiszterpár értéke 1CDD7h. Az alábbi ábrán látható, hogy a 16-tal való szorzást egy hexadecimális jeggyel való balra léptetéssel helyettesíthetjük.
Fizikai cím = SZEGMENS*16 + OFFSET
1 1
A
2
C
2
B
1
7
C
D
D
7
Legyen például a szegmenscím 0040h, az offset 0002h. Ekkor a cím így irható fel : 0040:0002. Ebben az esetben a szegmenscímet 16-tal szorozva 0400h értéket kapunk. Ehhez hozzáadva a 0002h értéket 0402h fizikai cím adódik. A memóriacímek alkotásához a lehetséges offszet-regisztereknek van egy alapértelmezett szegmensregiszter párja. Az alábbi táblázat mutatja, hogy mely offszetregiszterhez mely szegmensregiszter tartozik alapértelme-zésben. SI, DI:
DS
IP:
CS
BP, SP:
SS
A CPU SS regisztere kijelöli a memória egy 64K-s darabját (szegmensét) úgy, hogy az SS éppen aktuális értéke mellett az SP értéket változtatva milyen memóriacímeket érhetünk el. Ha pl. az SS értéke 1234h, akkor a verem szegmens (stack segment) a memória 1234h:0000h-tól 1234:FFFFh-ig terjedô része. A memóriának ezt a szegmensét veremnek nevezzük. A verembe adatokat lehet berakni, és onnan kivenni úgy, hogy mindig a legutoljára berakott értéket vesszük ki. A verem szegmensen belül az SP regiszter mindig az utoljára berakott adatra mutat. Ha újabbat akarunk berakni, akkor elôször bekerülô adat méretével csökkentjük az SP értékét, majd az SS:SP címre beírjuk az adatot. Ezt végzi a PUSH utasítás. Ha viszont adatot akarunk kivenni a verembôl, akkor elôször kivesszük az SS:SP által mutatott címrôl az adatot, majd a megfelelô mennyiséggel megnöveljük az SP értékét. Ezt végzi a POP utasítás. A verem tehát magasabb címtôl kezdve telitôdik, és mindig páros darab byte kerülhet bele és belôle ki.
Például tegyük fel, hogy az AX, BX regiszterek tartalma rendre 0001h, 0002h, az SP értéke pedig FFF0h, és az alábbi utasításokat hajtjuk végre: PUSH AX PUSH BX POP AX POP BX
Ekkor az alábbi módon alakulnak a memória- és regiszter értékek (xx: tetszôleges érték): Kezdetben: A PUSH AX után (1): SS:FFECh: xx SS:FFECh: xx SS:FFEEh: xx SP-> SS:FFEEh: 0001h SP-> SS:FFF0h: xx SS:FFF0h: xx
A PUSH BX után (2): SP-> SS:FFECh: 0002h SS:FFEEh: 0001h SS:FFF0h: xx
A POP AX után (3): SS:FFECh: 0002h SP-> SS:FFEEh: 0001h SS:FFF0h: xx és AX tartalma 0002h. A POP BX után (4): SS:FFECh: 0002h SS:FFEEh: 0001h SP-> SS:FFF0h: xx és BX tartalma 0001h.
A verem működése késôbb még fontos szerepet fog játszani. A CPU a következôképpen működik. 1. A CS:IP regiszterpár által mutatott helyrôl kiolvassa az éppen végrehajtandó utasítás kódját. 2. A kódból meghatározza, hogy hány byte-nyi paraméter tartozik még az utasításhoz, ennyi byte-ot kiolvas a kód utáni területrôl. Ezek lesznek az utasítás operandusai. 3. Végrehajtja a kód által meghatározott utasítást a beolvasott operandusokkal. 4. Növeli a CS:IP regiszter értékét annyival, amennyi byte volt a teljes utasítás operandusokkal együtt. Repeat Kód:=Kiolvas(CS:IP) For i:=1 To Paraméterek_Száma[Kod] Do Operandusok[i]:=Kiolvas(CS:IP+i) Végrehajt(Kód,Operandusok) (CS:IP) := (CS:IP)+Hossz[Kód] Until Áramszünet Megjegyzés: A fenti algoritmus csak vázlat, ugyanis vannak olyan utasítások, amelyek kódja nem egy, hanem két byte-os. Ezen kívül a vezérlésátadó utasítások működése is "sántít".
típus Boolean Byte Shortint Char Word Integer Longint Real String[n] String
hány bájton tárolódik 1 1 1 1 2 2 4 6 n+1 256
Kövessük végig a leírtakat egy példán. Tekintsük az alábbi unit-ból és főprogramból álló Pascal programot, és a memóriabeli elhelyezkedését illusztráló ábrát!
Unit Seged; Interface Var B1:Byte; Procedure Elj3; Implementation Procedure Elj3; Var B2:Byte; Begin {...} End; {-----------------} BEGIN B1:=5; END.
Program FoPrg; Uses Seged; Var B3:Byte; {-----------------} Procedure Elj1; Begin {...} End; {-----------------} Procedure Elj2; Far; Begin {...} End; {-----------------} BEGIN Elj1; Elj3; B3:=7; END.
A felhasznált unit és a főprogram
A memória vázlata Amint az a memória vázlatából is látszik, a főprogram moduljában definiált két eljárás és maga a főprogram egy közös kódszegmensbe kerül, a unit eljárása és annak a főprogramja pedig még egy kódszegmensbe. Ha lennének még további modulok is, akkor azok is újabb és újabb kódszegmensekbe kerülnének. Jól látszik az ábrából, hogy a főprogram a unit eljárása más szegmensben van, ezért csak távoli (far) eljáráshívással hívható.