KEZDJÜK A RIZSÁVALIgen, tudom, hogy van egy teljesen ugyanilyen leírás a hivatalos fórumon, és azt is, hogy ugyanazt fogom gyakorlatilag elmondani kétszer. Viszont fontosnak érzem, hogy azt az alapot kicsit felturbózzam, és megérthetõvé tegyem azok számára is, akik esetleg nem értik elsõre, hogy mi az a LIFO, heap, stack, és a többi. Épp ezért, és az emészthetõség miatt nem állt szándékomban a formális szaknyelv használata.
Ez után pedig térjünk rá, hogy ez a cikk lényegében kinek jó: akit érdekel. Ettõl a módod gyorsabb nem lesz, kevesebb bug biztos nem lesz benne, nem is lesz stabilabb, tehát tényleg csak azoknak ajánlom, akiket
érdekel, hogy valójában hogy is fut le a kódjuk, amit írnak.
AMXMint ahogy az a deAMX topicban is elhangzott, az AMX rövidítés jelentése
Abstract
Machine E
xecutor, vagyis absztrakt gép végrehajtó, értve ez alatt azt, hogy ez az a kód, amivel az absztrakt gép végrehajtja az utasításokat.
AZ ABSZTRAKT GÉPEKMi is egy absztrakt gép? Mint azt bizonyára mindenki tudja, a legfontosabb elméleti gép, automata az ún. Turing-gép. Gyakorlatilag egy olyan automata, amely végtelen sok memóriával rendelkezik. De mi is egy automata?
Elõször is szögezzük le, hogy a Turing-gép nem létezik, csak egy elméleti modell. Ezért használunk rá nagyon egyszerû, leíró nyelvezetet. A Turing-gép egy olyan gép, amelyhez egy végtelen hosszú papírszalag tartozik. Minden papírszalagon \"betûk\"/\"szimbólumok\" (ez a hivatalos megfogalmazás) találhatók. Egyszerûség miatt tekintsük ezeket nullának és egynek. A gépbe be van programozva egy adott utasítássorozat, amit végrehajt a szalagon, ezt a logikai vezérlõegységbe programozták be.
Például tegyük fel, hogy a szalagunkra ez van írva: 0[0]11. Az olvasófej a második nullán áll. Tegyük fel, hogy az az utasítássorozat van beprogramozva, hogy
1: HA 0 AKKOR 1
2: HA 1 AKKOR 0
3: EGYEL ELÕRE
4: UGRÁS 1-RE
A következõ utasítás a szalagon lévõ karaktereket 0100-ra változtatja, és nem áll meg. Amennyiben nem ugranánk vissza egyre a következõ változó értéke már nem lenne módosítva.
MIÉRT ELÕNYÖS AZ AMX?Az absztrakt gép egy számítógép virtuálisan. Ennek több elõnye van az x86 assemblyvel (tehát a \"tényleges\" natív assemblyvel) szemben:
- Több platformon, több architektúrán elfut: a p-kód miatt minden operációs rendszeren és a 8-bitestõl a 64-bites processzorig átportolták.
- A p-kódnak több elõnye is van: a modern operációs rendszerek nem engednek a CODE szegmensbe írni és a DATA szegmenst lefuttatni.
- Sokkal egyszerûbb a programot a saját \"mini-számítógépen\" (sandbox) futtatni: egy natív assemblyben írt program rekurzív utasítása könnyen kifagyaszthatja az alkalmazást, és megrongálhatja a rendszer STACK-jét.
A REGISZTEREKAz AMX regisztereket használó absztrakt gép. A regiszterek a processzorok elidegeníthetetlen részei a Neumman-elvek óta. Ennek elõnye az, hogy az absztrakt gép utasítása jobban \"rásimul\" a natív kódra, tehát a futásidõ csökken, valamint az utasítások száma csökkenthetõ.
Az AMX egy kétregiszteres struktúra, két számításra alkalmas regiszter található meg: a PRI és az ALT.
Az alábbi kódot:
a = bö 2;
Az AMX assemblyjében így kaphatjuk meg:
load.pri b ;betöltjük a b változót az elsõdleges (PRI) regiszterbe
const.alt 2 ;betöltjük a \"2\" konstanst az alternatív (ALT) regiszterbe
add ;az add utasítás a következõt csinálja: PRI = PRI + ALT
stor.pri a ;az a változóba eltároljuk a PRI értékét
A REGISZTEREK LISTÁJARegiszter rövidítése | Regiszter teljes neve | Használata |
PRI | Primary Register | ALU, logikai és számolási mûveletek |
ALT | Alternative register | Másodlagos regiszter a bonyorultabb mûveletekhez |
FRM | Stack frame pointer | Egy függvény lefutása elõtti stack állapotot tárolja |
CIP | Code instruction pointer | A végrehajtandó utasítás (az AMX-ben szinte mindig a main()-ra mutató pointer) |
DAT | DATA pointer | A DATA szegmens kezdetére mutató pointer |
COD | CODE pointer | A CODE szegmens kezdete |
STP | STACK top | A STACK teteje / legalacsonyabb memóriacíme |
STK | STACK index | A STACK jelenlegi helyzete |
HEA | HEAP pointer | A HEAP teteje |
MEMÓRIAKÉPAz AMX fájl memóriaképét, vagyis a memóriában elfoglalt helyét mutatja be az alábbi táblázat. A HEAP és a STACK alapvetõen nincs a bináris fájlban, elõállítását az AMX végzi a PREFIX adatok alapján.
Ahhoz, hogy a lenti memóriaképnek megfelelõ bináris kódot kapj, használd a #pragma compress 0 direktívát a scriptedben.
LEGALACSONYABB MEM.-CÍM |
PREFIX |
CODE |
DATA |
HEAP ⇊ |
STACK ⇈ |
LEGMAGASABB MEM.-CÍM |
A prefixA prefix rész a kód legelején terül el, és elsõdleges feladata általános metaadatok, pointerek, és a függvények listáját tárolni. A memóriatérképe a következõ (az X-el jelölt dinamikusan változik az adat mennyiségétõl függõen):
(a teljes táblázat megtalálható a pawn-imp guide-ben, én ezt a verziót Y_Lesstõl szereztem, köszönöm)
MÉRET | Memóriaterület | Leírás |
4 | Méret | A fájl mérete |
2 | Ismeretlen | |
1 | Fájl verzió | Az AMX verziója |
1 | Minimum VM verzió | |
2 | Flagek | A fordítónak fordítás közben megadott paraméterek |
2 | Defsize | |
4 | COD offset | A CODE szegmens kezdetének helyzete |
4 | DAT offset | A DATA szegmens kezdetének helyzete |
4 | HEA offset | A HEAP szegmens kezdetének helyzete |
4 | STP offset | A STACK szegmens kezdetének helyzete |
4 | CIP | A main() függvvény memóriacíme (vagy -1) |
4 | Publikus függvények listájára mutató pointer | |
4 | Natívák listájára mutató pointer | |
4 | Libek listájára mutató pointer | |
4 | Publikus változók listájára mutató pointer | |
4 | Public tagek listájára mutató pointer | |
4 | Names táblára mutató pointer | |
X | Public lista | |
X | Native lista | |
X | Lib lista | |
X | Pubvar lista | |
X | Tag lista | |
X | Name tábla | |
A listák egy eleme a következõképp épül fel:
MÉRET | Memóriaterület | Leírás |
4 | Függvény/változó pointer | A függvény kezdetére/vált. helyére mutató pointer |
4 | Név pointer |
A Names tábla a stringeket tartalmazza: ömlesztve, tömörítve ábrázojla.
A stringek NULL-delmitáltak (tehát a minden string végén szereplõ \\0 karakter jelzi a string végét), és tömörítettek. A konstansok (különösen a stringkonstansok) a DAT szegmensben tárolódnak, tehát irrelevánsak jelen esetben.
A code szegmensA TEXT vagy CODE szegmens egy olyan szegmens egy bináris fájlban, vagy a memóriában, amely a futtatható utasításokat tartalmazza. A CODE szegmensnek a memóriában általában csak 1 példányra van szüksége, mivel megosztható. A CODE szegmens szinte mindig csak olvasható, hogy megakadályozható legyen az, hogy a program véletlen a saját utasításait módosítsa.
A data szegmensA DATA szegmens a program virtuális memóriájának egy része, az egész program számára elérhetõ (ti. globális) változókat tárolja.
A heapA heap, vagy \"halom\" a memória dinamikusan használható része. A \"halom\" igen deskriptív: hiszen gyakorlatilag a halomra akármikor hozzáadhatunk vagy elvehetünk valamit anélkül, hogy az egész összedõljön. Ha egy új dinamikus változót hozunk létre, lényegében a heap-bõl vesszük el a területet.
Mivel a létrehozott változónak lefoglalt memóriaterület sosem ismeretes elõre, a
new statement mindig egy pointerrel tér vissza a heapban tárolt pozícióval.
A PAWN nyelv
nem használ dinamikus változókat, minden változót vagy a DATA (glob), vagy a STACK (lok) szegmensek tárolnak, ezért például a memory leak-ek nem lehetségesek. Azért ide megemlítem, ha valakinek van kedve a memory leakekhez, hogy Y_Less malloc függvénykönyvtárával lehetõséged van \"dinamikus\" memóriát kiosztani (azért idézõjelbe, mert ez sem igazi dinamikus memória, és ez sem a heapben van, csak egyfajta workaround).
A stackA STACK, hivatalos magyar nevén \"verem\", deskriptívabb nevén \"rakás\" egy memóriakezelési szerkezet. Mielõtt jobban megvizsgáljuk, nézzük meg az alábbi példát:
Tegyük fel, hogy van egy rakás téglád egymáson. Mivel a téglák nehezek és rakásban vannak (tehát mindegyik tégla a másik tetején), ezért lényegében három dolgot tehetsz a téglákkal anélkül, hogy összedõljenek:
- Ránézel a legfelsõ téglára
- Leveszed a legfelsõ téglát
- Felraksz egy újabb téglát
A stack egy olyan memóriakezelési struktúra, ami változókat tárol, hasonlóan a tömbökhöz. Viszont míg a tömbökben tetszés szerinti elemet elérhetsz, addig a stack ennél limitáltabb. Lényegében a fenti három dolgot teheted meg a stackkel:
- Megnézed, mi a felsõ érték, vagy top
- Leveszed a legfelsõ értéket, vagy pop
- Felraksz egy újabb értéket, vagy push
Ahogy a téglás hasonlat is mondja: ha valaki egy téglát rárak a rakásra, akárki, aki levesz egy téglát a legfelsõt fogja levenni. Ez a LIFO-elv: last in-first out. A legelsõ felrakott elem lesz a legelsõ, amit levesznek.
Ez, bár szép analógia, lehetne egy jobbat is létrehozni. Vegyünk egy adott számú postaládát: mindegyik egymás fölött, mindegyik csak egy dolgot tarthat magában, mindegyik üresként kezdi, és persze mindegyik az alatta lévõvel össze van ragasztva, így a számuk nem változik. A kérdés: ha a postaládák száma nem változtatható, hogyan kapunk egy stacket? Jelöljük meg egy matricával a legmagasabban lévõ üres postaládát. Minden a matrica alatt lévõ postaláda a stack tagja, minden a matrica fölött lévõ postaláda nem a stack tagja.
Ez szinte azonos azzal, hogy mûködik a stack. A stack egy elõre meghatározott mennyiségû memória: a postaládák memóriacímek, és a levelek a memóriacím alatt lévõ adatok. A \"matrica\" a fentebb említett STK regiszter: Stack Index/Stack Pointer, ami
mindig a legnagyobb elérhetõ stack címre mutat. Az egyetlen különbség a postaládás hasonlat és a valós stack közt az, hogy ha az adatot el akarjuk tüntetni, nem kell \"kiüríteni\" a postaládát: elég a matricát egyel lejjebb helyezni és az a memóriarész onnantól nem a stack tagja, amíg a következõ memóriarész felül nem írja.
Miket rakunk a stackbe? Változókat, paramétereket, és függvényhívásokat.
Mivel a globális változók a DATA szegmensbe kerülnek, ezért nekünk lényegében csak azzal kell foglalkoznunk, mi történik a lokális változókkal: tehát a függvényeken belül meghívottakkal.
Példaképp tekintsük meg az alábbi kódot:
main() { new a=4; foo(a,2); }
foo(a,b) { }
Ez, a nem mûködõ, viszont példaként tökéletesen használható kódrészlet tökéletes példa a stack bemutatására. Mikoris a main() függvény meghívja a foo függvényt, az alábbi játszódik le (ezt szaknyelven a
Standard Entry Sequence-nek hívjuk):
Az egyszerûség kedvéért a main()-t fogjuk hívni a meghívó függvvénynek míg a foo()-t a meghívott függvvénynek.
- A main() ráhelyezi a foo() függvény visszatérési pointerét, vagyis azt, hogy a függvény lefutása után a foo() függvény a main()-en belül hova ugorjon vissza.
- Ezután a main() függvény betölti foo() függvény paramétereit a stackbe fordított irányban (ez az ún. C-declaration order vagy cdecl).
- Ezután következik a main() függvény stack frame pointerének helyzete, szintén rákerül a stackre.
- A fenti adat memóriacíme tárolódik el a Stack Frame Pointerben (FRM regiszter). A pointernek több célja is van: egy részrõl meghatározza, hol kezdõdik az a stack szegmens, ahová a függvényünk (foo()) a helyi változóit fogja tárolni.
Más részrõl be kell lássuk, hogy az STK (a stack tetejére / legalacsonyabb memóriacímére mutató pointer) nem megbízhatóan adja vissza a változók helyét, hiszen új változók felvétele esetén a stack is feljebb kerül az STK-val együtt. Ez a FRM elsõdleges használati területe: nevezetesen, hogy a függvényekben használt lokális változók létrehozásához a pontatlan STK helyett a függvény lefutása során kötött FRM-t használjuk.
Mivel a stack memóriacímei csökkennek (lásd a memóriaképet), egy lokális változó (ami a mostani pointer fölött fog lenni) memóriacíme mindig a pointerhez képest negatív, míg a paraméterek értéke mindig pozitív lesz.
Harmadrészrõl az FRM alatt lévõ memóriacím (tehát a main() függvény ugyanilyen framejére mutató pointer) megadni azt a területet, ahova a függvény lefutásakor vissza kell léptetni a STK-t (érvénytelenítve ezzel az összes lokális változót).
- Ezután természetesen a függvény lefut, és minden lokális változó rákerül a stackre.
Minden stackre helyezett elem esetén természetesen frissítõdik a STK regiszter helye (ami a stack legfelsõ elemét mutatja).
Ha a függvény megáll, lefut a fenti ellentéte, a
Standard Exit Sequence - A STK (stack tetejére / legalacsonyabb memóriacímére mutató pointer) értéke frissül a FRM értékére, megszûntetve ezzel a lokális változók elérésnek lehetõségét.
- A FRM a mostani helyzete alatt található értékére frissül (ami pont a meghívó függvény ugyanolyan FRM értéke).
- A main() függvény visszatérési pointerét leszedi a stackrõl és odaugrik, és folytatódik a kód végrehajtása.
Zárásnak még annyit: hogy azért is futnak a több-threades programok gyorsabban, mert mindegyiknek külön stackje van.
A heap és a stack, stack overflowLehetõség van arra, hogy a stack számára elõre lefoglalt memóriát túllépjük, és beleírjunk a heap-be: ez általában nagy mennyiségû változó esetén következik be, ezt nevezzük stack overflownak.
Ez azért lehetséges, mert a heap és a stack ugyanazt a memóriaterületet kapja, de ellentétes irányban kezdõdnek: a heap egyre növekvõ, míg a stack egyre csökkenõ memóriacímeket kap.
A stack overflow tényérõl a fordító tájékoztatást ad: mégpedig kiírja, hogy az egyes szegmensek mennyi bájtot foglalnak el, és hogy a HEAP/STACK szegmensnek mekkora a maximális mérete, és mekkora az a méret, amit igényelne ahhoz, hogy minden változó elférjen benne. Ez azért lehetséges, mivel a stack mérete a fordítás során már ismert. Jogosan felmerülhet a kérdés, hogy akkor miért nem kap az AMX automatikusan több memóriát a programhoz: ez a mennyiség limitált és alapjában véve nem túl nagy, tehát megeshet, hogy egy normálisabb módhoz is növelni kell. Ezt a #pragma dynamic direktívával lehet megtenni.
(és ennyi, frissítések várhatóak)