s.d.s.2006 Il linguaggio assembly 8086 Introduzione Il linguaggio macchina Il linguaggio naturale di un microprocessore è il linguaggio macchina. Nel linguaggio macchina non esistono riferimenti astratti o simbolici e tutte le operazioni sono eseguite direttamente sui registri o in locazioni assolute di memoria. La programmazione in linguaggio macchina è stata a lungo l’unica possibile, all’inizio dell’epoca del calcolo elettronico, prima dell’introduzione degli assemblatori e dei compilatori. Il linguaggio macchina non è altro che l’insieme delle istruzioni definite per un particolare processore. Ogni istruzione è identificata dal suo codice, di solito riportato in binario o esadecimale. Il linguaggio macchina si compone di istruzioni alle quali fanno immediatamente seguito i relativi operandi. Un esempio di linguaggio macchina per il processore Intel 8086 è il seguente codice per il confronto del contenuto dell’accumulatore AX con la costante 2066: 00111101 00010010 00001000 Per facilitare la descrizione delle istruzioni e degli operandi, si fa uso della notazione esadecimale in alternativa a quella binaria. L’esempio precedente assume in esadecimale questo aspetto: 3D 12 08 Resta però difficile lavorare anche con questa notazione. Il codice rimane indistinguibile dagli operandi e solo a fatica, con l’aiuto di una tabella di conversione si riconosce in 3D l’istruzione di confronto. Un certo impegno è anche necessario per la traduzione di 12 08 in (1*16)+2+(8*256)=2066. (viene memorizzato prima il byte della parte bassa e poi il byte della parte alta del numero considerato a 16 bit) Oggi la programmazione in linguaggio macchina non ha praticamente più alcun interesse. Ovunque possibile viene fatto uso di linguaggi ad alto livello. Nei casi in cui si vuole programmare “il più vicino possibile” alle risorse della macchina si fa uso di un linguaggio assembler. Assembler Il codice binario (o la sua rappresentazione equivalente esadecimale) usato nel linguaggio macchina è molto scomodo; per l’uomo è molto più facile raffigurare e lavorare con simboli e messaggi piuttosto che con cifre. I linguaggi assembler o assembly sono stati introdotti proprio per eliminare i problemi di uso del linguaggio macchina. Le caratteristiche principali dei linguaggi assembler sono le seguenti: 1. In assembler le istruzioni non sono identificate da codici astratti ma da simboli letterali con significato mnemonico. ADD significa ad esempio addizione. 2. Alle variabili viene fatto riferimento per nome e non per locazione assoluta di memoria. 3. E’ possibile definire istruzioni macro assembler, composte a loro volta da altre istruzioni, e richiamarle nel programma. L’istruzione assembler corrispondente all’esempio in linguaggio macchina sopra riportato è: CMP AX, 2066 Pag. 1 s.d.s.2006 che senza dubbio è più facile da comprendere rispetto alla rappresentazione binaria e esadecimale del linguaggio macchina. L’assembler estende le istruzioni originali del linguaggio macchina, identificate da appositi simboli, con proprie istruzioni di definizione dati, operative, ecc. Il programma assembler viene tradotto in linguaggio macchina da un programma apposito, chiamato assemblatore. Ogni istruzione assembler corrisponde a una istruzione in linguaggio macchina. Un ulteriore vantaggio dell’assembler rispetto al linguaggio macchina è che l’assemblatore prima di tradurre il codice di ingresso effettua un controllo sulla sua correttezza. Questo contribuisce ad identificare ed eliminare eventuali errori. Il controllo degli errori da parte dell’assemblatore è formale e si riferisce solo all’aspetto delle istruzioni. In assembler, a differenza dei linguaggi ad alto livello, tutto è permesso. In assembler non esistono tipi di dati e la stessa distinzione tra dati e codice non è molto netta; nulla impedisce di trattare il codice come se fossero dei dati e viceversa. Il ricorso all’assembler può essere importante per generare delle funzionalità particolari e non disponibili nell’ambito del linguaggio di cui si fa uso ( S.Operativi o programmazione in tempo raale) o nei casi in cui sia importante la dimensione del codice macchina generato ( microcontrollori con capacità di memoria limitata). Consideriamo a titolo di esempio due programmi che svolgono la stessa funzione: entrambi scrivono sullo schermo la parola “ciao”. Il primo programma è scritto in C, il secondo in assembler. #include <stdio.h> main() { printf(“ciao \n”); } DSEG SEGMENT Outstr db “ciao”,13,10,”$” DSEG ENDS ;segmento dati SSEG SEGMENT stack dw 32 dup (?) SSEG ENDS ;segmento catasta CSEG SEGMENT assume cs:cseg, ds:dseg, ss:sseg start: mov bx,dseg mov ds,bx mov dx,offset outstr mov ah,09h int 21h ;segmento codice mov ah,4Ch int 21h CSEG ENDS END start ;termine programma ;richiamo MS-DOS ;DS=DSEG via bx ;puntatore a “ciao” ;uscita su schermo ;richiamo MS-DOS ; termine programma ; inizio a start Pag. 2 s.d.s.2006 Il primo programma è molto più compatto del secondo, oltre che più facile da leggere e capire. Il programma assembler è a prima vista certamente molto più complicato ed è necessario un certo tempo per analizzarne e comprenderne le funzioni, che in questo caso sono comunque molto semplici. Entrambi i programmi funzionano secondo lo stesso principio, richiamando un modulo del sistema operativo per la presentazione di una stringa sullo schermo. Nel programma in linguaggio C i dettagli di questa chiamata sono nascosti al programmatore al quale è sufficiente scrivere l’istruzione printf . Spetta al compilatore generare la chiamata al sistema operativo, aggiungere alla stringa i codici di controllo “a capo” e “ritorno carrello”, ecc. Nel programma assembler è necessario tenere esplicitamente conto di tutti questi aspetti. Le chiamate al sistema operativo hanno luogo per mezzo dell’istruzione INT 21h. La stringa di uscita è definita nell’area dati; con essa devono essere indicati esplicitamente i codici di “a capo” (ASCII 10) , “ritorno carrello” (ASCII 13) e termine stringa “$”. La differenza nello spazio occupato in memoria dai due programmi è evidente quando si passa a compilarli e collegarli. Le dimensioni in byte dei codici sorgente, oggetto (compilato) e eseguibile (collegato) dei due programmi sono qui confrontate: Cod. sorgente Cod. compilato Prog. eseguibile Programma C 57 byte 578 15 KB Programma assembler 682 194 610 La differenza nelle dimensioni del programma sorgente assembler è dovuta al maggior spazio richiesto per le istruzioni e per i commenti necessari. Il codice compilato è però già più compatto per il programma assembler. La differenza più rilevante si nota dopo che i programmi sono stati collegati alle rispettive biblioteche e routine di servizio: il programma in assembler manca quasi completamente di overhead, che invece caratterizza il programma in C. Il programma scritto in assembler è più rapido a caricarsi e eseguirsi; entrambi i programmi producono lo stesso risultato e non sono distinguibili solo sulla base di quest’ultimo. Non esistono criteri assoluti per optare per un linguaggio ad alto livello oppure assembler. Se con l’assembler è possibile scrivere programmi più efficienti, è anche vero che la loro stesura prende molto più tempo rispetto allo scrivere programmi in un linguaggio avanzato. Anche la documentazione e la manutenzione di programmi assembler sono più difficili e dispendiosi. La programmazione in assembler resta comunque di attualità in tutti i casi dove con la programmazione ad alto livello si raggiungono i limiti di capacità di memoria o velocità di esecuzione di una macchina. Alcune funzioni, in particolare quelle che agiscono direttamente sulle risorse del sistema, non sono realizzabili se non in assembler. Aspetto non trascurabile della programmazione assembler è il suo carattere didattico. Indipendentemente dal numero e tipo di prodotti software installati in un sistema, il processore, almeno con le architetture attuali, opera su istruzioni di macchina assimilabili a quelle di un programma assembler. L’assembler aiuta quindi a comprendere meglio i meccanismi di funzionamento della macchina. Esistono diversi linguaggi assembler per lo stesso processore, così come di ogni linguaggio ad alto livello esistono versioni differenti. Il microprocessore 8086 (e famiglia) Il microprocessore 8086, appartiene a una famiglia di processori prodotti dalla società Intel. Esso ha una lunghezza di parola di 16 bit, ma può lavorare anche su byte (8 bit). Il bus dati dell’8086 è di 16 bit e la memoria indirizzabile 1 Mbyte. Pag. 3 s.d.s.2006 Il set di istruzioni del microprocessore 8086 comprende circa 70 istruzioni di base combinabili in un massimo di 30 modi diversi di accesso alla memoria. L’8086 ha fatto la sua comparsa nel 1978. Un anno più tardi è uscito il tipo 8088, la cui unica differenza rispetto all’8086 è di fare uso di un bus dati di 8 bit invece che di 16, pur mantenendo intatta la possibilità di indirizzare 1 Mbyte di memoria. In tutto il resto, e in particolar modo in riferimento agli aspetti di programmazione, i due microprocessori sono identici. Nel 1983 la Intel ha prodotto il microprocessore 80286. Esso ha una lunghezza d parola e un bus dati a 16 bit, ma la possibilità di indirizzare 224 byte di memoria (16 Mbyte). Novità di questo nuovo processore è la possibilità di lavorare in due modi, reale e protetto. Nel modo reale l’indirizzamento è limitato a 1 Mbyte di memoria e le istruzioni sono quelle dell’8086 mantenendo quindi la compatibilità con quest’ultimo. Nel modo protetto si possono indirizzare 16 Mbyte di memoria oltre che fare uso di nuove istruzioni. L’8086 è stato progettato per facilitare la multiprogrammazione per mezzo sia di istruzioni sia di una gestione più razionale dello spazio di memoria. Nel 1986 compare l’Intel 80386. La sua lunghezza di parola e il suo bus sono a 32 bit; esso ha inoltre la possibilità di indirizzare direttamente 4 Gbyte di memoria, l’intero spazio descrivibile con 232 indirizzi. I registri Il microprocessore 8086 dispone di 14 registri per l’esecuzione delle operazioni aritmetiche e logiche e per l’indirizzamento delle aree dati, di programma e di catasta. Alcuni registri sono associati a particolari operazioni. 15 87 AH BH CH DH 0 Accumulatore AX Base BX Conteggio CX Dati DX AL BL CL DL 15 0 SP BP SI DI Puntatore catasta Puntatore base Indice sorgente Indice destinazione CS DS SS ES Segmento codice Segmento dati Segmento catasta Segmento extra IP Contatore istruzione ODITSZ A P C Flag Pag. 4 s.d.s.2006 I registri più flessibili e più frequentemente usati sono i registri dati AX, BX, CX, DX. Questi registri possono essere considerati sia come entità uniche a 16 bit, sia come coppie di registri a 8 bit. L’accumulatore AX è ad esempio composto dai registri a 8 bit AH e AL, singolarmente indirizzabili. In maniera analoga si hanno BH, BL, CH, CL, DH, DL. Il registro AX serve da accumulatore principale. Operazioni aritmetiche e logiche vengono di solito effettuate su questo registro. Tutte le operazioni di ingresso e uscita vengono effettuate via AX. BX è il registro base. Questo è l’unico dei registri di uso generale che può anche essere utilizzato per puntare a locazioni di memoria. CX è il registro di conteggio. Il contenuto di CX viene automaticamente diminuito di 1 a ogni ciclo nell’esecuzione di istruzioni iterative, facilitando in questo modo molte operazioni su stringhe e di spostamento dati. Il registro dati DX è anche utilizzato come puntatore in alcune istruzioni di ingresso/uscita. I registri AX, BX, CX, DX possono essere usati liberamente per operazioni aritmetiche e logiche, a eccezione di alcune operazioni quali la moltiplicazione MUL, la divisione DIV e altre ancora che richiedono che alcuni degli operandi si trovino in registri prestabiliti. Un seondo gruppo di registri è quello dei registri puntatori e indice. SP (stack pointer) è il puntatore all’inizio della catasta; viene automaticamente modificato all’esecuzione delle istruzioni PUSH, quando nuovi dati vengono messi in catasta, e POP quando i dati vengono tolti dalla catasta. BP è il puntatore base e serve per accedere direttamente a dati che si trovano sulla catasta. I due indici sorgente SI e destinazione DI puntano alla memoria dati. Essi vengono usati per passare più dati in successione da una locazione iniziale a una finale, ad esempio nel trasferimento di stringhe di caratteri. I registri CS, DS, SS, ES puntano ai segmenti definiti in memoria. In particolare il segmento di codice CS punta al segmento contenente le istruzioni del programma, DS (data segment) al segmento dell’area dati e SS (stack segment) al segmento di catasta. Non è necessario che i segmenti siano localizzati in zone diverse della memoria; le rispettive aree possono coincidere. E’ infine a disposizione un altro registro di segmento, ES (extra segment). Esso è usato per indirizzare un’area dati diversa da quella alla quale fa riferimento DS. IP(instruction pointer) è il puntatore alla prossima istruzione di programma che deve essere eseguita. IP è automaticamente incrementato ogni volta che una nuova istruzione viene letta dalla CPU. Le 9 flag del sistema 8086 sono raccolte in un registro collettivo che ha come nome stato del processore. Ogni flag corrisponde ad un bit del registro di stato; i rimanenti 7 bit non sono utilizzati. Le flag non sono indirizzabili singolarmente, per l’accesso ad esse occorre far uso delle funzioni definite a questo scopo. Le flag sono: Carry, Parità, Auxiliary carry, Zero, Sign, Trap, Interrupt, Direction e Overflow. In generale, le flag servono sia per determinare le condizioni di esecuzione del processo, sia per riportare il risultato di un’operazione e gli eventuali errori. Per questo i valori delle flag vengono modificati mediante l’uso di apposite istruzioni e anche automaticamente durante l’esecuzione di operazioni aritmetiche e logiche. Le flag Carry, Auxiliary carry, Overflow e Sign riflettono il risultato dell’ultima operazione aritmetica che è stata eseguita, segnalando eventuali condizioni di errore. Le flag Parità e Zero indicano in maniera compatta le condizioni di parità sui bit di una parola e se il risultato di una operazione è uguale a zero. Le flag Direction, Interrupt e Trap sono sotto il controllo esclusivo dell’operatore. Direction indica se le operazioni di trasferimento di blocchi di dati vanno eseguite per indirizzi crescenti o decrescenti di memoria; Interrupt serve all’abilitazione o disabilitazione delle interruzioni e Trap mette il processore in un modo di esecuzione di una sola istruzione alla volta. Quest’ultima flag è di utilità in fase di controllo dei programmi. Pag. 5 s.d.s.2006 Indirizzamento Il microprocessore 8086 è in grado di indirizzare un massimo di 1 Mbyte di memoria. Dato che la sua lunghezza di parola è di 16 bit, con i quali è possibile identificare 2 16 =65536 celle elementari di memoria (byte), è stato scelto di impiegare un sistema un sistema di indirizzamento più complesso basato sull’uso dei segmenti. Lo spazio di memoria di 1 Mbyte è considerato diviso in 65536 segmenti, ciascuno di 16 byte di lunghezza. Gli indirizzi effettivi di memoria sono calcolati combinando una parola di segmento e una di offset (spostamento) all’interno del segmento. Il valore del segmento è moltiplicato per 16 e addizionato alla parola di offset per ottenere il puntatore alla locazione assoluta di memoria. 15 00 0 0 0 indirizzo offset ( 16 bit) segmento (16 bit) 0000 + 19 locazione effettiva di memoria 0 (20 bit) In conseguenza di questo particolare metodo di indirizzamento gli indirizzi sono scritti con una coppia di parole segmento:offset. Ad esempio in forma esadecimale come: 0A16:255E. La locazione effettiva indirizzata è: 0255E +0A160 ________ 0C7BE Secondo questo principio diversi indirizzi segmentati possono corrispondere allo stesso indirizzo assoluto. In riferimento all’esempio mostrato, anche 0C16:055E e 0825:446E puntano alla medesima locazione. Il calcolo delle locazioni effettive di memoria avviene internamente al processore ed è completamente trasparente al programmatore. In altre parole, da programma vengono caricati i valori nei segmenti e nei registri di puntamento alla memoria. Le locazioni effettive ne risultano così definite e non è necessario calcolarle esplicitamente. Questo tipo di indirizzamento è il risultato di una scelta di sistema. Il microprocessore 8086 doveva avere una lunghezza di parola di 16 bit; 64 Kbyte erano però troppo poche per le applicazioni già previste in fase di sviluppo. 1 Mbyte di memoria di memoria indirizzabile è stato considerato un valore realistico e più adatto alle applicazioni del processore. E’ concepibile estendere il concetto degli indirizzi segmentati. Con due parole di 16 bit sono indirizzabili un massimo di 232 locazioni, corrispondenti a 4 Gbyte di memoria. Questo metodo è stato scartato nell’8086 probabilmente per evitare di appesantire il sistema con linee di bus più grandi, con un tempo più lungo per il calcolo degli indirizzi ecc. Pag. 6 s.d.s.2006 Nell’esecuzione della maggior parte delle operazioni il microprocessore 8086 fa uso di registri predeterminati. Il puntatore alla successiva istruzione da eseguire è dato dalla coppia CS:IP . Il puntatore alla locazione attuale di catasta è SS:SP. Non è possibile ridefinire questi registri, ma solo modificarne indirettamente il valore. Per l’indirizzamento dell’area dati si ha maggiore flessibilità. Principalmente si fa uso del segmento dati DS e del segmento extra ES insieme ai puntatori SI e DI o il registro base BX. Struttura di un programma MS Macro Assembler Nomi Un nome è una sequenza di caratteri che serve a identificare univocamente variabili e procedure definite in un programma. Esso può avere lunghezza qualsiasi; l’assembler fa riferimento ai primi 31 caratteri. Un nome deve iniziare con una lettera o uno dei segni “_”, “?”, “%”, “@”; esso non può iniziare con un numero, anche se è permesso fare uso di numeri internamente al nome. L’assembler interpreta allo stesso modo lettere maiuscole e minuscole che possono essere così liberamente scambiate. Un nome infine, non deve riprodurre uno dei termini predefiniti dall’assembler come ADD, MOV, LABEL, LENGTH, ecc. Formato delle istruzioni assembler Una riga di programma assembler ha il seguente aspetto: [nome] istruzione operandi [;commenti] Delle quattro parti che la compongono, solo l’istruzione di operazione e gli eventuali operandi sono obbligatorie. Le parti rimanenti sono opzionali. Il nome serve a identificare una parte di programma o una variabile: out_string DB “Ciao” ;variabile continue: MOV AX,BX ;parte di programma display_text PROC NEAR ;sottoprogramma L’istruzione rappresenta la parte principale dell’intera linea di programma. Essa può essere sia un’istruzione originale del linguaggio macchina 8086 sia un’istruzione Macro Assembler. Gli operandi, se richiesti dall’istruzione, devono sempre essere indicati. Il commento è un testo libero preceduto da un punto e virgola. Il commento serve a descrivere meglio l’istruzione assembler che da sola è, per sua natura, difficilmente comprensibile. E’ buona norma che i commenti siano concisi ma allo stesso tempo il più esplicativi possibile. E’ superfluo ripetere nel commento il significato di una istruzione (lo si capisce infatti dall’istruzione stessa). Piuttosto è meglio scrivere perché si fa uso proprio di una certa istruzione. Evitare quindi di scrivere: MOV AX,BX ; copia BX in AX scrivere piuttosto: MOV AX,BX ; parametro in AX ; per passaggio a sottoprogramma Rappresentazione dei dati La rappresentazione dei dati è una parte introdotta con il Macro Assembler. L’insieme di istruzioni di macchina originali 8086 non comprende infatti alcuna definizione di struttura di dati, ma offre soltanto istruzioni per operare su locazioni di memoria. Pag. 7 s.d.s.2006 Nel Macro Assembler sono a disposizione un certo numero di direttive per la definizione dei dati. I dati vengono incorporati nel codice macchina prodotto nella posizione e nel formato con il quale essi vengono definiti. Uno dei vantaggi maggiori del linguaggio assembler rispetto a quello di macchina è proprio la possibilità di fare riferimento alla memoria con operatori simbolici (i nomi delle variabili) invece che con indirizzi assoluti. Per la definizione delle variabili sono a disposizione le seguenti istruzioni: DB define byte DW define word DD define doubleword DQ define quadword DT define tenword Con DB si definiscono uno o più byte, singolarmente indirizzabili: constant textstring all_zero DB DB DB 10 “Questa è una riga di testo” 100 dup (0) Nel primo esempio è allocato (riservato) un byte di memoria all’indirizzo stabilito dal contatore; esso viene inizializzato con il valore 10. Nel secondo esempio, la stringa “Questa è una riga di testo” è scritta sotto forma di codici ASCII, a iniziare dal byte al quale punta textstring. Nel terzo esempio viene generata una stringa lunga 100 byte, ognuno dei quali assume valore iniziale 0. La direttiva dup serve a far ripetere 100 volte il valore indicato ( ). DW (Define word) serve alla definizione di una variabile che ha lunghezza di una parola (16 bit): total_count DW 0 Per total_count è allocata in memoria una parola con valore iniziale 0. Nell’8086 le parole sono memorizzate in due byte adiacenti. Nel byte con indirizzo inferiore è messa la parte meno significativa della parola, seguita dalla parte più significativa. La parola 1234 hex è pertanto memorizzata come 34 12. Occorre fare pertanto attenzione quando si accede ai singoli byte di una parola in memoria di fare riferimento alla parte effettivamente desiderata. DD, DQ e DT servono rispettivamente per l’allocazione in memoria di 4 byte, 8 byte e 10 byte. L’assemblatore è piuttosto rigido nel controllare, per quanto possibile, i tipi di dati. In fase di assemblaggio viene segnalato un errore se si prova a trasferire una variabile definita come byte in un registro della lunghezza di una parola o viceversa. Dato che le istruzioni operative, pur mantenendo lo stesso nome, hanno aspetto differente nel codice macchina generato, l’assemblatore ha bisogno di una dichiarazione esplicita dei tipi di dati per decidere quale istruzione vada scelta in ogni caso specifico. In assembler non è fatta alcuna distinzione tra dati variabili e costanti. Ogni dato in memoria è singolarmente indirizzabile e modificabile. L’istruzione EQU (nome EQU espressione) accetta quale espressione una costante, una stringa di testo o un’espressione. L’espressione numerica viene calcolata prima di essere assegnata alla variabile simbolica nome, un testo viene invece sostituito senza subire variazioni. a_1 EQU 24 ; a_1 prende il valore 24 pK EQU 10*20 ; pK prende il valore 200 msg EQU “esecuzione interrotta” DOScall EQU INT 21h Pag. 8 s.d.s.2006 inc10 EQU ADD AX,10 msg, DOScall e inc10 sono sostituiti dalle linee di testo indicate dopo EQU ogni volta che si incontrano nel programma sorgente. L’uso di EQU contribuisce a rendere più leggibili i programmi una volta che le proprie istruzioni mnemoniche siano state correttamente scelte. Segmenti Il microprocessore 8086 fa uso di 4 segmenti per indirizzare le sezioni di memoria in cui sono contenuti i dati, il codice del programma e la catasta. Con l’istruzione Macro Assembler SEGMENT è possibile dichiarare separatamente le aree da associare ai segmenti. La fine di un segmento è indicata da ENDS (End Segment) AreaDati SEGMENT … AreaDati ENDS ProgArea ProgArea SEGMENT … ENDS La definizione esplicita di tutti i segmenti non è obbligatoria. Un programma può consistere di un solo segmento. I registri DS e ES sono inizializzati dal sistema operativo all’atto del caricamento di un programma in modo da puntare all’inizio dell’area dedicata al processo, che non corrisponde a quella dove sono contenuti i dati. Se si vuole che DS punti effettivamente all’area dati, occorre caricare in esso l’indirizzo relativo con le istruzioni seguenti: MOV AX, AreaDati MOV DS,AX ; DS=AreaDati Nell’istruzione di spostamento dati MOV la destinazione è indicata per prima. Il passaggio attraverso AX è necessario in quanto non è possibile caricare direttamente una costante in un registro di segmento ma occorre passare per uno degli altri registri. Il caricamento diretto nel registro CS non è permesso in alcun caso. Questo registro può essere modificato solo da istruzioni di controllo dell’esecuzione del programma che agiscono contemporaneamente sul registro IP, quali ad esempio JMP, CALL, RET ecc. Una direttiva molto importante per le operazioni sui segmenti è quella di ASSUME. Questa direttiva serve ad indicare all’assemblatore quale segmento, tra quelli dichiarati, è da considerare associato ai registri di segmento. La direttiva ASSUME precede le istruzioni di movimento dati: ASSUME DS: AreaDati , CS: ProgArea ASSUME indica all’assemblatore in quale segmento si trovano le variabili in modo che possano essere calcolati i puntatori ad esse relativi. Direttive Semplificate Per definire i segmenti è possibile utilizzare una modalità semplificata .MODEL SMALL ; o altri modelli .DATA … ; i dati Pag. 9 s.d.s.2006 .CODE inizio: MOV AX,@DATA MOV DS,AX ; inizializza DS al segmento dati … … .STACK … END inizio Istruzioni di base Movimento dati Istruzione fondamentale per il movimento dei dati da una locazione di memoria a un’altra è MOV, che può operare sia su un byte (8 bit) che su una parola (16 bit). MOV ha come operandi registri, puntatori alla memoria oppure costanti. L’assemblatore riconosce dagli operandi se l’operazione è a 8 o a 16 bit e genera l’istruzione di macchina opportuna. Nell’istruzione MOV al primo operando corrisponde la destinazione e al secondo il punto di origine, come nell’esempio seguente dove un byte viene spostato da CH in AL: MOV AL,CH CH non viene modificato in seguito a questa operazione. In maniera analoga ha luogo lo spostamento di una parola da registro a registro: MOV BX,AX In questo caso il valore di AX viene copiato in BX. In linguaggio macchina è possibile far riferimento a locazioni assolute di memoria, come nel caso seguente dove una parola è copiata dalla memoria in AX: MOV AX,[04h] In questa operazione è sottointeso il segmento DS e la parola copiata si trova alla locazione assoluta DS:04. In un programma MS Macro Assembler i riferimenti alle locazioni di memoria riservate alle variabili sono simbolici e in questo caso si fa uso del puntatore a byte, BYTE PTR o a parola WORD PTR: alfa DW 1234h MOV AL, BYTE PTR alfa Il contenuto del byte meno significativo, a causa della registrazione in memoria a rovescio nella variabile alfa , 34h, è copiato in AL. Modi di indirizzamento della memoria Con l’istruzione MOV AL,[300h] si copia in AL il contenuto della locazione di memoria DS:300h. Il segmento DS è sottointeso. Tale modo di indirizzare la memoria è denominato diretto. Un altro modo di indirizzare la memoria è quello indiretto tramite registri. Si utilizzano in questo caso i seguenti registri: [BX] [SI] [DI] [BP] Pag. 10 s.d.s.2006 Per esempio l’istruzione MOV AH,[BX] copia il contenuto della locazione di memoria indirizzata da DS:BX in AL.. L’effettivo indirizzo dell’operando sorgente si ottiene dal segmento DS *16 + offset dove per offset si intende il contenuto di BX. E’ come se fosse scritto MOV AH,DS:[BX] . Nel caso in cui si volesse forzare il processore ad utilizzare un altro registro di segmento, questo deve essere esplicitamente indicato: MOV AH,CS:[BX] MOV AH,ES:[BX] Anche i registri SI e DI possono essere utilizzati con la stessa modalità di BX. MOV AL, [SI] MOV BL,[DI] il registro DS è sempre sottointeso. L’indirizzamento indiretto può essere utilizzato per ll’operando di destinazione. L’istruzione MOV [BX], AL copia il contenuto di AL nella locazione indirizzata da DS*16+BX . Il registro BP, a differenza degli altri, fa sempre riferimento all’area di stack che è indirizzata attraverso il registro di segmento SS . MOV BX,[BP] è trattato come MOV BX,SS:[BP] Istruzioni aritmetiche Le istruzioni aritmetiche più frequentemente usate sono: ADD, SUB, MUL, DIV, INC, DEC, CMP. Le istruzioni di addizione ADD e sottrazione SUB sono complementari; esse operano su un qualsiasi registro o locazione di memoria. Il primo operando contiene in seguito il risultato dell’operazione. Addizione della costante 5 ad AX, con risultato in AX: ADD AX,5 Addizione del contenuto di CX a quello di BX, con risultato in BX: ADD BX,CX Per l’addizione del contenuto di un registro a quello di una locazione di memoria valgono le considerazioni già fatte a proposito di MOV. Per i riferimenti alle variabili in memoria vengono usati i puntatori BYTE PTR e WORD PTR. Sottrazione del contenuto di BH da quello di AH, con risultato in AH: SUB AH,BH INC (incremento) e DEC (decremento) operano rispettivamente addizionando e sottraendo 1 al registro o alla locazione di memoria indicata. Queste istruzioni sono eseguite più rapidamente delle corrispondenti ADD reg/mem ,1 e SUB reg/mem ,1; inoltre prendono meno spazio in memoria. Incremento di SI di 1: INC SI La moltiplicazione MUL e la divisione DIV fanno uso di alcuni registri predefiniti. In entrambi i casi si distingue tra operazioni a 8 e a 16 bit; eventuali condizioni di overflow o di errore sono segnalate per mezzo delle flag aritmetiche. Moltiplicazione e divisione fanno sempre uso dell’accumulatore principale AX come uno degli operandi. Per la moltiplicazione tra due numeri a 8 bit uno degli operandi è messo in AL, l’altro può trovarsi in qualsiasi registro o locazione di menoria. Nell’istruzione il registro AL è sottointeso: MUL BL ; AL*BL -> AX Pag. 11 s.d.s.2006 Il risultato di una moltiplicazione con operando a 8 bit può avere lunghezza fino a 16 bit ed è messo in AX. Se gli 8 bit superiori del risultato sono diversi da 0, le flag CF e OF assumono valori 1 ad indicare la presenza di cifre significative. In maniera analoga avviene la moltiplicazione tra due parole a 16 bit. Una parola è messa in AX , l’altra è in un registro o locazione di memoria qualsiasi. Il risultato è diviso: i 16 bit inferiori sono in AX , i 16 bit superiori in DX. Se il contenuto della parola più significativa DX, è diverso da 0, CF e OF prendono valore 1. Nell’8086 sono possibili due operazioni di divisione, di una parola a 16 bit per un byte e di una parola doppia (a 32 bit) per una parola a 16 bit. La divisione ha luogo sempre su numeri interi senza segno e produce un quoziente e un resto anch’essi interi. Nella divisione a 8 bit il dividendo è messo in AX e il divisore, registro o locazione di memoria, è esplicitamente indicato: DIV CL In seguito a questa operazione il quoziente (intero) viene riportato in AL e il resto in AH. Nella divisione a 16 bit, il dividendo ha una lunghezza di 32 bit. I suoi 16 bit più significativi sono messi in DX e quelli meno significativi in AX. Effettuata l’operazione, il quoziente è ritornato in AX e il resto in DX. DIV WORD PTR alfa L’operazione di divisione è sempre riferita ad un registro o una locazione di memoria. L’assembler ha bisogno anche in questo caso di un riferimento univoco al tipo di indirizzamento in memoria, per BYTE o per WORD, in modo da generare le istruzioni macchina corrispondenti. Istruzioni logiche Il microprocessore 8086 dispone di istruzioni che permettono di operare sui registri con le più importanti funzioni logiche: AND, OR, NOT, XOR, di spostamento (shift) SHL e SHR e di rotazione ROL e ROR. Le istruzioni AND, OR, NOT e XOR agiscono contemporaneamente su tutti i bit di un registro o locazione di memoria. L’istruzione AND è usata in particolare per mascherare o estrarre singoli bit da una parola. Come per le funzioni matematiche, gli operandi delle istruzioni logiche sono registri e locazioni di memoria. Il primo degli operandi indicati è anche quello destinato a ricevere il risultato dell’operazione: AND AX,BX Solo nel caso di NOT l’operando è uno solo: NOT DX Nel seguente esempio l’istruzione AND è utilizzata per estrarre i primi 4 bit più significativi del registro AX , azzerando i successivi: AND AX,0F000h ( h serve a indicare che la costante rappresenta un valore esadecimale). L’istruzione di OR esclusivo XOR, è usata per azzerare il contenuto di un registro in alternativa a MOV reg,0: XOR AX,AX Le istruzioni di spostamento (shift) a destra e a sinistra, SHL e SHR e di rotazione (rotate) a destra e a sinistra, ROR e ROL richiedono che il numero di bit di spostamento o rotazione si trovi nel registro CL. Uno spostamento a sinistra di 4 bit del contenuto di AX viene effettuato nel modo seguente: MOV CL,4 SHL AX,CL Pag. 12 s.d.s.2006 Per lo spostamento e la rotazione di un solo bit è sufficiente la forma diretta dell’istruzione, passando all’assemblatore l’operando 1: SHR AX,1 Non è possibile fornire un operando costante diverso da 1; in tal caso occorre far uso del registro CL. Operazioni su catasta La catasta (stack) è una struttura dati utilizzata per memorizzare risultati intermedi di operazioni, per trasferire dati ad altri moduli del programma e per salvare lo stato dei registri quando l’esecuzione del programma principale viene interrotta per passare il controllo ad un sottoprogramma. La catasta è una zona riservata della memoria che può essere localizzata ovunque. L’accesso alla catasta funziona secondo il principio LIFO ( Last In First Out), l’ultimo dato a entrare è il primo a uscire. I registri relativi alla catasta sono il segmento SS (stack segment) e SP (stack pointer), puntatore alla posizione dell’ultimo elemento messo su catasta. SP è sempre riferito a SS; non è possibile cambiare questa definizione. Nell’implemetazione dell’8086 la catasta cresce da una posizione iniziale verso locazioni inferiori di memoria. Sulla catasta possono essere poste solo parole di 16 bit. Le operazioni principali su catasta sono due, PUSH e POP. Con l’istruzione PUSH un registro, oppure due byte di memoria, sono copiati nella locazione di memoria SS:SP dopo che SP è stato diminuito di due (lo stack cresce verso locazioni inferiori di memoria). Operazione inversa a PUSH è POP che toglie parole dalla catasta e le mette nel registro o nella locazione di memoria indicata come operando. Dopo un’operazione di POP , SP è incrementato di 2. Esempio di istruzioni di uso dello stack: PUSH AX POP ES ; ES=AX Dato che le operazioni sono limitate a parole di 16 bit, non è possibile eseguire ad esempio PUSH BH. Inoltre, anche sulla catasta le parole sono registrate con la parte meno significativa per prima. Le flag sono copiate in catasta con un’operazione unica PUSHF. Similmente POPF legge una parola dalla catasta e assegna di conseguenza i relativi valori alle 9 flag. La catasta è usata anche per il passaggio dei dati a sottoprogrammi. Dato che al richiamo del sottoprogramma sulla cima della catasta si trovano i puntatori di ritorno al processo chiamante ( CS e IP ) , non è possibile accedere ai dati contenuti in essa con operazioni di POP. Si fa in questo caso uso del puntatore di base BP caricato con il valore di SP. Le istruzioni relative hanno di solito il seguente aspetto: … PUSH BP MOV BP,SP MOV AX, [BP] +6 ; primo parametro MOV BX, [BP] + 8 ; secondo parametro … POP BP In seguito all’operazione PUSH BP sulla catasta si trovano in ordine, BP,IP,CS, l’ultimo parametro messo sulla catasta con PUSH, il penultimo parametro e così via. Per accedere a questi parametri è quindi necessario addizionare a BP una costante relativa alla loro posizione sulla catasta. Al termine del sottoprogramma, il valore originale di BP è ripristinato con un’istruzione POP. Pag. 13 s.d.s.2006 Operazioni di salto Le operazioni di salto servono ad interrompere l’esecuzione sequenziale di un programma per proseguirla da un altro punto. Le operazioni di salto vengono utilizzate per realizzare cicli iterativi e nella scelta tra più alternative di esecuzione. I salti possono essere incondizionati e condizionati. Con un salto incondizionato il controllo viene direttamente trasferito a un’altra parte del programma: JMP Label_1 In questo caso, l’esecuzione procede dalla locazione indicata Label_1. L’assemblatore converte i riferimenti simbolici in salti a locazioni assolute. I salti condizionati avvengono, come indica il nome, solo se è verificata una particolare condizione. Questa condizione è segnalato dallo stato delle flag; nel caso più frequente vengono usate la flag di carry CF e le flag di indicazione del risultato di un confronto. L’istruzione di salto condizionato relativo alla flag CF si chiama JC (junp if carry): JC cont1 Se il bit di carry ha il valore true (1) , viene effettuato un salto a cont1. La condizione relativa a CF può anche essere che il salto debba avere luogo solo quando la flag ha il valore false (0). L’istruzione usata si chiama JNC (jump if not carry): JNC cont2 In questo caso, se il bit di carry ha valore false, viene effettuato un salto a cont2. Nel caso di un salto condizionato combinato con un’operazione di confronto, è necessario che le due istruzioni vengano eseguite in immediata successione. In questo esempio AX è confrontato con BX; se i loro valori sono diversi è richiesto un salto a cont3: CMP AX,BX JNE cont3 JNE significa jmp if not equal, cioè salta se i due termini non sono uguali. Il confronto vero e proprio avviene con l’istruzione CMP; l’informazione sullo stato di uguaglianza è passata all’istruzione successiva mediante la flag zero. L’istruzione CMP esegue una operazione di sottrazione senza caricare il risultato, ma settando il flag Zero se gli operandi sono uguali. Nel set di istruzioni 8086 sono disponibili altre funzioni di salto condizionato: JE (Jmp if equal), JAE (Jmp if Above or Equal), JB (Jump if Below) , e molti altri. Alcune di queste istruzioni funzionano allo stesso modo come JAE e JNB ( Junp if Not Below). L’istruzione CMP richiede due operandi. Essa può prendere molte forme a seconda del tipo di confronto che ha luogo, tra registri oppure di un registro con un dato costante o il contenuto di una locazione di memoria. Nella forma più semplice e più frequentemente usata vengono confrontati registri tra di loro, oppure registri e costanti: CMP AX,BX CMP BX,5 Le istruzioni di confronto segnalano il risultato di una operazione per mezzo delle sei flag aritmetiche. Consideriamo il seguente esempio. I valori di AX e BX vengono confrontati tra loro. Se il valore contenuto in BX è maggiore di quello contenuto in AX occorre eseguire la parte di codice P1, in caso contrario P2: CMP AX,BX JBE P2 P1: ... … Pag. 14 s.d.s.2006 P2: JMP cont1 … … cont1: Non esistono chiamate condizionate a sottoprogrammi. Se si ha la necessità di richiamare un sottoprogramma solo al verificarsi di una certa condizione occorre far uso di qualche riga di codice in più per aggirare la chiamata quando la condizione non è verificata. Istruzioni di Input / Output Il processore 8086 vede i dispositivi di I/O in modo separato dalla memoria e quindi con istruzioni apposite. Lo spazio di indirizzamento dei dispositivi di Ingresso/Uscita è di 216 =65536 locazioni denominate port. Originariamente i port hanno dimensione di un byte (8 bit ). Le istruzioni per comunicare con i port sono: IN AL,DX OUT DX,AL La prima legge dal port il cui indirizzo è contenuto in DX e copia il valore letto nel registro AL. La seconda invia il contenuto del registro AL nel port il cui indirizzo è contenuto nel registro DX. Si noti che il registro DX a 16 bit è compatibile con il numero totale dei port 65536 (da 0 a 65535). Soltanto con indirizzi di port inferiori o al massimo uguali a 255 si può scrivere l’indirizzo del port direttamente nell’istruzione, senza usare DX: IN AL,61h Si può sempre comunque utilizzare il modo generale: MOV DX,61h IN AL,DX Sottoprogrammi Un tipo particolare di salto incondizionato è il richiamo di un sottoprogramma con l’istruzione CALL: CALL DISPLAY … DISPLAY: … … RET All’istruzione di CALL il processore provvede a salvare in catasta i valori di CS e IP. Nuovi valori per CS:IP corrispondenti alla locazione del sottoprogramma chiamato (nell’esempio DISPLAY), sono caricati nei rispettivi registri in modo che l’esecuzione possa procedere dalla nuova posizione. Il ritorno da sottoprogramma avviene con l’istruzione di chiusura RET. Con RET vengono letti due byte da catasta e assegnati a CS e IP. L’esecuzione continua quindi nel programma originale dall’istruzione successiva a quella di richiamo del sottoprogramma. E’ ovvio che al momento di eseguire RET , il puntatore alla catasta deve far riferimento ai byte corrispondenti a CS e IP. In caso contrario il controllo del programma passa ad una locazione errata che quasi certamente non contiene dati interpretabili come valide istruzioni del programma, provocando quasi subito l’arresto della macchina. Occorre quindi far sempre attenzione, se si fa uso della catasta all’interno di un sottoprogramma, a far corrispondere ad ogni istruzione PUSH una di POP prima di uscire dal sottoprogramma stesso con l’istruzione RET. Pag. 15 s.d.s.2006 Se la catasta è stata usata per passare dati al sottoprogramma, si può far uso dell’istruzione RET [n]. In questo caso n parole vengono cancellate dalla catasta durante l’operazione di ritorno, dopo la lettura di CS e IP. Gestione delle interruzioni Un’ interruzione (interrupt) è una sospensione dell’esecuzione regolare di un programma per permettere l’esecuzione di un programma di servizio. Un’interruzione può essere causata da una condizione fisica quale l’arrivo di un segnale su un port di ingresso, una condizione di errore quale la divisione per zero ecc. Le interruzioni si distinguono in hardware e software, a seconda che queste siano chiamate in seguito ad un evento fisico oppure un’istruzione di programma. Il richiamo da programma (software interrupt) si effettua con: INT n ; n compreso tra 0 e 255 L’esecuzione salta in questo caso al sottoprogramma il cui indirizzo si trova alla locazione 0:4*n e 0:4*n+2. Nel sistema 8086, lo spazio di memoria compreso tra 0:0 e 0:3FF hex è infatti occupato dagli indirizzi o vettori , dei sottoprogrammi di servizio delle interruzioni. I vettori 0-4 sono predefiniti nell’architettura dell’8086 per la gestione di stati di errore o di esecuzione. Gli altri vettori sono liberi, ma nei sistemi MS-DOS alcuni di essi sono utilizzati per le routine interne del sistema operativo. Alcuni vettori sono a disposizione del programmatore. Prima di passare il controllo al sottoprogramma di servizio all’interruzione, vengono salvati sulla catasta i valori di CS, di IP e delle flag. Questi valori sono automaticamente ripristinati con l’esecuzione dell’istruzione IRET (Interrupt Return) posta al termine del sottoprogramma di servizio. E’ compito del sottoprogramma di servizio salvare, e in seguito ripristinare, eventuali altri registri di cui esso fa uso durante l’esecuzione. Le Istruzioni MACRO Con una macro è possibile definire proprie istruzioni, anche di una certa complessità, che vengono trattate dall’assemblatore come se fossero istruzioni originali del sistema. In un certo senso, una macro svolge funzioni analoghe a quelle di un sottoprogramma. In entrambi i casi gruppi di istruzioni che devono essere eseguiti più volte allo stesso modo all’interno di un programma sono isolati e richiamati come una procedura unica. La differenza tra una macro e un sottoprogramma è che le istruzioni della macro vengono copiate e assemblate una per una ogni volta che è fatto riferimento ad essa. Il vantaggio dell’uso delle macro è che si risparmiano chiamate a sottoprogrammi che a loro volta comportato la messa di parametri in catasta, la modifica dei registri CS e IP e il loro successivo ripristino. Lo svantaggio consiste nel fatto che il programma assume dimensioni tanto maggiori quanto più viene fatto uso di macro. In pratica conviene definire brevi gruppi di istruzioni come macro e formare sottoprogrammi per blocchi più lunghi. Una macro può essere definita in un qualsiasi punto del programma, è sufficiente che la definizione preceda il primo richiamo che ad essa viene fatto. La macro è identificata dal suo nome, seguito dall’identificatore MACRO. La seguente macro converte il valore contenuto nei 4 bit più bassi (0-3) del registro AL nel codice ASCII corrispondente, 0-9 e A-F. Se il valore di questi bit è minore o uguale a 9 il codice ASCII è dato dalla sua somma con 30hex; se il valore in ingresso è maggiore di 9, occorre addizionare ancora 7 al risultato per ottenere il codice ASCII relativo alle lettere A-F (41hex-46hex). Conv_ascii MACRO ;input in AL ;output in AL LOCAL cont_1 Pag. 16 s.d.s.2006 Cont_1 AND AL,0Fh ADD AL,30h CMP AL,39h JBE cont_1 ADD AL,7h NOP ENDM ;mascheramento bit 4-7 ;conversione ascii 0-9 ;carattere <=9 ? ;carattere ascii tra 0 e 9 ;conversione ascii A-F ;nessuna operazione Questa macro è chiamata da programma facendo semplicemente uso del suo nome: Conv_ascii L’assemblatore, nell’incontrare la macro, sostituisce una per una, nel punto indicato le istruzioni in essa definite. Pag. 17