Liten nybôrjarassemblerskola fôr atari st/ste
Allra först några rader till dig som inte har använt något datorspråk förut. Du kan, såvida du inte har oändligt tålamod, sluta läsa här. Assembler är inget nybörjarspråk och även om du verkligen vill lära dig det har du nog större behållning av att lära dig ett lättare språk först.
Denna skola är alltså främst till för dig som har programmerat förut, kanske i någon BASIC-dialekt. Jag kom själv från BASIC för ungefär ett år sedan. Jag ska inte säga att jag är någon super- hacker, snarare tvärtom, men jag kan grunderna hyfsat och framför allt, jag tror att jag har ganska goda minnen av vad som var speciellt svårt i början. De flesta assemblerkurser har den svagheten att författarna inte vet vad en nybörjare i allmänhet tycker är svårt. Därför framställs allt mycket torrt och sakligt och inget område inom programmeringen täcks egentligen mer än ett annat för att det normalt sett är svårare att lära sig. Vanligtvis vet förmodligen heller inte författarna vad som är speciellt svårt.
De flesta assemblerkurser brukar i början ha ett avsnitt om varför man programmerar i assembler i stället för i lättare språk. Detta avsnitt är nästan identiskt för alla kurser och omfattar bl.a. att assembler är snabbare än andra språk, eftersom det ligger på nästan så låg nivå man kan komma och att det finns nästan obegränsade möjligheter. Jag skulle vilja inkludera lite personliga aspekter. Jag har märkt att jag kommer tillbaka till assembler hela tiden. BASIC kunde ibland bli riktigt tråkigt. I BASIC vet man inte riktigt vad som händer, utan det är främst till för att man ska nå resultat smidigt. Assembler är en riktig djupdykning i datorn och man måste dela upp problemen i dess minsta delar hela tiden. En annan anledning till att jag tycker det är roligare är på grund av möjligheterna. Det finns alltid maximal möjlighet att utveckla sina program. Detta är förstås bara mina personliga åsikter och jag vet inte hur många som delar dem.
Denna assemblerkurs är bara en bit på vägen men den innehåller (förhoppningsvis) tillräckligt många exempel för att du ska lära dig så pass mycket att du börjar känna dig hemmastadd i språket. Detta är nämligen en mycket viktig del i inlärningen och bara man känner sig säker på de enklaste sakerna så behöver man inte tänka på dem senare utan kan koncentrera sig på svårare saker.
För fortsatta studier rekommenderar jag varmt boken "Programmera 68000" av Steve Williams. Den är ursprungligen skriven på engelska men finns översatt till svenska. I den finns också en komplett beskrivning av instruktionsuppsättningen för Motorola 68000 (processorn som sitter i ST:n). En nödvändighet är också en referensbok med rutiner och dylikt. "Atari ST/STE Hårdfakta" av Magnus Zagerholm är den bästa bok jag vet i genren. Den innehåller alla fakta man kan önska sig och den är dessutom skriven på svenska.
För att kunna skriva assemblerprogram måste du ha en texteditor och en assembler. Det överlägset mest använda och i mitt tycke också bästa programpaketet är HiSofts DevPac 2. Detta paket innehåller allt du behöver; förutom editor och assembler även en debugger för felsökning. Rekommenderas varmt alltså.
En viktig skillnad från t.ex. BASIC är att koden som man skriver måste assembleras till exekverbar kod innan man kan köra programmet (i BASIC heter motsvarande företeelse kompilering). Du kan alltså inte bara skriva "run" för att köra programmet. Detta problem hade varit ganska bökigt om man hade varit tvungen att assemblera koden till disk för att sedan gå ur editorn och köra programmet varje gång man ville pröva. Nu är det emellertid så att i DevPac 2 finns möjligheten att assemblera koden till minnet. Detta är mycket smidigt och vid korta program tar det knappt någon tid alls. Man kan alltså skriva ett program, assemblera det till minnet, köra det och sedan gå ur programmet tillbaka till editorn och fortsätta skriva på det.
Först måste jag tjata lite om olika talsystem. Det må vara tråkigt men det är en smärre nödvändighet att förstå hur informationen representeras i en dator när man ska till att programmera assembler. Du kanske har hört talas om det binära, det decimala och det hexadecimala talsystemet. Datorn använder alltid det binära. Det decimala talsystemet är enklast att förstå för oss människor. Det är nämligen det vi använder till vardags, när vi räknar tråkiga mattetal, när vi mäter upp mjöl till pannkakan eller när vi läser rapporter från Statistiska Centralbyrån om hur mycket glass vi äter per år. Det "decimala" talsystemet heter som det gör eftersom det baserar sig på talet tio. Om du skriver några tal från noll och uppåt märker du att när du har räknat till nio och ska komma till tio klarar du dig inte längre med en siffra. Då får du ta till en siffra till. Det finns tio siffror i det decimala talsystemet, 0, 1, 2, 3, 4, 5, 6, 7, 8 och 9. Hur gör du när du skriver tio? Jo, du börjar helt enkelt om från noll och till vänster om den siffran sätter du en etta. 10 alltså! Inte så svårt, va? Och när du sedan kommer till nittionio och ska börja på etthundra så räcker siffrorna återigen inte till. Då tar du till en tredje siffra. Precis på samma sätt går det till i andra talsystem, bara det att i det binära finns det bara två siffror att tillgå och i det hexadecimala finns det så många som sexton.
Om vi tar ett exempel från det binära systemet: Du ska räkna från noll och uppåt. Först är det inte så svårt, 0 är 0 i alla talsystem. Sedan blir det 1. Men sedan då? När vi kommer till två? Det finns ju inga fler siffror i det binära systemet, så vi får helt enkelt ta och göra som vi gör med det decimala talsystemet; börja om från 0 och använda en till siffra. Värdet två i det binära talsystemet skriver man alltså 10. Tre blir 11, vid fyra får man ta till en siffra till och det blir följdaktligen 100. Fem blir 101, sex blir 110, sju blir 111 osv. Nu tycker du kanske att det blev en massa konstiga sifferkombinationer där på slutet men man gör ju i princip som vanligt: Går det inte att fylla på mer längst till höger får man börja om från 0 och istället fylla på till vänster om siffran. På samma sätt som en siffra i det decimala talsystemet är värd tio gånger mer än samma siffra till höger om den är en siffra i det binära talsystemet värd två gånger mer än samma siffra till höger. Ett exempel: Talet 437 i det decimala systemet är värt... tja, fyrahundratrettiosju som vi säger. 7:an betyder sju gånger ett, 3:an betyder tre gånger tio och 4:an betyder fyra gånger etthundra. I alla talsystem betyder siffran längst till höger siffrans värde gånger ett. Om vi tar ett binärt exempel, 101101, så gör vi ungefär på samma sätt, 1:an längst till höger betyder ett gånger ett. Siffran till vänster om den betyder noll gånger två. Nästa är ett gånger fyra, nästa ett gånger åtta, noll gånger sexton och ett gånger trettiotvå. Slår man ihop dessa värden får man 32+0+8+4+0+1=45. Fyrtiofem decimalt alltså. Det var väl inte Sè svårt?
Sedan finns också det hexadecimala talsystemet. Det använder man eftersom det är lite bökigt att skriva värden binärt. Det blir så många siffror. I det hexadecimala systemet finns sexton siffror. Hur ser då de ut? Jo, 0-9 är ju inga problem, men sedan finns ju inga "riktiga" siffror? Då har man beslutat sig för att använda bokstäverna A-F. A betyder tio, B betyder 11, C 12, D 13, E 14 och F betyder femton. Detta kan tyckas svårförståeligt i början, att man bara använder en siffra till ett värde som man normalt skriver med två siffror. Eftersom det finns sexton siffror har en siffra i detta hexadecimala talsystem sexton gånger större "vikt" än siffran som står till höger om den. Ett litet exempel kanske skulle sitta fint: Talet A7E hexadecimalt betyder alltså fjorton gånger ett + sju gånger sexton + tio gånger tvåhundrafemtiosex, vilket blir 2686 decimalt. Varför använder man just det hexadecimala systemet i stället för t.ex. det decimala som är så mycket enklare? En anledning är att man med en hexadecimal siffra kan representera exakt lika många olika värden (16) som med fyra binära siffror. Det är mycket behändigt när man t.ex. ska omvandla värden mellan talsystemen. Vi kan ta hex-talet A7E igen.
Eftersom en hexadecimal siffra som sagt rymmer lika många värden som fyra binära kan vi helt enkelt ta och dela upp talet i dess siffror. A betyder tio, och binärt är det, hmmm, hrrm, hum, 1010 (åtta plus två). 7 är 0111 och E är 1110. Varför skrev jag 0111 i stället för bara 111? Jo, för att alla siffrorna måste vara med när man skriver ihop det, annars blir det fel. Vi skriver alltså ihop allting, 1010 0111 1110. Det betyder A7E hexadecimalt och som ovan nämnts, 2686 decimalt.
Hur ska man då tala om vilket talsystem man använder när man skriver ett program? (Det spelar ingen roll vilket man använder, när man assemblerar programmet räknar datorn om alla värden till binära) 11, till exempel, betyder ju tre binärt, elva decimalt och sjutton hexadecimalt. Jo, om man skriver 11 som det är betyder det att det är decimalt (alltså elva), skriver man dollartecknen framför, $11, betyder det att det är hexadecimalt (alltså sjutton) och skriver man ett procenttecken framför, %11, betyder det tre (gissa vilket talsystem?). Har du somnat? Jo, tänkte väl det, men nu fortsätter vi. Såvida du inte hoppade över lite grann? Aja baja, detta är viktigt, så plugga på...
När man lagrar data brukar man använda tre olika storlekar på stället man ska lagra på, beroende på värdet man vill lagra. En "bit" är den minsta enheten i datorn (en etta eller en nolla) och de tre olika storlekarna är 1 byte (8 bits), 1 word (16 bits eller 2 bytes) och 1 longword (32 bits eller 4 bytes eller 2 words). I en byte kan man representera ett av 256 olika värden, eftersom två upphöjt till åtta (2*2*2*2*2*2*2*2) är just 256. I en byte brukar man till exempel lägga ASCII-värden, som oftast bara sträcker sig till 127. Ett word är ju sexton "bitar" och därför kan man representera ett av 65536 (två upphöjt till sexton) olika värden i det. För ett longword är motsvarande värde, håll i dig nu, 4294967296. Alla adresser till ett ställe i minnet lagrar man i longwords.
När man skriver assemblerprogram finns det, som med andra språk, ett speciellt format i vilket koden måste skrivas, så att assemblern kan förstå vad man vill ha gjort när man assemblerar källkoden.
En vanlig källkodsrads format kan se ut så här:
label instruktion kommentar
Förutom att skriva till RAM-minnet har 68000-processorn (huvudprocessorn i ST:n) också tillgång till 16 "register". Dessa kan tänkas som 16 longwords (kom ihåg att ett longword är fyra bytes) som processorn kan använda. Registren är väldigt viktiga, eftersom processorn kan komma åt register snabbare än minnet och kan utföra vissa operationer som bara går att använda på register och du kommer att se dem användas väldigt ofta.
De 16 registren är delade i två grupper - adressregistren och dataregistren.
Dataregistren kallas d0 till d7 och kan kommas åt helt enkelt genom att nämna dem. Ett exempel:
move.l #100,d3
flyttar in värdet 100 i register d3.
Adressregistren kallas a0 till a7. De kan också kommas åt direkt på samma sätt som dataregistren. a7 kallas ibland sp - som står för stackpekare - och är ett "speciellt" register reserverat för användning tillsammans med stacken, som kommer att förklaras senare.
Anledningen till att man skiljer mellan adress- och dataregister är helt enkelt därför att det finns några instruktioner som bara kan använda en av de typerna av register.
Om ett program behöver hänvisa till ett speciellt område av minnet, finns det ett antal metoder det kan använda. Den enklaste skulle vara att hänvisa till den faktiska "adressen" i minnet. Adressen är helt enkelt numret på byten - eller wordet - eller longwordet - till vilken vi hänvisar. Den första byten av RAM är byte noll, sedan kommer byte ett, två och så vidare ända till slutet av RAM i ST:n. Det första wordet ligger på adress noll, det andra wordet på adress två, följt av fyra, sex osv. Longwords ligger såklart på 0, 4, 8, 12 osv. Om vi vill flytta innehållet av ordet som börjar på adress 5432 (alltså byten på 5432 och byten på 5433) till dataregister 5, kan vi använda instruktionen:
move.w 5432,d5
Observera att inte stakettecknet (#) är med, och då betyder instruktionen alltså "flytta wordet som ligger på adress 5432 till lägsta wordet i dataregister 5" istället för "flytta värdet 5432 till dataregister 5".
Olyckligtvis vet vi sällan den exakta minnespositionen vi vill komma åt. Därför kan det vara mer lämpligt att hålla adressen i ett adressregister. Exempel:
move.l #5432,a3 ;Flyttar in VéRDET 5432 i a3
move.w (a3),d5 ;Flytta innehållet i wordet på
;adressen som hålls i a3, alltså
;det som ligger i adress 5432,
;till d5
Du kan se att genom att sätta parenteser omkring adressregistret säger vi åt 68000 att använda värdet som ligger i det registret som en adress. Vi kan också lägga till en offset... oups, du kanske inte vet vad som menas med offset. Well, here goes, offset är ett värde som används för att tala om hur mycket man ska lägga på en viss sak för att komma till det värde man vill ha. Taskig förklaring? Nåja, då tar vi ett exempel:
move.w 24(a1),d0
flyttar värdet som ligger i wordet på adressen man får om man lägger ihop 24 och det som ligger i a1. Förstod du inte det heller? Jo, om t.ex. värdet 2000 skulle ligga i a1 flyttar denna instruktion wordet som ligger i adress 2024 (2000+24) till låga wordet på d0. Nu hoppas jag det gick upp ett ljus. Det är alltså i detta fall 24 som är offset. OBServera att man INTE har något stakettecken på en offset. Om offset:en är ett hexvärde får du dock naturligtvis sätta ett $-tecken framför.
move.w $24(a1),d0
flyttar alltså wordet som ligger på adress 2036 till d0 om a1 skulle vara 2000, eftersom $24 betyder 36 decimalt. I Progga 68000 finns det ett bra avsnitt i början om vad hexadecimala tal egentligen är för något.
Nu komplicerar vi lite till (hehe). Man kan också lägga till innehållet i ett annat register för att få adressen till den byte/word/longword man vill komma åt. Det kan ske på följande vis:
move.l $80(a0,d0),d7
flyttar longwordet som ligger på adressen man får om man lägger ihop $80 (128 decimalt), värdet i a0 och värdet i d0, till d7.
Så finns det då också naturligtvis indirekt adressering (som det kallas om man skriver adressregistret inom parentes för att få ett värde som ligger på en adress, till skillnad från att behandla värdet i registret 'direkt') med fördekrementering respektive efterinkrementering. Dessa termer låter ungefär som allvarliga sjukdomar ('Vad är det för fel på dig?' 'Jag har fått en släng av fördekrementering') men låt dig inte avskräckas av det. Det innebär helt enkelt att man antingen drar ifrån från ett adressregister innan man flyttar ett värde till det eller i det andra fallet att man först slänger in värdet och sedan ökar registret. De två olika adresseringsmetoderna skrivs, ganska logiskt, t.ex. för a0:
-(a0) och
(a0)+
Hur mycket som dras ifrån eller läggs till till registret bestäms av vad det är man flyttar. Flyttar man en byte ökas eller minskas registret med ett (en byte), flyttar man ett word ökas eller minskas det med två bytes (ett word) och för ett longword är det naturligtvis 73 bytes... gick du inte på den? Nej då, fyra ska det givetvis vara.
Du har förmodligen redan stött på åtminstone fördekrementering av stackpekaren i t.ex. en rutin som väntar på en tangent:
move.w #7,-(sp)
trap #1
addq.l #2,sp
èterigen kan nämnas att sp är exakt detsamma som a7.
Vad kan man använda dessa saker till? Tja, om du vill ha ett område i minnet som börjar på t.ex. adress $5000 där det ligger en massa på varandra följande tal (OBS! Detta är bara ett meningslöst exempel!), dvs först ett word med värdet 1, sedan ett word med värdet 2 osv. upp till exempelvis 50 kan du göra t.ex. så här:
move.l #$5000,a3 ;Lägga in adressen där området ska börja
clr.l d0 ;Rensa hela d0
move.w #49,d3 ;en räknare, 50-1=49
lab: addq.l #1,d0 ;Lägga på ett på d0 varje gång
move.w d0,(a3)+ ;Lägga värdet i d0 på adressen som
;ligger i a3 och öka a3 så att det
;hamnar på nästa word
dbra d3,lab ;Loopar 50 ggr.
De adresseringsmetoder vi har lärt oss idag (vi? Det är ju bara jag som håller på och snackar skit hela tiden?) är alltså som följer:
(a1) Indirekt adressering via adressregister
(a1)+ Indirekt adr. via adressr. med efterinkrementering
-(a1) Indirekt adr. via adressr. med fördekrementering
$12(a1) Indirekt adr. via adressr. med förskjutning
$12(a1,a2) Indirekt adr. via adressr. med index
Och så finns det ju förstås också direkt adressering men det är ju enkelt, och så finns det också några andra adresseringsmetoder som man inte har så stor användning av på detta stadium.
Nu till några felsökningsråd:
1) Detta är inget felsökningsråd, utan ett gott råd INNAN det blir fel: SPARA (eller sejva, som man brukar säga för att inte bli utskrattad) ALLTID *INNAN* DU KôR IGèNG PROGRAMMET! Hur rätt man än tror att det blir, hänger sig fanskapet lik förbannat om inte elva, så nio gånger av tio.
2) Kolla om du har glömt stakettecknet, "#", någonstans. Det är nämligen en hemskans skillnad på t.ex:
move.l #label,a3
och
move.l label,a3
Med den första av dessa instruktioner trycker du in adressen där "label" ligger i a3. Den andra lägger in värdet som ligger i adressen som "label" pekar på i a3 (blev det krångligt? :-)). Vad det än blir för tal så kan man vara ganska säker på att programmet bombar för adressfel (3 bomber) förr eller senare, när datorn förtvivlat försöker lägga in något på en adress man behöver ca 3000Mb för att kunna använda. Och blir det inte adressfel blir det något annat fel.
3) Kolla om du har satt en looplabel fel, t.ex. på den instruktion där du tilldelar räknaren ett värde, såsom:
cul8r: move.w #50-1,d4
move.w #37,-(sp)
trap #14
addq.l #2,sp
dbra d4,cul8r
Om du gör detta hamnar datorn i en evighetsloop. Den går oftast inte att bryta med CTRL+C och därför är allt arbete förlorat om du inte har sparat innan du kör programmet.
Rutinen här ovanför gör inget annat än att vänta på VBI 50 gånger, alltså stå och vänta i en sekund. MEN, den är felskriven.
Vad som händer är att på första raden slänger man in ett värde i d4, för att använda d4 som räknare. Att man drar bort 1 beror på att det bara ska vara så i en räknare man använder dbra till. Rad två, tre och fyra är rutinen som väntar på 1 VBI. Sedan kommer raden som drar ifrån ett från d4, kollar om d4 är -1 och i andra fall hoppar den till den label som står på raden, i detta fall cul8r. Om labeln hade stått på andra raden skulle alltså programmet först ha utfört väntrutinen, dragit från ett från d4, funnit att 48 inte var lika med -1 och därför gått tillbaka och gjort rutinen igen. Nästa gång skulle den ha dragit ner d4 till 47, nästa gång 46 osv. När den kommit till -1 skulle programmet ha fortsatt. Nu är det så att den här rutinen är medvetet felaktig för att visa ett vanligt fel. Labeln står som synes på första raden och därför kommer d4 att sättas till 49 hela tiden. Räknaren d4 kommer alltså att pendla mellan att vara 48 och 49 hela tiden. Eftersom den aldrig får chans att räkna ned till -1 och avsluta kommer det att bli en evighetsloop.
Comprende? Goodie goodie...
4) Du kan ha tänkt fel precis när du höll på att programmera och satt en grej som bara skulle utföras en gång inuti en loop eller dylikt. Dessa små idiotiska fel är mycket vanliga. Alla gör dem. Till och med JAG! (sus genom publiken) Gå inte och häng dig bara för att du gör något sådant. Jo förresten, gör det. :-) Speciellt i början av en lång och framgångsrik codarkarriär (stort skämt) är det naturligtvis vanligt. Efter ett tag lär man sig ju automatiskt vad som brukar bli fel. Ett litet tillägg: éven om det ibland blir lite grann fel på datorn är det nästan aldrig så när det är något som inte funkar. Ibland har jag letat fel i en kvart eller nästan en timme i ett litet sketet program utan att hitta det. Då är det klart att man börjar fundera på om det är datorn det är fel på. Det ligger nog i människans natur att tro att det är "fel på datan". Tillvaron blir så mycket enklare om man kan skylla ifrån sig på olika saker som inte kan skylla tillbaka. Efter ett tag brukar man emellertid helt plötsligt komma på var felet ligger. Det värsta som kan hända då är att det finns ett sådant där litet fel till. Då balanserar man verkligen på vansinnets gräns och undrar huruvida man ska skratta eller gråta. Oftast gör man varken eller, utan bankar istället en stund på datorn medan man skriker "jävla jävla jävla dumma skrothög!!" så att grannarna vaknar och börjar dunka i väggen med en gammal sko. Nåja, tillbaka till ämnet.
5) Om ingenting funkar och du har letat fel i trekvart och hjärnan börjar torka och föräldrarna/hustrun/barnen skriker att du ska gå och lägga dig fastän klockan bara är halv fyra på natten, gå och lägg dig. Det funkar alltid.
När (om?) du lär dig grunderna och har fluktat lite i "Atari ST/STE Hårdfakta" och ändå inte förstår hur sjutton man ska kunna få ut något av att flytta värden mellan register får du ta och försöka skaffa några källkoder till program som redan finns. I slutet av "Hårdfakta" står några sådana, men de täcker långtifrån allt roligt man kan göra. Har du modem finns det nästan på alla BBS:er ett knippe källkodsfiler att plocka hem. Annars får du försöka ta kontakt med någon. Det är inte så svårt. Var med på ett konvent någon gång!
Och så till sist: Programmera 68000 är en himla bra bok med tanke på de utförliga beskrivningar som finns av de olika instruktionerna. Det står också en massa på varje instruktion om hur den ser ut i ren maskinkod. Låt mig uttrycka värdet av denna information så här: det är synd att den inte står på separata sidor, så att man kan riva ur dem och använda dem till något bättre, såsom toalettpapper eller underlägg när man löder någonting. Den övriga beskrivningen av instruktionerna är däremot utmärkt. Läs lite grann på de sidorna. Läs också från början till ungefär sidan 64. Där står det viktigaste man behöver kunna. Efter dessa sidor börjar boken behandla ett helt annat operativsystem än det som finns i ST:n men fram dit finns det som sagt mycket matnyttigt. Du kan också leta i registret (nej, inte dataregistret!) om det är något ord du vill ha förklarat.
Det är pga det käcka kapitel 3 i boken som jag inte tänker förklara några instruktioner här. Av kapitlen i början är det om adresseringsmetoder speciellt bra.
(C) 1992 Kalle Lundqvist alias PQ Lear // Brainstorm