Content extract
Assembly programozás szerkesztette: Iványi Péter September 27, 2010 2 Tartalomjegyzék 1 Bevezetés 11 1.1 RISC és CISC processzor architektúrák . 11 1.2 Assembly elsőre . 12 1.3 Miért tanuljunk assembly nyelvet? . 12 1.4 Mikor ne használjunk assembly nyelvet? . 13 1.41 A magas szintű programozási nyelvek előnye . 14 1.42 Az assembly hátrányai . 14 1.5 Mielőtt elkezdenénk assembly-ben programozni . 14 1.6 Szintakszis . 15 1.7 Assemblerek . 15 1.71 MASM . 15 1.72 GAS . 15 1.73 TASM . 15 1.74 NASM . 15 1.75
Melyik assembler? . 15 1.8 Összefoglalás . 16 1.9 Ellenőrző kérdések . 16 2 A számı́tógép felépı́tése 2.1 17 A processzor . 17 2.11 Végrehajtási ciklus . 18 2.12 A rendszer óra . 19 Cı́mzési architektúra . 19 2.21 Három cı́mes architektúra . 19 2.22 Két cı́mes architektúra . 20 2.23 Egy cı́mes architektúra . 20 2.24 Zéró cı́m architektúra . 20 2.25 Load/Store architektúra . 20 2.3 Regiszterek . 21 2.4 Végrehajtási sorrend . 21
2.41 Branching . 21 Memória . 22 2.51 22 2.2 2.5 Memória műveletek . 3 2.6 2.52 Olvasási ciklus . 22 2.53 Olvasási ciklus . 23 2.54 Memória tı́pusok . 23 2.55 Byte sorozatok tárolása . 24 2.56 Adat “alignment” problema . 25 Input/Output . 26 I/O eszközök elérése . 27 2.7 2.61 Összefoglalás . 27 2.8 Ellenőrző kérdések . 27 3 A processzor 29 3.1 Általános regiszterek . 30 3.2 Szegmentált cı́mzés először . 30 3.3 Cı́mzési
módok . 31 3.31 Direkt cı́mzési mód . 33 3.32 Indirekt cı́mzési mód . 34 3.4 Státusz regiszter . 35 3.5 Ellenőrző kérdések . 36 4 NASM assembler 37 4.1 Egy forrás file szerkezete . 37 4.2 Pszeudo utası́tások . 37 4.3 4.21 DB és társai . 38 4.22 RESB és társai . 38 4.23 Konstansok . 39 4.24 TIMES pszeudo utası́tás . 39 SEG kulcsszó . 39 További hasznosı́tási területek . 40 4.4 4.31 WRT kulcsszó . 40 4.5 Parancssori opciók .
40 4.6 Hibaüzenetek . 40 5 DEBUG program 41 5.1 Jelölések . 41 5.2 A DEBUG indı́tása . 41 5.3 A DEBUG parancsai . 41 5.4 Példák . 44 5.41 1. Példa 44 5.42 2. Példa 44 5.43 3. Példa 45 6 Első programok 47 6.1 Első program . 47 6.2 Egy karakter kinyomtatása . 48 4 6.3 Egy szöveg kinyomtatása . 49 6.4 Egy karakter beolvasása . 51 7 Assembly nyelv utası́tásai 7.1 7.2 7.3 7.4 53 Adatmozgató utası́tások . 54
7.11 MOV . 54 7.12 XCHG . 54 7.13 XLAT . 55 7.14 LDS . 55 7.15 LES . 55 7.16 LEA . 56 7.17 PUSH . 56 7.18 PUSHF . 56 7.19 PUSHA . 57 7.110 POP 57 7.111 POPF 58 7.112 POPA 58 7.113 LAHF 59 7.114 SAHF 59 Matematikai utası́tások . 60 7.21 INC . 60 7.22 DEC . 60 7.23 ADD .
60 7.24 ADC . 61 7.25 SUB . 61 7.26 SBB . 62 7.27 MUL . 62 7.28 IMUL . 63 7.29 DIV . 63 7.210 IDIV 64 7.211 NEG 64 7.212 CBW 65 7.213 CWD 65 Bitforgató és bitléptető utası́tások . 66 7.31 RCL . 66 7.32 RCR . 66 7.33 ROL . 67 7.34 ROR . 68 7.35 SAL, SHL . 68 7.36 SAR . 69 7.37 SHR .
69 Logikai utası́tások . 71 7.41 71 AND . 5 7.5 7.6 7.7 7.8 7.9 7.42 OR . 71 7.43 XOR . 71 7.44 NOT . 71 7.45 TEST . 71 7.46 CMP . 71 Vezérlésátadó utası́tások . 72 7.51 JMP . 72 7.52 Feltételes utası́tások . 72 7.53 JCXZ . 72 7.54 LOOP . 72 7.55 LOOPNZ . 72 7.56 LOOPZ . 73 7.57 CALL . 73 7.58 RET .
73 7.59 INT . 73 String kezelő utası́tások . 74 7.61 MOVSB, MOVSW . 74 7.62 CMPSB, CMPSW . 75 7.63 LODSB, LODSW . 75 7.64 STOSB, STOSW . 75 7.65 SCASB, SCASW . 75 7.66 REP . 76 7.67 REPZ . 77 7.68 REPNZ . 77 Processzor vezérlő utası́tások . 78 7.71 CLC . 78 7.72 STC . 78 7.73 CMC . 78 7.74 CLD 78 7.75 STD . 78 7.76 CLI . 78 7.77 STI .
79 Egyéb utası́tások . 80 7.81 NOP . 80 7.82 IN . 80 7.83 OUT . 80 Ellenőrző kérdések . 81 . 8 Assembly programokról 83 8.1 Programozási módszer . 83 8.2 Megszakı́tások . 84 8.21 Hardware-es megszakı́tások . 84 8.22 Megszakı́tások 8086-os processzorokon . 85 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 86 87 87 88 88 88 88 88 89 9 Példa programok
9.1 Egy byte bináris kinyomtatása 9.2 Egy hexadecimális szám kinyomtatása 9.3 Egy byte hexadecimális kinyomtatása 9.4 Egy decimális számjegy ellenőrzött beolvasása és kinyomtatása 9.5 Egy karakter beolvasása és módosı́tása 9.6 Öt karakter bolvasása és kinyomtatása fordı́tott sorrendben 9.7 Két egyjegyű szám összeadása 9.8 Egy karakter n-szeri kinyomtatása 9.9 Téglalap kinyomtatása 9.10 Sakktábla nyomtatása 9.11 ASCII tábla kinyomtatása 9.12 Szám kiı́rása decimális formában 9.13 Olvasás a memóriából 9.14 Közvetlen videó memóriába ı́rás 9.15 Szöveg beolvasása 9.16 Beolvasott szövegben karakterek számlálása 9.17 Beolvasott
szöveg nagy betűsre konvertálása 9.18 Feladatok . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 91 93 95 97 98 100 101 104 105 108 111 112 114 115 116 117 118 119 . . . . . . . . . . . . 121 121 121 123 123 123 124 126 126 128 128 130 135 8.3 8.4 8.5 8.6 8.7 8.8 8.23 INT 21h megszakı́tás 8.24 Kivételek Kitérő Linux-ra . COM programok . 8.41 Program Segment Prefix EXE programok . XOR használata . Assembly integer aritmetika . 8.71 BCD aritmetika Ellenőrző kérdések . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Függvények 10.1 A verem adatszerkezet 10.11 A verem implementációja 10.12 Verem műveletek 10.2 A verem használata 10.21 Értékek időleges tárolás 10.3 Függvények definı́ciója 10.31 Egymásba ágyazott függvényhı́vások 10.4 Paraméter átadás függvényeknek 10.41 Paraméter átadás regiszteren keresztül 10.42 Paraméter átadás memórián keresztül 10.43 Paraméter átadás vermen keresztül 10.44 Érték és cı́m szerinti paraméter átadás 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.5 10.6 10.7 10.8 10.9 10.45 Változó számú paraméter átadása függvénynek Lokális változók függvényekben . 10.51 ENTER és LEAVE utası́tások Rekurzı́v függvények . Hatékonyság . Ellenőrző kérdések . Feladatok . 11 Makrók 11.1 Egy soros makrók 11.2 Több soros makrók 11.21 Cı́mkék makrókban 11.22 “Greedy” makró paraméterek 11.3 Makrók és
függvények még egyszer 11.4 Makrók gyűjtemények 11.5 Ellenőrző kérdések . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 String műveletek 12.1 String utası́tások 12.11 String másolás 12.12 Stringek összehasonlı́tása 12.13 Keresés stringek-ben 12.14 LODSB és STOSB utası́tások használata 12.2 String utası́tások előnyei és hátrányai 12.3 Ellenőrző kérdések . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136 141 142 143 144 144 148 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149 149 151 152 153 154 154 157 . . . . . . . 159 160 160 163 165 166 166 168 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Példák függvényekre és szöveg kezelésre 169 13.1 Szöveg hosszának megállapı́tása 169 14 C és assembly programok kapcsolata 171 14.1 Függvény hı́vási konvenciók 171 14.11 16 bites mód 171 14.12 32 bites mód 171 15 Optimalizálás 15.1 Optimalizálás sebességre 15.11 Sorrenden kı́vüli
végrehajtás 15.12 Utası́tás betöltés és dekódolás 15.13 Utası́tás késleltetés és átbocsátási képesség 15.14 Függőségi lánc megtörése 15.15 Ugrások és függvény hı́vások 15.2 Optimalizálás méretre 15.3 Memória hozzáférés optimalizálása 15.4 Ciklusok optimalizálása 15.5 Vector programozás 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 173 173 174 174 174 174 174 174 174 174 15.6 Problémás utası́tások 174 16
Optimalizált példák 175 16.1 ASCII tábla nyomtatása rövidebben 175 17 Megjegyzések 177 17.1 Szokásos hibák 177 A ASCII táblázat 179 B Felhasznált irodalom 181 Példa programok listája . 182 Tárgymutató . 183 9 10 1. Fejezet Bevezetés Ez a jegyzet egy összeállı́tásnak, szerkesztett jegyzetnek indult inkább, mint egy önálló könyv. A jegyzet anyagát igyekeztem úgy összeszedni, hogy az egy egységes egészt alkosson. A jegyzet célja az assembly nyelv megismertetése a hallgatókkal. Ugyanakkor az assembly nyelv nagyon erősen kötődik a processzor architektúrához, ı́gy az assembly programozáshoz az architektúrával is meg kell ismerkedni egy kicsit. A jegyzet kifejezetten az Intel x86-os processzorának alacsony szintű programozásával foglalkozik, ezek
közül is a 8086-os processzorral. Ez az egyik legkorábbi Intel processzor és furcsának tűnhet a választás, hogy 2010 környékén is erről a processzorról beszélünk. A jegyzet ı́rása során fő célom az volt, hogy az alapokat minden hallgató meg tudja érteni és el tudja sajátı́tani. Ehhez egy viszonylag “egyszerű” processzorra volt szükség és főleg ezért választottam a 8086-os processzort alapul. A másik indok, hogy a szimulátorok és virtuális gépek biztos támogatják ezt a processzort és ı́gy bármilyen környezetben lehet az assembly programozást gyakorolni. Így a jegyzet főleg kezdőknek szól, de azt remélem, hogy a gyakorlottabb hallgatók is profitálnak a jegyzet elolvasásából. A jegyzet természetesen igyekszik foglalkozni az újabb utası́tásokkal és módszerekkel, amiket például a Pentium processzorokra fejlesztettek. 1.1 RISC és CISC processzor architektúrák
A processzor architektúrák két nagy csoportba sorolhatók: CISC (Complex Instruction Set Computers)1 , RISC (Reduced Instruction Set Computers)2 . A domináns processzor architektúra a piacon a Pentium processzor, ami a CISC családhoz tartozik, de ugyanakkor a jelenlegi trendek szerint egyre inkább a RISC architektúra kerül előtérbe. A RISC processzorok közé tartoznak a MIPS, SPARC, PowerPC és ARM processzorok. A 64-bites Itanium is RISC alapú processzor Mit jelent az, hogy komplex utası́tás a CISC architektúra esetén? Két szám összeadása egyszerű műveletnek számı́t. Ugyanakkor, ha egy tömböt átmásolunk és közben a tömb mutatókat folyamatosan frissı́tjük, az már komplex utası́tásnak számı́t3 A RISC rendszerek csak egyszerű utası́tásokat használnak, és bizonyos feltételeket is szabnak. Például az utası́tások argumentumainak a regiszterekben kell lenniük és nem a memóriában 1
Szabad fordı́tásban: Komplex utası́táskészletű számı́tógép fordı́tásban: Egyszerűsı́tett utası́táskészletű számı́tógép 3 Létezik ilyen utası́tás a CISC processzorokon, ez a MOVSB utası́tás, lásd 7.61 bekezdés 2 Szabad 11 1.2 Assembly elsőre Az assembly nyelven ı́rt programokat processzálni kell egy másik program, assembler, által ami gépi kódot generál. A gépi kódot fogja futtatni a processzor Nézzünk néhány assembly utası́tást: inc mov add [result] [meret], 45 [mask1], 256 Az első sorban megadott utası́tás megnöveli a ‘result’ változó értékét. A második sorban megadott utası́tás a 45-ös értéket tölti a ‘méret’ változóba, mı́g a harmadik utası́tás 256-ot add a ‘mask1’ változóhoz. A fenti kódrészlet C programozási nyelven a következőképpen néz ki: result++; meret = 45; mask1 = mask1+ 256; A példa alapján a
következőket lehet megállapı́tani az assembly nyelvről: • Az assembly nyelv utası́tásai kriptikusak. • Az assembly nyelv műveleteit mnemonikok ı́rják le, például add vagy mov. • Az assembly nyelv utası́tásai nagyon alacsony szintűek. Például a következőt már nem ı́rhatjuk le4 : mov [meret], [adat] A 1.1 táblázat néhány assembly utası́tást és a neki megfelelő gépi kódot mutatja A táblázatnál az első észrevétel, hogy a RISC processzorokon az utası́tások hossza fix. (Ezzel is csökkentve a komplexitást) A másik fontos észrevétel, hogy a gép kód megértése nagyon nehéz az emberek számára, hiszen több ezer szám kombinációt kellene megjegyezni. Ugyanakkor közvetlen, egy az egyes megfeleltetés van az assembly utası́tás és a gépi kód között ezért ha az utası́tást ı́rjuk le az pontosan megfelel a szándék szerinti gépi kódnak és ı́gy csak
‘mazochisták’ programoznának gépi kódban. Mindenki inkább az emberek számára jobban értelmezhető assembly parancsokat, mnemonikokat használja. Persze a digitális forradalom elején néhány programot még gépi kódban ı́rtak 1.3 Miért tanuljunk assembly nyelvet? Az assembly programozás nem annyira népszerű mint néhány éve volt. Ugyanakkor még mindig több oka van annak, hogy megtanuljunk assembly-ben programozni: • Tanulás: Fontos tudni hogyan működnek a processzorok és fordı́tók az utası́tás szinten. Ezen ismeretek segı́tségével meg lehet állapı́tani mely programozási módok a leghatékonyabbak, illetve, hogy a magasabb szintű programozási szerkezetek hogyan működnek. • Debuggolás: Több szempontból is hasznos lehet ha a fordı́tók által generált kódot meg tudjuk érteni illetve meg tudjuk állapı́tani, hogy mennyire jó, optimalizált kódot generál egy fordı́tó.
• Fordı́tók: Az assembly kód megértése elengedhetetlen ahhoz, hogy fordı́tót, debugger-t vagy egyéb fejlesztő eszközöket fejlesszünk. • Beágyazott rendszerek: A beágyazott rendszereknek nincs annyi erőforrása mint egy hagyományos PC-nek és ezért szükség lehet az assembly nyelvre, hogy ilyen rendszerekre gyors és hatékony kódot ı́rjunk. 4A magyarázat a 3. fejezetben található 12 Pentium processzor Assembly Művelet Gépi kód (hex) Üres művelet 90 nop inc result Növelés FF060A00 mov result, 45 Másolás C7060C002D00 and mask, 128 Maszkolás 80260E0080 MIPS processzor Assembly Művelet nop Üres művelet mov $t2, $t15 Másolás and $t2, $t1, 15 Logikai ÉS addu $t3, $t1, $t2 Összeadás Gépi kód (hex) 00000000 000A2021 312A000F 012A5821 1.1 tábla: Assembly parancsok és a megfelelő gépi kód • Hardware eszközök: A magas szintű programozási nyelvek korlátozott (absztrakt)
hozzáférést engednek a hardware elemekhez, ı́gy a hardware eszközök használatát és elérését biztosı́tó eszközvezérlőt ı́rni magas szintű nyelven nehéz vagy lehetetlen. Ilyen esetben is jól jöhet az assembly nyelv ismerete • Olyan utası́tásokat is használhatunk assembly-ben aminek a magasabb szintű nyelvekben nincs megfelelője. • Méretre való optimalizálás: A méretre való optimalizálás azt jelenti, hogy Program A kevesebb helyet foglal mint Program B de ugyanazt a feladatot látja el. A memória ma már olyan olcsó, hogy tulajdonképpen nem éri meg assembly-ben kódot ı́rni a program méretének csökkentése miatt. Ugyanakkor a cache még midig kis méretű és drága, ı́gy az erre optimalizált kód esetén még mindig fontos az assembly nyelv használata. • Sebességre való optimalizálás: A sebességre optimalizált program a lehető legrövidebb idő alatt végzi el a
feladatot. Habár a modern fordı́tók viszonylag jól optimalizálják a generált kódot, bizonyos esetekben a kézzel optimalizált assembly program részlet drámaian fel tudja gyorsı́tani a programot. Az utolsó két szempontból az utóbbi a fontosabb. Egyrészt a hely megtakarı́tás csak a program kódra vonatkozik és az adatra nem, másrészt a memória méretének növekedése miatt. Assemblyben azért lehet hatékony kódot ı́rni, mivel a nyelv sajátossága, hogy a generált kód csak azt tartalmazza amit beleı́rtunk, vagyis ami a feladat megoldásához kell Semmi más, extra információt nem fordı́t bele az assembler. A sebességre optimalizált alkalmazások két kategóriába sorolhatók: – idő hatékony alkalmazások: ezeknél a programoknál a gyorsabb futás jelent előnyt, de nincs különösebb probléma ha a sebesség lassabb; – idő kritikus alkalmazások: ebben az esetben a feladatot
adott idő alatt kell elvégezni. Általában ezek a valós idejű alkalmazások (real-time systems), például: repülőgép navigációs eszközei, robot kontroll rendszerek, kommunikációs szoftverek. 1.4 Mikor ne használjunk assembly nyelvet? Olyan sok hátránya van az assembly nyelven való programozásnak, hogy mielőtt elkezdenénk programozni assembly-ben más alternatı́vákat is vegyünk figyelembe. 13 1.41 A magas szintű programozási nyelvek előnye A magas szintű programozási nyelvek viszonylag kényelmes absztrakciót tesznek lehetővé, hogy az adott problémát megoldjuk. A magas szintű programozási nyelvek előnyei: • A programfejlesztés gyorsabb: A magas szintű programozási nyelvekben sokféle programozási konstrukció áll rendelkezésre. Általában rövidebbek is a programok • A programokat könnyebb karbantartani: A magas szintű programozási nyelven ı́rt programokat egyszerűbb
megérteni és ezért könnyebb mások által ı́rt programot áttekinteni és megérteni. • A programok hordozhatóak: A program nem tartalmaz processzor specifikus részleteket és ezért bármilyen rendszeren használhatóak.5 1.42 Az assembly hátrányai Az assembly-ben való programozás ellen szóló legfontosabb érvek: 1. Fejlesztési idő: Az assembly-ben való programozás szinte mindig több időt igényel mint a magasabb szintű programozási nyelv használata 2. Megbı́zhatóság és biztonság: Assembly nyelven könnyű hibát véteni Az assembler csak szintaktikai ellenőrzéseket végez 3. Debuggolás és ellenőrzés: Az assembly nyelven ı́rt programokban nehezebb hibát keresni, illetve nehezebb ellenőrizni a kódot, hogy az előı́rt feladatot oldja meg. 4. Karbantartás: Az assembly nyelven ı́rt programokat nehezebb módosı́tani és karbantartani A nyelv megengedi a “spagetti” kód ı́rási
technikát és egyéb trükkök is megengedettek a nyelvben, melyeket más nyelven nem lehet megvalósı́tani. 5. Hordozhatóság: Az assembly kód a hardware platformhoz kapcsolódik, csak az adott processzoron, architektúrán lehet lefuttatni 6. Modern fordı́tók: A modern fordı́tók sokat fejlődtek az elmúlt években és már nagyon jó kódot tudnak generálni és gyakran nehezebb jobb assembly kódot generálni. 1.5 Mielőtt elkezdenénk assembly-ben programozni Van néhány szempont amit figyelembe kell venni mielőtt egy komplex alkalmazást elkezdenénk assemblyben programozni: • Ha az a célunk, hogy egy program sebességét optimalizáljuk, akkor először azonosı́tsuk, hogy a program mely része fut a legtöbbet a processzoron. Ellenőrizzük, hogy mivel tölti a legtöbb időt a program, például a memória eléréssel, CPU utası́tások végrehajtásával, file-ok elérésével vagy valami mással.
• Döntsük el, hogy a fejlesztett program újrahasznosı́tható vagy csak egy egyedi alkalmazás. Ha a kódot újra fel akarjuk használni, akkor érdemes több időt tölteni az optimalizálással. • El kell dönteni, melyik assemblert használjuk, mivel a különböző assemblerek más-más szintakszist használhatnak. • A jelentős mértékben optimalizált kódot nehéz lehet olvasni, ı́gy a karbantartás miatt érdemes kisebb egységekbe szervezni a programot melyeknek jól definiált interface-e van és megfelelően van dokumentálva. 5 Itt főleg a forráskódról beszélünk, nem a futtatható, már lefordı́tott gépi kódú programról. 14 1.6 Szintakszis Kétféle jelentősebb szintakszis alakult ki az évek során, amiket az assembly programok ı́rásánál használhatunk: • AT&T szintakszis • Intel szintakszis 1.7 Assemblerek Több assembler is létezik az Intel processzorokra, melyek az
x86-os utası́tás készletet használják, vagyis a mnemonikokból Intel gépi kódot hoznak létre. Az alábbiakban csak néhányat mutatunk be 1.71 MASM Ez a Microsoft Assembler, mely a mai napig része a Microsoft fejlesztő környezetének, a Visual Studionak. A program neve: mlexe A MASM sokáig a “de-facto” ipari szabvány volt és több magasabb szintű programozási konstrukciót is tudott kezelni A formátuma nem teljesen ‘tiszta’, vannak inkonzisztens részek benne. Microsoft továbbra is fejleszti, de igazából minimális módon 1.72 GAS GAS rövidités megfelelője a GNU Assembler, mely a GNU binutils csomag része is. A GNU fordı́tók olyan formátumot generálnak, melyet ez az assembler képes lefordı́tani. GAS az úgynevezett AT&T szintakszist használja, bár ma már az Intel szintakszisnak megfelelő kódot is el tud fogadni. Ez az assembler használható Linux, Mac OS X és Windows alatt is. 1.73 TASM
Az egyik legnépszerűbb fejlesztői eszközöket a Borland cég készı́tette. Az általuk készı́tett programfejlesztő családba tartozik a Turbo Assembler is Sajnos ma már nem fejlesztik, az újabb utası́tások nem kerülnek bele, de még mindig elérhető az Interneten. Az assembler által használt szintakszis nagyon hasonló a MASM assembler szintakszisához. 1.74 NASM NASM megfelel a Netwide Assembler névnek és egy szabad forráskodú assembler, mely többféle objektum formátumot képes generálni és ı́gy több operációs rendszert támogat (Linux, Windows, Mac OS X, FreeBSD, stb). A szintakszisa tisztább mint a MASM assembler-é, de kevesebb magas szintű programozási konstrukciót képes kezelni. 1.75 Melyik assembler? Ez a jegyzet a NASM assemblert használja két fő ok miatt: • Az egyszerű szintakszis nagyon logikus és konzisztens. • Windows és Linux rendszeren is használható, melyek manapság a
legjobban elterjedt operációs rendszerek. 15 1.8 Összefoglalás Az assembly nyelv tanulása mind gyakorlati és pedagógia célokat szolgálhat. Még ha nem is szándékozunk assembly-ben programozni, akkor is érdemes megtanulni, mivel egy nagyon jó alapot ad ahhoz hogy megértsük, hogyan működnek a számı́tógépek. Amikor magas szintű programozási nyelvet használunk, akkor a rendszert egy “fekete dobozként” kezeljük. Ezzel szemben assembly programozás esetén a rendszert részleteit is ismerni kell, például a regisztereket. 1.9 Ellenőrző kérdések 1. Soroljon fel különböző processzorokat! 2. Mit jelent a CISC kifejezés és mi jellemző az ilyen processzorokra? 3. Soroljon fel indokokat miért érdemes assembly nyelvet tanulni? 4. Soroljon fel indokokat mikor kell assembly nyelvet tanulni? 5. Soroljon fel indokokat mikor ne használjunk assembly nyelvet? 6. Mi az assembly nyelv és a gépi kód
kapcsolata? 7. Magas szintű programozási nyelvben miért nem tudjuk teljes mértékben kontrollálni a hardwaret? 8. Miért hı́vjuk az assembly programozási nyelvet alacsony szintű nyelvnek és a C programozási nyelvet magas szintűnek? 9. Soroljon fel néhány különbséget a CISC és RISC processzorok között? 10. Hasonlı́tsa össze a két féle assembly szintakszist! 11. Soroljon fel assemblereket! 12. Miért lehet szükség az assembly használatára idő kritikus alkalmazások esetén? 13. Soroljon fel idő kritikus alkalmazásokat! 16 2. Fejezet A számı́tógép felépı́tése A számı́tógépnek alapvetően három fő komponense van: a központi egység vagy processzor (CPU), a memória, és az Input/Output eszközök. Lásd a 21 ábra A részek közötti kapcsolatot a rendszer busz biztosı́tja A memória tárolja a programokat és az adatokat is egyszerre Az Input/Output eszközök lehetnek a
billentyűzet, a képernyő és ı́gy tovább. A 22 ábra egy részletesebb nézetét adja a számı́tógépnek, ahol jól látható hogy a rendszer busz három részből áll: cı́m busz, adat busz és kontroll busz. A cı́m busz szélessége határozza meg az elérhető memória kapacitást, illetve az adat busz adja meg, hogy milyen méretű adatok mozoghatnak a CPU, a memória és az I/O eszközök között. Például a 8086-os processzornak 20 bites a cı́m busza és 16 bites az adat busza. Ezek alapján a processzor 220 byte-ot tud megcı́mezni, vagyis 1 MByte-ot és minden alkalommal 16 bit mozog az egységek között. A Pentium processzoroknak 32 cı́m vonaluk van a cı́m buszban és 64 adat vonala Így a Pentium 4 GByte memóriát tud megcı́mezni. A 2.2 ábrán az is fontos, hogy a buszok milyen irányba képesek adatot küldeni Látható, hogy a CPU a kontroll buszon keresztül ad utası́tásokat a
memóriának és az I/O alrendszernek, adatot viszont az adat buszon keresztül fogad. A kontroll buszon kiadható jelek: memória olvasás, memória ı́rás, I/O olvasás, I/O ı́rás, megszakı́tás és ı́gy tovább. 2.1 A processzor A processzor kontrollálja a legtöbb tevékenységet a rendszerben. Úgy érdemes rá gondolni, hogy a következő ciklust hajtja végre: 1. Egy utası́tás betöltése a memóriából (fetch), 2. Az utası́tás dekódolása, azonosı́tása (decode), CPU Memória Kapcsolat Input/Output 2.1 ábra: Absztrakt értelmezése a számı́tógépnek 17 CPU Memória Cím busz Adat busz I/O alrendszer Kontroll busz 2.2 ábra: Egy számı́tógép egyszerűsı́tett diagramja 3. Az utası́tás végrehajtása (execute) Ez a végrehajtási ciklus, vagy fetch-decode-execute ciklus. 2.11 Végrehajtási ciklus • Fetch – A processzor a betöltendő utası́tás cı́mét felrakja a
cı́m buszra. – A processzor a kontroll buszon keresztül memória olvasásra ad utası́tást a memória egységnek. A processzor addig vár amı́g az utası́tás meg nem jelenik az adat buszon. – A memória egységnek idő kell mı́g hozzáfér a szükséges részhez. Ez a hozzáférési idő – A memória a beolvasott utası́tást az adat buszra helyezi. – A processzor beolvassa az adat buszról az utası́tást • Decode – Azonosı́tani kell a beolvasott utası́tást. Ezt segı́tendő az utası́tások bizonyos kódolási mintát követnek, melyet majd a 7. fejezetben tárgyalunk • Execute – A végrehajtáshoz két egységre van szükség: egy kontrol és egy aritmetikai (ALU) egységre. A kontroll egység segı́t az időzı́tésben, mı́g az ALU egység a matematikai számı́tásokat végzi el. Megjegyezzük, hogy az adatok és az utası́tások nem mindig közvetlenül a memóriából jönnek, hanem
az úgynevezett cache-ből. A cache memóriához való hozzáférés gyorsabb A Pentium processzoron 16 KB-os cache van a chipen, melynek fele adat és fele utası́tás cache Szerencsére a cache használata hardware-ben van megoldva és teljesen transzparens a programozó számára. 18 2.12 A rendszer óra A rendszer óra a system clock. A rendszer óra teszi lehetővé, hogy a műveleteket összeszinkronizáljuk Az órajel 1-eseket és 0-kat ad ki sorozatban, szekvenciában. Az óra frekvencia értékét a másodpercenkénti ciklusok száma adja meg és a mértékegysége Hertz (Hz). A MHz és GHz 106 és 109 ciklust jelent másodpercenként. 1 óra frekvencia = (2.1) óra ciklus hossza A rendszer óra adja meg a számı́tógép sebességét. Minden processzor művelet végrehajtása több órajel ciklust igényel. Például egy 1 GHz-es Pentium processzoron egy adat továbbı́tása a memóriából a processzorra
három órajel ciklust igényel. Egy óra ciklus hossza: 1 = 1ns 1 × 109 (2.2) és ı́gy az adattovábbı́táshoz 3 ns-ra van szükség. A számı́tógépek sebességét növelhetjük úgy, hogy nagyobb órajel frekvenciát használunk. Például egy 2 GHz-es processzoron az adat továbbı́tás már csak 1.5 ns-ig fog tartani 2.2 Cı́mzési architektúra Érdekes módon az egyik legfontosabb tulajdonsága egy architektúrának hogy hány cı́met használunk az utası́tásokban. A legtöbb művelethez egy vagy két argumentumra van szükség Ezek alapján szokták a műveleteket binary és unary műveleteknek nevezni, ahol a a “bi-” kettőt, az “un-” egyet jelent. “Unary” művelet például a tagadás (NOT) művelet, mı́g “binary” művelet az összeadás és a kivonás. Ezek a műveletek egy eredményt adnak. Természetesen vannak kivételek, például az osztás Az osztásnál két
argumentumra van szükség, az osztandóra és az osztóra, viszont két eredmény is keletkezik: az eredmény és a maradék. Mivel a “binary” műveletek a leggyakoribbak és ebben az esetben két input argumentumra és egy eredmény, output argumentumra van szükség ezért ezért általában három cı́mre van szükség egy utası́tásnál. Ebben a részben azt nézzük meg, hogyan lehet három, kettő, egy és zérus cı́met használni az utası́tásokkal. 2.21 Három cı́mes architektúra A három cı́met használó utası́táskészlettel rendelkező processzoroknál a két input argumentumot és az egyetlen output argumentumot tudjuk megadni. A legtöbb modern processzor ilyen utası́táskészletet használ. Nézzünk egy példát: A = B + C * D - E + F + A mely pszeudo assembly-ben a következőképpen néz ki: mult add sub add add T,C,D T,T,B T,T,E T,T,F A,A,T ; ; ; ; ; T T T T A = = = = = C B B B B *
+ + + + D C C C C * * * * D D - E D - E + F D - E + F + A A példában az látható, hogy matematikai műveletre egy utası́tást kell megadni. Ami szintén szembetűnő, hogy az első utası́tást kivéve az első két argumentum azonos Mivel az esetek jelentős részében ı́gy van, ezért a sok duplikáció elkerülése végett “két-cı́mes” utası́táskészleteket is szoktak implementálni processzorokban. 19 2.22 Két cı́mes architektúra Ebben az esetben az utası́tásoknak csak cı́m argumentuma van és az egyik cı́m inputként és outputként is szolgál. Az Intel processzorok, például a Pentium is ilyen utası́tásokat használ Nézzük az előző példát újra: A = B + C * D - E + F + A mely pszeudo assembly-ben a következőképpen néz ki: load mult add sub add add T,C T,D T,B T,E T,F A,T ; ; ; ; ; ; T T T T T A = = = = = = C C B B B B * + + + + D C C C C * * * * D D - E D - E + F D - E
+ F + A Mivel csak két argumentum áll rendelkezésre ezért az első utası́tással betöltjük az adatot T-be. Ebben az esetben az a feltűnő, hogy az első 6 utası́tásban a T argumentum közös. Ha ez lesz az alap eset, akkor már csak egy cı́m, argumentum kell az utası́tásokhoz. 2.23 Egy cı́mes architektúra Ha a memória drága vagy lassú akkor egy speciális regisztert használ a processzor. Ez a regiszter szolgáltatja az input és az output argumentumot egy utası́tásnak. Ezt a regisztert akkumulátor regiszternek is szokták nevezni, mivel benne gyűlik össze, akkumulálódik, az eredmény A legtöbb arhitektúra esetén csak egy akkumulátor regiszter van. Ezt a regisztert nem kell megadni az utası́tásnak csak a másik argumentumot. 2.24 Zéró cı́m architektúra Arra is van lehetőség, hogy mindkét argumentum speciális helyen tárolódik és ı́gy nem kell megadni őket az utası́tásoknál.
Ezek a processzorok egy vermet használnak Az argumentumok a verem tetején vannak amiket az utası́tás levesz onnan, majd az eredményt is a verem tetejére teszi vissza. 2.25 Load/Store architektúra Ebben az esetben a műveleteket a processzor belső regiszterein végezhetjük el és külön utası́tással kell beolvasni az adatokat a memóriából a regiszterekbe, illetve a regiszterekből kiı́rni a memóriába. A fenti példa a következőképpen módosul: A = B + C * D - E + F + A mely pszeudo assembly-ben a következőképpen néz ki: load load load load load load mult add R1,B R2,C R3,D R4,E R5,F R6,A R2,R2,R3 R2,R2,R1 ; R2 = C * D ; R2 = B + C * D 20 sub add add store R2,R2,R4 R2,R2,R5 R2,R2,R6 A,R2 ; R2 = B + C * D - E ; R2 = B + C * D - E + F ; R2 = B + C * D - E + F + A A fenti példában hat regisztert is használunk. Bár nincs ennyire szükség, de ez általában jellemző ezekre az architektúrákra, hogy sok
regiszterük van. A RISC processzoroknak több regiszterük van mint a CISC processzoroknak. A MIPS processzornak 32 regisztere van, az Intel Itanium processzornak 128 regisztere és az Intel Pentium processzornak csak 10 regisztere van. 2.3 Regiszterek Minden processzorban vannak regiszterek, melyeket két fő csoportba sorolhatunk: • általános célú regiszterek, • speciális célú regiszterek. A speciális célú regisztereket további két csoportba oszthatjuk: felhasználó által elérhető regiszterek és csak a rendszer által elérhető regiszterek. A Pentium regisztereit a 3 fejezetben tárgyaljuk 2.4 Végrehajtási sorrend A program végrehajtása általában szekvenciálisan történik, az utası́tásokat egymás után hajtjuk végre. Az egyik regiszter, a “Program Counter” (PC) vagy “Instructon Pointer” (IP) regiszter, fontos szerept játszik a végrehajtási sorrend kezelésében. A processzor mindig azt az
utası́tást tölti be (fetch) amire a PC regiszter mutat. A betöltés után a PC regiszter értékét megnöveljük, hogy a következő utası́tásra mutasson. Ez a megnövelés lehet fix méretű, például a RISC processzoroknál, vagy változó méretű a CISC processzoroknál, ahogy ez látható a 1.1 A CISC processzorok esetén minden utası́tásnál külön meg kell állapı́tani, hogy mennyivel növeljük meg a PC regiszter értékét. A magasabb szintű programozási nyelvekben ugyanakkor vannak feltételes végrehajtási és ciklikus programozási konstrukciók, melyek a végrehajtási sorrendet változtatják meg valamilyen futás közbeni feltételtől függően. Ezek megvalósı́tása a processzorokban “speciális” módon történik 2.41 Branching A “branching” szó fordı́tása talán az elágazás lehet. Arról van szó, hogy az eredeti szekvenciát megszakı́tva, máshol
folytatódik a program végrehajtása. Két változata van: a feltétel nélküli és feltételes ugró utası́tás. Ezeknek az utası́tásoknak egy argumentuma van, mely explicit módon megadja az új utası́tás cı́mét. Ez azt jelenti, hogy amikor máshol kell folytatni a végrehajtást, akkor a PC regiszterbe az új cı́met töltjük be és ı́gy a következő “fetch”-nél már ezt a cı́met fogja használni a processzor. A végrehajtási sorrend a feltétel nélküli ugró utası́tás esetén a 2.3 ábrán látható Feltételes ugrás A feltételes ugrás esetén az új cı́m csak akkor töltődik be a PC regiszterbe, ha valamilyen feltétel teljesül. Kétféle módon szokták ezt megadni a különböző processzorokban: Set-Then-Jump : Az ilyen architektúrájú processzorokban a vizsgálat és az ugrás szét van választva. A két, különálló rész közötti kapcsolatot egy regiszter
biztosı́tja. A vizsgálat beállı́tja a regiszter értékét, majd az ugró utası́tás ezt a regisztert vizsgálja meg hogy bekövetkezzen-e az ugrás vagy sem. A Pentium processzorok ezt a technikát használják 21 utasítás jump utasítás a cím b utasítás utasítás c d 2.3 ábra: Ugró utası́tás Test-And-Jump : A legtöbb processzor összekombinálja a két részt, például a MIPS processzorok. Például: beq Rsrc1, Rsrc2, célcı́m összehasonlı́tja az Rsrc1 és Rsrc2 regiszterek tartalmát és ha egyenlőek, akkor a célcı́m-nél folytatódik a végrehajtás. 2.5 Memória A számı́tógép memóriáját úgy érdemes elképzelni mint sok elektronikus “kapcsoló” összessége. Ezek a “kapcsolók” két állapotban lehetnek: nyitott vagy zárt állapotban. Ugyanakkor ezeket az állapotokat érdemesebb 1 és 0 állapottal jellemezni. Így minden “kapcsolót” reprezentálni lehet
egy bináris számmal vagy bittel. A memória millió szám tartalmaz biteket A jobb kezelhetőség miatt a memória a biteket csoportokba szervezik. 8 bit csoportja egy byte Így a memória mint egy byte sorozat képzelhető el Minden byte-ra egy index számmal lehet hivatkozni. Az első index értéke 0 Az utolsó index értéke 2n − 1, ahol az n az adatbusz szélessége (hány bites). A memória sematikus képe a 24 ábrán látható 2.51 Memória műveletek Két alapvető művelet van: adat olvasás a memóriából és adat ı́rás a memóriába. Mindkét esetben szükség van egy memória cı́mre ahonnan olvashatunk, vagy ahova ı́rhatunk. Ezenkı́vűl az ı́rási művelet még egy adatot is igényel. 2.52 Olvasási ciklus 1. A processor elhelyezi az olvasandó adat cı́mét a cı́m buszon 2. A kontroll buszon a processzor kiadja a memória olvasási jelet 3. A processzor várakozik amı́g az olvasás
megtörténik és az adat megjelenik az adat buszon 4. A processzor beolvassa az adatot az adat buszról 22 32 FFFF FFFF 2 -1 FFFF FFFE 1 0000 0001 0 0000 0000 2.4 ábra: A memória sematikus képe 5. A kontroll buszon jelzi a processzor, hogy véget ért az olvasás Egy Pentium processzor olvasási ciklusa három órajel ciklusnak felel meg. Az első órajel ciklus alatt az 1. és 2 lépés hajtódik végre A második órajel ciklus alatt a processzor várakozik A harmadik órajel ciklus alatt az utolsó két lépés fut le. Ha a memóriának mégsem sikerül az olvasás, akkor ezt jelzi a processzornak ami egy újabb órajel ciklusig vár. 2.53 Olvasási ciklus 1. A processzor elhelyezi az ı́randó adat cı́mét a cı́m buszon 2. A processzor elhelyezi az adatot az adat buszra 3. A kontroll buszon a processzor kiadja a memória ı́rási jelet 4. A processzor várakozik amı́g az ı́rás megtörténik 5. A kontroll
buszon jelezzük az ı́rás végét A Pentium processzor ı́rási cı́klusa is három órajel ciklust igényel. Az 1 és 3 lépés az első órajel alatt következik be. A 2 lépés csak a második órajel ciklus alatt történik A második órajel ciklus végén jelzi az ı́rás végét. 2.54 Memória tı́pusok A memóriákat különböző kategóriákba lehet csoportosı́tani. Az egyik legfontosabb tulajdonsága a memóriáknak, hogy csak olvashatók vagy ı́rhatók-olvashatók. Szintén fontos tulajdonság, hogy a memória minden részének elérése azonos időben lehetséges (random-access) vagy csak szekvenciálisan. A szekvenciális elérés magyarázatához a legjobb példa egy kazetta, amikor is addig kell olvasni a kazettát, amı́g el nem értük a keresett adatot. Végül vannak a “volatile” memóriák, melyeknél amı́g feszültség alatt van az egység csak addig őrzi meg a tartalmat. A
“nonvolatile” memória akkor is megőrzi a tartalmát ha nincs feszültség alatt az egység. Csak olvasható memóriák A csak olvasható memóriák (Read Only Memory vagy ROM) csak olvasási műveletet enged. Ebbe a memóriába nem tudunk ı́rni. A fő előnyük, hogy egyben “nonvolatile” memóriák is A ROM memóriák 23 tartalmát a “gyárban” égetik bele. Ezeket a memóriákat olcsó gyártani A régebbi számı́tógépekben a BIOS általában ROM. Vannak úgynevezett programozható ROM-ok is (PROM), illetve törölhető (erasbale) PROM-ok (EPROM). Írható-olvasható memóriák Az ı́rható-olvasható memóriákat általában RAM-nak (random access memory-nak) is szokták nevezni, habár a ROM-ok esetén is igaz az, hogy minden része azonos időben érhető el. Ezeket a memóriákat két csoportba lehet sorolni: statikus és dinamikus. A statikus RAM memóriák (SRAM) megőrzi az adatot a
beı́rás után, minden további menipuláció nélkül, amı́g a rendszer feszültség alatt van. Ilyen memória a cache vagy a regiszterek. Ezzel szemben a fő memória dinamikus (DRAM) A DRAM egy komplex eszköz, mely kondenzátorok segı́tségével tárol egy bitet. A feltöltött kondenzátor jelöli az 1-es értéket Mivel a kondenzátorok idővel vesztenek a töltésükből ezért időközönként frissı́teni kell. Tipikusan 64 ms a frissı́tési periódus Az olvasás során azt teszteljük, hogy a kondenzátor fel van-e töltve. Ugyanakkor ez a tesztelés tönkre is teszi a töltést Ebben az értelemben a DRAM egy speciális memória, mivel az olvasás is destruktı́v, nem csak az ı́rás. A legtöbb memória esetén csak az ı́rás destruktı́v. A destruktı́v olvasás következménye, hogy az olvasás után egy helyreállı́tási ciklus szükséges. Ennek az a következménye, hogy az olvasás
kétszer olyan sokáig tart mint más memóriák esetén. Modern memória tı́pusok: • FPM DRAM: Fast page-mode DRAM • EDO DRAM: Extended Data Output DRAM • SDRAM: synchronous DRAM • DDR SDRAM • RDRAM: Rambus DRAM 2.55 Byte sorozatok tárolása Természetesen általában nem csak egy byte-ot kell tárolni, hanem több byte-ot is. Például egy egész számot a C programozási nyelvben általában 4 byte-on tárolunk. Felmerülhet a kérdés, hogy hogyan tároljuk ezt a 4 byte-ot a memóriában? A 2.5 ábra két megoldást is mutat Az ábrán az MSB jelölés a “Most Significant Byte”-nek felel meg, mı́g az LSB a “Least Significant Byte”. Mindkét megoldás esetén a 100-as cı́met adtuk meg, és ehhez képest történik a tárolás. A “Little-endian” tárolási módban a legkisebb helyiértékű byte (LSB) tárolódik először. Ezzel szemben a “Big-endian” tárolási módban a legnagyobb
helyiértékű byte (MSB) tárolódik legelőször. Nézzünk egy másik példát a “Little-endian” tárolási módra. Az 1234h hexadecimális szám esetén először a 34h, majd a 12h byte-ot tárolja a rendszer. Az 12345678h szám esetén a tárolási sorrend: 78h, 56h, 34h, 12h. Melyik tárolási módszer a jobb? Mindkettő ugyanolyan jó. Csak a processzor tervező döntése, hogy melyiket használja. A Pentium processzorok a “Little-endian” tárolási módot használják A MIPS és PowerPC processzorokon a “Big-endian” tárolási mód az alapértelmezett, de át lehet konfigurálni őket “Little-endian” tárolási módra is. Általában a különböző tárolási mód nem okoz problémát, ha mindig csak egyféle processzort használunk. Az igazi problémák akkor jelennek meg, ha különböző tárolási módszert használó processzorok között akarunk egy programot hordozni. Ebben az
esetben az adatokat konvertálni kell! 24 MSB LSB 11110100 10011000 10110111 00001111 cím 103 102 101 100 cím 11110100 10011000 10110111 00001111 103 102 101 100 Little-endian 00001111 10110111 10011000 11110100 Big-endian 2.5 ábra: “Little-endian” és “Big-endian” tárolási mód 2.56 Adat “alignment” problema Egy program végrehajtási sebességét több tényező is befolyásolja. A tényezők közül néhány a programozó befolyása alatt van, mı́g másokat nem tudnak befolyásolni Ebben a fejezetben az egyik fontos tényezőt vizsgáljuk meg. Tegyük fel, hogy egy 32 bites adatot szeretnénk olvasni a memóriából. Azt is tegyük fel, hogy az adat busz szintén 32 bites. Ha az olvasni kı́vánt adat cı́me néggyel osztható, akkor a memóriában pont úgy van elhelyezve, hogy egy sorba esik. Ez látható a 26 ábrán és ezt szoktuk illesztett, “aligned” adatnak nevezni. Mivel az adatbusz 32 bites,
ezért egyszerre 4 byte-ot, egy sort lehet beolvasni a memóriából. Ez azt jelenti, hogy ha a cı́m nem osztható néggyel, akkor az adat két sorba kerül és kétszer kell olvasni a memóriából, majd ezekből fogja a processzor összeállı́tani a szükséges 32 bites adatot. A kétszeri olvasásnak hatása van a program futására, mivel a nem illesztett adatok miatt lassabban fog futni! n+3 k+2 24-31 n+2 k+1 16-23 n+1 8-15 k 0-7 n+0 CPU adatbusz 32 bit k+3 memória adat1 adat2 2.6 ábra: Adat illesztés - “data alignment” Az adat illesztés problémája teljesen transzparans módon, vagyis nem jelenik meg a felhasználó számára, kivéve, hogy a program lassabban fut. A 16 bites adatokat 2 byte-ra kell illeszteni Ez azt 25 Cím busz Adat Adat busz Státusz Parancs Kontroll busz I/O eszköz I/O kontroller 2.7 ábra: Input/Output eszköz sematikus ábrája jelenti, hogy a cı́m legkisebb helyiértékű bite
zérus, vagyis a cı́m páros. A 32 bites adatokat 4 byte-ra kell illeszteni vagyis a cı́m két legkisebb helyiértékű bite zérus. És ı́gy tovább A Pentium processzorok megengedik az illesztett (“aligned”) és nem illesztett adattárolást is. Bizonyos processzorok az előbb leı́rt hatékonysági probléma miatt nem engedik meg, hogy az adat ne legyen illeszteve. 2.6 Input/Output Az Input/Output eszközök teszik lehetővé, hogy a számı́tógépek kommunikáljanak a “külvilággal”. Input/Output eszköz lehet hogy csak adatszolgáltatásra alkalmas, input-ra, például az egér, vagy csak output-ra képes, például a monitor, vagy input-ra és output-ra is képes. Így lényegében az I/O eszközöknek két fő célja van, a külvilággal kommunikálni és adatot tárolni. Mindegyik kommunikáció a rendszer buszon keresztül történik, bár az I/O eszközök nem közvetlenül kapcsolódnak a
buszhoz, hanem van egy I/O kontroller az eszköz és a rendszer busz között, ahogy ez 2.7 ábrán látható Két fontos ok miatt van szükség ezekre az I/O kontrollerekre: 1. A különböző I/O eszközöket különböző módon kell kezelni Ez azt jelenti, hogy a különböző eszközökkel különböző módon kell kommunikálni, néha várni kell az adat megérkezésére vagy vezérlő jeleket kell adni. Ha a processzornak kellen mind ezt a feladatot ellátni, akkor több időt töltene ezzel, mint a felhasználó kiszolgálásával, vagyis a programok futtatásával. Az I/O eszköz kontroller elvégzi a processzor helyett ezeket a feladatokat. 2. A másik ok, hogy a rendszer buszon keresztül küldött elektromos jel igen alacsony, ami azt is jelenti, hogy a rendszer busz nem lehet túl hosszú. Emiatt az I/O eszköz kontrollerek közel vannak a processzorhoz, például a számı́tógép házban, és majd a
kontroller tud külön, erősebb jelet küldeni az eszköznek. Az 2.7 ábra azt is mutatja, hogy az I/O eszköz kontrollerekben általában három regiszter is van Például egy nyomtató esetén a “Státusz regiszter” jelzi, hogy az eszköz készen áll-e, az “Adat regiszterbe” kell tenni a nyomtatandó karaktert és a “Parancs regiszterben” kell utası́tást adni az eszköznek, hogy nyomtassa ki a karaktert. A processzor I/O portokon keresztül éri el ezekete a regisztereket Az I/O port nem más mint az I/O eszközön levő regiszter cı́me. Az I/O portok lehetnek a memóriára illesztettek, memory-mapped I/O. Ilyen rendszer például a MIPS processzorban van A Pentium processzorok egy I/O cı́m tartományt használnak Ez a cı́m tartomány különbözik a memória cı́m tartományától Ebben az esetben külön I/O utası́tásokat kell használni Ugyanakkor ez az utóbbi technika a memóriára illesztett I/O-t is
lehető teszi. Később látni fogjuk, hogy például a képernyő a memóriára illeszthető és úgy is ı́rhatunk a képernyőre, hogy egy speciális memória területre ı́runk. Ezzel szemben a billentyűzettel lehet I/O utası́tásokkal is kommunikálni. 26 2.61 I/O eszközök elérése Amikor assembly-ben programozunk közvetlenül vezérelhetjük az I/O eszközöket. Bár erre lehetőségünk van, de leı́rások és segı́tség nélkül gyakran nagyon bonyolult lehet, illetve minden esetben saját input és output függvényeket kellene kifejlesztenünk. Ezenkı́vül, ha mindenkinek teljesen szabad hozzáférése van az I/O eszközökhöz, akkor rosszindulatú emberek ezt ki is használhatják. Ezért van az, hogy általában az operációs rendszer kontrollálja az eszközökhöz való hozzáférést, illetve biztosı́tja a rutinokat is amiket használhatunk. A rutinok általában valamilyen
megszakı́tást használnak A megszakı́tásokat a 8.2 bekezdésben tárgyaljuk 2.7 Összefoglalás Ebben a fejezetben a számı́tógép alapvető elemeit ismertük meg olyan mélységben amire szükségünk lehet az assembly programozás során. Ezek az ismeretek lehetnek újak, illetve bizonyos fogalmak előfordulhattak más tárgyak keretében. 2.8 Ellenőrző kérdések 1. Milyen részekből áll egy sematikus számı́tógép? 2. Mi a fetch-decode-execute ciklus? Melyik lépésben, mi történik? 3. A rendszer busz milyen fő részekből áll? Melyiknek mi a szerepe? 4. Ha a processzornak 64 cı́m vonala van, mekkora lehet maximálisan a memória mérete? Mi az utolsó byte cı́me? 5. Mi határozza meg, hogy a memória és a processzor között adatok mérete mekkora? 6. Egy 2GH-es processzorban mekkora egy óra jel ciklus? 7. Miben különbözik a “load/store” architektúra a többi architektúrától? 8.
Adjon magyarázatot arra hogy a RISC processzorokon az utası́tások egymás utáni végrehajtása miért lehet gyorsabb mint a CISC processzorokon! 9. Mit jelent a három cı́mes architektúra? 10. Miben különbözik a három és két cı́mes architektúra? 11. Hogyan lehet olyan architektúrát megvalósı́tani, amelyikben az utası́tásoknak nem kell argumentumot megadni? 12. Adja meg a következő matematikai műveleteket pszeudo assembly-ben két cı́mes architektúra esetén: E = A * B + C - D 13. A RISC vagy a CISC processzorokra jellemző a nagy számú regiszter? Melyik cı́mzési architektúra esetén van szükség erre a nagyszámú regiszterre? 14. A Pentium processzor a RISC vagy CISC processzorok családjába tartozik? 15. A MIPS processzor a RISC vagy CISC processzorok családjába tartozik? 16. Hogyan lehet megvalósı́tani a feltételes ugró utası́tást különböző architektúrákon? 17. Mi a PC regiszter
szerepe a feltételes ugró utası́tás végrehajtása során? 27 18. Hogyan néz ki a memória sematikus képe? Jelölje a minimum és maximum indexet 16 bites adatbusz esetén. 19. Mi jellemzi a ROM memóriákat? 20. Mi a különbség a statik és dinamikus RAM-ok között? 21. Mi a DRAM működésének alapelve? Miért dinamikus memória? 22. Miért kell frissı́teni a DRAM memóriát? 23. Mi a különbség a “volatile” és “nonvolatile” memória között? 24. Mit jelent a “little-endian” tárolási mód? 25. Hogyan tárolódik a 44443333h hexadecimális szám a “big-endian” tárolási móddal? 26. Milyen tárolási módot használ a Pentium processzor? 27. Mit jelent az adat “alignment”? 28. Miért van hatással a nem illesztett adat tárolás a programok sebességére? 29. Miért van szükség I/O kontrollerre? 30. A processzor milyen módokon kommunikálhat az I/O kontrollerrel? 28 3.
Fejezet A processzor Az Intel cég 1969-ben vezette be az első processzorát, a 4004 processzort. Ezt követte a 8080 és 8085 processzorok. Ezek a processzorok vezettek az Intel Architektúra (IA) kidolgozásához, a 8086os processzorhoz, 1979-ben A 8086 processzornak 20 bites cı́m busza és 16 bites adatbusza van A következő generáció a 80186 processzor volt, melyben újabb utası́tásokat vezettek be, de a cı́m és adat busz mérete változatlan maradt. Mivel ezt a processzort nem igazán használták, ı́gy az igazi következő generációs processzor a 8086 után a 80286 processzor volt. A 80286 processzornak 24 bites cı́m busza van, amivel 16 MByte memóriát lehet megcı́mezni. Ugyanakkor az adatbusz megmaradt 16 bites A másik újı́tás a védett mód (protected mode) bevezetése volt. Az Intel cég első igazi 32 bites processzora a 80386-os processzor volt, melynek 32 bites cı́m és adat busza van. Ezzel a processzorral
4GByte memóriát lehet megcı́mezni, ráadásul akár egyben, ami lehetővé tette a “flat” módot. A 80486 processzor 1989-ben jelent meg A processzorba beépült a matematikai ko-processzor, egy 8KB-os L1-es cache is került a hardware-be, támogatta az L2 cache-t is és lehetővé vált a párhuzamos futtatás. A Pentium processzorok a “legújabbak” az Intel-től, bár itt a Pentium nevet mint a processzorok egy családjának a neveként használjuk. Az első Pentium processzort 1983-ban mutatták be Azóta megjelentek a Pentium Pro, Pentium II, Pentium III és Pentium 4 processzorok. A processzorokról ad áttekintést a 3.1 táblázat A hagyományos Intel architektúrától jelentősen eltér az Itanium processzor, mely RISC alapú, eltérően az Intel más processzoraitól, és más újı́tásokat is tartalmaz. Ugyanakkor, ma (2009 szeptember) már kijelenthetjük, hogy ez a processzor nem váltotta be a hozzá
fűzött reményeket és nem terjedt el olyan mértékben a számı́tógépes piacon, hogy jelentős szereplője legyen. Processzor Év 8086 80286 80386 80486 Pentium Pentium Pro Pentium II Pentium III Pentium 4 1979 1982 1985 1989 1993 1995 1997 1999 2000 Frekvencia (MHz) 8 12.5 20 25 60 200 266 500 1500 Regiszter méret 16 16 32 32 32 32 32 32 32 Adat busz méret 16 16 32 32 64 64 64 64 64 3.1 tábla: Processzorok áttekintése 29 Maximum memória 1 MB 16 MB 4 GB 4 GB 4 GB 64 GB 64 GB 64 GB 64 GB 3.1 Általános regiszterek A 3.2 táblázat tartalmazza a 8086-os processzor általános célú regisztereinek listáját A táblázatban az is látható, hogy a 16 bites regiszterek közül melyeket lehet 8 bites “darabokban”, regiszterekként használni. Fontos megjegyezni, hogy 8 bites regiszterek esetén az egyik regiszter értékének megváltoztatása nincs hatással a regiszter többi részére. Például ha az AL
regiszter résznek értéket adunk attól még az AH regiszter rész nem fog megváltozni! Speciális regiszter az IP vagy instruction pointer regiszter, mely a következő végrehajtandó utası́tásra mutat. Ezt a regisztert kontroll regiszternek is szoktuk nevezni Végül a szegmens regiszterek listáját a 3.3 táblázat tartalmazza Ezek a regiszterek támogatják a szegmentált memória kezelését. Név Akkumulátor Bázis Számláló Adat Forrás index Cél index Bázis pointer Stack pointer Teljes regiszterek 0-15 bit AX BX CX DX SI DI BP SP 8-15 bit AH BH CH DH 0-7 bit AL BL CL DL 3.2 tábla: Általános célú regiszterek Név Kód szegmens Data szegmens Extra szegmens Stack szegmens Regiszterek 0-15 bit CS DS ES SS 3.3 tábla: Szegmens regiszterek 3.2 Szegmentált cı́mzés először Ebben a fejezetben csak az úgynevezett valós módú memória architektúrát (real-mode memory architecture) tárgyaljuk, mely a
8086-os processzorra jellemző. A processzor 1MB memóriát tud megcı́mezni A memória megcı́mzéséhez 20 bites értékre van szükség. Az első memória hely cı́me: 00000h, mı́g az utolsó memória hely cı́me: FFFFFh. Mivel minden regiszter 16 bites a 8086 processzorban, ezért az ı́gy megcı́mezhető memória mérete korlátozott: 216 vagy 65536 byte. Ennek következtében a memóriát szegmensekre kell osztani, melyek mérete 65536 byte. Így, ebben a szegmentált memóriában meg kell adni egy szegmens bázis cı́met és egy offszetet. Ez a két érték adja meg a logikai cı́met A szegmens bázis cı́m adja meg, hogy a szegmens hol kezdődik a memórián belül, mı́g az offszet a szegmensen belüli helyet adja meg. A 31 ábra a fizikai memória cı́mek és a szegmentált, logikai cı́mzés közötti kapcsolatot mutatja. Amint látható, a szegmens fizikai cı́me 20 bites (12000h). Hogyan lehet 20 bites cı́met
tárolni 16 bites regiszterekben? A válasz az, hogy sehogy, és egy trükköt kell alkalmazni. A lényeg, hogy a szegmens regiszter a 20 bites cı́mből a 16 legnagyobb helyiértékű bitet tárolja (most significant bit) és feltételezzük, 30 fizikai cím 12345 offszet (345) szegmens bázis (1200) 12000 3.1 ábra: Kapcsolat a fizikai és logikai cı́mek között hogy az utolsó 4 bit zérus, amit nem kell tárolni. Ez persze azt is jelenti, hogy a szegmensek fizikai cı́me csak olyan lehet, aminek a 4 legkisebb helyiértékű bite zérus lesz, vagyis: 00000h, 00010h, 00020h, 00030h, . FFFE0h, FFFF0h A szegmensen belüli memória helyet az offszet adja meg A programozónak csak a logikai cı́mzéssel, vagyis a szegmens:offszet párral kell foglalkoznia, ami automatikusan konvertálódik 20 bites fizikai cı́mmé, ahogy ez a 3.2 ábrán látható Lényegében a szegmens regiszter tartalmához jobbról hozzáillesztünk négy
darab zérus bitet, majd ehhez az értékhez hozzáadjuk az offszet értékét. Ha hexadecimális számrendszerben dolgozunk, akkor a szegmens regiszter tartalmához egy nullát kell hozzáı́rni és ehhez kell az offszet értékét hozzáadni Nézzünk egy példát, ahol a logikai cı́m hexadecimális számrendszerben: 1200:0345 és ı́gy a fizikai cı́m: 12345, mivel: 1 2 1 2 + 0 3 3 0 4 4 0 5 5 Minden logikai cı́mhez tartozik egyetlen fizikai cı́m. Ugyanakkor a fordı́tottja nem igaz, vagyis egy fizikai cı́mhez több logikai cı́m is tartozhat. Például a 1200:0345 és 1000:2345 logikai cı́mek ugyanazt a fizikai cı́met adják meg. A logikai cı́m tárolásához tehát kell egy szegmens regiszter és egy offszet regiszter. A szegmens regisztereket a 33 táblázat sorolta fel Ezek a regiszterek teljesen függetlenek egymástól és a megcı́mzett szegmensek lehetnek szomszédosak, átfedők, ugyanazok illetve
egymástól távoli memória tartományok, ahogy ezt a 3.3 ábra is mutatja Offszet regiszterként csak azok a regiszterek használhatók, melyeket a cı́mzésben lehet használni: BX, BP, SP, SI, DI. 3.3 Cı́mzési módok A CISC processzorok nagy számú cı́mzési módot támogatnak, szemben a RISC processzorokkal. A 8086-os processzor, mint egy CISC tı́pusú processzor, három fő cı́mzési módot támogat: 1. Regiszter mód: Ebben az esetben regiszterek adják az utası́tás a bemeneti paraméterét és az 31 19 szegmens regiszter 0 4 3 0000 19 16 15 0000 0 offszet regiszter Összeadó 19 0 20 bites fizikai cím 3.2 ábra: Fizikai cı́m generálása 8086 processzoron 3.3 ábra: A szegmensek egymáshoz való viszonya eredményt is regiszterben tároljuk. Például: MOV AX, BX Ez a leghatékonyabb cı́mzési mód, mivel az adat a processzoron belül marad, nincs szükség memória műveletre. 2. Közvetlen
mód: Ebben az esetben az adat az utası́tás része és bár az adat a memóriában van, de a kód szegmensben, nem az adat szegmensben. Az adat mindig egy konstans szám és csak forrás lehet, vagyis a közvetlenül megadott számot tároljuk, vagy manipuláljuk az utası́tással. Az utası́tás másik operandusa mindig valamilyen más tı́pusú, cı́mzésű kell legyen, például regiszter. Például: MOV AX, 1234h 3. Memória mód: Amikor az egyik operandus a memóriára hivatkozik, akkor több cı́mzési lehetőségünk is van. Itt érdemes az előző bekezdésre emlékezni, hogy egy szegmens és egy offszet cı́m komponenst kell megadni a végső, fizikai cı́mhez A szegmens részt vagy explicit módon adjuk meg, vagy a használt regiszterek és kontextus határozza meg az egyik szegmens regisztert. A különböző cı́mzési módok csak az offszet formáit adja meg. A cı́mzési módok formáit a 34
táblázat foglalja össze. Ezekben az esetekben az adat mindig az adat szegmensben van és a hozzáférés lassabb mint az előző két esetben. Memória cı́mzésre csak a BX, BP, SP, SI, DI regiszterek használhatók. A BP és SP regiszter esetén az SS szegmens regisztert feltételezzük, mı́g a többi regiszter esetén (BX, SI, DI) a DS szegmens reg- 32 Direkt Regiszter [disp] [BX] [BP] [SI] [DI] Cı́mzési mód Indirekt Bázisrelatı́v Indexelt [BX+disp] [BP+disp] [SI+disp] [DI+disp] Bázisrelatı́v indexelt eltolás nélkül eltolással [BX+SI] [BX+SI+disp] [BX+DI] [BX+DI+disp] [BP+SI] [BP+SI+disp] [BP+DI] [BP+DI+disp] 3.4 tábla: Cı́mzési módok Szegmens regiszter DS DS SS DS DS DS DS SS SS Offszet [disp] [BX+disp] [BP+disp] [DI+disp] [SI+disp] [BX+SI+disp] [BX+DI+disp] [BP+SI+disp] [BP+DI+disp] Cı́mzési példa [DS:4423] [DS:BX+3] [SS:BP+5512] [DS:DI+6201] [DS:SI+18] [DS:BX+SI+35] [DS:BX+DI+43] [SS:BP+SI+12] [SS:BP+dI+22]
3.5 tábla: Cı́mzési mód példák isztert feltételezzük a cı́mzésnél, ha nincs szegmens regiszter megadva az utası́tásban. Természetesen a szegmens regiszter explicit módon is megadható, ı́gy például a BX regiszterrel az ES szegmens regiszter is használható. Cı́mzési példákat a 35 táblázat tartalmaz 3.31 Direkt cı́mzési mód Ez az egyik legegyszerűbb cı́mzési mód. Az adat az adat szegmensben található és általában elegendő az offszetet megadni. Ilyenkor automatikusan a DS regisztert feltételezhetjük Természetesen explicit módon elő is ı́rhatjuk a szegmens regisztert. Mı́g a végső gépi kódban konkrét számnak, cı́mnek kell szerepelnie, assembly-ben lehetőségünk van szimbólikus cı́mek használatára. Ha szimbólikus cı́met használunk az assembler majd kiszámolja és behelyettesı́ti a konkrét cı́met a gépi kódba. Vegyük a következő adat
definı́ciókat a 4.21 fejezet alapján: valasz tabla1 nev1 DB ’i’ DB 0, 0, 0, 0, 0 DB ’Jim Doe’ majd nézzünk néhány példát ezeknek az adatoknak a direkt cı́mzésére: MOV MOV MOV MOV AL, [valasz] [valasz], ’n’ [nev1], ’K’ [tabla1], 32 ; ; ; ; AL-be az ’i’ karakter valasz-t felulı́rjuk ’n’ karakterrel Ezután ’Kim Doe’ lesz a név az els} o tárolt érték 32 lesz Nagyon fontos, hogy a szögletes zárójelet használjuk, ha magát az adatot akarjuk betölteni vagy kiı́rni. Hasonlı́tsuk össze az alábbi két utası́tást: MOV BX, [tabla1] MOV BX, tabla1 33 Az első utası́tás a ‘tabla1’ cı́men található 16 bites, word értéket tölti be a BX regiszterbe. A második utası́tásban a ‘tabla1’ cı́mét töltjük be a BX regiszterbe! Ez utóbbi jelölésre például az indirekt cı́mzésnél van szükségünk. 3.32 Indirekt cı́mzési mód A direkt cı́mzési
módot az egyszerűsége folytán leginkább arra használjuk, hogy egy-egy változót közvetlenül olvassunk vagy ı́rjunk. Ugyanakkor arra már nem alkalmas, hogy egy tömb n-edik elemét módosı́tsuk,1 erre inkább az indirekt cı́mzés alkalmas. Az indirekt cı́mzés során valamelyik cı́m regisztert használjuk arra, hogy például egy tömb cı́met a regiszterbe tötltsük, majd a regiszter módosı́tásával a tömb különböző elemeit elérjük. Vegyük azt a példát amelyben egy 10 elemű tömb elemit érjük el: tomb DB 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; | ; [BX+4] . MOV BX, tomb MOV [BX], byte 11 ; tomb[0] = 11 ADD BX, 4 MOV [BX], byte 66 ; tomb[4] = 66 Fontos megjegyezni, hogy az adat méretét is figyelembe kell venni a cı́mzés során. A fenti példában 10 darab byte adatot kezeltünk, mı́g a következő példában word méretű adatokat használunk: tomb DW 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; | ; [BX+4] .
MOV BX, tomb MOV [BX], word 11 ; tomb[0] = 11 ADD BX, 4 MOV [BX], word 66 ; tomb[2] = 66 Ennél a példánál arra érdemes emlékezni, hogy egy word adat két byte-ból áll, és a BX regiszterhez négyet hozzáadva a BX által mutatott cı́met négy byte-al toljuk el. A két példa közötti különbséget a 3.4 ábra mutatja Az ábrán nem véletlen a 66 00 adat, mivel bár a programban csak 66 szerepel, de ez word méretű adatként 0066 lesz, amit viszont az Intel processzorok a “Little endian” módon tárolnak. Az indirekt cı́mzéshez szükséges cı́met másképpen is betölthetjük a regiszterbe: MOV BX, tomb helyett LEA BX, [tomb] A fő különbség az, hogy az első esetben a cı́met az assembler a gépi kód generálása közben számolja ki addig a második esetben a cı́met futási időben határozza meg a rendszer. Bár ebben az esetben nincs közvetlen előnye, de összetettebb cı́mzés esetén már
igen, például: MOV BX, tomb ADD BX, SI 1 Hacsak nincs minden elemnek a tömbben saját neve. 34 tomb[4] tomb[0] 0 1 byte 2 3 11 4 5 . 66 tomb[0] tomb[1] tomb[2] 0 1 word 2 3 11 00 4 5 . 66 00 3.4 ábra: Byte és word méretű tömbök közötti különbség helyett LEA BX, [tomb+SI] lehet használni. 3.4 Státusz regiszter A státusz regiszter 16 bitből áll. Ebből a 16 bitből kilencet használ a 8086-os processzor A további 7 bit nincs definiálva illetve nem használja a processzor. A státusz bitek függetlenek egymástól és természetesen az értékük csak 1 vagy 0 lehet. A státusz bitek alapvetően jelzések Lényegében értesı́tik a programozót hogy a processzor valamilyen állapotban van ı́gy a program reagálni tud ezekre az állapotokra. Minden státusz bitnek külön jelentése van és külön két betűs szimbólummal jelöljük őket A 3.6 táblázat mutatja a bitek jelölését
OF : Overflow bit. Értéke 1 ha az utolsó eredmény az előjeles szám ábrázolási tartományon kı́vülre esik. Minden más esetben az értéke nulla DF : Direction bit. Ez egy furcsa státusz bit, mivel ebben az esetben nem a processzor jelez a programozónak, hanem fordı́tva, a programozó jelez a processzornak Ez a bit adja meg hogy string műveletek során a cı́mek növekedjenek vagy csökkenjenek. Amikor a bit értéke 1, akkor a string műveletek során a cı́mek csökkennek, ha a bit értéke 0, akkor pedig csökkennek a cı́mek. Ezt a bitet még újra megvizsgáljuk a 7.6 fejezetben IF : Interrupt enable bit. Ez egy két irányú státusz bit, vagyis nem csak a processzor jelezhet egy állapotot, hanem mi is képesek vagyunk jelezni a processzornak. Amikor ennek a bitnek az értéke 1, akkor a megszakı́tások engedélyezettek és bármikor bekövetkezhetnek. Ha a bit értéke zérus akkor CPU nem veszi figyelembe a
megszakı́tásokat. TF : Trap bit. Ennek a bitnek a segı́tségével lehet programokat lépésenként végrehajtani Ha be van állı́tva (értéke 1), akkor a processzor egy utası́tást hajt végre majd meghı́v egy megszakı́tást. Általános esetben a programozók nem használják, de például a DEBUG programnál elengedhetetlen. SF : Overflow bit. Értéke 1 ha az utolsó eredményben a legmagasabb helyiértékű bit értéke 1, vagyis negatı́v szám az eredmény. Ha az eredmény pozitı́v szám, akkor a legmagasabb helyiértékű bit értéke zérus és a ez a bit is zérus lesz. ZF : Zérus bit. Értéke 1 ha az utolsó eredmény zérus volt Ha az utolsó eredmény bármilyen zérustól különböző értékű, akkor ennek a bitnek az értéke zérus. 35 15 14 13 12 11 OF 10 DF 9 IF 8 TF 7 SF 6 ZF 5 4 AF 3 2 PF 1 0 CF 3.6 tábla: Státusz regiszter bitjei AF : Auxiliary bit. Ezt a
bitet csak a BCD aritmetika során használjuk A BCD aritmetikát a 871 fejezetben tárgyaljuk. PF : Parity (paritás) bit. Ez a bit azt jelzi, hogy az utolsó művelet eredményében (mint bináris számban) hány darab 1-es bit van. Például a 0F2h hexadecimális szám binárisan 1111 0010 amiben páratlan számú 1-es van és ı́gy a paritás bit értéke zérus lesz. A 03Ah binárisan 0011 1100 és mivel páros számú 1-es található az eredményben ı́gy a paritás bit értéke egy lesz. Ez bit tulajdonképpen abból az időből származik, amikor még minden kommunikáció soros porton keresztül zajlott. A soros kommunikáció esetén a hiba érzékelésének egyik módja volt, hogy a küldés előtt megállapı́tjuk az adat paritását, majd átküldjük az adatot és a paritás bitet. A “túloldalra” megérkezett adatot a paritás bittel lehet ellenőrizni. CF : Carry bit. Értéke 1 ha az utolsó
eredmény az előjel nélküli szám ábrázolási tartományon kı́vülre esik. Például ha egy aritmetikai vagy shift művelet során egy 9 vagy 17 bit is keletkezik, akkor ennek a bitnek az értéke 1 lesz. Minden más esetben az értéke nulla Példa a 87 fejezetben látható. 3.5 Ellenőrző kérdések 1. Mi a szegmens? Miért kell a Pentium processzorokon szegmentált memória architektúrát használni? 2. Valós módban miért 64KByte méretű egy szegmens? 3. Valós módban egy szegmens nem kezdődhet bárhol a memóriában Miért? 4. A 8086-os processzoron négy szegmens lehet egyszerre kezelni Miért? 5. Mutassa be a fizikai cı́m kiszámı́tásának módját logikai cı́mből! 6. Konvertálja az alábbi logikai cı́met fizikai cı́mmé: 3911:0200, 3000:0333 7. Az IP regiszterhez, melyik szegmens regiszter tartozik? 8. Lehet-e a CS és DS regiszter értéke ugyanaz? 9. Lehet-e a ES regiszter értéke: 1234h?
10. Melyik szegmens regiszter járul az SP regiszterhez? 11. Mire való a státusz regiszter? 12. Soroljon fel néhány dolgot, hogy mit jelezhet a státusz regiszter! 13. Mi a különbség a sor orientált és oszlop orientált tömb tárolási módok között? 14. Adva van a következő adat: tomb resb 12 töltse ki a hiányzó részeket, hogy a 4. és 5 elemet hasonlı́tsuk össze: MOV SI, MOV AX, [tomb+SI] CMP AX, 36 4. Fejezet NASM assembler 4.1 Egy forrás file szerkezete Az assembly nyelvben, illetve a legtöbb assembler alatt és ı́gy a NASM assembler alatt is, a forráskódban a sor a következő négyféle dolgot tartalmazhatja: cı́mke: utası́tás operandus ; megjegyzés A négyféle rész közül a legtöbb opcionális, hiszen egy sorban lehet csak megjegyzés, csak cı́mke, ezek kombinációja, esetleg cı́mke és utası́tás és ı́gy tovább. Az operandus jelenléte mindig az utası́tástól
függ. Ha a sor végén a ’backslash’ () karakter áll, ez azt jelenti, hogy a következő sor is az adott sorhoz tartozik, annak folytatása. A NASM assemblerben nincs semmilyen megkötés a SPACE-ek és tabulátorok használatára. Ezekből a karakterekből, a sor részei között bármennyit használhatunk. Rááadásul a cı́mke után a kettőspont is opcionális. Ez sajnos hibákhoz vezethet! Például, habár a lodsb utası́tást szeretnénk leı́rni egy sorba, de a lodab szöveget ı́rtuk le, az assembler nem fog szólni mivel úgy tekint rá mintha abban a sorban csak egy cı́mkét definiáltunk volna. A NASM assemblernek van egy kapcsolója (-w+orphan-labels) melynek megadása esetén az assembler szólni fog ha egy sorba, kettőspont nélkül, ı́runk le egy cı́met. A cı́mkékben használható érvényes karakterek a betűk és számok, ‘ ’, ‘$’, ‘#’, ‘@’, ‘˜’, ‘.’ és ‘?’. A
cı́mke első karaktere betű, ‘ ’, ‘?’ vagy pont () lehet Ha egy cı́mke ponttal kezdődik, annek sepciális jelentése van. A NASM assembler képes lefordı́tani a 8086, 386, 486, Pentium, P6, FPU és MMX utası́táskészleteket. Az utası́tások előtt szerepelhet egy prefix: REP, REPE/REPZ, REPNE/REPNZ, LOCK. A szegmens regiszter is megadható prefixként: es mov [bx], ax ami egyenértékű azzal, hogy mov [es:bx], ax Az utóbbi jelölés preferált, mivel más szintaktikai elemekkel ez konzisztens. Ha nem adtunk meg prefixet, például nem adtunk meg szegmens regisztert, akkor a NASM automatikusan generálni fogja. 4.2 Pszeudo utası́tások A pszeudo utası́tások nem igazi processzor utası́tások, de egy forrás sorban az ‘utası́tás’ részben szerepelhet. 37 4.21 DB és társai A DB, DW, DD, DQ, DT adatokat ad meg a forráskódban. A fő különbség, hogy milyen méretű adatot/adatokat definiálnak DB : egy
byte-ot vagy byte sorozatot definiál. DW : egy word-ot vagy word sorozatot definiál. Egy word két byte-ból áll Itt azt is figyelembe kell venni, hogy az Intel processzor ‘Little endian’ tárolási módot használ, vagyis a tárolás során a byte-okat felcseréli. DD : egy vagy több double word-ot definiál. Egy double word 4 byte-ból áll Ez az adatméret használható egyszeres precizitású floating-point számok (float) megadására is a matematikai koprocesszor számára. DQ : egy vagy több quad word-ot definiál. Egy quad word 8 byte-ból áll Ez az adatméret használható dupla precizitású floating-point számok (double) megadására is a matematikai koprocesszor számára. DT : egy vagy több ten byte-ot definiál. Egy ten byte adat 10 byte-ból áll Az alábbi lista több példát is mutat az adatok definiálására: db db db db dw dw dw dw dd dd dq 0x55 0x55,0x56,0x57 ’a’,0x55 ’hello’,13,10,’$’
0x1234 ’a’ ’ab’ ’abc’ 0x12345678 1.234567e20 1.234567e20 ; ; ; ; ; ; ; ; ; ; ; egy byte 0x55 3 bytes egymás után egy karakter és egy byte keverve karakter sorozat és számok keverve 0x34 0x12 0x41 0x00 0x41 0x42 0x41 0x42 0x43 0x00 0x78 0x56 0x34 0x12 floating-point konstans double-precision konstans A fenti példában érdemes megfigyelni a 6. és 8 sort A 6 sorban úgy tűnik, hogy csak egy karaktert definiálunk, de mivel a sor elején a DW pszeudo utası́tás szerepel, ami ‘word’ adatméretet, vagyis két byte-ot jelent. Ennek megfelelően a lefordı́tás után valójában két byte-ot foglal le az assembler, amelyből az egyik zérus lesz. A fenti példákból az is látható, hogy a karakterek (ASCII kódok) és számok szabadon keverhetők. A karaktersorozatok többféleképpen is megadhatók, egyszeres vagy dupla aposztrofok között, illetve egyben vagy karakterenként: db ’hello’ db
’h’,’e’,’l’,’l’,’o’ db "hello" 4.22 RESB és társai A RESB, RESW, RESD, RESQ és REST pszeudo utası́tások inicializálás nélküli adatoknak foglal helyet. Az ‘inicializálás nélküli adat’ azt jelenti, hogy csak az adat tárolására szükséges helyet foglalja le az assembler, de a helyen tárolandó adat bármi lehet kezdetben. (Lehet zérus vagy a memória egy részlete, bármi.) Ezek a pszeudo utası́tások több adatnak foglalnak helyet, például: buffer: resb 64 wordvar: resw 1 db "hello" ; 64 byte-nyi helyet foglal ; 1 word-nyi helyet foglal 38 4.23 Konstansok Ugyanúgy mint a legtöbb programozási nyelvben, például a C programozási nyelvben, assembly-ben is lehet konstansokat definiálni a forráskódban. A konstansokat egy helyen definiáljuk és a programban mindenhol csak egy névvel, szimbolikusan hivatkozunk rájuk. Ezeknek a konstansoknak kettős lehet szerepe: •
ha későbbi fejlesztések során a konstans értékét meg kellene változtatni, akkor csak a definı́ció helyén kell megváltoztatni az értéket, hiszen a programban csak a szimbólikus név szerepel és • ha a konstans neve ‘olvasmányos’, vagyis jelentéssel bı́r, akkor a program olvasása során könnyebb értelmezni a forráskódot. Például nem csak egy szám szerepel az adott helyen, argumentumban, hanem a szám funkcióját leı́ró név. A C programozási nyelvben a konstansok definı́ciója makrókkal lehetséges: #define PI 3.1415 A NASM assemblerben az ennek megfelelő konstans definı́ció: hossz EQU 12 4.24 TIMES pszeudo utası́tás Ha a TIMES pszeudo prefixet használjuk, akkor arra utası́tjuk a NASM Assemblert, hogy az adott utası́tást vagy adatot többször fordı́tsa bele az eredmény programba. Például: zerobuffer: TIMES 64 db 0 Ennek a sornak a hatására 64 darab zérus byte kerül az
eredmény programba. Ezt a sort érdemes összevetni az alábbi sorral, ami csak a helyet lefoglalja le, de a helyen tárolt adatról nem mond semmit. (Lásd: 4.22 fejezetet) zerobuffer: resb 64 A TIMES pszeudo utası́tás egyéb, összetett módon is használható, illetve egyszerűen utası́tásoknál is. Például az alábbi sor azt adja meg, hogy a MOVSB (761 fejezet) utası́tást háromszor kell a programba belefordı́tani: TIMES 3 movsb 4.3 SEG kulcsszó Ha egy olyan programot ı́runk amelyik több szegmensből áll, akkor egy memória hely (például változó) elérésénél szükség lehet a memória hely preferált szegmens cı́mére is.1 A preferált itt annyit jelent, hogy az assembler preferálja ezt a cı́met. Ilyen esetben lehet használni a seg kulcsszót Például: mov ax, SEG valami mov es, ax mov bx, valami aminek hatására a ES:BX regiszterek a valami változó memória cı́mét fogja tartalmazni. 1A
szegmens és offszet regisztereket használó memória cı́mzésről részletesebb leı́rás található a 3.2 fejezetben 39 4.31 További hasznosı́tási területek A seg kulcsszó használható távoli függvény hı́vásnál is, amikor a függvénynek a szegmense és offszet cı́me is kell. Például: call (SEG nyomtat):nyomtat Ha egy memória hely teljes cı́mét, szegmens és offszetet, is tárolni kell akkor a következő módon lehet ezt megadni: db valami, SEG valami Itt se felejtsük el, hogy a fordı́tott sorrend a ‘Little endian’ tárolási mód miatt van. 4.4 WRT kulcsszó Előfordulhat, hogy nem a preferált szegmensen keresztül akarunk hivatkozni egy memória helyre. (Ezt megtehetjük, hiszen a szegmensek átfedik egymást a 8086-os gépeken.) Ilyen esetben a wrt (With Reference To) kulcsszót lehet haználni. Például: mov ax, egy masik szegmens mov es, ax mov bx, valami wrt egy masik szegmens 4.5
Parancssori opciók 4.6 Hibaüzenetek 40 5. Fejezet DEBUG program Ez a fejezet a DEBUG program használatát mutatja be. A DEBUG program segı́tségével már lefordı́tott programokat tudunk “debuggolni”, lépésenként végrehajtani és közben a rendszer állapotát megvizsgálni, illetve hibákat (bug-okat) keresni. Bár a program igen egyszerű, azt is mondhatnánk “fapados”, azért is érdemes megismerni ezt a programot, mivel minden Microsoft Windows rendszeren létezik. 5.1 Jelölések cı́m - cı́mleı́rás • segmens:offset - pl. 0044:0f57 • segmens reg:offset - pl. ES:0f30 • offset - pl. 400 tartomány - egy memóriatartomány kijelölése • cı́m cı́m • cı́m, cı́m • cı́m L hossz lista - egymás után leı́rt hexadecimális számok szöveg - dupla aposztrofok között karaktersorozat 5.2 A DEBUG indı́tása • D:DEBUG <ENTER> : A program elindul és egy minusz jel jelzi, hogy a program
várja a felhasználó parancsait • D:DEBUG file<ENTER> : Betölti a file programot és belép a DEBUG programba 5.3 A DEBUG parancsai q - (Quit) kilépés a programból h val1 val2 - (Hex) kiı́rja a két érték összegét és különbségét 41 -h 9 000A -h 1 000A 1 0008 9 FFF8 d tartomány - (Dump) memória tartalmának kiı́rása a képernyőre -d c000:0010 C000:0010 24 C000:0020 4D C000:0030 52 C000:0040 2F C000:0050 29 C000:0060 50 C000:0070 40 C000:0080 E8 - 12 20 4F 56 00 43 00 26 FF 43 58 42 87 49 12 56 FF 4F 2F 45 DB 52 10 8B 00 4D 4D 20 87 2B 00 D8 00 50 47 42 DB 10 80 E8 00 41 41 49 87 01 00 C6 00-60 54-49 2D-47 4F-53 DB-87 10-00 00-38 56-74 00 42 31 20 DB 00 37 22 00 4C 30 28 87 18 34 8C 00 45 30 56 DB 00 2D C8 00 20 20 31 87 00 32 3D 20 4D 56 2E DB 00 00 00 49 41 47 32 87 00 FF C0 42 54 41 20 DB 03 FF 74 $.‘ IB M COMPATIBLE MAT ROX/MGA-G100 VGA /VBE BIOS (V1.2 ). PCIR+. @.874-2 .&VVt"=t -d 100 130
xxxx:0100 EB 24 0D 0A 54 68 69 73-20 69 73 20 6D 79 20 66 xxxx:0110 69 72 73 74 20 44 45 42-55 47 20 70 72 6F 67 72 xxxx:0120 61 6D 21 0D 0A 24 B4 09-BA 02 01 CD 21 B4 00 CD xxxx:0130 21 - .$This is my f irst DEBUG progr am!.$! ! s tartomány szöveg - (Search) memória tartományban megkeresi a szöveg valamennyi előfordulását -s fe00:0 ffff "BIOS" FE00:0021 FE00:006F -d fe00:0 FE00:0000 FE00:0010 FE00:0020 FE00:0030 FE00:0040 FE00:0050 FE00:0060 FE00:0070 41 4D 20 41 6E 41 1B 49 77 20 42 77 63 77 41 4F 61 43 49 61 2E 03 77 53 72 4F 4F 72 6F 0C 61 20 64 4D 53 64 66 04 72 76 20 50 20 20 74 01 64 34 53 41 43 53 77 01 20 2E 6F-66 54-49 4F-50 6F-66 61-72 6F-66 4D-6F 35-31 74 42 59 74 65 74 64 50 77 4C 52 77 20 77 75 47 61 45 49 61 49 E9 6C 00 72 20 47 72 6E 12 61 DB 65 34 48 65 63 14 72 32 49 38 54 20 2E 20 20 EC 42 36 20 49 20 43 42 33 Award SoftwareIB M COMPATIBLE 486 BIOS COPYRIGHT Award Software I nc.oftware Inc Aw.oftw C .Award Modular B IOS
v4.51PG23 c tartomány cı́m - (Compare) összehasonlı́t két memória tartományt f tartomány szöveg - (Fill) memória tartomány feltöltése a szöveggel -f 100 12f -d 100 12f xxxx:0100 xxxx:0110 xxxx:0120 ’BUFFER’ 42 55 46 46 45 52 42 55-46 46 45 52 42 55 46 46 BUFFERBUFFERBUFF 45 52 42 55 46 46 45 52-42 55 46 46 45 52 42 55 ERBUFFERBUFFERBU 46 46 45 52 42 55 46 46-45 52 42 55 46 46 45 52 FFERBUFFERBUFFER e cı́m lista - (Enter) értékek bevitele a memóriába. Ezzel a paranccsal byte sorozatot lehet beı́rni a memóriába.1 -e 100 B4 09 BA 0B 01 CD 21 B4 00 CD 21 g [cı́m] - (Go) programvégrehajtás folytatása. Ha a cı́m is adva van akkor a megadott cı́mre egy töréspontot tesz a debugger és az IP regiszter által megadott cı́mtől folytatja a végrehajtást. A töréspont azt jelenti, hogy ha a végrehajtás során a töréspont cı́mét elérjük, akkor a végrehajtás 1 Mivel a gépi kód is csak egy byte
sorozat, ezért amit ı́gy beı́runk a memóriába az lehet egyszerűen adat, de lehet program is! 42 leáll (megtörik) és egy parancssor jelenik meg aminek segı́tségével szétnézhetünk a rendszerben. Megvizsgálhatjuk a regiszterek állapotát vagy a memória tartalmát. Persze az is előfordulhat, hogy a végrehajtás során soha nem jutunk el a töréspontig. Ebben az esetben a “program” végigfut és végül vagy hibával vagy sikeresen leáll a futás. a [cı́m] - (Assemble) az opcionálisan megadott cı́mtől kezdve assembly parancsokat gépelhetünk be. Ebben az esetben a DEBUG program, mint egy assembler működik. -a 100 xxxx:0100 xxxx:0102 xxxx:0123 xxxx:0126 xxxx:0128 xxxx:012B xxxx:012D xxxx:012F jmp 126 ; adat átugrása db 0d,0a,"This is my first DEBUG program!" db 0d,0a,"$" mov ah,9 ; 09-es funkcionalitás mov dx,102 ; DS:DX -en a $-al lezárt string int 21 ; string kiı́rása int 20 ;
kilépés u [tartomány] vagy u [cı́m] - (Unassemble) az aktuális vagy a megadott cı́mtől az opcionálisan megadott tartományban assembly mnemonikra fordı́tja a gépi kódot. -u 126 12F xxxx:0126 B409 xxxx:0128 BA0201 xxxx:012B CD21 xxxx:012D B400 xxxx:012F CD21 - MOV MOV INT MOV INT AH,09 DX,0102 21 AH,00 21 i port - (Input) a megadott port-ról beolvas egy byte-ot o port byte - (Output) a megadott port-ra kiı́r egy byte-ot Portra való kiı́rás, vagy portból való beolvasás közvetlen kommunikációt jelent a hardware-rel. -o -i 18 -o -i 55 70 04 71 70 02 71 ; ; ; ; ; ; Kérdezzük le az órát Olvassuk vissza az értéket 18 óra Kérdezzük le a percet Olvassuk vissza az értéket 55 perc n filenév - (Name) a file nevének megadása. Erre a parancsra akkor van szükség, ha a memória egy darabját mint programot szeretnénk kiı́rni. p - egy függvény hı́vás vagy megszakı́tás végrehajtása egészben.
Ebben az esetben a függvény utası́tásait nem lépésről-lépésre hajtjuk végre, hanem egyben. r [reg] - (Register) alapesetben a regiszterek tartalmát ı́rja ki a képernyőre. A státusz bitek szövegesen jelennek meg, melyek értelmezését a 5.1 táblázatban jelennek meg Ha a regiszter is meg van adva, akkor lehetővé teszi a megadott regiszter értékének megadását. -r cx CX 0100 :273 t - (Trace) egy utası́tás végrehajtása w - (Write) a BX és CX regiszterekben együttesen megadott byte-nyi adatot ı́r ki az ‘n’ paranccsal megnevezett file-ba. Ha a file már létezett akkor a DEBUG felülı́rja! 43 Státusz bit Carry Parity Aux. carry Zero Sign Trap Direction Interrupt Overflow 1 CY PO AU ZR PL 0 NC NE NA NZ NG UP EI OV DW DI NV 5.1 tábla: A státusz bitek szöveges megjelenése 5.4 Példák Nézzünk néhány példát a DEBUG program használatára. A példákban a szegmens cı́met négy
darab ‘x’ jelöli (xxxx), mivel a memóriában bárhova betöltődhetnek a programok. A példákban a DEBUG programot kell elindı́tani a megadott módon, illetve a minusz (‘-’) jel utáni részt kell begépelni. 5.41 1 Példa Írjunk egy programot mely egy csillag karaktert nyomtat ki. A programot assembly mnemonikok segı́tségével adjuk meg. Utána a programot lefuttatjuk a DEBUG programban, majd kiı́rjuk a merev lemezre, az aktuális könyvtárba. C:> DEBUG -a 100 xxxx:100 mov ah,02 xxxx:102 mov dl,2a xxxx:104 int 21 xxxx:106 int 20 xxxx:108 ; csak ENTERT nyomjunk -r AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=xxxx ES=xxxx SS=xxxx CS=xxxx IP=0100 NV UP EI PL NZ NA PO NC xxxx:0100 MOV AH,02 -g * Program terminated normally -r cx :8 -n csillag.com -w -q C:>csillag.com * C:> 5.42 2 Példa Használjuk a DEBUG programot arra, hogy megnézzük az előző program futását lépésről-lépésre. Az
“egyszerű” utası́tásoknál a ‘t parancsot, a megszakı́tások végrehajtásánál a ‘p’ parancsot használjuk. A megszakı́tásoknál azért kell a ’p‘ parancsot használni, hogy a megszakı́tás során végrehajtandó utası́tásokat ne lépésenként, hanem egyszerre hajtsuk végre. C:> DEBUG csillag.com 44 -r AX=0000 BX=0000 DS=xxxx ES=xxxx xxxx:0100 B402 -u xxxx:0100 B402 xxxx:0102 B22A xxxx:0104 CD21 xxxx:0106 CD20 xxxx:0108 0000 xxxx:010A 0000 xxxx:010C 0000 xxxx:010E 0000 xxxx:0110 0000 xxxx:0112 0000 xxxx:0114 0000 xxxx:0116 0000 xxxx:0118 0000 xxxx:011A 0000 xxxx:011C 0000 xxxx:011E 0000 -t AX=0200 BX=0000 DS=xxxx ES=xxxx xxxx:0102 B402 -t AX=0200 BX=0000 DS=xxxx ES=xxxx xxxx:0104 CD21 -p * AX=022A BX=0000 DS=xxxx ES=xxxx xxxx:0106 CD20 -p CX=0008 DX=0000 SP=FFFE SS=xxxx CS=xxxx IP=0100 MOV AH,02 MOV MOV INT INT ADD ADD ADD ADD ADD ADD ADD ADD ADD ADD ADD ADD BP=0000 SI=0000 DI=0000 NV UP EI PL NZ NA PO NC AH,02 DL,2A 21
20 [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL CX=0008 DX=0000 SP=FFFE SS=xxxx CS=xxxx IP=0100 MOV DL,2A BP=0000 SI=0000 DI=0000 NV UP EI PL NZ NA PO NC CX=0008 DX=002A SS=xxxx CS=xxxx INT 21 SP=FFFE IP=0100 BP=0000 SI=0000 DI=0000 NV UP EI PL NZ NA PO NC CX=0008 DX=002A SS=xxxx CS=xxxx INT 20 SP=FFFE IP=0100 BP=0000 SI=0000 DI=0000 NV UP EI PL NZ NA PO NC Program terminated normally -q C:> 5.43 3 Példa Írjuk meg az első példában szereplő programot gépi kódban. Ebben az esetben a programot mint byte sorozatot visszük be. C:> DEBUG -e 100 b4 02 b2 2a cd 21 cd 20 -u xxxx:0100 B402 MOV AH,02 xxxx:0102 B22A MOV DL,2A xxxx:0104 CD21 INT 21 xxxx:0106 CD20 INT 20 xxxx:0108 0000 ADD [BX+SI],AL xxxx:010A 0000 ADD [BX+SI],AL xxxx:010C 0000 ADD [BX+SI],AL xxxx:010E 0000 ADD [BX+SI],AL xxxx:0110 0000 ADD [BX+SI],AL xxxx:0112 0000 ADD [BX+SI],AL xxxx:0114 0000 ADD [BX+SI],AL
xxxx:0116 0000 ADD [BX+SI],AL xxxx:0118 0000 ADD [BX+SI],AL xxxx:011A 0000 ADD [BX+SI],AL xxxx:011C 0000 ADD [BX+SI],AL xxxx:011E 0000 ADD [BX+SI],AL -q 45 C:> 46 6. Fejezet Első programok Ez a fejezet néhány egyszerű programot mutat be részletes magyarázattal. A magyarázat néha nagyon részletes és több különböző variációt mutat be, hogy a teljesen kezdők is megszerezzék a szükséges alapokat bonyolultabb programok megı́rásához. 6.1 Első program Nézzük meg az első assembly programot NASM assemblerre ı́rva. A program az 61 táblán látható Ez a program az egyik legkisebb program amit ı́rni lehet Microsoft Window alatt.1 1 2 org 100h INT 20h 6.1 tábla: Első program Az 1. sor nem assembly utası́tás, hanem azt jelöli, hogy a program a 100-as hexadecimális cı́men kezdődik. A hexidecimális számot a szám után ı́rt kis ‘h’ betű jelenti Ha a ‘h’ betű nem szerepel 100-as szám
után, akkor a program a 100-as decimális cı́men kezdődik, ami 64 hexadecimális számnak felelne meg és ez komoly hiba! A magyarázat arra, hogy miért a 100-as hexadecimális cı́men kell kezdődjön a program a 8.4 fejezetben található A 2. sor egy szoftveres megszakı́tást hı́v meg Az INT az utası́tás mı́g a “20h” a megszakı́tás számát jelenti. Ez a szám is hexadecimálisan van megadva Természetesen, ha akarjuk, decimálisan is megadható a megszakı́tás száma. Ez a példa a 62 táblán látható Az “INT 20h” megszakı́tás arra való, hogy egy COM programból kilépjünk. A kilépés azt jelenti, hogy visszatérünk az operációs rendszerhez. 1 2 org 100h INT 32 6.2 tábla: Első program egy változata Ha a programot az elso.asm file-ba mentettük, akkor a lefordı́tása a NASM segı́tségével a következő sor begépelésével lehetséges: 1 Lehet kisebb programot is ı́rni, de ennek
most nincs jelentősége. 47 C:> nasm -o elso.com elsoasm A NASM alap esetben “COM” programokat fordı́t, ı́gy a tı́pust nem kell megadni. A -o opcióval azt adjuk meg, hogy a fordı́tás eredményét milyen file-ba ı́rjuk. Az utolsó paraméter adja meg, hogy melyik assembly forrás file-t kell lefordı́tani. A NASM program paramétereinek részletes listáját a 4 fejezet tartalmazza. A fordı́tás eredménye egy bináris file lesz, mely csak két byte-ot (!) fog tartalmazni. A file tartalma hexadecimális formában: CD 20 Ez a példa azt mutatja, hogy az INT utası́tás gépi kódja a CD hexadecimális érték. Az is látható, hogy az “org 100h” sorból nem generálódik bináris kód, hiszen ez a sor csak azt jelöli, hogy a program milyen cı́men kezdődjön a memóriában. Mivel minden COM program a 100h cı́men kezdődik a memóriában, ezért semmilyen extra utası́tásra nincs szükség a bináris
programban. Mit jelent az, hogy a program a 100 hexadecimális cı́men kezdődik? Nézzük meg a programot a DEBUG program segı́tségével. (A DEBUG program használatát a 5 fejezetben tárgyaltuk) C:> DEBUG elso.com -r AX=0000 BX=0000 CX=0002 DX=0000 DS=1481 ES=1481 SS=1481 CS=1481 1481:0100 CD20 INT 20 -q SP=FFFE IP=0100 BP=0000 SI=0000 DI=0000 NV UP EI PL NZ NA PO NC A példában az látható, hogy a programot az 1481 szegmensre tölti be az operációs rendszer. A szegmens cı́m lehet más is! Ami ennél fontosabb, hogy az offszet cı́m éppen a 100 hexadecimális cı́m! 6.2 Egy karakter kinyomtatása A következő program egy csillag karaktert nyomtat ki. A program listája az 63 táblán látható 1 2 3 4 5 org 100h MOV AH, 2 MOV DL, 2ah INT 21h INT 20h 6.3 tábla: Egy karakter kinyomtatására szolgáló program A program nagyon egyszerű, mivel az 2. és 3 sor az INT 21h szoftveres megszakı́tás paramétereit állı́tja
be. Ebben az esetben az AH regiszter határozza meg a megszakı́tás által végrehajtandó műveletet és a DL regiszter adja a kinyomtatandó karakter ASCII kódját. A MOV utası́tás a jobb oldali argumentum értékét átmásolja a bal oldali argumentumba. A 2 sorban azt adjuk meg, hogy az AH regiszter értéke a 2-es szám legyen. Ez lényegében megfelel az értékadás műveletnek más programozási nyelvben. A 3 sorban a DL regiszternek a 2A hexadecimális számot adjuk meg. A 2A hexadecimális szám a csillag (‘*’) karakternek felel meg. (Lásd az A függelék) A 4.21 bekezdésben azt láttuk, hogy egy byte adatot számmal és karakterrel is megadhatunk Ráadásul a karaktert egyszeres (’) vagy kétszeres (”) aposztrofok között is megadhatjuk. A 64 táblán látható programban a nyomtatandó karakter nem számmal, hanem ténylegesen karakterként van megadva. Ezt az utóbbi ı́rásmódot érdemes
használni, mert ebben az esetben egyértelmű, ASCII táblázat használata nélkül, hogy melyik karaktert akarjuk kinyomtatni. 48 1 2 3 4 5 org 100h MOV AH, 2 MOV DL, ’*’ INT 21h INT 20h 6.4 tábla: Alternatı́va a második programra 6.3 Egy szöveg kinyomtatása A harmadik program egy szöveget nyomtat ki a képernyőre. A program listája az 65 táblán látható 1 2 3 4 5 6 org 100h MOV AH, 9 MOV DX, adat INT 21h INT 20h adat: db ’HELLO$’ 6.5 tábla: Egy szöveg kinyomtatására szolgáló program Ebben a programban is az INT 21h szoftveres megszakı́tást kell használni. Az AH regiszter most is megszakı́tás által végrehajtandó műveletet határozza meg. Mivel most egy egész sor karaktert kell kinyomtatni a DL regiszter nem elegendő, de a DX regiszter is csak két byte-nyi karaktert tud tárolni. Ebben az esetben a DX regiszter a karaktersorozat cı́mét tartalmazza. Az assembly programban szerencsére nem kell
pontosan megadni a cı́met Miért szerencse? Mert ahhoz, hogy pontosan megadjuk az adat cı́mét minden assembly utası́tás esetén tudnunk kellene, hogy hány byte-os gépi kód generálódik belőle és ezek segı́tségével kellene kiszámolnunk az aktuális cı́met. A programban szimbólikusan lehet megadni a karaktersorozat cı́mét. Maga a karaktersorozat a program végére került A karaktersorozat a 4.21 bekezdésnek megfelelően van definiálva: egy cı́m, utána egy kettőspont, a db kulcsszó és egyszeres aposztrofok között maguk a karakterek Az utolsó karakternek a dollár jelnek (‘$’) kell lennie Ez a dollár jel zárja le a szöveget.2 Ennek a speciális karakternek a segı́tségével állapı́tja meg a rendszert, hogy meddig kell a byte-okat kinyomtatni.3 Ha a dollár jelet nem adnánk meg, akkor a rendszer addig nyomtatná a karaktereket amı́g el nem ér egy dollár jelet. Elvileg előfordulhat, ha nincs
dollár jel a memóriában, hogy az egész memóriát kinyomtatja a program és soha nem áll le. Ezt a programot is érdemes megnézni a DEBUG programban, hogy több dolgot is megnézzünk: C:> DEBUG hello.com -u xxxx:0100 B409 xxxx:0102 BA0901 xxxx:0105 CD21 xxxx:0107 CD20 xxxx:0109 48 xxxx:010A 45 xxxx:010B 4C xxxx:010C 4C xxxx:010D 4D MOV MOV INT INT DEC INC DEC DEC DEC AH,02 DX,109 21 20 AX BP SP SP DI 2 Korábbi tanulmányokból ismert lehet, hogy a C programozási nyelvben zéró áll a szöveg végén. másik stratégia, hogy megadjuk a karaktereket és a karakterek számát. Ezt módszert a Pascal programozási nyelv használja. 3A 49 xxxx:010E xxxx:0110 xxxx:0112 xxxx:0114 xxxx:0116 xxxx:0118 xxxx:011A xxxx:011C xxxx:011E -d 100 xxxx:0010 xxxx:0020 xxxx:0030 xxxx:0040 xxxx:0050 xxxx:0060 xxxx:0070 xxxx:0080 -q 2400 0000 0000 0000 0000 0000 0000 0000 0000 B4 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 AND ADD ADD ADD ADD ADD ADD
ADD ADD BA 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 CD 00 00 00 00 00 00 00 AL,00 [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL 21 00 00 00 00 00 00 00 CD-20 00-00 00-00 00-00 00-00 00-00 00-00 00-00 48 00 00 00 00 00 00 00 45 00 00 00 00 00 00 00 4C 00 00 00 00 00 00 00 4C 00 00 00 00 00 00 00 4D 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .! HELLO$ . . . . . . . A következő észrevételeket tehetjük: • Az assembler a fordı́tás során kiszámolta a karaktersorozat cı́mét, ami jelenleg a 109h cı́mtől indul. • A cı́m két byte-ból áll és ezt a rendszer “little-endian” módon tárolja: BA 09 01. • A DEBUG program teljesen azonos módon kezeli az adatot és a programot is. Igazából nincs is különbség, hiszen mind a kettő egy byte sorozat. Például látható, hogy az “unassemble” parancs során a karaktersorozatot is
utası́tásként értelmezi a DEBUG program. Ezért van az, hogy az adatokat és a programot nem szabad keverni. Erre a hibára egy példát mutat az 66 tábla Ha ezt a programot megnézzük a DEBUG programban, akkor azt fogjuk tapasztalni, hogy az adatot is mint utası́tásokat fogja értelmezni a DEBUG és ráadásul értelmetlen kódot kapunk. A példa azt mutatja, hogy az adatokat és a programot nem szabad összekeverni! 1 2 3 4 5 6 org 100h adat: db ’HELLO$’ MOV AH, 9 MOV DX, adat INT 21h INT 20h 6.6 tábla: Hibás program C:> DEBUG hello.com -u xxxx:0100 48 xxxx:0101 45 xxxx:0102 4C xxxx:0103 4C xxxx:0104 4D xxxx:0105 24B4 xxxx:0107 09BA0001 xxxx:010B CD21 xxxx:010D CD20 xxxx:010F 0000 xxxx:0111 0000 xxxx:0113 0000 DEC INC DEC DEC DEC AND OR INT INT ADD ADD ADD AX BP SP SP DI AL,B4 [BP+SI+0100],DI 21 20 [BX+SI],AL [BX+SI],AL [BX+SI],AL 50 xxxx:0115 xxxx:0117 xxxx:0119 xxxx:011B xxxx:011D xxxx:011F -q 0000 0000 0000 0000 0000 0000 ADD
ADD ADD ADD ADD ADD [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL [BX+SI],AL 6.4 Egy karakter beolvasása A 6.7 táblázat egy nagyon egyszerű programot mutat, ami beolvas egy karaktert a felhasználótól és újra kinyomtatja. A karakter beolvasásra is használható az operációs rendszer megszakı́tása és ekkor a funkció kódnak 1-nek kell lennie. A funkicó kódot a 2 sorban állı́tjuk be A 3 sorban az INT 21h megszakı́tás lefutása után a beolvasott karakter ASCII kódja az AL regiszterben lesz. Ezt akarjuk újra kinyomtatni, ezért másoljuk át a DL regiszterbe a 4. sorban, majd az 5 sorban az új funkció kódot is beállı́tjuk Végül az INT 21h megszakı́tás meghı́vásával végezzük a nyomtatást a 6. sorban 1 2 3 4 5 6 7 org 100h MOV AH, 1 INT 21h MOV DL, AL MOV AH, 2 INT 21h INT 20h 6.7 tábla: Egy karakter beolvasása és kinyomtatása 51 52 7. Fejezet Assembly nyelv utası́tásai
Ebben a fejezetben az assembly utası́tások közül a legfontosabbakat tekintjük át. A fontossági sorrendet úgy próbáltam megállapı́tani, hogy amelyeket a leggyakrabban használjuk a programokban, vagy amelyekre leginkább szükség lehet a tanulás során. Azt is meg kell jegyezni, hogy a fejezetben csak a 8086 processzor utası́táskészletéből kerültek kiválasztásra az itt felsorolt utası́tások. Ennek az a magyarázata, hogy véleményem szerint ezek az utası́tások elegendőek a fontosabb koncepciók bemutatására. A fejezet végén a be nem mutatott parancsokat azért felsoroljuk Szintakszis: • mem: memória cı́m • reg: regiszter • op: memória cı́m vagy regiszter 53 7.1 Adatmozgató utası́tások 7.11 MOV Szintakszis MOV op1, op2 Az op2 tartalmát az op1-be ı́rja, felülı́rva annak korábbi értékét. Az utası́tás operandusai a következők lehetnek: MOV MOV MOV MOV MOV regiszter,
számadat regiszter, regiszter regiszter, memória memória, számadat memória, regiszter Mint látható, az nem fordulhat elő, hogy egy utası́tás mindkét operandusa a memóriára hivatkozzon! Egy utası́tás csak egyszer hivatkozhat a memóriára! Szintén fontos, hogy közvetlenül nem lehet egyik szegmens regiszterbe sem ı́rni, csak áttételesen, például: MOV AX, 1234h MOV DS, AX Példák MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV MOV AX, 1234 AX, ES ES, AX AL, 0ffH AH, AL AL, [BX] [SI], DL AX, [BX] [DI], BP AX, [0ffffh] [0200h], DX [ES:0100h], CX 7.12 XCHG Szintakszis XCHG op1, op2 Az op1 és op2 tartalmát felcseréli. Mind a két operandusnak azonos méretűnek kell lennie, 8 vagy 16 bitesnek. Az utası́tás előnye, hogy a cserét segédváltozó nélkül hajtja végre Az operandusok cseréje általában a következőképpen történik: MOV temp, op2 MOV op2, op1 MOV op1, temp 54 Példák XCHG XCHG XCHG XCHG
AL, BL CX, DX DH, [4351h] [DS:3333h], BP 7.13 XLAT Szintakszis XLAT A BX regiszter egy maximum 256 bytes táblázatra mutat és az AL regiszterben levő értéknek megfelelő elemet veszi ki a táblázatból és tölti be az AL regiszterbe. Az utası́tás tulajdonképpen egy konverziót hajt végre. Példák MOV BX, tabla MOV AL, 0fh XLAT MOV DL, AL MOV AH, 2 INT 21h . . tabla: db ’0123456789ABCDEF’ 7.14 LDS Szintakszis LDS reg, mem A utası́tás második operandusa által megadott memóriahelyen található 4 byte-os mutatót betölti a DS szegmens regiszterbe és az első operandusként megadott regiszterbe. Ilyen módon egyetlen utası́tással lehet betölteni egy másik szegmensben levő változó cı́mét. Az utası́tás végrehajtása után azonnal cı́mezhető a memórapozı́ció. Példák LDS BX, [valtozo] MOV AX, [DS:BX] 7.15 LES Szintakszis LES reg, mem 55 A utası́tás második operandusa által megadott
memóriahelyen található 4 byte-os mutatót betölti az ES szegmens regiszterbe és az első operandusként megadott regiszterbe. Ilyen módon egyetlen utası́tással lehet betölteni egy másik szegmensben levő változó cı́mét. Az utası́tás végrehajtása után azonnal cı́mezhető a memórapozı́ció. Példák LES BX, [valtozo] MOV AX, [ES:BX] 7.16 LEA Szintakszis LEA reg, mem Az utası́tás első operandusaként megadott regiszterbe betölti a második operandus offszet cı́mét. A cı́met futás közben számolja ki, nem előre, illetve bármilyen cı́mzési mód használható. Példák LEA BX, [valtozo+BX] LEA DI, [BX+4] LEA DI, [AX+CX] 7.17 PUSH Szintakszis PUSH op A két byte-os operandust a veremre helyezi az utası́tás. Az utası́tás egyenértékű a következő két utası́tással: SUB SP, 2 MOV [SS:SP], op Az operandus lehet szegmens regiszter vagy bármilyen 16 bites regiszter, memória cı́m
vagy konstans szám adat. Az utası́tás fordı́tottja a POP utası́tás Példák PUSH PUSH PUSH PUSH AX DS [0003h] 7 ; [DS:0003] tartalma a veremre ; 0007h word a veremre 7.18 PUSHF Szintakszis PUSHF 56 A státusz regiszter teljes tartalmát a verem tetejére másolja. Az utası́tás egyenértékű a következő két utası́tással: SUB SP, 2 MOV [SS:SP], statusz-regiszter Az utası́tás fordı́tottja a POPF utası́tás, mellyel együtt jól használható olyan esetekben, amikor a státusz regiszter egy-egy vagy több bitjét akarjuk módosı́tani vagy lekérdezni. Példák PUSHF AND [SS:SP], 1 POPF ; statusz regiszter elmentése ; csak az 1-es bitet tartjuk meg, maszkoljuk ; az új értéket a statusz regiszterbe ı́rjuk Tegyük fel, hogy azt szeretnénk megállapı́tani, hogy a carry bit értéke nulla vagy egy: PUSHF POP AX AND AL, 00000001b JZ nulla volt egy volt: . Az első sor elmenti a státusz biteket a vermen, amit
ezután letöltünk az AX regiszterbe. Az AX regiszter alsó byte-jának a nulladik bitje felel meg a carry bitnek ı́gy a harmadik sorban ezt a bitet maszkoljuk. Ezután ugró utası́tást végzünk attól függően, hogy a maszkolás eredménye zérus vagy nem zérus volt. Persze ez a “komplikált” utası́tás sorozat egyszerűen helyettesı́thető a JC cim és az JNC cim utası́tásokkal, melyek attól függően ugranak a megadott cı́mre, hogy a carry bit egy vagy éppen nulla volt. 7.19 PUSHA Szintakszis PUSHA Az utası́tás 8 regisztert tölt fel a veremre. A veremmutatót (SP) 16-al csökkenti, majd a következő sorrendben a regiszterek tartalmát felmásolja a veremre: AX, CX, DX, BX, SP, BP, SI, DI. Az SP regiszter esetén az utası́tás végrehajtása előtti értéket menti el! Az utası́tás fordı́tottja a POPA utası́tás. 7.110 POP Szintakszis POP op Az utası́tás a veremmutató (SP) regiszter által
mutatott word értéket az op operandusba ı́rja bele. Az utası́tás egyenértékű a következő két utası́tással: MOV op, [SS:SP] ADD SP, 2 57 Az operandus lehet szegmens regiszter, bármilyen 16 bites regiszter vagy memória cı́m. Fontos, hogy az operandus nem lehet a CS regiszter. Ez azért van, mert ha ilyen módon engednénk felülı́rni a CS regisztert akkor ezzel áttételesen szegmensek közötti ugró utası́tást engednénk meg, hiszen a következő utası́tásra a CS:IP regiszter páros mutat. Példák POP POP POP POP AX DS [0003h] [ES:0003h] 7.111 POPF Szintakszis POPF A státusz regiszter teljes tartalmát felülı́rja a verem tetején tárolt 16 bites értékkel. Az utası́tás egyenértékű a következő két utası́tással: MOV statusz-regiszter, [SS:SP] ADD SP, 2 Az utası́tás a PUSHF utası́tással együtt használható jól olyan esetben, amikor a státusz regiszter egy-egy vagy több
bitjét akarjuk módosı́tani vagy lekérdezni. Példák PUSHF AND [SS:SP], 1 POPF ; statusz regiszter elmentése ; csak az 1-es bitet tartjuk meg, maszkoljuk ; az új értéket a statusz regiszterbe ı́rjuk 7.112 POPA Szintakszis POPA Az utası́tás 8 regisztert állı́t helyre a vermen eltárolt értékekből. Az utası́tás a PUSHA fordı́tottja és megfelel a következő utası́tásoknak: POP POP POP ADD POP POP POP POP DI SI BP SP, 2 BX DX CX AX 58 7.113 LAHF Szintakszis LAHF Az utası́tás betölti az AH regiszter megfelelő pozı́ciójába a SF, ZF, AF, PF és CF státusz biteket. A 71 tábla mutatja a bitek helyét az AH regiszterben. 7. SF 6. ZF 5. 4. AF 3. 2. PF 1. 0. CF 7.1 tábla: LAHF utası́tás után az AH regiszter tartalma Példák LAHF SHR AH, 6 AND AH, 1 ; AH értéke 1 vagy 0 a ZF bittol fuggoen 7.114 SAHF Szintakszis SAHF Az utası́tás az AH regiszter megfelelő bitjeivel felülı́rja a SF, ZF,
AF, PF és CF státusz biteket. A 71 tábla mutatja a bitek helyét az AH regiszterben. 59 7.2 Matematikai utası́tások Az ebben a fejezetben tárgyalt utası́tások esetén az is feltüntetésre kerül, hogy az utası́tás hogyan befolyásolja a státusz regiszter egyes bitjeit. A táblázatokban az ‘x’ jelzi a megváltoztatott biteket, mı́g a ‘?’ jel azt jelenti, hogy a bit meghatározatlan, vagyis lehet 1 és 0 is. 7.21 INC Szintakszis INC op Az utası́tás az operandus értékéhez 1-et ad hozzá a CF státusz bit megváltoztatása nélkül. Érintett státusz bitek OF DF IF x TF SF ZF x x AF x PF x CF Példák INC INC INC INC AX BL [100h] [ES:220h] 7.22 DEC Szintakszis DEC op Az utası́tás az operandus értékéből 1-et von le a CF státusz bit megváltoztatása nélkül. Érintett státusz bitek OF DF IF x TF SF ZF x x Példák DEC DEC DEC DEC AX BL [100h] [ES:220h] 7.23 ADD Szintakszis ADD op1, op2
60 AF x PF x CF Az utası́tás az op1 operandushoz adja az op2 operandus értékét és az eredményt az op1-be ı́rja. Az operandusok méretének meg kell egyeznie, két 8 bites vagy két 16 bites értéket lehet csak összeadni. Érintett státusz bitek OF DF IF x TF SF ZF x x AF x PF x CF x Példák ADD ADD ADD ADD ADD AX, BX [adat], CX CX, [valtozo] BL, 4 SI, [BP+8] 7.24 ADC Szintakszis ADC op1, op2 Az utası́tás az op1 operandushoz adja az op2 operandus értékét és a Carry flag (CF) értékét is, majd az eredményt az op1-be ı́rja. Az operandusok méretének meg kell egyeznie, két 8 bites vagy két 16 bites értéket lehet csak összeadni. Érintett státusz bitek OF DF IF x TF SF ZF x x AF x Példák ADC AX, BX ADC [adat], CX ADC CX, [valtozo] ADC BL, 4 ADC SI, [BP+8] . ; összeadás átvitellel MOV CX, 0 MOV AL, 80h MOV AH, 80h MOV CL, AL ADD CL, AH ADC CH, 0 ; eredmény: CX tartalma 100h lesz 7.25 SUB
Szintakszis SUB op1, op2 61 PF x CF x Az utası́tás az op1 operandusból kivonja az op2 operandus értékét és az eredményt az op1-be ı́rja. Az operandusok méretének meg kell egyeznie, két 8 bites vagy két 16 bites értéket lehet csak összeadni. Érintett státusz bitek OF DF IF x TF SF ZF x x AF x PF x CF x Példák SUB SUB SUB SUB SUB AX, BX [adat], CX CX, [valtozo] BL, 4 SI, [BP+8] 7.26 SBB Szintakszis SBB op1, op2 Az utası́tás az op1 operandusból kivonja az op2 operandus értékét és a Carry flag (CF) értékét is, majd az eredményt az op1-be ı́rja. Az operandusok méretének meg kell egyeznie, két 8 bites vagy két 16 bites értéket lehet csak összeadni. Érintett státusz bitek OF DF IF x TF SF ZF x x AF x PF x CF x Példák SBB SBB SBB SBB SBB AX, BX [adat], CX CX, [valtozo] BL, 4 SI, [BP+8] 7.27 MUL Szintakszis MUL op Ez az utası́tás 8 vagy 16 bites előjel nélküli számok
közötti szorzást hajt végre. Az operandus határozza meg, hogy 8 vagy 16 bites számokat szoroz-e össze. 8 bites szorzás esetén az AL regiszter tartalmát összeszorozza az op operandussal és az eredményt az AX regiszterben tárolja el. Ha az eredményben az AH regiszter tartalma zérus akkor a CF és OF státusz bitek értéke zérus lesz, egyébként 1. 16 bites szorzás esetén az AX regiszter tartalmát összeszorozza az op operandussal és az eredményt az DX:AX regiszterekben tárolja el. Az operandusok előjel nélküli számok és bármilyen cı́mzési mód használható Ha az eredményben az DX regiszter tartalma zérus akkor a CF és OF státusz bitek értéke zérus lesz, egyébként 1. 62 Érintett státusz bitek OF DF IF x TF SF ZF ? ? AF ? PF ? CF x Példák MOV AL, 2 MUL 4 . adat: db 4 . MOV AL, 2 MUL [adat] ; AX tartalma 8 lesz ; AX tartalma 8 lesz 7.28 IMUL Szintakszis IMUL op Ez az
utası́tás 8 vagy 16 bites előjeles szorzást hajt végre. Az operandus határozza meg, hogy 8 vagy 16 bites számokat szoroz-e össze. 8 bites szorzás esetén az AL regiszter tartalmát összeszorozza az op operandussal és az eredményt az AX regiszterben tárolja el. Ha az eredményben az AH regiszter tartalma zérus akkor a CF és OF státusz bitek értéke zérus lesz, egyébként 1. 16 bites szorzás esetén az AX regiszter tartalmát összeszorozza az op operandussal és az eredményt az DX:AX regiszterekben tárolja el. Az operandusok előjel nélküli számok és bármilyen cı́mzési mód használható Ha az eredményben az DX regiszter tartalma zérus akkor a CF és OF státusz bitek értéke zérus lesz, egyébként 1. Érintett státusz bitek OF DF IF x TF SF ZF ? ? AF ? PF ? CF x Példák MOV AL, 2 IMUL 4 . adat: db 4 . MOV AL, 2 IMUL [adat] ; AX tartalma 8 lesz ; AX tartalma 8 lesz 7.29 DIV Szintakszis DIV
op Ez az utası́tás előjel nélküli osztást hajt végre. Ha byte, 8 bites, operandust adunk meg az utası́tásban, akkor az AX regiszter tartalmát az operandussal elosztja és a hányadost az AL regiszterben, a maradékot az AH regiszterben tárolja el. Word vagy szó méretű operandus esetén a DX:AX regisztrerek tartalmát 63 elosztja az operandussal majd a hányadost a AX regiszterben és a maradékot a DX regiszterben tárolja el. Ha az osztó zérus vagy a hányados túl nagy, hogy elférjen az AL vagy AX regiszterben, akkor az INT 0 megszakı́tás hı́vódik meg. Érintett státusz bitek OF DF IF ? TF SF ZF ? ? AF ? PF ? CF ? Példák MOV AX, 12 DIV 10 ; AL = 1, AH = 2 7.210 IDIV Szintakszis IDIV op Ez az utası́tás előjeles osztást hajt végre. Ha byte, 8 bites, operandust adunk meg az utası́tásban, akkor az AX regiszter tartalmát az operandussal elosztja és a hányadost az AL regiszterben, a maradékot az
AH regiszterben tárolja el. Word vagy szó méretű operandus esetén a DX:AX regisztrerek tartalmát elosztja az operandussal majd a hányadost a AX regiszterben és a maradékot a DX regiszterben tárolja el. Ha az osztó zérus vagy a hányados túl nagy, hogy elférjen az AL vagy AX regiszterben, akkor az INT 0 megszakı́tás hı́vódik meg. Érintett státusz bitek OF DF IF ? TF SF ZF ? ? AF ? PF ? CF ? Példák MOV AX, 12 IDIV 10 ; AL = 1, AH = 2 7.211 NEG Szintakszis NEG op Az operandust nullából kivonja és hozzáad egyet, majd az eredményt eltárolja az operandusban, felülı́rva annak korábbi értékét. Az utası́tás lényegében kettes komplemensét képzi az operandusnak Ha az eredmény operandus zérus, akkor a CF bit értéke zérus lesz, egyébként pedig 1. Érintett státusz bitek OF DF IF x TF SF ZF x x 64 AF x PF x CF x Példák NEG AX NEG BL NEG [DS:100h] 7.212 CBW Szintakszis CBW Az
utası́tás az AL regiszterben található előjeles byte-ot előjeles szóvá alakı́tja az AX regiszterben. Ez úgy történik, hogy az AL regiszter legnagyobb helyiértékű bitjét bemásolja az AH regiszter minden bitjébe. Assembly-ben ez a következő utası́tásoknak felel meg: TEST AL, 128 JNZ egy zerus: MOV AH, 00h JMP vege egy: MOV AH, 0FFh vege: Az utası́tás egy 8 bites szám előkészı́tését végzi előjeles osztáshoz. 7.213 CWD Szintakszis CWD Az utası́tás az AX regiszterben található előjeles word-ot előjeles értékké alakı́tja az DX:AX regiszterekben. Az utası́tás egy 16 bites szám előkészı́tését végzi előjeles osztáshoz 65 7.3 Bitforgató és bitléptető utası́tások Az alábbi utası́tásokban az op operandus lehet 8 vagy 16 bites és tetszőleges cı́mzési módot alkalmazhatunk. Az utası́tásoknál a “kilépő” bit minden esetben megjelenik Carry bitben Az
utası́tások két csoportba sorolhatók: • Bitforgató, rotáló utası́tások: RCL, RCR, ROL, ROR • Bitléptető, shiftelő utası́tások: SAL, SAR, SHL, SHR 7.31 RCL Szintakszis RCL op, 1 RCL op, CL RCL op, szamlalo CF Az utası́tás balra forgatja az operandus értékét olyan módon, hogy a legnagyobb helyiértékű bit a Carry bitbe kerül, a Carry bit pedig a legkisebb helyiértékű bit helyébe. Minden más bit eggyel balra tolódik Az utası́tás működését a 7.1 ábra is szemlélteti Az utası́tás képes a forgatást egyszer vagy többször végrehajtani, a második operandustól függően. 7.1 ábra: Az RCL utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF AF PF CF x Ha a CF bit az operandus magas bitjével egyenlő, akkor az OF bit értéke zérus lesz, mı́g ha nem egyenlő akkor 1 lesz. Példák RCL RCL RCL RCL AL, 1 AX, 3 BX, CL [DS:2456h], CL Az utası́tás
használatára a 9.1 fejezet mutat egy példa programot 7.32 RCR Szintakszis RCR op, 1 RCR op, CL RCR op, szamlalo 66 CF Az utası́tás jobbra forgatja az operandus értékét olyan módon, hogy a Carry bit a legnagyobb helyiértékű bitbe másolódik, a legkisebb helyiértékű bit pedig a Carry bitbe kerül. Minden más bit eggyel jobbra tolódik. Az utası́tás működését a 72 ábra is szemlélteti Az utası́tás képes a forgatást egyszer vagy többször végrehajtani, a második operandustól függően. 7.2 ábra: Az RCR utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF AF PF CF x Példák RCR RCR RCR RCR AL, 1 AX, 3 BX, CL [DS:2456h], CL 7.33 ROL Szintakszis ROL op, 1 ROL op, CL ROL op, szamlalo CF Az utası́tás az első operandust balra rotálja “önmagán” és a “kicsorgó” legmagasabb helyiértékű bit kerül a Carry bitbe. A működést a 73 ábra mutatja be Az
utası́tás képes a forgatást egyszer vagy többször végrehajtani, a második operandustól függően. 7.3 ábra: Az ROL utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF Példák ROL AL, 1 67 AF PF CF x ROL AX, 3 ROL BX, CL ROl [DS:2456h], CL 7.34 ROR Szintakszis ROR op, 1 ROR op, CL ROR op, szamlalo CF Az utası́tás az első operandust jobbra rotálja “önmagán” és a “kicsorgó” legalacsonyabb helyiértékű bit kerül a Carry bitbe. A működést a 74 ábra mutatja be Az utası́tás képes a forgatást egyszer vagy többször végrehajtani, a második operandustól függően. 7.4 ábra: Az ROR utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF AF PF CF x Példák ROR ROR ROR ROR AL, 1 AX, 3 BX, CL [DS:2456h], CL 7.35 SAL, SHL Szintakszis SAL SAL SAL SHL SHL SHL op, op, op, op, op, op, 1 CL szamlalo 1 CL szamlalo Az utası́tás balra léptet
minden bitet. A legmagasabb helyiértékű bit a Carry bitbe kerül, mı́g a legalacsonyabb helyiértékű bit törlődik Az utası́tás működését a 75 ábra mutatja be Az utası́tás képes a bit léptetést egyszer vagy többször végrehajtani, a második operandustól függően. Érdemes megjegyezni, hogy ha egyszer hajtjuk végre az utası́tást, akkor ez megfelel a 2-vel való szorzásnak. Az esetleges szorzási túlcsordulás a Carry bitben jelenik meg 68 CF 0 7.5 ábra: Az SAL utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF x x AF PF CF x Példák SHL SHL SHL SHL AL, 1 AX, 3 BX, CL [DS:2456h], CL Az utası́tás használatára a 8.7 fejezet mutat néhány példát 7.36 SAR Szintakszis SAR op, 1 SAR op, CL SAR op, szamlalo CF Az utası́tás jobbra léptet minden bitet. A legalacsonyabb helyiértékű bit a Carry bitbe kerül, mı́g a legmagasabb helyiértékű bit
ismétlődik. Az utası́tás működését a 76 ábra mutatja be Az utası́tás képes a bit léptetést egyszer vagy többször végrehajtani, a második operandustól függően. Érdemes megjegyezni, hogy ha egyszer hajtjuk végre az utası́tást, akkor ez megfelel a 2-vel való előjeles osztásnak. A hányados az operandusban marad, mı́g a maradék a Carry bitbe kerül 7.6 ábra: Az SAR utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF x x 7.37 SHR Szintakszis SHR op, 1 69 AF PF CF x SHR op, CL SHR op, szamlalo CF Az utası́tás jobbra léptet minden bitet. A legalacsonyabb helyiértékű bit a Carry bitbe kerül, mı́g a legmagasabb helyiértékű bit törlődik. Az utası́tás működését a 77 ábra mutatja be Az utası́tás képes a bit léptetést egyszer vagy többször végrehajtani, a második operandustól függően. Érdemes megjegyezni, hogy ha egyszer hajtjuk
végre az utası́tást, akkor ez megfelel a 2-vel való előjel nélküli osztásnak. A hányados az operandusban marad, mı́g a maradék a Carry bitbe kerül 0 7.7 ábra: Az SHR utası́tás működése Érintett státusz bitek OF DF IF x TF SF ZF x x AF PF CF x Példák Az alábbi programrészlet azt ellenőrzi, hogy a legalacsonyabb helyiértékű bit zérus vagy egy: . SHR AL, 1 JNC zerus ; kód végrehajtása ha a bit 1 JMP vege zerus: ; kód végrehajtása ha a bit 0 vege: . 70 7.4 Logikai utası́tások 7.41 AND A 0 1 0 1 B 0 0 1 1 A AND B 0 0 0 1 7.2 tábla: AND utası́tás igazság táblája 7.42 OR A 0 1 0 1 B 0 0 1 1 A OR B 0 1 1 1 7.3 tábla: OR utası́tás igazság táblája 7.43 XOR A 0 1 0 1 B 0 0 1 1 A XOR B 0 1 1 0 7.4 tábla: XOR utası́tás igazság táblája 7.44 NOT A 0 1 NOT A 1 0 7.5 tábla: NOT utası́tás igazság táblája 7.45 TEST 7.46 CMP 71 Feltétel = nem = >
>= < <= Előjeles JE, JZ JNE, JNZ JG, JNLE JGE, JNL JL, JNGE JLE, JNG Előjel nélkül JE, JZ JNE, JNZ JA, JNBE JAE, JNB JB, JNAE JBE, JNA 7.6 tábla: Feltételes utası́tások 7.5 Vezérlésátadó utası́tások 7.51 JMP 7.52 Feltételes utası́tások Az assembly programozási nyelvben nincsennek magasabb szintű programozási konstrukciók, például ciklus. Minden ilyen szerkezetet feltételes ugrásokkal kell megvalósı́tani A feltételes ugrás azt jelenti, hogy a program futása nem a következő utası́tással folytatódik, hanem a feltételes ugrás által megadott cı́men, ha a feltétel teljesül. Itt a feltétel nem jelenti azt, hogy az utası́tás valamilyen összehasonlı́tást végezne, csak annyit, hogy egy korábbi utası́tás által beállı́tott státusz bit értéke alapján történhet ugrás. Például a JE cı́m utası́tás azt jelenti, hogy ha a ZF (zérus státusz bit) értéke
egy (1) akkor a ’cı́m’-en folytatódik a program végrehajtása. Ha a zérus státusz bit értéke nulla (0) akkor a JE cı́m utası́tás utáni utası́tással folytatódik a program végrehajtása. A 76 táblázat sorolja fel a különböző feltételes ugró utası́tásokat. Fontos, hogy lehetőség szerint ne keverjük az előjeles és előjel néküli feltételes ugró utası́tásokat. 7.53 JCXZ 7.54 LOOP Szintakszis LOOP cim Az utası́tás 1-el csökkenti a CX regiszter értékét és ha az ı́gy kapott érték nem nulla akkor a cim cı́mre adja át a vezérlést. Ha a csökkentés után a CX értéke zérus lesz, akkor a LOOP utası́tás utáni utası́tással folytatja program a végrehajtást. Fontos megérteni, hogy ez az utası́tás egy hátul tesztelő ciklusnak felel meg. Így először mindig a csökkentés következik be és csak utána az ellenőrzés 7.55 LOOPNZ Szintakszis LOOPNZ cim
LOOPNE cim Az utası́tás angol neve: “Loop while Non Zero” vagy “Loop while Not Equal”. Az utası́tás először is megvizsgálja a ZF státusz bit értékét. Ha a státusz bit értéke nulla, akkor csökkenti a CX regiszter értékét 1-el és ha ı́gy a CX regiszter értéke még nem nulla, akkor végrehajtja az ugrást a cim cı́mre. Ellenkező esetben az utası́tás után folytatja a program a végrehajtást. 72 7.56 LOOPZ Szintakszis LOOPZ cim LOOPE cim Az utası́tás angol neve: “Loop while Zero” vagy “Loop while Equal”. Az utası́tás először is megvizsgálja a ZF státusz bit értékét. Ha a státusz bit értéke 1, akkor csökkenti a CX regiszter értékét 1-el és ha ı́gy a CX regiszter értéke még nem nulla, akkor végrehajtja az ugrást a cim cı́mre. Ellenkező esetben az utası́tás után folytatja a program a végrehajtást. Ennél az utası́tásnál a ZF státusz bit
jelöli, hogy miért ér véget a ciklus. Ha CX regiszter lett zérus, akkor a ZF státusz bit értéke 1 lesz, ha pedig az utolsó státusz bit művelet eredménye volt nullától különböző, akkor a ZF státusz bit értéke nulla lesz. 7.57 CALL Szintakszis CALL cim Az utası́tás először elmenti a CALL utası́tást követő utası́tás cı́mét a verem tetején. Ez lesz a függvény visszatérési cı́me. Ezután az argumentumként megadott cı́mre adja át a vezérlést, vagyis a cim cı́men folytatódik a vgérehajtás. Az utası́tással lehet közeli vagy távoli vezérlés átadást végrehajtani. Más szavakkal az argumentum lehet csak offszet vagy szegmens és offszet cı́m. Az utası́tást a függvények meghı́vására használjuk. A függvényből visszatérést a RET utası́tás végzi, lásd 7.58 bekezdés A függvényekről részletesebb leı́rás a 10 fejezetben található
7.58 RET Szintakszis RET vagy RET n Az utası́tás egy függvény végén szokott szerepelni. Feladata, hogy a verem tetejéről levegyen egy visszatérési cı́met, majd a vezérlést átadja erre a cı́mre Fontos megérteni, hogy az utası́tás nem vizsgálja meg a verem tetejét, akármit talál ott az utası́tás azt visszaatérési cı́mnek fogja tekinteni. Az utası́táshoz optcionálisan tartozhat egy argumentum. Ebben az esetben az argumentum egy szám n, ami megadja, hogy a visszatérési cı́m levétele után még hágy byte-ot kell levenni a veremről. Ezeket a levett értékeket eldobjuk. A különböző függvény hı́vás utáni takarı́tási stratégiákról a 1043 bekezdésben olvashatunk. 7.59 INT 73 7.6 String kezelő utası́tások A string kezelő utası́tások memóriablokkokkal végeznek műveletet. A string, tulajdonképpen szöveg, a szöveg pedig nem más mint egy karakter sorozat vagy
byte sorozat és egy byte sorozat amikor a memóriában tároljuk akkor pedig megfelel egy memóriablokknak. Az utası́tások általában a DS:SI, ES:DI és AX regisztereket használják. A regisztereken kı́vül fontos szerepet játszik még a Direction (Irány) státusz bit, mivel ez határozza meg, hogy a művelet során a cı́mekhez hozzáadunk vagy a cı́mekből kivonunk értékeket. A Direction (Irány) státusz bitet a CLD (774 bekezdés) és STD (775 bekezdés) utası́tások állı́tják be. A string kezelő parancsokat részletesen tárgyaljuk a 12. fejezetben 7.61 MOVSB, MOVSW Szintakszis MOVSB MOVSW Az utası́tások angol neve: “MOVe String Byte” illetve “MOVe String Word”. Az utası́tás a DS:SI regiszterek által megcı́mzett byte-ot átmásolja az ES:DI cı́mre. Az SI és DI regiszterek értéke 1-el növekszik ha a Direction státusz bit értéke nulla vagy a regiszterek értéke 1-el csökken, ha a
Direction státusz bit értéke egy. A MOVSW utası́tás csak annyiban különbözik, hogy két byte-ot vagyis egy szót (word) másol át és az SI és DI regiszterek értéke kettővel módosul a Direction státusz bit értékétől függően. Ezek az utası́tások a kivételek közé tartoznak, mivel egy utası́tás kétszer is hozzáfér a memóriához. A másik fontos megjegyzés, hogy ezek az utası́tások csak egy szegmensnyi adatot képesek átmásolni, mivel ha a DI regiszter értéke eléris a FFFFh értéket és 1-el megnöveljük az értékét akkor a nulla értéket kapjuk, ami a szegmens első byte-jára mutat, ı́gy “körbefordulás” következik be. Ez azért van, mert az utası́tás nem módosı́tja a szegmens regisztereket, azokat nem növeli és nem csökkenti. A 8086osnál modernebb processzorokon azért lehet több adatot átmásolni, mivel az ESI és EDI 32 bites regisztereket
használjuk a műveletben. Nagyon fontos, hogy az utası́tás a használt regiszterek kezdeti értékét nem állı́tja be, azt a programozónak kell megadni! Példák Az alábbbi kódrészlet átmásol 4 word méretű adatot: forras: dw 1111h, 2222h, 3333h, 4444h cel: dw 0000h, 0000h, 0000h, 0000h . MOV SI, forras MOV DI, cel MOV CX, 4 CLD ujra: MOVSW LOOP ujra . Ezt a példát érdemes összehasonlı́tani egy egyszerűsı́tett változattal, ami a 7.66 bekezdésben található 74 7.62 CMPSB, CMPSW Szintakszis CMPSB CMPSW Az utası́tások angol neve: “CoMPare String Byte” illetve “CoMPare String Word”. A CMPSB utası́tás a DS:SI regiszterek által megcı́mzett byte-ot összehasonlı́tja az ES:DI által megcı́mzett byte-al. Az SI és DI regiszterek értéke 1-el növekszik ha a Direction státusz bit értéke nulla vagy a regiszterek értéke 1-el csökken, ha a Direction státusz bit értéke egy. Az utası́tások a
státusz biteket is beállı́tja úgy, mintha egy CMP utası́tást (lásd 7.46 bekezdés) hajtottunk volna végre A CMPSW utası́tás hasonlóan működik, de word méretű adatokkal dolgozik, és az SI és DI regiszterek értéke is 2-vel változik. 7.63 LODSB, LODSW Szintakszis LODSB LODSW Az utası́tások angol neve: “LOaD String Byte” illetve “LOaD String Word”. A LODSB utası́tás a DS:SI regiszterek által megcı́mzett byte-ot betölti az AL regiszterbe. Az SI regiszter értéke 1-el növekszik ha a Direction státusz bit értéke nulla vagy az SI regiszter értéke 1-el csökken, ha a Direction státusz bit értéke egy. A LODSW utası́tás csak abban különbözik, hogy a DS:SI regiszterek által megadott cı́mről egy word-öt (2 byte-ot) töltünk be az AX regiszterbe. 7.64 STOSB, STOSW Szintakszis STOSB STOSW Az utası́tások angol neve: “STOre String Byte” illetve “STOre String Word”. A STOSB
utası́tás az AL regiszter értékét az ES:DI regiszterek által megadott cı́mre ı́rja. Az DI regiszter értéke 1-el növekszik ha a Direction státusz bit értéke nulla vagy az DI regiszter értéke 1-el csökken, ha a Direction státusz bit értéke egy. A STOSW utası́tás csak abban különbözik, hogy az AX regiszter tartalmát ı́rja ki az ES:DI regiszterek által megadott cı́mre. A DI regiszter értéke 2-vel változik a Directioin státusz bit értékétől függően. 7.65 SCASB, SCASW Szintakszis SCASB SCASW Az utası́tások angol neve: “SCAn String Byte” illetve “SCAn String Word”. A SCASB utası́tás az AL regiszter értékét összehasonlı́tja az ES:DI regiszterek által megcı́mzett byte-al. A státusz biteket az ‘[ES:DI] - AL’ különbség szerint lesznek beállı́tva. Az DI regiszter értéke 1-el növekszik ha a 75 Direction státusz bit értéke nulla vagy az DI regiszter értéke
1-el csökken, ha a Direction státusz bit értéke egy. A SCASW utası́tás csak abban különbözik, hogy az AX regiszter tartalmát hasonlı́tja össze az ES:DI regiszterek által megcı́mzett word-el. A DI regiszter értéke 2-vel változik a Directioin státusz bit értékétől függően. Amikor a REP prefix-el együtt használjuk, akkor ez az utası́tás, a megadott memória blokkban az AL vagy AX regiszter tartalmát keresi meg. 7.66 REP Szintakszis REP string-utası́tás Ez tulajdonképpen nem is utası́tás, hanem egy prefix, amit a string kezelő utası́tások elé tehetünk. A neve a “REPeat”, ismétlés angol szóból származik. Ennek a prefixnek az a feladata, hogy az utána megadott string kezelő utası́tást többször hajtsa végre. Az ismétlések számát a CX regiszter adja meg, amit előre be kell állı́tani. Fontos, hogy a CX értéke az adatok számát adja meg, nem pedig az adatok byte-jainak
számát Például 4 word adat másolása esetén az alább két kódrészlet egyenértékű, de figyeljük meg a különböző CX értékeket: . MOV CX, 4 REP MOVSW . egyenértékű a következő kóddal: . MOV CX, 8 REP MOVSB . Ennek a prefixnek az a nagy előnye, hogy egyetlen utası́tással akár egész szegmenst át lehet másolni (MOVSB), szegmensnyi adatokat össze lehet hasonlı́tani (CMPSB), felül lehet ı́rni (STOSB) vagy egy értéket a szegmensben megkeresni (SCASB). További részletek a 12 fejezetben található Ezzel a prefix-el sem lehet a szegmensek határát átlépni. Így például word adatokkal végzett műveletek esetén a 8086-os processzoron nincs értelme a 7FFFh értéknél nagyobb értéket adni a CX regiszternek a REP prefix esetén. Példák Az alábbbi kódrészlet átmásol 4 word méretű adatot. A programrészlet egyenértékű a 761 bekezdéssben bemutatott példával.
forras: dw 1111h, 2222h, 3333h, 4444h cel: dw 0000h, 0000h, 0000h, 0000h . MOV SI, forras MOV DI, cel MOV CX, 4 CLD REP MOVSW . 76 7.67 REPZ Szintakszis REPZ string-utası́tás REPE string-utası́tás Ennek a prefixnek a neve az “REPeat while Zero” illetve “REPeat while Equal” kifejezésekből származik. Ez a prefix is a CX regisztert használja, az értékét csökkenti amı́g el nem éri a zérust. A prefix ezen kı́vül a Zérus státusz bitet (ZF) is megvizsgálja és ha a bit értéke zérus (vagyis a művelet eredménye nem volt zérus) akkor abbahagyja az utası́tás ismétlését. Fontos, hogy a kilépés okát a Zérus (ZF) státusz bitből lehet megtudni. Ha az ismétlés azért állt le, mert az utolsó művelet eredménye nullától különbözött, akkor a ZF értéke zérus lesz, ha pedig a CX regiszter lett zérus, akkor a ZF értéke egy lesz. Érdekes, hogy a REPZ prefixnek és REP prefixnek az
operáció kódja megegyezik. Ez azt jelenti, hogy a MOVS, LODS és STOS utası́tások esetén csak a REP prefixnek van érteme. A SCAS és CMPS utası́tások esetén a prefix pedig mindig REPZ-nek felel meg. Ez azért van, mert a MOVS, LODS és STOS utası́tások nem módosı́tják a státusz biteket, ı́gy nem gond, hogy csak a REP prefixet lehet használni. 7.68 REPNZ Szintakszis REPNZ string-utası́tás REPNE string-utası́tás Ennek a prefixnek a neve az “REPeat while Non Zero” illetve “REPeat while Not Equal” kifejezésekből származik. Ez a prefix is a CX regisztert használja, az értékét csökkenti amı́g el nem éri a zérust A prefix ezen kı́vül a Zérus státusz bitet (ZF) is megvizsgálja és ha a bit értéke egy (vagyis az utolsó művelet eredménye zérus volt) akkor abbahagyja az utası́tás ismétlését. Fontos, hogy a kilépés okát a Zérus (ZF) státusz bitből lehet megtudni. Ha az
ismétlés azért állt le, mert az utolsó művelet eredménye zérus volt, akkor a ZF értéke egy lesz, ha pedig a CX regiszter lett zérus, akkor a ZF értéke zérus lesz. 77 7.7 Processzor vezérlő utası́tások 7.71 CLC Szintakszis CLC Az utası́tás angol neve: “CLear Carry”. Az utası́tás nullára állı́tja a Carry státusz bit értékét 7.72 STC Szintakszis STC Az utası́tás angol neve: “SeT Carry”. Az utası́tás 1-re állı́tja a Carry státusz bit értékét 7.73 CMC Szintakszis CMC Az utası́tás angol neve: “CoMplement Carry”. Az utası́tás nullára állı́tja a Carry státusz bit értékét 7.74 CLD Szintakszis CLD Az utası́tás angol neve: “CLear Direction”. Az utası́tás 0-ra állı́tja a Direction (Irány) státusz bitet, aminek a string kezelő utası́tások esetén van jelentősége. Ebben az esetben a MOVS, LODS, STOS, SCAS és CMPS parancsok növelik a DI és/vagy
SI regiszter értékét. 7.75 STD Szintakszis STD Az utası́tás angol neve: “SeT Direction”. Az utası́tás 1-re állı́tja a Direction (Irány) státusz bitet, aminek a string kezelő utası́tások esetén van jelentősége. Ebben az esetben a MOVS, LODS, STOS, SCAS és CMPS parancsok csökkentik a DI és/vagy SI regiszter értékét. 7.76 CLI Szintakszis CLI Az utası́tás angol neve: “CLear Interrupt”. Az utası́tás nullára állı́tja az Interrupt státusz bit értékét Ez az utası́tás letı́ltja a hardware megszakı́tások fogadását. 78 7.77 STI Szintakszis STI Az utası́tás angol neve: “SeT Interrupt”. Az utası́tás 1-re állı́tja az Interrupt státusz bit értékét Ez az utası́tás engedélyezi a hardware megszakı́tások fogadását. 79 7.8 Egyéb utası́tások 7.81 NOP Szintakszis NOP Ez egy nagyon egyszerű utası́tás, mivel nem csinál semmit. Angol nevén: “No
OPeration” 7.82 IN 7.83 OUT 80 7.9 Ellenőrző kérdések 1. Tegyük fel, hogy az Intel processzorokon nem lenne LDS utası́tás Hogyan implementálná ezt az utası́tást? Írja le a kódot ami egyenértékű az LDS utası́tással. 2. Tegyük fel, hogy a következő adatok vannak definiálva: num1 DW 100 num2 DB 225 char1 DB ’Y’ num3 DD 0 Érvényesek-e az alábbi utası́tások: (a) MOV AX, BX (b) MOV AX, num2 (c) MOV BL, num1 (d) MOV BL, [num1] (e) MOV DH, char1 (f) MOV char1, num2 (g) MOV [char1], [num2] (h) MOV IP, num1 (i) ADD 75, AX (j) CMP 75, AX (k) SUB char1, ’A’ (l) SUB [char1], ’A’ (m) XCHG AL, num2 (n) XCHG AL, [num2] (o) XCHG AL, 23d (p) XCHG AL, [23d] (q) INC num3 (r) INC [num3] 3. Az alábbi kódrészletekben a ‘MOV AX, 1’ vagy az ‘MOV BX, 1’ utası́tás fog lefutni: (a) (b) mov CX,5 sub DX,DX cmp DX,CX jge jump1 mov BX,1 jmp skip1 jump1: mov AX,10 skip1: . mov mov shr cmp CX,5 DX,10 DX,1 CX,DX 81 je
jump1 mov BX,1 jmp skip1 jump1: mov AX,10 skip1: . 4. Magyarázza el szövegesen, hogy mit csinál a következő kód részlet: (a) NOT AX ADD AX, 1 (b) NOT BX ADD BX, 1 (c) SUB SUB MOV ADD ADD ADD ADD AH, DH, DL, DX, DX, DX, DX, AH DH AL DX DX AX DX (d) SUB SUB MOV MOV SHL SHL ADD AH, DH, DL, CL, DX, AX, DX, AH DH AL 3 CL 1 AX 5. Kell-e tudni az AX regiszter kezdeti tartalmát ahhoz, hogy megállapı́tsuk az AX regiszter tartalmát az alábbi kódrészlet lefutása után? Ha igen, magyarázza meg miért! Ha nem, magyarázza el mi lesz az AX regiszter tartalma? (a) MOV DX, AX NOT AX OR AX, DX (b) MOV DX, AX NOT AX AND AX, DX 82 8. Fejezet Assembly programokról Talán csodálkozunk, hogy az eddigi fejezetek miért kellettek. Ahhoz, hogy assembly nyelven programot tudjunk ı́rni, szükséges tudni: • a CPU regiszterek neveit • a verem hogyan működik • hogyan lépjünk ki • hogyan definiáljunk szimbólumokat, lokális és
globális • hogyan használjuk az eszközöket (fordı́tókat, linkereket) 8.1 Programozási módszer Az assembly programozásra jellemző, hogy mindent explicit módon meg kell adni a programban. Nincsennek magas szintű programozási konstrukciók, azokat magunknak kell létrehoznunk A 8.1 táblán látható egy elöltesztelő ciklus megvalósı́tása A táblán látható hogyan “alakul át” az elöltesztelő ciklus feltételes ugró utası́tásokká, hiszen csak ezek állnak rendelkezésre az assembly programozási nyelvben. Ugyanakkor az is látható, hogy a magas szintű programban a feltételben az szerepel, hogy ismételjük meg az utası́tásokat ha az AX regiszter nem egyenlő a zérus értékkel. Ezzel szemben az assembly kódban a feltételes ugrás akkor ugrik a ciklus végére, ha az utolsó műveletben a zérus státusz bit be lett állı́tva, vagyis az összehasonlı́tásnál az AX regisztert
zérusnak találtuk. Vagyis a feltétel megfordult az assembly kódban. Valójában nem kell ennek ı́gy lenni, lehetőségünk van arra, hogy ugyanazt a feltételt használjuk az assembly-ben, mint a magasabb szintű programozási nyelvben. Lényegében arról van szó, hogy a feltételes assembly utası́tások “megfordı́thatók”. Ez látható a 82 táblán Így a program egy kicsit komplikáltabb. ujra: CMP AX, 0 JZ vege . JMP ujra vege: while(AX != 0) { . } 8.1 tábla: Elöltesztelő ciklus megvalósı́tása 83 ujra: CMP JNZ JMP tovabb: . JMP vege: while(AX != 0) { . } AX, 0 tovabb vege ujra 8.2 tábla: Alternatı́v megvalósı́tása az elöltesztelő ciklusnak 8.1 ábra: A megszakı́tások osztályozása 8.2 Megszakı́tások A megszakı́tás, vagy angol nevén interrupt, egy mechanizmus ami által a program végrehajtásának folyamata megváltozhat. Ilyen mechanizmus az ugró utası́tások és ide
tartozik a függvény is (lásd 10. fejezet) Lényegében a megszakı́tás felfüggeszti a jelenlegi program futását és a vezérlést átadja egy megszakı́tás kezelőnek (interrupt service routine, ISR). Amikor a megszakı́tás kezelő befejezte a műveleteit, visszaadja a vezérlés arra a pontra, ahol a programot megszakı́tottuk, úgy mintha a programot meg sem szakı́tottuk volna. A fő különbség a függvények és a megszakı́tások között, hogy a megszakı́tást szoftver is meghı́vhatja és hardware esemény is kiválthatja, mı́g egy függvényt csak szoftver hı́vhatja meg. Tulajdonképpen ez a különbség nagyon fontos, mivel amikor egy hardware-s esemény bekövetkezik, akkor a hardware csak megszakı́tás segı́tségével tud a processzortól számı́tási időt “szerezni”, hogy az adott eseményt lekezeljük. Így a megszakı́tások egy hatékony lehetőséget biztosı́tanak nem várt
események kezelésére is. A másik nagyon fontos különbség a függvények és a megszakı́tások között, hogy a megszakı́tás kezelők memória rezidensek (állandóan a memóriában vannak), mı́g a függvényeket a programmal együtt töltünk be a memóriába csak egy “időre”. A szoftveres megszakı́tások “kiváltására” az INT utası́tást lehet használni. Például amikor a felhasználótól szeretnénk egy karaktert beolvasni, akkor a megfelelő INT utası́tás végrehajtásával a megfelelő megszakı́tás indul el, amely a billentyűzetről beolvassa a karaktert. Valójában a szoftveres és hardware-es magszakı́tásokon kı́vűl van egy harmadik tı́pusú megszakı́tás is, az úgynevezett kivételek (exception). A kivételek az utası́tás hibákat kezelik, mint például a zérussal való osztás. A 81 ábra mutatja a megszakı́tások osztályozását 8.21 Hardware-es
megszakı́tások A hardware-es megszakı́tások is két csoportba sorolhatók: • maszkólható és 84 • nem maszkolható hardware-es megszakı́tások. A nem maszkolható megszakı́tásokat (Non-maskable Interrupt, NMI) a processzor mindig azonnal kezeli. Ilyen megszakı́tás generálódik például a RAM paritás hiba esetén ami memória hibát jelöl. A maszkolható megszakı́tások esetén a megszakı́tás végrehajtása késleltethető addig, amı́g a végrehajtás egy “kedvezőbb” pontot ér el. A maszkolható megszakı́tások működésére a következő példával mutatható be. Egy program futása során egy megszakı́tás következik be. Ekkor a program futása felfüggesztődik és elindul a megszakı́tás kezelő futása. Ha a megszakı́tás nem akarja, hogy egy újabb megszakı́tás beavatkozhasson akkor a processzor maszkolhatja a megszakı́tásokat. Ha a megszakı́tásokat maszkoljuk,
akkor egy újabb megszakı́tásnak várnia kell. (Kivéve a nem maszkolható megszakı́tások!) 8.22 Megszakı́tások 8086-os processzorokon Ez a fejezet csak a 8086-os processzorok megszakı́tás kezelésével foglalkozik. Ez a modern processzorokon a “valós modú” (real mode) megszakı́tás kezelésnek felel meg A “védett modú” (protected mode) megszakı́tás kezeléssel itt nem foglalkozunk. A processzor 256 megszakı́tást támogat, amelyek száma nullától 255-ig tart. A megszakı́tás száma nagyon fontos mivel ezzel azonosı́thatjuk a megszakı́tásokat és ez a szám a megszakı́tás tábla indexét is megadja. A megszakı́tás tábla a nullás fizikai cı́men található A megszakı́tás táblában minden bejegyzés 4 byte-ból áll. Egy ilyen bejegyzést megszakı́tás vektornak szoktunk nevezni Egy bejegyzés egy CS:IP regiszter párost ad meg, 2 byte a CS regiszternek és 2 byte az IP regiszternek.
Ezek után ha egy megszakı́tás cı́mét szeretnénk megtalálni a megszakı́tás táblában, akkor a megszakı́tás indexét szorozni kell 4-el és a kapott hexadecimális szám adja meg az offszetet. Például az INT 10h megszakı́tás cı́me: 10h × 4 = 40h, ami a 0000:0040h cı́mnek felel meg. Ha már ki tudjuk számolni egy megszakı́tás cı́mét, akkor az INT 10h megszakı́tást másképpen is meg tudjuk hı́vni: mov bx,0h mov es,bx mov bx,40h mov ah,0eh ; megszakı́tás paraméterei mov al, 1 pushf call far es:[bx] popf Részletesen a következő történik egy megszakı́tás végrehajtása során: 1. Először a státusz biteket elmentjük a vermen 2. Töröljük a TRAP és INTERRUPT státusz biteket, hogy más megszakı́tás ne szakı́thassa meg az aktuális megszakı́tás kezelő végrehajtását 3. A CS és IP regisztereket feltöltjük a veremre 4. A megszakı́tás táblából betöltjük a CS
regiszter értékét: (index × 4) + 2 5. A megszakı́tás táblából betöltjük az IP regiszter értékét: (index × 4) 6. Megszakı́tás kezelése 7. Helyreállı́tjuk az IP regisztert a veremből 85 8.2 ábra: Kommunikációs lehetőségek alkalmazások és a hardware között 8. Helyreállı́tjuk a CS regisztert a veremből 9. Helyreállı́tjuk a státusz regisztert a veremből Az utolsó három műveletet az IRET utası́tás végzi el 8.23 INT 21h megszakı́tás A szoftveres megszakı́tásokat az INT utası́tás kiadásával lehet elindı́tani. A DOS rendszereken (lényegében a Windows alatt is) az INT 21h szoftveres megszakı́tás nagyon sokféle szolgáltatást biztosı́t a programozó számára. A 8.2 ábra mutatja, hogy egy alkalmazásnak több lehetősége is a hardware eszközökkel való kommunikációra. A legmagasabb szintű szolgáltatásokat az INT 21h adja, például file kezelés A
BIOS szolgáltatás azt jelenti, hogy olyan megszakı́tásokat hı́vunk meg amelyek közvetlenebbül kommunikálnak a hardware-el. Ilyen megszakı́tás az INT 10h amelyik a grafikus megjelenı́tő kártyával és az INT 16h amelyik a billentyűzettel kommunikál. Egy harmadik lehetőség, amikor közvetlenül egy porton keresztül küldünk ki adatokat a hardware-re és ugyanı́gy a portból olvasunk adatokat. Ez a legalacsonyabb szintű hardware vezérlés, amihez már az adott hardware eszköz kézikönyve is szükséges, hiszen tudnunk kell, hogy milyen byte-okat küldjünk, milyen sorrendben és milyen időzı́téssel. 8.24 Kivételek A kivételeket három csoportja soroljuk: 1. faults (hibák), 2. traps (csapdák) és 3. aborts (kilépések) A faults és traps kivételek utası́tás határon jelennek meg. A faults kivételek az utası́tás végrehajtása előtti állapotot használják a bekövetkeztük során.
Ilyen például a nullával való osztás, ami a DIV és IDIV utası́tások végrehajtása során következhet be. Így ha ilyen hiba következik be a kivétel a hiba végrehajtása előtti állapotra fogja visszaállı́tani a processzort. Másik tipikus fault kivétel a “segmentnot-present” hiba, ami azt jelenti, hogy az adott szegmens nincs a memóriában A hiba bekövetkezte után a hiányzó szegmenst be kell tölteni és a program végrehajtása ezután folytatódhat. Itt is az utası́tás 86 előtti állapottól folytatódik a végrehajtás, hiszen az utası́táshoz szükséges szegmens már jelen van a memórában és ı́gy újra végrehajtható az utası́tás. Traps kivételek ezzel szemben az utası́tást követő “határon” hajtódnak végre, vagyis ebben az esetben nincs utası́tás újravégrehajtás. Például a túlcsordulás (overflow) megszakı́tás egy ilyen trap-nek felel meg. A
felhasználó által definiált megszakı́tások is trap-nek felelnek meg Az abort megszakı́tások hibákat jelentenek, például hardware hibákat vagy nem konzisztens rendszer táblákról jelentenek a felhasználó felé. 8.3 Kitérő Linux-ra Bár eddig nem sok szó esett a Linuxról de egy rövid bekezdésig itt megemlı́teném. A Linux rendszer nagyon sok szolgáltatást biztosı́t a 80h megszakı́táson keresztül. Valójában 180 különböző rendszer hı́vást biztosı́t ez a megszakı́tás. A szolgáltatást itt is az EAX regiszter értékével lehet beállı́tani 8.4 COM programok A DOS 1.0 ”operációs rendszer” csak COM tı́pusú programokat használt Az utána következő DOS 20 során vezették csak be az EXE programokat. A DOS 10 rendszer alatt egy igen egyszerű szabály szerint működtek a programok: miután elindult a program a teljes memóriát kezelhette a program. Ebben az időben az
”operációs rendszer” még nem használt memória menedzselést (memory management), ı́gy egy program bármit csinálhatott a teljes memóriában. Az ”operációs rendszer” csak azt tartotta nyilván, hogy hol van az első szabad hely ahova a következő programot betöltheti. Amikor a DOS egy új programot akart elindı́tani, akkor előkészı́tett egy speciális területet a program számára a szabad terület első 256 byte-nyi helyén. A speciális terület neve: Program Segment Prefix (PSP) Az előkészı́tés után a DOS egyszerűen betöltötte a programot a 256 byte-nyi PSP utáni területre. Mindenféle ellenőrzés és további feldolgozás nélkül. A betöltés után még egy-két regisztert előkészı́tett a rendszer, beállı́tott egy visszatérési cı́met, majd a program elejére ugorva elindı́totta azt. Mivel 256 byte egyenlő 100h hexadecimális értékkel ezért van az, hogy a COM
programok a 100h cı́men kezdődnek. Mindig! A DOS által elvégzett beállı́tások egy COM programhoz: • A CS, DS, ES, SS regisztereket beállı́tja a PSP elejére. • Az IP regiszternek a 100h értéket adja, ahol majd a program futása elkezdődik. • Az SP regiszter a verem tetejére van állı́tva. Ez általában az FFFEh értéknek felel meg (Ez a szegmens tetejének cı́me.) • A BX:CX regiszter pár a COM program méretét fogja tükrözni. A BX regiszter a 32 bites értéknek a felső 16 bitjét mı́g a CX az alsó 16 bitjét tárolja. Így egy 256d byte méretű program esetén: BX = 0000h CX = 0100h • Az AX, DX, SI és DI regiszterek értéke zérus lesz. 87 8.41 Program Segment Prefix 8.5 EXE programok 8.6 XOR használata 8.7 Assembly integer aritmetika 8.71 BCD aritmetika 88 8.8 Ellenőrző kérdések 1. Mik a különbségek függvények és megszakı́tások között? 2. Amikor egy
megszakı́tás végrehajtódik, akkor a státusz bitek automatikusan elmentődnek Ugyanakkor egy függvényhı́vás során a státusz bitek nem mentődnek el automatikusan. Magyarázza meg miért van ez ı́gy? 3. Hogyan lehet “kikapcsolni” a maszkolható megszakı́tásokat? 4. Hogyan csoportosı́tjuk a megszakı́tásokat? 89 90 9. Fejezet Példa programok Ebben a fejezetben néhány példa program kerül bemutatásra, melyek remélhetőleg elősegı́tik az assembly programozás elsajátı́tását. A programok nincsennek optimalizálva és ebben a fejezetben a lehető legegyszerűbb változatban kerülnek bemutatásra. A későbbi fejezetek átolvasása után arra biztatnám az olvasót, hogy térjen vissza ezekhez a programokhoz és próbálja meg őket átalakı́tani, rövidebb, más módon is megı́rni. 9.1 Egy byte bináris kinyomtatása Ez a program egy szokásos gyakorlat az assembly nyelv tanulás
során. A program azt mutatja be, hogy egy byte hogyan épül fel bitekből, és a biteket hogyan lehet megfelelő sorrendben kinyomtatni. A 91 tábla mutatja a program listáját. 1 org 100h 2 MOV BL, [adat] 3 MOV AH, 02 4 MOV CX, 0008 5 ujra: 6 MOV DL, 0 7 RCL BL, 1 8 ADC DL, 30h 9 INT 21h 10 LOOP ujra 11 INT 20h 12 adat: 13 db 10100011b 9.1 tábla: Egy byte bináris kinyomtatására szolgáló program A 2. sorban betöltjük az adat cı́m alatt tárolt értéket Az adat a program végén van tárolva és bináris formátumban van megadva. A 3 sorban előkészı́tjük a nyomtatást, vagyis megadjuk az INT 21h megszakı́tás funkció kódját. A 4 sorban a CX regiszterbe betöltünk 8-at, mivel egy byte-ban 8 bit van és a nyomtatást nyolcszor kell megismételni, vagyis nyolcszor kell 1-et vagy 0-át nyomtatni. Mivel egy karaktert fogunk kinyomtatni, ezért a nyomtatandó karakternek végül a DL regiszterben kell lennie, ı́gy a 6.
sorban nullával inicializáljuk a DL regisztert A 7 sorban az RCL utası́tást használjuk (Lásd 7.31 fejezet) Miért? Először is az L betű az utası́tás nevének végén azt jelenti, hogy balra 91 Lépés Kezdeti 1. 2. 3. 4. 5. 6. 7. 8. CF ?? 1 1 0 1 0 1 1 0 128 1 1 0 1 0 1 1 0 ?? 64 1 0 1 0 1 1 0 ?? 1 32 0 1 0 1 1 0 ?? 1 1 Byte 16 8 1 0 0 1 1 1 1 0 0 ?? ?? 1 1 1 1 0 0 1 4 1 1 0 ?? 1 1 0 1 0 2 1 0 ?? 1 1 0 1 0 1 1 0 ?? 1 1 0 1 0 1 1 9.2 tábla: Egy byte balra forgatása a Carry biten keresztül végezzük a bit léptetést vagy bit forgtást. Ez azért fontos, mert a legmagasabb helyiértékű bit a byte bal oldalán található (lásd ??. ábra) és a képernyőre is balról-jobbra végezzük a nyomtatást, vagyis először a legmagasabb helyiértékű bitet kell kinyomtatni. Ha megnézzük a 71 vagy 75 ábrákat, akkor azt látjuk, hogy egy léptetés vagy forgatás során a legmagasabb helyiértékű bit
a Carry bitbe kerül. A Carry bitet a CF doboz jelöli Amikor egy bitet ilyen módon betöltünk a Carry bitbe, akkor később használhatjuk ezt a bitet egy feltételes ugrásnál, lásd JC (7.52 fejezet) és JNC (752 fejezet) utası́tásokat. Ugyanakkor ebben a programban a Carry bitet másképp használjuk A programnak végül vagy az ‘0’ vagy a ‘1’ karaktert kell kinyomtatnia amelyek ASCII kódja 30h és 31h. Ez azt jelenti, hogy a DL regiszterhez hozzá kell adni 30h és még a Carry bit értékét Erre szerencsére van külön utası́tás, az ADC utası́tás (lásd 7.24 fejezetet) Mivel ez az utası́tás hozzáadja az értékeket a DL regiszterhez, ezért kellett a 6. sorban nullát tölteni a DL regiszterbe Végül a 9. sorban az INT 21h megszakı́tás segı́tségével kinyomtatjuk a karaktert A 10 sorban a LOOP utası́tás (lásd 7.54fejezet) csökkenti a CX regiszter tartalmát eggyel és ha a CX regiszter
értéke nem zérus, akkor a megadott cı́mre ugrik. Ez azt jelenti, hogy a legbaloldalibb bit nyomtatása után ismét a legbaloldalibb bitet fogja nyomtatni, hiszen a byte-ot mindig balra forgatjuk. A 92 táblázat mutat egy példát arra, hogy hogyan alakul a byte és a Carry bit értéke az egyes lépések, forgatás során. 92 9.2 Egy hexadecimális szám kinyomtatása Egy hexadecimális szám 0 és 16 közötti érték lehet. Ezeket az értékeket 4 biten lehet reprezentálni, ı́gy ez a program csak egy byte (8 bit) felét tudja kinyomtatni. A hexadecimális számrendszerben 16 számjegy van: • 0 - 9 számjegyek ASCII kódja: 30h - 39h • A - F számjegyek ASCII kódja: 41h - 46h A programban feltételezzük, hogy a hexadecimális szám a BL 8 bites regiszterben van eltárolva. Ez azt jelenti, hogy a byte értéket meg kell vizsgálni, hogy a fenti tartományokból melyikbe esik és aszerint kell hozzáadni a
megfelelő értéket, hogy számjegy ASCII kódját kapjuk meg. A pszeudo kódban az algoritmus a következőképpen néz ki: IF BL < 0Ah THEN BL = BL + 30h ELSE BL = BL + 37h Ez azt jelenti, hogy ha a BL regiszter tartalma kisebb mint 10, akkor csak 30h értékkel kell módosı́tani, egyébként 37h értéket kell a BL regiszter tartalmához hozzáadni. Bár az assembly programozási nyelvben megvalósı́tható a feltételes utası́tás, de közvetlen ELSE ág nincs. (Az ELSE ágba eső utası́tások akkor hajtódnak végre, ha a feltétel hamis volt.) Ennek következtében a kódot egy kicsit át kell szervezni, ami pszeudo kódban ez lesz: BL = BL + 30h IF BL >= 03Ah THEN BL = BL + 07h Az előző változatban 30h vagy 37h értéket kellett a regiszterhez hozzáadni, ı́gy ebben a változatban 30h-at mindenképpen hozzáadunk és 07h-et már csak akkor kell hozzáadni, ha a kapott érték nagyobb vagy egyenlő mint 3Ah.
(Számoljuk végig, hogy a nulla szám ASCII karakterének száma 30h, az egy számé 31h és ı́gy tovább. Végül a kilences szám karakterének száma 39h, vagyis a 3Ah már a tizes számjegynek felelne meg, de a tizes számjegyet már az A betű jelöli aminek az ASCII kódja már 41h.) Az assembly program listája az 9.3 táblán látható A hexadecimális érték a programban van definiálva a szam cı́men, és ezt az értéket töltjük be a BL regiszterbe. A program végül kinyomtatja a hexadecimális számjegyet, ebben az esetben a B betűt. Az 9.3 táblán látható programban a második sorban a BL regiszterbe töltjük a szam cı́men tárolt byte értéket. A harmadik sorban az AH regiszterbe a 2-es értéket töltjük Ennek az értéknek csa később van jelentősége de mivel a későbbi utası́tások nem módosı́tják az AH regiszter tartalmát ezért itt is nyugodtan beállı́thatjuk ezt az
értékét. A negyedik sorban a BL regiszter tartalmát átmásoljuk a DL regiszterbe. Most már feltűnhet, hogy valójában arra törekszünk, hogy a program végén az INT 21h megszakı́tást használjuk a karakter kinyomtatására. (Ha egy karaktert akarunk kinyomtatni a INT 21h megszakı́tással, akkor az AH adja meg a funkcó kódot – 02h – és a DL regiszter fogja tartalmazni a kinyomtatandó karakter ASCII kódját. Lásd 62 bekezdést) Az 5-8 sorok valósı́tják meg az előbb tárgyalt pszeudo kódot. Először a DL regiszterhez hozzáadunk 30h értéket, majd az ı́gy kapott értéket a következő sorban összehasonlı́tjuk a 3Ah értékkel. A CMP utası́tás csak összehasonlı́tást végez és beállı́tja a státusz regisztert. A JL utası́tás a feltételes ugrás és a státusz bitek értékétől függően folytatódik a végrehajtés. Ez aztjelenti, hogy ha a DL regiszter értéke kisebb
mint 3Ah akkor átugorjuk a 8. sort Ha nagyobb, akkkor a 8 sorban folytatódik a végrehajtás és még hozzáadunk 7-et, hogy az A karakter ASCII kódjátólinduló értékeket kapjunk. A karakter kinyomtatása a 10. sorban történik az INT 21h megszakı́tással 93 1 org 100h 2 MOV BL, [szam] 3 MOV AH, 02 4 MOV DL, BL 5 ADD DL, 30h 6 CMP DL, 3ah 7 JL szamjegy 8 ADD DL, 07 9 szamjegy: 10 INT 21h 11 INT 20h 12 szam: 13 db 0bh ; DL < 3ah 9.3 tábla: Egy hexadecimális szám kinyomtatására szolgáló program 94 9.3 Egy byte hexadecimális kinyomtatása Ez a program annyiban különbözik az előző programtól, hogy ez a program 2 hexadecimális számot fog kinyomtatni. Miért? Egy byte 8 bitet vagy kétszer 4 bitet tartalmaz 4 biten pedig pontosan 16 féle számot lehet ábrázolni ami megfelel egy hexadecimális számjegynek. Bár mostanra már egyértelműnek kell lennie hogyan lehet 4 biten 16 számot ábrázolni, de
a teljesség kedvéért a 9.4 táblázat felsorolja a 4 biten tárolható számokat decimális és bináris alakban. Ez azt jelenti, hogy egy byte-ban az alsó négy bit és a felső négy bit is megfelel egy hexadecimális számjegynek, vagyis ha egy byte-ot headecimálisan akarunk kinyomtatni, akkor két hexadecimális számjegyet kell kinyomtatni. A 9.5 tábla tartalmazza az assembly program listáját Ez a program a 93 programot kétszer tartalmazza. (Igazából függvényeket kellene használni, hogy kevesebb legyen a kód ismétlés, de a függvényekről majd a 10. fejezetben tanulunk) Az extra utası́tások a 95 táblán látható programhoz képest azt biztosı́tják, hogy mindig az alsó 4 bit tartalmazza a hexadecimális számot és a felső 4 bit pedig mindig zérus legyen! A második sorban betöltjük a byte-ot a BL regiszterbe, majd a harmadik sorban az INT 21h megszakı́tás funkció kódját állı́tjuk be
az AH regizterben. Mivel kétszer kell kezelnünk a BL regisztert (alsó és felső négy bit) ezért az eredeti értéket a BL regiszterben tároljuk és majd a DL regisztert módosı́tjuk. Ezért a BL regiszter tartalmát átmásoljuk a DL regiszterbe a 4 sorban Mivel a számokat balról jobbra ı́rjuk, ezért először a magasabb helyiértékű biteket kell figyelembe venni, vagyis a felső 4 bitnek megfelelő számot kell kinyomtatni. Bár ez igaz, de azt is figyelembe kell venni, hogy a 93 program az alsó négy bittel dolgozott. Ezért nincs más teendőnk, mint a felső négy bitet “eltolni” az alsó négy bitbe. Sokféleképpen lehet ezt megtenni, de a legegyszerűbb bitléptető utası́tást használni A bitléptető utası́tások közül az SHR utası́tás diagramja a 7.7 ábrán látható Fontos, hogy ez az utası́tás a jobbra léptetés során balról nullákat léptet be, vagyis a felső 4 bitet
lenullázza. Miután az alsó 4 biten van a kinyomtatandó hexadecimális számjegy, a 9.3 programot lehet használni a nyomtatásra (A 93 program jelölve van a 9.5 táblán) A 13. sorban helyreállı́tjuk a DL regiszter értékét a BL regiszterből (Itt kell megjegyezni, hogy természetesen a szam cimről újra betölthetnénk az értéket, de itt néhány órajel ciklust megtakarı́tva a BL regisztert használjuk mint gyors, időleges tárolóhely. Ha valamiért mégis szükségünk lenne a BX regiszterre a programban, például cı́mzésnél, akkor természetesen célszerűbb lenne a szam cı́mről betölteni az értéket a DL regiszterbe.) A második szám kinyomtatásánál csak az alsó 4 bitre van szükségünk. Igen ám, de figyelni kell arra, hogy a felső 4 bit is ott van byte-ban A 93 program feltételezi, hogy a felső 4 bit zérus. Ennek elérésére “maszkolást” fogunk használni, vagyis a felső
4 bitet ki kell nullázni, úgy hogy az alsó 4 bit értéke ne változzon. A maszkolás a 14 sorban az AND utası́tással valósı́tsjuk meg. A 0Fh hexadecimális szám binárisan 00001111b és ezt használjuk az AND utası́tással, vagyis ahol a bit értéke 1 ott a DL regiszter tartalma változatlan marad, ahol pedig nulla, ott a DL regiszter bitje biztos hogy zérus lesz. (Ez elég egyszerűen belátható, ha megnézzük az AND utası́tás igazság tábláját, lásd 7.2 tábla) Miután a DL regiszter megfelelően be van állı́tva, nincs más hátra mint kinyomtatni a 9.3 program szerint. Ez is jelölve van a 95 táblán) 95 Decimális 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Bináris 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 9.4 tábla: 4 biten ábrázolható számok bináris alakban 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 org 100h MOV BL, [szam] MOV AH, 02 MOV DL,
BL MOV CL, 4 SHR DL, CL ; ADD DL, 30h ; CMP DL, 3ah ; JL szam1 ; ADD DL, 07 ; szam1: ; INT 21h ; MOV DL, BL ; AND DL, 0Fh ; ADD DL, 30h ; CMP DL, 3ah ; JL szam2 ; ADD DL, 07 ; szam2: ; INT 21h ; INT 20h szam: db 0FFh bit léptetés jobbra --| | | elozo program | | --| DL helyreallitasa maszkolás --| | | elozo program | | --| 9.5 tábla: Egy byte hexadecimális formátumú kinyomtatására szolgáló program 96 9.4 Egy decimális számjegy ellenőrzött beolvasása és kinyomtatása A 9.6 program azt mutatja be, hogyan lehet megvizsgálni, hogy a beolvasott karakter számjegy-e Az érvényes karakterek tartománya: 0-9, melyek ASCII kódja egymás utáni az ASCII táblában, lásd A.1 táblázat. Bár a program számjegyeket fogad csak el, természetesen a programot könnyű úgy átı́rni, hogy más, egymás utáni karaktereket fogadjon el. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 org 100h ujra: MOV MOV INT MOV
INT CMP JB CMP JA MOV MOV INT INT AH, 9 DX, uzenet 21h AH,1 21h ; beolvasott karakter AL-ben AL, ’0’ ; also korlat hiba AL, ’9’ ; felso korlat hiba DL, AL ; nyomtatas elokeszitese AH, 2 21h 20h MOV MOV INT JMP DX, hiba str AH, 9 21h ujra hiba: uzenet: db 0Dh,0Ah,’Adjon meg egy szamot: $’ hiba str: db 0Dh,0Ah,’Ervenytelen karakter!$’ 9.6 tábla: Egy számjegy beolvasása és kinyomtatása A 3-5. sorokban egy üzenetet ı́runk ki a képernyőre a felhasználó számára A 6 és 7 sorban a INT 21h megszakı́tást használjuk, hogy egy karaktert beolvassunk a felhasználótól. A beolvasott karakter az AL regiszterbe kerül. A 8 sorban az AL regiszter tartalmát összehasonlı́tjuk a ‘0’ karakter ASCII kódjával. A CMP utastás beállı́tja a státusz regiszter bitjeit a két érték egymáshoz való viszonya alapján. Ezeknek a biteknek az állapotát vizsgálja meg a 9 sorban a JB utası́tás és ha az AL regiszter
tartalma kisebb mint a ‘0’ karakter ASCII kódja, akkor a hiba cı́mre ugrik a program. Ellenkező esetben a következő, 10., sorban folytatódik a program A 10 és 11 sorban az AL regiszter tartalmát a ‘9’ karakter ASCII kódjával hasonlı́tjuk össze és ha az AL regiszter tartalma nagyobb akkor szintén a hiba cı́mre ugrik a program. Ha az AL regiszter tartalma a megfelelő tartományban van, akkor a program a 12. sorban folytatódik A 12-14 sorok között csak annyi történik, hogy a megfelelő regiszterek tartalmát beállı́tjuk, olyan módon, hogy egy karaktert ki tudjunk nyomtatni a képernyőre. A 15. sorban kilépünk a programból A 16-19 sorok közötti utası́tások egy hiba üzenetet ı́rnak ki a képernyőre. A 20 sorban egy feltétel nélküli ugrással a program elejére ugrunk, hogy újra be lehessen olvasni egy karaktert. A 22 és 24 ssorban a karaktersorozat elején a 0Dh és 0Ah byte-ok azért vannak
megadva, hogy egy “sort emeljenek a képernyőn”, vagyis a következő sorba nyomtassuk ki a szöveget. (Próbáljuk ki a programot úgy, hogy ezeket a byte-okat kitöröljük.) 97 9.5 Egy karakter beolvasása és módosı́tása Ez a program egy karaktert olvas be és az utána következő karaktert nyomtatja ki. Az “a” betű helyett a “b” betűt, a “b” betű helyett a “c” beűt és ı́gy tovább. 1 2 3 4 5 6 7 8 org 100h MOV AH, 1 INT 21h MOV DL, AL INC DL MOV AH, 2 INT 21h INT 20h 9.7 tábla: Egy karakter beolvasása és az utána következő kinyomtatása A fenti programban semmilyen ellenőrzés nincs ı́gy a programot most kiegészı́tjük annak vizsgálatval hogy a bolvasott karakter tényleg kis betű-e. A programnak van még egy “rejtett” hibája: Mi történik ha a “z” betűt adjuk meg? Mivel a karakter számszerű értékéhez hozzáadunk egyet és a “z” betű ASCII kódja (ASCII
száma) 122 decimális vagy 7A hexadecimális ezért a program a 123-as ASCII kódú karaktert nyomtatja ki, a “{ karaktert. A 98 program ezt a problémát is kijavı́tja 98 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 org 100h MOV AH, 1 INT 21h MOV DL, AL CMP DL, ’a’ JB nem betu CMP DL, ’z’ JA nem betu JNE novel MOV DL, ’a’ JMP nyomtat novel: INC DL nyomtat: MOV AH, 2 INT 21h vege: INT 20h nem betu: MOV AH, 9 MOV DX, nem betu szoveg INT 21h JMP vege nem betu szoveg: db 10,13,’Nem betut adtal meg!$’ 9.8 tábla: Egy karakter beolvasása és az utána következő kinyomtatása 99 9.6 Öt karakter bolvasása és kinyomtatása fordı́tott sorrendben A 9.9 táblán látható program beolvas öt karaktert, ezeket eltárolja, majd fordı́tott sorrendben kinyomtatja a beolvasott karaktereket A program jó példa az regiszteres cı́mzésre (A cı́mzési módokat a 3.3 fejezet tárgyalja) A 2. sorban a CX
regiszter értékét 5-re állı́tjuk, mivel egy ciklusban fogjuk beolvasni a karaktereket és a CX regiszter lesz a ciklus változó. A 3 sorban a DI regiszterbe a tárolásra használt hely cı́mét töltjük be. Az 5 és 6 sorban az INT 21h megszakı́tással beolvasunk egy karaktert az AL regiszterbe A 7. sorban eltároljuk a beolvasott karaktert a DI regiszter által megadott cı́mre (Bár nincs megadva szegmens regiszter, de adat esetén automatikusan feltételezhetjük a DS szegmens regisztert, ı́gy a 7. sor a következő is lehetne: MOV [DS:DI], AL A 8. sorban a DI regiszter értékét 1-el növeljük meg, mivel minden karakter 1 byte méretű, ı́gy a következő karaktert majd a következő byte-on kell eltárolni. A 9 sorban található LOOP utası́tással valósı́tjuk meg a ciklust. Az utası́tás csökkenti a CX regiszter értékét 1-el és ha a regiszter nem nulla, akkor a megadott cı́mre ugrik, jelen esetben
újabb karaktert olvas be. A ciklus végén a 10. sor fog végrehajtódni, ahol a DI regiszter értékét 1-el csökkentjük Erre azért van szükség, mert az 5. karakter eltárolása után is megnöveljük a DI regiszter tartalmát, ı́gy az már egy hatodik karakterre mutatna, ami nem definiált a programban. Így a 10 sorban végrehajtott csökkentés után a DI regiszter megint az 5. karakterre fog mutatni, amit majd kinyomtatunk először A 11 sorban ismét a ciklus változót állı́tjuk be, mı́g a 12. sorban az INT 21h megszakı́tás funkciókódját adjuk meg. A cikluson belül a 14 sorban a DL regiszterbe töltjük be a karaktert amit a 15 sorban nyomtatunk ki. A 16 sorban a DI regisztert ismét csökkentjük, mivel fordı́tott sorrendben akarjuk kinyomtatni a karaktereket mint ahogy beolvastuk őket. A ciklust itt is a LOOP utası́tással valósı́tjuk meg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 org 100h MOV
CX, 5 MOV DI, karakterek ujra: MOV AH, 1 INT 21h MOV [DI], AL INC DI LOOP ujra DEC DI MOV CX, 5 MOV AH, 2 nyomtat: MOV DL, [DI] INT 21h DEC DI LOOP nyomtat INT 20h karakterek: db 0,0,0,0,0 9.9 tábla: Öt karakter beolvasása és kinyomtatása fordı́tott sorrendben 100 9.7 Két egyjegyű szám összeadása A 9.10 program beolvas két decimális számjegyet, összeadja őket, majd az eredménynek megfelelő számú csillag (‘*’) karaktert nyomtat ki. A programban a számjegyek beolvasása kétszer szerepel. A kód ismétlés elkerülésére a legjobb lenne függvényt használni, de erről csak később lesz szó a 10. fejezetben Az első beolvasott számot a CL regiszterben tároljuk el a 13. sorban A második beolvasott számjegy az AL regiszterben alakul ki a 24. sorban A 25 sorban összeadjuk a két számot és az eredményt a CL regiszterben tároljuk el A 26. sorban a CH regisztert kinullázzuk, hogy a teljes CX regiszter
tartalmazza az eredményt és a LOOP utası́tást tudjuk használni. A karakterek nyomtatásának előkészı́tése a 27 és 28 sorban történik, mı́g a nyomtatási ciklus a 30. és 31 sorban található A 9.11 program egy másik lehetőséget mutat a program megvalósı́tására Ebben a programban a beolvasást egy ciklussal végezzük el. Mivel a ciklushoz szükség van a CL regiszterre ezért a beolvasott számokat itt a memóriában tároljuk el. Ebben a programban a számok összeadását a 21 és 22 sorban végezzük el, majd a nyomtatást az előzőekhez hasonlóan, ciklussal oldjuk meg. 101 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 org 100h szam 1: MOV AH, 9 ; elso szam bekerese MOV DX, uzenet INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB nem szam 1 CMP AL, ’9’ JA nem szam 1 SUB AL, ’0’ ; karakterbol szam MOV CL, AL ; karakter
tárolasa szam 2: MOV AH, 9 ; masodik szam bekerese MOV DX, uzenet INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB nem szam 2 CMP AL, ’9’ JA nem szam 2 SUB AL, ’0’ ; karakterbol szam ADD CL, AL ; osszeadas XOR CH, CH MOV AH, 2 ; nyomtatas elokeszitese MOV DL, ’*’ ujra: INT 21h LOOP ujra INT 20h nem szam 1: MOV AH, 9 MOV DX, nem szam szoveg INT 21h JMP szam 1 nem szam 2: MOV AH, 9 MOV DX, nem szam szoveg INT 21h JMP szam 2 uzenet: db 0Dh,0Ah,’Adjon meg egy szamot: $’ nem szam szoveg: db 10,13,’Nem szamot adtal meg!$’ 9.10 tábla: Két szám összeadása és az eredmény kinyomtatása 102 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 org 100h MOV DI, szamok MOV CL, 0 szam be: MOV AH, 9 ; elso szam bekerese MOV DX, uzenet INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB nem szam CMP AL, ’9’ JA nem szam SUB AL, ’0’ MOV [DI], AL INC DI INC CL CMP CL, 2 JNE szam be ; olvasas vege MOV CL,
byte [szamok] ADD CL, byte [szamok+1] XOR CH, CH MOV AH, 2 ; nyomtatas elokeszitese MOV DL, ’*’ ujra: INT 21h LOOP ujra INT 20h nem szam: MOV AH, 9 MOV DX, nem szam szoveg INT 21h JMP szam be szamok: db 0, 0 uzenet: db 0Dh,0Ah,’Adjon meg egy szamot: $’ nem szam szoveg: db 10,13,’Nem szamot adtal meg!$’ 9.11 tábla: Két szám összeadása és az eredmény kinyomtatása ciklussal 103 9.8 Egy karakter n-szeri kinyomtatása A 9.12 táblán látható program beolvas egy karaktert, utána egy decimális számjegyet és az elsőként beolvasott karaktert annyiszor nyomtatja ki, amekkora a másodiknak beolvasott szám volt. A program mostanra talán nem igényel magyarázatot, mivel minden részlete eddig már szerepelt egy korábbi programban. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 org 100h MOV AH, 9 MOV DX, uzenet1 INT 21h MOV AH, 1 INT 21h MOV [karakter], AL olvas:
MOV AH, 9 MOV DX, uzenet2 INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB hiba CMP AL, ’9’ JA hiba SUB AL, ’0’ XOR CX, CX MOV CL, AL MOV AH, 9 ; uj sor nyomtatasa MOV DX, ujsor INT 21h MOV AH, 2 ; karakter sorozat nyomtatasa MOV DL, [karakter] nyomtat: INT 21h LOOP nyomtat INT 20h hiba: MOV AH, 9 MOV DX, uzenet nem szam INT 21h JMP olvas ujsor: db 10, 13, ’$’ karakter: db 0 uzenet1: db 10, 13, ’Adjon meg egy karaktert: $’ uzenet2: db 10, 13, ’Adjon meg egy számjegyet: $’ uzenet nem szam: db 10, 13, ’Nem számjegyet adott meg!’ 9.12 tábla: Egy karakter n-szeri kinyomtatására szolgáló program 104 9.9 Téglalap kinyomtatása A 9.13 és 914 táblákon bemutatott program beolvas két egyjegyű számot ellenőrzéssel, a sorok és oszlopok számát, majd a számoknak megfelelő méretű téglalapot nyomtat ki a képernyőre. A program igazából nem túl bonyolult, de az üzenetek nyomtatása és az ellenőrzések miatt
olyan hosszú lett, hogy két táblán kerül bemutatásra. A program speciális abban az értelemben, hogy ez a program nem “lineáris”. Ez azt jelenti, hogy a 2. sorban már rögtön 19 sorra ugrunk és ott folytatódik a program végrehajtása A 2 és 19 sor közé bekerült néhány adat, illetve a hiba kezelő programrészletek is. Itt ez jelenti azt, hogy a program nem lineáris, nem csak fentről, lefelé fut a program és adat is beékelődik a programba. A 20-22. sorokban egy üzenete ı́runk ki a felhasználónak, majd a 23 és 24 sorokban beolvasunk egy karaktert. A 25 és 28 sorok között ellenőrizzük, hogy a beolvasott karakter számjegy-e Ha nem számjegyet olvastunk be, a program egy hibaüzenetet ı́r ki és újra megpróbál beolvasni egy számjegyet. A 29. sorban a beolvasott számjegy ASCII kódját számmá konvertáljuk és eltároljuk a CH regiszterben A 32-34. sorokban egy újabb üzenetet ı́runk
ki, majd ismét egy karaktert olvasunk be a 35 és 36 sorban. A 37-40 sorok között ismét ellenőrzést hajtunk végre, hogy a beolvasott karakter szám-e Érdemes megfigyelni, hogy a két beolvasásnál külön hiba üzenet nyomtató részt használtunk: hiba1 és hiba2. Miért? Ez azért van, mert ha csak egy hibaüzenet nyomtató programrészlet lenne, akkor a hiba üzenet kinyomtatása után két különböző helyre kellene valahogy visszatérnie. Egyszer a hiba1 cı́mre, máskor pedig a hiba2 cı́mre. Természetesen ezt nem lehet A függvényeknél majd látni fogjuk, hogy ezt hogyan lehet megvalósı́tani, de itt most azt az egyszerű megoldást használjuk, hogy a kódot megismételjük. A másodszorra beolvasott értéket a BL regiszterben tároljuk el. A téglalap nyomtatását a 914 tábla mutatja be. A 46-48. sorokban csak egy soremelést nyomtatunk A 50 sorban beállı́tjuk a karakter nyomtatási funkció
kódot és az 51. sorban a nyomtatandó karaktert adjuk meg Az 52 sorban a CL regiszterbe átmásoljuk a BL regiszter tartalmát, az oszlopok számát. A CL regiszter lesz az egyik ciklus változó A másik ciklus változó a CH regiszter. Miért kell két ciklus változó? Nézzük meg a következő C kódot, ami egy téglalapot nyomtat ki: for(ch = n; ch > 0; ch--) { for(cl = m; cl > 0; cl--) { printf(’’o’’ } printf(’’ ’’ } A fenti kódban az látható, hogy van egy belső és egy külső ciklus. Ezeket jelöli a belso és kulso cı́m az assembly programban. Az is látható, hogy amikor a belső ciklus elkezdődik a CL regisztert mindig újra kell inicializálni. Ez történik a 52 sorban, amikor a CL regiszterbe átmásoljuk a BL regiszter tartalmát. (A BL regiszter tartalma nem változik meg a nyomtatás során) A belső ciklusban a 54. sor végzi a nyomtatást A 55 sor végzi a ciklus változó
csökkentését, majd a 56. sor valósı́tja meg a feltételes ugrást Ha a CL regiszter tartalma a csökkentés után nem zérus, akkor megismételjük a nyomtatást, mivel a belso cı́mre ugrik a program. Ha zérus lett a CL regiszter, akkor a 57. sorban folytatódik a program futása A 57-59 sorokban egy soremelést nyomtatunk ki Mivel itt megváltozik a funkció kód az AH regiszterben, ezért kell az 50. sorban ismét beállı́tani a karakter nyomtató funkció kódot. A 60 és 61 sorok valósı́tják meg a külső ciklust A 62 sorban lépünk ki a programból. 105 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 org 100h JMP olvas1 hiba1: MOV AH, 9 MOV DX, uzenet nem szam INT 21h JMP olvas1 hiba2: MOV AH, 9 MOV DX, uzenet nem szam INT 21h JMP olvas2 uzenet nem szam: db 10, 13, ’Nem számjegyet adott meg!’ uzenet1: db 10, 13, ’Adja meg a sorok szamat: $’
uzenet2: db 10, 13, ’Adja meg az oszlopok szamat: $’ olvas1: MOV AH, 9 MOV DX, uzenet1 INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB hiba1 CMP AL, ’9’ JA hiba1 SUB AL, ’0’ MOV CH, AL olvas2: MOV AH, 9 MOV DX, uzenet2 INT 21h MOV AH, 1 INT 21h CMP AL, ’0’ JB hiba2 CMP AL, ’9’ JA hiba2 SUB AL, ’0’ MOV BL, AL ; folytatódik . 9.13 tábla: Egy téglalap kinyomtatására szolgáló program első része 106 45 ; nyomtatas itt kezdodik 46 MOV AH, 9 ; uj sor 47 MOV DX, ujsor 48 INT 21h 49 kulso: 50 MOV AH, 2 51 MOV DL, ’o’ 52 MOV CL, BL 53 belso: 54 INT 21h 55 DEC CL 56 JNE belso 57 MOV AH, 9 58 MOV DX, ujsor 59 INT 21h 60 DEC CH 61 JNE kulso 62 INT 20h 63 ujsor: 64 db 10, 13, ’$’ 9.14 tábla: Egy téglalap kinyomtatására szolgáló program második része 107 9.10 Sakktábla nyomtatása Ez a program tulajdonképpen nagyon hasonlı́t a téglalap nyomtató programhoz (9.9 fejezet), de itt nem egyféle karaktert kell nyomtatni,
hanem felváltva különböző karaktereket. A 915 program mutatja be a sakktábla nyomtatását megvalósı́tó assembly program. Ebben a programban is két egymásba ágyazott ciklus van. Az egyik ciklus változó a BX regiszter, mı́g a másik a CX regiszter. Bár a program 8x8-as téglalapot nyomtat ki, de a CX regiszter kezdeti értéke csak 4. Ez azért van mert négy darab dupla karaktert nyomtatunk ki egy sorba: vagy XO vagy OX karaktereket. Azért van kétféle dupla karakter, mivel az egyiket a páros a másikat a páratlan sorokba nyomtatjuk. A 7 sorban vizsgáljuk meg, hogy a sor száma, BX regiszter, páros-e Hogyan csináljuk? A 7. sorban a TEST utası́tás egy maszkolást végez és a legkisebb helyiértékű bitet tartja meg a BX regiszterből. Ez azért elegendő, mivel a felsőbb bitek minden kettő hatványai azok csak párosak lehetnek, ezért a legalsó bit az ami eldönti, hogy a BX-ben tárolt érték páros
vagy páratlan. Ha a legkisebb helyiértékű bit 1 akkor a BX regiszter tartalma páratlan, ha zérus, akkor páros. Miután eldőlt, hogy páros vagy páratlan sort nyomtatunk, kinyomtatjuk a két megfelelő karaktert a belső ciklusban. A belső ciklus a 6 és 21 sor között van A 22 és 26 sorok között csak egy sor emelést nyomtatunk. A 27 sorban csökkentjük a BX regisztert, a külső ciklus ciklusváltozóját és ha nem zérus akkor a 28. sorban a külső ciklus elejére ugrunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 org 100h mov bx, 8 mov ah,2 kulso: mov cx, 4 belso: test bx,1 jz paros paratlan: ; ez a cim igazábol nem kellene mov dl,’O’ int 21h mov dl,’X’ int 21h jmp ciklus paros: mov dl,’X’ int 21h mov dl,’O’ int 21h ciklus: loop belso ; uj sor mov dl, 0dh int 21h mov dl, 0ah int 21h dec bx jnz kulso int 20h 9.15 tábla: Egy sakktábla kinyomtatására szolgáló program
108 Ez a sakktábla nyomtatási feladat arra is jó, hogy a XOR utası́tás egy másik jellemző alkalmazását is bemutassuk. Két értékre alkalmazva a XOR utası́tást egy harmadik értéket fogunk kapni a 74 táblázat szerint. Ha újra alkalmazzuk ugyanazt a XOR utası́tást az eredményre, akkor az eredeti értéket kapjuk vissza. Nézzünk erre egy példát: MOV XOR XOR XOR XOR AL, AL, AL, AL, AL, 33h 11h 11h 11h 11h ; ; ; ; ; AL AL AL AL AL = = = = = 33h !!! 22h 33h !!! 22h 33h !!! A XOR utası́tásnak ezt a tulajdonságát használja ki a 9.16 program A 6 sorban a nyomtatandó karaktereket össze-XOR-oljuk, (amiből kapunk valamilyen értéket) de ezután ezt az értéket újra XORolva az egyik karakterrel, hol az egyik, ‘X’, hol a másik, ‘O’, karaktert kapjuk meg a 10. sorban Ebben a programban is a BX regiszter a külső ciklusváltozó és a CX regiszter a belső ciklusváltozó, aminek viszont 8 a
kezdeti értéke, mivel itt egyesével nyomtatjuk ki a karaktereket. A belső ciklus után a 13 sorban elmentjük a DX regiszter tartalmát, mert a soremelés nyomtatásnál tönkretesszük a regiszter tartalmát. A 14-17 sorokban nyomtatjuk ki a soremelés karaktereket és a 18 sorban visszaállı́tjuk a DX regiszter tartalmát. A 21. sor szorul még magyarázatra Nézzük meg a következő két sort: XOXOXOXO OXOXOXOX Azt lehet látni, hogy a második sort ugyanazzal a karakterrel kell kezdeni, mint amivel az első sort lezártuk. A 21 sor ezt biztosı́tja, hogy a XOR utası́tás alkalmazásával a ‘X’ karaktert “átugorjuk” Végül a 22. és 23 sor a külső ciklust valósı́tja meg 109 1 org 100h 2 mov ah,2 3 mov bx, 8 4 mov dl, ’X’ 5 mov dh, ’O’ 6 xor dh, dl 7 kulso: 8 mov cx, 8 9 belso: 10 xor dl,dh 11 int 21h 12 loop belso 13 mov bp, dx ; elmenti DX-et 14 mov dl, 0dh ; új sor nyomtatása 15 int 21h 16 mov dl, 0ah
17 int 21h 18 mov dx, bp ; visszaállitás DX-et 19 ; mivel ugyanazzal a karakterrel 20 ; folytatódik a következö sor 21 xor dl,dh 22 dec bx 23 jnz kulso 24 int 20h 9.16 tábla: Egy sakktábla kinyomtatására szolgáló program XOR utası́tással 110 9.11 ASCII tábla kinyomtatása A 9.17 táblán látható program az ASCII táblában található karaktereket nyomtatja ki a képernyőre A program igen egyszerű. A 2 sorban adjuk meg, hogy 256 darab karaktert fogunk kinyomtatni A 3 sorban megadjuk az első karakter ASCII kódját. A 4 sorban az INT 21h megszakı́tás funkció kódját adjuk meg. A 6 sor végzi a nyomtatást, majd a 7 sorban a DL regiszter megnövelésével a következő ASCII karaktert állı́tjuk be. A 8 sor csökkenti a CX regiszter tartalmát eggyel és ha még nem zérus, akkor az ujra cı́mre ugrik. A LOOP utası́tás segı́tségével képezzük a ciklust mely 256-szor fut le Az utolsó sorban
kilépünk a programból. 1 2 3 4 5 6 7 8 9 org 100h MOV CX, 256 MOV DL, 0 MOV AH, 2 ujra: INT 21h INC DL LOOP ujra INT 20h 9.17 tábla: Az ASCII tábla kinyomtatására szolgáló program 111 9.12 Szám kiı́rása decimális formában Ha egy számot decimális formában akarunk kinyomtatni, speciális nyomtatási eljárást kell alkalmazni. A 9.18 program erre mutat egy példát Az algoritmus lényege, hogy a számot mindig 10-el osztjuk és az osztás maradéka mindig egy decimális számjegyet ad, hiszen a maradék 0 és 9 közötti lehet. Nézzünk egy példát: 152 / 10 -> 15 és a maradék: 2 15 / 10 -> 1 és a maradék: 5 1 / 10 -> 0 és a maradék: 1 Amint ez látható tényleg a szám számjegyeit kapjuk meg, de fordı́tott sorrendben. Ezt is figyelembe kell venni az algoritmusnál. A 2. sorban az AX regiszterbe töltjük be azt a számot amit decimális formában szeretnénk kinyomtatni Érdemes arra is
gondolni, hogy mivel az AX regiszter 16 bites regiszter ezért 21 6 = 65536 féle számot tud csak tárolni, ahol a legkisebb szám a nulla és a legnagyobb szám a 65535. Erre az információra azért van szükség, mivel ı́gy már tudjuk, hogy maximum 5 számjegyet kell majd kinyomtatni. Ezt mutatja a 18 sorban az szamstr változó definı́ciója, ami 5 darab SPACE karaktert tartalmaz és a végén egy dollár jelet ($). A dollár jel azért kell a végére, hogy majd egyben tudjuk a számokat kinyomtatni az INT 21h megszakı́tással. A 3 sorban az SI regiszterbe azt a cı́met töltjük be ami az utolsó SPACE karakterre mutat a szamstr változóban. Az 5 sorban adjuk meg az osztót A 6. sorban azt készı́tjük elő, hogy majd az DX:AX számot osztjuk egy 16 bites regiszterrel és ı́gy majd az eredmény is egy 16 bites regiszterbe kerül. Az osztást a 7 sorban végezzük el A hányados az AX regiszterbe, a maradék a DX
regiszterbe kerül A maradék csak 0 és 9 közötti szám lehet A nyomtatáshoz a számot ASCII karakterré kell konvertálni, vagyis a számhoz hozzáadjuk a ‘0’ karakter ASCII kódját. A 8. sor után a DL regiszter tartalma a 30h és 39h értékek közötti szám lesz, mely megfelel a 0 és 9-es számok ASCII kódjának. A 9 sorban eltároljuk az ASCII kódot az SI regiszter által megadott helyre A 10. sorban az SI regisztert azért csökkentjük eggyel, hogy a következő karaktert az előző elé ı́rjuk A 11. sor a leállı́tási feltétel Itt azt vizsgáljuk, hogy a hányados zérus-e, vagyis nincs további számjegy amit konvertálni kellene. A 12 sor tartalmazza a feltételes ugrást, ami az ujra cı́mre ugrik ha ha van még szám amit konvertálni kell. Ha a hányados zéros, AX regiszter tartalma zérus, akkor nincs más hátra mint kinyomtatni az eltárolt számokat. A program olyan értelemben trükkös,
hogy a szamstr változó alap esetben SPACE karaktereket tartalmaz, és ı́gy ha csak két jegyű számot tárolunk el, azért még a program jól fog működni, mivel ebben az esetben a program legfeljebb 3 SPACE karaktert nyomtat a számok előtt. 112 1 org 100h 2 MOV AX, 54321 3 MOV SI, szamstr 4 ujra: 5 MOV BX, 10 ; 6 MOV DX, 0 ; 7 DIV BX ; 8 ADD DL, ’0’ ; 9 MOV [SI], DL ; 10 DEC SI ; 11 CMP AX, 0 12 JNZ ujra 13 MOV AH, 9 14 MOV DX, szamstr 15 INT 21h 16 INT 20h 17 szamstr: 18 db ’ ’,’$’ + 4 10-es osztassal valasztunk le egy jegyet DX:AX az osztando szam, BX az oszto hanyados -> AX, maradek -> DX szamjegy karakterre eltaroljuk visszafelé tároljuk 9.18 tábla: Szám kiı́rása decimális formában 113 9.13 Olvasás a memóriából A 9.19 program arra mutat egy példát, hogyan lehet a memóriában, egy fix cı́men lévő byte-ot megcı́mezni, illetve azt vizsgálni, hogy annak a byte-nak valahányadik bitje 1-e. A
0:417-es cı́men található byte ı́rja le a billentyűzet LED-ek állapotát. Például ha a 7 bit értéke 1, akkor a CAPS LOCK be van kapcsolva. Az alábbi program azt fogja vizsgálni, hogy a CAPS LOCK be van-e kapcsolva és annek megfelelő üzenetet fog kiı́rni. A 2. és 3 sorban az ES szegmens regiszterbe a zérus szegmens cı́met töltjük Mivel szegmens regiszterbe nem lehet közvetlenül értéket tölteni, ezért használjuk az AX regisztert. A 4 sorban direkt módon, szegmens regiszterrel együtt adjuk meg a vizsgálandó byte cı́mét. A byte-ot betöltjük a BL regiszterbe. Az 5 sor egy maszkolást hajt végre Lényegében a 40h = 0100 0000b értékkel és mivel az AND utası́tást használjuk ezért a művelet után csak a 7. bit értéke marad meg Például: 0101 1001 <- BL AND 0100 0000 <- Maszk -------------0100 0000 Ez azt jelenti, hogy az 5. sor után a BL regiszter értéke vagy zérus vagy pedig 40h
lesz Ezt a feltételt használja ki a 6. sorban a feltételes ugrás (Nem kell külön összehasonlı́tó utası́tás, CMP, mivel az AND utası́tás már megfelelően beállı́tja a Zérus bitet, ZF.) Mivel csak egy üzenetet akarunk kinyomtatni és csak a nyomtatandó üzenet más ezért attól függően hogy a zérust kaptunk-e a DX regiszterbe más cı́met töltünk. Fontos lehet még kiemelni a 8 sorban a feltétel nélküli ugrást, JMP Erre azért van szükség, mert miután beállı́tottuk a DX regiszter tartalmát a 7. sorban, már csak nyomtatni kell és nem szabad engedni hogy a DX regiszter tartalmát felülı́rjuk. Ha nincs a feltétel nélküli ugrás akkor pedig ez történne, mivel a 9. sorban folytatódna a program A 12. sortól már ugyanazt kell csinálni mind a két esetben, beállı́tani a funkció kódot az INT 21h megszakı́tásnak, meghı́vni a megszakı́tást, majd kilépni a programból. 1 2
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 org 100h mov mov mov and jnz mov jmp ax,0 es,ax bl,[es:417h] bl, 40h eg dx,kikapcs kiir eg: mov dx,bekapcs kiir: mov ah,9 int 21h int 20h kikapcs: db ’Ki van kapcsolva$’ bekapcs: db ’Be van kapcsolva$’ 9.19 tábla: CAPS LOCK állapotának nyomtatására szolgáló program 114 0.bit 1.bit 2.bit 3.bit 4.bit 5.bit 6.bit 7.bit az előtér kék szı́nösszetevője az előtér zöld szı́nösszetevője az előtér piros szı́nösszetevője az előtér intenzitása a háttér kék szı́nösszetevője a háttér zöld szı́nösszetevője a háttér piros szı́nösszetevője a villogás ki-bekapcsolása (a bit 1 értékénél villog) 9.20 tábla: A képernyő byte attribútumának bitjei 9.14 Közvetlen videó memóriába ı́rás Lehetőség van arra, hogy egy program közvetlenül a videokártya memóriájába ı́rjon adatot és ilyen módon közvetlenül a
képernyőre ı́rjunk. Ehhez persze tudnunk kell, hogy melyik szegmensen kezdődik a képernyő memória, vagyis hova van leképezve a videokártya memóriája. Általában a 0B800h a szegmens cı́me, kivéve például a Herkules video kártya (ma már nem használják ezt a kártyát). Tehát a 0B800h a szegmens cı́met fogjuk használni a 80x25 karakteres szöveges képernyő esetén. A nyomtatáshoz még azt is tudnunk kell, hogy milyen a képernyő felépı́tése. Minden megjelenı́tett karakterhez két byte tartozik: az egyik byte maga a karakter, a másik byte pedig a karakter attribútuma. Az attribútum a karakter szı́nét, intenzitását és villogását jelenti a 9.20 táblázat szerint A képernyő bal felső sarkához tartozik a zérus oszlop és zérus sor karakter pozı́ció. Hogyan lehet egy tetszőleges pozı́ciójú karaktert kinyomtatni? 115 9.15 Szöveg beolvasása Karakterenként olvasunk.
ESC-re vége Az INT 21 előkészı́tése és használata. 116 9.16 Beolvasott szövegben karakterek számlálása 117 9.17 Beolvasott szöveg nagy betűsre konvertálása 118 9.18 Feladatok 1. Írjunk programot, mely megszámolja, hogy egy byte-ban hány darab 1-es értékű bit van 2. Írjunk programot, mely bekér egy decimális számjegyet A számjegyről eldönti, hogy nagyobb-e mint öt és ennek megfelelő üzenetet ı́r ki: “Nagyobb mint öt”, “Kisebb mint öt”, “Egyenlő öttel”. 119 120 10. Fejezet Függvények 10.1 A verem adatszerkezet A verem tulajdonképpen egy LIFO (Last In First Out) adatszerkezet. Ez azt jelenti, hogy az utoljára elltárolt értéket olvashatjuk ki legelőször. A vermet angolul stack-nek is szokták nevezni Ha analógiát keresünk a verem adatszerkezetre, akkor talán egy raktárat képzelhetünk el. A padló a legalsó szint, amire tehetünk egy dobozt,
amire újabb dobozt tehetünk és ı́gy tovább. Ha a legfelső dobozra van szükségünk, akkor csak levesszük a kupac tetejéről. Ezzel szemben, ahhoz hogy a legalsó dobozt kivegyük, az összes többi felette levő dobozt le kell emelni. Amint látható egy verem esetén mindig csak a tetejéhez férünk hozzá (top-of-stack, TOS). A verembe helyezés műveletét PUSH-nak, a verem tetejéről való kivételt pedig POP-nak szoktuk nevezni. A 101 ábra bemutatja a verem egyszerűsı́tett működését. 10.11 A verem implementációja Az x86-os architektúra esetén a verem a memóriában található. Erre fontos lesz emlékezni később, mivel ezek szerint a vermet kezelhetjük LIFO adatszerkezetként, de mint véletlen hozzáférésű adatszerkezetként is! Mivel a verem a memóriában van ezért a megvalósı́tásához az SS szegmens és SP regisztert kell használni. Ez a regiszter páros (SS:SP) mutat mindig a
verem tetejére Az SS szegmens regiszter a verem szegmensének a cı́mét határozza meg, mı́g az SP regiszter a szegmensen belül adja meg a verem tetejének az offszetjét. Az 102a ábra azt mutatja, hogy az eddigiek alapján hogyan képzelhetjük el a vermet x86-os architektúra esetén. Sajnos ez a kép helytelen Az x86-os architektúra esetén a verem helyes képe a 10.2b ábrán látható Ezek alapján a verem adatszerkezet legfontosabb tulajdonságai: • Csak word méretű adat tölthető fel a veremre. Byte önmagában nem • A verem a magasabb memória cı́mtől az alacsonyabb memória cı́m felé “növekszik”. Ez a 102b ábra alapján azt jelenti, hogy a verem lefelé “növekszik”. 10.1 ábra: A verem működése 121 (a) (b) 10.2 ábra: a) A verem adatszerkezet helytelen képe b) A verem adatszerkezet valódi képe x86-os architektúrán. (a) (b) (c) 10.3 ábra: a) A verem elméleti képe üres
állapotban b) A verem állapota FEABh érték tárolása után c) A verem állapota 1234h érték tárolása után. • Az SS:SP regiszter páros mindig az utoljára eltárolt elemre mutat. Pontosabban az utoljára eltárolt szó (word) alsó byte-jára. A 10.3a ábra azt mutatja, hogy amikor a verem üres, akkor a verem hogyan néz ki “elméletileg” A 10.3b és 103c ábra azt mutatja, mi történik amikor adatokat teszünk a verembe Egy word feltöltése során először az SP regiszter értékét 2-vel csökkentjük majd a word-öt eltároljuk az SS:SP által mutatott memória cı́men. Amikor egy word-öt kiveszünk a veremből, először kimásoljuk az értéket az SS:SP által mutatott cı́mről, majd az SP regiszter értékét 2-vel csökkentjük. Itt kell megjegyeznünk, hogy a 10.3a ábra valóban csak elméleti képe a verem üres állapotának A magyarázathoz tegyük fel, hogy a veremben egy
érték, egy szó (word) van eltárolva. Amikor csak egy elem van a vermen akkor az SP regiszter értéke FFFEh. Amikor ezt az értéket kivesszük a veremből, akkor az SP regisztert meg kell növelni 2-vel. Ebben az esetben az SP értéke 10000h, ami nagyobb mint amit 16 biten tárolni lehetne, ı́gy az SP regiszter valójában a 0000h értéket tárolja majd. Ez azt jelenti, hogy a regiszter értéke “körbefordul” (wrap around) és az SP regiszter a szegmens elejére fog mutatni. 122 10.12 Verem műveletek Az x86-os architektúrán többféle utası́tás is létezik a verem kezelésére. Az alapvető verem műveleteknek mint push és pop van megfelelő assembly utası́tása: PUSH és POP. (Lásd 717 és 7110 bekezdés) További assembly utası́tások, melyek a vermet kezelik: • PUSHF (7.18 bekezdés) • PUSHA (7.19 bekezdés) • POPF (7.111 bekezdés) • POPA (7.112 bekezdés) 10.2 A verem használata A veremnek
több haszna is van az assembly nyelvben: • értékek időleges tárolása • paraméterátadás függvényeknek • lokális változóknak memória terület • vezérlés átadás 10.21 Értékek időleges tárolás A verem jól használható arra, hogy változókat és értékeket időlegesen eltároljunk. Például vegyük azt az esetet, hogy két változót fel akarunk cserélni: xchg [valtozo1], [valtozo2] Sajnos ez nem működik, mivel egy utası́tás kétszer nem férhet hozzá a memoriához. Az egyik megoldás a következő lehet: mov mov mov mov ax, [valtozo1] bx, [valtozo2] [valtozo1], bx [valtozo2], ax de ekkor két regisztert is használnunk kell és 4 memória műveletet végzünk. Ez a megoldás gondot jelenthet, mivel a végrehajtásához találnunk kellene 2 szabad regisztert, és sokszor problémás lehet egy programban. De mi van akkor, ha a fenti példában az AX és BX regiszterek
értékeire a csere után is szükség van? A megoldás az, hogy időlegesen el kell menteni az értéküket, el kell végezni a műveletet, majd helyre kell állı́tani a regiszterek értékét. ; mentsük el a regisztereket push ax push bx ; végezzük el a cserét mov ax, [valtozo1] mov bx, [valtozo2] mov [valtozo1], bx mov [valtozo2], ax 123 ; állı́tsuk helyre a regisztereket pop bx pop ax Bár ez a megoldás működik, de szükséges néhány megjegyzést tenni: 1. Először is a fenti megoldásban nyolcszor férünk hozzá a memóriához Ne felejtsük, hogy a verem műveletek is hozzá férnek a memóriához. 2. Másodszor, fontos észre venni, hogy ha a regiszterek eredeti értékét szeretnénk visszakapni, akkor amilyen sorrendben a regisztereket elmentjük a vermen azzal ellentétes sorrendben kell helyreállı́tani a regisztereket. Egy másik példa az időleges tárolásra: push dx push bx push cx . ;
utası́tások pop cx pop bx pop dx 3. Harmadszor, létezik egy elegánsabb megoldás a változók cseréjére push push pop pop [valtozo1] [valtozo2] [valtozo1] [valtozo2] Ebben a megoldásban pont az előző szabályt sértjük meg, de szándékosan, mivel ı́gy pontosan azt a hatást érjük el, hogy a regiszterek értékei felcserélődnek. Ezen kı́vül az is érdekes itt, hogy a fenti POP utası́tások kétszer férnek hozzá a memóriához, hiszen a veremből (memóriából) olvasunk és az argumentumban megadott memória cı́mre ı́runk. A POP utası́tás egy kivétel a szabály alól, vagyis hogy általában egy utası́tás nem férhet hozzá kétszer a memóriához. A másik kivétel ez alól a szabály alól a szöveg kezelő műveletek lesznek (lásd 12. fejezet) Nagyon fontos megérteni a fenti listában a 2. és 3 pont közötti különbséget Ebből a bekezdésből is jól látható, hogy a
verem mérete folyamatosan növekszik és csökken a program futása során. A paraméter átadást, lokális változók kezelését és vermen keresztüli vezérlés átadást a következő fejezetben tárgyaljuk. 10.3 Függvények definı́ciója A függvények önálló logikai program egységek amelyek valamilyen konkrét feladatot végeznek el. A függvényeket szokták alprogramnak is nevezni és nagyon fontos szerepet játszanak a moduláris program fejlesztésben. Bár bizonyos programozási nyelvek különbséget tesznek függvények és procedurák között, az assembly programozási nyelvben erre nincs szükség. Az assembly programozási nyelvben csak függvényeket definiálhatunk, amelyeknek paramétereket adhatunk át és kaphatunk vissza értéket. Ez a felfogás nagyban hasonlı́t a C programozási nyelvhez. Nagyon egyszerű definiálni egy függvényt assembly-ben. Két utası́tásra van
szükség: CALL (lásd 7.57 bekezdés) és RET (lásd 758 bekezdés) utası́tásokra Nézzünk egy egyszerű példát: 124 org 100h xor ax, ax call fvg int 20h fvg: add ax, 2 ret Ebben a példában a függvény neve fvg. Más assemblerekkel ellentétben, ahol speciálisan kell egy függvényt definiálni a NASM assembler esetén igen egyszerűen csak egy memória cı́met kell megadni, ami a függvény eleje és a végére egy RET utası́tás kell. Érdemes megvizsgálnunk a függvények működését is. A CALL utası́tás végrehajtása során, mivel a CALL utası́tás már betöltődött az utası́tás “értelmezőbe” ezért az IP regiszter a következő utası́tásra mutat, ahol a programnak folytatnia kell a működését miután a függvény lefutott. Erre a processzor úgy fog emlékezni, hogy ezt a cı́met elmenti a veremre. Ezután a CALL utası́tás a megadott cı́mre adja át a
vezérlést, vagyis az IP regiszter értékét megváltoztatja. Nézzük meg az előző példából generált gépi kódot: xxxx:0100 31C0 xxxx:0102 E80200 xxxx:0105 CD20 xxxx:0107 050200 xxxx:010A C3 org 100h xor ax, ax call fvg int 20h fvg: add ax, 2 ret Látható, hogy a CALL fvg utası́tásból E8 02 00 gépi kód keletkezik. Az E8-as érték adja meg az utası́tás kódját és a következő két szám pedig az fvg szimbólikus cı́mnek felel meg. A 02 00 értékek viszont nem felelnek meg a 107-es cı́mnek. Mi történik itt? Arról van szó, hogy a CALL utası́tás képes közeli és távoli ugrásra is. Amikor a CALL utası́tás és a függvény egy szegmensen belül van, akkor elegendő csak az offszetet megadni. A szegmens regiszterre nincs szükség. Ez látható a jelenlegi példában is, bár itt az fvg szimbólikus cı́mből nem konkrét cı́met számol ki az assembler, hanem egy eltolási
értéket. Azt számolja ki, hogy hány byte-nyit kell ugrania a CALL utası́tásnak ahhoz, hogy a program a függvény kezdő cı́ménél folytassa a végrehajtást. A gépi kódban a 02 00 byte sorozat található az E8-as utası́tás kód után, ami valójában 0002-nak felel meg, hiszen little-endian tárolási módot használ az x86-os architektúra. A 0002 érték azt adja meg, hogy a függvény kezdete két byte-nyi távolságra van és ı́gy a 105+2=107-es cı́men kell folytatni a végrehajtást. A függvényeknél az is nagyon fontos, hogy a függvény végén a RET utası́tásnak szerepelnie kell. Ez azért nagyon fontos, mivel a CALL utası́tás által a vermen eltárolt cı́met ez az utası́tás veszi le a veremről majd az ı́gy kapott cı́mre adja át a vezérlést. Az utası́tás működésének egy “különleges” demonstrálására nézzünk egy utası́tás sorozatot ami megfelel egy
feltétel nélküli ugró utası́tásnak: . push cim ret . cim: ; jmp cim A PUSH utası́tás a cim szimbólikus cı́met tölti fel a veremre, amit a RET utası́tás levesz és erre a cı́mre adja át a vezérlést. Tulajdonképpen a RET utası́tás ı́gy vezérlés átadást végez a verem felhasználásával, ami a verem egyik felhasználási módja. 125 10.31 Egymásba ágyazott függvényhı́vások A függvények egymásba ágyazhatók, ami azt jelenti, hogy egy függvény is hı́vhat egy másik függvényt. A 10.1 tábla egy “mesterséges” példát mutat arra, hogy a függvényeket hogyan lehet egymásba ágyazni. A 104 ábra pedig azt mutatja be, hogy a 101 táblán bemutatott program végrehajtása során az IP regiszter milyen értékeket vesz fel (az utası́tásokat milyen sorrendben hajtjuk végre) és a verem milyen értékeket tárol egy adott pillanatban. A veremben tárolt értékek
ebben a példában csak visszatérési cı́mek, a forrás program sorszámai. Az ábrában a nyı́l az SS:SP regiszter páros által mutatott cı́met jelöli. Nézzünk egy példát, ahol a 10.4d ábrán a verem tetején a visszatérési érték 6, ami a jelen példában a 6. sort jelenti a forráskódban, és a következő végrehajtandó utası́tás a 10 sorban található (IP=10) A 10. sorban egy RET utası́tás van, ami a verem tetejéről leveszi a 6-os értéket és ezt tölti be az IP regiszterbe, ı́gy nem véletlen, hogy a 10.4e ábrán már eggyel kevesebb érték van és a következő utası́tás amit végre kell hajtani az a 6. sorban található, vagyis IP=6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 org 100h call muvelet int 20h muvelet: call torol call addketto ret torol: xor ax, ax ret addegy: inc ax ret addketto: call addegy call addegy ret 10.1 tábla: Példa az egymásba ágyazott függvényekre
10.4 Paraméter átadás függvényeknek A paraméter átadás egy kicsit komplikáltabb mint más magas szintű programozási nyelvekben. Assemblyben az a program részlet amelyik szeretne egy függvényt meghı́vni a paramétereket egy olyan helyre másolja amit önmaga és a függvény is elér, és csak ezután történik a függvényhı́vás. Háromféle lehetőségünk van a paraméter átadásra közös területen keresztül: • regiszteren, • memórián és • vermen keresztül. 126 (a) IP=2 (b) IP=5 (c) IP=9 (d) IP=10 (e) IP=6 (f) IP=15 (g) IP=12 (h) IP=13 (i) IP=16 (j) IP=12 (k) IP=13 (l) IP=17 (m) IP=7 (n) IP=3 10.4 ábra: A 101 táblán látható program nyomonkövetése 127 10.41 Paraméter átadás regiszteren keresztül A függvény működéséhez szükséges paramétereket regiszterekbe tesszük a függvény meghı́vása előtt. A módszerre nézzünk egy példát,
melyet a 10.2 tábla mutat be A program beolvas egy karaktert majd azt egy függvény segı́tségével ismét kinyomtatja, de a nyomtatást egy függvény végzi. 1 org 100h 2 MOV AH, 01 3 INT 21h 4 CALL charnyomtat 5 INT 20h 6 ; nyomtatandó karakter AL-ben adódik át 7 charnyomtat: 8 MOV AH, 02 9 MOV DL, AL 10 INT 21h 11 RET 10.2 tábla: Paraméter átadás regiszteren keresztül A módszernek vannak előnyei és hátrányai: • Előnyök – A módszer kényelmes és könnyű kis számú paramétert átadni egy függvénynek. – A módszer nagyon gyors, mivel minden paraméter már a regiszterekben lesz a függvényben. • Hátrányok – A fő hátrány, hogy csak néhány paramétert lehet regiszteren keresztül átadni, mivel csak limitált mennyiségű általános regiszter áll rendelkezésre. – Mivel a hı́vási metódus során bizonyos regisztereknek speciális szerepük lehet, ezért szükség lehet
ezeket addig a vermen elmenteni, amı́g a paraméterátadás meg nem történik. Ugyanakkor ez azt is jelenti, hogy a második előnyt elveszı́tjük, hiszen a paraméterátadásban verem műveletre is szükség van. 10.42 Paraméter átadás memórián keresztül A memórián keresztüli paraméter átadás hasonló a regiszteren keresztüli paraméter átadáshoz. Ebben az esetben lényegében a memóriában kijelölünk egy területet ahova a paramétereket a hı́vó program részlet bemásolhatja és ahonnan a függvény majd kiveheti a paramétereket. Ezt a megoldást is érdemes egy példával demonstrálni, ami 10.3 táblán látható A módszer előnye, hogy akárhány paraméter átadható egy függvénynek. Fő hátránya, hogy memória elérésre van szükség az összes paraméter átadása során Erre egy példát a 10.4 tábla mutat Ebben a példában a függvény három számot ad
össze 128 1 2 3 4 5 6 7 8 9 10 11 12 13 14 org 100h MOV AH, 01 INT 21h MOV [char], AL CALL charnyomtat INT 20h ; nyomtatandó karakter a memóriában charnyomtat: MOV AH, 02 MOV DL, [char] INT 21h RET char: db 0 10.3 tábla: Paraméter átadás memórián keresztül 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 org 100h ; regiszterek beállı́tása, ; amit valamilyen számı́tásból kaptunk MOV AX, 0001h MOV BX, 0010h MOV CX, 0100h ; . ; paraméter átadás memórián keresztül MOV SI, paramtabla MOV [SI], AX MOV [SI+2], BX MOV [SI+4], CX CALL osszead ; az eredmeny a DX regiszterben INT 20h ; regiszterek összeadása osszead: MOV SI, paramtabla MOV DX, [SI] ADD DX, [SI+2] ADD DX, [SI+4] RET paramtable: dw 0, 0, 0 10.4 tábla: Második példa a paraméter átadásra memórián keresztül 129 10.5 ábra: Verem állapota amikor két paramétert adunk át egy függvénynek a vermen keresztül 10.43 Paraméter
átadás vermen keresztül Ez az egyik leggyakoribb paraméter átadási módszer. Ebben az esetben a függvény paramétereit a vermen tároljuk el és csak utána hı́vjuk meg a függvényt. Ezt a módszert is egy példán keresztül lehet a legjobban bemutatni. Vegyünk egy olyan függvényt aminek két számot kell átadni: push [szam1] push [szam2] call osszead Miután a CALL utası́tást végrehajtottuk a verem állapota a 10.5 ábrán látható Az ábrából kitűnik, hogy a függvényen belül igen nehéz hozzáférni a paraméterekhez, mivel a verem tetején a visszatérési cı́m is ott van. A függvényen belül például megtehetjük a következőt: pop ax pop bx pop cx push ax ; ; ; ; IP ide kerül szam2 ide kerül szam1 ide kerül IP cı́m vissza kerül a veremre Lényegében először a visszatérési cı́met vesszük le, utána a paramétereket és végül a visszatérési cı́met vissza
kell tenni a veremre. A probléma ezzel a megoldással az, hogy félre kell tenni regisztereket a paraméterek kinyerésére, vagyis ezeket a regisztereket másra nem használhatjuk és ráadásul azt a helyzetet sem tudjuk kezelni amikor például több mint 10 paramétert szeretnénk átadni a függvénynek. A problémára a legjobb megoldás, hogy a paramétereket hagyjuk a vermen és csak akkor férjünk hozzájuk amikor a paraméterek kellenek. Ez az a pont ahol fontos arra emlékezni, hogy a verem a memóriában található és ı́gy memóriaként is kezelhető (lásd 10.11 bekezdés) Ez azt jelenti, hogy az adatok a veremben egymás utáni memória cı́meken találhatók és hivatkozhatunk rájuk SP, SP+2 és SP+4 cı́mekkel. Ebben az esetben az SP (Stack Pointer) regiszter mint egy viszonı́tási pont szolgál Így egy paraméter kiolvasását a következőképpen tehetjük meg: MOV BX, [SP+2] ; szam2 ide kerül MOV
CX, [SP+4] ; szam1 ide kerül Sajnos még ezzel a megoldással is van egy kis probléma. Az jelenti itt a problemát, hogy az SP regiszter értéke folyamatosan változhat a függvényen belül, mivel a függvényben bárhol használhatunk PUSH és POP utası́tásokat. Ha mégis ennél a megoldásnál maradunk, akkor arra kell nagyon figyelni, hogy az SP regiszternek mindig az aktuális értékét vegyük alapul. Például az előző programnál maradva a paraméterek kiolvasása módosul amikor a függvényen belül további verem műveleteket végzünk. PUSH [szam1] PUSH [szam2] CALL osszead 130 10.6 ábra: Verem állapota amikor két paramétert adunk át egy függvénynek a vermen keresztül, de függvényben az SP regiszter értéke is módosul . osszead: PUSH AX PUSH BX ; verem állapota az ábrán MOV AX, [SP+6] ; szam2 ide kerül MOV BX, [SP+8] ; szam1 ide kerül . A 10.6 ábra a verem állapotát mutatja be
akkor, amikor éppen a szam2 paramétert szeretnénk kiolvasni Az ábrából látható, hogy az SP regiszter által mutatott cı́m és a például a szam2 paraméter cı́me közötti távolság megváltozott. Mindezek után nézzük a “legjobb” megoldást. Az előző megoldással az volt a probléma, hogy az SP regiszter állandóan változhat. Ennek megoldására amikor belépünk a függvénybe készı́tünk egy “fix” pontot, amihez képest a paramétereket elérhetjük. Az SP regiszter helyett a BP regisztert is használhatjuk referencia pontként, miután az SP értékét belemásoltuk. Az előző példa végleges megoldását a vermen kereszt-li paraméter átadásra a 10.5 tábla mutatja be A programhoz tartozó verem állapotait a 10.7 ábra mutatja be A következőkben nézzük meg, hogyan alakul ki a verem állapota. A programban a paraméterek eltárolása ugyanúgy történik mint eddig, két
PUSH utası́tással feltöltjük az értékeket a veremre a 4. és 5 sorban Ez látható a 107a ábrán A 6 sorban található CALL utası́tás feltölti a 7. sor cı́mét a veremre, lásd a 107b ábra Ez az ábrában az IP értékkel van jelölve. Ezután a CALL utası́tás átadja a vezérlést az osszead cı́mre Itt először eltároljuk a BP regiszter értékét a 12. sorban, ahogy ez a a 107c ábrán látható Erre azért van szükség, mivel minden függvény ezt a regisztert használja és felül fogjuk ı́rni, de a függvényből kilépve szükség lehet az eredeti értékére. (Így a 12 sorban a vermet időleges érték tárolásra használjuk) Ezután az SP regiszter aktuális értékét átmásoljuk a BP regiszterbe a 13. sorban Ezzel lényegében létrejött a stack frame vagy “függvény keret”. Ez látható a 107d ábrán A függvényhez tartozó stack frame a függvényre
vonatkozó minden fontos információt tartalmaz: • a függvény paramétereit, • a függvény visszatérési értékét, • az elmentett BP regiszter értékét és • a lokális változókat is. (A lokális változókról a 105 bekezdésben olvashatunk) A fenti kialakı́tás miatt szokták a BP regisztert “frame pointer”-nek is nevezni. Ezután a 14 és 15 sorban elmentünk 2 regisztert, mely utası́tások módosı́tják az SP regisztert. Ez látható a 107e ábrán 131 Végül a 16. és 17 sorban kiolvassuk a két paramétert és összeadjuk őket A 107f ábrán látható az, hogy a paraméterek kiolvasásához szükséges cı́meket hogyan számolhatjuk ki. Természetesen ahogy felépı́tettük a vermet, úgy a függvény végén vissza is kell állı́tani, hogy az eredeti állapotot kapjuk vissza, mintha a függvény hı́vással semmi sem történt volna. Ez azt jelenti, hogy amilyen
sorrendben feltöltöttük az értékeket a veremre, azzal ellentétes sorrendben le kell vennünk a veremről. Így először helyreállı́tjuk az elmentett regisztereket a 18 és 19 sorban A függvény végén a BP regisztert is vissza kell állı́tani, hiszen lehet hogy ezt a függvényt egy másik függvény hı́vta meg és a BP regiszter a hı́vó függvény frame pointer-ét tartalmazza. A BP regiszter visszaállı́tását a 20 sorban végezzük el Végül a függvényből a RET paranccsal lépünk ki a 21 sorban Ahogy ezt már fent leı́rtuk, a RET utası́tás leveszi a függvény visszatérési értékét. Ebben a pillanatban a verem állapotát a 10.7a ábrának felel meg Az ábra jól mutatja, hogy a függvény hiába tért vissza a 7 sorhoz a veremben még vannak adatok. Ezeket az adatokat el kell távolı́tani a veremről, mivel a függvényhı́vás után nincs értelme a vermen tartogatni, hiszen
nincs többé hasznuk. Ezen kı́vül ha nem távolı́tjuk el, akkor a sokszori függvényhı́vás megtöltené a vermet. Két lehetőségünk van ezt a problémát orvosolni: • a hı́vó programrészlet takarı́t vagy. • a hı́vott programrészlet takarı́t. Ezeket a stratégiákat tárgyaljuk a következő bekezdésben. 1 org 100h 2 . 3 ; paraméter átadás vermen keresztül 4 PUSH [szam1] 5 PUSH [szam2] 6 CALL osszead 7 ADD SP, 4 8 ; az eredmeny az CX regiszterben 9 . 10 INT 20h 11 osszead: 12 PUSH BP 13 MOV BP, SP 14 PUSH AX 15 PUSH BX 16 MOV CX, [BP+6] ; szam1 kiolvasása 17 ADD CX, [BP+4] ; szam2 hozzáadása 18 POP BX 19 POP AX 20 POP BP 21 RET 10.5 tábla: Példa a paraméter átadásra a vermen keresztül Függvényhı́vás utáni takarı́tás Az előző bekezdésben, a 10.5 táblán bemutatott programban a függvényhı́vás után törölni kell 4 bytenyi értéket Használhatnánk például ezt a
megoldást is: POP CX POP CX 132 (a) (b) (d) (c) (e) (f) 10.7 ábra: Verem állapotai a 105 táblán bemutatott programban 133 de ezzel az a gond, hogy tönkretesszük a CX regiszter értékét. Így itt is inkább a vermet, mint memória tömböt kezeljük és ezért csak az SP regisztert módosı́tjuk: ADD SP, 4 Mivel a 10.7 ábrán a memória cı́mek lentről felfelé növekednek, ezért ahhoz hogy “eldobjunk” 4 byte-nyi értéket a veremről az SP regiszter értékét meg kell megnövelni. Az eldobás szó azért szerepel idézőjelek között, mert valójában az értékeket nem töröljük, csak annyi történik, hogy az SP regiszter mozgatásával átállı́tjuk a foglalt és a szabad részek közötti határt. Az SP regiszter által mutatott cı́mnél nagyobb cı́men elhelyezkedő értékek foglaltak, a kisebb cı́men elhelyezkedő értékek szabadok, azokat bármikor
felülı́rhatjuk, törölhetjük. Azt is fontos megérteni, hogy a következő teljesen helytelen: osszead: . ADD SP, 4 RET mivel ı́gy a visszatérési értéket is eltávolı́tanánk az ADD utası́tással. Ebben az esetben a LIFO szabály is sérül, hiszen a függvény hı́vás során alkalmazott sorrenddel ellentétes módon kellene mindent helyreállı́tani, de itt előbb próbáljuk meg felszabadı́tani a paraméterek helyét és csak utána akarjuk levenni a veremről a visszatérési értéket. Ha mégis ezt a megoldást szeretnénk, vagyis hogy a hı́vott függvény takarı́tson a vermen, akkor a RET utası́tásnak egy opcionális paramétert kell megadni. Például a: RET 4 utası́tás azt jelenti: IP = [SS:SP] SP = SP + 2 + 4 A fenti műveletben a 2 azért kell, hogy a visszatérési cı́met eltávolı́tsuk a veremről, a 4-es érték pedig a RET után megadott érték, és annyi byte-ot “dobunk el”
a veremről. Így első ránézésre furcsa lehet, hogy két különböző stratégia is van egy függvény paramétereinek letakarı́tására a veremről: 1. a hı́vó programrészlet takarı́t vagy 2. a hı́vott programrészlet takarı́t Hogy melyiket haználjuk, az attól függ, hogy a függvény paramétereinek száma fix vagy változó lehet. Ha egy függvénynek fix számú argumentuma van, akkor assembly-ben a 2. megoldás preferált Ez azért van, mert ı́gy csak egyszer, a függvény végén kell implementálni ezt a kódrészletet. Ugyanakkor ha egy függvénynek változó számú argumentuma lehet, akkor csak az 1. megoldás használható Mit jelent az, hogy változó számú argumentum? Itt gondoljunk például a C programozási nyelvben használt printf függvényre. Ezt a függvényt többféleképpen is meghı́vhatjuk: printf(’’Hello’’); printf(’’Az eredmeny: %d’’, ertek);
printf(’’x: %lf -- y: %lf’’, x, y); Erre a kérdésre még visszatérünk a 14. fejezetben 134 Állapotmegörzés A 10.5 táblán bemutatott programban a függvény elején elmentünk regisztereket a PUSH utası́tással, illetve a végén helyreállı́tjuk őket a POP paranccsal. Miért lehet erre szükség? Vegyük a következő programrészletet: MOV CX, szam ciklus: CALL szamolo . LOOP ciklus . Ebben a programrészletben a CX regiszter tárolja a ciklus változó értékét. Ha a szamolo függvény módosı́tja a CX regisztert, akkor a program logikája helytelen lesz, hiszen a CX regiszter értéke nem fut végig az általunk megadott tartományon. Ezért nagyon fontos, hogy minden olyan regisztert amit a függvényben használunk, azt a függvény elején elmentsünk, majd a függvény végén helyreállı́tsunk. Itt is felmerülhet a kérdés, hogy a hı́vó vagy a hı́vott programrészlet végezze a
mentést és a helyreállı́tást. Mi történik akkor, ha a hı́vó programrészletnek kell a regisztereket elmenteni? • A program karbantartása hihetetlenül nehéz lenne, mivel ha később a hı́vott függvény módosı́tjuk és az eredetitől eltérő regisztereket használna a függvény, akkor mindenhol ahol a függvényt meghı́vjuk módosı́tani kellene a programot. • A program mérete megnövekedne, mivel ha egy függvényt többször meghı́vunk, akkor minden alkalommal a regiszterek elmentését és helyreállı́tását is le kell programozni. Ezen okoknál fogva a regiszterek mentését és helyreállı́tását csak a hı́vott függvényben szoktuk leprogramozni, ahogy ez a 10.5 táblán bemutatott programban is láttuk Ez az megfontolás megfelel a moduláris programozás elveinek is. Még egy kérdést érdemes tisztázni: miért nem használjuk mindig a PUSHA és POPA utası́tásokat, amelyek
minden regisztert elmentenek és helyreállı́tanak? • Először is előfordulhat, hogy egy függvény vissza akar adni egy értéket a hı́vó függvénynek. Ezt általában regiszteren keresztül szoktuk megtenni, méghozzá az AX regiszteren keresztül. Ebben az esetben az AX regisztert nem kell elmenteni és nem szabad visszaállı́tani, vagyis felülı́rni a visszaadandó értéket. • Másodszor a PUSHA utası́tás végrehajtása 5 órajel ciklust igényel, mı́g egy PUSH utası́tás csak 1 órajel ciklusig tart. Ez azt jelenti, hogy a PUSHA utası́tásnak csak akkor van értelme ha 5 regiszternél többet akarunk elmenteni. 10.44 Érték és cı́m szerinti paraméter átadás Csak a teljesség kedvéért érdemes itt megemlı́teni az érték és cı́m szerinti paraméter átadás közötti különbséget. A témára még visszatérünk a 14 fejezetben is A legtöbb programozási nyelvben, ı́gy a C
programozási nyelvben is, az érték szerinti paraméter átadás az alapértelmezett módszer. Ez azt jelenti, hogy egy értéket amit szeretnénk átadni a függvénynek azt átmásoljuk a függvény “területére”. Az eddigiek alapján ez megfelel annak, hogy az értéket felmásoljuk a függvény stack frame-jébe. A cı́m szerinti paraméter átadás esetén a változó cı́mét adjuk át a függvénynek, vagyis a változó cı́mét felmásoljuk a függvény stack frame-jébe. A két módszer közötti különbség bemutatására a 10.6 táblán és a 107 táblán bemutatott programokat érdemes összehasonlı́tani. Mind a két program beolvas két karaktert, amelyeket eltárolunk a memóriában, majd 135 1 org 100h 2 ; két karakter beolvasása 3 MOV AH, 01 4 INT 21h 5 MOV [char], AL 6 MOV AH, 01 7 INT 21h 8 MOV [char+1], AL 9 10 PUSH word [char] 11 CALL char2nyomtat 12 INT 20h 13 14 char2nyomtat: 15
PUSH BP 16 MOV BP, SP 17 PUSH AX 18 PUSH DX 19 PUSH BX 20 MOV BX, [BP+4] 21 MOV AH, 02 22 MOV DL, BL 23 INT 21h 24 MOV DL, BH 25 INT 21h 26 POP BX 27 POP DX 28 POP AX 29 POP BP 30 RET 31 char: db 0, 0 10.6 tábla: Érték szerinti paraméter átadás a vermen keresztül egy függvény segı́tségével a karaktereket kinyomtatjuk. A 106 táblán látható programban, a 10 sorban a char változó értékét feltöltjük a veremre Az ı́gy feltöltött értéket a frame stack-ből a 20 sorban visszaolvassuk a BX regiszterbe, majd külön-külön kinyomtatjuk a BL és BH regiszter tartalmát. A 10.7 táblán látható program egy kicsit más Ebben a programban a 10 sorban a char változó cı́mét töltjük fel a verembe és a 20. sorban ezt a cı́met olvassuk ki a veremből majd másoljuk át a BX regiszterbe. Így a 22 és 24 sorban az átadott cı́mről olvassuk ki a nyomtatandó karaktereket Egy kicsit mesterkéltnek tűnhet
ez a példa, de a valódi programok esetén is van jelentősége a cı́m szerinti paraméter átadásnak. Az egyik legfontosabb alkalmazási területe a cı́m szerinti paraméter átadásnak, amikor egy tömböt szeretnénk átadni egy függvénynek. Ilyenkor nem érdemes a teljes tömböt feltölteni a veremre, hanem elegendő csak a tömb cı́mét átadni a függvénynek. A 108 tábla egy másik példát mutat a cı́m szerinti paraméter átadásra, amiben egy szöveget nyomtatunk ki egy függvénnyel. 10.45 Változó számú paraméter átadása függvénynek A C programozási nyelvben arra is lehetőség van, hogy egy függvény változó számú paramétert fogadjon el. Ilyen függvények a scanf és a printf függvények Ebben az esetben a hı́vott függvény 136 1 org 100h 2 ; két karakter beolvasása 3 MOV AH, 01 4 INT 21h 5 MOV [char], AL 6 MOV AH, 01 7 INT 21h 8 MOV [char+1], AL 9 10 PUSH char 11
CALL char2nyomtat 12 INT 20h 13 14 char2nyomtat: 15 PUSH BP 16 MOV BP, SP 17 PUSH AX 18 PUSH DX 19 PUSH BX 20 MOV BX, [BP+4] 21 MOV AH, 02 22 MOV DL, [BX] 23 INT 21h 24 MOV DL, [BX+1] 25 INT 21h 26 POP BX 27 POP DX 28 POP AX 29 POP BP 30 RET 31 char: db 0, 0 10.7 tábla: Cı́m szerinti paraméter átadás a vermen keresztül nem tudja előre, hogy paramétert adunk át neki. Többféle megoldás is létezik ennek a szituációnak a kezelésére. Az egyik legegyszerűbb módszer, hogy az első paraméter megadja, hogy hány további paraméter kerül a veremre. Egy dologra azonban figyelni kell, hogy a paraméterek száma az utolsó legyen amit feltöltünk a veremre, éppen a visszatérési cı́m fölé. Ezt a helyzetet a 108 ábra mutatja be A 10.9 tábla egy érdekes minta programot mutat be az előbb bemutatott változó paraméterű függvényekre. A program folyamatosan olvas számjegyeket (“bármennyit”), amı́g nullát nem
adunk meg. Ezeket a számjegyeket átadjuk egy függvénynek, ami összeadja a számokat és visszaadja az összegüket. Végül a program az összegnek megfelelő darab pontot nyomtat ki a képernyőre Nézzük a program működését: A 2. sor egy előkészı́tés, a CX regisztert lenullázzuk, mivel a CX regiszterben fogjuk számolni, hogy hány számot adott meg a felhasználó. Az 4 és 5 sorban olvasunk be egy számjegyet. Itt ellenőrizni is kellene, hogy csak számjegyeket adhat meg a felhasználó, de ettől az ellenőrzéstől most eltekintünk. A 6 sorban a karakter ASCII kódját számmá konvertáljuk Ha ennek a műveletnek az eredménye zérus, akkor a felhasználó a zérus számot adta meg, és véget ér a beolvasás. A 8. és 9 sor segı́tségével feltöltjük a beolvasott számot a veremre és a 10 sorban megnöveljük a számlálót. Így a CX regiszter azt fogja mutatni, hogy hány
értéket töltöttünk fel a veremre A 14 sorban magát a számlálót is feltöltjük a veremre. A 15 sorban szereplő CALL utası́tással átadjuk a 137 1 org 100h 2 PUSH szoveg ; szöveg cı́me 3 CALL nyomtat 4 INT 20h 5 6 nyomtat: 7 PUSH BP 8 MOV BP, SP 9 PUSH AX 10 PUSH DX 11 MOV AH, 09 12 MOV DX, [BP+4] ; a cı́met olvassuk ki 13 INT 21h 14 POP DX 15 POP AX 16 POP BP 17 RET 18 szoveg: db ’Hello vilag$’ 10.8 tábla: Szöveg nyomtatása függvénnyel, mely demonstrálja a cı́m szerinti paraméter átadást a vermen keresztül vezérlést a függvénynek. A 29 és 30 sor befejezi a függvény stack frame-jének előkészı́tését Ekkor a 10.8 ábrán látható állapot alakul ki a vermen A 31 sorban elmentjük a CX regisztert, mivel a 32. sorban betöltjük a paraméterek számát a CX regiszterbe A végeredmény az AX regiszterben fog kialakulni ezért a 33. sorban lenullázzuk az AX regisztert A 34 sorban az SI
regisztert úgy készı́tjük elő, hogy ha ezt az értéket hozzáadjuk a BP regiszterhez, akkor rögtön az első paraméterre fog mutatni. Ezután a 36 sorban az AX regiszterhez adjuk az aktuális paramétert A 37 sorban az SI regiszter értékét kettővel növeljük meg, mivel a vermen 2 byte-onként, vagyis word-önként, vannak a paraméterek eltárolva. A 38 sorban található LOOP utası́tás az előzőleg a CX regiszterbe betöltött számszor végrehajtja a ciklust. A 34-38 sorok közötti részt meg lehetne oldani másképpen is, például: ADD BP, 6 ujra ad: ADD AX, [BP] LOOP ujra ad 10.8 ábra: A verem állapota, változó számú paraméter esetén 138 de ennek a módszernek az a hátránya, hogy a BP regisztert folyamatosan módosı́tjuk és ı́gy többet nincs lehetőségünk az eredeti számlálót elérni. A 39-41 sorok között megfelelően befejezzük a függvényt Az egyik érdekesebb
részlet a 16-18. sorok között található, mivel a függvény előtt feltöltött értékeket le is kell takarı́tani a veremről. Ugyanakkor nem tudjuk előre, hogy hány darab értéket kell letörölni a veremről. Ezt a problémát itt úgy oldjuk meg, hogy először a CX regiszter értékét megnöveljuk 1-el. Így most a CX regiszter a paraméterket számát plusz egy értéket tartalmaz A plusz egy arra kell, hogy a számlálót is feltöltöttük a veremre. Ezután megduplázzuk a CX értékét, mivel a vermen word méretű adatokat tárolunk. Végül a 18 sorban az SP regiszter módosı́tásával eldobjuk a feltöltött értékeket a veremről. A 20-25. sorok között annyi pont karaktert nyomtatunk ki, amekkora számot az AX regiszterben a függvény visszaadott. Erre már láttunk példát korábban 139 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
36 37 38 39 40 41 org 100h mov cx, 0 ujra olvas: mov ah, 1 int 21h sub al, ’0’ jz olvas vege xor ah, ah push ax inc cx jmp ujra olvas olvas vege: push cx call osszead inc cx ; a szamlalo is bele tartozzon add cx, cx ; cx = cx * 2 add sp, cx mov cx, ax mov ah, 2 mov dl, ’.’ nyomtat: int 21h loop nyomtat int 20h osszead: push bp mov bp, sp push cx mov cx, [bp+4] xor ax,ax mov si, 6 ujra ad: add ax, [bp+si] add si, 2 loop ujra ad pop cx pop bp ret 10.9 tábla: Változó számú szám összeadása és annyi pont nyomtatása 140 10.5 Lokális változók függvényekben Az eddigi bekezdésekben nem esett arról szó, hogy hogyan kezeljük a függvények lokális válotzóit. Például vegyük a következő C program részletet: int szamol(int a, int b) { int temp, n; temp = a; n = b; . } Ebben a a program részletben az n és temp változók akkor jönnek létre amikor belépünk a függvénybe, és a változók megszünnek amikor
kilépünk a függvényből. Így ezekre a változók lokálisak és “dinamikusak” Az ilyen dinamikus változóknak helyet foglalhatnánk az adat szegmensben is, de ezt a módszert két oknál fogva nem szoktuk használni: 1. Helyfoglalás az adat szegmensben statikus és akkor is megmarad a terület amikor már nem a függvényben vagyunk, nem a függvény utası́tásait hajtjuk végre. 2. Ennél fontosabb indok, hogy ez a fajta helyfoglalás nem működik rekurzı́v függvényekkel A rekurzı́v függvényekkel a 10.6 bekezdésben foglalkozunk Ezért a lokális változókat a vermen szoktuk létrehozni. A fenti C függvény esetén az assembly-ben létrehozott verem a 10.9 ábrán látható módon néz ki Ahogy ez látható, a BP regiszter, mint frame pointer, a lokális változók elérésére is alkalmas. A lokális változók lefoglalása igen egyszerűen megoldható, csak megint memória tömbként kell
tekinteni a veremre és két lokális változó esetén ı́gy néz ki: SUB SP, 4 Mivel egy változót 2 byte-on tárolunk, ezért négyet vonunk ki az SP regiszterből. A kivonásra pedig azért van szükség, mert ı́gy a verem tetejét elmozdı́tjuk és szabad helyek jönnek létre. Ezek után nézzük meg, hogyan lehet a fenti C programrészletet megvalósı́tani assembly-ben: PUSH [b] PUSH [a] CALL szamol ADD SP, 4 . szamol: PUSH BP MOV BP, SP SUB SP, 4 PUSH AX . MOV AX, [BP+4] MOV [BP-2], AX MOV AX, [BP+6] MOV [BP-4], AX . POP AX ADD SP, 4 POP BP RET ; ; ; ; ’a’ változó betöltése ’temp’ változóban tárolás ’b’ változó betöltése ’n’ változóban tárolás 141 10.9 ábra: Lokális változók helye a stack frame-ben Itt fontos megérteni, hogy a lokális változók az assembly kódban tulajdonképpen “átalakulnak” abban az értelemben, hogy például a fenti programrészletben a temp
változóra [BP-2] módon lehet hivatkozni, illetve a n változóra [BP-4] módon lehet hivatkozni. Ezért az assembly programokban makrót (11. fejezet) szoktak használni az ilyen változók megnevezésére: %define temp [BP-2] %define n [BP-4] . PUSH [b] PUSH [a] CALL szamol ADD SP, 4 . szamol: PUSH BP MOV BP, SP SUB SP, 4 PUSH AX . MOV AX, [BP+4] ; MOV temp, AX ; MOV AX, [BP+6] ; MOV n, AX ; . POP AX ADD SP, 4 POP BP RET ’a’ változó betöltése ’temp’ változóban tárolás ’b’ változó betöltése ’n’ változóban tárolás 10.51 ENTER és LEAVE utası́tások A 386-os Intel processzor óta van két speciális utası́tás amely közvetlenül támogatja a lokális változók látrehozását és megszüntetését. Az ENTER utası́tás formátuma: ENTER bytes, level A bytes paraméter azt adja meg, hogy hány byte-ot kell lefoglalni lokális változóknak. Ha nincs szükség lokális változókra,
akkor az értéke lehet zérus is. A második paraméter a függvények egymásba 142 ágyazottságát adja meg. Ha itt nem zérust adunk meg, akkor level darab frame pointert másolunk a frame stack-be, a korábbi stack frame-ből. Így a: ENTER XXX, 0 utası́tás megfelel az PUSH BP MOV BP, SP SUB SP, XXX utası́tásoknak. A LEAVE utası́tás az ENTER utası́tás által lefoglalt stack frame-t szabadı́tja fel 10.6 Rekurzı́v függvények A rekurzı́v függvények abban speciálisak, hogy a függvény tulajdonképpen önmagát hı́vja meg. Más szóval úgy is elképzelhetnénk a dolgot, hogy a függvény egy újabb példányát hozza létre, és azt a függvényt hı́vjuk meg. Ez valójában nincs ı́gy, a függvény kódból csak egy van, de a függvény környezete az ami “megduplázódik”. Ezért nagyon fontos a verem használata, hiszen a vermen akárhány stack frame-t elhelyezhetünk. A
regiszteren vagy memórián keresztűli paraméterátadásnál csak egy terület van ahova a paramétereket ı́rhatjuk, ezért egy újabb függvény hı́vás felülı́rja az értékeket és ı́gy a függvény visszatérése után helytelen kódot hajtanánk végre. A rekurzı́v függvények bemutatására vegyünk először egy pseudo kódban ı́rt függvényt: FÜGGVÉNY nyomtat(n) print n HA n != 0 nyomtat(n-1) HA VÉGE FÜGGVÉNY VÉGE Ha ezt a függvényt meghı́vjuk a nyomtat(2) módon, akkor a program kinyomtatja a 210 értékeket. Ebben az az érdekes, hogy egy számsorozatot nyomtatunk ki, pedig a kódban semmilyen ciklus nincs! Ezért szokták a rekurziót a 4. ciklus képző programozási mószernek is nevezni1 A pseudo kódnak megfelelő assembly program a 10.10 táblán látható, illetve a verem állapotai a program futása során a 10.10 ábrán látható Az egyes alábrák azt az állapotot
mutatják, amit az alcı́mben megadott program sorban szereplő utası́tás végrehajtása után kapnánk a vermen. Az ábrában a szı́nes dobozok a foglalt memória területet jelölik a fehér dobozok a szabad memória területek. Az SS:SP regiszter páros, ami a verem tetejét jelöli, mindig a szı́nes és a fehér dobozok közé mutat. Az ábrákban a dobozban szereplő IP=4 jelöli a függvény visszatérési értéket, a számok a dobozban pedig a függvény paraméternek (n) felelnek meg. Ezek után nézzük a program működését részletesen, hogy mindenki számára világos legyen a működési elv. A 2 sorban a 2 értéket töltjük fel a veremre, mint word adattı́pus (1010a ábra) A 3. sorban meghı́vjuk a nyomtat függvényt, ami a visszatérési értéket feltölti a veremre (1010b ábra), majd átadja a vezérlést a 8. sornak A 8 sorban eltároljuk a a BP regiszter értékét és ezzel
létrehoztuk a függvény stack frame-jét. Ez látható a 1010c ábrán Mivel a függvényben az AX, BX 1 Emlékezzünk az algoritmusok tantárgy ismereteire: 1. elől tesztelő ciklus 2. hátul tesztelő ciklus 3. számláló ciklus 143 és DX regisztereket is használjuk, ezért ezeket a regisztereket elmentjük a 10-12. sorokban (1010d1010f ábrák) A 13 sorban az INT 21h megszakı́tás funkció kódját állı́tjuk be, majd a 14 sorban a veremről betöltjük a függvény paraméterét a BX regiszterbe. A 15 sorban átmásoljuk a paramétert a DL regiszterbe, mivel a nyomtatásnál az INT 21h megszakı́tás ezt a regisztert fogja használni. Miért nem rögtön a DX regiszterbe ı́rtuk bele a függvény paraméterét a 14. sorban? Azért nem, mert a nyomtatáshoz a DL regiszter értékét módosı́tani kell, a számot a számnak megfelelő karakter ASCII kódjává kell alakı́tani úgy, hogy
hozzáadunk 30h értékét. A 17 sorban elvégezzük a nyomtatást a megszakı́tással. A 18 sorban megvizsgáljuk, hogy a függvény paramétere, a BL regiszter, zéruse Mivel nem zérus a BL regiszter értéke, ezért a 20 sorban folytatjuk Először is csökkentjük a paraméter értékét, majd ezt az értéket feltöltjük a veremre (10.10g ábra) a 21 sorban A 22 sorban a függvényhı́vás ismét feltölti a visszatérési értéket (10.10h ábra) és átadja a vezérlést a 8 sornak A 8. és 9 sor befejezi a stack frame előkészı́tését (1010i ábra) Itt látható az, hogy csak egy függvény van, de mivel a verem tetején más adatok vannak, ezért ez egy “új” függvénynek számı́t, hiszen más adatokkal fog számolni. Ezt követi a paraméter beolvasása (14 sor), a nyomtatás (13-17 sor), illetve az ellenőrzés, hogy zérus-e a függvény paramétere. Mivel a függvény paramétere nem
zérus, ezért csökkentjük a paraméter értékét 1-el (20. sor), majd feltöltjük a veremre a 21 sorban (1010m ábra) A 22. sor újra meghı́vja a nyomtat függvényt, vagyis a 23 sor mint visszatérési érték felkerül a veremre (10.10n ábra) és átadja a vezérlést ismét a 8 sornak Immár harmadszor a BP regiszter felkerül a veremre (10.10o ábra) a 8 sorban, ezzel a harmadik stack frame-et előkészı́tve A 10-12 sorokban a regisztereket ismét elmentjük, illetve a 13-17. sorokban a nullás értéket kinyomtatjuk A 18. sorban a BL regiszter értékét nullának fogjuk találni, ı́gy a 25 sorba ugrunk A 25-28 sorokban eldobjuk az elmentett AX, BX, DX és BP értékeket. Ez látható a 1011a-1011d ábrákon A 29. sorban a verem tetején látható cı́mre (1011d ábra) adódik át a vezérlés, vagyis a 23 sorra A 23. sorban eldobjuk a verem tetejére feltöltött paramétert (1011e ábra) Ezt követi a
25-28 sorok végrehajtása, ahol megint helyreállı́tjuk az elmentett regisztereket (10.11f-1011j ábrák), majd a 29 sorban a verem tetején található cı́mre visszatér a függvény, vagyis a 23. sorba Itt ismét eldobjuk a vermen található értékekeket (10.11k-1011p ábrák) Amikor ismét a 29 sorba érünk akkor a verem tetejéről levesszük a visszatérési cı́met (10.11p ábra) és elugrunk az ı́gy megadott cı́mre, a 4 sorba A 4. sor végrehajtásával az utolsó értéket is letakarı́tjuk a veremről és visszaáll az eredeti állapot, amikor is semmi nem volt a vermen (10.11r ábra) 10.7 Hatékonyság Ahogy ebben a fejezetben láttuk a függvények nagy mértékben támogatják a moduláris programozást. Ugyanakkor a függvények használatáért “fizetnünk” is kell, mivel a paraméter átadási módszerek megvalósı́tásához extra utası́tásokat kell alkalmazni, amelyek nem
közvetlenül az algoritmushoz tartoznak. Az ilyen extra utası́tásokat “overhead”-nek nevezzük Vegyünk egy programrészletet, két változó cseréjét. Ez látható a 1011 táblán A bal oldali programrészlet egyszerűen végrehajtja a cserét, a jobb oldali program függvényként végzi el ugyanezt. A második esetben lényegében csak a függvényhı́vással és a RET utası́tással egészı́tjük ki a programot. Mind a két programrészletben ECX-szer hajtjuk végre a műveletet. (Jelen példában azért használjuk a 32 bites regisztereket, mert “nagyon sokszor” szeretnénk végrehajtani, hogy mérhető adatokat kapjunk.) Amennyiben mind a két kódrészletet lemérjük 50 000 000 futás után, azt kapjuk, hogy alap esetben 160 ms-ig, mı́g függvényként való futtatás esetén 240 ms-ig tart a programrészlet futása. A különbség ezen mérések szerint másfélszeres (Más mérések
szerint ez lehet 2.3-szeres különbség is [2]) Azt is érdemes meggondolni, hogy ha paramétereket is átadunk a függvénynek akkor ez az “overhead” még nagyobb lehet, ı́gy sebességre való optimalizálás esetén a függvény hivások minimalizálandók (lásd 15.1 és 1515 bekezdés) 10.8 Ellenőrző kérdések 1. Mi a különbség a sor és verem adatszerkezetek között? 144 (a) IP=2 (b) IP=3 (c) IP=8 (d) IP=10 (e) IP=11 (n) IP=22 (f) IP=12 (o) IP=8 (g) IP=21 (p) IP=10 (h) IP=22 (q) IP=11 (i) IP=8 (j) IP=10 (k) IP=11 (l) IP=12 (m) IP=21 (r) IP=12 10.10 ábra: A verem állapotai a 1010 táblán bemutatott rekurzı́v függvény végrehajtása során 145 (a) IP=25 (b) IP=26 (k) IP=29 (c) IP=27 (l) IP=23 (d) IP=28 (m) IP=25 (e) IP=29 (n) IP=26 (f) IP=23 (o) IP=27 (g) IP=25 (p) IP=28 (h) IP=26 (q) IP=29 (i) IP=27 (j) IP=28 (r) IP=4 10.11 ábra: A verem állapotai a 1010 táblán
bemutatott rekurzı́v függvény végrehajtása során 146 1 org 100h 2 push 2 3 call nyomtat ; nyomtat(3) 4 add sp,2 5 int 20h 6 7 nyomtat: 8 push bp 9 mov bp,sp 10 push ax 11 push bx 12 push dx 13 mov ah, 2 ; 14 mov bx, word [bp+4] ; | 15 mov dl, bl ; | print n 16 add dl, 30h ; | 17 int 21h ; 18 cmp bl, 0 ; HA n != 0 19 jz nyomtat vege 20 dec bl ; 21 push bx ; | nyomtat(n-1) 22 call nyomtat ; 23 add sp,2 24 nyomtat vege: 25 pop dx 26 pop bx 27 pop ax 28 pop bp 29 ret 10.10 tábla: Számjegyek nyomtatása rekurzı́v függvénnyel 2. Mi az a stack underflow? Melyik művelet tudná ezt okozni? 3. Mi az a stack overflow? Melyik művelet tudná ezt okozni? 4. Melyek a verem fő felhasználási területei? 5. Melyik utası́tással lehet feltölteni egy értéket a veremre? 6. Ha POP utası́tás nem megengedett, akkor hogyan tudunk levenni egy értéket a veremről? 7. Mely regiszterek mutatják meg a verem tetejének cı́mét? 8. Hogyan
cserélhetjük fel két regiszter tartalmát a vermen keresztül? 9. Mi az a stack frame és mit tartalmaz? 10. Melyik regiszter a frame pointer? 11. Milyen módszerekkel adhatunk át paramétert egy függvénynek? 12. Ha a regiszteren keresztül adunk át paramétert, akkor ennek a módszernek milyen előnyei és hátrányai vannak? 147 ujra: mov mov mov mov eax, ertek1 ebx, ertek2 ertek2, eax ertek1, ebx dec ecx jnz ujra ujra: call csere dec ecx jnz ujra jmp vege csere: mov eax, ertek1 mov ebx, ertek2 mov ertek2, eax mov ertek1, ebx ret vege: 10.11 tábla: Programrészlet két változó cseréjére függvény nélkül és függvénnyel 13. Mely memória területen definiálunk lokális változókat egy függvényben? 14. Miért nem az adat szegmensben definiáljuk a lokális változókat? 10.9 Feladatok 9. fejezetben bemutatott programokat ı́rjuk át úgy, hogy az időleges tárolásra ne a MOV utası́tást
használjuk, hanem a vermet. 148 11. Fejezet Makrók A makrók nagyon érdekes és hasznos programozási konstrukciók. Mit nevezünk makrónak? Ez sajnos a programozási nyelvtől is függ. Az assembly programozási nyelvben a makrók hasonlók a C programozási nyelvben használt makrókhoz, de érdekes módon egy kicsit többet is tudnak Persze erre az extra segı́tségre szükség is van, mivel az assembly programozási nyelv egy alacsony szintű vagyis gépközeli programozási nyelv. Assembly-ben a makró arra ad lehetőséget, hogy egy szöveg blokkhoz egy nevet rendeljünk hozzá, majd amikor az assembler az adott névvel találkozik a forráskódban, akkor a név helyére a szöveg blokkot illeszti be. A behelyettesı́tés angol neve: macro expansion Egyszerűen fogalmazva a macro egy fejlett szöveg behelyettesı́tési mechanizmus. 11.1 Egy soros makrók A legegyszerűbb makrók egy sorosak. Erre egy példa: %define
csereaxbx XCHG AX, BX amit egy programkódban a következőképpen is használhatunk: MOV AX, 1 MOV BX, 2 . csereaxbx . Amikor az assembler ezt lefordı́tja, akkor azt két lépésben teszi. Először is elvégzi a makró behelyettesı́téseket: MOV AX, 1 MOV BX, 2 . XCHG AX, BX . majd ezután az assembler legenerálja a bináris kódot. Ez azt jelenti, hogy egy két lépéses (two-pass) fordı́tási folyamat végeredményeként jön létre a bináris kód. Ez a két lépéses folyamat jellemző a C programozási nyelvre is, ahol az első lépés az előfeldolgozás (preprocessing) és a második lépés a fordı́tás (compiling). 149 A makrók összetettebbek is lehetnek, mivel rendelkezhetnek argumentummal vagy paraméterrel, mint a függvények. Ugyanakkor ez felveti a makrók és függvények közötti kapcsolatot Mikor, melyiket és hogyan használjuk? Egy függvény tulajdonképpen egy al-programnak felel meg.
Egy gyakorlatiasabb megfogalmazás szerint a függvény egy olyan utası́tás sorozatnak felel meg, amelyet sokszor, több különböző helyen használunk a programunkban. Így a függvény létrehozásához az ismétlődő kódrészletet “kiemeljük”, nevet adunk neki, majd amikor szükségünk van rá, akkor a nevével hivatkozunk rá és ezzel átadjuk a függvénynek a vezérlést (meghı́vjuk). Ez a definı́ció nagyon hasonló a fenti makró definı́cióhoz Nézzük makrók és függvények közötti különbségeket: • A legfontosabb különbség a makrók és függvények között a generált bináris kódban van. – Egy függvény esetén a bináris kódot az assembler egyszer generálja le, majd minden alkalommal amikor a kódrészletre szükség van, akkor a CALL utası́tással hı́vjuk meg. Ez azt is jelenti, hogy a függvényhı́vás helyére a CALL utası́tás bináris kódja
kerül. – Egy makrók esetén, ahol a makró neve szerepel, oda lesz behelyettesı́tve a makró forráskódja és oda kerül a bináris kód is. Minden alkalommal Ez azt jelenti, hogy mindenhol, ahol a makró neve szerepel a kódban, oda a makró teljes bináris kódja belefordul. • A másik nagyon fontos különbség a paraméter átadási módszerben van. – Függvények esetén a 10. fejezetben tárgyalt paraméter átadási módszerek lehetségesek: regiszteren, memórián és vermen keresztül. – Makrók esetén közvetlen paraméter átadás lehetséges, úgy mintha egy magas szintű programozási nyelvet használnánk. Például: %define csere(a,b) XCHG a, b . csere(AX, BX) . Ebből a kódrészletből az alábbi kód generálódik a makró behelyettesı́tés után: . XCHG AX, BX . Ezek után a makró definiálás néhány további fontos szabálya a következő: • Egy makró definı́ció
során a kis és nagy betű közötti különbség számı́t. • Rekurzı́v definı́ció esetén a behelyettesı́tés csak egyszer történik meg. Például: %define a(x) 1+a(x) mov ax, a(3) amiből a következő kód generálódik: mov ax, 1+a(3) • Nagyon fontos, hogy a behelyettesı́tés a makró használatakor történik és nem a definiáláskor. Például: %define b(x) 2*x %define a(x) 1+b(x) mov ax, a(8) 150 A példában amikor az assembler meglátja a a(8) kifejezést, akkor először az a(x) makró helyettesı́tődik be: mov ax,1+b(8) ezután pedig a b(x) makró behelyettesı́tése történik meg: mov ax,1+2*8 11.2 Több soros makrók Több soros makrók esetén a szintakszis a következő: %macro név param szám utası́tások utası́tások . %endmacro A név adja meg a makró nevét, amivel hivatkozni lehet rá. A param szám adja meg a makrónak megadható paraméterek számát. Nézzük az
első egyszerű példát, amely tulajdonképpen egy szorzásnak felel meg: %macro szorzasAX 4el shl AX, 2 %endmacro Ez egy olyan makró, aminek nincs paramétere és használata igen egyszerű: mov AX, 3 szorzasAX 4el amiből a generált kód a következő lesz: mov AX, 3 shl AX, 2 Ez a példa elég korlátozott, mivel csak az AX regisztert képes 4-el megszorozni. Ha szeretnénk ezt a korlátozást eltávolı́tani, akkor egy olyan makrót kellene definiálnunk, amelyiknek megadhatjuk, hogy melyik regisztert akarjuk megszorozni: %macro szorzas 4el 1 shl %1, 2 %endmacro A név után megadott szám adja meg, hogy a makrónak van egy argumentuma. Erre az argumentummal egy százalék jellel és utána a paraméter számával lehet hivatkozni, például: %1. Így ha ezt a makrót használjuk: mov BL, 3 szorzas 4el BL akkor bármilyen regiszter megadhatunk, például BL, és ı́gy a behelyettesı́tett kód a következő lesz: 151 mov
BL, 3 shl BL, 2 Ha több paramétert akarunk átadni egy makrónak, erre is lehetőség van: %macro csere 2 XCHG %1, %2 %endmacro Ennek a makrónak a használata a következő lesz: csere AX, BX Amint ez látható a makrók teljesen “integrálódhatnak” az assembly nyelvbe. Ennek sajnos az is a következménye, hogy a makrók és az utası́tások néha egy kicsit össze is keverhetők, például: %macro push 2 push %1 push %2 %endmacro . push ax push bx, cx Itt is látható, hogy az első alkalommal a push egy utası́tás, mı́g a második alkalommal a push egy makró, amit majd az assembler lecserél a definı́cióban megadott utası́tásokra. 11.21 Cı́mkék makrókban Eddig csak olyan makrókat láttunk amelyekben nem használtunk semmilyen memória cı́met, ugyanakkor a cı́mek egy kis figyelmet érdemelnek. Tegyük fel, hogy a következő makrót definiáljuk, amely egy értéket összehasonlı́t zérussal és
aszerint állı́tja be az AX regiszter értékét, hogy zérus volt-e az érték: %macro cmp zero 1 cmp %1, 0 jz nulla mov AX, 1 nulla: mov AX, 0 %endmacro Ha ezt a makrót többször is használjuk: cmp zero CX . cmp zero DX . akkor a következő kód generálódik: cmp CX, 0 jz nulla mov AX, 1 nulla: mov AX, 0 152 . cmp DX, 0 jz nulla mov AX, 1 nulla: mov AX, 0 . Ahogy ez itt is látható, ebben az esetben a nulla cı́m kétszer is megjelenik a kódban. Ezt az assembler nem engedi meg és hibát fog generálni Ilyen esetben a megoldás az, hogy a nulla cı́mnek “lokálisnak”, egyedinek kell lennie az egyes makró behelyettesı́tések során. Ezt úgy lehet elérni, hogy a cı́m elé kettő darab százalék jelet kell tenni: %macro cmp zero 1 cmp %1, 0 jz %%nulla mov AX, 1 %%nulla: mov AX, 0 %endmacro Ha ezt az újabb makró definı́ciót használjuk: cmp zero CX . cmp zero DX . akkor már nem lesz semmi gond, hiszen a
következő kód generálódik: cmp CX, 0 jz nulla2345 mov AX, 1 nulla2345: mov AX, 0 . cmp DX, 0 jz nulla7453 mov AX, 1 nulla7453: mov AX, 0 . A makrókban a dupla százalék jellel definiált cı́mek esetén az assembler garantálja, hogy minden egyéb, a programban előforduló cı́mtől eltérő cı́met fog generálni, vagyis minden makró behelyettesı́tés esetén a cı́m egyedi lesz. 11.22 “Greedy” makró paraméterek Az is előfordulhat, hogy nem csak fix számú argumentumot akarunk megadni egy makrónak, hanem néha többet is. Erre az esetre való a “greedy” paraméter, ami azt jelenti, hogy minden utána következő érték hozzá tartozik. Például: %macro PRINT 1+ 153 JMP %%atlep %%szoveg: DB %1, ’$’ %%atlep: MOV DX, %%szoveg MOV AH,9 INT 21H %endmacro A példában a 1+ azt jelenti, hogy a makrónak egy paramétere biztosan van, de lehet több is. Abban az esetben ha több paramétert is megadunk,
akkor mindegyik, egymás után a %1 helyére kerül. A fenti makró használatára a példa: PRINT ’Hello vilag’, 10, 13 amiből a következő kód generálódik: JMP atlep87643 szoveg8964: DB ’Hello vilag’, 10, 13, ’$’ atlep87643: MOV DX, szoveg8964 MOV AH,9 INT 21H 11.3 Makrók és függvények még egyszer A fenti ismeretek tükrében érdemes még egyszer összehasonlı́tani a függvényeket és a makrókat egy példán keresztül. A példában az előző bekezdés makróját hasonlı́tjuk össze egy függvénnyel, amelyik szintén egy szöveget nyomtat ki. Az összehasonlı́tás a 111 táblán található A táblából úgy tűnik, hogy a makrók rövidebb kódot generálnak, de általában inkább az a helyzet, hogy a további makró hı́vások esetén újra és újra több kód kerül be a forráskódba, mı́g az újabb függvényhı́vások esetén csak újabb PUSH, CALL és ADD
utası́tások adódnak hozzá a programhoz. 11.4 Makrók gyűjtemények A gyakran használt makrókat össze lehet gyűjteni egy file-ba, majd ezeket később igen könnyen lehet használni. Például definiáljunk két egyszerű makrót Az egyik makró egy billentyű leütésére vár, a másik makró pedig a programból való kilépést hajtja végre: %macro BillVar 0 MOV AH, 0 INT 16h %endmacro %macro Kilep 0 INT 20h %endmacro Ezeket a makrókat mentsük el egy makro.inc file-ba Ha ezután ezeket a makrókat egy programban használni szeretnénk, akkor a következő módon járhatunk el: 154 %include "makro.inc" org 100h BillVar Kilep Ebből is látható, hogy ha megfelelő neveket használunk a makrók esetén, akkor nagyon “beszédes” (könnyen érthető, értelmezhető) programokat tudunk ı́rni a segı́tségükkel. 155 Makró definı́ció %macro PRINT 1+ JMP %%atlep %%szoveg: DB %1, ’$’
%%atlep: MOV DX, %%szoveg MOV AH,9 INT 21H %endmacro Makró használat PRINT ’Hello’, 13, 10 PRINT ’Vilag’, 13, 10 Makróból generált kód JMP atlep6345 szoveg9092: DB ’Hello’, 13, 10, ’$’ atlep6345: MOV DX, szoveg9092 MOV AH,9 INT 21H JMP atlep7231 szoveg1235: DB ’Vilag’, 13, 10, ’$’ atlep7231: MOV DX, szoveg1235 MOV AH,9 INT 21H Függvény definı́ció PRINT: PUSH BP MOV BP, SP PUSH AX PUSH DX MOV AH, 9 MOV DX, [BP+4] INT 21h POP DX POP AX POP BP RET Függvény használat PUSH szoveg1 CALL PRINT ADD SP, 2 PUSH szoveg2 CALL PRINT ADD SP, 2 . szoveg1: db ’Hello’, 13, szoveg2: db ’Vilag’, 13, Függvényből generált kód PRINT: PUSH BP MOV BP, SP PUSH AX PUSH DX MOV AH, 9 MOV DX, [BP+4] INT 21h POP DX POP AX POP BP RET . PUSH szoveg1 CALL PRINT ADD SP, 2 PUSH szoveg2 CALL PRINT ADD SP, 2 . szoveg1: db ’Hello’, 13, szoveg2: db ’Vilag’, 13, 11.1 tábla: Makrók és függvények összehasonlı́tása 156 10, ’$’
10, ’$’ 10, ’$’ 10, ’$’ 11.5 Ellenőrző kérdések 1. Mi az a makró? 2. Miben hasonlı́tanak és miben különböznek a makrók és a függvények? 3. Hogyan definiálhatunk lokális cı́meket makrókban? 4. Hogyan definiálhatunk több soros makrót? 5. Mi történik rekurzı́v makrók esetén? 6. Fel kell-e tölteni a makró paramétereit a veremre a makró meghı́vása előtt? 7. Mit jelent az, hogy “greedy” makró paraméter? 8. Mi lesz a forrás kód a következő makró használata után: %define %define %define . MOV AX, bb(x) 2+x aa(x) 1+bb(x) cc(x) [BX+aa(x)] cc(1) 157 158 12. Fejezet String műveletek A string kezelő utası́tások memóriablokkokkal végeznek műveletet. A string, tulajdonképpen szöveg, a szöveg pedig nem más mint egy karakter sorozat vagy byte sorozat és egy byte sorozat amikor a memóriában tároljuk akkor pedig megfelel egy memóriablokknak. A szövegek
tárolására két stratégiát alkalmazhatunk: • fix méretű szövegek és • változó méretű szövegek. A fix méretű szövegek esetén minden szöveg azonos méretű és ı́gy egyszerű a kezelésük. Ugyanakkor két probléma is lehet a fix méretű szövegekkel: • Ha az adott szöveg hosszabb mint a fix méret, akkor azok a karakterek amelyek a fix méreten túlra esnek elvesznek. Ilyenkor lerövidı́tjük (truncation) a szöveget • Ha az adott szöveg rövidebb mint a fix méret, akkor a maradék karaktereket ki szokták tölteni (padding). Ezt a két problémát figyelembe kell venni akkor, amikor a fix méretű szövegek méretét próbáljuk meghatározni. Ösztönösen nagy értéket akarunk választani, ı́gy minden szöveg belefér és egy szöveget sem kell lerövidı́teni. Ugyanakkor ha csak egy nagy és több kisebb szöveg van, akkor elég sok helyet elpazarolhatunk. Erre a dilemmára
jelent megoldást a változó méretű szöveg A változó méretű szövegek esetén a szöveg pontosant annyi karaktert tárol, mint amennyire szükség van. Ugyanakkor a szöveg karakterein kı́vül még egy adatra szükség van és ez a szöveg hossza A szöveg hosszát kétféle stratégiával lehet megadni: • explicit módon tároljuk a szöveg méretét • egy lezáró karaktert (sentinel character) használunk. Nézzünk egy példát arra, amikor explicit módon tároljuk a szöveg méretét: szoveg: db ’Valamilyen szoveg’ hossz: db $-szoveg ahol a $ jel az aktuális cı́met jelenti. Ez a megoldás azért működik, mivel a $ jel éppen a szoveg cı́men található szöveg utolsó karaktere utáni cı́met jelenti és ebből vonjuk ki magát a szoveg cı́met. Feltételezve, hogy a NASM fordı́tó a szoveg szimbólikus cı́met a 200d decimális cı́mre fordı́tja le, akkor a fenti példában a $ jel
értéke 217d lesz és ı́gy a hossz cı́men a 17d értéket fogjuk tárolni. Így természetesen ezt is ı́rhatnánk: 159 szoveg: db ’Valamilyen szoveg’ hossz: db 17 de ezzel az a probléma, hogy ha később megváltoztatjuk a szöveg tartalmát akkor a hossz változót is meg kell vátloztatni. Az előző megoldást alkalmazva a szöveg hossza automatikusan számolódik ki ı́gy leveszi ezt a terhet rólunk. A másik megoldásban egy lezáró karakterrel jelöljük a szöveg végét, ı́gy nincs szükség explicit módon tárolni a szöveg hosszát, hiszen a szöveg elejétől a lezáró karakterig kell csak megszámolni a karakterek számát. Ennél a módszernél fontos feltételezés, hogy a lezáró karakter nem fog előfordulni magában a szövegben. Azt már láttuk, hogy ha az INT 21h megszakı́tást használjuk a 09h funkciókóddal akkor a szöveg végén a $ jelnek kell szerepelnie. Ezzel
szemben a C programozási nyelvben a szöveg végén az ASCII NULL karakter szerepel. Ez a karakter nem összekeverendő a nullás szám ASCII kódjával, mivel ez az érték 30h lenne, mı́g az ASCII NULL karakter értéke 00h. Az ilyen nullával lezárt szövegeket ASCIIZ szövegnek is szokták nevezni. Nézzük erre is egy példát: szoveg: db ’Valamilyen szoveg’,0 12.1 String utası́tások A string utası́tások a 7.6 bekezdésben találhatók Amint a bekezdésből látható, az utası́tások operandusai lehetnek egy forrás cı́m, egy cél cı́m vagy mindkettő A 8086-os processzoron a forrás cı́m a DS:SI (SI = source index) regiszter pár adja meg, mı́g 32 bites rendszeren a DS:ESI regiszterek adják meg a forrás cı́met. Hasonlóan a 8086-os processzoron a cél cı́met az ES:DI (DI = destination index) regiszterek, mı́g 32 bites rendszeren az ES:EDI regiszterek adják meg. Az utası́tások a leı́rtak alapján
automatikusan frissı́tik az SI illetve DI regisztereket az adat méretével: byte, 2 byte (word) vagy 4 byte (dword). A frissı́tés lehet csökkentés vagy növelés is, az irány (Direction) státusz bittől függően Az utası́tások jelentősége abban van, hogy ismétlő prefix-el lehet használni, amelyeket a 7.6 bekezdés szintén tárgyal. A prefix lehet feltételes vagy feltétel nélküli 12.11 String másolás Az adatok másolását érdemes egy kicsit jobban megvizsgálni, mivel érdekes működési módokat fedezhetünk fel, illetve későbbiekben azokat jól használhatjuk is. A másolás alapja a MOVS utası́tás (lásd ??. bekezdés), melynek működését a következő pseudo-kód tudja leı́rni: [ES:DI] = [DS:SI] if(DF == 0) { DI = DI + 1 SI = SI + 1 } else { DI = DI - 1 SI = SI - 1 } String másolás egyszerűen Az adatok másolása esetén általában nem kell azzal foglalkozni, hogy az index
regisztereket növeljük vagy csökkentjük a másolás során. A következő assembly programrészletben növekvő cı́mek mellett 160 (a) (b) (c) (d) (e) (f) 12.1 ábra: A MOVSB utası́tás egyszerű működése másoljuk át a tömböt: cld lea lea mov rep . array1: array2: si, array1 di, array2 cx, 321 movsb repb 321 repb 321 A másolás folyamatát a 12.1 ábra mutatja be String másolás átlapolással Ezek után nézzük meg mi történik a következő programrészletben? cld lea si, array1 161 (a) (b) (c) (d) (e) (f) 12.2 ábra: A MOVSB utası́tás működése átlapolással és növekvő cı́mekkel lea di, array2 mov cx, 321 rep movsb . array1: db ’X’ array2: repb 321 A változás abban van, hogy Úgy tűnik mintha az array1 nevű változó most csak 1 elemű lenne. Akkor mégis hogyan fog ez a programrészlet 321 byte-ot átmásolni? Azt kell figyelembe venni, hogy az assembler az
egymás után ı́rt adat vagy kódrészleteket közvetlenül egymás után fogja elhelyezni a memóriában a fordı́tás során. A jelen esetben ez azt jelenti, hogy először az array1 cı́m által jelölt egy byte-ot teszi a memóriába, majd közvetlenül utána másik 321 byte-ot helyez el. Ez az elrendezés a 12.2a ábrán látható Ennek az a következménye, hogy ı́gy a két tömb (array1 és array2) tulajdonképpen át van lapolva, más szóval egymásba érnek. Ebben az esetben nem mindegy, hogy növekvő vagy csökkenő cı́mek mellett másoljuk át az adatokat. A 122 ábra azt a helyzetet mutatja, amikor a Direction státusz bit zérus és az SI illetve DI regiszterek növekednek a MOVSB utası́tás ismételt végrehajtása során. Az ábrában a szagggatott vonallal jelölt nyı́l jelöli, hogy melyik adat hova lesz átmásolva. Amint látható az ábrából, ebben az esetben lényegében azt kapjuk,
hogy az array1 cı́men lévő byte értékével felülı́rjuk az array2 tömb minden elemét. Egy tömb adott byte-al való felülı́rására ugyanakkor jobb a STOS utası́tása (lásd 7.64 bekezdés) String másolás nagyobb átlapolással Esetleg valakiben felmerülhet, hogy ezek után mi van akkor, ha nem csak egy byte-ot definiálunk az array1 cı́men? A helyzet az, hogy a működés nem változik, csak most, az array1 cı́men definiált több értékkel, mint mintával ı́rjuk felül az array2 cı́men található tömböt. Például az alábbi kód működését: cld lea lea mov rep si, array1 di, array2 cx, 321 movsb 162 (a) (b) (c) (d) (e) (f) 12.3 ábra: A MOVSB utası́tás működése nagyob átlapolással és növekvő cı́mekkel Végeredményben ismétlődő mintát kapunk. . array1: array2: db ’X’, ’Y’, ’Z’ repb 321 a 12.3 ábra mutatja be Az ábra azt mutatja, hogy a
programrészletben megadott három byte fog ismétlődni az array2 cı́mtől is. Erre már nem lenne képes a STOSB utası́tás, csak akkor ha az ismétlődő minta byte, word vagy double word méretű. String másolás átlapolással fordı́tott forrás és cél cı́mmel A 12.11 bekezdésben bemutatott másolást egy kicsit másképpen is felı́rhatjuk, vagyis például a forrás és cél cı́met felcserélhetjük. Nezzük meg mi történik ilyenkor: cld lea lea mov rep . array1: array2: si, array2 di, array1 cx, 321 movsb db ’X’ repb 321 Amint a 12.4 ábrán is látható, ebben az esetben az array2 cı́men található tömböt “eggyel előre másoljuk”. Ha csak az array2 tömböt tekintjük, akkor ez lényegében azt jelenti, hogy a tömb első elemét “töröljük”, hiszen a programrészlet végrehajtása után az array2 tömbben már nem található az első elem, illetve az utolsó elem a tömb
végén kétszer szerepel. Ha a legutolsó elemre nincs szükség duplán akkor az array2 tömb lényegében egy elemmel kevesebbet tartalmaz. Ez helyzet látható a 12.5 ábrán 12.12 Stringek összehasonlı́tása Az összehasonlı́tás alapja a CMPS utası́tás melynek működését a következő pseudo-kód ı́rja le: 163 (a) (b) (c) (d) (e) (f) 12.4 ábra: A MOVSB utası́tás működése átlapolással és fordı́tott forrás és cél cı́mmel (a) 12.5 ábra: A tömbök állapota a MOVSB utası́tás végrehajtása utána, amikor a tömbök át vannak lapolva és fordı́tott forrás és cél cı́mmel. [ES:DI] összehasonlı́tása [DS:SI] if(DF == 0) { DI = DI + 1 SI = SI + 1 } else { DI = DI - 1 SI = SI - 1 } Fontos azt tudni, hogy az összehasonlı́tás az assembly nyelvben úgy történik, hogy tulajdonképpen a két értéket kivonjuk egymásból és az eredmény alapján a státusz
biteket beállı́tjuk. Például ha a két érték egyenlő, akkor a különbségük zérus lesz. Ezért van, hogy a zérus státusz bit jelzi az értékek egyenlőségét vagy nem egyenlőségét. A CMPS utası́tás nagyon jól kombinálható a REPZ, REPE, REPNZ és REPNE prefixekkel. Stringek egyenlősége A következő programrészlet két szöveget hasonlı́t össze: string1: db ’abcxef’,0 strlen EQU $ - string1 string2: db ’abcdef’,0 . 164 mov CX, strlen mov SI,string1 mov DI,string2 cld repe cmpsb Az összehasonlı́tás addig folytatódik ameddig a karakterek megegyeznek, illetve a CX regiszter értéke nem zérus. A fenti példában a program addig fog futni, amı́g meg nem találja a string1 szövegben az x és a string2 szövegben a d karaktereket, melyek nem egyenlőek. Ugyanakkor fontos, hogy amikor az összehasonlı́tás véget ér az SI és DI regiszterek az utoljára összehasonlı́tott karakterek
utáni karakterre mutatnak, vagyis az e karakterre. Tehát ha a szövegekben az első eltérő karakterre vagyunk kiváncsiak, akkor az SI és DI regisztereket csökkenteni kell eggyel. 12.13 Keresés stringek-ben A szövegben való keresésre a SCAS utası́tás használható. Az utası́tás pseudo kódja: [ES:DI] összehasonlı́tása AL-el if(DF == 0) { DI = DI + 1 } else { DI = DI - 1 } Ez az utası́tás is az ismétlő prefixekkel használható a legjobban. Egy karakter keresése Nézzünk egy egyszerű példát, amikor azt szeretnénk ellenőrizni, hogy egy karakter megtalálható-e a szövegben és ha igen, akkor a cı́mére is kiváncsiak vagyunk, tehát, hogy hol található a karakter a szövegben: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 string1: db ’abcxef’,0 strlen EQU $ - string1 . mov CX, strlen mov DI, string1 mov AL, ’x’ cld repne scasb jnz nincs meg dec di . jmp vege nincs meg: . vege: A 8. sorban a kereső
parancsot addig ismételjük, amig az AL regiszter értéke nem egyenlő az ES:DI regiszterpár által mutatott értékkel, vagy a CX regiszter értéke zérus nem lesz. A 9 sorban azt ellenőrizzük, hogy miért ért véget a keresés. Ha a zérus státusz bit értéke nulla, akkor nem találtuk 165 meg a karaktert ezért ugrunk a 9. sorban Ha valóban megtaláltuk a karaktert, akkor a zérus státusz bit értéke 1 lesz és “átesünk” a 10. sorba A 10 sorban azért csökkentjük a DI regiszter értékét, hogy a regiszter valóban arra a karakterre mutasson, amelyiket megtaláltunk. lényegében a keresést meg is fordı́thatjuk. Ebben az esetben amı́g a memória értékei megegyeznek (egyenlőek) a megadott karakterrel, addig folyamatosan továbblépünk a memóriában, mı́g ha a memóriában található érték különbözik az AL regiszter értékétől akkor leáll a keresés. Például egy
szöveg elején ugorjunk át minden SPACE karaktert: 1 string1: db ’ abc’,0 2 strlen EQU $ - string1 3 . 4 mov CX, strlen 5 mov DI, string1 6 mov AL, ’ ’ 7 cld 8 repe scasb 9 dec di 10 . A program végén a DI regiszter az első nem SPACE karakterre fog mutatni. Így a programban a 8. sorban a REPE prefix azt jelenti, hogy ismételjük addig a SCASB utası́tást, amı́g egyenlő az AL regiszterrel. 12.14 LODSB és STOSB utası́tások használata Elsőre furcsának tűnhetnek a LODSB és STOSB utası́tások. Főleg azért mert nem igazán használhatók a REP prefixekkel. (Azért nincs értelme használni a REP prefixet például a LODSB utası́tással mivel ez csak azt jelentené, hogy n-szer betöltünk értéket az AL regiszterbe.) Ugyanakkor abban az esetben, ha nem csak át kell másolni az adatokat, hanem közben valamilyen módon módosı́tani is kell, akkor a LODSB utası́tással be tudjuk tölteni az adatot, majd a
módosı́tás után a STOSB utası́tással tárolni tudjuk. Erre nézzünk egy példát, amelyben minden karakter helyett az utána következő karaktert szeretnénk tárolni: 1 2 3 4 5 6 7 8 9 cld lea si, forras lea di, cel mov cx, [hossz] ujra: lodsb inc al stosb loop ujra 12.2 String utası́tások előnyei és hátrányai Két fő előnye van ezeknek az utası́tásoknak: • Az index regiszterek automatikusan módosulnak a Direction státusz bit szerint. • Képesek egyszerre két, a memóriában levő operandussal dolgozni, vagyis például képesek memóriából memóriába másolni. Az utası́tások nem csak, hogy egyszerűek és elegánsak, de nagyon hatékonyak is. Az utası́áts annál gyorsabb minnél nagyobb méretű adatokat másolunk egyszerre, ı́gy a MOVSB utası́tásnál “gyorsabb” 166 a MOVSW és ennél “gyorsabb” a MOVSD utası́tás. Így például ha 4099 byte-ot szeretnénk a
lehető leggyrosabban átmásolni egyik cı́mről egy másikra, akkor a következő kódrészlet használható: 1 2 3 4 5 6 7 8 cld lea lea mov rep movsw movsb . si, forras di, cel cx, 1024 movsd ; 4096 byte ; 2 byte ; 1 byte A 4. sorban azért adunk meg 1024-et, mivel a MOVSD utası́tások 4 byte-ot mozgatnak és ı́gy 1024 × 4 = 4096 byte-ot másolunk át, majd a 6. és 7 sorban a maradék kettő és egy byte-ot mozgatjuk át Ebben a kódrészletben a szöveg mérete előre ismert volt, de lehetőség van hasonlóan gyors adat másolásra akkor is, ha a szöveg méretét nem ismerjük előre. Erre mutat példát a következő programrészlet: 1 cld 2 lea si, forras 3 lea di, cel 4 mov cx, [meret] 5 shr cx, 2 6 jz kevesebb mint 4 7 rep movsd 8 kevesebb mint 4: 9 mov cx, [meret] 10 and cx, 11b 11 jz vege 12 rep movsb 13 vege: ; osztás 4-el ; maszkolás, 0-3 lehet A 4. sorban betöltjük a másolandó adat méretét, amit az 5
sorban elosztunk 4-el Azért 4-el, mert megpróbáljuk a MOVSD utası́tást használni és ez az utası́tás 4 byte-ot mozgat egyszerre, ı́gy a CX regiszterbe a meret negyedét kell tárolni. Ugyanakkor a 6 sorban ellenőrizni kell, hogy a meret negyede az nagyobb-e mint zérus. Ha kisebb, például csak 3 byte-ot kell átmásolni, akkor nem szabad használni a MOVSD utası́tást és ezért átugorjuk azt. Ha nagyobb, akkor 4 byte-onként átmásoljuk az adatokat. Ekkor még mindig előfordulhat az, hogy amikor a meret értékét eloszottuk 4-el, akkor volt valamennyi maradék. Ezt a maradékot az 5 sorban “eldobtuk”, vagyis nem vettük figyelembe A 4-el való osztásnak a maradéka lehet: 0, 1, 2 és 3. Ahhoz, hogy megállapı́tsuk, hogy mennyi a maradék a 10 sorban maszkoljuk a meret értékét úgy, hogy a CX értéke csal 0, 1, 2 és 3 lehet. Itt is megvizsgáljuk, hogy volt-e maradék. Ha nem volt maradék, akkor a 11
sorból a 13 sorba ugrunk Ha volt maradék, akkor a 12. sorban a REP MOVSB utası́tással másoljuk át a byte-okat 167 12.3 Ellenőrző kérdések 1. Mik az előnyei és hátrányai a fix méretű szövegeknek? 2. Mik az előnyei és hátrányai a változó méretű szövegeknek? 3. Hasonlı́tsa össze a különböző szöveg tárolási módokat, amikor lezáró karaktert tárolunk illetve amikor közvetlenül tároljuk a szöveg hosszát. 4. Mik az előnyei a string kezelő utası́tások használatának? 5. Miért nincs értelme a REP prefixnek a LODSB utası́tás esetén? 6. Adjon meg olyan esetet, amikor fontos a Direction státusz bit értéke! 7. Hasonlı́tsa össze a következő két szöveg definı́ciót Mik az előnyök és hátrányok: szoveg: db ’Hello vilag’ hossz: dw $-szoveg illetve szoveg: db ’Hello vilag’ hossz: dw 11 168 13. Fejezet Példák függvényekre és szöveg
kezelésre 13.1 Szöveg hosszának megállapı́tása 169 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 org 100h push szoveg call strlen add sp, 2 int 20h szoveg: db ’abcd’,0 strlen: push bp mov bp, sp push cx push di push es les di, [bp+4] mov cx, 0ffffh cld mov al,0 repne scasb jcxz sl no string inc di mov ax, di sub ax, [bp+4] clc jmp sl done sl no string: stc sl done: pop es pop di pop cx pop bp ret ; szöveg pointer ES:DI-be ; lehetséges maximális hossz ; NULL karakter ; if ECX = 0, not a string ; növeljük 1-el, ı́gy 0-ra mutat ; szöveg hossz AX-ben ; nem volt hiba ; carry 1 => nem szöveg 13.1 tábla: Egy szöveg hosszát megállapı́tó függvény 170 14. Fejezet C és assembly programok kapcsolata 14.1 Függvény hı́vási konvenciók 14.11 16 bites mód Egy függvénynek a paramétereket a vermen keresztül adjuk át. A legelső paraméter kerül a legalacsonyabb
memória cı́mre Akár 8 vagy 16 bite értéket akarunk feltölteni a veremre minden alkalommal egy 16 bites, word értéket kell tárolni a vermen. Ha 16 bitnél több bites értéket akarunk tárolni, akkor is word értékekeket kell feltölteni a veremre little-endiam módon. Ez azt jelenti, hogy a legkisebb memória cı́mre kerül a legkisebb helyiértékű word. Mindez azt jelenti, hogy minden érték a vermen 2 byte-ra van illesztve (2 byte aligned). A függvények a visszatérési értékükett regiszteren keresztül adjuk át. 8 bites egész szám esetén az AL regisztert használjuk, 16 bites egész szám esetén az AX regisztert használjuk, 32 bites egész szám esetén a DX:AX regisztereket használjuk, boolean érték esetén az AX regisztert használjuk és valós szám esetén az ST0 FPU regisztert használjuk. 14.12 32 bites mód A cdecl hı́vási konvenció az alap eset Linux-on. Konvenció cdecl
stdcall fastcall, Microsoft és GNU fastcall, Borland Paraméter sorrend a vermen Első paraméter az alacsony cı́men Első paraméter az alacsony cı́men Az első két paraméter ECX és EDX regiszterekben, a többi paraméter stdcall szerint Az első három paraméter EAX, ECX és EDX regiszterekben, a többi paraméter stdcall szerint 14.1 tábla: Függvény hı́vási konvenciók 32 bites módban 171 Paraméter eltávolı́tó a hı́vó a függvény a függvény a függvény 172 15. Fejezet Optimalizálás A fejezetben tárgyalt eljárások és módszerek egy része csak a modern x86-os processzorok esetén használható, mivel egy részük még nem is létezett az 8086-os processzor idejében. 15.1 Optimalizálás sebességre Az első és legfontosabb dolog, hogy azonosı́tsuk azt a kód részletet, ahol a programunk legtöbb időt tölti.1 Ez az egyik legfontosabb alapelv, mivel a mai programok
egy jelentős része gyakran sokkal több időt tölt modulok, erőforrások betöltésével, adatbázisok elérésével mint valamilyen számı́tással a programban. Így ha csak a számı́tást optimalizáljuk akkor a program által felhasznált időnek csak az 1%-át javı́tjuk, mı́g a többi idő változatlan marad. Az assembly kód használata egy programban csak akkor hasznos, ha a program CPU intenzı́v, például: kép és hang feldolgozás, titkosı́tás, rendezés, adat tömörı́tés és komplikált matematikai számı́tás. A CPU intenzı́v programokra általában az jellemző, hogy van egy olyan ciklus amit a program nagyon sokszor végrehajt. Ez általában az úgynevezett legbelső ciklus (innermost loop) Ezt a program részletet kell megtalálni és optimalizálni. Ha bármilyen más program részt optimalizálunk, akkor tulajdonképpen csak az időnket pazaroljuk, mivel nem tudunk gyorsı́tást
elérni és a programunkat csak átláthatatlanná tesszük az optimalizálással. A másik fontos alapelv, hogy érdemes magát az optimalizálandó algoritmust tanulmányozni, mivel sokszor egy másik algoritmus választásával már jobb eredményt érhetünk el. 15.11 Sorrenden kı́vüli végrehajtás Lényegében minden modern x86-os processzor képes a sorrenden kı́vüli végrehajtásra (out-of-order execution). 1 “Premature optimization is the root of the evil”. 173 1 2 3 4 5 6 mov ax, [mem1] imul 6 mov [mem2], ax mov bx, [mem3] add bx, 2 mov [mem4], bx 15.1 tábla: Sorrenden kı́vüli végrehajtás 15.12 Utası́tás betöltés és dekódolás 15.13 Utası́tás késleltetés és átbocsátási képesség 15.14 Függőségi lánc megtörése 15.15 Ugrások és függvény hı́vások Függvényhı́vások eltüntetése Feltétel nélküli ugrások eltüntetése 15.2 Optimalizálás
méretre 15.3 Memória hozzáférés optimalizálása 15.4 Ciklusok optimalizálása 15.5 Vector programozás 15.6 Problémás utası́tások 174 16. Fejezet Optimalizált példák 16.1 ASCII tábla nyomtatása rövidebben Ez a program a 9.11 fejezetben bemutatott program rövidebb változata Ebben a programban két dolgot használunk ki. Az egyik az, hogy a DL regiszter egy 8 bites regiszter és ı́gy 0 és 255 közötti számokat tud tárolni, illetve az ASCII karaktereket is 0 és 255 közötti számok reprezentálnak. Így a regiszter és az ASCII karakterek számai között egy az egyes megfeleltetést tudunk létrehozni. Az igazi trükk a 6. sorban van, itt növeljük meg mindig a DL regiszter tartalmát Igen ám de amikor a DL regiszter tartalma 255, majd megnöveljük az értékét, akkor bár 256-ot kellene kapni, de ezt nem képes a regiszter tárolni, ı́gy “átfordul”. Ez azt jelenti, hogy a 255 után a 0
következik Ez fog történni 6. sorban és az INC utası́tás be is állı́tja a ZF státusz bitet megfelelően, vagyis nincs szükség CMP utası́tásra, elegendő a feltételes utası́tást használni. Ezeknek a trükköknek a segı́tségével 15 byte-ról 12 byte-ra lehet csökkenteni a lefordı́tott program méretét. 1 2 3 4 5 6 7 8 org 100h MOV DL, 0 MOV AH, 2 ujra: INT 21h INC DL JNZ ujra INT 20h 16.1 tábla: Az ASCII tábla kinyomtatására szolgáló optimalizált program 175 176 17. Fejezet Megjegyzések 17.1 Szokásos hibák Az alábbi lista a leggyakrabban elkövetett hibákat tartalmazza: • Elfelejtjük a regisztereket elmenteni! Minden művelet vagy függvény elején mentsük el a regisztereket, illetve a művelet vagy függvény végén állı́tsuk helyre a regisztereket. Erre azért lehet szükség, mert a regiszter értékére később szükségünk van, vagy nem szeretnénk, hogy a
művelet vagy függvény az általa megváltoztatott regiszterekkel a végrehajtásban az utána következő műveleteket befolyáolja. Ne felejtsük el, hogy a POP és PUSH utası́tások sorrendje különböző kell legyen (lásd ??. fejezet) • A PUSH vagy POP utası́tásnak nincs megfelelő párja. Ez lényegében azt jelenti, hogy bármilyen végrehajtási útvonalon fut le a program a PUSH és POP műveletek számának meg kell egyeznie. Vegyük a következő példát: push bx test cx, cx jz vege . pop bx vege: ret Ha a program a vege cı́mre ugrik, akkor a pop bx utası́tás nem hajtódik végre. Ez azt jelenti, hogy a ret utası́tás a BX regiszter korábbi értékét fogja levenni a veremről és ı́gy rossz cı́mre fog ugrani a program. • Egy speciális célra foglalt regisztert nem rendeletésszerűen használunk. Például a BP regiszternek speciális rendeltetése van amikor függvényeket használunk •
Stack-relatı́v cı́mzés használata PUSH műveletek után. A stack-relatı́v cı́mzés itt azt jelenti, hogy az SP regisztert használjuk a cı́mzésben. Például: mov [sp+4], di push ax push bx push bp cmp si, [sp+4] 177 A fenti kódrészletben a programozó eredeti célja valószı́nűleg az volt, hogy az SI és DI regisztereket összehasonlı́tsa, de a két PUSH utası́tás megváltoztatja az SP regiszter értékét. Gyakorlásképpen gondoljuk végig, hogy az SI regiszter mivel lesz összehasonlı́tva? A válasz lábjegyzetben található.1 • Egy változó értékének és cı́mének összekeverése! valtozo: dw 0 . mov bx, valtozo ; valtozo cı́me kerül BX-be mov ax, [valtozo] ; valtozo értéke kerül AX-be mov cx, [bx] ; valtozo értéke kerül CX-be • A függvény hı́vási könvenciókat nem tartjuk be. Fontos, hogy a programokban a függvényeknek a megfelelő sorrendben adjuk át a
paramétereket. • Elfelejtjük a RET utası́tást a függvény végéről. Ha a RET utası́tást nem tesszük a függvény végére, akkor a program a függvény utolsó utası́tása utáni művelettel fog folytatódni anélkül, hogy visszatérne a hı́vási ponthoz. • Elfelejtjük kiürı́teni a Floating-Point Unit vermét. Mielőtt az FPU verem bármelyik “regiszterébe” értéket ı́rhatnánk a regisztert törölni kell. (Lásd ?? fejezet) • Az irány státusz bitet (direction flag) elfelejtjük megfelelően beállı́tani. • Előjeles és előjel néküli egész számok összekeverése a műveletek során. • Rossz indexelést használunk egy tömb adatszerkezet elérése során. A tömb indexet meg kell szorozni a tömb egy elemének méretével: tomb: dw 0, 0, 0, 0, 0, 0, 0, 0, 0 . mov bx, tomb . mov si, 2 lea di, [bx+si*2] • Egy tömb cı́mzésénél a nem megfelelő indexet használjuk.
Figyeljünk arra, hogy egy n elemű tömbben az elemek indexe nullától n − 1-ig tart. • A LOOP műveletet úgy használjuk, hogy a CX regiszter értéke zérus. Ne feledjük, hogy a LOOP művelet először csökkenti a CX regiszter értékét és csak utána ellenőrzi, hogy a regiszter zérus lett-e. Vegyük például a következő kódrézletet: mov cx, 0 ujra: . loop ujra Gyakorlásképpen határozzuk meg, hogy a fenti kódrészlet hányszor fut le? A válasz lábjegyzetben található.2 1A 2A fenti kódrészletben valójában az SI és az AX regiszterek tartalma lesz összehasonlı́tva. fenti kódrészlet 216 -szor fog lefutni. 178 A. Függelék ASCII táblázat Elvileg 256 ASCII karakter van. Az A1 táblán látható ASCII táblázat csak a fontosabb karaktereket tartalmazza és direkt ilyen módon van ábrázolva. dec ⇓ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ⇒ hex 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 0
NULL BELL BS HT LF VT FF CR 16 1 32 2 SPC ! ” # $ % & ’ ( ) * + , . / 48 3 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 64 4 @ A B C D E F G H I J K L M N O 80 5 P Q R S T U V W X Y Z [ ] ˆ A.1 tábla: Egyszerűsı́tett ASCII táblázat A táblázatban szereplő speciális szimbólumok magyarázata: BELL - Alarm BS - Backspace HT - Horizontal TAB LF - Linefeed VT - Vertical TAB FF - Formfeed CR - Carriage return 179 96 6 ‘ a b c d e f g h i j k l m n o 112 7 p q r s t u v w x y z { | } ˜ DEL SPC - Space DEL - Delete 180 B. Függelék Felhasznált irodalom 1. Agner Fog: Optimizing subroutines in assembly language An optimization guide for x86 platforms, Copenhagen University College of Engineering, 2009 2. Sivarama P Dandamudi: Introduction to Assembly Language Programming, For Pentium and RISC Processors, Springer, 2005. 181 Példa programok listája Öt karakter beolvasása és kinyomtatása fordı́tott sorrendben, 100 Érték
szerinti paraméter átadás a vermen keresztül, 136 ASCII tábla kinyomtatása, 111 ASCII tábla nyomtatása rövidebben, 175 Cı́m szerinti paraméter átadás a vermen keresztül, 136 CAPS LOCK állapotának nyomtatása, 114 Egy byte bináris kinyomtatása, 91 Egy byte hexadecimális kinyomtatása, 95 Egy hexadecimális szám kinyomtatása, 93 Egy karakter beolvasása és a köv. kinyomtatása (a), 98 Egy karakter beolvasása és az utána köv. kinyomtatása (b), 98 Egy karakter kinyomtatása, 48 Egy karakter n-szeri kinyomtatása, 104 Egy sakktábla kinyomtatása, 108 Egy sakktábla kinyomtatása XOR-al, 109 Egy szöveg kinyomtatása, 49 Egy számjegy beolvasása és kinyomtatása, 97 Egy téglalap kinyomtatása, első rész, 105 Egy téglalap kinyomtatása, második rész, 105 Egymásba ágyazott függvények, 126 Első program, 47 Két szám összeadása, 101 Két szám összeadása ciklussal, 101 Második
példa a paraméter átadásra memórián keresztül, 128 Példa a paraméter átadásra a vermen keresztül, 132 Paraméter átadás memórián keresztül, 128 Paraméter átadás regiszteren keresztül, 128 Szöveg hosszát megállapı́tó függvény, 169 Szöveg nyomtatása függvénnyel, 136 Szám kiı́rása decimális formában, 112 Számjegyek nyomtatása rekurzı́v függvénnyel, 144 Változó számú szám összeadása és annyi pont nyomtatása, 139 182 Tárgymutató rekurzı́v függvény, 141, 143 rendszer busz, 17 RISC, 11 ASCII, 48 AT&T, 15 big-endian, 24 branching, 21 seg kulcsszó, 39 stack, 121 stack frame, 131 C programozási nyelv, 124, 135, 136 CALL, 130 Carry bit, 92 CISC, 11 COM, 87 times kulcsszó, 39 TOS, 121 ugró utası́tás, 21 data alignment, 25 wrt kulcsszó, 40 EXE, 88 függvény, 124 frame pointer, 131, 141 gépi kód, 12 I/O kontroller, 26 időleges tárolás, 123 Intel,
15 LIFO, 121, 134 Little endian, 38, 40 Little-endian, 125 little-endian, 24 lokális változók, 141 LSB, 24 maszkolás, 95 megszakı́tás, 27 memory management, 87 moduláris programozás, 124, 144 MSB, 24 nasm, 125 overhead, 144 paraméter átadás, 126 paraméter átadás memóriával, 126, 128 paraméter átadás regiszterrel, 126, 128 paraméter átadás veremmel, 126, 130 POP, 121, 123, 124, 130 POPA, 135 Program Segment Prefix, 87 PSP, 87 PUSH, 121, 123, 130 PUSHA, 135 183