Les 9: Stacks & Subroutines. In de vorige les hebben we het gehad over optellen en aftrekken, het nut van re-entrant code, en binary coded decimal. We kunnen nu al kleine programmaatjes schrijven met behulp van de move, add en sub instructies, maar het blijft natuurlijk erg triviaal. Daar komt nu spoedig verandering in, want de elementen die we nog nodig hebben om echte programma's te schrijven, nl. subroutines, tests, en conditional en unconditional jumps komen nu spoedig aan de beurt. Daarom wil ik vandaag een belangrijk onderwerp aansnijden, dat bij programmeren in assembler altijd een grote plaats inneemt: de stack. Wat is een stack? Zoals we hebben gezien in les 2 bestaat het externe geheugen voor de 68000 microprocessor uit een lange lijst van opeenvolgend genummerde vakjes. We kunnen een adresregister naar een van die vakjes laten wijzen en dat vakje en zijn buren dan gebruiken om waarden in te bewaren. Als we dat doen met de addressing mode -(an) als we iets wegschrijven, en met (an)+ als we wat ophalen, hebben we een stack! Dit schreeuwt om een voorbeeld. Stel a6 bevat de waarde 10000. d1 bevat de waarde $300 en d2 de waarde $FF. Met de instructies: move.l d1,-(a6) move.l d2,-(a6) bereiken we het volgende: op adres 9996 bevindt zich een langwoord met de waarde $300. Op adres 9992 bevindt zich een langwoord met waarde $FF. In A6 bevindt zich de waarde 9992. A6 wijst dus naar het laatste woord dat we hebben weggeschreven. ( Ga dit na. ) Als we nu in ons programma een of andere berekening maken waarbij d1 en d2 betrokken zijn, gaat de oorspronkelijke inhoud daarvan verloren. Maar als we maar van a6 zijn afgebleven, hoeven we alleen maar de instructies: move.l (a6)+,d2 move.l (a6)+,d1 uit te voeren om alles weer bij het oude te hebben. Zelfs A6 wijst weer naar de oude plaats, 10000! Alleen, de oorspronkelijke inhoud van de geheugenbytes 9992 tm 10000 is natuurlijk verloren gegaan. Maar daar stond (hopelijk) niets interessants. We hebben nu een stack gebruikt; a6 is in dit geval de stackpointer, het wijst naar de stack. Het idee van een stack is te vergelijken met een stapel papiertjes, waar getallen op staan, die op een prikker zitten vastgeregen. We kunnen een papiertje op de prikker toevoegen, of er een papiertje afhalen, maar we kunnen niet zien wat er op de papiertjes onder de bovenste staat, en we kunnen die er evenmin afhalen zonder eerst de papiertjes te verwijderen die er bovenop zitten. Een beeld dat ook vaak gebruikt wordt is dat van die automatische dienbladkarren in sommige self-service restaurants, waar een veer onder zit zodat er steeds maar een blad boven de kar uitsteekt. Het is natuurlijk wel mogelijk om met een adressering als 4(a6) een geheugenvakje te lezen dat niet 'aan de beurt' is, maar dan omzeilen we de spelregels voor stacks. | De ESSENTIE van een stack is dat wat er het EERST op gaat, er het | LAATST af komt. Dit principe heet ook wel LIFO (last in first out), | wat natuurlijk op hetzelfde neerkomt. ( Overigens is het natuurlijk ook mogelijk om een stack te maken door bij het schrijven (an)+ en bij het lezen -(an) te gebruiken; dit levert een stack op die in de richting van hogere adressen groeit. Maar bij conventie gebruiken we stacks die naar beneden groeien. ) Hoewel alle adresregisters als stackpointer kunnen worden gebruikt, getuige het bovenstaande voorbeeld met a6, is er in de 68000 microprocessor een register dat speciaal bedoeld is om bij bepaalde instructies als stackpointer te dienen. Dat is register a7. Veel assembleerprogramma's accepteren dan ook de afkorting SP (of sp) voor a7. Bij bepaalde instructies maakt de processsor gebruik van register a7 zonder dat we dat expliciet hebben gespecificeerd. Omdat de processor altijd een lang woord op de stack schrijft in zo'n geval, mag register a7 nooit naar een oneven adres verwijzen. Dan zou namelijk die schrijfoperatie niet door kunnen gaan. Het register a7 onderscheidt zich dan ook van de registers a0-a6 doordat het niet eens een oneven getal KAN bevatten. Ieder oneven getal dat we erin zetten wordt automatisch even gemaakt. Als we in a7 10000 hebben staan en we voeren uit: move.b d0,-(a7) dan staat het register a7 daarna op 9998 en de geschreven byte bevindt zich in adres 9998 (en niet 9999 zoals we misschien zouden denken.) Als we een stack gebruiken moeten we dus zeker weten dat het geheugengebied waarnaar de stackpointer wijst inderdaad door ons gebruikt mag worden, en dat het ook groot genoeg is om we alle waarden die we willen schrijven op te kunnen nemen zonder dat we ons eigen programma in wandelen. ( Meestal wijst de stackpointer een paar honderd of -duizend bytes voorbij het eind van ons eigen programma, zodat we als we teveel op de stack wegschrijven zelf het eerst de wrange vruchten hiervan plukken. Want als de stack over een stuk code heenloopt werkt ons programma natuurlijk niet meer goed. ) Goed, we weten nu wat een stack is. Wat kunnen we er nog meer mee doen behalve wat registers bewaren? Daarmee komen we bij de subroutines. Hoewel we in de volgende les meer over subroutines zullen horen, zal ik er in het kader van de uitleg van de stack vast wat over zeggen. Wat is een subroutine? Een subroutine kunnen we bijvoorbeeld maken van een stukje code dat in een programma meer dan eens wordt uitgevoerd, of van een stukje code dat zo handig is dat we het in een ander programma ook wel zouden willen gebruiken. In het voorbeeld van les 5 bestond het hoofdprogramma bijna helemaal uit subroutine-aanroepen, of subroutine calls in het engels. We kunnen natuurlijk dezelfde code twee keer opschrijven, maar we kunnen het ook maar een keer doen en er dan verder naar verwijzen. Voorbeeld: start moveq #0,d0 moveq #$10,d1 moveq #$20,d2 add.l d0,d1 add.l d1,d2 ; testje: wat staat er nu in d0, d1, d2 ? moveq #0,d0 moveq #20,d1 moveq #$F1,d2 add.l d0,d1 add.l d1,d2 ; en hier? Een volstrekt triviaal programma, dat geef ik onmiddelijk toe, maar je moet toch wat met die drie instructies.... Als we het bovenstaande stukje code bestuderen valt ons op dat de sequentie add.l d0,d1 add.l d1,d2 tweemaal voorkomt. We kunnen hier dus misschien een subroutine van maken. Als volgt: start moveq #0,d0 moveq #$10,d1 moveq #$20,d2 bsr addregs moveq #0,d0 moveq #20,d1 moveq #$F1,d2 bsr addregs * hier kan van alles tussen staan... addregs add.l d0,d1 add.l d1,d2 rts Het stukje dat gelabeld is 'addregs' wordt nu steeds uitgevoerd als de processor de instructie 'BSR addregs' tegenkomt. Hoe werkt dat: BSR is Branch to SubRoutine. Als de processor deze instructie tegenkomt zet hij het adres van de volgende instructie (hier dus het adres van moveq #0,d0) op de stack, om te onthouden waar de uitvoering van het programma gebleven was. Dan laadt hij het adres van addregs in de PC en begint de eerste instructie daarvan uit te voeren. Aan het eind van iedere subroutine staat een RTS instructie, ReTurn from Subroutine. Die zorgt dat de bovenste waarde op de stack in de PC wordt geladen, zodat de processor daarna verder gaat met de instructie moveq #0,d0 waar hij gebleven was toen de BSR instructie er tussen kwam. A7 wordt weer opgehoogd en wijst naar het zelfde adres als voor de BSR instructie. RTS doet hetzelfde als 'move.l (a7)+,pc' zou doen als dat zou bestaan. De stack wordt dus weer op de oorspronkelijke waarde gezet en de uitvoering van het programma gaat verder met de instructie na de BSR instructie. We hebben hier ook voor het eerst gezien hoe een label werkt. Een label is voor het assembleerprogramma gelijk aan een getal, namelijk het aantal bytes dat ligt tussen het begin van onze code en het adres van de instructie waar het voor staat. Als we schrijven 'BSR addregs' doet de assembler het volgende: hij kijkt wat het adres is waar de BSR instructie zit, en wat de waarde van adres is, en berekent het verschil. Dat is dan het aantal bytes dat er gesprongen moet worden, en dat wordt aan de codering van de bsr instructie toegevoegd. Als we dus op twee plaatsen 'BSR addregs' schrijven, wordt er in feite tweemaal een verschillende instructie gecodeerd door de assembler, omdat de afstand natuurlijk altijd anders is. De stack werkt dus zo dat tijdens het uitvoeren van een subroutine een nieuwe subroutine kan worden aangeroepen, het terugkeeradres daarvoor wordt gewoon bovenop dat van de vorige gestapeld, en uiteindelijk is er voor iedere aanroep van een subroutine een corresponderende RTS instructie zodat de stack op het eind precies staat waar ie stond, mits iedere subroutine inderdaad met een RTS eindigt en mits er geen stoute programmaonderdelen zijn die ook dingen op de stack gaan zetten, want dat gooit de boekhouding natuurlijk wel heel snel in de war.... Op deze manier is het nesten van subroutines onbeperkt mogelijk, zolang er in de stack tenminste nog plaats is. Is dat niet het geval dan treedt er een 'Stack overflow' op, wat meestal onmiddelijk fataal is voor de werking van het programma. De eis dat we zelf niets op de stack mogen zetten is te rigide, en kan worden afgezwakt: We mogen wel iets op de stack zetten in een subroutine, MITS we het er maar weer afhalen voor we de subroutine verlaten met een RTS instructie. Als die RTS instructie maar het juiste terugkeeradres op de stack vindt waarvandaan de aanroep is gebeurd mogen we alles met de stack doen. Dus een subroutine als: fout add.l d0,d1 ; tel registers op add.l d1,d2 ; tel registers op move.l d2,-(a7) ; resultaat op de stack rts ; dit mag niet ! geeft waarschijnlijk een crash, omdat het resultaat van de optelling wel geen geldig adres zal zijn om naar toe te springen. Omdat RTS gewoon de bovenste lange waarde van de stack plukt, gaat het mis. Maar: goed add.l d0,d1 add.l d1,d2 move.l d2,-(a7) move.l (a7)+,result(a4) rts Werkt wel, als tenminste result(a4) een geldig adres is. ( Al is dit natuurlijk niet de handigste manier om een resultaat te krijgen. ) Waar het in dit geval om gaat is dat de stack voor de RTS in de oude staat hersteld moet zijn.