Les 8: Optellen en aftrekken. In de vorige les hebben we de familie van de MOVE instructie en de CLR instructie bekeken, die verantwoordelijk zijn voor 30 tot 40 % van de instructies in een programma. Nu snappen we dus eigenlijk al een derde van het programmeren in assembler! Een computer is voor leken vooral een rekenmachine. Dat dat niet erg overeenkomt met de werkelijkheid zien we als we naar de tabel in les 5 kijken, waaruit blijkt dat circa 10 % van de instructies optellingen en aftrekkingen zijn, en minder dan 1 % vermenigvuldigingen en delingen. Maar omdat ze toch wel belangrijk zijn, erg op elkaar lijken, en een goed model zijn voor andere instructies: De optelinstructies. Dit zijn: add ; tel twee getallen op, een moet in een datareg staan adda ; de dest is een adresregister addi ; de source is een immediate operand addq ; als addi maar voor kleine getallen, 1..8 addx ; add met medenemen van de eXtendvlag. Om maar met de meest specifieke te beginnen: addq.l #1,d0 ; telt 1 bij d0 op. Deze instructie kan alleen worden gebruikt met immediate data kleiner dan of gelijk aan 8. De source is dus altijd een van de getallen 1..8. De destination mag in iedere mode worden gegeven mits wijzigbaar. Dus geen pc-relatieve adressering, en geen immediate data. addq.w #3,#3 ; is dus niet mogelijk, en zou ook geen zin hebben, want waar moet de processor het antwoord laten? Maar waarom mag addq.l #5,10(pc) nou niet? Dit zou als instructie wel zin kunnen hebben. Om dit uit te leggen even een intermezzo. -------------------------------------------------------------------------- INTERMEZZO I: Re-entrant code. Dit betekent: weer binnen te treden code. De 68000 processor is zo ontworpen dat hij in staat is om meer programma's tegelijk te draaien. Eigenlijk niet echt tegelijk, in de zin dat er instructies van beide programma's op hetzelfde tijdstip worden uitgevoerd, maar wel zo dat het mogelijk is heel snel heel en weer te schakelen van het ene naar het andere programma. Dat schakelen gebeurt door 'interrupts', onderbrekingen. Komt er een interrupt binnen dan maakt de processor de instructie af waar hij mee bezig was, zet het adres van de volgende instructie die hij wilde gaan uitvoeren even weg (met behulp van de Supervisor stackpointer, waar we tot nu toe overheen gewandeld zijn), evenals de toestand van het statusregister/conditiecode register, en springt dan naar het operating system. Het resultaat hiervan is dat precies bekend is waar hij gebleven was, er gaat geen enkele informatie van het programma verloren. Ho! En de registers dan? Die zouden door het volgende programma toch gewijzigd kunnen worden? Inderdaad. De eerste taak van het operating system is dan ook die registerinhouden te redden, met de MOVEM instructie. Daarna kan het operating system de processor op een ander programma zetten, en als dat een poosje gedraaid heeft wordt ook dat weer onderbroken en het operating system kan nu, na de toestand van dat programma te hebben bewaard, het eerste programma weer opstarten door de registers en vlaggen weer terug te zetten en op het adres verder te gaan waar de processor gebleven was. De interrupts komen van buiten, bijvoorbeeld door een kloksignaal dat iedere 100e seconde een interrupt genereert, of doordat een invoerapparaat een seintje geeft dat het een paar databytes kwijt wil. Ook kan een programma zelf door de TRAP instructie even de controle afgeven aan het operating system. Dit heet een software interrupt. Het aantal programma's dat tegelijk aanwezig kan zijn is alleen beperkt door de aanwezige geheugenruimte. Nu een gedachtenkronkel. Het komt bij computers vaak voor dat er meer mensen tegelijk aan zitten te werken. Met dit mechanisme kunnen we de beschikbare tijd verdelen tussen de programma's die ze ieder draaien. Maar stel nou eens dat ze allemaal hetzelfde programma willen draaien? Dat kan natuurlijk door ze allemaal een kopie in het geheugen te geven van het programma. Maar als het een groot programma is, zeg 200 K, en we hebben maar 500 K geheugen aanwezig, betekent dat dat we hoogstens twee gebruikers kunnen helpen. En de 68000 is op zich krachtig genoeg om er veel meer te bedienen. De oplossing is om de gebruikers van hetzelfde programma gebruik te laten maken, dus niet van meer kopieen ervan, maar echt van dezelfde code op dezelfde adressen. Maar, dan mag de code die de een gebruikt niet door de ander worden gemodificeerd. Dat wil zeggen dat elke gebruiker zijn eigen gebiedje heeft waar zijn variabelen worden opgeslagen, die niet kunnen worden aangesproken door een andere incarnatie van hetzelfde programma. Iedere keer als een incarnatie van het programma moet worden opgestart, reserveert het dus een nieuw stukje geheugen hiervoor. Dit is bijvoorbeeld, wat er gebeurt bij het operating system unix: iedere gebruiker heeft zijn eigen incarnatie van het programma, zonder dat daarbij voor iedere gebruiker een aparte kopie nodig is. We zien nu dat dit mechanisme nooit mogelijk zou zijn bij gebruik van pc-relatieve writes. 10(pc) is in iedere incarnatie van het programma hetzelfde adres. Als we dat gaan veranderen is het programma voor de volgende gebruiker niet meer identiek. Dat is de reden dat de makers van de 68000 hebben besloten deze mogelijkheid te verbieden, om het schrijven van re-entrant code te stimuleren. Om dezelfde reden is het ongewenst (maar niet verboden) om naar absolute adressen te schrijven. Hoewel het op de de Atari ST en de Amiga niet gebruikelijk is om meer mensen tegelijk te bedienen, is het wel mogelijk om meerdere programma's tegelijk te laten draaien, en, mits die programma's zorgvuldig geschreven zijn, ook twee keer hetzelfde programma met een andere set data. We merken als programmeurs -en als gebruikers- als het goed is niets van het feit dat onze programma's geregeld onderbroken worden. Zo doet een ST met zwartwit monitor iedere 70e seconde een groot aantal dingen, kijken of de diskdrive motor nog loopt en hem zo nodig afzetten, kijken of de geluidschip aan een nieuw bevel toe is, kijken of we niet stiekum van monitor hebben gewisseld, noem maar op. Eind intermezzo. -------------------------------------------------------------------------- Maar we hadden het eigenlijk over de ADDQ instructie. Deze instructie moet liefst altijd worden gebruikt als het op te tellen getal kleiner dan of gelijk aan 8 is, want het is dan de snelste en meest compacte methode. De ADDI instructie lijkt sterk op de ADDQ instructie; ook hier tellen we een immediate source op bij een destination. addi.w #$200,d0 ; telt $200 bij het lage woord van d0 op. Het verschil is dat de sourceoperand iedere grootte mag hebben en dat de instructie groter en langzamer is. ( De operand mag natuurlijk niet groter zijn dan in de instructiegrootte wordt gemeld, addi.b #$123,d0 kan natuurlijk niet omdat #$123 meer dan een byte in beslag neemt...) Voor adresregisters als destinatie moeten we ADDA gebruiken; deze instructie kent de grootten .w en .l, maar dat slaat dan op de grootte van de source-operand, die wordt namelijk sign-extended en de gehele inhoud van het adresregister wordt beinvloed. Iedere source adressing mode is toegestaan. adda.w #$1000,a0 ; telt $1000 op bij a0 adda.l #$1000,a0 ; doet precies hetzelfde, maar kost als instructie twee bytes meer omdat $1000 nu als $00001000 wordt weergegeven. Nog meer voorbeelden: adda.w d0,a5 adda.l 4(a4),a0 In de overige gevallen gebruiken we ADD of ADDX. add.l d0,d3 ; telt de inhoud van d0 bij d3 op. Bij gebruik van de ADD instructie moet minstens een van de operands een dataregister zijn, dus source, destination of allebei, zoals hier. Deze instructie bestaat in de smaken .b .w .l Voorbeelden: add.l -(a7),d0 add.w (a6)+,d3 add.w d3,(a0) De ADDX instructie is handig bij het verwerken van zeer grote getallen, die niet in een register passen. Stel we hebben twee getallen van 64 bit, die we willen optellen. We zetten de getallen in d0 en d1 en d2 en d3. In d0 staat het hoge langwoord van het eerste getal, in d1 het lage, en in d2 en d3 staan het hoge en lage langwoord van het tweede getal. Met de sequentie: add.l d1,d3 ; add low halves; set eXtend bit if a carry occurs. addx.l d0,d2 ; and add hi halves with extend. tellen we nu de twee lange getallen zo op dat er een correct antwoord in d2 en d3 komt te staan. Bij andere processors zouden we naar de toestand van het Carry bit moeten gaan kijken om te bepalen of we d2 moeten corrigeren voor een carry bij de optelling van d1 en d3. De ADDX instructie mag niet in alle adressing modes worden gebruikt, maar alleen in de modes addx. dn,dn ; en addx. -(an),-(an) ; Als we nog veel grotere getallen hebben kunnen we ze dus in het geheugen zetten met de hoogste (meest significante) byte op het laagste adres, en ze dan optellen met de sequentie: move #4,CCR ; carry bit op nul en zero bit op een zetten addx.l -(a0),-(a1) ; addx.l -(a0),-(a1) ; addx.l -(a0),-(a1) Waarbij natuurlijk a0 naar het laagste deel van het ene en a1 naar het laagste deel van het andere enorme getal wijst voor we beginnen. In het voorbeeld tellen we twee getallen van 96 bits op, maar dat kan natuurlijk naar believen worden uitgebreid. Conditiecodes. Waar we het nog niet over hebben gehad, zijn de conditiecodes. Het eenvoudigste is ADDA, (en SUBA) die hebben namelijk geen invloed hierop. Alle andere add (en sub) instructies beinvloeden de conditiecodes wel. Als het resultaat 0 is, wordt de Z vlag gezet. Als het hoogste bit van het resultaat 1 is, wordt de N vlag gezet. Als het resultaat bovendien niet in de gebruikte grootte past, worden de X, C en V vlaggen gezet. Er is voor optellen en aftrekken geen verschil tussen het gedrag van de C en V vlaggen. ( Slechts bij weinig instructies, nl de BCD groep en de shift/rotate groep, is dat er wel.) Aftrekken. De instructies van deze groep zijn geheel analoog aan de optellingen. We hebben een SUBQ voor het aftrekken van getallen tussen 1 en 8, een SUBI voor het aftrekken van grotere getallen, een SUB voor gewone aftrekkingen waarbij een van de operands een dataregister moet zijn, een SUBA voor aftrekkingen van een adresregister, en een SUBX instructie voor het bewerken van zeer grote getallen. Een paar voorbeelden: subq.l #8,d1 suba.w #$200,a0 subi.b #$20,-(a6) sub.w (a0),d6 subx.l d1,d2 Merk op dat de -a vorm prioriteit heeft over de -i vorm als er zowel een directe source als een adresregister destination is. De -q vorm heeft weer prioriteit over de -a vorm als er een kleine source-operand en een adresregister destination is. Merk ook op dat hoewel de constructie moveq #2,a0 ; illegaal! verboden is, de constructie addq.l #2,a0 ; wel legaal wel mag. Overigens zal het assemblerprogramma ons er aan herinneren als we hier fouten in maken; ik vergis me ook nog wel eens. Het herstellen van zo'n vergissing is echter een kwestie van seconden. We zien dat de 68000 instructieset zo regelmatig is opgebouwd dat we na de optellingen nauwelijks nog woorden vuil hoeven te maken over de aftrekkingen. Een buitenbeentje: BCD operaties. BCD staat voor Binary Coded Decimal. Dit is een speciale weergave van getallen in de computer. Het is me eerlijk gezegd een raadsel waarom dit er in gebouwd is; programmeurs kunnen in COBOL, een ouderwetse maar nog veel gebruikte programmeertaal, specificeren dat een berekening in decimale waarden moet worden uitgevoerd, zodat er bij optellingen van bv. guldens en centen geen onverwachte afrondingsfouten kunnen optreden. Dat wordt makkelijker gemaakt door deze instructies. Bij het weergeven van breuken in het binaire stelsel is het niet altijd mogelijk dit exact te doen, want er zijn natuurlijk repeterende binaire breuken, zoals er ook repeterende decimale breuken zijn, en de precisie waarmee we een getal kunnen weergeven wordt beperkt door een eindig geheugen. Er zijn zelfs getallen die niet repeteren maar ook niet in een eindig aantal decimalen (of binalen) achter de komma weer te geven zijn, zoals Pi, of e. Dat is allemaal geen probleem, meer dan 32 bits = 8 cijfers nauwkeurigheid is vrijwel nooit echt nodig, maar het komt voor dat programmeurs die zich dat niet realiseren diep geschokt tot de ontdekking komen dat 1 / 3 * 3 voor een computer niet 1 is maar 0.9999999... Nu is zelfs dat voor de meeste mensen nog te verteren maar waarom is (1/5) * 5 = 4.9999...? 1/5 is toch gewoon 0.2? Niks repeterends aan. Stop. In binaire notatie 1/5 = 0.00110011001100110011001100110011..... en repeteert dus wel. Sommige breuken zijn dus in decimale notatie wel maar in binaire notatie niet exact weer te geven. Dat is een 'fact of life'. Om nu te zorgen dat we niet op onverwachte momenten op zulke breuken stoten bestaat de BCD notatie. (Denk ik.) Hoe werkt het: in een byte tellen we niet tot 255 maar van 0 tot 99, door in iedere halve byte een cyfer van 0 tot 9 weer te geven, gewoon volgens de hex code, met dit verschil dat we de letters a-f weglaten. van 99 komen we dus niet naar 9A als we er eentje bij tellen maar naar 00. De binaire weergave van het getal 99 in BCD is 1001 1001. Voor getallen in deze notatie bestaan een paar instructies, die allemaal alleen op byte grootte operands slaan. Getallen in deze notatie nemen veel meer geheugen in beslag dan gewone binaire getallen, en het rekenen ermee duurt veel langer. Ik zal er dus niet verder op ingaan behalve door ze even te noemen. ABCD, Add Binary Coded Decimal telt twee BCD bytes op. SBCD trekt twee BCD bytes af. NBCD Negate Binary Coded Decimal maakt van een BCD byte het 10-complement. en dat zijn de laatste woorden die ik hieraan vuil maak.