LINGUAGGI

CORSO DI
ASSEMBLY

Seconda puntata del corso di programmazione sul linguaggo Assembly MC68000, il linguaggio macchina dell'Amiga



di Paolo Russo


Come si programma in Assembly

   Il primo passo per realizzare un programma è, naturalmente, sedersi e pensare con calma a cosa si deve fare; è consigliabile poi scarabocchiare una prima versione del programma su carta. Si può quindi passare alla fase di editing: con l'ausilio di un qualunque editor, come il comando ED del CLI, si digita il programma e si salva il testo (anche un word processor va bene, purché capace di salvare in ASCII). Se si decide di usare ED bisogna entrare in CLI, richiamare l'editor digitando ED nomesorgente (dove nomesorgente è il nome che si decide di assegnare al testo, noto in gergo programmatorio come codice sorgente) e, dopo aver inserito tutto il testo, salvarlo con ECC X. Bisogna quindi convertire il testo così ottenuto in un programma in linguaggio macchina; questa operazione, che nell'ambito dei linguaggi evoluti è svolta da un interprete o da un compilatore, richiede invece nel nostro caso un assemblatore (più diffusamente noto come assembler). Ne esistono di molti tipi e in questa serie di articoli verrà impiegato il macroassembler standard prodotto dalla Metacomco; gli altri assembler possono differirne nell'uso delle macro, delle label locali (nn$) e delle direttive di assembly come CNOP 0,2 che serve ad allineare il programma a indirizzo pari: se quindi avete un assembler diverso leggete il relativo manuale. Se invece avete lo stesso assembler potete richiamarlo, sempre da CLI, con ASSEM nomesorgente -O nomeoggetto, dove quest'ultimo parametro è il nome con cui desiderate battezzare il programma assemblato, generalmente noto come codice oggetto. Occorre infine linkare il tutto con il comando ALINK nomeoggetto TO nomedefinitivo; ALINK si trova, assieme ad ASSEM, nella directory C del disco dell'assembler e se il CLI ha qualche difficoltà nel trovarlo dovrete specificare il pathname completo (ASSEM-DEVEL:c/ASSEM e ASSEM-DEVEL:c/ALINK) quando li attivate. Dopo tutta questa procedura avrete ottenuto un file chiamato nomedefinitivo che può essere mandato in esecuzione da CLI semplicemente digitandone il nome. Altri tipi di assembler possono avere l'editor e il linker incorporati (caratteristica che fece la fortuna del TurboPascal), allo scopo di semplificare la vita del programmatore. I possessori del sopracitato assembler della Metacomco sono vivamente consigliati di usare estesamente il RAM Disk e le sequenze di comandi in base alla propria esperienza individuale, per non estenuare in misura eccessiva il drive e la propria pazienza.
   Se il vostro programma non funziona esiste un'ampia gamma di tool per il debugging come monitor e disassembler (in genere presenti nello stesso package); il primo vi consente di controllare da vicino lo stato dei registri del processore e delle locazioni di memoria durante l'elaborazione, permettendo di inserire dei breakpoint (istruzioni di stop temporaneo) nel programma o di eseguire un'istruzione alla volta (tracing); il secondo riconverte qualunque codice oggetto in qualcosa di abbastanza simile al codice sorgente originario, anche se si perdono tutte le label.

Struttura di una linea

   L'Assembly non prevede l'uso del multistatement, cioè della possibilità di inserire più comandi in una stessa riga. Ogni linea di programma possiede una struttura ben precisa e può essere pensata come suddivisa  in campi, la lunghezza di ognuno dei quali può però variare da linea a linea. I campi sono quattro: label, istruzione, operandi e commento. Una label (etichetta) è una corta stringa alfanumerica usata per marcare un punto del programma, come riferimento per i salti e per la manipolazione delle variabili; e importante notare che le label esistono solo per l'assemblatore, non per il microprocessore. Se a esempio si contrassegna un punto del programma con una label e poi in un altro punto si inserisce un'istruzione di salto a quella etichetta l'assembler sostituirà alla label, nell'istruzione di salto, un numero che rappresenta l'indirizzo di memoria a cui saltare. Il microprocessore ragiona sempre in termini di indirizzi numerici: le label sono comode per gli esseri umani e per questo motivo l'assembler le adotta, convertendole poi in numeri e indirizzi al momento di generare il codice oggetto. Il campo della label può essere vuoto.
   Il campo successivo, quello dell'istruzione, non può mai essere nullo e deve contenere il nome dell'istruzione stessa, mentre gli operandi, se presenti, devono essere inseriti nel campo successivo, separati l'uno dall'altro da una virgola (non possono esservene più di due). Il commento finale è opzionale. Si può, se lo si desidera, riservare un'intera linea a scopo di commento, inserendo nella prima colonna un carattere particolare che in genere è un asterisco ma che può in effetti variare da assembler ad assembler. I vari campi devono essere separati da almeno uno spazio; questo significa che anche se in una linea non c'è alcuna label occorre comunque lasciare almeno uno spazio prima di digitare il nome dell'istruzione, in modo da non confondere l'assembler.

Byte, word e long word

   I registri del 68000, come illustrato nella scorsa puntata, sono a 32 bit, ossia sono delle long word. Nonostante ciò, non sempre il processore esegue calcoli con 32 bit: per motivi di ordine pratico ci si limita spesso a 16 oppure 8 bit. La lunghezza dei dati su cui si opera non è intrinseca al dato stesso ma deve essere specificata in ogni istruzione. Per esempio esistono tre forme dell'istruzione ADD: ADD.B, ADD.W e ADD.L che sommano rispettivamente numeri a 8, 16 e 32 bit. Naturalmente B, W ed L significano rispettivamente byte, word e long word. L'istruzione ADD.B D1 ,D2 somma gli otto bit meno significativi di D1 e D2, ponendo il risultato negli otto bit meno significativi di D2. I rimanenti 24 bit di D2 non vengono alterati; eventuali riporti da e verso il nono bit vengono troncati senza pietà.
   Quando non è presente alcun suffisso l'assembler parte solitamente dal presupposto che esista un .W sottinteso; in altre parole ADD D1,D2 significa ADD.W D1,D2.
   Quando si lavora con i registri occorre tenere presente che ogni operazione su byte e word agisce sempre sulla parte meno significativa del registro coinvolto. I registri indirizzi si comportano in maniera alquanto originale: per motivi tecnici è vietato operare su di loro con una lunghezza di soli otto bit; se si tenta di scrivervi, sommarvi o sottrarvi un numero a soli 16 bit questo viene automaticamente esteso a 32 bit e l'operazione coinvolge poi l'intero registro. Esempio: ADD.B # 10,A0 è illegale, ADD.W # 10,A0 e ADD.L # 10,A0 agiscono entrambe su 32 bit. L'unica eccezione a questa regola è costituita dalle istruzioni ADDQ.W #n,An e SUBQ.W #n,An che agiscono sulla sola word inferiore di An.
   Questa è un'importante eccezione che è opportuno ricordare, in quanto capita sovente di incrementare o decrementare un registro indirizzi con ADDQ e SUBQ, con l'intenzione di agire su long word, e di dimenticare il .L nella illusoria consapevolezza che tutte le operazioni sui registri indirizzi avvengano a 32 bit. Il bug che ne deriva colpisce sporadicamente (solo quando si verifica un riporto da o verso la word superiore, che resta invece inalterata) ed è quindi assai arduo da snidare se non se ne sospetta l'esistenza. Questo bug è così sfuggente che non è troppo raro trovarlo addirittura in qualche sistema operativo, come ha potuto accertare l'autore dell'articolo mentre, armato di disassembler, passava al setaccio la ROM del suo QL.

I modi di indirizzamento

   Abbiamo visto in precedenza come l'istruzione ADD D1, D2 sommasse il contenuto di D1 al registro D2; in questo caso l'istruzione fondamentale è ADD mentre D1 e D2 sono i suoi operandi. Che cosa avremmo potuto utilizzare in luogo di D1 e D2? In altri termini, quali sono i possibili operandi su cui può agire l'istruzione ADD?
   Nell'ambito di un linguaggio evoluto questa domanda non avrebbe senso: ovunque un'istruzione richieda un parametro di un certo tipo è possibile utilizzare una qualunque espressione che, una volta calcolata, fornisca un valore del medesimo tipo. Per esempio in AmigaBASIC i comando PSET (x,y) traccia un punto alle coordinate x e y, dove questi due parametri possono essere costituiti da espressioni a valore numerico di arbitraria lunghezza e complessità. Nel linguaggio Assembly invece non si può mai sostituire un parametro con un'espressione, a meno che il parametro non sia una costante (cioè un numero), che può essere sostituita da una espressione formata da costanti, la quale verrà calcolata in fase di assemblaggio e non in fase di run-time. A esempio ADD # 13,D5 può essere scritto come ADD #8 + 5,D5 mentre ADD D1 + D0,D2 non ha senso in Assembly. Del resto, se fosse sufficiente usare il segno ' +' per eseguire un'addizione non ci sarebbe alcun bisogno dell'istruzione ADD.
   In Assembly esiste una ristretta gamma di forme che un'espressione può assumere perché sia lecito usarla come operando, e quel che è peggio è che non tutte queste forme sono ammesse in ogni istruzione. In un linguaggio evoluto non è possibile stimare esattamente quanta memoria occupa una linea di programma, ma in Assembly sì: l'occupazione di memoria di una singola istruzione è sempre pari a due byte più il numero di byte aggiuntivi richiesti da ognuno degli operandi. La tabella 1 mostra le espressioni consentite, chiamate modi di indirizzamento, assieme al loro consumo di memoria; passiamole brevemente in rassegna:

   #n: l'operando è il numero n. Se l'istruzione è veloce (quick), ossia se il suo nome termina per Q (MOVEQ, ADDQ, SUBQ) l'uso di # n come primo operando non richiede byte aggiuntivi.

   Dn: l'operando è il registro dati Dn, o meglio lo è il dato contenuto in Dn.

   An: come sopra, con la differenza che il registro coinvolto è un registro indirizzi. La distinzione è sensata in quanto molte istruzioni operano con una sola delle due classi di registri.

   (An): questo modo di indirizzamento e i seguenti sono indiretti. L'operando è la locazione di memoria puntata da An, ossia il cui in.dirizzo è contenuto in An.

   (An)+ : come sopra, ma con l'aggiunta del postincremento: dopo che il dato puntato da An e stato utilizzato, An viene incrementato di un numero pari alla lunghezza del dato in questione.

   -(An): come sopra, ma con il predecremento: prima che il dato venga utilizzato, An viene decrementato della lunghezza del dato.

   d16(An): l'operando è la locazione di memoria il cui indirizzo si ottiene sommando il numero di 6 ad An. La costante d16, detta displacement o spiazzamento, è a 16 bit. Sarebbe forse stato piij logico indicare questo modo come (An + d16), ma questo formato non è standard e non viene riconosciuto dagli assem blatori.

   d8(An,i): come sopra, ma per ottenere l'indirizzo occorre sommare anche i , detto registro indice, che può essere un qualunque registro, sia dati che indirizzi, Lo spiazzamento ammesso è a soli otto bit, mentre l'indice i può essere inteso sia come word che come tong word a seconda del suffisso che gli viene posposto.

   d16(PC): l'operando è la locazione di memoria il cui indirizzo si ottiene sommando d16 al program counter. Questo modo di indirizzamento è spesso impiegato per manipolare dati e variabili incorporati nel programma stesso e il cui indirizzo è quindi vincolato alla posizione che il programma occupa nella RAM. In tal modo il programma che si ottiene è position independent, cioè indipendente dalla posizione; ciò significa che il programma è in grado di trovare i suoi dati in qualunque zona di memoria venga caricato. Questo argomento sarà ripreso in futuro. Purtroppo i modi di indirizzamento relativi al program counter funzionano solo per leggere dati dalla memoria e non per scriverveli, a causa di una assai discutibile scelta dei progettisti.

   d8(PC,i): come sopra, con l'aggiunta di un registro indice. Non molto usato a causa della limitazione imposta sul displacement, serve a manipolare tabelle di dati incluse nel programma.

   a16: l'operando è la locazione il cui indirizzo è il numero a16, che risulta quindi essere un indirizzo a 16 bit. Questo modo di indirizzamento può essere usato solo nei 32K inferiori di RAM.

   a32: come sopra, ma l'indirizzo è a 32 bit e consente di raggiungere qualunque locazione di memoria. Quando si specifica un indirizzo è l'assembler a decidere se considerarlo un a16 o un a32.

   range: utilizzato unicamente dall'istruzione MOVEM, è un insieme di registri. Per esempio, D0-D3/D5/A1 /A3-A5 è un range che,comprende i registri D0, D1, D2, D3,

   label: utilizzata nei salti, per specificare il punto a cui si salta. Tramite le label si può anche manipolare un dato incorporato nel programma: in questo caso l'assembler convertirà automaticamente la label in un d8(PC,i), a seconda dei casi, come si vedrà meglio in seguito.

   Tutti gli indirizzi, gli indici e gli spiazzamenti sono intesi come numeri dotati di segno.

Alcune istruzioni

   L'istruzione più usata in ogni programma è MOVE, che, come si può arguire dal nome stesso, sposta dati da un punto a un altro e può essere considerata un'istruzione di assegnazione, simile all'arcaico LET di alcuni dialetti BASIC. Il comando MOVE richiede due operandi che devono essere separati, com'è d'uso in Assembly, da una virgola; il primo di essi, detto sorgente, fornisce il valore da assegnare al secondo, detto destinazione. Per esempio MOVE.W D1,D2 copia la word meno significativa di D1 in quella di D2, senza alterarne la metà superiore; MOVE.L D0,A3 assegna il contenuto di D0 ad A3; MOVE.B # 10,D6 assegna il valore dieci al byte inferiore di D6 mentre M0VE.B 10,D6 legge il byte di memoria all'indirizzo dieci e lo pone in D6 (attenti al ' #' ! MOVE è l'istruzione che consente la massima libertà nella scelta dei modi di indirizzamento per gli operandi; quasi ogni combinazione di modi è lecita. La maggior parte delle altre istruzioni consente tale libertà per uno solo dei due operandi, quello sorgente o quello destinazione, a scelta del programmatore, mentre l'altro parametro può variare in un ambito molto limitato (solitamente #n e Dn, ma non sempre). A esempio ADD.W D1,D1, ADD.W 16(A3),D1 e ADD.W D1,16(A3) sono lecite mentre ADD.W 16(A3),16(A3) non lo è; MOVE.W 16(A3),16(A3) è invece perfettamente lecita, per quanto improduttiva in questo caso particolare data la coincidenza dei due operandi. Tutto questo può sembrare un po' complicato ed eccessivamente mnemonico, ma l'esperienza insegna che il programmatore medio apprende in breve tempo con l'esercizio le combinazioni istruzione-operandi proibite e le rare volte che commette un errore l'assembler stesso lo segnala prontamente.
   Esiste poi una piccola gamma di istruzioni di uso comunissimo che non consentono quasi nessuna libertà nella scelta dei parametri: a esempio MOVEQ # n,Dn, che richiede assolutamente un dato immediato (a otto bit, che viene esteso a 32, cosa questa atipica per un MOVE) come sorgente e un registro dati come destinazione. Una curiosità: la Q sta per quick, ossia veloce, in quanto la suddetta istruzione consuma poca memoria ed è caratterizzata da un basso tempo di esecuzione; in altre parole essa è di uso molto pratico. Le istruzioni che limitano fortemente la scelta degli operandi sono tra le più usate; accade quindi qualcosa di simile al modo in cui alcuni linguaggi umani, come l'italiano e l'inglese, gestiscono i verbi: tra di essi, quelli irregolari sono quasi sempre i più usati.
   Un operando capace di assumere qualunque forma viene chiamato indirizzo effettivo (effective address) e abbreviato in ea; per esempio la forma generale dell'istruzione di trasferimento è MOVE ea,ea, per sottolineare il fatto che non esiste quasi nessuna limitazione nella scelta dei modi di indirizzamento; l'istruzione di addizione può invece assumere una delle seguenti forme: ADD ea,Dn - ADD ea,An - ADD # n,ea - ADD Dn,ea. Anche l'addizione, come l'assegnazione, possiede una forma 'irregolare' veloce, che può assumere solo la forma ADDQ # n,ea dove n deve essere compreso tra uno e otto (viene però esteso a 8, 16 o 32 bit in base al suffisso .B, .W o .L). Questa simpatica istruzione è l'equivalente 68000 della meno flessibile INC di altri microprocessori.

Un piccolo esempio

   Fino a questo momento non sono ancora state presentate sufficienti nozioni da consentire la realizzazione di un vero e proprio programma. Si può però prendere in considerazione qualche microscopico esempio. Il problema è il seguente: esistono tre interi a 16 bit in memoria agli indirizzi consecutivi 100000, 100002 e 100004, che per brevità indicheremo come A, B e C; bisogna porre nel terzo la somma dei primi due. Esistono svariati modi per farlo e ne prenderemo in considerazione alcuni.
 
 

PROGRAMMA 1

MOVE A,D0
MOVE B,D1
ADD D1,D0
MOVE D0,C

PROGRAMMA 2

MOVE A,D0
ADD B,D0
MOVE D0,C
 

PROGRAMMA 3

MOVE.L #A,A0
MOVE (A0)+,D0
ADD (A0)+,D0
MOVE D0,(A0)

   Il primo metodo è il meno efficiente (forse il compilatore di un linguaggio evoluto lo sceglierebbe), il secondo è il più logico e intuitivo mentre il terzo, sfruttando il fatto che i dati in memoria sono consecutivi, è il più compatto e veloce. Non assemblate realmente questi programmi, se lo faceste otterreste una Guru Meditation dovuta all'assenza di un'istruzione di ritorno e alla arbitraria e distruttiva alterazione della word 100004. Se anche per assurdo tutto funzionasse non vedreste comunque nulla sullo schermo. Prossimamente, quando sarà stato illustrato l'uso dei flag e delle istruzioni di controllo, sarà possibile realizzare qualche programma vero, anche se breve.
 

SIMBOLO
N. BYTE
NOME
Q B W L
#n 0 2 2 4 immediato
DN / 0 0 0 diretto a registro dati
An / 0 0 0 diretto a registro indirizzi
(An) / 0 0 0 indiretto a registro
(An)+ / 0 0 0 indiretto a registro con postincremento
-(An) / 0 0 0 indiretto a registro con postdecremento
di6(An) / 2 2 2 indirietto a registro con offset
d8(An,i) / 2 2 2 indiretto a registro indicizzato
d16(PC) / 2 2 2 relativo al program counter
d8(PC,i) / 2 2 2 relativo al program counter, indicizzato
a16 / 2 2 2 assoluto corto
a32 / 4 4 4 assoluto lungo
range / / 2 2 lista di registri
label (0,2,4) label
    Se una label segue l'istruzione Bcc, DBcc e BSR allora viene convertita in un offset a 16 bit (n.byte=2). Se invece segue l'istruzione Bcc.S o BSR.S allora viene convertita in un offset a 8 bit (n.byte=0) altrimenti:
 
label
viene convertito in a16 o a32
 (n.byte=2,4)
label (PC) viene convertito in d16(PC)  (n.byte=2)
label (PC,i) viene convertito in d8(PC,i)  (n.byte=2)
Tabella 1: modi di indirizzamento del 68000.