ÚÄÄÄ ÚÄÄÄ
ÚÄ ÚÄ
ÚÄ ÚÄ ÚÄ
ÚÄ ÚÄÚÄ ÚÄÚÄ
ÚÄ ÚÄ ÚÄ
ÚÄ ÚÄÄ
ÚÄ
ÚÄÄ ÚÄÄ ÚÄÄ
ÚÄ Ú ÚÄ ASSEMBLY
RULEZ
ÚÄÄÄÄÄÄÄÄÄ ÚÄ
ÚÄ ÚÄ
ÚÄÄ ÚÄÄ ÚÄ
ÚÄ ÚÄ ÚÄ IV.rész
ÚÄÄ ÚÄÄ ÚÄÄÄÄ
ÚÄ ÚÄ
ASSEMBLY FORRÁSKÓD
(ahogy a TASM kéri) |
Ebben a részben
megtesszük az elsõ gyakorlati lépést elsõ assembly
programunk megírása felé. Elõször meg kell
ismerni A TASM által kért fileformátumot. De mi is az a
TASM? Igazi neve Turbo Assembler és a Borland cég terméke.
Azért ezt használjuk, mert én is ezzel dolgozok. Persze
az itt közölt forráskódok lefordíthatóak
MASM-mal is, mert egyelõre a TASM MASM módját használjuk.
Remélem minden tiszta.
A fordításhoz kell még a
TLINK nevû program, de ha megvan a TASM, akkor valószínûleg
meg van ez is. ha egyik sincs meg, akkor letöltheted a http://members.tippnet.co.yu/~formater/ -rõl.
Mivel én általában (mindig) COM filet írok ezért ezen jó szokásomtól most sem térek el. Így a forráskódok eleje mindig ugyanolyan lesz:
code segment para public
'code'
assume cs:code;ds:code;es:code;ss:code
org 100h
start:
Na itt mi mit jelent?
Elõször is meg kell nyitnunk egy logikai szegmenst a fordító számára. Erre azért van szükség, hogy a TASM tudja, melyik szegmenshez kell viszonyítania az általunk definiált változók és egyebek offszetértékét. Ennek COM filenál nem sok értelme van, hiszen a COM file egy szegmensbe belefér. Mivel a program legelején nyitottam meg a logikai szegmenst, ezért a fordító által lefordított program offszetjei helyesen a program elejéhez viszonyítottak lesznek. Egy kicsit késõbb megérted ha még nem értenéd (amin nem csodálkozom). Ezt a logikai szegmenst persze egyszer majd le is kell zárni. Ezt majd a program végén teszem meg, de ha majd nagyobb, több ilyen szegmenst igénylõ programot írsz (mikor lesz még az), akkor egy-egy szegmenst bármikor lezárhatsz majd akár újra is nyithatod.
Az ASSUME utasítás megmondja az assemblernek, hogy melyik szegmens-regiszterhez melyik logikai szegmenst rendelje. Ez teszi lehetõvé, hogy az offset érték a szegmens regiszter tényleges tartalmához igazodjon. Ezért mikor a programban definiált változóra (valójában memóriarész) hivatkozunk akkor a megfelelõ szegmens regiszter tartalmának (általában DS vagy ES) összhangban kell lennie az ASSUMEmal hozzárendelt értékekkel.
Az ORG direktíva (utasítás) az utána megadott értékre állítja a fordító logikai utasításmutatóját. Ez azt eredményezi, hogy az ORG utáni offszet értékek betöltésekor ennyivel nagyobb érték töltõdik be. COM filenál ez azért szükséges, mert a DOS nem a szegmens elejére, hanem 100h bytetal messzebbre tölti be.
A start címke pedig a program kezdetét jelzi. Ez egyébként nem csak start lehet, mert majd a program végén dõl el a valódi kezdet. Ez csak EXE proggykhoz lesz fontos, de itt is kell.
Nem csak a forráskód elejének van kötött formája, hanem a végének is. Ide mindig kell tenni egy end utasítást, ami után a program kezdetét jelölõ címkét kell leírni. Ez az általam ide írt programokban mindig end start. A példaprogramjaimban (amik mindig COM fileba fordíthatók) az end start elõtt szerepelni fog még egy code ends is, ami lezárja a code logikai szegmenst. amíg nem vagy elég tapasztalt javaslom használd ezt a módszert. A program vége tehát így fog kinézni:
code ends
end start
Na akkor most egy-két példa ami majd kitisztítja az agyakat.
code segment para public
'code'
assume cs:code;ds:code;es:code;ss:code
org 100h
pelda1 db 77h
mov ah,[pelda1]
Tegyük fel, hogy a program futása a MOV utasítással kezdõdik (valójában nem, de ez most nem számít, ez csak példa). Itt azt akarom, hogy az ah regiszter tartalma a pelda1 memóriarész tartalma legyen. A [pelda1] helyére nem 77h fog kerülni, hanem a pelda1 offsetje, azaz 100h (eltte az org 100h-val tolta el az offseteket). Most variáljuk egy kicsit a dolgot.
code segment para public
'code'
assume cs:code;ds:code;es:code;ss:code
org 100h
pelda1 db 77h
code1 segment para public
'code'
assume ds:code1
mov ah,[pelda1]
Ez most nem fog "lefordulni", mert a TASM a DS regiszter tartalmáról azt hiszi, hogy az a code1 szegmenscíme, pedig valójában a code-é. A pelda1 pedig a code szegmensben van definiálva, így nincs olyan offset, ami a code1 szegmensbõl a pelda1-re mutathatna. És végül még egy kis fejtágítás.
code segment para public
'code'
assume cs:code;ds:code;es:code;ss:code
org 100h
pelda1 db 77h
code1 segment para public
'code'
assume ds:code1
pelda2 db 95h
mov ah,[pelda2]
mov al,es:[pelda1]
Ez már lefordítható, hiszen a pelda2 a code1 szegmensben van deklarálva. Itt a [pelda2] helyére 00h fordítódik be, mivel a pelda2 a code1 szegmens legelején, azaz 00h offszeten van. A második mov is lefordítható, mert az alapértelmezett DS helyett az ES-hez kérem a viszonyítást és a TASM úgy tudja, hogy az ES regiszterben a code szegmenscíme van, a pelda1 pediglen ebben van deklarálva. Természetesen az elsõ MOV (mov ah,[pelda1]) futás közben nem a pelda2 értékét (95h) fogja az AH-ba tölteni, mert a DS regisztert nem írtuk át és COM program indításakor az mindig a program elejére mutat, valójában a code szegmens legelsõ byteját tölti majd be. Mi csak a TASMmal hitettük el hogy átírtuk DS-t. Ezzel a módszerrel lehet az csináltatni az assemblerrel, amit akarunk, de az nem engedné meg normál körülmények között.
Jó ötlet, ha a forráskód kezdetébõl és végébõl csinálsz egy filet és ezt elnevezed mondjuk alap.asm-nak. Ezután ezt a filet egyszerûen átmásolod más névre és ebbe írod a programot. Egy kis idõmegtakarítás.
Akinek nem volt érthetõ az ne szégyellje, hiszen nem szégyen (legfeljebb nekem), hanem írjon nekem vagy az újságnak és a következõ számban válaszolok. Játsszunk kérdezz-feleleket!
DEFINE BYTE/WORD/DOUBLEWORD
Szerepelt az elõbbi példákban egy bizonyos db. Ez nem darabot jelent, hanem ezt: Define Byte. Ez az utasításmutató (a TASM-é) által mutatott címre elhelyez egy byteot, amire majd a db elõtti címkével hivatkozhatunk. Mint az elõzõ példában is látszott, ilyen esetben mikor a címkére hivatkozunk, akkor az egy memóriahivatkozásként fog lefordulni, tehát futás közben az adott memóriacím tartalma töltõdik be.
Nem csak byteot lehet így deklarálni hanem szót (word), duplaszót (doubleword) és még másokat is.
Sõt, ha egy hosszabb szöveget akarsz tárolni, mondjuk kiíráshoz, azt is DB-vel oldhatod meg:
szoveg db 'Ide a szöveg karaktereinek kódjai fordítódnak be'
Szó memóriarész hivatkozásához a DW szükséges, míg a duplaszóhoz DD.
Példul:
ez_egy_byte
db 23h
ez_meg_szo dw
1234h
ez_pedig_duplaszo
dd 12345678h
A DB/DW/DD utáni szám a memóriacím tartalmát adja meg futáskor, ez benne magában a lefordított programban. FIGYELEM!! Ez nem dinamikus változó (aki magas szintû nyelven programozik az tudja mi ez), ez nem indításkor deklarálódik, hanem benne van a programban! Az utasítás a TASM-nak szól.
Egy ilyen "változóra"
(nevezzük változónak az egyszerûség kedvéért,
valójában ez csak memóriacím és nincs meghatározott
típusa, mint pl.: Pascalban. Még a Byte vagy Word esetleg DoubleWord
sajátossága is csak a fordító számára
létezik. Errõl mindjárt szó lesz még) a szabványos
hivatkozási mód a következõ: [címke]. Ez az
címke által címzett memória-címre való
memóriahivatkozás. A fordító elfogadja ha csak simán
cimke-t írunk, de jobb ha megszokod a [ ]-t mert van ahol mindenképpen
kell. Ha mindenhova kiteszed ahova szabály szerint kell, akkor biztos
nem lesz baj.
Akkor most nézzük a méret (byte,...) sajátosságait, ami mint mondtam csak a TASM számára kell. Most a példákban nem írom ki a forráskód elejét, hiszen ezek még nem futtathatók.
sz dw 1234h |
; egy szó részt deklarálunk melynek alapértéke 1234h |
mov ah,[szo] |
; ez nem jó mert a TASM szerint a szo változó két byte hosszú (és igaza is van), az AH pedig csak egy, ezért a szo nem tölthetõ AH-ba. Igenám de mi van ha nekem a szo változó elsõ byteja kell. erre két megoldás is kínálkozik. Az elsõ a következõ: |
mov ah, byte ptr [szo] |
Ez azt mondja a TASMnak (a byte ptr mondja), hogy ok, tudom hogy a szo a szónak van deklarálva, de én most mégis mint byte akarok rá hivatkozni, tehát csak az elsõ byteját akarom elérni. A TASM tudomásul veszi és úgy fordítja le mintha a szo bytenak lenne deklarálva. A második megoldás nekem kevésbé tetszik: |
ez_byte label byte |
|
mov ah,[szo] |
; az elõbbiek alapján ez rossz |
mov ah,byte ptr [szo] |
; ez már jó |
mov ah, [ez_byte] |
; ez is jó mert az ez_byte címke bytenak van deklarálva és a szo-val egyezõ címen van |
Ezt a ptr-es
játékot el lehet játszani minden fajtával (byte,
szó, duplaszó) oda-vissza. A példákban mindig ptr-t
fogok használni, mert így nem kell mindig új címkéket
keresgélni, hanem tudjuk, hogy most másként hivatkozom
a címkére.
Pár példa kis kommenttel:
szavacska dw
1243h
duplacska dd 23456785h
byteocska db 23h
ez_is_byte db 34h
mov ah,byte ptr [szavacska] ; AH=szavacska
elsõ byteja
mov al,byte ptr [duplacska] ; na ez
mi lehet?
mov ah,[byteocska] ;
egyértelmû, nem?
mov bx,word ptr [duplacska] ; BX=duplacska
elsõ szava
mov cx,word ptr [byteocska] ; Ez CX-be
a byteocska és az ez_is_byte
által
kiadott szót tölti be
mov bx,[szavacska] ;
nem sztrapacska
Duplaszót nem lehet 8086 regiszterbe tölteni (majd a 80386-on :-).
Ha már a szavaknál járunk, akkor elmondom hogy is tárolja a proci ezeket. Nem úgy ahogy gondolnád, hanem fordítva. Vagyis a 1234h számot nem úgy tárolja, hogy 12h 34h, hanem 34h 12h. Mindig a legkisebb helyiérték– byte tárolódik elõször. Utána a következõ és így tovább. Például a 12345678h duplaszó memóriabeli formája a következõ: 78h 56h 34h 12h. Ezt a tárolási módot little-endiannak is becézik, nem t'om miért. Az összes INTEL proci little endian. A másik mód, amikor a leírás sorrendjében tárolják, a big-endian. Példul a MOTOROLA procik ilyenek. Egyébként szavaknál a kisebb helyiértékû byteot LSB-nek (Least Significant Byte) a nagyobbat pedig MSB-nek (Most Significant Byte) is nevezik. Néha én is így fogom nevezni õket, nem árt szokni az angol szaknyelvet.
LABEL
Akkor mi is az a label?
Egy címkét definiál az adott helyre az
adott típussal.
Használata: címke label típus
A címkének még definiálatlannak kell lenni ( ez egyértelmõ, nem?). A típus lehet BYTE, WORD, DWORD, QWORD,TWORD, NEAR, FAR. A QWORD 8 byteot jelent, a TBYTE pedig tizet, de ezeket sohasem használjuk majd itt.
A NEAR és a FAR a hivatkozás típusát adja meg. A NEAR közelre (egy szegmensen belülre) a FAR távolra (másik szegmensbe) mutat. Mi itt csak a NEAR-t használjuk, mire olyan nagy programot írsz aminek már FAR kell akkora már tudni fogod.
A label nem foglal le memóriát, csak az adott részre való utalást könnyíti meg. így akár utasítások bytejait is el lehet érni, átírás céljából. A jó program, ha kell átírja magát, még akkor is ha csak egy kis komplikációt okozna a nem átírás. Persze csak az írja át a programkódot, aki tudja mit csinál.
Egyszerûsíti a címke definiálást, hogy nem csak labellel hanem kettõs-ponttal zárva is címke alakul ki, melynek típusa NEAR (ez kell nekünk :). Ezt fõleg ugrási címeknél használjuk majd.
pl.:
ide_ugorj:
jmp ide_ugorj
JMP
utasítás (JuMP Unconditionaly):
Használat: JMP cimke
Feltétel nélkül elugrik a cimke által megjelölt helyre. A cimke itt nem csak a labellel (vagy kettõsponttal) meghatározott lehet, hanem egy változó címkéje is.
A következõ
módokon adható meg az ugrási cím:
Címke |
Példa |
short_label |
JMP nagyon_kozel |
near_label |
JMP kisse_tavolabb |
far_label |
JMP nagyon_messze |
memóriatartalom16 |
JMP [ugras_ahova_mondom] |
regiszter16 |
JMP DI |
memóriatartalom32 |
JMP [messzire_ugorj] |
A short_label
és a near_label pontosan ugyanaz a mi szempontunkból, csak az
assemblernek kell tudnia, hogy mit használjon. A short_labeles ugrás
akkor generálódik, mikor a cél 127 byteon belül van.
Ekkor nem offszetre mutat az JMP, hanem egy relatív értéket
tartalmaz, ami elõjeles, így lehet egy 127 byteos körön
belül ugrálni. Ez minden JMP-nál 1 byteot megspórol.
A near_labellé fordított JMP már egy offszet értéket
tartalmaz ami az aktuális kódszegmenshez viszonyítandó.
A far_label a FAR-ként deklarált címekre teszi lehetõvé
a vezérlésátadást. A memóriatartalom16 egy
16 bites (szó) memóriacím alapján indirekt ugrást
produkál. A proci kiolvassa a megadott szót a memóriából
és az általa leírt offszetre ugrik. A regiszter16 ugyanez
csak 16 bites regiszterrel végrehajtva. A memóriatartalom32 egy
duplaszó alapján távolra (FAR) ugrik. Mi csak a short és
near labelt és legfeljebb a 16 bites indirekt ugrásokat fogjuk
használni.
Példa:
ide_ugrik:
... ;
kevesebb mint
... ;
127 byte
jmp ide_ugrik ;
az ide_ugrik 127 bytenál közelebb van
;
ezért a fordító short JMP-t készít
... ;
ezek az utasítások már 127 bytenál messzebb
... ;
teszik az ide_ugrik címkét a JMP-hoz
jmp ide_ugrik ;
ide_ugrik 127 bytenál messzebb van
;
ezért near JMP fordítódik be
indirekt_ugras dw 2345h ; 2345h lesz
az offszet az ugráshoz
jmp [indirekt_ugras] ;
A 2345h offszet– helyen folytatja programot
mov bx,[indirekt_ugras]
jmp bx ;
Ugyanaz, mint az elõbb
Az indirekt ugrással vigyázni kell, mert ha az indirekt cím nem egy jó utasításra mutat, hanem mondjuk adatterületre, akkor a gép könnyen elszállhat.
És most az elsõ assembly program megírása. Nagy levegõt és RAJT!
Lucifer of ZeroBit