A tömb azonos típusú elemekből álló, sorosan tárolt adatcsoport. Amennyiben hossza előre ismert, statikus tömbnek nevezik, ha hossza a program futása során (run time) változik, dinamikus tömbnek, vagy listának nevezzük. Ebben a cikksorozatban a statikus tömbökkel foglalkozunk. A tömböknek több dimenziója is lehet, ez alapján tudunk belőlük kijelölni egy adott elemet. Ezt a kijelölést hívjuk indexelésnek, a jelölő dimenziókat pedig - mi másnak, mint - indexnek.
Mikor
lehet szükségünk tömbök alkalmazására? Amennyiben sok, azonos jellegű adatot
kell tárolni. Például írjunk olyan programot, amelyik megmondja, hogy az év
megadott hónapja hány napos. Fel kell venni egy 12 elemű egydimenziós tömböt,
amelyet elemenként az adott hónap napjainak számával töltünk fel, majd írunk
egy programot, amiben beolvasunk egy számot, és visszakeressük a hozzá tartozó
hónap napjainak számát a tömbből.
A logikai
terv már adott, vegyük a fizikai tervet. Figyelembe kell venni a tömb elemeinek
méretét, hogy a lehető legkisebb memóriát foglalja le a program. A hónapok
napjainak értéke, mint szám az [1,31] tartományba esik, ezt 5 biten
ábrázolhatjuk, mert 2^5 = 16, ebbe belefér az adott tartomány. Számokat
kiolvasni azonban csak 8, 16 és 32 bitenként tud a processzor, tehát vegyük az
5 bithez legközelebb álló, 8 bites ábrázolást. 8 bites elemekből álló, vagyis
úgynevezett "bájtos tömböt" fogunk deklarálni.
Korábban
már említettem, hogy az EBX regisztert érdemes címzéseknél használni, a tömb
indexelés pedig teljes mértékben fedi a címzés fogalmát. EBX regisztert a
hónap, mint szám értékével töltjük majd fel, és ezzel indexelünk. Valamire
azonban az indexelésnél figyelni kell: az első hónap valójában a nulladik
hónap, mert a tömb nulladik eleme adja az első hónap számát. EBX értékét tehát
eggyel csökkenteni kell. A számítógépek és a matematikusok már csak ilyenek...
Azt
hiszem, minden tisztázott, lássuk a medvét (tekintsük az macit)...
mov ebx, 2 ; februar
mov esi, Offset tomb
dec ebx
mov al, ds:[esi+ebx] ; AL-be kerul a napok
szama
vege:
; ...
; ide kerul a program kilepteteset
; elvegzo kodreszlet
tomb:
db 31 ; januar
db 28 ; februar
db 31 ; marcius
db 30 ; aprilis
db 31 ; majus
db 30 ; junius
db 31 ; julius
db 31 ; augusztus
db 30 ; szeptember
db 31 ; oktober
db 30 ; november
db 31 ; december
Tudom,
tudom, a február lehet 29 napos is. Na tessék, itt van egy olyan program, ami
erre is figyel. EAX-ben azt kell megadni, hogy szökőév van-e. Ha igen, akkor
amennyiben a 2. hónap napjait kérdezzük, a tömb februárhoz tartozó elemének
értékéhez egyet hozzáad.
mov ebx, 2 ; februar kell mar megint... ;)
; csak nem birunk rola leszokni?!
mov esi, Offset tomb
mov ah, 0 ; ez majd kesobb kell
dec ebx
cmp ebx, 1
je SzokoevEllenorzes
jmp Kiolvas ; ha nem februar
SzokoevEllenorzes:
cmp eax, 1
je DeJoSzokoev
jmp Kiolvas ; ha nem szokoev
DeJoSzokoev:
mov ah, 1 ; szokovben februar eggyel
tobb (masnapos:)))
Kiolvas:
mov al, ds:[esi+ebx]
add al, ah ; hozzaadjuk es kesz
vege:
; ...
tomb:
db 31 ; januar
db 28 ; februar
db 31 ; marcius
db 30 ; aprilis
db 31 ; majus
db 30 ; junius
db 31 ; julius
db 31 ; augusztus
db 30 ; szeptember
db 31 ; oktober
db 30 ; november
db 31 ; december
Nézzünk
példát többdimenziós tömbökre is. Vegyünk olyan esetet, amikor egy mátrixban
számokat tárolunk, mert egy koordináta rendszer pontjait ábrázoljuk vele. Ha
egy adott koordinátán az érték 0, ott nincs jelölt pont, ha 1, akkor jelölt
pont van. Hasonlóan tárolják a színes képeket is. Ezt bittérképnek (bitmap)
hívják, innen a Windows képformátumának "BMP" (BitMaP)
kiterjesztése.
Egy (3 x
3)-as mátrixot fogunk kezelni, s ezzel megmutatom, hogy a többdimenziós tömbök
elemei is természetesen sorfolytonosan foglalják a memóriát. Az elemek
indexe a [0,2] tartományba esik, mert az első elem a nulladik, vagyis a
harmadik a második (juuuuuuuj)... A sorfolytonos elhelyezkedés miatt a 0. sor
2. eleme után az 1. sor 0. eleme következik, etc, estöbö, estöbö.
Hogy
meghatározzuk az ehhez tartozó relatív memóriacímet, vagyis az eltolást a tömb
elejéhez képest, a koordináta Y értékét meg kell szorozni 3-mal (mert ennyi
elemet kell soronként átugrani), majd ehhez hozzá kívántatik adni az X
koordináta értékét.
A szorzást
az Intel processzoron a MUL (multiplicate) utasítással érhetjük el, 8 biten AL
regiszterbe kerül a szorzandó, BL-be a szorzó, az eredmény pedig AX-ben jön
vissza, 16 biten, mert ha pl. 60-at szorzunk össze 60-nal, az 3600, ez pedig
csak 16 biten fér el. Gondoskodni kell tehát a régi értékek mentéséről.
Adjuk meg
a mátrixot:
( 0, 0, 1 )
( 0, 1, 1 )
( 1, 0, 1 )
A következő
program a 2. sor 1. elemét fogja kiolvasni (a gép "agyával"
gondolkodva az 1. sor 0. elemét). Nézzük meg a mátrixot, itt egy 0 értékű elem
áll.
mov ah, 1 ; Y
mov al, 0 ; X
push ax ; mentes a szorzas miatt
mov al, ah
mov bl, 3
mul bl
mov bx, ax
pop ax ; visszatolt
mov ah, 0 ; felso resz nullazasa
add bx, ax ; 16 bites osszeadas
mov si, Offset matrix
mov al, ds:[si+bx] ; AL-be kerul ami kerul
))
vege:
matrix:
db 0 ; (0,0)
db 0 ; (0,1)
db 1 ; (0,2)
db 0 ; (1,0)
db 1 ; (1,1)
db 1 ; (1,2)
db 1 ; (2,0)
db 0 ; (2,1)
db 1 ; (2,2)
Kövessük
végig a mátrixot elemenként, sorfolytonosan! A (2,1) koordonáta, ami gépi
indexelés szerint (1,0) a 3-as relatív címen van, mert index=( Y * elemek_száma
+ X), vagyis ((1*3=3)+0)=3. A 3. indexű elem értéke 0, ez valóban a helyes
eredmény.
Miért van
szükség konstansokra? Mint említettem, egy konstans literál olyan érték, melyet
elnevezve is el tudunk érni, vagyis azonosítója van. Ilyen volt az ESO, NAP,
SZEL, es persze a NAPSZEMUVEG is. Hogyan definiáljunk konstans literálokat? Az
"equ" (equal, vagyis "egyenlő") kulcsszóval...
ESO equ 1
HO equ 2
SZEL equ 3
NAP equ 4
UDVOZLES equ "Sziamia!"
Ezek után
ha valahova beírjuk, hogy SZEL, akkor a fordító behelyettesíti oda a 3 számot.
Ha valahova beírjuk, hogy UDVOZLES, oda a fordító beírja, hogy
"Sziamia!". Természetesen lehetne a konstansok azonosítóját kisbetűvel
is írni, ez a nagybetűsség programozási stílus kérdése, ajánlott ezt így
végezni, jobban olvasható a program, elsőre látszik, hogy egy nagybetűs
azonosító mögött egy konstans rejtőzik.
Mi a
makró? A makró egy elnevezett utasításblokk (vagyis programrészlet),
azonosítóval hivatkozhatunk rá, ekkor a mögötte álló forráskódot a fordító
behelyettesíti a hivatkozás helyére. A makró tehát arra jó, hogy hosszabb
utasítássorozatokat ne kelljen sokszor újra és újra leírni. A makrók neve
ugyanúgy, mint az utasításoké és függvényeké, kisbetűvel írandó - ez persze
csak stílusossági szempontjából érdekes.
Esetenként
a makrókat egymásba lehet ágyazni, de ez fordítóprogram kérdése (vagyis akkor
lehet, ha a fordító megengedi).
Nézzünk
egy példát makróra:
kilepes MACRO
mov ax, 4C00h
int 21h
ENDM
Elfajult
programozók képesek tömbelemeket is makrókba zúzni, ez az
igazi-programozó-utánzás azonban nem vezet mindig jóra, mert nehezítheti a
program megértését (mikor két év múlva elő kell venni valamit és akkor előjön,
hogy mit-hol-miért-mivan?).
matrix_2x2 MACRO
db 0
db 0
db 0
db 0
ENDM
A
paraméterek egy adott környezetet határoznak meg. Alkalmazásukra függvények
hívásánál leht szükség. A paraméterezés funkciója, hogy a függvényeknek
adatokat adjunk át. Két féle függvény létezik: a típusos és a típus nélküli
(void) függvény. Típusos függvény meghívásakor egy bizonyos típusú értéket ad
vissza, a típus nélküli függvény nem ad vissza értéket. Ez utóbbit bizonyos
nyelvekben eljárásnak is szokták nevezni. Itt, assembly nyelven nincs értelme
eljárásokról beszélni, egy függvényt általában azért készítenek, hogy
visszaadjon egy értéket.
Lehet szó
például olyan függvényről, ami nem kér bemenő adatot, csak szolgáltat. Ilyen
például egy getc() függvény, ez a billentyűzetről olvas egy karaktert. De lehet
írni olyan függvényt is - például a szorzas() -, aminek megadunk két értéket és
a függvény ezek szorzatával tér vissza. Ezek a megadott értékek a függvény
paraméterei. Amennyiben a függvény paraméterei közvetlen értékek, érték
szerinti átadásról beszélünk. Abban az esetben, ha a függvénynek változók
címeit adjuk át, cím szerinti átadásról beszélünk. Nézzünk erre egy-egy példát.
Az első
példában két 8 bites számot adunk össze, a 8 bites értékeket karakteres, vagy
bájt típusú értéknek hívjuk. A függvény neve legyen szorzas(), és az szorzatot
egy 16 bites (short integer) értékben adja vissza.
; szorzas(byte al, byte ah) : word(*AX*) {
szorzas:
push bx ; elmentjuk BX eredeti erteket
mov bl ah
mul bl
mov ax, bx ; AX-szel terunk vissza
pop bx ; visszatoltjuk BX eredeti erteket
; }
Azt az
esetet, amikor az értékek regisztereken keresztül vándorolnak, gyors hívásnak
(fast call) nevezik.
Paramétereket
általában a vermen keresztül szoktak átadni. Intel processzoron két stílus
terjedt el, a Pascal és a C stílus szerinti paraméterezés. Pascal esetén ha két
paraméter van, akkor a verem tetején az utolsó paraméter értéke van, az alján
pedig az elsőé. C stílus esetén ez fordított. Nekem mindegy, ki mit szokott
meg, a nagy többség a Pascal stílust használja. Ez úgy vélem, feladatfüggő. Ha
az utolsó paraméter egy ritka esetben használatos érték, szinte opcionális
paraméter, mint például van egy szövegkiíró függvényünk, aminek utolsó
paramétere lehet egy logikai változó, ami ha igazra van állítva (ezt úgy is
mondhatjuk, hogy "be van billentve"), akkor a függvény kiír egy
vigyort is ) a kiírandó szöveg végére. Nos, úgy vélem, ez a plusztevékenység a
szövegkiírás, mint algoritmus szempontjából a leglényegtelenebb, és szinte oda
nem illő komponens. Mégis Pascal stílusú átadásnál ezt a logikai változót kapja
meg először a rutin ahelyett, hogy ezt kapná meg utoljára, mint C stílusban.
Akkor
lássuk, hogyan is néz ki az első feladat C stílusban átadott paraméterekkel:
mov al 4 ; negy
mov ah 9 ; szorozva kilenccel
push ax
call szorzas
pop ax ; ezutan AX-ben lesz az eredmeny
S íme, a
függvény:
; szorzas(byte, byte) : word {
szorzas:
mov bp, sp
mov al, ss:[bp + 2] ; szorzando
mov bl, ss:[bp + 3] ; szorzo
mul bl
mov ss:[bp + 2], ax ; ax erteke vissza a
stackbe
ret
; }
Ez azért
hagy némi kívánnivalót - de legalábbis magyaráznivalót - maga után. Mielőtt a
függvényt meghívtuk, két 8 bites értéket elhelyeztünk a verembe. Amikor a
függvényt meghívtuk, a visszatérési címet is letolta a "call"
utasítás. Ez egy 16 bites cím, mert a processzor valós módjában dolgozunk.
Vagyis ezt a 16 bitet át kell lépnünk, hogy a paraméterekhez hozzáférjünk. 2
bájttal lemászva a verembe, megtaláljuk az egyik 8 bites paramétert, ez lesz a
szorzandó; még egy bájttal, vagyis 3 bájttal lejjebb pedig a másikat, a szorzót
találjuk. Elvégezzük a szorzást, az eredmény "ax" regiszterbe kerül.
Ezt visszaírjuk a verembe, a 2. bájttól, hiszen ott van szabad hely. Ezt az
értéket egy külső, hívó program, mely ezen függvényt hívja, a "pop"
utasítással kiveheti a veremből. Miért is? A "ret" hatására a verem
tetején lévő visszatérési cím beíródott IP-be, vagyis a vezérlés visszaugrott a
hívó félhez, majd pedig minden feljebbugrott a veremben 16 bittel, mert egy 16
bites IP regiszter érték volt a verem tetején. Ezáltal a verem tetejére új
érték, mégpedig a szorzat került. Ezt lehet onnan lehalászni a "pop"
utasítással.
Egy fontos
dolog: SP-vel közvetlenül nem lehet indexelni, mert Intel bácsi így találta ki.
Az szerinte nem arra való... Jó, hát ha nem, hát nem. De az Assembly
azért van, hogy átverjük a processzor agyát: BP-vel lehet indexelni, akkor
mázoljuk be BP-be SP értékét, aztán adjunk neki... Gonosz, mi?
A
kifejezés operátorok, változók, elnevezett és nevezetlen literálok olyan
kombinációja, mely egy értéket ad eredményül. Ilyen kifejezés például a (4 +
3). Értéke, mint tudjuk, 7. Ilyen kifejezés a (2 > 4) is, ennek értéke
HAMIS. Ez utóbbi egy relációs, logikai értéket adó kifejezés. A
("cica"+' '+"mica") értéke "cica mica": ez egy
szöveges kifejezés. Gyűjtsük össze, milyen kifejezések lehetnek.
aritmetikai kifejezés
3 + 4 értéke 7
(3 * 4) - 27 értéke -15
a=2; b=4; c=5; értékadások
((a + b) / 2) + c értéke 8
i << 3 balra léptetés hárommal,
vagyis szorzás 2^3-nal
relációs kifejezés
a < 2 igaz, ha a értéke kisebb, mint 2
q == 3 igaz, ha q értéke egyenlő 3-mal
logikai kifejezés
b & c igaz, ha b értéke ÉS c
értéke is igaz
(a | b) & (c | d) igaz, ha a VAGY b
igaz ÉS c VAGY d is igaz
szöveges kifejezés
'a' + ' ' + 'b' "a b"
"ma" + "ci" "maci"
Az
operátor egy műveletvégző nyelvi egység. Az egyoperandusú operátorral egy
értéken végzünk műveletet, ilyen a -- és ++ operátor, mert ezek egy értéket
rendre csökkentenek, illetve növelnek eggyel. A kétoperanusú operátorok két
értéken dolgoznak, s egy harmadik értéket képeznek belőlük. Ilyen például a +
operátor, mert például a (3 + 4) esetében két értékkel, a 3-mal és a 4-gyel
operálnak, ezeket összeadják, s harmadik értékként az összeget szolgáltatják,
7-et.
A következő
táblázat az operátorokat tartalmazza és példákat ad rájuk. Az első oszlop az
operátor jele (C szintaktikában, amit ugyan nem hátrányos megtanulni annak
ellenére sem, hogy az Assemblerek nem ismerik), a második az operátor
jelentése, a harmadik az Assembly mnemonic (vagyis az operátor kulcsszava
Assembly nyelven). A negyedik oszlop néhány példát tartalmaz.
Operátor |
Jelentés |
Mnemonic |
Példák |
= |
legyen egyenlő |
Mov |
Mov al,3 Mov
eax,ebx |
+ |
Összeadás |
Add |
Add al,5 |
- |
Kivonás |
Sub |
Sub al,7 |
++ |
Eggyel
való növelés |
Inc |
Inc si |
-- |
Eggyel
való csökkentés |
Dec |
Dec bp |
* |
Egész
szorzás |
Mul |
Mul ebx |
/ |
Egész
osztás |
Div |
Div ebx |
>> |
Jobbra
tolás |
Shr |
Shr
edx,1 |
<< |
Balra
tolás |
Shl |
Shl
edx,1 |
(nincs) |
Jobbra
forgatás |
Ror |
Ror
eax,1 |
(nincs) |
Balra
forgatás |
Rol |
Rol
eax,1 |
(nincs) |
Jobbra
forgatás carry-vel |
Rcr |
Rcr
edx,1 |
(nincs) |
Balra
forgatás carry-vel |
Rcl |
Rcl
eax,1 |
Mi az a
shiftelés, vagyis tolás? Végezzünk el példaként néhány műveletet az alábbi 8
bites számon: 01011011b (a 'b' utótag a szám bináris alakjára utal). Toljuk
jobbra, az eredmény 00101101b lesz, és a CARRY (átvitel) bit értéke 1, mivel ez
a kicsúszó bit értéke. Balra tolva az eredmény 10110110b és a CF (Carry Flag)
most 0. Jobbra forgatva 10101101b lesz, mert a jobb oldalt kilépő bit nem csak
CF-be kerül, hanem visszaíródik bal oldalra, a szám legfelső bitjeként. Balra
forgatásnál tehát 10110110b lesz az érték és CF 0 lesz.
A forgatás
másik fajtája, a Carry-n kereszüli (through carry) forgatás annyit jelent, hogy
először beállítjuk a carry értékét pl 1-re, majd ha elvégzünk egy carry-s
jobbra forgatást (RCR), akkor 01011011b-ből 10101101b lesz és carry értéke is
most 1 lesz, mert ennyi csúszott ki. Vagyis olyan, mintha 9 bittel végeznénk műveletet.
Operátor |
Jelentés |
Mnemonic |
Példák |
& |
ÉS |
And |
And al,
1010b |
| |
VAGY |
Or |
Or bl,
1010b |
^ |
Kizáró Vagy |
And |
Xor dl,
cl |
! |
Tagadás |
Not |
Not eax |
- |
Előjelváltás |
Neg |
Neg eax |
Csak a NEG
műveletre térnék ki, a többi úgy érzem, magától értetődő. A "neg" művelet
egyoperandusú, és a megadott regiszter értékét negálja, vagyis mínusz eggyel
szorozza: a kettes komplemensét képezi.
A
következő részben már konkrét, egy az egyben fordítható és futtatható
programokkal (ujjgyakorlatok és barátai) szeretném a tisztelt olvasóközönséget
boldogítani ...
Németh Róbert -
nrobcsi@freemail.hu