Grundlæggende assembler-programmering

Lars K. Schunk
schunk@gmail.com

2001

Åben dokumentlicens

Dette dokument er frigivet under Åben dokumentlicens (ÅDL). Det betyder i bund og grund at du frit må videregive og/eller ændre dokumentet eller dele af det under forudsætning af at ÅDL overholdes. Læs ÅDL for detaljerne.1

1  Indledning

Det at programmere i assembler er noget af det tætteste du kan komme på din computers CPU. Assembler-koder er næsten en direkte oversættelse af CPU'ens instruktioner som består af 0'er og 1-taller. Derfor er det muligt at lave meget små og meget hurtige programmer i assembler. Til gengæld kræver det også mere af programmøren.

Nu om dage er hastighed og størrelse ikke en særlig væsentlig fordel ved assembler. Nutidens computere er hurtige og har masser af hukommelse. Derfor fører assembler måske nok lidt af en skyggetilværelse, for man kan lave komplicerede programmer, meget hurtigere og lettere, i f.eks. C++, og har du tænkt dig at lave Windows-programmer, så vil det tage meget lang tid at lave noget fornuftigt i assembler som du ikke kunne lave på en enkelt dag i Visual Basic eller Delphi.

Men hvorfor så lære assembler? Svaret er simpelt: for at lære lidt mere om hvordan din computer fungerer---inde bag det hele. En Windows-haj ved måske nok meget om computere---og hans eller hendes viden kan også bruges til noget. Men ved du hvordan assembler fungerer, så ved du også en del mere om hvordan din CPU fungerer. Og du vil endda forstå en lille smule mere når du klikker på knappen Detaljer efter en ulovlig handling i Windows. Hvad man så end vil bruge det til ...

Inden vi går i gang vil jeg lige understrege at den type assembler som du lærer her, ikke er alt det nyeste. Det jeg beskriver her, er assembler som det fungerer på en 80386'er, men eftersom alle nye computere er bagudkompatible, så fungerer det også på f.eks. en Pentium. Når du først lige har fået fat i idéen, så er grundlæggende assembler faktisk rimelig simpelt, men skal du videre, og lære Pentium-specifikke instruktioner, så bliver det straks sværere og mere uoverskueligt, for man erstatter ikke den gamle teknologi---man bygger ovenpå, og det medfører både fordele (bagudkompatibilitet) og ulemper (uoverskuelighed). Desuden får du nok ikke brug for de teknologiske nyskabelser når det gælder CPU'ens registre og instruktionssæt lige med det samme.

Hvis du efter at have læst denne artikel, har lyst til at læse en mere detaljeret gennemgang, så kan jeg varmt anbefale den fremragende, underholdende og meget begyndervenlige bog Assembly Language Step-by-Step af Jeff Duntemann2. Mere eller mindre alt hvad jeg selv ved om assembler-programmering har jeg fra denne bog. Hvad kan min artikel så tilbyde? Først og fremmest kan den give et hurtigt og relativt kort overblik over emnet. Herudover afslutter jeg artiklen med et større eksempel, nemlig et fire på stribe-spil, som jeg synes er noget mere interessant end de små eksempler der findes i førnævnte bog. Men alting til sin tid. Lad os starte ved begyndelsen.

2  De nødvendige værktøjer

Inden man overhovedet kan komme i gang med at programmere i assembler, skal man have de rigtige værktøjer. Heldigvis findes der gratis programmer som du kan bruge. Det vigtigste program er assembleren NASM3. Det er det program som skal oversætte din assembler-kode (.asm-filer) til såkaldt object code (.obj-filer).

Når NASM har lavet en .obj-fil, skal programmet ALink4 lave en .exe-fil ud af .obj-filen. ALink er det man kalder en linker. Ved hjælp af ALink kan du sammenkæde flere .obj-filer til én .exe-fil. Du kan f.eks. lave en .asm-fil med en masse rutiner som du skal bruge i dine programmer. Den laves til en .obj-fil med NASM. Så kan du lave selve programmet i en ny .asm-fil som du også laver til en .obj-fil, og sammenkæder dem så til sidst med ALink. Bemærk at selv om du kun har én .asm-fil, skal du stadig bruger linkeren til at lave din .obj-fil om til en .exe-fil.

Et tredje program er ikke strengt nødvendigt, men det er meget smart, i hvert fald mens man er i gang med at lære assembler. Det er programmet NASM-IDE5. Det kan du bruge til at skrive dine assembler-programmer i. NASM-IDE fremhæver de forskellige instruktioner så du får et godt overblik over dit program. Hvis du ikke vil bruge NASM-IDE, så kan du bare bruge DOS Edit eller Windows Notepad. NASM, ALink og NASM-IDE er alle gratis at bruge.

Jeg anbefaler at du laver et bibliotek som hedder C:\Asm. Så kan du lægge NASM i C:\Asm\Nasm, og ALink i C:\Asm\ALink osv. Du bør også kalde et bibliotek C:\Asm\Projekt eller lignende som du kan bruge til at gemme dine assembler-programmer i.

Det er en god idé at lægge de to biblioteker C:\Asm\Nasm og C:\Asm\ALink ind i din sti (path). Hvis du har lagt NASM-IDE ind, er det også en god idé at lave en nasmide.bat-fil der ser således ud:
@echo off
cd c:\asm\projekt
c:\asm\nasmide\nasmide.exe
Gem filen i et bibliotek som ligger i din sti. Så kan du starte NASM-IDE hvor som helst ved at skrive nasmide, og så kommer du automatisk ind i C:\Asm\Projekt hvor du bør gemme dine assembler-filer.

Til sidst skal du sørge for at NASM-IDE kan finde NASM. Det gør du ved at starte NASM-IDE op og klikke på Assembler i menuen Options. I det vindue som kommer frem, skal du i feltet NASM Location, skrive sti og filnavn på NASM. Hvis du har lagt programmerne hvor jeg anbefalede dig at lægge dem, skal der stå C:\Asm\Nasm\nasm.exe.

3  Computerens hukommelse (RAM)

Når man skal programmere i assembler, er det vigtigt at vide noget om hvordan computerens hukommelse er organiseret og hvordan den fungerer. Først og fremmest skal du vide hvad man kalder de forskellige mængder af informationer som kan lagres i hukommelsen. Der er mange, men de vigtigste er bit, nibble, byte, word, double word, kilobyte og megabyte.
Bit
En bit er det mindste stykke information som computeren kan arbejde med. En bit er enten 0 eller 1.

Nibble
En nibble er en halv byte eller 4 bit og en nibble kan derfor have en værdi mellem 0 og 15. I hexadecimal mellem 0 og F. En nibble kan altså altid betegnes med et enkelt hexadecimalt ciffer.

Byte
En byte er ``måleenheden'' i computerens hukommelse ligesom meter er det i metersystemet. En byte består af 8 bit (2 nibbles) og kan derfor have en værdi mellem 0 og 255 (00 og FF i hex). Så en byte kan altså altid beskrives med et to-cifret hexadecimalt tal.

Word
Et word består af 2 byte efter hinanden. Den første byte kaldes high byte, og den sidste byte kaldes low byte. Et word kan have en værdi mellem 0 og 65535 (hex: 0000-FFFF). Det binære talsystem og det hexadecimale talsystem passer så godt sammen at man kan tage hver byte for sig, f.eks. FF og AA (dec: 255 og 170) og bare sætte dem sammen til FFAA som i decimal er 65450, og i det binære talsystem er det 1111111110101010. Hvis du deler dette binære tal op i to stykker med 8 bit (en byte) i hver, bliver det til 11111111 og 10101010. Hver for sig kan de oversættes til hex, og vi kommer tilbage til FF og AA. Det er altså meget let at omregne fra hex til binær (og omvendt) og det er hovedårsagen til at hex bruges så meget i computerverdenen.

Double word
Et double word (dword) består af to words efter hinanden og har en værdi mellem 0 og 4.294.967.295 (hex: 0-FFFFFFFF).
Bit, byte og word er dem du kommer til at bruge mest. Der er mange flere, men de bruges sjældent. Det næste du skal vide er hvordan man adresserer computerens hukommelse. Det gøres ved hjælp af segmenter og offsets. Når du laver almindelige DOS-programmer, har du den første megabyte hukommelse til rådighed. Det er 1.048.576 byte som så har adresserne 0 til FFFFF. Hver byte har en adresse, så forestil dig hukommelsen som en meget lang gade hvor hver byte bor i et hus som har et nummer. Det højeste nummer er her FFFFF. Bemærk at det består af 5 F'er og derfor af 20 bit. Du er altså nødt til at bruge 2 words for at få nummeret på en adresse (selv om 2 words i alt består af 32 bit). Det ene word indeholder segmentet, mens det andet indeholder offset-værdien.

Hver 16'ne byte i hukommelsen er begyndelsen på et segment, dvs. segment 0000 starter på adresse 00000. Segment 0001 starter på adresse 00010 (dec 16), segment 0002 starter i adresse 00020 (dec 32), osv. Det sidste segment FFFF starter på adresse FFFF0 (dec 1.048.560). Når du har et segment, skal du have en offset-værdi. Offset-værdien er også et word, dvs. et tal mellem 0000 of FFFF. Offset-værdien viser hvor langt fra segment-adressen, din ønskede adresse ligger. I små programmer er det meget almindeligt at man angiver segmentet én gang for alle, og så flytter rundt med offset-værdien. Det giver 64 kilobyte (0-65535; 0-FFFF) at bevæge sig rundt på hvilket sikkert er fuldt tilstrækkeligt for dine første mange assembler-programmer. Du angiver en adresse på denne måde:
segment:offset
For eksempel C800:0AC3. Her er du i segment C800, og offset-værdien er 0AC3. Hvis du vil omregne dette til den egentlige adresse kan du starte med at omregne C800 til decimal. Det er 51.200. Det ganger du med 16 og får 819.200 (husk på at der er 16 byte mellem hvert segments start-adresse). Til sidst omregner du offset-værdien 0AC3 til decimal og lægger den til. 0AC3 er 2755 i decimal og 819.200 + 2755 er lig med 821.955 hvilket er den endelige adresse. Den kan du omregne til hex og så får du C8AC3. Som du kan se er der en vis lighed mellem segment:offset-adressen og den egentlige adresse, men eftersom den egentlige adresse bruger 5 hexadecimale cifre, er du nødt til at bruge 2 words til at beskrive adressen.

Du kan betragte det hele som om computeren er lidt nærsynet, og derfor kun kan ``se'' 64 kilobyte frem for sig. Derfor placerer du den på et segment. Vær i øvrigt opmærksom på at eftersom der kun er 16 byte mellem de punkter hvor segmenter starter, og at man fra hvert segment kan se 64 kilobyte frem, så er der mange andre segment:offset-kombinationer med hvilke du kan finde frem til adressen C8AC3.

Hvis du har lyst til at eksperimentere lidt med hex, binær og decimal, så kan du bruge f.eks. Windows' lommeregner til at regne med disse talsystemer.

4  CPU'ens registre

Når du programmerer i assembler, har du CPU'ens interne registre til rådighed hvori du kan lægge resultater, mellemresultater og mange andre ting. Du har først og fremmest nogle segmentregistre. Disse registre er 16 bit store og kan (og bør kun) bruges til at gemme segment-adresser. De er som følger:
CS
Code Segment. Dit program består af nogle instruktioner som befinder sig et sted i computerens hukommelse. CS-registret indeholder adressen for det segment hvori dit program ligger.

DS
Data Segment. Dine data (variable o.lign.) ligger også et sted i hukommelsen. DS-registret indeholder adressen for det segment hvori dine data ligger.

SS
Stack Segment. Stakken er et område i hukommelsen hvor computeren og du kan gemme midlertidige data. Jeg vender tilbage til stakken i afsnit 7. Nu skal du bare vide at stakken har en segment-adresse som skal ligge i SS.

ES
Extra Segment. Dette er et ekstra segment som ikke skal bruges til noget specielt. Det kan du bare bruge hvis du får behov for at pege på et eller andet segment. Der er to registre mere af denne type som hedder FS og GS. Deres navne står ikke for noget specielt.
Husk på at hver gang du ser et register der ender på S, så er det et segmentregister.

De næste fire registre er registrene AX, BX, CX og DX. Disse registre er også 16 bit store, og de bruges til ting som mellemregninger og resultater. Du kan få adgang til hver af disse registres ``halvdel'' ved at bruge et H eller et L i stedet for X'et. H'et står for high byte og L'et står for Low byte. AX består altså af AH og AL, BX består af BH og BL, CX af CH og CL, og DX af DH og DL.

Hvis du lægger værdien A43C ind i register AX, så vil AH indeholde værdien A4, og AL vil indeholde værdien 3C. Hvis du så ændrer værdien af AL til f.eks. B3, så har AX nu værdien A4B3. En smart ting ved dette er at du kan ændre en værdi i AL uden at røre ved værdien i AH. Disse fire registre er de eneste registre som har den egenskab at de kan deles op i 8-bit-halvdele, og ud over denne egenskab har hvert af disse fire registre en speciel egenskab hvoraf BX's kommer i næste afsnit. Grunden til at man ikke bare har givet alle fire registre alle de specielle egenskaber er at CPU'en har nogle fysiske begrænsninger. Det er altså begrænset hvor mange transistorer der er i CPU'en, og derfor har man måttet gå på kompromis mange steder. Du vil støde på flere sådanne begrænsninger senere hen, for som nævnt i indledningen, så er det at programmere i assembler mere eller mindre så tæt på CPU'en som du kommer.

Der er også to indeksregistre SI og DI i CPU'en (du kan også kalde dem for offset-registre). Disse bruges til at gemme offset-værdier, og de skal bruges sammen med de førnævnte segmentregistre når du skal pege på et sted i hukommelsen. SI står for Source Index (kilde-indeks), og DI står for Destination Index. Hvis du får brug for et ekstra indeksregister, kan du også bruge BX som indeksregister (det er BX's specielle egenskab). Hvis du f.eks. vil pege på adressen C8AC3 som på segment:offset-formen kan skrives som C800:0AC3, så gemmer du C800 (segment-adressen) i et af segmentregistrene (som regel DS eller ES) og 0AC3 (offset-værdien) gemmer du i SI, DI eller BX. Hvis du har gemt segment og offset i DS og SI, kan du beskrive adressen ved at sige DS:SI. Hvis intet andet er angivet, så går man ud fra at du mener DS (Data Segment-registret). Dette vil give mere mening når du kommer i gang.

Der er tre registre mere som du nok ikke får så meget brug for. Det er BP, SP og IP som også indeholder offset-værdier. BP og SP står for henholdsvis Base Pointer og Stack Pointer. Disse bruges sammen med det midlertidige lager stakken (som jeg forklarer mere om i afsnit 7). Hvis intet andet er angivet, går man ud fra at det tilhørende segment-register er SS (Stack Pointer-registret). SP peger på det næste frie sted på stakken. Du gør klogt i ikke at røre SP. BP kan du bruge til at pege på andre steder i stakken. IP står for Instruction Pointer og bruges sammen med CS (Code Segment-registret) til at pege på den næste instruktion som skal udføres. CPU'en sørger selv for at ændre IP efterhånden som den udfører dine instruktioner. IP kan du slet ikke ændre direkte---og det er måske også kun godt nok ...

Flag-registret er det sidste register. Det er 16 bit stort og bliver kaldt FLAGS. Flag-registrets bit er de forskellige flag. Der er 9 flag som hver har en forkortelse på to bogstaver. Flagene er som følger: Overflow Flag (OF), Direction Flag (DF), Interrupt Enable Flag (IF), Trap Flag (TF), Sign Flag (SF), Zero Flag (ZF), Auxiliary Flag (AF), Parity Flag (PF) og Carry Flag (CF). Det er heldigvis ikke nødvendigt at huske alle flagene da du, som programmør, ikke har brug for særlig mange. De to flag som du vil bruge mest er Zero Flag (ZF) og Carry Flag (CF). Ét flag er repræsenteret af én bit i Flag-registret, og et flag kan således være enten 1 (flaget er hejst) eller 0 (flaget er ikke hejst).

Flagene bruges til at vise nogle forskellige tilstande i CPU'en. Dine programmer kan så teste flagene og reagere alt efter om et flag er set (hejst) eller cleared (ikke hejst). Zero Flag bliver som regel sat når en instruktion medfører at et eller andet giver nul. Hvis en operation medfører et nul, så bliver ZF's bit sat lig med 1, og hvis en operation ikke bliver nul, så bliver ZF's bit sat lig med 0. Det virker måske lidt underligt, men husk at 1 betyder ``vift med flaget''. Og hvis en operation bliver nul, så skal ZF vifte med sit flag (ZF=1). Det andet flag som du kan få brug for, er Carry Flag (CF). Carry betyder mente, og hvis en operation medfører at en bit kommer i mente, så bliver CF sat lig med 1. Flagene får du først brug for når du skal lave noget lidt mere avanceret.

5  Det første program

Nu skal vi til at programmere assembler! Du kan jo passende starte med at taste følgende ind i NASM-IDE eller Notepad eller hvad du nu bruger (du behøver ikke at taste de linjer ind som starter med et semikolon):
 
;------------------------------------------
;          Start på vores fil
;------------------------------------------

[BITS 16]

;------------------------------------------
;          Vores programkode
;------------------------------------------

SEGMENT kode

..start:

     mov        ax,data
     mov        ds,ax
     mov        ax,minStak
     mov        ss,ax
     mov        sp,stacktop

     mov        dx,besked
     mov        ah,9
     int        21h

     mov        ax,4C00h
     int        21h

;--------------------------------------------
;          Vores data
;--------------------------------------------

SEGMENT data

     besked     db    "Assembler-programmering!", 13, 10, "$"

;---------------------------------------------
;          Vores stak
;---------------------------------------------

SEGMENT minStak stack

     resb 64

stacktop:

;---------------------------------------------
;          Slut på vores fil
;---------------------------------------------
Programmet er skrevet i det der hedder Real Mode Segmented Model. Det er faktisk en forældet måde at skrive programmer på, men når jeg har valgt at beskrive denne model, er det fordi det en god idé at lære noget om CPU'en og computerens hukommelse. Senere kan du skrive programmer i Real Mode Flat Model som forenkler en hel del ting hvad angår segmenter og offsets.

Alle linjer der starter med et semikolon, er kommentarer til os selv, og ignoreres af assembleren.

Så den første egentlige linje [BITS 16] fortæller assembleren at den skal generere 16-bit-kode. Husk på at den fil du lige har skrevet er kildekoden. Kildekoden skal oversættes til maskinkode. Hvis du vil køre programmet, skal du først gemme filen som eks1.asm. Derefter oversætter du den med NASM:
     C:\ASM\PROJEKT>nasm eks1.asm -f obj -o eks1.obj
Så har du en fil som hedder eks1.obj. Den laver du til en .exe-fil med ALink:
     C:\ASM\PROJEKT>alink eks1.obj
Og så kører du blot den nye fil som hedder eks1.exe. Bemærk at .exe-filen ikke fylder mere end 139 byte!

Nå, videre med gennemgangen af vores kildekode. Resten af koden er delt op i tre dele: Code Segment, Data Segment og Stack Segment. Code-delen indeholder selve programmet. Data-delen indeholder de data vi skal bruge i vores program, og Stack-delen har med stakken at gøre.

Vi markerer vores Code Segment med direktivet SEGMENT kode. Herefter markerer vi hvor vores programkode starter med en etiket (eng. label). Den skal hedde ..start: (det skal skrives med små bogstaver, med to punktummer foran, og et kolon bagefter!) Det virker måske lidt dumt at man skal vise hvor programmet starter, men et program behøver ikke nødvendigvis at starte fra begyndelsen af filen. Så kommer programmets første fem instruktioner:
     mov        ax,data
     mov        ds,ax
     mov        ax,minStak
     mov        ss,ax
     mov        sp,stacktop
De bruges til at initialisere vores segmentregistre. Dette skal faktisk gøres i ethvert program. Som du kan se, bruges mov-instruktionen meget. Det er også den mest brugte instruktion i assembler. Det eneste mov gør, er at flytte rundt med data. Du vil opdage at du vil komme til at bruge meget tid på bare at flytte rundt med data. Hvis du ser på vores program, vil du se at det næsten ikke bruger andet end mov-instruktioner!

Det første vi skal have gjort er at flytte vores datasegment-adresse ind i CPU-registret DS så vi kan bruge vores data. Her støder vi på CPU'ens første begrænsning: Man kan ikke lægge en værdi direkte ind i DS. Derfor er vi nødt til først at lægge vores adresse ind i AX, og så derefter flytte indholdet af AX ind i DS. Det er det som de to første mov-instruktioner gør. Instruktionen mov ax,data betyder: flyt datasegment-adressen ind i register AX. Nu er vi så heldige at vi ikke behøver at kende den præcise adresse på vores datasegment. Det har vi fået assembleren til at holde styr på ved at kalde vores datasegment for data med direktivet SEGMENT data længere nede i kildekodefilen. Hvis vi i stedet havde kaldt vores datasegment for f.eks. papir, så skulle vi have brugt instruktionen mov ax,papir i stedet.

Så nu indeholder register AX altså vores datasegment-adresse, men den skal jo ligge i register DS (husk: Data Segment). Det gør vi med instruktionen mov ds,ax som betyder: flyt indholdet af AX ind i DS. Måske synes du at det hele virker omvendt, og at instruktionen burde have været mov ax,ds. Det virker meget mere naturligt, men sådan er det ikke! Overalt i Intel assembler kommer destinationen først og kilden kommer bagefter. I øvrigt er navnet mov lidt vildledende, for når du flytter noget fra AX til DS, så bliver værdien ikke slettet fra AX, så det du egentlig foretager dig, er at du kopierer værdien fra register AX til register DS.

De næste to instruktioner gør det samme, bare med staksegmentet (Stack Segment). Den sidste instruktion mov sp,stacktop sætter SP (Stack Pointer) til at pege på den første plads i stakken. Du skal ikke vide så meget om stakken endnu, for vi skal ikke selv bruge den, men den skal være med i programmet alligevel.

De næste tre instruktioner er vores egentlige program:
     mov        dx,besked
     mov        ah,9
     int        21h
Som du kan se, bruger vi igen nogle mov-instruktioner, og til sidst en int-instruktion. Nede i vores datadel har vi indsat en sætning, en streng af tegn. Den har vi kaldt besked, og vi er nu igen så heldige at assembleren tager sig af at holde styr på offset-adressen for os. besked er nemlig lig med offset-adressen på starten af vores tekststreng (som vi har defineret længere nede i kildekode-filen).

int-instruktionen bruges til at kalde et såkaldt interrupt. I dette tilfælde er det interrupt 21h. Bemærk h'et efter 21. Det fortæller assembleren at det er en hexadecimal værdi du her bruger, og du skal altid bruge et h til at markere at et tal er i hex. I appendikset kan du se at interrupt 21h er de såkaldte DOS-funktioner. Det er altså et bibliotek af rutiner som du kan bruge i dine programmer. Nummeret på den ønskede funktion skal du gemme i register AH inden du kalder interrupt 21h. Det er det vi gør med mov ah,9. Vi flytter 9 ind i register AH (AH er den øvre halvdel af register AX). I oversigten kan du se at funktion 9 hedder Print String, dvs. at den kan udskrive en streng på skærmen. Funktion 9 skal på en eller anden måde vide hvilken streng den skal udskrive for dig, så det skal også angives før brug af int 21h. Du skal gemme strengens placering i hukommelsen i DS:DX, dvs. at segmentet skal ligge i DS, og offset-værdien skal ligge i DX. Segmentet har vi allerede gemt. Det var faktisk det første vi gjorde i programmet (de to første instruktioner). Eftersom ``variablen'' besked er lig med offset-værdien til starten af vores streng, bruger vi instruktionen mov dx,besked til at flytte offset-værdien ind i DX. Så sørger assembleren for at indsætte den rigtige offset-værdi på besked's plads. Så tager interrupt 21h over og udskriver vores streng på skærmen tegn for tegn indtil den støder på tegnet $. Hvis du vil have den til at skrive et dollartegn, kan du altså ikke bruge interrupt 21h funktion 9!

De to sidste instruktioner er:
     mov        ax,4C00h
     int        21h
Her bruger vi også interrupt 21h. I modsætning til før, så bruger vi nu funktion 4Ch i stedet for funktion 9. Funktion 4Ch kaldes også for Terminate Process hvilket betyder at vores program slutter. 4Ch skal gemmes i AH (ligesom 9 skulle gemmes i AH for at vælge funktion 9), så hvorfor gemmer vi 4C00h i AX? Det er faktisk bare en hurtigere måde at gemme to værdier på. Vi kunne i stedet have brugt to mov-instruktioner: Først gemmer vi 4Ch i AH, dernæst gemmer vi 00 i AL. I AL skal vi nemlig lægge en errorlevel-værdi når vi kalder funktion 4Ch. Det er ikke strengt nødvendigt at gemme noget i AL, men det er god skik at gemme et 00 hvis errorlevels alligevel ikke bruges. Errorlevels kan bruges hvis du har et stort program med mange forskellige måder at afslutte på. Så kan du gemme en forskellig errorlevel-værdi for hvert sted hvor programmet bliver afsluttet, så du kan holde styr på hvor dit program bliver afsluttet vha. en .bat-fil. Dette bruges især til at finde fejl med.

Den næste del er vores datadel:
SEGMENT data

     besked     db    "Assembler-programmering!", 13, 10, "$"
Her definerer vi vores data. I dette program har vi ikke så meget data. Vi indsætter i dette tilfælde en masse tegn efter hinanden, omringet af anførselstegn, herefter indsætter vi 13 og 10, og til sidst et dollartegn som markerer afslutningen på strengen. 13 og 10 markerer en vognretur og et linjeskift. Du kan se disse koder i en ASCII-tabel6. Grunden til at man skal bruge to koder for at skifte linje er at man af tekniske årsager gjorde det på den måde i den fjerne fortid. Foran står der besked som betyder at vi sætter besked til at være lig med den første byte i vores streng (her er det et A). Så kan vi bruge ordet besked i stedet for den rigtige offset-værdi. Midt imellem de to ting står der db. Det står for Define Byte. Man kan også definere words og double words med dw og dd. Det har vi dog ikke brug for her. Ét tegn fylder netop én byte. I stedet for at skrive et dollartegn i anførselstegn kunne du også bare have skrevet ASCII-værdien for det. Så ville sidste del af linjen have set sådan ud: ..., 13, 10, 36 da et dollartegn har ASCII-værdien 36.

Til sidst er der stakken:
SEGMENT minStak stack

     resb 64

stacktop:
Her kalder vi stakken for minStak og den er af typen stack. Vi reserverer 64 byte til stakken med direktivet resb 64 (Reserve Bytes). Til sidst har vi en etiket der peger på toppen af stakken, så derfor kalder vi den for stacktop: Denne etiket bruger vi i instruktion nr. 5 i vores program når vi skal sætte SP (Stack Pointeren) til at pege på starten af stakken, dvs. toppen af stakken. Husk på at selv om stacktop: er i bunden af kildekodefilen, så arbejder assembleren nedefra og op i hukommelsen, mens den læser kildekodefilen oppefra og ned, så etiketten stacktop: er altså det sidste som assembleren bearbejder, og stacktop: bliver så lig med den offset-værdi der ligger højest i hukommelsen (inden for vores program)!

6  DEBUG-programmet

Når du har fået lavet din eks1.exe-fil, kan du kigge nærmere på den med programmet Debug. Du starter Debug ved at skrive:
     C:\ASM\PROJEKT>debug eks1.exe
Debug svarer med et - (minustegn). Prøv at skrive et d (dump) efterfulgt af Enter. Her ser du den rå kode for vores program. Til venstre kan du se hvor i hukommelsen programmet ligger, angivet med segment:offset-notationen. I midten er vores kode i hexadecimale tal. Dette kunne lige så godt have stået i binære tal, men det ville fylde en hel del mere og ville ikke give et særlig godt overblik. Hvert tocifret hex-tal repræsenterer en byte (8 bit) i hukommelsen. Til højre på skærmen står vores kode oversat til ASCII-tekst. Hvert tegn har en ASCII-værdi; det er dog ikke alle ASCII-tegn som kan skrives med et symbol (f.eks. Enter) og disse tegn skrives som et . (punktum). Til højre kan du genkende vores tekststreng som vi skrev. Bemærk de to punktummer mellem ordet Assembler-programmering! og dollar-tegnet. Ovre i hex-tallene kan du se at de er tallene 0D og 0A (dec 13 og 10) som jo var vores linjeskift og som ikke kan skrives med symboler. Derfor er de skrevet som to punktummer.

Prøv nu at skrive et u (unassemble) efterfulgt af Enter. Her har Debug oversat hex-tallene tilbage til assembler. Du kan f.eks. se i adresse ????:0010 at B4 09 er instruktionen mov ah,09. (Grunden til de fire spørgsmålstegn er at segment-adressen ikke vil være den samme på din computer som på min. Det kommer an på hvor i hukommelsen der var plads til programmet da det blev indlæst i Debug.) Du kan også se at vores etiketter (data, minStak, besked osv.) er blevet erstattet af de rigtige segment-adresser og offset-værdier. Efter instruktionen CD 21 (int 21) kommer der nogle instruktioner som vi ikke har noget med at gøre. De kan være hvad som helst, og de er højst sandsynligt ikke engang ``rigtige'' instruktioner. Der ligger bare nogle tal i hukommelsen efter vores program er slut som Debug oversætter til instruktioner.

Hvis du taster t (trace), kører du programmet én instruktion ad gangen hvorefter du ser en oversigt over registrenes indhold. Efter den første instruktion er udført, kan du se at AX er blevet lig med adressen på vores datasegment. På tredje linje af oversigten kan du se hvilken instruktion som CS:IP (Code Segment:Instruction Pointer) peger på. Det er den næste instruktion som vil blive udført når du taster t. Den næste instruktion er her mov ds,ax. Tast t, og i den nye oversigt kan du se at DS er blevet lig med AX, og IP er blevet ændret til at pege på den næste instruktion. Hvis du fortsætter sådan et stykke tid, kommer du til instruktionen int 21 (Debug arbejder kun i hex, og bruger derfor ikke noget h til at markere at tallene er i hex). Hvis du trace'r denne instruktion, vil du opdage at du kommer et helt andet sted hen i hukommelsen. Det sker netop fordi du med int 21 kalder en af de mange DOS-funktioner, og disse DOS-funktioner ligger jo også et sted i hukommelsen og består af maskinkode-instruktioner; Debug følger bare efter. Tryk q (quit), for at afslutte Debug da DOS-funktionerne er ret lange, og du har sikkert ikke lyst til at trace igennem det hele.

7  Stakken

Hvordan finder computeren egentlig tilbage til vores program når den er færdig med at udføre en DOS-funktion? CS:IP kan jo kun pege på én instruktion ad gangen. Det er her stakken kommer i brug. Stakken er et midlertidigt lager hvor du kan gemme data og hente dem igen. Når computeren udfører en int-instruktion, så bliver adressen på den næste instruktion i programmet gemt på stakken. Når DOS-funktionen så bliver afsluttet med en iret-instruktion (Interrupt Return), så henter den adressen fra stakken igen. Stakken fungerer efter LIFO-princippet (Last In, First Out) hvilket betyder at det sidste der bliver lagt på stakken, også er det første der bliver hentet igen. Du kan naturligvis også bruge stakken manuelt. Her skal vi bruge instruktionerne push og pop.
 
         push     ax               push     ax       
         push     bx               push     bx
      ... andre koder ...       ... andre koder ...
         pop      bx               pop      ax
         pop      ax               pop      bx

            Rigtigt!                  Forkert!!
Lad os se to eksempler på brug af push og pop. Vi forestiller os at du skal bruge registrene AX og BX til noget, men du vil ikke miste de værdier som de allerede har! Derfor skubber du dem ind på stakken med push. Efter du udført en masse andre ting, giver du AX og BX deres gamle værdier tilbage med pop-instruktionen. Til venstre ovenfor er det gjort på den rigtige måde. BX kom sidst ind, så derfor kommer BX først ud. Til højre er det gjort forkert. Her vil AX og BX have byttet værdier efter pop'erne. Det kan selvfølgelig være at det var meningen, men så kunne du have brugt instruktionen xchg ax,bx i stedet. Ligesom CS:IP peger på den næste instruktion der skal udføres, så peger SS:SP på den næste plads på stakken hvor der er plads til noget data. Når du bruger push og pop, så bliver SP ændret så den peger på det rigtige sted. Husk at der altid kun er én stak i brug ad gangen. Det vil sige at din stak bliver brugt af hele computeren. I vores program reserverede vi kun 64 byte til stakken, men det er sikrere at reservere f.eks. 512 byte til den. Hvis der opstår et såkaldt Stack Overflow (stakken ``flyder over'' sine grænser), så begynder der at ske underlige ting og sager.

8  Real Mode Flat Model

I afsnit 5 skrev vi vores program i Real Mode Segmented Model. I dette afsnit vil vi lave nøjagtig samme program, men denne gang i det der hedder Real Mode Flat Model. Denne model er mere enkel, men begge modeller er lidt forældede. Den nye model hedder Protected Mode Flat Model der bruges til 32-bit-programmering, men den model vil jeg ikke komme ind på her.

Da vi brugte Real Mode Segmented Model havde vi hele den første megabyte af computerens hukommelse til rådighed. Vores program var dog et meget lille program, og det vil de fleste af dine fremtidige programmer nok også være hvis du skriver dem i assembler. Så kan vi jo nøjes med en .com-fil som fylder under 64 kilobyte. 64 kilobyte lyder ikke af meget, men det er en pæn størrelse når vi snakker assembler!

Som du husker(?) brugte vi i Real Mode Segmented Model segmenter og offsets til at udpege områder i hukommelsen. Vi placerede et segment i et segmentregister, og så placerede vi en offset-værdi i et indeksregister. Offset-værdien kunne have en værdi mellem 0 og 65.535 (0-FFFF) og vi kunne således ``se'' 64 kilobyte frem for os fra det aktuelle segment.

I Real Mode Flat Model behøver vi ikke bekymre os om segmenter eftersom hele vores program, vores data og stakken ligger inden for 64 kilobyte. Computeren sætter derfor automatisk alle segmentregistrene for dig. Lad os se på vores program i Real Mode Flat Model:
[BITS 16]
[ORG 100h]

 
[SECTION .text]

     mov      dx,besked
     mov      ah,9
     int      21h

     mov      ax,4c00h
     int      21h

 

[SECTION .data]

     besked   db       "Assembler-programmering!", 13, 10, "$"

;---------------------------------------------
;        Slut på vores fil
;---------------------------------------------
Som du kan se er programmet blevet væsentlig mindre. Du kan kompilere programmet med NASM på følgende måde hvis du har gemt filen som eks2.asm:
     C:\>nasm eks2.asm -f bin -o eks2.com
Bemærk at når du laver en simpel .com-fil behøver du ikke bruge linkeren (ALink) bagefter. NASM ordner selv tingene for dig ved brug af -f bin. Programmet fylder nu kun 39 byte!

Men lad os nu se på de nye ting. Vi starter endnu engang med [BITS 16] for at fortælle NASM at vi vil generere 16-bit-kode. Altså et almindeligt DOS-program. Herefter har vi direktivet [ORG 100h]. Det betyder at vi springer de første 256 byte (hex 100h) over, og computeren skal udføre vores instruktioner herfra. Grunden til at dette er nødvendigt er at .com-programmer bruger de første 256 byte til forskellige ting og sager som vi ikke har lyst til at pille ved---derfor springer vi dem over, og markerer med [ORG 100h] at vores program starter 256 byte længere fremme.

Næste linje er [SECTION .text]. I Real Mode Flat Model arbejder vi ikke med segmenter, men med sektioner inden for det enkelte segment som vi har til rådighed. Den førnævnte linje fortæller hvor vores programkode befinder sig. Bemærk her at vi nu ikke skal initialisere segmentregistrene som vi gjorde i Real Mode Segmented Model (de første fem instruktioner). På den måde bliver programmet endnu simplere. Vi har heller ikke brug for en start-etiket da vi skal kalde vores kodesektion for .text.

Vores data-sektion markerer vi med [SECTION .data], og så kan vi initialisere vores data på samme måde som vi gjorde i Real Mode Segmented Model.

Vi har ikke brug for at reservere plads til stakken. Den ligger nemlig i toppen af de 64 kilobyte (vores segment), og hver gang vi push'er noget på stakken ``bevæger'' stakken sig tættere på vores programkode. Det vil sige at hvis vores program er meget stort, og vi lagrer en masse data på stakken, så kan de to ting kollidere, og resultatet af dét er sikkert ikke særlig pænt ...

9  Adressering af data

Der er tre måder at adressere data på: Immediate, register og memory. Immediate er den nemmeste at forstå, og vi har også allerede brugt den. Her er et eksempel:
     mov      ah,9
Her flytter vi tallet 9 ind i register AH. Der er ingen tvivl om at tallet er 9. Det vil altid være tallet 9 der bliver lagt ind i AH når denne instruktion bliver udført. Immediate data er altså en fast værdi som er ``indbygget'' i instruktionen. Her er nogle andre eksempler:
     int      21h
     mov      ax,4c00h
     mov      dx,besked
Den sidste kræver måske lidt forklaring. Her skal du huske at vi i data-sektionen satte besked til at være lig med offset-værdien til starten af det område i hukommelsen som indeholder vores streng. Vi sætter altså kort sagt DX til at være lig med adressen på starten af strengen.

Her er et eksempel på brug af register-data:
     mov      ax,bx
Her sætter vi AX til at være lig med BX. Vi kopierer altså indholdet af BX over i AX. Bemærk her at man ikke kan kopiere indholdet af et 2-byte-register over i et 1-byte-register og omvendt som f.eks AH over i BX. Her er nogle flere eksempler:
     mov      ax,di
     mov      si,cx
Til sidst er der memory-data. Det er en lille smule mere kompliceret. Her er et eksempel:
     mov      ax,[bx]
Her fortæller de kantede parenteser at vi snakker om memory-data. Det antages så at register BX indeholder en adresse. Den værdi som ligger på denne adresse i hukommelsen, bliver kopieret over i register AX. BX indeholder her offset-værdien. Segmentet antages i de fleste tilfælde at ligge i register DS (Data Segment), men det skal du jo ikke bekymre dig om når du arbejder i Real Mode Flat Model. Her er et eksempel der måske illustrerer tingene lidt bedre:
     mov      ax,[besked]
besked repræsenterer en offset-adresse som peger på vores streng. Ved at sætte kantede parenteser omkring, fortæller vi at det er indholdet af den adresse besked peger på som vi vil have fat i. Hvis vi holder os til vores program fra før, vil AX nu indeholde ASCII-værdien for tegnet A som jo er første tegn i vores streng.

Vi havde ikke brug for memory-metoden i vores program. Vi skulle bruge offset-værdien til starten af vores streng inden vi kaldte interrupt 21h, funktion 9, som forventer at DX indeholder offset-værdien til starten af den streng der skal udskrives.

Der er en smart ting ved memory-data. Betragt følgende eksempel:
     mov      [bx+di+2],al
Her flytter vi indholdet af register AL ind på den adresse, som BX+DI+2 peger på. Det vil sige at computeren først tager indholdet af BX. Derefter lægger den indholdet af DI til, og til sidst lægger den 2 til resultatet. Det er denne adresse hvis indhold bliver sat lig med indholdet af AL. På den måde kan vi ændre register DI, og instruktionen vil pege på en helt anden adresse. Der er nogle krav til brugen af denne metode. Det første register skal være enten BX eller BP. Det andet register skal være DI eller SI, og den sidste værdi skal være en konstant, dvs. et tal som er fast defineret i koden. Du kan dog godt udelade det sidste tal, men du kan ikke tilføje flere registre som f.eks. [BX+DI+SI+3]. Måske har du svært ved lige at se det smarte i det her, men i det store, afsluttende eksempel i afsnit 15 kommer vi til at bruge det i stor stil.

10  Type Specifiers

Hvis du vil være på den sikre side (og nogle gange kræver assembleren at du er), så kan du bruge såkaldte type specifiers til at angive om en offset-værdi peger på en byte eller et word eller noget helt tredje. Nogle gange kan man selv regne ud, hvilken type der er tale om, men det er ikke altid assembleren kan. Det kan du specificere sådan her:
     mov      al,BYTE [BX]
BX indeholder en offset-værdi. Men denne offset-værdi peger jo både på en byte og et word osv. Ved at angive typen som det er gjort ovenfor, kan der ikke være tvivl om at det er en byte vi er ude efter. Det kan være svært at indse hvornår assembleren kræver en type specifier. Men du kan bare starte med at lade være med at bruge dem. Hvis assembleren så, under kompileringen, skriver ``operation size not specified'' og et linjenummer, så ved du hvor den kræver én, og så er det jo blot at indsætte den.

11  Procedurer

Når du laver større programmer, kan det være smart at opdele dit program i flere blokke. Så kan du lave et hovedprogram som kalder de forskellige blokke. Lad os se på en ny version af vores sædvanlige program:
 
[BITS 16]
[ORG 100h]

 
[SECTION .text]

     call  write
     jmp   end

 
write:
     mov  dx,besked
     mov  ah,9
     int  21h
     ret

 
end:
     mov  ax,4c00h
     int  21h

 
[SECTION .data]

     besked   db       "Assembler-programmering!", 13, 10, "$"

;---------------------------------------------
;        Slut på vores fil
;---------------------------------------------
Vi har nu delt vores program op i to underrutiner som vi har markeret med etiketter, her write: og end:. Vores ``egentlige'' program består nu kun af en call-instruktion og en jmp-instruktion. Første instruktion er call write, så computeren springer til etiketten write: og fortsætter derfra indtil den møder en ret-instruktion (return) der fortæller computeren at den skal gå tilbage til det oprindelige program. Derefter kalder den end-proceduren som afslutter programmet. end-proceduren kaldes med jmp-instruktionen. Computeren glemmer så alt om hvor den var og følger blindt efter. Det gør dog ikke noget, for denne procedure skal jo afslutte programmet, og vi har ikke brug for at vende tilbage til hovedprogrammet. end-proceduren har derfor heller ikke nogen ret-instruktion. Husk: call og ret hører sammen!

Der er ikke de store fordele ved at dele et så lille program op i procedurer, men det hjælper meget på store programmer som vi også skal se i afsnit 15.

12  Instruktionerne inc og dec

Du kan bruge instruktionen inc til at forhøje en værdi med 1, og instruktionen dec til at formindske med 1. Eksempel:
     mov      ax,5
     inc      ax
     dec      ax
Her lægger vi først 5 ind i AX. Så bruger vi inc på AX. Nu vil AX have værdien 6. Herefter bruger vi dec på AX, og AX har værdien 5 igen. Hvis AX havde været 255 (hex FF) og man brugte inc, så ville AX få værdien 0. Omvendt hvis du bruger dec og AX har værdien 0, så vil den få værdien 255.

13  Instruktionerne add og sub

Instruktionerne add og sub kan du bruge hvis du vil lægge en værdi til eller trække en værdi fra en anden værdi. Eksempel på add:
     mov      ax,5
     mov      cx,4
     add      ax,cx
Her lægger vi 4 til værdien 5, og resultatet 9 bliver lagt i AX som dermed erstatter den ene operand, her 5. sub fungerer på næsten samme måde:
     mov      ax,10
     sub      ax,3
AX får værdien 10 hvorefter sub trækker 3 fra værdien i AX, og resultatet bliver lagt i AX som dermed mister den første værdi, her 10. Hvis resultatet er negativt, så bliver Carry Flag (CF) sat lig med 1. Du kan så teste Carry Flag, og lade programmet træffe en beslutning. Mere om disse ting i næste afsnit.

14  Lad computeren træffe beslutninger

Overskriften til dette afsnit er måske ukorrekt. Det du skal lære nu, er hvordan du kan teste flagenes tilstand, og så gøre noget forskelligt alt efter resultatet af en sådan test. Vi starter med at se på instruktionen cmp (compare = sammenlign):
     cmp      cx,5
cmp-instruktionen er faktisk en sub-instruktion. Forskellen er bare at resultatet af subtraktionen ikke gemmes. Men hvis resultatet bliver nul (dvs. de to værdier var ens), så sættes Zero Flag (ZF) til 1. Hvis ikke, så sættes ZF til 0. Vi kan altså bruge ovenstående eksempel til at teste om CX har værdien 5. Hvis ja, så bliver ZF lig med 1; hvis nej, så bliver ZF lig med 0. Vi kan teste med je-instruktionen (jump if equal):
     cmp      cx,5
     je       videre
Hvis CX er lig med 5, så sender je-instruktionen os til det sted i programmet som er markeret med etiketten videre:. Hvis CX er forskellig fra 5, så fortsætter programmet med at udføre instruktionen efter je. Vi kunne også have brugt den ``omvendte'' funktion jne (jump if not equal). Så ville computeren springe til videre: hvis CX var forskellig fra 5.

Der er mange j??-instruktioner. Her en liste over de mest anvendelige:

ja jump if above
jae jump if above or equal
jb jump if below
jbe jump if below or equal
je/jz jump if equal/Jump if zero
jne/jnz jump if not equal/Jump if not zero
jg jump if greater
jge jump if greater or equal
jl jump if less
jle jump if less or equal
jc jump if Carry Flag set
jnc jump if not Carry Flag set


Som du kan se, så er den eneste beslutning en computer kan træffe, en beslutning om enten at hoppe---eller at lade være med at hoppe. Men et hop kan selvfølgelig have stor betydning. Computeren ``hopper'' ved at ændre register IP (Instruction Pointer) som jo indeholder adressen på den næste instruktion som skal udføres. Så IP sættes simpelthen bare lig med den adresse som står efter j??-instruktionen. Denne adresse er så typisk repræsenteret vha. af en etiket.

Den føromtalte call-instruktion fungerer på samme måde som jmp-instruktionen. Men umiddelbart inden IP ændres, bliver den gamle værdi (som indeholder adressen på instruktionen lige efter call-instruktionen) gemt på stakken. ret-instruktionen gør så ikke andet end at hente værdien tilbage fra stakken igen og lægge den ind i register IP!

Sådan nogle detaljer behøver du naturligvis slet ikke at vide noget om, men det er alligevel kendskabet til sådanne ting som kan gøre assembler-programmering sjovt.

15  Det afsluttende eksempel

I dette afsnit skal vi se på et afsluttende eksempel: et fire på stribe-spil. Det ser måske lidt overvældende ud---og det er det måske egentlig også. Men her kommer programkoden, og så vil jeg forklare det bagefter. Det vil være en god idé at have appendikset ved hånden under læsning og gennemgang af programmet.
; *** Fire på stribe ***

[BITS 16]

;------------------------------------------------------------

;----------------------Code Section--------------------------

;------------------------------------------------------------

SEGMENT code

; *** Main Program ***

..start:
     mov  ax,data
     mov  ds,ax               ; Initialisér data-segmentet.
     mov  ax,minStak
     mov  ss,ax               ; Initialisér stakken.
     mov  sp,stacktop

     call clrBoard            ; Tømmer pladen for brikker.

turn:
     mov  BYTE [spiller],'1'  ; Spiller 1's tur.
     call drawScreen          ; Tegner pladen.
     call getMove             ; Henter spiller 1's træk.
     call makeMove            ; Udfører spiller 1's træk.
     call checkStatus         ; Er spillet vundet eller uafgjort?

     mov  BYTE [spiller],'2'  ; Spiller 2's tur.
     call drawScreen          ; Tegner pladen.
     call getMove             ; Henter spiller 2's træk.
     call makeMove            ; Udfører spiller 2's træk.
     call checkStatus         ; Er spillet vundet eller uafgjort?

     jmp  turn

 

; *** Generelle procedurer ***

gotoXY:                  ; !!! Gem X i DL og Y i DH !!!
     mov  bh,0           ; Display Page 0
     mov  ah,02h         ; VIDEO Service 02h: Position Cursor
     int  10h
     ret

clrScreen:
     mov  cx,0           ; Vælg hele skærmen.
     mov  dx,184Fh       ;   (184Fh <=> Y=24 og X=79)
     mov  al,0           ; 0=Slet i stedet for scroll.
     mov  bh,07h         ; Display attribute = 7 (Normal)
     mov  ah,06h         ; VIDEO Service 06h: Initialize/Scroll
     int  10h
     ret

write:                   ; !!! Gem strengens adresse i DS:DX !!!
     mov  ah,09h         ; DOS-funktion 09h: Print String
     int  21h
     ret

end:
     mov  ax,4C00h       ; Afslut programmet.
     int  21h

 

; *** Clear Board ***

clrBoard:
     mov  ax,ds
     mov  es,ax          ; Gem Data-segment i ES.

     mov  di,plade       ; Gem pladens start-offset i DI.
     mov  cx,6*7         ; Alle pladens felter (6*7)
     mov  al,'+'         ;   skal sættes lig med '+'.

     cld                 ; Vi skal skyde +'er nede fra og op.
rep stosb                ; Her skyder vi vores +'er!

     ret

 

; *** Draw Screen ***

drawScreen:
     call clrScreen      ; Rydder skærmen.

     mov  dl,60          ; X=60
     mov  dh,0           ; Y=0
     call gotoXY         ; Placér markør.
     mov  dx,streng1     ; Skriver "9: Afslutter spillet" i
     call write          ;   skærmens øverste højre hjørne.

     mov  dx,streng2     ; Skriver "    1234567"
     call write

     mov  bx,1           ; File Handle=1 (Standard Output)
     mov  cx,7           ; Skriv 7 byte.
     mov  si,0           ; Start med pladens første række.

printRow:
     mov  dx,plade       ; Gem pladens start-offset i DX.
     add  dx,si          ; Gem rækkenr. i DX.

     mov  ah,40h         ; DOS-funktion 40h: Write to File
     int  21h

     mov  dx,streng3     ; Linjeskift + "    "
     call write

     add  si,7           ; Gå til næste række
     cmp  si,42          ;   hvis der er flere...
     jne  printRow

     mov  dx,nylinje     ; Linjeskift.
     call write

     ret

 

; *** Get Move ***

getMove:
     mov  dx,streng4     ; Skriver "Spiller "
     call write

     mov  dl,[spiller]   ; Skriver spillerens nr.
     mov  ah,02h         ; DOS-funktion 02h: Print Character
     int  21h

     mov  dx,streng5     ; Skriver "---------".
     call write

prompt:
     mov  dx,streng6     ; Skriver "Indtast træk: "
     call write

     mov  ah,0           ; Keyboard Service 00h: Get Key From Buffer
     int  16h

     cmp  al,'9'         ; 9 slutter spillet.
     je   end

     cmp  al,'1'         ; Tester for mindre end 1.
     jl   badKey

     cmp  al,'7'         ; Tester for større end 7.
     jg   badKey

     sub  al,49               ; Konverterer ASCII-koden
                              ;   til en værdi mellem 0 og 6.
     mov  bx,plade            ; Gem pladens start-offset i BX.
     mov  ah,00               ; Sørger for at high byte ikke ændrer værdien.
     mov  di,ax               ; Gem trækket i DI.
     cmp  BYTE [bx+di],'+'    ; Hvis det _ikke_ er et +, så er
     jne  illegalMove         ;   trækket ugyldigt.

     ret

badKey:
     mov  dx,streng7     ; Skriver "Du skal indtaste..."
     call write
     jmp  prompt

illegalMove:
     mov  dx,streng8     ; Skriver "Ugyldigt træk!"
     call write
     jmp  prompt



; *** Make Move ***

makeMove:
     mov  bx,plade            ; Gem pladens start-offset i BX.
     add  bx,35               ; Hop til pladens nederste række.
     mov  di,ax               ; Gem træk (kolonne) i DI.
                              ;   AL blev lig med træk i getMove.
itIsNot:
     cmp  BYTE [bx+di],'+'    ; Tester om feltet er tomt (et +).
     je   itIs
     sub  bx,7                ; Hop op til næste række.
     jmp  itIsNot

itIs:
     cmp  BYTE [spiller],'1'  ; Er det spiller 1's træk?
     jne  player2
     mov  BYTE [bx+di],'X'    ; Læg spiller 1's brik (X) på pladen.

     ret

player2:
     mov  BYTE [bx+di],'O'    ; Læg spiller 2's brik (O) på pladen.

     ret
 

; *** Check Status ***

checkStatus:
     cmp  BYTE [spiller],'1'  ; Er det spiller 1's tur?
     je   yes
     mov  BYTE [spiller],'O'  ; Nej, læg spiller 2's brik i spiller.
     jmp  horizInit

yes:
     mov  BYTE [spiller],'X'  ; Ja, læg spiller 1's brik i spiller.

 

; --- Tjekker for 4 på stribe vandret ---

horizInit:
     mov  bx,plade            ; Gem pladens start-offset i BX.
     mov  al,[spiller]        ; Flyt spillerens brik ind i AL til testning.

     mov  di,0                ; Start i pladens øverste venstre hjørne.
     mov  cx,0

horiz:
     cmp  [bx+di],al
     jne  not1
     cmp  [bx+di+1],al
     jne  not1
     cmp  [bx+di+2],al
     jne  not1
     cmp  [bx+di+3],al
     jne  not1
     jmp  gameWon             ; Der er 4 på stribe vandret!

not1:
     inc  di                  ; Næste felt.
     inc  cx

     cmp  di,39               ; Er vi færdige med at tjekke vandret?
     je   vertInit            ; Ja, fortsæt med lodret.

     cmp  cx,4                ; Bevæger vi os over i 3. zone?
     je   newRow1             ; Ja, næste række.

     jmp  horiz               ; Nej, fortsæt med næste felt.

newRow1:
     add  di,3                ; Næste række.
     mov  cx,0

     jmp  horiz               ; Gennemløb løkken igen.

 

; --- Tjekker for 4 på stribe lodret ---

vertInit:
     mov  di,0                ; Start i pladens øverste venstre hjørne.

vert:
     cmp  [bx+di],al
     jne  not2
     cmp  [bx+di+7],al
     jne  not2
     cmp  [bx+di+14],al
     jne  not2
     cmp  [bx+di+21],al
     jne  not2
     jmp  gameWon             ; Der er 4 på stribe lodret!

not2:
     inc  di                  ; Næste felt.

     cmp  di,21               ; Er vi færdige med at tjekke lodret?
     je   diag1Init           ; Ja, fortsæt med diagonalt.

     jmp  vert                ; Nej, fortsæt med næste felt.

 

; --- Tjekker 1. zone for 4 på stribe diagonalt ---

diag1Init:
     mov  di,0                ; Start i pladens øverste venstre hjørne.
     mov  cx,0

diag1:
     cmp  [bx+di],al
     jne  not3
     cmp  [bx+di+8],al
     jne  not3
     cmp  [bx+di+16],al
     jne  not3
     cmp  [bx+di+24],al
     jne  not3
     jmp  gameWon             ; Der er 4 på stribe diagonalt!

not3:
     inc  di                  ; Næste felt.
     inc  cx

     cmp  di,18               ; Er vi færdige med at tjekke 1. zone?
     je   diag2Init           ; Ja, fortsæt med 2. zone.

     cmp  cx,4                ; Bevæger vi os over i 3. zone?
     je   newRow3             ; Ja, næste række.

     jmp  diag1               ; Nej, fortsæt med næste felt.

newRow3:
     add  di,3                ; Næste række.
     mov  cx,0

     jmp  diag1               ; Gennemløb løkken igen.

 

; --- Tjekker 2. zone for 4 på stribe diagonalt ---

diag2Init:
     mov  di,21               ; Start i række 3 (3*7=21).
     mov  cx,0

diag2:
     cmp  [bx+di],al
     jne  not4
     cmp  [bx+di-6],al
     jne  not4
     cmp  [bx+di-12],al
     jne  not4
     cmp  [bx+di-18],al
     jne  not4
     jmp  gameWon           ; Der er 4 på stribe diagonalt!

not4:
     inc  di
     inc  cx

     cmp  di,39               ; Er vi færdige med at tjekke 2. zone?
     je   tieInit             ; Ja, fortsæt med test for uafgjort.

     cmp  cx,4                ; Bevæger vi os over i 3. zone?
     je   newRow4             ; Ja, næste række.

     jmp  diag2               ; Nej, fortsæt med næste felt.

newRow4:
     add  di,3                ; Næste række.
     mov  cx,0

     jmp  diag2               ; Gennemløb løkken igen.

 

; --- Tjekker om spillet er uafgjort (ingen tomme felter) ---

tieInit:
     mov  di,0

tie:
     cmp  BYTE [bx+di],'+'    ; Er der flere tomme felter?
     je   notTie              ; Ja, fortsæt spillet.

     inc  di
     cmp  di,7                ; Er sidste felt nået?
     je   gameTie             ; Ja, spillet er uafgjort.
     jmp  tie                 ; Nej, gennemløb løkken igen.

notTie:
     ret

 

; --- Spillet er uafgjort ---

gameTie:
     call drawScreen
     mov  dx,strengB
     call write
     call end

 

; --- Spillet er vundet ---

gameWon:
     cmp  al,'X'              ; Har spiller 1 vundet?
     jne  player2won

     call drawScreen
     mov  dx,streng9          ; Ja.
     call write
     call end

player2won:
     call drawScreen
     mov  dx,strengA          ; Nej, spiller 2 vandt.
     call write
     call end

 

;------------------------------------------------------------

;----------------------Data Section--------------------------

;------------------------------------------------------------

SEGMENT data

plade    resb 6*7       ; Pladens højde=6, bredde=7

streng1  db   "9: Afslutter spillet",13,10,13,10,'$'
streng2  db   "    1234567",13,10,"    $"
streng3  db   13,10,"    $"
streng4  db   "Spiller $"
streng5  db   13,10,"---------$"
streng6  db   13,10,"Indtast Træk: $"
streng7  db   13,10,13,10,"Du skal indtaste et træk mellem 1 og 7!",13,10,'$'
streng8  db   13,10,13,10,"Ugyldigt træk!",13,10,'$'
streng9  db   13,10,"Spiller 1 har vundet!",13,10,'$'
strengA  db   13,10,"Spiller 2 har vundet!",13,10,'$'
strengB  db   13,10,"Spillet er uafgjort!",13,10,'$'

nylinje  db   13,10,'$'

spiller  db   '*'

 

;------------------------------------------------------------

;----------------------Stack Section-------------------------

;------------------------------------------------------------

SEGMENT minStak stack

     resb 512

stacktop:

 
;----------------------End Of File---------------------------
Inden vi går i gang med at gennemgå programmet, vil jeg anbefale dig at prøve det først så du kan se hvordan det skal køre. Programmet er skrevet i Real Mode Segmented Model. Eftersom det jo er et lille program (alt er relativt), så havde det sikkert været bedre at skrive det i Real Mode Flat Model, men det vil jeg overlade til dig som en øvelse. Når jeg har gennemgået programmet, skal jeg nok give nogle tip til hvordan det gøres. Det er faktisk rimelig simpelt.

I starten af programmet fortæller vi assembleren at vi ønsker at generere 16-bit-kode med [BITS 16]. Vores fire på stribe-program er delt op i de tre segmenter som jo er traditionelle for Real Mode Segmented Model: SEGMENT code, SEGMENT data og SEGMENT minStak stack. Lad os starte med at se på data-segmentet.

I data-sektionen reserverer vi 6*7 (altså 42) byte i hukommelsen vha. direktivet resb. Den første af disse 42 byte er så repræsenteret af navnet plade. Disse 42 byte skal bruges til at indeholde oplysninger om spillepladen (eller ``spillestativet'').

Her er et kort over de 42 byte der illustrerer brugen lidt bedre:
0 1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31 32 33 34
35 36 37 38 39 40 41
plade er lig med adressen på byte 0, dvs. pladens øverste venstre hjørne. Hvis dette felt er tomt, så gemmer vi ASCII-værdien for et + i adressen plade. Har spiller 1 en brik i feltet, så gemmer vi ASCII-værdien for et X, og hvis spiller 2 har en brik i feltet, så gemmer vi ASCII-værdien for et O. Hvis vi i stedet skal bruge f.eks. felt nr. 3, skal vi bruge adressen plade+3 osv. De to optrukne streger på kortet skal du ikke tænke på nu. De gør det mere tydeligt hvad der sker når vi skal til at tjekke om der er fire på stribe.

Der er ikke nogen tekniske årsager til at skrive resb 6*7 i stedet for resb 42. Det er for at tydeliggøre at vores plade har 6 rækker og 7 kolonner.

Herefter initialiserer vi en masse strenge. Det er beskeder og kommentarer som vi under programmet skal have vist på skærmen.

Til sidst initialiserer vi en byte spiller til at have ASCII-værdien for en *. Denne byte skal bruges til at holde styr på hvis tur det er. Er det spiller 1's tur, gemmer vi ASCII-værdien for 1. Er det spiller 2's tur, så gemmer vi ASCII-værdien for 2. Stjernen er bare en pladsholder under initialiseringen, og har ikke nogen praktisk betydning. Den vil blive erstattet når den første spiller begynder sin tur. Egentlig kunne vi også have reserveret en byte vha. resb og på den måde gjort vores .exe-fil én byte mindre, men nu kan vi lettere finde vores byte i et program som DEBUG. Så skal vi lede efter en stjerne (selv om der jo godt kan dukke en stjerne op i .exe-filen på andre måder).

I stak-segmentet reserverer vi 512 byte til stakken minStak. Stakken blev gennemgået i afsnit 7, og vi kommer ikke til at bruge den direkte i dette program.

15.1  Main Program

Lad os så komme i gang med selve programmet. De første fem instruktioner efter ..start:-etiketten initialiserer segmenterne som det blev forklaret i afsnit 5. Herefter kalder vi underrutinen clrBoard som sørger for at sætte alle de 42 byte, som repræsenterer pladen, til at være lig med ASCII-værdien for + (tomt felt).

Herefter kommer vi til vores hovedrutine, som vi har markeret som turn:
turn:
     mov  BYTE [spiller],'1'  ; Spiller 1's tur.
     call drawScreen          ; Tegner pladen.
     call getMove             ; Henter spiller 1's træk.
     call makeMove            ; Udfører spiller 1's træk.
     call checkStatus         ; Er spillet vundet eller uafgjort?

     mov  BYTE [spiller],'2'  ; Spiller 2's tur.
     call drawScreen          ; Tegner pladen.
     call getMove             ; Henter spiller 2's træk.
     call makeMove            ; Udfører spiller 2's træk.
     call checkStatus         ; Er spillet vundet eller uafgjort?

     jmp  turn
Her har vi delt vores hovedrutine op i to dele, én for spiller 1 og én for spiller 2. Når spiller 2 er færdig med sin tur, tager vi turen endnu en gang med jmp turn osv. En tur starter med at sætte den adresse som spiller peger på til at være lig med den ASCII-værdi som repræsenterer vores aktuelle spiller (her ASCII-værdien for tegnene 1 og 2). Husk at vi bruger kantede parenteser om spiller fordi vi skal ændre indholdet af den adresse som spiller peger på, og ikke ændre indholdet af spiller selv. Vi sætter apostroffer omkring 1 og 2 for at vise at det ikke er tallet 1, men ASCII-værdien for tegnet 1 som vi vil gemme. Vi kunne selvfølgelig have slået op i en ASCII-tabel og så have skrevet ASCII-værdien direkte, men det er lettere at læse programmet på den måde vi bruger her. Bemærk at vi her skal bruge en type specifier så assembleren ved at spiller peger på en byte.

Herefter kalder vi de fire store underrutiner som får hele programmet til at fungere: drawScreen, getMove, makeMove og checkStatus. Jeg bruger altid engelske navne til mine underrutiner så jeg slipper for at bruge æ, ø og å.

drawScreen opdaterer det vi ser på skærmen, dvs. pladen og diverse beskeder. Hver gang en spiller har indtastet sit træk skal pladen opdateres så man kan se den nye brik på pladen. getMove er den underrutine som får spillerens træk. Den udskriver en besked, og spilleren skal så indtaste sit træk. makeMove udfører trækket, dvs. opdaterer pladen i hukommelsen (de 42 byte som repræsenterer vores plade). Til sidst kalder vi checkStatus. Det er den største og mest komplicerede rutine. Det er den som efter hvert træk skal teste om der er fire på stribe, enten vandret, lodret eller diagonalt. Hvis det er tilfældet, så er spillet slut. Hvis ikke, så vender vi tilbage til hovedrutinen, og spillet fortsætter.

15.2  Generelle procedurer

Efter hovedprogrammet har vi lige nogle generelle procedurer. Dem laver vi så vi i resten af programmet blot kan skrive call clrScreen for at rydde skærmen. Det gør programmet lettere at læse.

gotoXY-rutinen bruges til at placere markøren et bestemt sted på skærmen:
gotoXY:                  ; !!! Gem X i DL og Y i DH !!!
     mov  bh,0           ; Display Page 0
     mov  ah,02h         ; VIDEO Service 02h: Position Cursor
     int  10h
     ret
Som du kan se af int 10h-instruktionen bruger vi interrupt 10h: Video Display Services. Jeg har samlet nogle få af disse ``VIDEO services'' i appendikset. Når man kalder interrupt 10h, forventes det at nummeret på den ønskede service er gemt i register AH. Vi gemmer derfor 02h i AH lige inden vi kalder interruptet. I appendikset kan vi se at service 2 hedder Position Cursor, og den kan bruges til at placere markøren et bestemt sted på skærmen. I beskrivelsen af service 2 står der at vi skal gemme et 0 i BH, så det gør vi også inden kaldet. 0'et angiver Display Page 0. Display Pages er noget halvkompliceret noget, og derfor har jeg bare skrevet at man skal gemme et 0 i BH. Der står også i beskrivelsen at vi skal gemme X-koordinaten for markørens ønskede position i DL og Y-koordinaten i DH. Det er der dog ikke meget mening i at gøre i selve rutinen. Det skal gøres inden man kalder gotoXY. Vi husker naturligvis at afslutte rutinen med ret for return så vi vender tilbage til det kaldende program.

clrScreen-rutinen bruges til at rydde skærmen:
clrScreen:
     mov  cx,0           ; Vælg hele skærmen.
     mov  dx,184Fh       ;   (184Fh <=> Y=24 og X=79)
     mov  al,0           ; 0=Slet i stedet for scroll.
     mov  bh,07h         ; Display attribute = 7 (Normal)
     mov  ah,06h         ; VIDEO Service 06h: Initialize/Scroll
     int  10h
     ret
Her bruger vi VIDEO service 6: Initialize/Scroll. Derfor gemmer vi et 6 i AH. I CL og CH skal vi gemme X- og Y-koordinaterne for det øverste venstre hjørne af det skærmareal som vi vil påvirke, og i DL og DH skal vi gemme koordinaterne for det nederste højre hjørne. Husk på at skærmens øverste venstre hjørne har koordinaterne (0,0). Skærmens nederste højre hjørne har koordinaterne (79,24) så længe du bruger en skærm med 25 linjer a 80 tegn. Her skal vi bruge hele skærmens areal. Vi sætter CX til 0 så både CL og CH bliver 0. DX sætter vi til 184Fh. 18h er 24 i decimal og 4F er 79 i decimal. I AL kan man angive hvor mange linjer området som man har markeret, skal scrolles (rulles som når man trykker Enter i bunden af skærmen). Hvis man angiver et 0, slettes området. Vi har gemt et 0 så området slettes. I BH gemmes en display attribute. Her gemmer vi 7 som er det normale. Efter alt dette, kan vi nu kalde interrupt 10h, og derefter vende tilbage til det kaldende program med ret.

write-rutinen gør ikke andet end at kalde DOS-funktionen Print String som så udskriver den streng på skærmen hvis start-adresse er gemt i DS:DX.

end-rutinen slutter programmet.

15.3  Clear Board

clrBoard:
     mov  ax,ds
     mov  es,ax          ; Gem Data-segment i ES.

     mov  di,plade       ; Gem pladens start-offset i DI.
     mov  cx,6*7         ; Alle pladens felter (6*7)
     mov  al,'+'         ;   skal sættes lig med '+'.

     cld                 ; Vi skal skyde +'er nede fra og op.
rep  stosb               ; Her skyder vi vores +'er!

     ret
Denne rutine kaldes inden spillets start. Den skal sørge for at sætte alle pladens felter lig med ASCII-værdien for tegnet + som er vores symbol for at feltet er tomt. Her bruger vi en af CPU'ens mere avancerede instruktioner, nemlig stosb (Store String by Byte). Den har jeg ikke omtalt før, men her kommer lidt forklaring. Den bruges til at kopiere indholdet af register AL til adressen som fremkommer vha. ES:DI. Vi vil gerne kopiere ASCII-værdien for + til den adresse som plade peger på. Derfor kopierer vi vores datasegment-adresse over i ES (husk at man ikke kan kopiere direkte fra DS til ES, så vi bruger AX som mellemmand). I DI gemmer vi pladens start-offset, dvs. adressen på pladens første byte. Nu er vi egentlig klar til at udføre stosb. Men det hjælper jo ikke meget kun at tømme det første felt på pladen. Vi skal gerne have tømt alle 42 felter! Her kan vi bruge rep-præfikset til stosb. rep står for repeat (gentag) og udfører altså stosb-instruktionen mere end én gang. Hvor mange gange den skal gentages angiver vi i CX, så vi flytter 42 (6*7) ind i CX så vi får tømt alle felterne. Nu kan man undre sig over om man på denne måde ikke bare skyder en masse +'er ind på den samme plads. ES:DI peger jo stadig på pladens første felt, ikke? Men sådan foregår det ikke. rep sørger nemlig for at forhøje DI med 1 for hver gentagelse. Inden vi begynder at ``skyde'' +'er, udfører vi kommandoen cld (Clear Direction Flag). Det sætter flaget DF til 0. Når DF er 0, så vil rep forhøje DI med 1 for hver gentagelse. Er DF lig med 1, så vil rep formindske DI med 1 for hver gentagelse, og på den måde gå ``tilbage'' i hukommelsen.

Når alle pladens felter er sat lig med ASCII-værdien for +, så er clrBoard færdig med arbejdet, og kontrollen kan vende tilbage til det kaldende program.

15.4  Draw Screen

Denne rutine kaldes hver gang skærmbilledet skal opdateres. Vi starter med at kalde rutinen clrScreen som rydder skærmen. De næste fem linjer er her:
     mov  dl,60          ; X=60
     mov  dh,0           ; Y=0
     call gotoXY         ; Placér markør.
     mov  dx,streng1     ; Skriver "9: Afslutter spillet" i
     call write          ;   skærmens øverste højre hjørne.
Vi kalder vores rutine gotoXY som kræver at vi gemmer koodinaterne for vores ønskede position i DL og DH. Vi skal have skrevet en besked om hvordan man afslutter spillet i højre hjørne. Den besked har vi defineret i datasegmentet og markeret starten af den med navnet streng1. For at kunne udskrive strengen, skal start-adressen ligge i DX, så vi kopierer den derover. Bemærk her at vi ikke bruger kantede parenteser da vi netop skal bruge adressen på strengen inden vi kalder vores write-rutine.
     mov  dx,streng2     ; Skriver "    1234567"
     call write
Her skriver vi en række numre således at hver kolonne har et nummer mellem 1 og 7. Vi rykker også lige et par mellemrum ind på skærmen så det kommer til at se lidt pænt ud.

Nu skal vi have ``tegnet'' vores plade. Da vi gemmer ASCII-værdierne +, X og O direkte i hukommelsen, kan vi skrive dem direkte på skærmen. Men vi kan ikke bruge DOS-funktion 9: Print String da den forventer at strengen afsluttes med et $. Vi vælger i stedet at bruge DOS-funktion 40h: Write To File. Den forventer at vi gemmer et file handle i BX. Et file handle er et nummer som repræsenterer en åben fil. Nummer 1 er dog Standard Output, og hvis vi skriver til ``filen'' med file handle 1, så skriver vi faktisk til skærmen! Så vi sætter BX=1:
     mov  bx,1           ; File Handle=1 (Standard Output)
     mov  cx,7           ; Skriv 7 byte.
DOS-funktion 40h skal selvfølgelig også vide hvornår den skal slutte med at skrive tegn. Den forventer at antallet af byte som skal skrives er gemt i CX. Vi skal udskrive en række ad gangen, og vores plade har 7 kolonner, så vi gemmer 7 i CX.
     mov  si,0           ; Start med pladens første række.

printRow:
     mov  dx,plade       ; Gem pladens start-offset i DX.
     add  dx,si          ; Gem rækkenr. i DX.

     mov  ah,40h         ; DOS-funktion 40h: Write to File
     int  21h
Vi sætter SI til at være 0. SI skal holde styr på hvilken række vi skal til at udskrive. Vi starter med den første række. DOS-funktion 40h forventer at start-adressen på de data der skal udskrives, ligger i DS:DX. Så vi sætter DX til at være lig med den adresse som plade peger på. Herefter lægger vi indholdet af SI til DX og gemmer resultatet i DX (add-instruktionen). Når SI er 0, så er DX stadig lig med den adresse som plade peger på som jo er første felt (og første række) på pladen. Vi kalder nu funktion 40h som udskriver 7 byte (pladens første 7 felter, dvs. den første række) på skærmen.
     mov  dx,streng3     ; Linjeskift + "    "
     call write
Når det er gjort, skifter vi linje og skriver 4 mellemrum igen.
     add  si,7           ; Gå til næste række
     cmp  si,42          ;   hvis der er flere...
     jne  printRow
Vi lægger 7 til SI. Hvis SI var 0 i forvejen bliver SI nu 7. Når vi nu kører rutinen igen fra printRow:, så bliver DX lig med plade+7, og peger således på anden række (se kortet over pladen). Inden vi haster videre med at udskrive næste række, skal vi dog lige sørge for at teste om SI er blevet lig med 42 hvilket betyder at vi er klar til at udskrive række nr. 7. Da vi kun har 6 rækker, er det en dårlig en idé at udskrive række 7. Så vi tester om SI er lig med 42. Hvis det ikke er tilfældet (Jump If Not Equal), kan vi roligt springe til printRow:. Hvis SI er lig med 42, går vi videre til næste instruktion:
     mov  dx,nylinje     ; Linjeskift.
     call write

     ret
Vi udskriver et linjeskift så der bliver lidt afstand mellem pladen og den efterfølgende tekst.

15.5  Get Move

getMove:
     mov  dx,streng4     ; Skriver "Spiller "
     call write
getMove-rutinen bruger vi til at hente spillerens træk. Spilleren skal udføre sit træk ved at indtaste et tal mellem 1 og 7 for at angive hvilken kolonne (eller søjle i stativet) som han eller hun vil lade sin brik falde ned i. De første to linjer udskriver den streng som streng4 peger på, dvs. "Spiller ". Bagefter skal vi have skrevet spillerens nummer. ASCII-værdien for nummeret står i [spiller], dvs. at det står i den adresse som spiller peger på. Da vi bruger ASCII-værdier til at holde styr på hvis tur det er, kan vi udskrive indholdet direkte på skærmen. Vi bruger her DOS-funktion 2: Print Character:
     mov  dl,[spiller]   ; Skriver spillerens nr.
     mov  ah,02h         ; DOS-funktion 02h: Print Character
     int  21h
DOS-funktion 2 udskriver et enkelt tegn på skærmen. ASCII-koden for tegnet skal ligge i DL, så vi lægger indholdet af den adresse som spiller peger på ([spiller]) ind i DL.

Herefter laver vi en understregning så det kommer til at se pænt ud.
     mov  dx,streng5     ; Skriver "---------".
     call write
Vi skriver en prompt så spilleren kan se at der nu skal indtastes et træk:
prompt:
     mov  dx,streng6     ; Skriver "Indtast træk: "
     call write
Vi skal nu have læst et tegn fra tastaturet. Det gør vi vha. DOS-interrupt 16h som giver os adgang til nogle keyboard services. Når vi kalder DOS-interrupt 16h med int 16h, skal vi i AH angive hvilken service vi ønsker at benytte os af. I vores tilfælde skal vi have læst et tegn fra tastaturet. Det er Keyboard Service 0: Get Key From Keyboard Buffer:
     mov  ah,0           ; Keyboard Service 00h: Get Key From Buffer
     int  16h
Keyboard Service 0 returnerer ASCII-koden for det indtastede tegn i AL. Vi skal nu sikre os at det indtastede er lovligt i vores spil. Vi har allerede skrevet en besked på skærmen om at et tryk på 9-tasten afslutter spillet. Så det tester vi for her:
     cmp  al,'9'         ; 9 slutter spillet.
     je   end
Vi husker at vi har med ASCII-koder at gøre, og derfor sætter vi apostroffer omkring 9-tallet for at angive ASCII-værdien for tegnet 9. AL indeholder fra Keyboard Service 0 ASCII-værdien for det som spilleren indtastede. Det sammenligner vi så med ASCII-værdien for 9 (cmp al,'9'). Hvis disse to værdier er ens (Jump If Equal), så har spilleren indtastet 9, og ønsker at afslutte spillet; vi hopper til end-rutinen. Hvis de to værdier er forskellige fra hinanden, går computeren videre til næste instruktion. Det er en ny test:
     cmp  al,'1'         ; Tester for mindre end 1.
     jl   badKey
Denne gang tester vi med ASCII-værdien for tegnet 1. Hvis vi slår op i en ASCII-tabel, kan vi se at tegnet 1 har ASCII-værdien 49. Alle de tegn hvis ASCII-værdi er mindre end 49 er derfor ikke et af vores lovlige tal. Vi kan derfor bruge jl (Jump If Less). Hvis spilleren har indtastet et tegn hvis ASCII-værdi er mindre end 49, hopper vi til badKey som ligger længere nede i getMove-rutinen. Vi skal også have afskaffet alle de tegn, som har en ASCII-værdi på over 55 som er ASCII-værdien for tegnet 7. Det gør vi med jg (Jump If Greater):
     cmp  al,'7'         ; Tester for større end 7.
     jg   badKey
Hvis vi er kommet så langt som hertil, er vi nu sikre på at spilleren har tastet et tal mellem 1 og 7, og AL indeholder en værdi mellem 49 og 55 (ASCII-værdierne for tegnene 1 til 7). Vi skal nu finde ud af om trækket er gyldigt. Hvis spilleren f.eks. vælger at smide en brik i søjle 3, og søjle 3 allerede er fyldt op, så er trækket ugyldigt. Vi skal altså have testet pladens øverste række (følg med på skemaet over pladen):
     sub  al,49               ; Konverterer ASCII-koden
                              ;   til en værdi mellem 0 og 6.
     mov  bx,plade            ; Gem pladens start-offset i BX.
     mov  ah,00               ; Sørger for at high byte ikke ændrer værdien.
     mov  di,ax               ; Gem trækket i DI.
     cmp  BYTE [bx+di],'+'    ; Hvis det _ikke_ er et +, så er
     jne  illegalMove         ;   trækket ugyldigt.

     ret
Vi starter med at trække 49 fra det tal som ligger i AL. Vi husker at AL indeholder et tal mellem 49 og 55. Ved at trække 49 fra tallet, får vi et tal mellem 0 og 6 som er noget mere hensigtsmæssigt i de efterfølgende instruktioner. Vi gemmer plade i BX. BX indeholder nu adressen for felt 0, dvs. pladens øverste venstre hjørne. Vi vil gerne have gemt AL i DI (forklaringen på dette kommer om lidt). Vi husker her at DI er et 16-bit-register, mens AL er et 8-bit-register, eller rettere: den lave halvdel af 16-bit-registret AX. Vi kan derfor ikke lægge AL over i DI. I stedet gemmer vi et 0 i AH (AX's øvre halvdel) og så indeholder AX jo værdien AL (vores oprindelige værdi i AL har bare fået foranstillet 8 nuller). Nu kan vi flytte AX over i DI da de begge er 16-bit-registre. Grunden til denne ``register-akrobatik'' er at vi med instruktionen cmp BYTE [bx+di],'+' nu kan teste lige præcis det rigtige felt. BX indeholder pladens første felt, og når vi lægger DI (som har en værdi mellem 0 og 6) til, får vi det rigtige felt. Vi tester så om det indeholder ASCII-værdien for tegnet + som i vores spil repræsenterer et tomt felt. Gør det ikke det, er feltet allerede optaget, og hvis det øverste felt i en søjle er optaget, så er hele søjlen fyldt op, og trækket er ugyldigt. Så vi hopper til rutinen illegalMove som ligger længere nede i getMove-rutinen.
badKey:
     mov  dx,streng7     ; Skriver "Du skal indtaste..."
     call write
     jmp  prompt
Her kommer vi til hvis spilleren har indtastet noget som ikke var et tal mellem 1 og 7 (vi har allerede testet for 9 som afslutter spillet), og vi skriver her en meddelelse til spilleren om at indtaste et tal mellem 1 og 7. Så hopper vi tilbage til prompt:, og venter på en ny indtastning.
illegalMove:
     mov  dx,streng8     ; Skriver "Ugyldigt træk!"
     call write
     jmp  prompt
Her ender vi hvis spillerens træk er ugyldigt, dvs. at der ikke er plads til flere brikker i den ønskede søjle. Spilleren får besked og vi hopper tilbage til prompt:.

15.6  Make Move

Vi har nu fået spillerens træk fra getMove-rutinen og sikret os at det er gyldigt. Trækket er et tal mellem 0 og 6 og blev gemt i AX hvor det stadig ligger når vi kommer til makeMove-rutinen. Det der skal ske her, er at vi skal have opdateret pladen i hukommelsen med det nye træk. Det nye træk bliver senere udskrevet på skærmen når den bliver opdateret i drawScreen-rutinen.
makeMove:
    mov  bx,plade            ; Gem pladens start-offset i BX.
    add  bx,35               ; Hop til pladens nederste række.
    mov  di,ax               ; Gem træk (kolonne) i DI.
                             ;   AL blev lig med træk i getMove.
Vi gemmer pladens start-offset (felt 0) i BX. Herefter lægger vi 35 til værdien så vi kommer ned på pladens nederste række (se igen skemaet over pladen). Så lægger vi trækket fra AX ind i DI. Adressen som BX+DI peger på, er nu det nederste felt i den ønskede søjle. Vi skal nu teste om dette felt er tomt så vi kan lægge en brik i det:
itIsNot:
    cmp  BYTE [bx+di],'+'    ; Tester om feltet er tomt (et +).
    je   itIs
    sub  bx,7                ; Hop op til næste række.
    jmp  itIsNot
Her tester vi så for vores +. Hvis der er et + i det pågældende felt, så er feltet tomt, og vi hopper til itIs-rutinen længere nede i makeMove-rutinen. Hvis feltet ikke er tomt, så trækker vi 7 fra BX. På den måde kommer vi til rækken lige oven over den nuværende. Hvis det nuværende felt f.eks. er felt 37, så trækker vi 7 fra og befinder os så på felt 30 som er feltet lige ovenover (se skemaet igen). Vi tager så testen endnu engang (jmp itIsNot) for at se om dette felt så er tomt, og fortsætter så indtil vi finder søjlens nederste tomme felt. Husk på at vi allerede har testet om der overhovedet er et tomt felt i søjlen. Det gjorde vi i getMove-rutinen. Vi ved altså at der er et tomt felt, spørgsmålet er bare hvor det er. Når vi finder det, skal vi have placeret en brik i feltet:
itIs:
    cmp  BYTE [spiller],'1'  ; Er det spiller 1's træk?
    jne  player2
    mov  BYTE [bx+di],'X'    ; Læg spiller 1's brik (X) på pladen.

    ret

player2:
    mov  BYTE [bx+di],'O'    ; Læg spiller 2's brik (O) på pladen.

    ret
Vi tjekker om det er spiller 1 der har turen. I så fald skal spiller 1's brik lægges på det tomme felt som vi lige har fundet. Det gør vi ved at lægge ASCII-værdien for tegnet X ind på den adresse som BX+DI peger på. Er det spiller 2's tur, springer vi ned til player2: og lægger ASCII-værdien for tegnet O ind på den rette plads.

15.7  Check Status

Vi kommer nu til den sidste af de store underrutiner som får spillet til at fungere. Det er den rutine som efter hvert træk skal teste om der er 4 på stribe eller om spillet er uafgjort (pladen er fyldt op uden nogen vinder). Det er den største og mest komplicerede rutine, men lad os se på den stykke for stykke:
checkStatus:
     cmp  BYTE [spiller],'1'  ; Er det spiller 1's tur?
     je   yes
     mov  BYTE [spiller],'O'  ; Nej, læg spiller 2's brik i spiller.
     jmp  horizInit

yes:
     mov  BYTE [spiller],'X'  ; Ja, læg spiller 1's brik i spiller.
Vi skal have testet vores plade på kryds og tværs efter de rigtige kombinationer. Vores plade består af +'er, X'er og O'er. [spiller] indeholder enten tegnet 1 eller 2 alt efter hvis tur det er. Her er det hensigtsmæssigt at erstatte 1 eller 2 med X eller O så vi kan sammenligne pladens felter med [spiller]. Det gør vi ved at teste om [spiller] har ASCII-værdien for tegnet 1. Hvis det er tilfældet, hopper vi til yes: som erstatter '1' med 'X'. Hvis [spiller] ikke indeholder '1', så indeholder det ASCII-værdien for tegnet 2, og vi kan så gemme 'O' i [spiller] og hoppe over yes: vha. jmp horizInit.
horizInit:
     mov  bx,plade            ; Gem pladens start-offset i BX.
     mov  al,[spiller]        ; Flyt spillerens brik ind i AL til testning.

     mov  di,0                ; Start i pladens øverste venstre hjørne.
     mov  cx,0
Vi skal nu til at teste om der er fire på stribe vandret---et eller andet sted på pladen. Vi gemmer plade's start-offset (felt 0) i BX. Vi skal til at teste det område i hukommelsen som indeholder vores plade, og da det ikke er muligt at sammenligne memory-data med memory-data, flytter vi [spiller] ind i AL. På den måde kommer vi til at sammenligne memory-data med register-data.

Vi tager et kig på skemaet over pladen, og finder ud af at hvis spilleren har en brik i felt 0, så skal spilleren også have brikker i felt 0+1, felt 0+2 og felt 0+3 for at have fire på stribe vandret. Hvis det er tilfældet, er spillet vundet. Hvis ikke, tager vi felt 1 som udgangspunkt. Så skal spilleren have brikker i felt 1, felt 1+1, felt 1+2 og felt 1+3 for at have 4 på stribe vandret. Sådan fortsættes indtil vi kommer til felt 4. Her ser vi at felt 4+3 er lig med felt 7, som jo ligger på næste række. Vi skal altså ikke bruge felt 4, 5 og 6 som udgangspunkt for testning. Jeg har trukket en lodret linje på kortet på side 9 mellem søjle 3 og søjle 4 (den første søjle er søjle 0). Vi skal altså have gennemløbet en løkke hvor alle felter på venstre side af den optrukne linje, tur efter tur, bliver brugt som udgangspunkt for en test af om der fire på stribe vandret.

Da BX indeholder adressen på felt 0, bruger vi DI til at gemme nummeret for feltet som skal være udgangspunkt. CX bruger vi til at sørge for at vi holder os på venstre side af den optrukne streg.
horiz:
     cmp  [bx+di],al
     jne  not1
     cmp  [bx+di+1],al
     jne  not1
     cmp  [bx+di+2],al
     jne  not1
     cmp  [bx+di+3],al
     jne  not1
     jmp  gameWon             ; Der er 4 på stribe vandret!
Her går testen løs. Vi sammenligner felterne med AL som indeholder den aktuelle spillers brik. Hvis bare ét felt ikke indeholder samme værdi som AL, så har spilleren ikke en brik i dette felt, og spilleren har ikke fire på stribe i dette tilfælde. Så springer vi til not1:. Hvis vi kommer helskindet igennem testen, så er der fire på stribe vandret, og vi springer til gameWon: som ligger længere nede i checkStatus-rutinen.
not1:
     inc  di                  ; Næste felt.
     inc  cx

     cmp  di,39               ; Er vi færdige med at tjekke vandret?
     je   vertInit            ; Ja, fortsæt med lodret.

     cmp  cx,4                ; Bevæger vi os over i 3. zone?
     je   newRow1             ; Ja, næste række.

     jmp  horiz               ; Nej, fortsæt med næste felt.

newRow1:
     add  di,3                ; Næste række.
     mov  cx,0

     jmp  horiz               ; Gennemløb løkken igen.
Hvis der ikke var fire på stribe, er vi klar til at tage næste felt som udgangspunkt for en test. Vi forhøjer DI med 1 (inc di), og vi forhøjer CX med 1. Hvis DI nu er lig med 39, så er vi kommet ned til bunden af pladen og står lige til højre for den optrukne linje (se skemaet), og vi kan hoppe videre til vertInit: hvor vi tester for fire på stribe lodret. Ellers går vi videre og tester om CX er lig med 4. Hvis det er tilfældet, så er vi kommet over på den højre side af stregen på kortet, og vi skal hoppe til næste række. Det gør vi i newRow1: hvor vi lægger 3 til DI som så vil starte på en ny række, og vi sætter CX tilbage til 0 så den kan holde øje med stregen på kortet igen. Vi hopper så tilbage og tester for fire på stribe vandret igen. Denne gang med DI's nye værdi som udgangspunkt.

Hvis der ikke var fire på stribe vandret, går programmet videre og tester for fire på stribe lodret. Vi nulstiller DI så vi igen starter med pladens øverste venstre hjørne som udgangspunkt:
vertInit:
    mov  di,0                ; Start i pladens øverste venstre hjørne.
Hvis vi igen kigger på kortet og siger at felt 0 er udgangspunkt, så skal den aktuelle spiller have en brik i felt 0, felt 0+7, felt 0+14 og felt 0+21 for at have 4 på stribe lodret. Jeg har trukket en vandret linje på kortet, og når vi kommer under denne linje, er der ikke flere muligheder for at have fire på stribe lodret. Det fungerer altså i princippet på samme måde som testen af fire på stribe vandret. Dog er der her den fordel at vi bare forhøjer DI med 1 for hver gennemkørsel indtil DI er lig med 21, og vi slipper for at skulle rode med CX:
vert:
    cmp  [bx+di],al
    jne  not2
    cmp  [bx+di+7],al
    jne  not2
    cmp  [bx+di+14],al
    jne  not2
    cmp  [bx+di+21],al
    jne  not2
    jmp  gameWon             ; Der er 4 på stribe lodret!

not2:
    inc  di                  ; Næste felt.

    cmp  di,21               ; Er vi færdige med at tjekke lodret?
    je   diag1Init           ; Ja, fortsæt med diagonalt.

    jmp  vert                ; Nej, fortsæt med næste felt.
Hvis DI er lig med 21, har vi testet for alle muligheder for fire på stribe lodret, og vi kan gå videre til diag1Init:, som tester for fire på stribe diagonalt.

Når vi skal teste for fire på stribe diagonalt, deler vi pladen op i tre ``zoner''. Zonerne afgrænses af de to optrukne streger på skemaet. Zone 1 er det areal der er øverst til venstre. Zone 2 er arealet der er nederst til venstre. Zone 3 er hele området til højre for den lodrette streg.

Vi starter med at teste zone 1 for fire på stribe diagonalt, dvs. at alle felterne i zone 1 skal bruges som udgangspunkt i test. Når vi starter med felt 0, så skal den aktuelle spiller have brikker i felt 0, felt 0+8, felt 0+16 og felt 0+24 for at have fire på stribe diagonalt. Vi sætter DI til 0 (felt 0) og CX til 0 da vi skal holde øje med at vi ikke bevæger os over i zone 3:
diag1Init:
    mov  di,0                ; Start i pladens øverste venstre hjørne.
    mov  cx,0

diag1:
    cmp  [bx+di],al
    jne  not3
    cmp  [bx+di+8],al
    jne  not3
    cmp  [bx+di+16],al
    jne  not3
    cmp  [bx+di+24],al
    jne  not3
    jmp  gameWon             ; Der er 4 på stribe diagonalt!
Hvis testen ikke resulterer i at der er fire på stribe, så går vi videre med næste felt:
not3:
    inc  di                  ; Næste felt.
    inc  cx

    cmp  di,18               ; Er vi færdige med at tjekke 1. zone?
    je   diag2Init           ; Ja, fortsæt med 2. zone.

    cmp  cx,4                ; Bevæger vi os over i 3. zone?
    je   newRow3             ; Ja, næste række.

    jmp  diag1               ; Nej, fortsæt med næste felt.

newRow3:
    add  di,3                ; Næste række.
    mov  cx,0

    jmp  diag1               ; Gennemløb løkken igen.
Det her skulle gerne minde dig om testen for fire på stribe vandret. Vi forhøjer DI og CX med 1. Hvis DI herefter er lig med 18, så er vi færdige med at teste zone 1 for fire på stribe diagonalt, og hopper videre til diag2Init: som tester zone 2. Hvis CX er lig med 4, så er vi kommet over i zone 3, og vi hopper ned til newRow3: som lægger 3 til DI så vi kommer ned til starten af næste række, og CX sættes tilbage til 0 så den er klar til at holde øje med zonegrænsen igen.

Hvis der ikke var fire på stribe diagonalt i zone 1, går vi til zone 2. Vi sætter her DI til at være lig med 21 som er første felt i zone 2. Vi nulstiller også CX.
diag2Init:
    mov  di,21               ; Start i række 3 (3*7=21).
    mov  cx,0
Her i zone 2 skal vi teste på en lidt anden måde end i zone 1 hvor vi testede for diagonalt ``nedad mod højre''. I zone 2 skal vi teste for diagonalt ``opad mod højre''. Hvis vi f.eks. står i felt 21, så skal den aktuelle spiller have brikker i felt 21, felt 21-6=15, felt 21-12=9 og felt 21-18=3 for at have fire på stribe diagonalt:
diag2:
    cmp  [bx+di],al
    jne  not4
    cmp  [bx+di-6],al
    jne  not4
    cmp  [bx+di-12],al
    jne  not4
    cmp  [bx+di-18],al
    jne  not4
    jmp  gameWon           ; Der er 4 på stribe diagonalt!
Hvis der ikke er fire på stribe, fortsættes med næste felt:
not4:
    inc  di
    inc  cx

    cmp  di,39               ; Er vi færdige med at tjekke 2. zone?
    je   tieInit             ; Ja, fortsæt med test for uafgjort.

    cmp  cx,4                ; Bevæger vi os over i 3. zone?
    je   newRow4             ; Ja, næste række.

    jmp  diag2               ; Nej, fortsæt med næste felt.

newRow4:
    add  di,3                ; Næste række.
    mov  cx,0

    jmp  diag2               ; Gennemløb løkken igen.
Vi forhøjer DI og CX med 1. Hvis DI herefter er blevet 39, er vi kommet over i zone 3 og kan springe videre til tieInit: som tester om pladen er fyldt op og spillet dermed er uafgjort. Vi tester om CX er 4 og hopper i så fald til newRow4: som lægger 3 til DI og dermed peger på næste rækkes første felt på pladen.

Hvis der ikke har været fire på stribe overhovedet, skal vi teste om pladen er fyldt, og spillet dermed er uafgjort. Det kræver kun at vi tester pladens øverste række:
tieInit:
    mov  di,0

tie:
    cmp  BYTE [bx+di],'+'    ; Er der flere tomme felter?
    je   notTie              ; Ja, fortsæt spillet.

    inc  di
    cmp  di,7                ; Er sidste felt nået?
    je   gameTie             ; Ja, spillet er uafgjort.
    jmp  tie                 ; Nej, gennemløb løkken igen.

notTie:
    ret
Vi sætter DI til 0 (pladens øverste venstre hjørne). Så tester vi om feltet er tomt (så vil der være et '+'). Hvis det er tilfældet, så er pladen endnu ikke fyldt og vi hopper til notTie: som vender tilbage til hovedrutinen og fortsætter spillet. Hvis feltet ikke var tomt, forhøjer vi DI med 1 så det peger på næste felt. Hvis DI er lig med 7, så er vi kommet ned på række 2 uden at finde et tomt felt i øverste række og spillet er dermed uafgjort. Vi hopper derfor til gameTie:.

Som du kan se, så skal computeren igennem rigtig mange ting hver eneste gang en spiller har udført et træk, men det hele sker på langt under et sekund (medmindre du har en ufattelig langsom computer).
gameTie:
    call drawScreen
    mov  dx,strengB
    call write
    call end
Denne rutine kommer vi til hvis spillet er uafgjort. Vi kalder drawScreen-rutinen så spillerne kan se at det sidste træk blev udført. Derefter udskriver vi en besked om resultatet på skærmen, og til sidst kalder vi end-rutinen som afslutter programmet.
gameWon:
    cmp  al,'X'              ; Har spiller 1 vundet?
    jne  player2won

    call drawScreen
    mov  dx,streng9          ; Ja.
    call write
    call end
Hvis en spiller har fået fire på stribe, kommer vi hertil. Vi tester om det var spiller 1 der vandt ved at sammeligne AL med 'X'. Vi husker at AL har indeholdt den aktuelle spillers brik gennem checkStatus-rutinen. Hvis det ikke er tilfældet, så var det spiller 2 der vandt, og der hoppes til player2Won:. Ellers opdateres skærmbilledet så vi kan se det træk der afgjorde spillet. Der skrives en besked om resultatet på skærmen og end-rutinen kaldes.
player2won:
    call drawScreen
    mov  dx,strengA          ; Nej, spiller 2 vandt.
    call write
    call end
Her ender vi hvis det var spiller 2 der vandt spillet. Skærmbilledet opdateres, der skrives en besked på skærmen og programmet afsluttes.

15.8  Optimering af programmet

Hvis vi kompilerer og linker programmet, får vi en .exe-fil ud af det som fylder 918 byte. Det er jo i sig selv en meget lille fil nu om dage, men den kan blive endnu mindre! Vi kan jo starte med at konvertere det til Real Mode Flat Model og dermed lave en .com-fil i stedet. Jeg vil give lidt grundlæggende tip om dette, og overlade resten af det til dig som en øvelse.

Start med at tage et kig på det lille program i starten af afsnit 8. Som du kan se, så kan du starte med at slette hele stak-segmentet i vores program.

Du skal også huske at en .com-fil bruger de første 256 byte i det segment som du får stillet til rådighed. Derfor indsætter du direktivet [ORG 100h] i starten af programmet.

Et program i Real Mode Flat Model eksisterer kun inden for ét segment, så derfor skal du ikke opdele programdelene i segmenter, men i sektioner. Kode-delen skal markeres som [SECTION .text]. Datadelen skal markeres som [SECTION .data]. I den forbindelse kommer her noget som jeg ikke har været inde på før. I sektionen .data skal du definere dine initialiserede data, dvs. de data som du giver en værdi med det samme (f.eks. fast definerede strenge). Dine ikke-initialiserede data skal være i en sektion som du markerer med [SECTION .bss]. Her skal du definere de områder i hukommelsen som du reserverer med f.eks. resb. I vores tilfælde skal plade altså defineres i denne sektion.

Vi skal slette de instruktioner som har med segmentregistre at gøre idet hele vores program ligger i samme segment. Vi kan altså fjerne de første fem instruktioner i programmet. Det er dem som initialiserer segmenterne i Real Mode Segmented Model. I rutinen clrBoard har vi også to instruktioner som behandler DS og ES. Disse instruktioner slettes også.

Til sidst skal vi slette etiketten ..start:.

Hvis vi nu kompilerer til en .com-fil, kommer vores program til fylde 780 byte, altså 138 byte mindre end før. Vores lille program kan allerede ligge på én almindelig 1.44MB-diskette langt over tusind gange, så måske er der ikke nogen grund til at anstrenge sig for at gøre det mindre. Men du kan godt gøre det, for programkoden er ikke rigtigt optimeret. Hvis du ser den grundigt efter, kan du måske finde nogle instruktioner som egentlig er overflødige. For eksempel hvis man initialiserer et register som allerede har den rigtige værdi fra en anden rutine, eller j??-instruktioner der kunne være placeret mere hensigtsmæssigt.

Med det afsluttende eksempel slutter også denne artikel, men der er gode muligheder for at lære mere på internettet. Du kan evt. starte din tur med at lægge vejen forbi siden www.programmersheaven.com hvor der er en hel sektion om assembler-programmering med artikler, eksempler og diskussionsfora.

A  Appendiks

I dette appendiks kan du finde en oversigt over CPU'ens registre, flagene og hvad man kalder de forskellige mængder af hukommelse. Jeg har også samlet de mest interessante Video Services, Keyboard Services og DOS-funktioner. Du kan sikkert finde langt mere udførlige oversigter på internettet, men de er muligvis også mere uoverskuelige. En ting du lige skal bide mærke i, er at en ASCIIZ-streng er en række ASCII-værdier som ender med værdien 0---ikke ASCII-værdien for tegnet 0.

A.1  CPU'ens registre

Segmentregistre
CS Code Segment
DS Data Segment
SS Stack Segment
ES Extra Segment



Andre registre
AX (AH,AL)
BX (BH,BL) (DS:BX)
CX (CH,CL)
DX (DH,DL)
 
SI Source Index (DS:SI)
DI Destination Index (DS:DI)
 
BP Base Pointer (SS:BP)
SP Stack Pointer (SS:SP)
IP Instruction Pointer (CS:IP)



FLAG-registret:

    Set-symbol Clear-symbol
Flag Navn i DEBUG i DEBUG
OF Overflow Flag OV NV
DF Direction Flag DN UP
IE Interrupt Enable Flag EI DI
TF Trap Flag - -
SF Sign Flag NG PL
ZF Zero Flag ZR NZ
AF Auxiliary Flag AC NA
PF Parity Flag PE PO
CF Carry Flag CY NC

A.2  Udtryk for mængder af hukommelse

    Antal     Værdi-  
Navn   byte     mængde  
  Dec   Hex Dec   Hex
Byte 1   01h 0-255   00h-FFh
Word 2   02h 0-65.535   00h-FFFFh
Double Word 4   04h 0-4.294.967.295   00h-FFFFFFFFh
Quad Word 8   08h -   -
Paragraph 16   10h -   -
Page 256   100h -   -
Segment 65536   10000h -   -

A.3  BIOS-interrupt 10h: Video Display Services

Gem nummeret på den ønskede service i AH før brug af INT 10h.

VIDEO Service 02h: Position Cursor

Gem et 0 i BH.
Gem X-koordinat i DL.
Gem Y-koordinat i DH.

VIDEO Service 06h: Initialize/Scroll

Gem X-koordinat for øverste venstre hjørne i CL.
Gem Y-koordinat for øverste venstre hjørne i CH.
Gem X-koordinat for nederste højre hjørne i DL.
Gem Y-koordinat for nederste højre hjørne i DH.
Gem antallet af linjer der skal scrolles i AL. Gem 0 for at slette.
Gem display attribute i BH (07h er det normale).

VIDEO Service 1Ah: PS/2 Identify Adapter

Gem et 0 i AL.

Returnerer: AL=1Ah hvis en PS/2 BIOS er til stede (VGA eller MCGA).
  Hvis ikke -- så er der ikke en VGA- eller MCGA-skærm.
  BL=Display Adapter Code hvis PS/2 BIOS er til stede.

A.4  DOS-interrupt 16h: Keyboard Services

Gem nummeret på den ønskede service i AH før brug af INT 16h.

Keyboard Service 00h: Get Key From Keyboard Buffer

Hvis der ikke er et tegn i bufferen, venter computeren til der er.
Returnerer SCAN-kode i AH.
Returnerer ASCII-kode i AL.

Keyboard Service 01h: Checks to see if a key is ready to grab

Sætter Zero Flag hvis en taste er klar til grab.
Grab den med Keyboard Service 00h.
Returnerer SCAN-kode i AH.
Returnerer ASCII-kode i AL.

Keyboard Service 02h: Returns shift flags in AL

Bit 7: Insert er aktiv
Bit 6: Caps Lock er aktiv
Bit 5: Num Lock er aktiv
Bit 4: Scroll Lock er aktiv
Bit 3: Alt er nedtrykket
Bit 2: Ctrl er nedtrykket
Bit 1: Venstre Shift er nedtrykket
Bit 0: Højre Shift er nedtrykket

Keyboard Service 03h: Typematic Rate and Delay

Gem 5 i AL.
Gem Delay Value i BH (0-3: 250, 500, 750, 1000 millisekunder).
Gem Typematic Rate i BL (0-1Fh). 1Fh = langsomst (2 tegn/sek), 0 = hurtigst (30 tegn/sek)

Keyboard Service 05h: Stuff Keyboard

Gem SCAN-kode i CH.
Gem ASCII-kode i CL.

Returnerer: 0 = Ingen fejl
  1 = Keyboard-buffer er fuld

Keyboard Service 12h: Returns shift flags in AL and AH

Fungerer ligesom Keyboard Service 02h, men AH har flere informationer:
Bit 7: SysRq er nedtrykket
Bit 6: Caps Lock er aktiv
Bit 5: Num Lock er aktiv
Bit 4: Scroll Lock er aktiv
Bit 3: Højre Alt er aktiv
Bit 2: Højre Ctrl er aktiv
Bit 1: Venstre Alt er aktiv
Bit 0: Venstre Ctrl er aktiv

A.5  DOS-interrupt 21h: DOS-funktioner

Gem den ønskede DOS-funktion i AH før brug af INT 21h.

Funktion 02h: Print Character

Gem tegnets ASCII-kode i DL.

Funktion 09h: Print String

Gem strengens adresse i DS:DX. Strengen skal slutte med et $ (ASCII 36/24h).

Funktion 3Ch: Create File

Gem fil-attributter i CL:

Bit 0: Read-Only
Bit 1: Hidden
Bit 2: System
Bit 3: Volume Label
Bit 4: Subdirectory
Bit 5: Archive
Bit 6: Reserveret
Bit 7: Reserveret


I DS:DX gemmes en pointer til en ASCIIZ-streng der indeholder filnavnet.

Returnerer: CF=1: Der opstod en fejl. Error Code gemmes i AX (stort set unødvendig).
  CF=0: Ingen fejl. File Handle gemmes i AX (skal bruges til at referere til filen).

Funktion 3Dh: Open File

Gem Open Mode i AL:

Bits 2-0: Access Code
  000: Read Only Access
  001: Write Only Access
  010: Read and Write Access


I DS:DX gemmes en pointer til en ASCIIZ-streng der indeholder filnavnet.
Returnerer: CF=1: Der opstod en fejl. Error Code gemmes i AX (stort set unødvendig).
  CF=0: Ingen fejl. File Handle gemmes i AX (skal bruges til at referere til filen).

Funktion 3Eh: Close File

Gem File Handle i BX.
Returnerer: CF=1: Der opstod en fejl.

Funktion 3Fh: Read From File

Gem File Handle i BX.
Gem antallet af byte der skal læses i CX.
DS:DX skal indeholde adressen hvor de læste data skal gemmes.
Returnerer: AX = Antallet af byte som blev læst.
  Hvis det er 0, så prøvede du at læse fra slutningen af filen.

Funktion 40h: Write To File

Gem File Handle i BX.
Gem antallet af byte der skal skrives i CX.
DS:DX skal indeholde adressen på de data som skal gemmes i filen.
Returnerer: AX = Antallet af byte som blev læst.
  Hvis tallet ikke er lig med det antal byte du ville skrive, så er der en fejl.

Funktion 41h: Delete File

I DS:DX gemmes en pointer til en ASCIIZ-streng der indeholder filnavnet.
Returnerer: CF=1: Der opstod en fejl. Error Code gemmes i AX:
    AX=2: File Not Found
    AX=3: Path Not Found
    AX=5: Access Denied
  CF=0: Ingen fejl.

Funktion 4Ch: Terminate Process

Gem evt. en ERRORLEVEL-værdi i AL
(det er god skik at gemme et 00h hvis ERRORLEVELS ikke bruges).

1
Læs ÅDL på http://www.linuxbog.dk/licens.html.
2
Læs mere om bogen på Jeff Duntemanns hjemmeside: http://www.duntemann.com
3
http://sourceforge.net/projects/nasm
4
http://alink.sourceforge.net/download.html
5
http://uk.geocities.com/rob_anderton
6
Se f.eks. http://www.asciitable.com

This document was translated from LATEX by HEVEA.