Les 10: branches, jumps, subroutine calls, effective address instructies In de vorige les hebben we gezien wat een stack is en hoe hij werkt. We hebben dat met een paar voorbeelden toegelicht. Vandaag gaan we wat uitgebreider op onvoorwaardelijke sprongen in het programma in en op de lea en pea instructies, die overigens met evenveel recht bij de move instructies hadden kunnen worden besproken. Er zijn een aantal instructies die in een programma altijd tot een sprong leiden, en een aantal die alleen tot een sprong leiden als aan bepaalde voorwaarden mbt de toestand van de vlaggen is voldaan. Die laatste zullen we in een volgende les bespreken. De instructies die altijd tot een sprong leiden zijn: JSR ; Jump to SubRoutine, overal in het geheugen BSR ; Branch to SuBroutine, als jsr maar niet verder dan 32 Kbyte JMP ; JuMP, spring naar een plaats overal in het geheugen BRA ; BRAnch, spring naar een plaats niet verder dan 32 Kbyte RTS ; ReTurn from Subroutine, spring naar het adres op de stack RTE ; ReTurn from Exception, wordt voor het operating system gebruikt, en normaliter niet door de applicatieprogrammeur. RTR ; return and restore condition codes, zelden gebruikt. Alle spronginstructies behalve RTR laten de statusvlaggen onveranderd! Overigens is het nulpunt voor iedere relatieve spronginstructie gelijk aan het punt dat 2 bytes voorbij het adres van de spronginstructie ligt, niet het adres van de spronginstructie zelf zoals je zou denken. Van de BSR instructie bestaan 2 vormen, een korte en een lange. We schrijven: bsr.s addregs ; of ook bsr addregs ; Sommige assemblers maken automatisch een bsr.s instructie als het adres waar naar toe moet worden gesprongen binnen het bereik van een korte bsr ligt. Persoonlijk houd ik hier helemaal niet van, als een sprong kort moet dan specificeer ik dat graag zelf. Het verschil is dat een BSR.S instructie twee bytes lang is, omdat de sprongafstand in de tweede byte van de instructie staat, terwijl de gewone BSR instructie 4 bytes lang is omdat de displacement daar als een woord achter wordt gezet. Dat houdt ook in dat we met een BSR.S instructie van -126 tot +128 bytes kunnen springen, meer past er niet in een byte, en met een gewone BSR kunnen we adressen met een offset van -32766 tot +32768 halen. ( Gerekend vanaf het adres van de bsr instructie.) Een sprong-offset wordt dus door de processor beschouwd als een getal met teken. De BSR instructie gebruikt dus altijd (impliciet) een PC-relatieve adressering om sprongen van maximaal 32 K te maken. Als we sprongen van meer dan 32 K willen maken (zal in zelf geschreven assembler programma's niet gauw voorkomen) moeten we de JSR instructie gebruiken. Die heeft het voordeel dat er naar een subroutine overal in het geheugenbereik kan worden gesprongen, maar is wat trager en moet voor die lange sprongen dan een absoluut adres gebruiken, waardoor ons programma niet meer positie-onafhankelijk kan zijn. Er moet dan door de loader worden gereloceerd, zie verderop. De BSR instructie gebruikt altijd (impliciet) een adressering ten opzichte van de PC, maar de JSR is wat flexibeler, en kan worden gebruikt in de addressing modes: jsr (an) ; address register indirect jsr d(an) ; address register indirect met offset jsr d(an,Ri) ; idem met indexregister jsr (adres van 16 bits) ; kort absoluut jsr ( volledig adres) ; lang absoluut jsr d(PC) ; pc-relatief jsr d(PC,Ri) ; idem met indexregister. De mode JSR d(PC) is beperkt tot 32 K byte offset en is dus functioneel volkomen identiek met bsr, en duurt ook even lang om uit te voeren. Alleen de BSR.s instructie neemt wat minder plaats in. Zowel bij de BSR als bij de JSR instructies wordt het volledige adres van de instructie die na de sprong komt op de stack gezet, waarbij de stackpointer dus met vier wordt verlaagd. Als de aangeroepen subroutine nu maar met RTS eindigt terwijl de stackpointer naar dit terugkeeradres wijst zal het programma vanzelf goed terechtkomen op de plaats waar de uitvoering van het programma voor de subroutine werd onderbroken. Of we in onze programma's BSR of JSR gebruiken is onbelangrijk. Ik gebruik zelf altijd 'BSR label' ( en eventueel 'BSR.S label'als het kan, om ruimte te sparen), maar dit heeft geen duidelijke voordelen boven 'JSR label(PC)' als de korte vorm van bsr tenminste niet mogelijk is. Zouden onze programma's echter grotere sprongen moeten maken dan 32 K voor- of achterwaarts, dan zitten we aan JSR vast, en wel de niet pc-relatieve vormen. Het programma zal dan door een loader moeten worden 'geherhuisvest', relocated in het Engels. De assembler vult om te beginnen bij het vertalen een adres in, ervan uitgaand dat het begin van ons programma op adres $00000000 komt te liggen, en houdt verder een lijstje bij van alle locaties die op deze manier zijn berekend. Dat lijstje wordt dan aan onze programmafile toegevoegd. Dit heet de 'relocation information'. Voeren we het programma nu uit, dan zorgt het operating system voor de relocatie. Dat wil zeggen dat bij al deze adressen de waarde van het beginadres wordt opgeteld. We merken er dus in de praktijk niets van dat nog niet bekend is waar in het geheugen ons programma zal gaan draaien. Als het mogelijk is moeten we echter proberen code te schrijven die overal kan draaien zonder relocatie, want dat spaart tijd en ruimte. Maar we hoeven er geen halszaak van te maken, het gaat meestal allemaal vanzelf goed. ( Het is in bv. GFA Basic mogelijk om stukje machinecode in te lassen en dat als een functie aan te roepen. De functiecode wordt dan volledig in het Basicprogramma geintegreerd. Als we dat willen doen moeten we natuurlijk wel zorgen dat onze code geen adressen bevat die moeten worden gereloceerd, want dat gebeurt in in dit geval niet. ) Dat brengt ons bij de BRA en JMP instructies. Deze zijn gelijk aan de BSR en JSR instructies, met dit verschil dat het adres van de instructie volgend op de sprong nu NIET op de stack wordt gezet. Springen we dus met BRA of JMP ergens naar toe, dan weten we niet meer waar we vandaan gekomen zijn. De BRA instructie is wederom altijd ten opzichte van de waarde van de program counter, met een offset van + of - 32 K byte voor de gewone en + 128 of - 126 byte voor de korte vorm. De JMP instructie heeft dezelfde addresseringsmogelijkheden als de JSR instructie. Wederom geldt dat we in zelfgeschreven assemblerprogramma's vrijwel altijd met BRA en BRA.S zullen uitkomen. Zoals gezegd, geldt voor alle PC-relatieve offsets dat ze worden berekend ten opzichte van het adres van de instructie plus 2. De instructie jmp -2(pc) zal dus naar zichzelf toespringen en een oneindige lus veroorzaken. De RTS instructie is eigenlijk een sprong naar het adres dat zich op dat moment op de stack bevindt. Het effect zou kunnen worden beschreven met jmp (a7)+ ; ( N.B. Dit is geen legale instructie.) We kunnen inderdaad een adres op de stack duwen en er dan met rts naar toe springen, het is helemaal niet nodig dat dat adres door een JSR of BSR op de stack is gezet. Deze techniek wordt gebruikt in sommige implementaties van de taal FORTH. De RTE instructie zullen we later bespreken als we het over de supervisor mode gaan hebben. Hier wil ik volstaan met te zeggen dat het de manier is waarop de processor na een trap of interrupt terug kan keren naar het gebruikersprogramma. Het returnadres wordt nu niet van de gewone stack gehaald maar van de supervisor stack, evenals de inhoud van het statusregister. De supervisormode wordt uitgeschakeld. De RTR instructie laadt eerst het conditiecode register van de stack in een woord en laadt daarna de pc met het langwoord dat daaronder ligt op de stack (eigenlijk dus daarboven, omdat de stack in de richting van het lage geheugen groeit.) Wordt zelden gebruikt. De effectief-adres instructies: LEA en PEA. LEA en PEA zijn eigenlijk twee zusjes. De afkortingen staan voor Load Effective Address en Push Effective Address. Ze berekenen beide een effectief adres. Wat is dat? Nou, als we schrijven move.l #$100,4(a6) dan is het vakje 4(a6) de bestemming van de move. Het adres van dit vakje uitgedrukt als nummer en niet als 4(a6) is het effectief adres. Met de sequentie: lea 4(a6),a0 move.l #$100,(a0) bereiken we precies hetzelfde resultaat, zij het wat omslachtiger. De destination van LEA is altijd een adresregister. Het belangrijkste nut van LEA is om een relatief adres om te zetten in een absoluut adres tijdens de uitvoering van een programma. Stel we hebben een stukje tekst in ons programma staan dat we bij het draaien op het scherm afgedrukt willen hebben. Onze print routine heeft een absoluut adres nodig waar de af te drukken string zich bevindt, maar we weten bij het schrijven nog niet op welk adres zich onze tekst zal bevinden bij het uitvoeren van het programma. Nu kunnen we schrijven movea.l #tekst,a0 bsr print ; ervan uitgaand dat print het adres in a0 wil... maar dat houdt in dat ons programma niet meer positie-onafhankelijk is, omdat het adres van tekst door de loader zal moeten worden berekend. Het is beter te schrijven: lea tekst(pc),a0 bsr print Dit is mogelijk als 'tekst' op minder dan 32 K afstand van de PC ligt, is volledig positie-onafhankelijk, 2 byte kleiner, en ook nog eens meer dan twee keer zo snel. Merk op dat we bij de absolute vorm (die met movea) het # teken moeten gebruiken om aan te geven dat we het adres van tekst bedoelen en niet de inhoud ervan, en dat dat bij lea niet nodig is. De volgende addressing modes zijn voor lea toegestaan: lea (an1),an2 ; is zinloos want identiek aan movea.l an1,an2 lea d(an),an ; is heel nuttig, want het is de snelste manier om d bij an op te tellen als d>8 ( addq.l #x,an duurt overigens even lang bij size .l, maar is sneller bij size <.w>. En 'addq.l #4,a7' is wel kleiner dan 'lea 4(a7),a7' ) lea d(an,Ri) ; handig om het startpunt van een record of structure in te laden. lea ; geen veelgebruikte instructie, weinig specifiek nut. lea d(PC) ; de meest gebruikte en nuttigste vorm lea d(PC,Ri) ; ook zeer nuttig. Een andere reden om LEA te gebruiken is de regel te omzeilen dat je niet mag schrijven naar PC-relatieve locaties. Stel je heb een variable in je code staan waar je naartoe wilt schrijven, maar je code moet positie-onafhankelijk blijven (altijd een goed principe.) Dan is het het beste om lea var(pc),a0 move.w #waarde,(a0) te gebruiken, aangezien 'move.w #waarde,var(pc)' niet mag en 'move.w #waarde,var' niet positie-onafhankelijk is. Het is helemaal het beste om alle variabelen aaneengesloten te definieren en een vast adresregister, bijvoorbeeld a6, naar het begin van het blok te laten wijzen. Alle variabelen worden dan als 'offset(a6)' geadresseerd. PEA lijkt hier sterk op, alleen wordt het berekende effectieve adres niet naar een adresregister geschreven maar op de stack gezet. De toegestane addressing modes zijn dezelfde als die voor LEA. PEA wordt meestal gebruikt om het adres van een string of variable op de stack te zetten alvorens een subroutine aan te roepen om iets met die string of variabele te doen. Het vervangt dan de sequentie lea var(PC),a0 move.l a0,-(a7) die 6 bytes en 22 tikken kost door pea var(PC) die 4 bytes en 16 tikken kost. Zowel lea als pea leveren een long op, en kunnen dus niet in de lengte .w of .b worden gebruikt. Ze veranderen beide de statusvlaggen NIET. Les 11: voorwaardelijke sprongen. In de vorige les hebben we gezien wat de instructies LEA en PEA, JSR en BSR, JMP en BRA, en RTS doen. Vandaag wil ik het hebben over de conditional branches. Dat zijn allemaal instructies die kijken naar de status van een of meer vlagbits, en afhankelijk van de uitkomst daarvan wel of niet springen. Als ze niet springen gaat het programma gewoon door met de instructie volgend op de voorwaardelijke sprong, en anders op de plek die de branch daarvoor aanwijst. Verder wil ik kijken naar de decrement-and-branch-on-condition instructies, maar daar hebben we het straks wel over. Alle voorwaardelijke sprongen worden genoteerd als: Bcc label ; of Bcc.S label Waarbij cc staat voor condition code. cc moet niet verward worden met CC, wat een van die conditiecodes is en staat voor Carry Clear. De mogelijke conditiecodes zijn: nr code naam conditie opmerkingen 0 T True 1 ; Niet beschikbaar voor Bcc 1 F False 0 ; Niet beschikbaar voor Bcc 2 HI HIgher !C & !Z ; deze vier voor tekenloze getallen 3 LS Lower or Same C | Z 4 CC Carry Clear C 5 CS Carry Set C 6 NE Not Equal Z 7 EQ EQual Z 8 VC oVerflow Clear V 9 VS oVerflow Set V 10 PL PLus N 11 MI MInus N 12 GE Greater or Eq. ( N & V ) | ( !N & !V ) 13 LT Less Than ( N & !V ) | ( !N & V ) 14 GT Greater Than ( N & V & !Z ) | ( !N & !V & !Z ) 15 LE Less or Equal Z | ( N & !V ) | ( !N & V ) ! = NOT ; | = OR ; & = AND. Voor wie niet zo thuis is in computerlogica zijn hier de tabellen voor AND, OR en NOT: NOT. input output 1 0 0 1 NOT inverteert zijn argument dus. AND. inputs output 0 0 0 1 0 0 0 1 0 1 1 1 AND levert dus alleen een 1 op als beide inputs 1 zijn. OR. inputs output 0 0 0 1 0 1 0 1 1 1 1 1 OR levert dus alleen en nul als beide inputs 0 zijn. We kunnen de voorwaarde GT nu als volgt herformuleren in ( een soort van) Nederlands: GT is waar als (het N-bit 1 is en het V-bit 1 is en het Z-bit 0 is) of ook als (Het N-bit 0 is en het V-bit 0 is en het Z-bit 1 is) Het is geen kattepis dit te doorgronden, maar er is een veel eenvoudiger manier om het te onthouden. De laatste vier voorwaarden gebruik je als je met getallen met teken, signed numbers, werkt. Als je twee signed numbers met elkaar vergelijkt ( die instructies krijgen we nog ) en je wilt iets doen als a groter is dan b, gebruik je de test BGT, branch on Greater. Gebruik je unsigned numbers, dan heb je de test HI nodig. Voor getallen waarvan het tekenbit nul is, is de uitkomst in beide gevallen overigens hetzelfde. Voor het vergelijken van adressen gebruik je dus altijd de vormen HI, LS, CC (HS is een andere schrijfwijze) en CS (LO is een andere schrijfwijze); voor unsigned numbers ook, en voor alle signed numbers de laatste vier, GT, GE, LT, en LE. PL, MI, EQ en NE spreken voor zichzelf; je kunt ze gebruiken om te springen als een uitkomst positief(nul is ook positief), negatief, nul of ongelijk aan nul is. VS en VC kunnen worden gebruikt om te kijken of bij een vermenigvuldiging of deling een overflow heeft plaatsgevonden, en dus het antwoord niet juist is. Alle Bcc instructies bestaan dus in een lange en in een korte vorm, geschreven als bijvoorbeeld beq.s next ; kost 2 bytes bne ongelijk ; kost 4 bytes Alle Bcc instructies hebben zelf geen invloed op de toestand van de conditiecodevlaggen. Een voorbeeld. Stel we hebben na het uitvoeren van een subroutine een resultaat teruggekregen op de stack. We willen nu dit resultaat testen, en als het negatief is een errormessage afdrukken. Als het positief of nul is gaan we door met het programma. Hiervoor geef ik nu twee methoden aan: Voorbeeld a: springen op het resultaat van een subroutine * ...instructies.. move.l d0,-(a7) ; zet een argument op de stack bsr subr ; voer de subroutine uit. Hierna bevindt zich ; een resultaat op de stack, nemen we even aan. move.l (a7)+,d0 ; haal het resultaat op. Dit stelt meteen de ; vlaggen in op het resultaat in d0 bmi error ; spring indien d0<0 naar error * ... instructies... ; en ga anders gewoon door * ------------------------- error pea errmessage(pc) bsr print bra exit Het is mogelijk precies hetzelfde effect te bereiken met een BPL instructie. Voorbeeld b: springen op het resultaat van een subroutine. * instructies.. move.l d0,-(a7) ; zet een argument op de stack bsr subr ; voer de subroutine uit. Hierna bevindt zich ; een resultaat op de stack. move.l (a7)+,d0 ; haal het resultaat op. Dit stelt meteen de ; vlaggen in op het resultaat in d0 bpl.s next ; en indien positief, sla het navolgende over... ; merk op dat we hier de .s vorm kunnen ; gebruiken omdat we zien dat de afstand erg ; klein is pea errmessage(pc) ; laad adres van foutboodschap bsr print ; print message bra exit ; en verlaat programma next * ...instructies.. * ---------------- Er bestaan dus de instructies BHI, BLS, BCC=BHS, BCS=BLO, BNE, BEQ, BVC, BVS, BPL, BMI, BGE, BLT, BGT, en BLE, allemaal in een lange en een korte vorm met respectievelijk een bereik van -126..+128 en + of - 32 K byte. De vormen BHS en BLO worden niet door alle assemblers geaccepteerd. Anders dan bij BSR en BRA bestaat er geen Jcc instructie (die sprongen van meer dan 32 K zou toelaten). Er is ook maar een adressing mode, nl impliciet pc-relatief. Dat wil zeggen dat we gewoon het label opschrijven waar we naar toe willen springen. De Bcc instructies zijn dus veelzijdige, compacte en nuttige leden van de instructieset. De DBcc instructiefamilie. Nu wil ik een wat bizarre maar ook zeer nuttige vorm van conditional branch bespreken, de DBcc instructie. Deze is beperkter in zijn toepassing dan de Bcc familie, maar minstens even nuttig, aangezien hij tot aanzienlijke besparingen in tijd en ruimte kan leiden. Ga er even voor zitten want dit is niet eenvoudig, De DBcc instructie bestaat met alle conditiecodes die er ook voor de Bcc instructie zijn, dus DBCC, DBCS, DBEQ, DBGE, DBGT, DBHI, DBLE, DBLS, DBLT, DBMI, DBNE, DBPL, DBVC, DBVS, en daarnaast nog de vormen DBT en DBF. De sprongafstand bij een DBcc instructie is steeds -126..+128 byte. Behalve een label heeft de DBcc instructie ook nog een dataregister nodig. Voorbeeld: dbeq d0,loop Wat doet dit? 1) Er wordt gekeken naar de conditiecodes. Als die voldoen aan de conditie ( in dit geval 'equal') wordt er niets gedaan en gaat het programma door naar het volgende statement. 2) d0.w wordt met 1 verminderd. Als hierdoor d0.w = -1 optreedt gaat het programma door naar het volgende statement. Is dat niet het geval dan wordt naar het label gesprongen (in dit geval loop genaamd.) DBcc is dus een manier om een klein stukje programma een aantal keren, maximaal 64 K keer, te laten uitvoeren. Je zou de functie van de instructie kunnen parafraseren als 'lus tot de conditie waar is of het dataregister -1 wordt en trek steeds een van het dataregister af.' Merk op dat: 1) Als de conditie waar wordt, het dataregister niet meer wordt verminderd; 2) Om een zelfde effect te bereiken met een Bcc instructie je de tegengestelde voorwaarde zou moeten nemen, dus in het bovenstaande geval bne.s loop. De lus komt overeen met de 'loop until' constructie van hogere programmeertalen. 3) De lus pas stopt als het dataregister -1 wordt, en niet nul, (als tenminste de conditie niet eerder vervuld werd). 4) Het geenszins nodig is dat het label voor de DBcc instructie staat, hoewel dat wel het meest gebruikelijk is. Maar als je een dataregister wilt testen op nul, en het is niet erg als het register daarbij wordt veranderd, is DBNE d0,label sneller dan TST.W d0/BEQ.S label. Maar we raken nu een beetje in hogere sferen van het programmeren in assembler. De instructie DBT gaat altijd door zonder het dataregister te verminderen en heeft, voor zover mij bekend althans, geen enkel praktisch nut. Maar de instructie DBF, die voor veel assemblers moet worden genoteerd als DBRA, is van groot nut om een exact omschreven, van tevoren vaststaand aantal malen een lus te doorlopen. Er zijn twee manieren om zo'n lus op te zetten. De eerste is het zo te doen dat de lus helemaal niet wordt doorlopen als de count 0 is. Dit is de meest gebruikte. De constructie die hiervoor wordt gebruikt is als volgt: move.w teller,d0 ; initialiseer loop teller bra.s test ; en kijk eerst of ie al niet nul is. loop * * ... Hier worden de statements van de loop geplaatst... * test dbra d0,loop ; als d0 niet -1 wordt na decrement, nog eens. ( Onthoud dit voorbeeld want het is een van de fundamenten van gezond programmeren in assembler. ) Deze loop wordt precies keer doorlopen, ook als teller nul is, want dan worden de statements in de loop helemaal niet uitgevoerd. Het decrementen van de teller heeft overigens geen effect op de conditiecodes, dus als de loop verlaten is staan de vlaggen zo als ze door de statements in de loop werden achtergelaten. De andere manier van loop-organisatie is er van boven in te rollen, maar dan wordt de loop altijd minstens een keer uitgevoerd, ook als teller om te beginnen 0 is. move.w teller,d0 subq.w #1,teller ; min een voor correct aantal iteraties loop * * ....Hier weer de loop statements ... * dbra d0,loop ; als d0 niet -1 na decrement, nog eens... Het indexregister is dus 16 bit groot, en het maximale aantal loops treedt op als d0 = 0 bij het binnentreden van dit stukje code. De lus wordt dan 65535 keer uitgevoerd. Een praktisch en nuttig voorbeeld. Stel we willen een string beginnend op een adres in a0 kopieren naar een vrij geheugengebied waarvan het adres in a1 staat. We weten dat de string wordt afgesloten door een 0 en dat hij niet langer is dan 64 K. De kopieeroperatie kan dan efficient worden gecodeerd als volgt: moveq #-1,d0 ; max 64 K iteraties loop1 move.b (a0)+,(a1)+ ; deze move stelt de conditiecodes in. dbeq d0,loop1 ( Je kunt ook bne.s loop1 doen, wat precies even snel is maar niet d0 verandert. Deze operatie kost 22 kloktikken per byte op een 68000) We gebruiken hier eigenlijk niet eens de teller in d0, en zetten hem daarom op de grootste waarde die we kunnen. Na afloop kunnen we echter wel zien hoeveel bytes er gekopieerd zijn in deze manoeuvre, door d0 positief te maken met de instructie NEG.W d0 ( die we nog niet hebben besproken) Als de conditiecode 'eq' waar wordt, wordt d0 namelijk niet meer gedecrement. Merk op dat dit positief maken alleen werkt zolang d0 negatief is, dus tot een bereik van 32 K. Bij een nulstring wordt de loop dus verlaten met d0=-1, bij een string van 1 letter met d0=-2, etc. Ga dit na. Ook kunnen we een maximum stellen aan het aantal te kopieren bytes, door dit in te vullen voor we de loop binnen gaan, minus 1 dan natuurlijk. Als we niet willen dat er meer dan 10 bytes worden gekopieerd, kunnen we de eerste instructie uit het bovenstaande voorbeeld vervangen door moveq #9,d0 en klaar is Kees. Over het algemeen zijn stringbewerkingen heel mooi met de DBcc instructie te programmeren. Kortom, gebruik de DBcc instructie zoveel mogelijk. De 68010 processor heeft zelfs een speciale voorziening waardoor dit soort zeer kleine loops razendsnel kan worden uitgevoerd, een beetje overeenkomstig de REP constructie van de 8086/186/286 processors. Dan mag er maar 1 kleine instructie in de loop staan, zoals hier. Tenslotte wil ik vandaag nog de NOP instructie behandelen, die heel opmerkelijk is omdat hij geen enkel effect heeft op de processorstatus behalve dat de PC wordt opgehoogd. We schrijven: nop ; doet niets. Deze instructie is ondanks dat hij dus niets doet toch heel nuttig. Toepassingen: het invoegen van een heel korte pauze in een programma (NOP kost 4 tikken), het verwijderen van instructies uit een programma in het kader van een kraakactie of om een fout te herstellen als je niet de originele code bij de hand hebt, het openlaten van ruimte in een bestaand programma om er later wat extra's in te kunnen bouwen.