ÚÄÄÄ        ÚÄÄÄ     ÚÄ       ÚÄ
  ÚÄ  ÚÄ      ÚÄ   ÚÄ   ÚÄÚÄ   ÚÄÚÄ
 ÚÄ    ÚÄ     ÚÄ        ÚÄ  ÚÄÄ  ÚÄ
ÚÄÄ    ÚÄÄ      ÚÄÄ     ÚÄ   Ú   ÚÄ    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
szo dw 1234h

 

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.

   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