Kicsit megállt ez a sorozat, de nekem is fel kellett hoznom magam, hogy folytatni tudjam. A mostani részben megismerkedünk a számolással, elvégre a számítógép egy számító gép. (Pocsék poén achivement unlocked)
Kétféle módon számolhatunk. Az egyik módszerrel csak alapműveleteket végezhetünk egész számokon, a másikkal viszont a megnyílik előttünk a lebegőpontos számítás és a teljes matematikai függvénytár is. Jól hangzik nem? Kíváncsi vagyok, hogy a végére is ezt mondjátok-e.
Kezdjük az egyszerű módszerrel. Az egyszerű módszernél használhatjuk az általános célú regisztereket. Számolhatunk velük, de csak egész értékekkel dolgozhatunk. Összeadás:
mov ax, 3
add ax, 4
Az ax regiszterben az ax (3) és a 4 összege kerül, tehát hét. Olyan, mint a magasabb szintű programozási nyelvekben az x = x + 4. Szorzás:
mov bx, 4
mul bx
Az ax regisztert megszorozza egy másik regiszterrel. Közvetlen értékadás nem lehetséges, mint az add esetén. Az eredményt mindig ax-ben fogja tárolni. A div, ami az osztást végzi, kicsit másképp működik, ugyanis az eredménnyel két regisztert fog megtölteni. Az ax-t és a dx-t. Az ax-be kerül a hányados, a dx-be a maradék. Emiatt van egy ökölszabály, hogy a dx regisztert az osztás előtt nullázzuk. Ha nem így teszünk, a programunk fagyni fog.
mov dx, 0
mov ax, 10
mov bx, 2
div bx
Még egy fontos dolgot kell megemlíteni az osztással kapcsolatban. Nem lehet neki közvetlenül értéket adni, mint az összeadásnál. Ezért használom a bx regisztert közvetítőnek.
Egyszerűen hangzik, nem? Nos, van néhány probléma. Először is, az eredmények nagyobbak lehetnek, mint ami a regiszterben elfér. Másodszor, a negatív számokat máshogy tárolják. Ezért külön utasítások vannak az előjeles számokra szorzás és osztás esetén. Ezek az imul és az idiv. Ezekkel most nem foglalkozom, de tudjatok róla, hogy ilyenek is vannak. Most csak az egyszerű esetekkel foglalkozom, aztán ha már lesz rutin, visszatérünk rájuk.
A hibakezeléssel sem foglalkozunk, mert az intrók nem nagyon kezelik a hibákat, mert azzal értékes helyet pazarolnak. Honnan tudjuk, hogy a művelet hibás? A processzor segít nekünk, ahogy tud. Fogja magát és újabb regisztereket fog beállítani. A következő részben csak debug lesz, ott visszatérek erre.
A bonyolult módszer a matematikai segédprocesszor, az FPU használatán alapul (ami csak régen volt külön processzor, most már egybe van építve a CPU-val, de hát a kompatibilitás miatt így maradt meg a köztudatban). Itt újabb regisztereket kapunk, amit ST-vel és egy számmal jelzünk. Tehát az első regiszter az ST0. Újabb regiszter kezelő utasításokat kell megismerni. Ezek a regisztereket nem érhetjük el szabadon, hanem egy verem szerű képződményen keresztül. Azért hívom verem-szerűnek, mert nem tud megtelni. Ha nyolcnál több értéket pakolsz bele, a régieket egyszerűen elfelejti. Bármilyen hibaüzenet nélkül. Jó, mi?
Hogyan tudunk adatokat másolni ezekbe a regiszterekbe? Csakis memóriából. De azért ne örüljetek annyira! Ezek a regiszterek 80 bitesek. A lefoglalt memórai tehát vagy kisebb, vagy nagyobb lesz, mint a regiszter, soha nem akkora. Ez majd jusson eszetekbe, ha nem értitek, miért változott meg a szám, amit az FPU-ba töltöttetek.
Először jelezzük a kódban, hogy mostantól FPU-t is akarunk használni:
finit
Most pedig másoljunk értékeket a regiszterekbe. Hiszen értékek nélkül nem tudunk számolni. Mivel eddig egész számokkal dolgoztunk, most is egész számokat fogunk használni.
fild [memóriacím]
Ezzel a memóriacímről az értéket az ST0 regiszterbe másoljuk. A kapcsos zárójel nélkül a memóriacímet másolja át, nem az ott tárolt értéket. Ha még egyszer kiadjuk a parancsot, akkor az ST0 értéke ST1-be kerül, az új érték meg ST0-ba. Újabb parancsok tovább tolják a régi értékeket a verem mélye felé.
Ha ki szeretnénk nyerni az értékeket, akkor azt a következő módon tehetitek meg:
fist [memóriacím]
Az ST0 érték a memóriacímre kerül, ST1 pedig ST0-ba. Ha az értékek az FPU regisztereiben vannak, akkor kezdődhet a számolás.
Összeadás fadd, szorzás fmul. Az eredmény szinte minden esetben az ST0-ba kerül, hasonlóan ahhoz, ahogy az ax-be helyezik az eredményt az egyszerű módszernél. Az alapműveletek mellett használhatjuk a szögfüggvényeket is. Ennek örömére készítettem egy egyszerű programot, ami egy kört rajzol, és rengeteg FPU műveletet érint.
Sokféle módon lehet kört rajzolni, ez a módszer végigmegy 0-tól 360 fokig az összes szögön, és mindegyik szöghöz kiszámít egy X és Y koordinátát. Ha elég kis lépéssel haladunk végig a szögeken, akkor folytonos lesz a kör. Ez nem a legoptimálisabb módszer, mert egy pontra többször is rajzol, de sok számítás kell hozzá, könnyen érthető és látványos az eredmény. Demonstrációs célra tökéletes.
org 100h
section .text
start:
mov ax, 13h
int 10h
mov ax, 0A000h
mov es, ax
mov cx, 628
cyc:
mov word [r], cx
finit
fild word [r]
fmul dword [step]
fsincos
fmul dword [radius]
fadd dword [centery]
frndint
fimul dword [width]
fistp dword [y]
fmul dword [radius]
fadd dword [centerx]
frndint
fiadd dword [y]
fistp word [y]
mov di, [y]
mov ax, 2
mov [es:di], al
loop cyc
waitsec:
in al, 60h
dec al
jnz waitsec
mov ax, 03h
int 10h
ret
section .data
radius: dd 70.0
centerx: dd 160.0
centery: dd 100.0
step: dd 0.01
width: dd 320
section .bss
y: resd 1
r: resw 1
Kezdjük a végén, mert új elemek vannak a programban. Mint említettem, az FPU-nak memóriacímek kellenek. Ezeket definiáljuk a section .data és section .bss részeknél. Előbbi statikus adat, nem módosíthatjuk. Definiáljuk a kör sugarát (70), a kör középpontját (160 és 100), egy lépésközt, valamint a képernyő szélességét (320). Meg kell adni a felhasználni kívánt memória méretét is. A dd a double word, vagyis 32 bit méretű. Ez a legegyszerűbb adattípus, amivel számokat cserélhetünk az FPU regisztereivel.
Ha módosítani is szeretnénk az adott memóriaterületet, akkor a section .bss-ben definiálhatunk változókat. Ebben a programban két változót fogunk használni, az y-t és az r-t. A dd helyett itt resd-vel adjuk meg a 32 bites típust. A resw egy word méretű memóriarész, tehát csak 16 bites. A legvégén a szám azt jelenti, mennyi elemet akarunk az adott adattípusból. Tehát ha egy tömbre lenne szükségünk, akkor ennek a tömbnek az elemeinek a számát adhatjuk meg- Az adattípusokról most elég ennyit megjegyezni, de tudjunk róla, ennél sokkal több van. Ami a lényeg jelen pillanatban, hogy d-vel definiáljuk, ami konstans, res-el ami változhat.
Oké, a program nagy része ismerős lehet. Ami új, az a finit-el kezdődik, és az utolsó fistp-vel végződik. Koncentráljunk erre a részre. Ahogy említettem, a finit inicializálja az FPU-t, innentől használhatjuk a szuper utasításokat.
fild word [r]
Az r változó értéke ezzel az ST0-ba kerül. A [r] az r változó memóriacíme, ebből még nem lehet tudni, hogy mekkora az adott változó. Ezért kell explicite megmondani a word kulcsszóval, hogy mennyi bájtot is akarunk beolvasni.
fmul dword [step]
Az ST0-t és a step értékét összeszorozzuk. Mivel ST0 az r változó, ezért az ST0 mostantól a r * step szorzat eredményét tartalmazza. A step változó már dupla szó, ezért dword-el jelezzük, hogy a számot nagyobb memóriáról kell beolvasni.
fsincos
Egy lépésben kiszámoljuk az ST0 szinusz és koszinusz értékét. Az ST0-ba kerül a szinusz, ST1-be a koszinusz kerül. Beteg mi? Szerencsére a kör pontjainak kiszámításához pont erre a két szögfüggvényre van szükségünk.
fmul dword [radius]
A ST0, vagyis a szinusz eredményt megszorozzuk a kör sugarával, tehát megkapjuk a kör adott szögéhez tartozó pont Y koordinátáját, ha a kör középpontja a 0 pontban lenne. De nem ott van, ezért szükség lesz a következő utasításra is:
fadd dword [centery]
Vagyis hozzáadjuk a kör középpontjának Y koordinátáját, ami szintén dupla szó (dword). Van egy lebegőpontos Y koordinátánk az ST0 regiszterben. Ezt képernyő pixelnek kell megfeleltetni. Bizonyára emlékszünk, hogy ebben a felbontásban, amiben dolgozunk, a pixelek folyamatosan mennek balról jobbra, ha a képernyő szélére érnek, ugranak egy sort. Ezért az Y * WIDTH + X képlettel kiszámoljuk, melyik pixelről van szó. A számításoknak még nincs vége!
Először is a lebegőpontos számokat egésszé konvertáljuk, mert a képernyő pozíció egész szám.
frndint
Az utasítás az ST0 regiszter tartalmát egész számmá konvertálja. Több módon is egésszé konvertálhatunk egy tizedestörtet, ez az utasítás a legközelebbi egész számra kerekít. Mivel az egész számokat máshogy tárolja a gép, ezért nekünk kell tisztába lenni, hogy milyen típusú adatokon hajtjuk végre a műveleteket, és a típusnak megfelelő üzenetet kell használni.
fimul dword [width]
A fimul a szorzás, de egész számokkal. Az Y koordinátánk már egész szám, ezért így szorozzuk.
fistp dword [y]
Az ST0 regiszter értékét az y változóba helyezzük. Az X koordináta eddig az ST1-ben volt, az most az ST0-ba kerül. Megismételhetjük az egész procedurát, hogy megkapjuk a másik koordinátát.
fmul dword [radius]
fadd dword [centerx]
frndint
fiadd dword [y]
Az utolsó lépésnél az X koordinátát, ami az ST0 regiszterben van, hozzáadjuk az Y * WIDTH részhez, amit korábban elhelyeztünk az Y változóban. Igen, itt kicsit félrevezető a változó neve, mert nem a tiszta Y koordinátát tartalmazza. Sőt, továbbmegyünk! Kimentjük az értéket az utolsó fistp utasítással az Y változóba, ami immár a pixel sorszámát tartalmazza a memóriában. De 256 byte intróknál spórolni kell a változókkal, mert sok helyet foglalnak.
Aki idáig eljutott, és begépelte, lefordította a programot, csalódni fog. Teljes kör helyett csak egy félkört fog rajzolni! A program hibát tartalmaz. A következő részben sajnos uncsi memória méretekkel és még uncsibb debuggolással fogunk megismerkedni. Megtaláljuk a hibát, és kijavítjuk.