Breve panoramica sulla famiglia 80x86 La famiglia 80x06 è nata nel 1981 con il processore 8086 e prosegue attualmente con la linea Pentium. Ciò che accomuna tutti questi processori è che sono tutti compatibili all'indietro, ma ciascuna nuova generazione ha aggiunto via via nuove caratteristiche e maggior potenza di calcolo rispetto ai suoi predecessori. Oggi rimangono ben pochi computer in uso, dotati di processori 8088, 8086, 286, 386 o 486 poiché si tratta di computer lenti e superati. Anche i computer dotati di processore Pentium della prima generazione stanno ormai diminuendo, mano a mano che il software diviene sempre più esigente in termini di risorse e velocità. Pentium MMX, Pentium II e Pentium III rappresentano i processori maggiormente in uso oggi. Rappresentazione dei numeri in notazione binaria Prima di cominciare a capire come programmare in Assembly, è opportuno cercare di comprendere come i numeri sono rappresentati all'interno della CPU. I numero sono rappresentati in notazione binaria, ovvero in base 2. Ci sono diversi termini che sono utilizzati per descrivere numeri di differenti dimensioni, e sono descritti qui di seguito. 1 BIT: 0 Un bit è il tipo di dato più semplice che esiste. Può assumere valore zero oppure uno. 1 NIBBLE: 0000 4 BITS Un nibble è quattro bit, o mezzo byte. Notate che può assumere il valore massimo di 15 (1111 = 15). Esso è la base della notazione esadecimale (base 16), che viene utilizzata per rappresentare i numeri nei programmi Assembly. I numeri esadecimali vanno da 1 a F e sono seguiti da un h per indicare che il numero è espresso in questa notazione. Ad esempio: Fh = 15 decimale. I numeri esadecimali che cominciano con una lettera sono preceduti da uno 0 (zero). 1 BYTE 00000000 2 NIBBLES 8 BITS Un byte è 8 bit o 2 nibble. Un byte ha un valore massimo di FFh (255 decimale). Poiché un byte è 2 byte, la rappresentazione esadecimale è formata da due cifre consecutive seguite da h. Ad esempio: 3Dh. Un byte è anche la dimensione dei registri a 8-bit, che vedremo in seguito. 1 WORD 0000000000000000 2 BYTES 4 NIBBLES 16 BITS Una word è costituita da due byte affiancati. Una word può assumere un valore massimo di FFFFh (65536). Poiché una word è composta da quattro nibble, è rappresentata da quattro cifre esadecimali. Una word è anche la dimensione dei registri a 16-bit. Registri I registri sono locazioni di memoria all'interno della CPU dove un numero può essere memorizzato e manipolato. Vi sono tre dimensioni di registro: 8-bit, 16-bit, e (dal 386 in poi) 32-bit. Vi sono quattro differenti tipi di registri: registri general-purpose, registri di segmento, registri indice e registri di stack. Prima descriveremo i registri principali. I registri di stack e segmento saranno affrontati in seguito. Registri General-Purpose Registri General-Purpose Sono registri a 16-bit. Vi sono quattro registri general-purpose: AX, BX, CX, DX. Sono suddivisi in registri a 8-bit. AX è suddiviso in AH, che contiene il byte alto e AL che contiene il byte basso. Con il processore 386 e superiori vi sono anche registri a 32-bit. Questi hanno gli stessi nomi dei registri a 16-bit ma con una E come prefisso (es.: EAX). Si possono utilizzare AL, AH, AX ed EAX separatamente e considerare come registri separati per alcune oprazioni. Se AX contenesse 24689 decimal: AH 01100000 AL 01110001 AH varrebbe 96 e AL varrebbe 113. Se aggiungessimo 1 ad AL, questo varrebbe 114 e AH rimarrebbe invariato. SI, DI, SP e BP possono essere utilizzati come registri general-purpose, ma hanno usi più specifici. Questi non sono suddivisi in due metà. Registri Indice Questi sono a volte chiamati registri puntatore e sono registri a 16-bit. Sono utilizzati principalmente con le istruzioni che operano con le stringhe di caratteri. Vi sono tre registri indice: SI (source index), DI (destination index) e IP (instruction pointer). Con il processore 386 e superiori vi sono anche registri indice a 32-bit: EDI e ESI. Nota: Come registro indice per le stringhe si può usare anche il registro BX. IP è un registro indice ma non può essere manipolato direttamente poiché contiene l'indirizzo dell'istruzione successiva durante l'esecuzione. Registri di Stack BP e SP sono registri di stack e sono utilizzati quando si ha a che fare con lo stack. Li vedremo in seguito. Segmenti e Offset I primi progettisti del processore 8008 decisero che nessuno avrebbe mai avuto bisogno di utilizzare più di un megabyte di memoria, così costruirono il chip in modo che non potesse indirizzare memoria oltre il megabyte. Il problema è che per indirizzare un intero megabyte sono necessari almeno 20 bit. I registri hanno solo 16 bit, ma i progettisti non ritennero di utilizzare due registri perché credevano che lo spazio di indirizzamento che si otteneva con 32 bit fosse esagerato per chiunque. Pertanto idearono quello che sembrava una soluzione intelligente per risolvere il problema: segmento e offset. Ecco come viene ottenuto l'indirizzamento con due registri, ma senza utilizzare 32 bit. OFFSET = SEGMENT * 16 SEGMENT = OFFSET / 16 (the lower 4 bits are lost) Un registro contiene il segmento e un altro registro contiene l'offset. Unendo insieme i due registri si ottiene un indirizzo a 20-bit. SEGMENT 0010010000010000---OFFSET ----0100100000100010 20-bit Address 00101000100100100010 DS contiene il segmento e SI contiene l'offset. Poiché entrambi hanno dimensione 16-bit gli indirizzi si accavallano. Nello schema è mostrato come la coppia di indirizzi DS:SI è utilizzata per ottenere l'indirizzo di 20 bit. La notazione standard per una coppia Segmento/Offset è SEGMENT:OFFSET. I registri di segmento sono CS, DS, ES, SS. Con il 386 e processori successivi esistono anche FS e GS. I registri di offset sono BX, DI, SI, BP, SP, IP. Con il processore 386 che opera in mpdalità protetta, QUALSIASI registro generale può essere utilizzato come registro di offset. (Eccetto IP, che non può essere manipolato direttamente). Lo Stack Poiché esistono soltanto sei registri che possono essere utilizzati per la maggior parte delle operazioni, è naturale chiedersi come superare questa apparente limitazione. E' semplice. Esiste una speciale area di memoria, denominata stack, in cui possono essere salvati e recuperati i valori desiderati. Quest'area di memoria è organizzata come una pila di piatti. L'ultimo che viene messo è il primo ad essere prelevato. Questo tipo di organizzazione è denominata Last On First Off (LOFO) oppure Last In First Out (LIFO). Come lo stack è organizzato Via via che nuovi dati sono posti nello stack, questo cresce verso il basso. Come si vede dalla figura, lo stack ha inizio ad un indirizzo alto e cresce verso il basso. E' opportuno accertarsi di non mettere troppi dati nello stack, se non si vuole rischiare l'errore di stack overflow. Introduzione alle istruzioni Assembly In Assembly sono disponibili moltissime istruzioni, ma sono soltanto una ventina quelle che bisogna assolutamente conoscere e che vengono utilizzate più spesso. La maggior parte delle istruzioni sono identificate da un codice mnemonico di tre caratteri e sono seguite da uno o più operandi separati da virgole. Per esempio per mettere un dato in un registro, si può utilizzare l'istruzione MOV. mov mov mov mov ax,10 bx,20 cx,30 dx,40 ; put 10 into ax ; put 20 into bx ; put 30 into cx ; put 40 into dx Notate che in Assembly tutto quello che viene scritto dopo il ; (punto e virgola) viene ignorato. Questo è utile per poter commentare il codice. Push e Pop: Due istruzioni per utilizzare lo Stack Abbiamo già parlato dello stack, ma non di come fare ad inserire e prelevare i dati. Ci sono due semplici istruzioni che è necessario conoscere: PUSH e POP. Ecco la sintassi per utilizzarli: PUSH: Inserisce un dato in cima allo stack Sintassi: push dato POP: Preleva un dato dallo stack e lo memorizza in uno specifico registro o variabile. Sintassi: pop registro (o variabile) Questo esempio di codice mostra come utilizzare le istruzioni PUSH e POP. push cx push ax pop cx pop ax ; put cx on the stack ; put ax on the stack ; put value from stack into cx ; put value from stack into ax Notate che i valori di CX e AX vengono scambiati. C'è un istruzione specifica per scambiare i valori contenuti in due registri: XCHG, che può essere usato per rimpiazzare il precedente frammento di codice con la sola istruzione "xchg ax,cx". Tipi di operando Ci sono tre tipi di operando in Assembly: immediati, registro, e memoria. Un operando immediato è un valore numerico conosciuto al momento della compilazione ed è sempre costante. Un operando di tipo registro è un valore contenuto in un registro come ad esempio AX o SI. Un operando in memoria è un valore che viene mantenuto e acceduto in memoria. Some Instructions that you will need to know Questo è un elenco di alcune importanti istruzioni che è necessario conoscere prima di poter comprendere e scrivere programmi in Assembly. MOV: copia un valore da una locazione ad un'altra. Sintassi: MOV destinazione, sorgente Ad esempio: mov ax,10 ; moves an immediate value into ax mov bx,cx ; moves value from cx into bx mov dx,Number ; moves the value of Number into dx INT: richiama una funzione DOS o del BIOS. Sintassi: INT interrupt number Ad esempio: int 21h int 10h ; Calls DOS service ; Calls the Video BIOS interrupt La maggior parte degli interrupt hanno più di una funzione. Questo significa che è necessario fornire un numero specificatore alla funzione che si desidera richiamare. In genere questo numero viene specificato in AH. Ad esempio per stampare un messaggio sullo schermo bisogna fare così: mov ah,9 int 21h ; subroutine number 9 ; call the interrupt Ma prima bisogna specificare cosa stampare. Questa funzione richiede che: la coppia DS:DX contenga il puntatore a dove si trova il contenuto della stringa. La stringa deve essere terminata con il simbolo di dollaro ($). Questo sarebbe semplice da ottenere se il registro DS potesse essere manipolato direttamente; bisogna invece utilizzare il registro AX nel modo seguente. L'esempio mostra la tecnica da utilizzare: mov dx,OFFSET Message ; DX contains offset of message mov mov mov int ax,SEG Message ; AX contains segment of message ds,ax ; DS:DX points to message ah,9 ; function 9 - display string 21h ; call dos service Le direttive OFFSET e SEG servono per ottenere il segmento e l'offset dove si trova il messaggio, e non il suo contenuto. Ora che sappiamo come scrivere il codice per visualizzare il messaggio, non resta che dichiarare il messaggio. Nel segmento dati dichiariamo il messaggio nel modo seguente: Message DB "Hello World!$" Notate che la stringa è terminata con il simbolo di dollaro. Cosa significa 'DB'? DB è un codice mnemonico che sta per "declare byte" e il messaggio è un array di byte (un carattere ASCII occupa un byte). I dati possono essere dichiarati di varie dimensioni: byte (DB), word (DW) e double (DD). Ecco alcuni esempi di dichiarazione di dati: Number1 db ? Number2 dw ? Il simbolo di punto di domanda alla fine significa che il dato non è inizializzato, cioè non ha un valore iniziale. Se si vuole specificare un valore di inizializzazione, è sufficiente scrivere: Number1 db 0 Number2 dw 1 In questo caso Number1 assume valore 0 e Number2 è uguale a 1, quando il programma è mandato in esecuzione. Se si dichiara una variabile come word, non si può copiare il valore di questa variabile in un registro a 8-bit, e allo stesso modo non si può dichiarare una variabile come byte e copiarne il contenuto in un registro a 16-bit. Per esempio: mov al,Number1 mov ax,Number1 ; ok ; error mov bx,Number2 mov bl,Number2 ; ok ; error Tutto quello che è necessario ricordarsi è che si può soltanto copiare byte in registri a 8-bit e word in registri a 16-bit. Il primo programma Assembly Ora che abbiamo illustrato le istruzioni fondamentali e come dichiarare i dati, vediamo un programma Assembly completo, che possa essere compilato. Listing 1: 1STPROG.ASM ; This is a simple program which displays "Hello World!" ; on the screen. .model small .stack .data Message db "Hello World!$" ; message to be display .code mov dx,OFFSET Message ; offset of Message is in DX mov ax,SEG Message ; segment of Message is in AX mov ds,ax ; DS:DX points to string mov int mov int ah,9 21h ax,4c00h 21h END start ; function 9 - display string ; call dos service ; return to dos DOS ;end here Istruzioni per la compilazione Queste sono le istruzioni per compilare e linkare un programma. Se avete un assemblatore diverso da TASM o A86, consultate il vostro manuale di istruzioni. Turbo Assembler tasm file.asm tlink file [/t] L'opzione /t genera un eseguibile .COM, ed è utilizzabile solo se il modello di memoria è dichiarato .small nel file sorgente. A86 a86 file.asm Questo comando compila il programma, generando un eseguibile .COM, indipendentemente dal modello di memoria dichiarato. Qualche semplice esempio Il modo in cui abbiamo specificato l'indirizzo del messaggio da stampare era un po' complicato. Servivano ben tre linee di codice, non del tutto semplici da ricordare. mov dx,OFFSET MyMessage mov ax,SEG MyMessage mov ds,ax Possiamo rimpiazzare tutto questo con una singola linea. Questo rende il codice più semplice da leggere e da ricordare. mov dx,OFFSET MyMessage Perché funzioni, bisogna anche aggiungere queste istruzioni all'inizio del programma. mov ax,@data mov ds,ax Nota: per l'assemblatore A86, la prima delle due linee è leggermente diversa: mov ax,data Questa tecnica si basa sul fatto che tutti i dati in un segmento hanno il medesimo SEG value. Impostando questo valore in DS, evita di doverlo ricaricare ogni volta che desideriamo indirizzare un altro dato nello stesso segmento. Input da tastiera Utilizzeremo l'interrupt 16h, funzione 00h per leggere la tastiera. Questa chiamata legge un dato dal buffer della tastiera. Se nessun dato è presente, aspetta fino a quando non è disponibile. Restutuisce lo SCAN code in AH e la traduzione ASCII in AL. xor ah,ah int 16h ; function 00h - get character ; interrupt 16h Tutto quello di cui dobbiamo preoccuparci per ora è il valore ASCII che si trova in AL. Nota: XOR effettua un OR booleano esclusivo. E' comunemente utilizzato per azzerare un registro o una variabile. Stampare un carattere Il problema è che abbiamo il tasto premuto in AH. Come facciamo a visualizzarlo? Non possiamo usare la funzione 9h perché avremmo dovuto aver già definito una stringa terminandola con il simbolo di dollaro. Questo è invece ciò che dobbiamo fare. ; after calling function 00h of interrupt 16h mov dl,al mov ah,02h int 21h ; move al (ascii code) into dl ; function 02h of interrupt 21h ; call interrupt 21h Se desiderate salvare il valore di AH, è necessario fare PUSH di AX prima della chiamata e recuperarlo in seguito con POP. Controllo del flusso In Assembly c'è un insieme di comandi per il controllo del flusso, come in molti altri linguaggi. Prima di tutto l'istruzione fondamentale: jmp label Tutto quello che fa è di saltare alla locazione specificata e continuare l'esecuzione del codice da quel punto. Per esempio: jmp ALabel . . . ALabel: Cosa dobbiamo fare se vogliamo valutare delle condizioni? Abbiamo letto un valore dalla tastiera ma ora vogliamo fare qualche operazione. Ad esempio stampiamo qualcosa se questo valore è uguale ad una certa costante. Come facciamo? E' semplice. Utilizziamo comandi di salto condizionato. Firstly you compare the operand with the data and then use the correct command. cmp ax,3 je correct ; is AX = 3? ; yes Ecco un elenco di questi comandi: Istruzioni di salto su condizione JA JAE JB JBE JNA JNAE JNB JNBE JZ JE JNZ JNE JC Salta se il primo operando è maggiore del secondo operando Come sopra, ma salta anche se gli operandi hanno lo stesso valore Salta se il primo operando è minore del secondo operando Come sopra, ma salta anche se gli operandi hanno lo stesso valore Salta se il primo operando non è maggiore del secondo operando (JBE) Salta se il primo operando non è maggiore del o uguale al secondo operando (JNB) Salta se il primo operando non è minore del secondo operando (JAE) Salta se il primo operando non è minore del o uguale al secondo operando (JA) Salta se i due operandi sono uguali Come JZ Salta se i due operandi NON sono uguali Come JNZ Salta se il flag di carry è impostato CMP: compara un registro o una variabile con un valore Sintassi: CMP registro o variabile, valore jxx destination Un esempio d'uso è il seguente: cmp al,'Y' je ItsYES ; compare the value in al with Y ; if it is equal then jump to ItsYES Ogni istruzione richiede una certa quantità di spazio nel segmento codice. Si ottiene un avviso dall'assemblatore se si cerca di saltare a locazioni lontane più di 127 byte in entrambe le direzioni. Si può risolvere il problema, modificando una sequenza come questa: cmp ax,10 je done ; is AX 10? ; yes, lets finish in qualcosa come questa: cmp ax,10 ; is AX 10? jne notdone ; no it is not jmp done ; we are now done notdone: Questo risolve il problema, ma in genere è più opportuno organizzare il codice usando chiamate a procedure. Ora vedremo un programma che dimostra input, output e controllo del flusso. Listing 2: PROGFLOW.ASM ; a program to demonstrate program flow and input/output .model tiny .code org 100h start: mov dx,OFFSET Message ; display a message on the screen mov ah,9 ; using function 09h int 21h ; of interrupt 21h mov dx,OFFSET Prompt ; display a message on the screen mov ah,9 ; using function 09h int 21h ; of interrupt 21h jmp First_Time Prompt_Again: mov dx,OFFSET Another ; display a message on the screen mov ah,9 ; using function 09h int 21h ; of interrupt 21h First_Time: mov mov int xor dx,OFFSET Again ; display a message on the screen ah,9 ; using function 09h 21h ; of interrupt 21h ah,ah ; function 00h of int mov mov mov 16h bl,al dl,al ah,02h ; ; ; ; interrupt 16h gets a character save to bl move al to dl function 02h - display character int 21h ; call DOS service cmp bl,'Y' je Prompt_Again cmp bl,'y' je Prompt_Again ; ; ; ; is if is if al=Y? yes then display it again al=y? yes then display it again theEnd: mov dx,OFFSET GoodBye ; print goodbye message mov ah,9 ; using function 9 int 21h ; of interrupt 21h mov ah,4Ch int 21h .DATA CR equ 13 LF equ 10 Message Prompt Again Another GoodBye DB DB DB DB DB ; terminate program ; enter ; line-feed "A Simple Input/Output Program$" CR,LF,"Here is your first prompt.$" CR,LF,"Do you want to be prompted again? $" CR,LF,"Here is another prompt!$" CR,LF,"Goodbye then." end start Alcune istruzioni che si devono conoscere Quello che segue è un elenco di alcuni istruzioni Assembly fondamentali che è molto importante conoscere e che sono utilizzate molto spesso. ADD: Aggiunge il contenuto del secondo operando al valore del primo operando Sintassi: ADD operando1,operando2 Questo esempio aggiunge il valore di operando2 a quello di operando1. Il risultato è memorizzato nell'operando1. Un tipo di dato immediato non può essere utilizzato come operando1, ma può essere utilizzato come operando2. SUB: Sottrae il contenuto del secondo operando dal valore del primo operando Sintassi: SUB operand1,operand2 Questo esempio sottrae il valore di operando2 da quello di operando1. Il risultato è memorizzato nell'operando1. Un tipo di dato immediato non può essere utilizzato come operando1, ma può essere utilizzato come operando2. MUL: Moltiplica due interi senza segno (sempre positivi) IMUL: Moltiplica due interi con segno (positivi o negativi) Sintassi: MUL registro o variabile IMUL registro o variabile Questo esempio moltiplica AL o AX per il valore del registro o della variabile specificati. Se si specifica un operando della dimensione di un byte, AL viene moltiplicato per l'operando e il risultato è posto nel registro AX. Se invece si specifica un operando della dimensione di una word, AX viene moltiplicato per l'operando e il risultato è posto in DX:AX. Con i processori 386, 486 e Pentium può essere utilizzato anche il registro EAX e il risultato viene posto in EDX:EAX. DIV: Divide due interi senza segno (sempre positivi) IDIV: Divide due interi con segno (positivi o negativi) Sintassi: DIV registro o variabile IDIV registro o variabile L'istruzione di divisione funziona in modo simile alla moltiplicazione, dividendo il valore contenuto in AX per il registro o la variabile specificati. Il risultato è memorizzato in due diverse locazioni. AL contiene il quoziente della divisione mentre il resto è posto in AH. Se l'operando è un registro a 16-bit, il numero alla locazione DX:AX viene diviso per l'operando e il quoziente e il resto sono posti rispettivamente in AX e DX. Introduzione alle procedure In Assembly una procedura è l'equivalente di una funzione in C o in Pascal. Una procedura fornisce un modo semplice per incapsulare un calcolo complesso in modo che possa essere utilizzato senza preoccuparsi di come funziona internamente. Con procedure attentamente strutturate, è possibile realizzare programmi di grosse dimensioni, nascondendo i dettagli implementativi e ottenendo codice più leggibile. Ecco come si definisce una procedura: PROC AProcedure . . ; some code to do something . ret ; if this is not here then your computer will crash ENDP AProcedure Invocare una procedura è altrettanto semplice. Tutto quello che si deve fare è quanto segue: call AProcedure Il programma che segue è un esempio di come usare una procedura. E' simile al primo programma visto, tutto quello che fa è stampare la frase "Hello World!" sullo schermo. Listing 3: SIMPPROC.ASM ; This is a simple program to demonstrate procedures. It should ; print Hello World! on the screen when ran. .model tiny .code org 100h Start: call Display_Hi mov ax,4C00h int 21h ; Call the procedure ; return to DOS Display_Hi PROC mov dx,OFFSET HI mov ah,9 int 21h ret Display_Hi ENDP HI DB "Hello World!" ; define a message end Start Procedure che passano parametri Le procedure non sarebbero così utili se non si potessero passare parametri per modificarli o utilizzarli all'interno della procedura. Vi sono tre diversi modi di passare un parametro: in un registro, in memoria e sullo stack. Di seguito ci sono tre esempi che svolgono tutti lo stesso compito. Stampano il carattere ASCII 254 in un punto specifico dello schermo. Le dimensioni (in byte) dei tre programmi una volta compilati sono rispettivamente: 38 per il parametro passato con registro, 69 per il parametro passato in memoria e 52 per il parametro passato sullo stack. Passare i parametri nei registri Il vantaggio di passare i parametri nei registri è la semplicità. Basta copiare i parametri nei registri prima di invocare la procedura. Listing 4: PROC1.ASM ; this a procedure to print a block on the screen using ; registers to pass parameters (cursor position of where to ; print it and colour). .model tiny .code org 100h Start: mov mov mov mov dh,4 dl,5 al,254 bl,4 ; ; ; ; row to print character on column to print character on ascii value of block to display colour to display character call PrintChar mov ax,4C00h int 21h ; print our character ; terminate program PrintChar PROC NEAR push bx push cx ; save registers to be destroyed xor bh,bh mov ah,2 int 10h ; clear bh - video page 0 ; function 2 - move cursor ; row and col are already in dx pop bx xor mov mov int bh,bh ah,9 cx,1 10h pop cx ; restore bx ; ; ; ; display page - 0 function 09h write char & attrib display it once call bios service ; restore registers ret ; return to where it was called PrintChar ENDP end Start Passare i parametri in memoria Il vantaggio di questo metodo è l'estrema semplicità, ma ha lo svantaggio di rendere il programma più ingombrante e lento. Per passare i parametri attraverso la memoria, è sufficiente copiare i parametri in variabili situate in memoria. Si può usare una variabile nello stesso modo in cui si usa un registro, ma bisogna tenere conto che le istruzioni che utilizzano i registri sono molto più veloci. Listing 5: PROC2.ASM ; this a procedure to print a block on the screen using memory ; to pass parameters (cursor position of where to print it and ; colour). .model tiny .code org 100h Start: mov mov mov mov Row,4 ; row to print character Col,5 ; column to print character on Char,254 ; ascii value of block to display Colour,4 ; colour to display character call PrintChar mov ax,4C00h int 21h ; print our character ; terminate program PrintChar PROC NEAR push ax cx bx xor mov mov mov int bh,bh ah,2 dh,Row dl,Col 10h mov mov xor mov mov int al,Char bl,Colour bh,bh ; ah,9 ; cx,1 ; 10h ; ; save registers to be destroyed ; clear bh - video page 0 ; function 2 - move cursor ; call Bios service pop bx cx ax display page - 0 function 09h write char & attrib display it once call bios service ; restore registers ret ; return to where it was called PrintChar ENDP ; variables to store data Row db Col db Colour db Char db ? ? ? ? end Start Passare i parametri sullo stack Questo è il modo più potente e flessibile di passare i parametri. Il problema è che risulta un poco più complicato. Listing 6: PROC3.ASM ; this a procedure to print a block on the screen using the ; stack to pass parameters (cursor position of where to print it ; and colour). .model tiny .code org 100h Start: mov dh,4 ; mov dl,5 ; mov al,254 ; mov bl,4 ; push dx ax bx row to print string on column to print string on ascii value of block to display colour to display character ; put parameters onto the stack call PrintString ; print our string pop bx ax dx mov ax,4C00h int 21h ;restore registers ;terminate program PrintString PROC NEAR push bp mov bp,sp push cx ; save bp ; put sp into bp ; save registers to be destroyed xor mov mov int bh,bh ; clear bh - video page 0 ah,2 ; function 2 - move cursor dx,[bp+8] ; restore dx 10h ; call bios service mov mov xor mov mov int ax,[bp+6] bx,[bp+4] bh,bh ; ah,9 ; cx,1 ; 10h ; pop cx pop bp ; character ; attribute display page - 0 function 09h write char & attrib display it once call bios service ; restore registers ret ; return to where it was called PrintString ENDP end Start Stack di una procedura con due parametri Per prelevare un parametro dallo stack, tutto quello che serve fare è conoscere l'indirizzo dove si trova. L'ultimo parametro si trova all'indirizzo BP+2 e il precedente all'indirizzo BP+4 Cosa sono i "Memory Models"? In molti esempi abbiamo utilizzato la direttiva .MODEL per specificare il modello di memoria da utilizzare. Cosa significa? Sintassi: .MODEL MemoryModel Dove MemoryModel può essere SMALL, COMPACT, MEDIUM, LARGE, HUGE, TINY o FLAT. Tiny Significa che esiste un unico segmento per codice e dati. Questo tipo di programma può essere un eseguibile .COM. Small Significa che per default tutto il codice si trova in un unico segmento e tutti i dati dichiarati si trovano in un altro solo segmento. Questo implica che tutte le procedure e le variabili sono indirizzate come NEAR utilizzando solo gli offset. Compact Significa che per default tutto il codice si trova in un unico segmento ma i dati possono essere dichiarati in più segmenti diversi. Questo implica che i dati sono indirizzati utilizzando sia il segmento sia l'offset. Gli elementi di codice (procedure) sono NEAR e le variabili sono FAR. Medium E' l'opposto di compact. I dati sono NEAR e le procedure sono FAR. Large Questo significa che sia le procedure sia le variabili sono FAR. Bisogna indirizzarle entrambe utilizzando sia il segmento, sia l'offset. Flat Questo modello di memoria si usa quando il processore opera in modalità protetta e la memoria non è segmentata. E' utilizzabile solo con sistemi operativi a 32-bit, come Windows 9x e Windows NT. Macro (in Turbo Assembler) Nota: Tutti gli esempi sono in Turbo Assembler. Le macro sono molto utili per raggruppare sequenze di istruzioni usate frequentemente, per le quali non è opportuno realizzare una procedura. Le macro sono sostituite al momento della compilazione con il codice che contengono. Questa è la sintassi per definire una macro: Name_of_macro macro ; ; ; endm a sequence of instructions Questi due esempi illustrano le macro per salvare e ripristinare di gruppi di registri: SaveRegs macro push push push push ax bx cx dx endm RestoreRegs macro pop pop pop pop dx cx bx ax endm Notate che i registri sono prelevati in ordine inverso a quello con cui sono stati salvati nello stack. Per usare una macro in un programma è sufficiente utilizzare il nome della macro come una qualsiasi istruzione. SaveRegs ; some other instructions RestoreRegs Questo esempio mostra come si può usare una macro per risparmiare righe di codice. Questa macro stampa semplicemente un messaggio sullo schermo. OutMsg macro SomeText local PrintMe,SkipData jmp SkipData PrintMe db SomeText,'$' SkipData: push ax dx cs mov dx,OFFSET cs:PrintMe mov ah,9 int 21h pop cs dx ax endm L'unico problema con le macro è che usandole troppo spesso, la dimensione del codice del programma tende ad aumentare e si rischia di incontrare errori di duplicazione delle label e di nomi delle variabili. Il modo corretto di risolvere questo problema è usare la direttiva LOCAL che consente di dichiarare nomi che valgono solo all'inerno delle macro. Sintassi: LOCAL name Dove name è il nome di una variabile locale o di una label. Macro con parametri Un'altra utile proprietà delle macro è che possono avere parametri. Il numero di parametri è limitato solo dalla lunghezza della linea di codice. Sintassi: Name_of_Macro macro par1,par2,par3 ; ; commands go here ; endm Questo esempio somma il primo e il secondo parametro e pone il risultato nel terzo: AddMacro macro num1,num2,result push ax ; save ax from being destroyed mov ax,num1 ; put num1 into ax add ax,num2 ; add num2 to it mov result,ax ; move answer into result pop ax ; restore ax endm Istruzioni per le stringhe In Assembly vi sono diverse utili istruzioni per operare con le stringhe. Segue l'elenco delle istruzioni e la sintassi per utilizzarle: MOV* Move String: copia un byte, una word o una double word dall'indirizzo DS:SI all'indirizzo ES:DI Sintassi: movsb movsw movsd ; move byte ; move word ; move double word CMPS* Compare string: confronta un byte, una word o una double word all'indirizzo DS:SI con un byte, una word o una double word all'indirizzo ES:DI Sintassi: cmpsb cmpsw ; compare byte ; compare word cmpsd ; compare double word Nota: Questa istruzione è normalmente utilizzata con il prefisso REP. SCAS* Search string: cerca il valore in AL, AX o EAX nella stringa all'indirizzo ES:DI Sintassi: scasb scasw scasd ; search for AL ; search for AX ; search for EAX Nota: Questa istruzione è normalmente utilizzata con il prefisso REPZ o REPNZ. REP Prefisso utilizzabile davanti ad un'istruzione per le stringhe per ripetere l'istruzione tante volte quanto specificato nel registro CX. Sintassi: rep StringInstruction STOS* Copia un byte, una word o una double word da AL, AX o EAX all'indirizzo ES:DI Sintassi: stosb stosw stosd ; move AL into ES:DI ; move AX into ES:DI ; move EAX into ES:DI LODS* Copia un byte, una word o una double word dall'indirizzo ES:DI in AL, AX o EAX Sintassi: lodsb lodsw lodsd ; move ES:DI into AL ; move ES:DI into AX ; move ES:DI into EAX L'esempio successivo mostra come usare queste istruzioni. Listing 11: STRINGS.ASM .model small .stack .code mov mov mov mov mov int ax,@data ; ax points to of data segment ds,ax ; put it into ds es,ax ; put it in es too ah,9 ; function 9 - display string dx,OFFSET Message1 ; ds:dx points to message 21h ; call dos function cld mov mov mov rep ; clear direction flag si,OFFSET String1 ; make ds:si point to String1 di,OFFSET String2 ; make es:di point to String2 cx,18 ; length of strings movsb ; copy string1 into string2 mov ah,9 ; function 9 - display string mov dx,OFFSET Message2 ; ds:dx points to message int 21h ; call dos function mov dx,OFFSET String1 ; display String1 int 21h ; call DOS service mov dx,OFFSET Message3 ; ds:dx points to message int 21h ; call dos function mov dx,OFFSET String2 ; display String2 int 21h ; call DOS service mov si,OFFSET Diff1 ; make ds:si point to Diff1 mov di,OFFSET Diff2 ; make es:di point to Diff2 mov cx,39 ; length of strings repz cmpsb ; compare strings jnz Not_Equal ; jump if they are not the same mov ah,9 ; function 9 - display string mov dx,OFFSET Message4 ; ds:dx points to message int 21h ; call dos function jmp Next_Operation Not_Equal: mov ah,9 ; function 9 - display string mov dx,OFFSET Message5 ; ds:dx points to message int 21h ; call dos function Next_Operation: mov di,OFFSET SearchString ; make es:di point to string mov cx,36 ; length of string mov al,'H' ; character to search for repne scasb ; find first match jnz Not_Found mov mov int jmp ah,9 ; function 9 - display string dx,OFFSET Message6 ; ds:dx points to message 21h ; call dos function Lodsb_Example Not_Found: mov ah,9 ; function 9 - display string mov dx,OFFSET Message7 ; ds:dx points to message int 21h ; call dos function Lodsb_Example: mov ah,9 ; function 9 - display string mov dx,OFFSET NewLine ; ds:dx points to message int 21h ; call dos function mov mov xor mov cx,17 ; length of string si,OFFSET Message ; DS:SI - address of string bh,bh ; video page - 0 ah,0Eh ; function 0Eh - write character NextChar: lodsb int 10h ; AL = next character in string ; call BIOS service loop NextChar mov ax,4C00h int 21h ; return to DOS .data CR equ 13 LF equ 10 NewLine db CR,LF,"$" String1 db "This is a string!$" String2 db 18 dup(0) Diff1 db "This string is nearly the same as Diff2$" Diff2 db "This string is nearly the same as Diff1$" Equal1 db "The strings are equal$" Equal2 db "The strings are not equal$" Message db "This is a message" SearchString db "1293ijdkfjiu938uHello983fjkfjsi98934$" Message1 Message2 Message3 Message4 Message5 Message6 Message7 db db db db db db db "Using String instructions example program.$" CR,LF,"String1 is now: $" CR,LF,"String2 is now: $" CR,LF,"Strings are equal!$" CR,LF,"Strings are not equal!$" CR,LF,"Character was found.$" CR,LF,"Character was not found.$" end Scoprire la versione del DOS In molti programmi è necessario scoprire la versione del DOS. Questo perché si sta utilizzando una funzione DOS che è disponibile soltanto da una certa versione in poi. Per conoscere la versione è sufficiente fare come segue: mov ah,30h int 21h ; function 30h - get MS-DOS version ; call DOS function Questa funzione restituisce il major number di versione in AL e il minor number in AH. Per esempio se la versione fosse 4.01, AL varrebbe 4 e AH varrebbe 01. Il problema è che dal DOS 5 in poi, il comando SETVER può modificare la versione che deve essere restituita. Il modo per aggirare il problema è il seguente. mov ah,33h mov al,06h int 21h ; function 33h - actual DOS version ; subfunction 06h ; call interrupt 21h Questo frammento di codice restituisce la versione corrente del DOS, indipendentemente dal fatto che sia stato utilizzato SETVER per modificare il numero di versione. La major version viene posta in BL e la minor version in BH. Ovviamente funziona solo con le versioni del DOS dalla 5 in poi. Push e Pop multipli Si possono inserire e prelevare dallo stack più di un registro con una sola linea di codice, sia con TASM, sia con A86. Questo rende il codice più semplice da comprendere. push ax bx cx dx pop dx cx bx ax ; save registers ; restore registers Quando TASM e A86 compilano queste linee, le traducono in istruzioni di PUSH e POP separate. Questa notazione server soltanto per maggior concisione e comprensibilità. Nota: Perché le istruzioni mostrate sopra siano compilate correttamente da A86, bisogna separare i registri con una virgola (,). Le istruzioni PUSHA/PUSHAD e POPA/POPAD PUSHA è un'utile istruzione che inserisce nello stack tutti i registri general-purpose. In pratica PUSHA corrisponde alle seguenti istruzioni: temp push push push push push push push push = SP ax cx dx bx temp bp si di Il vantaggio principale è di essere un comando molto conciso. Inoltre viene eseguito molto più velocemente delle corrispettive istruzioni. POPA esegue la funzione inversa, prelevando tutti questi registri dallo stack. PUSHAD e POPAD svolgono le stesse funzioni di PUSHA e POPA, ma utilizzando i registri a 32-bit: ESP, EAX, ECX, EDX, EBX, EBP, ESI e EDI. Usare lo Shift per moltiplicazione e divisione Le istruzioni MUL e DIV sono molto lente e dovrebbero essere utilizzate solo quando la velocità non è una richiesta. Per effettuare moltiplicazioni e divisioni più velocemente si possono utilizzare gli operatori di shift a destra o a sinistra. Ogni operazione di shift moltiplica o divide per una potenza di 2. Funziona allo stesso modo degli operatori << e >> del linguaggio C. Ci sono quattro differenti modi di utilizzare l'istruzione di shift. SHL Moltiplica per due un operando senza segno SHR Divide per due un operando senza segno SAR Divide per due un operando con segno SAL Moltiplica per due un operando con segno La sintassi è identica per tutti e quattro le modalità: SHL operando1,operando2 Nota: Con il processore 8086 operando2 può assumere solo il valore 1. Con il processore 286/386 il valore di operando2 deve essere minore o uguale di 31. Cicli Usare l'istruzione Loop è preferibile rispetto ad utilizzare un JMP con label. Si imposta il numero di ripetizioni da effettuare nel registro CX ed ogni volta che il programma incontra l'istruzione Loop, CX viene automaticamente decrementato e viene fatto un salto alla label specificata come argomento. Il salto deve essere limitato a 127 byte in avanti o 128 byte all'indietro. Sintassi: mov cx,100 Label: . . . Loop Label: ; 100 times to loop ; decrement CX and loop to Label Questo produce esattamente lo stesso risultato del codice seguente: mov cx,100 Label: dec cx jnz Label ; 100 times to loop ; CX = CX-1 ; continue until done Quale ritenete più comprensibile? Utilizzare DEC/JNZ risulta essere più veloce dal 486 in poi ed è utile quando non è possibile utilizzare il registro CX. L'istruzione JNZ salta se il flag zero non è impostato. Azzerando il registro CX, il flag zero viene impostato. Come usare un debugger E' tempo di utilizzare un debugger per capire cosa un programma sta effettivamente facendo. A questo scopo si può usare il Turbo Debugger. Ma prima abbiamo bisogno di un programma su cui lavorare. Listing 12: DEBUG.ASM ; example program to demonstrate how to use a debugger .model tiny .code org 100h start: push ax push bx push cx ; save value of ax ; save value of bx ; save value of cx mov ax,10 mov bx,20 mov cx,3 ; first parameter is 10 ; second parameter is 20 ; third parameter is 3 Call ChangeNumbers pop cx pop bx pop ax ; restore cx ; restore bx ; restore dx mov ax,4C00h int 21h ; exit to dos ChangeNumbers PROC add ax,bx ; adds number in bx to ax mul cx ; multiply ax by cx mov dx,ax ; return answer in dx ret ChangeNumbers ENDP end start Ora compiliamolo come eseguibile .COM e poi digitiamo: td name of file Il Turbo Debugger viene eseguito, e vengono mostrate le istruzioni che compongono il nostro programma. Per esempio le prime linee del programma vengono mostrate come: cs:0000 50 cs:0001 53 cs:0002 51 push ax push bx push cx Alcune combinazioni di tasti per il Turbo Debugger: F5 Size Window F7 Next Instruction F9 Run ALT F4 Step Backwards I numeri che vengono copiato nei registri sono differenti da quelli del codice sorgente perché sono rappresentati in forma esadecimale (base 16) poiché questa notazione è semplice da convertire in forma binaria, ed è anche più concisa e comprensibile. Sulla destra dello schermo c'è una finestra che mostra i valori contenuti dei registri. In questo momento tutti i registri principali sono vuoti. Premendo F7 per eseguire la prima linea del programma. Poiché la prima linea inserisce il valore del registro AX nello stack, si vede che lo stack pointer (SP) viene modificato. Continuate a premere F7 fino ad arrivare alla linea mov ax,000A, e oltrepassatela. Ora, guardando nella finestra che mostra il contenuto dei registri, si vede che AX contiene A. Procedendo passo passo, BX assume il valore 14 e CX assume il valore 3. Poi AX assume il valore 1E che è A+14, e così via istruzione dopo istruzione. Infine CX, BX e AX sono impostati al loro valore iniziale di zero. Altre operazioni di output in modalità testo Vi sono altri modi per ottenere l'output in modalità testo. Questo primo programma è un esempio di come muovere il cursore per visualizzare una stringa di testo in una posizione a piacere. Listing 12: TEXT1.ASM .model tiny .code org 100h start: mov mov mov xor int dh,12 dl,32 ah,02h bh,bh 10h mov dx,OFFSET Text mov ah,9 int 21h ; ; ; ; ; cursor col cursor row move cursor to the right place video page 0 call bios service ; DS:DX points to message ; function 9 - display string ; all dos service mov ax,4C00h int 21h ; exit to dos Text DB "This is some text$" end start L'esempio successivo mostra come scrivere sullo schermo utilizzando la funzione 40h dell'interrupt 21h. Listing 13: TEXT2.ASM .model small .stack .code mov ax,@data mov ds,ax ; set up ds as the segment for data mov mov mov mov int ah,40h ; bx,1 ; cx,17 ; dx,OFFSET Text 21h ; function 40h - write file handle = 1 (screen) length of string ; DS:DX points to string call DOS service mov ax,4C00h int 21h ; terminate program .data Text DB "This is some text" end Il programma che segue mostra come impostare e richiamare la funzione 13h dell'interrupt 10h "write string". Questa funzione ha la caratteristica di poter scrivere una stringa in un posto a piacere sullo schermo con uno specifico colore. Tuttavia è complessa da impostare. Listing 14: TEXT3.ASM .model small .stack .code mov ax,@data mov es,ax mov mov mov xor mov mov mov mov int ; set up ds as the segment for data ; put this in es bp,OFFSET Text ah,13h ; al,01h ; bh,bh ; bl,5 ; cx,17 ; dh,5 ; dl,5 ; 10h ; ; ES:BP points to message function 13 - write string attrib in bl,move cursor video page 0 attribute - magenta length of string row to put string column to put string call BIOS service mov ax,4C00h int 21h ; return to DOS .data Text DB "This is some text" end Il programma successivo mostra come scrivere sullo schermo usando rep stosw per scrivere nella memoria video. Listing 15: TEXT4.ASM .model small .stack .code mov ax,0B800h ; segment of video buffer mov xor mov mov mov cld rep es,ax di,di ah,4 al,"G" cx,4000 stosw ; ; ; ; put this into es clear di, ES:DI points to video memory attribute - red character to put there ; amount of times to put it there ; direction - forwards ; output character at ES:[DI] mov ax,4C00h int 21h ; return to DOS end Il programma successivo mostra come scrivere una stringa nella memoria video. Listing 15: DIRECTWR.ASM ; write a string direct to video memory .model small .stack .code mov ax,@data mov ds,ax mov mov mov mov mov xor ax,0B800h ; segment of video buffer es,ax ; put this into es ah,3 ; attribute - cyan cx,17 ; length of string to print si,OFFSET Text ; DX:SI points to string di,di Wr_Char: lodsb mov es:[di],al inc di mov es:[di],ah inc di ; ; ; ; put next character into al output character to video memory move along to next column output attribute to video memory loop Wr_Char ; loop until done mov ax,4C00h int 21h ; return to DOS .data Text DB "This is some text" end Modalità 13h La modalità 13h è disponibile soltanto sulle schede VGA, MCGA e superiori. Verificare che la modalità 13h sia consentita E' opportuno comunicare all'utente se il suo computer non è in grado di supportare la modalità 13h, invece di lasciare che il programma vada in crash. Ecco come fare. Listing 16: CHECK13.ASM .model small .stack .data NoSupport db "Mode 13h is not supported on this computer." db ,"You need either a MCGA or VGA video" db ,"card/monitor.$" Supported db "Mode 13h is supported on this computer.$" .code mov ax,@data mov ds,ax ; set up DS to point to data segment ; use ax call Check_Mode_13h jc Error ; check if mode 13h is possible ; if cf=1 there is an error mov ah,9 ; function 9 - display string mov dx,OFFSET Supported ; DS:DX points to message int 21h ; call DOS service jmp To_DOS ; exit to DOS Error: mov ah,9 ; function 9 - display string mov dx,OFFSET NoSupport ; DS:DX points to message int 21h ; call DOS service To_DOS: mov ax,4C00h int 21h ; exit to DOS Check_Mode_13h PROC ; Returns: CF = 1 Mode 13h not possible mov ax,1A00h int 10h cmp al,1Ah je Mode_13h_OK stc Mode_13h_OK: ; Request video info for VGA ; Get Display Combination Code ; Is VGA or MCGA present? ; mode 13h is supported ; mode 13h isn't supported CF=1 ret Check_Mode_13h ENDP end E' sufficiente includere questo codice per verificare se la modalità 13h è supportata, per essere certi di poterla utilizzare. Impostare la modalità video Impostare la modalità è molto semplice. Ecco come fare. mov ax,13h int 10h ; set mode 13h ; call BIOS service Una volta che ci troviamo in modalità 13h e abbiamo completato le operazioni, è necessario re-impostare la modalità precedente. Questo viene fatto in due fasi. Prima dobbiamo salvare la modalità video e poi re-impostarla al valore originario. VideoMode db ? . . mov ah,0Fh ; function 0Fh - get current mode int 10h ; Bios video service call mov VideoMode,al ; save current mode ; program code here mov al,VideoMode ; set previous video mode xor ah,ah ; clear ah - set mode int 10h ; call bios service mov ax,4C00h ; exit to dos int 21h ; call dos function Ora che sappiamo come entrare in modalità 13h, facciamo qualche operazione. Per prima cosa scriviamo alcuni pixel sullo schermo. Funzione 0Ch: Write Graphics Pixel Questa funzione imposta un punto colorato sullo schermo alle coordinate grafiche specificate. INPUT: AH = 0Ch AL = Colore del punto CX = Colonna dello schermo (coordinata x) DX = Riga dello schermo (coordinata y) OUTPUT: Nulla eccetto il pixel sullo schermo. Questo programma dimostra come disegnare dei punti. Disegna quattro pixel rossi nel mezzo dello schermo. Listing 17: PIXINT.ASM ; example of plotting pixels in mode 13 using bios services ; INT 10h .model tiny .code org 100h start: mov ax,13 int 10h ; mode = 13h ; call bios service mov mov mov mov int ah,0Ch al,4 cx,160 dx,100 10h ; ; ; ; ; inc int inc int dec int dx 10h cx 10h dx 10h ; plot pixel downwards ; call BIOS service ; plot pixel to right ; call BIOS service ; plot pixel up ; call BIOS service function 0Ch color 4 - red x position = 160 y position = 100 call BIOS service xor ax,ax int 16h ; function 00h - get a key ; call BIOS service mov ax,3 int 10h ; mode = 3 ; call BIOS service mov ax,4C00h int 21h ; exit to DOS end start Alcune ottimizzazioni Questo metodo non è troppo veloce e potremmo renderlo molto più veloce. Come? Scrivendo direttamente nella memoria video. Ciò si ottiene abbastanza facilmente. Il segmento di memoria VGA si trova all'indirizzo 0A000h. Per conoscere l'indirizzo di ciascun pixel, utilizzate questa semplice formula per ottenere l'offset. Offset = X + ( Y * 320 ) Tutto quello che dobbiamo fare è scrivere un valore a questa locazione, ottenendo così un pixel. Il numero rappresenta il colore desiderato. Ci sono due istruzioni che possiamo usare per scrivere un pixel sullo schermo. La prima possibilità è quella di utilizzare stosb per impostare il valore nel registro AL all'indirizzo ES:DI; la seconda possibilità è quella di sfruttare una nuova forma dell'istruzione MOV, come segue: mov es:[di], color Quale sarebbe meglio utilizzare? La scelta deve essere guidata dalla velocità di esecuzione, ovvero dal numero di cicli necessari per svolgere l'operazione. Minore è il numero di cicli, più veloce è l'operazione. Instruction STOSB MOV AL to SEG:OFF Pentium 486 3 1 386 286 86 5 4 3 1 4 3 11 10 Se si utilizza l'istruzione MOV, bisogna ricordarsi di incrementare DI (operazione che STOSB effettua automaticamente). Esempi Qui di seguito sono riportati gli esempi illustrati durante le lezioni. Indirizzamento immediato // INDIRIZZAMENTO IMMEDIATO // L'operando sorgente è un valore numerico. asm { // Il registro AX viene impostato a zero XOR EAX,EAX // Il registro AX viene caricato impostando separatamente // la parte alta (AH) e la parte bassa (AL) con due operandi // a 8 bit MOV AH,00 MOV AL,04 // Il registro AX viene caricato con una singola istruzione // utilizzando un operando a 16 bit // (ogni numero esadecimale che inizia con una lettera deve // essere preceduto dallo 0) MOV AX,0FFFFH // La dimensione in bit dell'operando sorgente viene adattata // a quello dell'operando destinazione MOV AX,302 // Lo stesso vale per operandi a 8 bit MOV AL,-40 } Indirizzamento a registro // INDIRIZZAMENTO A REGISTRO // L'operando sorgente è contenuto in uno dei registri. asm { // Il contenuto di AX viene trasferito nel registro DX XOR EAX,EAX MOV AX,01234H MOV DX,AX // Si possono usare registri sorgente e destinazione a 8 bit XOR EAX,EAX MOV AL,056H MOV DL,AL } Indirizzamento diretto // INDIRIZZAMENTO DIRETTO // L'operando sorgente viene prelevato all'indirizzo di memoria // specificato. In genere l'operando è una label. DWORD dwDato = 0x12345678; // dwDato DD 012345678H WORD wDato = 0x1234; // wDato DW 01234H BYTE bDato = 0x12; // bDato DB 012H asm { // EAX viene caricato con il contenuto della locazione // di memoria dwDato (32 bit) XOR EAX,EAX MOV EAX,dwDato // AX viene caricato con il contenuto della locazione // di memoria wDato (16 bit) XOR EAX,EAX MOV AX,wDato // AL viene caricato con il contenuto della locazione // di memoria bDato (8 bit) XOR EAX,EAX MOV AL,bDato } Indirizzamento indiretto con registro // INDIRIZZAMENTO INDIRETTO CON REGISTRO // L'operando sorgente viene prelevato all'indirizzo di memoria // contenuto in un registro. DWORD dwDato = 0x12345678; // dwDato DD 012345678H WORD wDato = 0x1234; // wDato DW 01234H BYTE bDato = 0x12; // bDato DB 012H asm { // EAX viene caricato con il contenuto della locazione // di memoria dwDato (32 bit) XOR EAX,EAX LEA EBX,dwDato MOV EAX,[EBX] // AX viene caricato con il contenuto della locazione // di memoria wDato (16 bit) XOR EAX,EAX LEA EBX,wDato MOV AX,[EBX] // AL viene caricato con il contenuto della locazione // di memoria bDato (8 bit) XOR EAX,EAX LEA BX,bDato MOV AL,[EBX] } Indirizzamento relativo con registro base // INDIRIZZAMENTO RELATIVO CON REGISTRO BASE // L'indirizzo effettivo dell'operando sorgente viene determinato // sommando uno spiazzamento al contenuto di un registro base. char msg[] = "Ibis redibis non morieris"; // msg DB 'Ibis redibis non morieris','$' asm { // Imposta AL con il secondo carattere del messaggio XOR EAX,EAX LEA EBX,msg MOV AL,[EBX]+1 // Imposta AL con il terzo carattere del messaggio XOR EAX,EAX LEA EBX,msg MOV AL,2[EBX] // Imposta AL con il quarto carattere del messaggio XOR EAX,EAX LEA EBX,msg MOV AL,[EBX+3] } Indirizzamento diretto con registro indice // INDIRIZZAMENTO DIRETTO CON REGISTRO INDICE // L'indirizzo dell'operando viene calcolato sommando un indirizzo // di memoria al contenuto di un registro indice. char msg[] = "Ibis redibis non morieris"; // msg DB 'Ibis redibis non morieris','$' asm { // Imposta AL con il quarto carattere del messaggio XOR EAX,EAX MOV EBX,3 MOV AL,msg[EBX] } Indirizzamento con registri base e indice // INDIRIZZAMENTO CON REGISTRI BASE E INDICE // L'indirizzo dell'operando viene calcolato sommando il contenuto // del registro base con il contenuto del registro indice. char msg[] = "Ibis redibis non morieris"; // msg DB 'Ibis redibis non morieris','$' asm { // Imposta AL con il quarto carattere del messaggio XOR EAX,EAX LEA EBX,msg MOV ECX,3 MOV AL,[EBX][ECX] } Somma esadecimale con indirizzamento immediato // SOMMA ESADECIMALE CON INDIRIZZAMENTO IMMEDIATO // Somma di tre numeri utilizzando l'indirizzamento immediato. asm { // Azzera EAX SUB EAX,EAX // Carica AL con il valore 23H MOV AL,23H // Somma 0AH ad AL ADD AL,0AH // Somma 10H ad AL ADD AL,10H // Il risultato è in AL } Sottrazione esadecimale con indirizzamento diretto // // // // SOTTRAZIONE ESADECIMALE CON INDIRIZZAMENTO DIRETTO I numeri da sottrarre vengono referenziati tramite il loro indirizzo e vengono sottratti utilizzando la tecnica di indirizzamento diretto. WORD WORD WORD WORD NUM1 = 0x1234; NUM2 = 0x24; NUM3 = 0xABC; RISULTATO; // // // // NUM1 DW 1234H NUM2 DW 24H NUM3 DW 0ABCH RISULTATO DW ? asm { // Azzera EAX SUB EAX,EAX // Carica NUM1 nel registro AX MOV AX,NUM1 // Sottrae NUM2 a NUM1 SUB AX,NUM2 // Sottrae NUM3 al risultato precedente SUB AX,NUM3 // Memorizza il risultato in RISULTATO MOV RISULTATO,AX } Somma in precisione multipla con indirizzamento diretto // SOMMA IN PRECISIONE MULTIPLA CON INDIRIZZAMENTO DIRETTO WORD WORD WORD WORD WORD WORD NUM1 = 0x1234; NUM2 = 0x5678; NUM3 = 0x9ABC; NUM4 = 0xDEF0; RIS_LSW; RIS_MSW; // // // // // // NUM1 DW NUM2 DW NUM3 DW NUM3 DW RIS_LSW RIS_MSW asm { // Azzera EAX e EBX SUB EAX,EAX 1234H 5678H 9ABCH 0DEF0H DW ? DW ? SUB EBX,EBX // Carica il primo numero nel registro AX MOV AX,NUM1 // Somma il secondo numero al primo ADD AX,NUM2 // Se il Carry è a 1, viene sommato a BX ADC BX,00H // Somma il terzo numero al registro AX ADD AX,NUM3 // Se il Carry è a 1, viene sommato a BX ADC BX,00H // Somma il quarto numero al registro AX ADD AX,NUM4 // Se il Carry è a 1, viene sommato a BX ADC BX,00H // La somma AX viene copiata in RIS_LSW MOV RIS_LSW,AX // La somma BX viene copiata in RIS_MSW MOV RIS_MSW,BX } Somma in precisione multipla con indirizzamento con registro indice // SOMMA IN PRECISIONE MULTIPLA CON INDIRIZZAMENTO CON REGISTRO INDICE // - Una tabella in memoria contiene gli addendi. // - Un ciclo di programma scandisce la tabella. // - L'indirizzamento con registro indice permette di accedere // alle righe della tabella. WORD TABELLA[] = {0x1234, 0x5678, // TABELLA DW 1234H, 5678H 0x9ABC, 0xDEF0, // DW 9ABCH, 0DEF0H 0x1111, 0x2222, // DW 1111H, 2222H 0x3333, 0x4444}; // DW 3333H, 4444H WORD RIS_LSW; // RIS_LSW DW ? WORD RIS_MSW; // RIS_MSW DW ? asm { // Azzera EAX, EBX, ECX, EDX SUB EAX,EAX SUB EBX,EBX SUB ECX,ECX SUB EDX,EDX // Indirizzo di TABELLA in EBX LEA EBX,TABELLA // Indice a 0 MOV ESI,00H // Primo numero in AX MOV AX,TABELLA[ESI] // Numero di righe della tabella MOV CX,08H ANCORA: // Incrementa l'indice di una WORD (2 BYTE) ADD ESI,02H // Somma l'i-esimo elemento della tabella ADD AX,TABELLA[ESI] // Se il Carry = 1, viene sommato a DX ADC DX,00H // se CX non è 0, continua LOOP ANCORA // Tasferisce AX in RIS_LSW MOV RIS_LSW,AX // Tasferisce DX in RIS_MSW MOV RIS_MSW,DX } Somma di numeri decimali con indirizzamento indiretto con registro // SOMMA DI NUMERI DECIMALI CON INDIRIZZAMENTO INDIRETTO CON REGISTRO BYTE ADDENDI[] = {1,2,3,4,5,6,7,8,9,10}; // ADDENDI DB 1,2,3,4,5,6,7,8,9,10 BYTE RISULTATO; // RISULTATO DB ? asm { // Azzera EAX, EBX, ECX, EDX SUB EAX,EAX SUB EBX,EBX SUB ECX,ECX SUB EDX,EDX // Indirizzo di ADDENDI in EBX LEA EBX,ADDENDI // Imposta il contatore MOV CX,10 // Primo numero in AX MOV AL,ADDENDI AGAIN: // Incrementa l'indice di 1 BYTE ADD EBX,01H // Somma l'i-esimo addendo ADD AL,[EBX] // Conversione decimale della somma DAA // se CX non è 0, continua LOOP AGAIN // Copia il risultato in RISULATATO MOV RISULTATO,AL } Moltiplicazione per somme ripetute // MOLTIPLICAZIONE PER SOMME RIPETUTE BYTE MOLTIPLICANDO = 0x2A; // MOLTIPLICANDO DB 2AH BYTE MOLTIPLICATORE = 0x78; // MOLTIPLICATORE DB 78H WORD PRODOTTO; // PRODOTTO DB ? asm { // Azzera EAX, EBX, ECX, EDX SUB EAX,EAX SUB EBX,EBX SUB ECX,ECX SUB EDX,EDX // Moltiplicatore in CL MOV CL,MOLTIPLICATORE SUB CL,01H // Moltiplicando in AL MOV AL,MOLTIPLICANDO CICLO1: // Somma il moltiplicando al risultato parziale ADD AL,MOLTIPLICANDO ADC AH,00H // Se CL non è 0, continua LOOP CICLO1 // Risultato finale in PRODOTTO MOV PRODOTTO,AX } Moltiplicazione, elevamento al quadrato e al cubo con l'istruzione di moltiplicazione // MOLTIPLICAZIONE, ELEVAMENTO AL QUADRATO E AL CUBO CON // L'ISTRUZIONE DI MOLTIPLICAZIONE WORD NUM1 = 0x1234; // NUM1 DW 1234H WORD NUM2 = 0x00CD; // NUM2 DW 00CDH DWORD RIS1; // RIS1 DD ? DWORD RIS2; // RIS2 DD ? DWORD RIS3; // RIS3 DD ? asm { // Azzera EAX, EBX, ECX, EDX SUB EAX,EAX SUB EBX,EBX SUB ECX,ECX SUB EDX,EDX // MOLTIPLICAZIONE // Azzera il registro di overflow SUB DX,DX // NUM1 in AX MOV AX,NUM1 // Moltiplicazione di NUM1 con NUM2 MUL NUM2 // AX in LSB di RIS1 MOV WORD PTR RIS1,AX // DX in MSB di RIS1 MOV WORD PTR RIS1+2,DX // ELEVAMENTO AL QUADRATO // Azzera il registro di overflow SUB DX,DX // NUM1 in AX MOV AX,NUM1 // NUM1 moltiplicato per se stesso MUL NUM1 // AX in LSB di RIS2 MOV WORD PTR RIS2,AX // DX in MSB di RIS2 MOV WORD PTR RIS2+2,DX // ELEVAMENTO AL CUBO // Azzera il registro di overflow SUB DX,DX // NUM2 in AX MOV AX,NUM2 // AX = AX * NUM2 MUL NUM2 // AX = AX * NUM2 MUL NUM2 // AX in LSB di RIS3 MOV WORD PTR RIS3,AX // DX in MSB di RIS3 MOV WORD PTR RIS3+2,DX } Divisione con variabili di tipo doubleword // DIVISIONE CON VARIABILI DI TIPO DOUBLEWORD DWORD WORD WORD WORD DIVIDENDO = 0x2A8B7654; // DIVIDENDO DD DIVISORE = 0X5ABC; // DIVISORE DW QUOZIENTE; // QUOZIENTE DW RESTO; // RESTO DW asm { // Azzera EAX, EDX SUB EAX,EAX SUB EDX,EDX 02A8B7654H 5ABCH ? ? // LSB di DIVIDENDO in AX MOV AX,WORD PTR DIVIDENDO // MSB di DIVIDENDO in DX MOV DX,WORD PTR DIVIDENDO+2 // (DX AX) diviso per DIVISORE DIV DIVISORE // AX in QUOZIENTE MOV QUOZIENTE,AX // DX in RESTO MOV RESTO,DX } Estrazione di radice quadrata // ESTRAZIONE DI RADICE QUADRATA WORD NUMERO = 0x1CE4; // NUMERO DW 1CE4H WORD RADICE; // RADICE DW ? asm { // Azzera EAX, ECX, EDX SUB EAX,EAX SUB ECX,ECX SUB EDX,EDX // Numero da sottrarre MOV CX,01H CICLO2: // Sottrazione dell'i-esimo numero dispari SUB NUMERO,CX // Esce dal ciclo se NUMERO < 0 JL FINE // Incrementa il valore della radice quadrata INC DX // Calcola il successivo numero dispari ADD CX,02H // Ripete il ciclo JMP CICLO2 // Il risultato è in RADICE FINE: MOV RADICE,DX } Tabella di lookup per logaritmi // // // // TABELLA DI LOOKUP PER LOGARITMI Il programma determina il logaritmo di un numero, dopo aver memorizzato in forma tabellare i valori della funzione logaritmo per un predefinito insieme di numeri. WORD TABELLA[] = {0,3010,4771,6021,6990,7782,8451,9031,9542,10000}; // TABELLA DW 0,3010,4771,6021,6990,7782,8451,9031,9542,10000 BYTE NUMERO = 7; // NUMERO DB 7 WORD LOGARITMO; // LOGARITMO DW ? asm { // Azzera EAX, EBX, EDX SUB EAX,EAX SUB EBX,EBX SUB EDX,EDX // Indice per la ricerca MOV AL,NUMERO // Corregge l'indice (non esiste il logaritmo di 0) SUB AL,1 // Moltiplica l'indice per 2 (la tabella è fatta di WORD) ROL AL,1 // Indirizzo di TABELLA in EBX LEA EBX,TABELLA // Converte da BYTE a WORD CBW // Somma indirizzo e indice ADD BX,AX // Estrae il dato in tabella MOV DX,[EBX] // Risultato in LOGARITMO MOV LOGARITMO,DX } Conversione di codici mediante tabelle di lookup // CONVERSIONE DI CODICI MEDIANTE TABELLE DI LOOKUP // Il programma esegue la conversione di un numero esadecimale // nella codifica Gray equivalente. BYTE GRAY[] = {0x2,0x3,0x7,0x5,0x4,0xC,0xD,0xF,0xE,0xA}; // GRAY DB 0010B, 0110B, 0111B, 0101B, 0100B // DB 1100B, 1101B, 1111B, 1110B, 1010B BYTE VALORE = 8; // VALORE DB 8H BYTE RISPOSTA; // RISPOSTA DB ? asm { // Azzera EAX, EBX SUB EAX,EAX SUB EBX,EBX // Indirizzo della tabella di codifica in EBX LEA EBX,GRAY // Numero da convertire (indice tabella) in AL MOV AL,VALORE // Codifica in AL XLAT GRAY // Risultato in RISPOSTA MOV RISPOSTA,AL } Conversione da numeri ASCII a numeri esadecimali // CONVERSIONE DA NUMERI ASCII A NUMERI ESADECIMALI // Il programma converte un numero espresso in formato ASCII // nel corrispondente numero in formato esadecimale. // L'input si suppone compreso tra 30H e 46H. // ASCII // 30H // 31H // 32H // 33H // 34H // 35H // 36H // 37H // 38H // 39H // 41H // 42H // 43H // 44H // 45H // 46H HEX 0H 1H 2H 3H 4H 5H 6H 7H 8H 9H AH BH CH DH EH FH BYTE ASCII = 0x42; // ASCII DB 42H BYTE HEX; // HEX DB ? asm { // Azzera EAX SUB EAX,EAX // Numero ASCII da convertire in AL MOV AL,ASCII // Conversione SUB AL,30H // Il numero è compreso tra 0 e 9? CMP AL,9 // Salta se il numero è maggiore di 9 JG LETTERA // Vai alla fine JMP FINEPRG // Conversione per numeri maggiori di 9 LETTERA:SUB AL,07H // Risultato in HEX FINEPRG:MOV HEX,AL }