(Incompleto) MINI CORSO DI ASSEMBLER 8086

(tratto dal sito:http://www.giobe2000.it/Tutorial/Cap01/Pag/cap01-01.asp
http://www.enricomilano.it/download/guide/c/11c.htm)

LINGUAGGIO MACCHINA

Rappresenta l'insieme delle istruzioni che un elaboratore è in grado di eseguire direttamente. E' strettamente correlato alla realizzazione fisica dell'elaboratore stesso.
La struttura di massima di un'istruzione macchina è la seguente:

OP-CODE DEST SOURCE

dove:
- Il campo
Op-code specifica il tipo di operazione richiesta (es., “copia”)
- Il campo
Dest specifica l’operando di destinazione (es., con 3 bit si individua uno degli 8 registri generali)
- Il campo
Source specifica l’operando sorgente
es., l’indirizzo di alfa relativo al segmento dati)

Per scrivere un codice in linguaggio macchina è necessario quindi:
- conoscere l'architettura della macchina
- ragionare in termini del comportamento dell'elaboratore
- conoscere i dettagli relativi alla scrittura delle singole istruzioni ovvero:
  a) i codici numerici e il formato interno delle istruzioni macchina
  b) le rappresentazione degli operandi
- gestire direttamente gli indirizzi in memoria per il riferimento ai dati e per i salti al codice
Ad esempio, per scrivere nel linguaggio macchina dell'8086 l'istruzione:

MOV destinazione, sorgente

è necessario scegliere tra:
- 14 diversi codici operativi, a seconda del tipo di operandi
- svariate possibili rappresentazioni degli operandi stessi
L'istruzione macchina che si ottiene ha una lunghezza variabile tra i due e i sei byte, a seconda delle scelte effettuate.

LINGUAGGIO ASSEMBLER

Si tratta di un linguaggio a più alto livello rispetto a quello macchina. I suoi vantaggi sono:

- nasconde i dettagli realizzativi delle singole istruzioni (codici operativi, formati, ecc.)
- associa ai dati e alle istruzioni nomi simbolici che identificano in modo univoco le corrispondenti posizioni di memoria (eliminando, così, la necessità di utilizzare indirizzi espliciti).

Costringe ancora il programmatore a ragionare in termini della logica della macchina a cui si
riferisce. L'architettura della macchina a cui il linguaggio si riferisce non viene nascosta in
alcun modo. Ad esempio, con il linguaggio assembler è possibile riferirsi direttamente ai registri, azione preclusa ai linguaggi di alto livello a causa della loro indipendenza dall’hardware.

Distinguiamo nel linguaggio due tipi di istruzioni:
-
Istruzioni eseguibili - a cui corrispondono le istruzioni del linguaggio macchina
-
Direttive (o pseudoistruzioni) - controllano il comportamento dell'assemblatore in fase di
traduzione - facilitano lo sviluppo dei programmi - permettono:
   a) la suddivisione di un'applicazione in più moduli
   b) il controllo del formato dei listati
   c) la definizione di macro, ecc.

STRUTTURA DI UN PROGRAMMA

Un programma in Macro Assembler ha generalmente la seguente struttura:

.386
.MODEL ......, tipo convenzione parametri
   
; indica al compilatore il modello di memoria da usare
.STACK ......
   
; dimensiona lo Stack (ad esempio 100h)
.DATA
   
; inizio del segmento dati
.DATA?
   
; inizio del segmento dati
.CONST

.CODE
Etichetta_di_Inizio:    ; inizio del segmento di codice
    ........
END
Etichetta_di_Inizio ; fine del programma

.386

Questa e' una direttiva per l'assembler, a cui diciamo di usare il set di istruzioni 80386. Potreste anche usare .486, .586 ma la scelta piu' sicura e' quella di utilizzare sempre .386 - Non va messa se non necessaria

 

.MODEL

Definisce il tipo dei segmenti di memoria da utilizzare nel programma. Le principali scelte possibili sono:

- TINY: tutto il codice e i dati sono in un unico segmento (stanno in 64Kb). Questo modello è il modello utilizzato per i file con estensione COM.

- SMALL: è il modello più comune, un segmento per il codice e uno solo per i dati (statici, globali, heap) e lo stack, Tutti i segmenti non superano i 64Kb. Quindi il valore iniziale di DS ed SS non coincide con quello di CS

- MEDIUM: il codice usa più segmenti, può quindi superare la barriera dei 64Kb. I dati e lo stack sono gestiti come nel modello small.

- COMPACT: è come lo small ma per accedere ai dati uso puntatori di tipo FAR quest'ultimi possono infatti superare i 64Kb.

- LARGE: è come il compact ma con il codice in più segmenti; sia codice che dati superano i 64Kb.

- HUGE: Il modello huge consente di gestire (in teoria) sino ad 1 Mb di dati statici e globali, estendendo ad essi la modalità di indirizzamento implementata dai modelli large e medium per il codice. E' l'unico modello che estende ad 1 Mb il limite teorico sia per il codice che per tutti i tipi di dato

- FLAT : supporta la modalità a 32bit dei processori 386+; in questa modalità viene abolita il metodo di indirizzamento SEGMENTO:OFFSET, l'indirizzo è dato da un unico numero a 32bit. Sotto Win32 è l'unico modello ammesso

Se si vuole, sul sito esiste un approfondimento sui modelli di memoria.
Il memory model (modello di memoria) e' oggi drasticamente diverso dai vecchi giorni del mondo a 16-bit. In Win32, non e' piu' necessario preoccuparsi del modello di memoria o dei segmenti! C'e' solo un modello di memoria: il Flat memory model. Non esistono piu' segmenti da 64K. La memoria e' un largo e continuo spazio di 4 GB. Questo significa anche che non bisognerà piu' giocare con i segment registers. Potremo usare qualsiasi segment register per indirizzare qualsiasi punto nello spazio di memoria. Questo e' un GRANDE aiuto ai programmatori, ed è ciò che rende la programmazione in assembly per Win32 semplice quanto quella in C.

Come secondo argomento opzionale .MODEL ha la specifica della convenzione di passaggio dei parametri. Questa convenzione specifica l'ordine con cui i parametri verranno passati, da sinistra-verso-destra o da destra-verso-sinistra. In Win16, ci sono due tipi di convenzioni di chiamata, C e PASCAL La convenzione di chiamata C passa i parametri da destra a sinistra, cioe', il parametro all'estrema destra e' PUSHato per primo. Il caller (chiamante) e' responsabile del bilanciamento dello stack frame dopo la call. Ad esempio, volendo chiamare una funzione denominata foo (int primo_param, int secondo_param, int terzo_param) con la convenzione C, il codice assomiglierebbe a questo:

push [terzo_param]   // ; Push il terzo parametro ...
push [secondo_param] // ; Seguito dal secondo ...
push [primo_param]   // ; e dal primo
call foo
add sp, 12           // ; Il caller bilancia lo stack frame

La convenzione PASCAL e' l'inverso di quella per il C. Essa passa i parametri da sinistra a destra, e il callee e' responsabile per il bilanciamento della stack dopo la call. Win16 adotta la convenzione PASCAL poiché produce codici piu' piccoli. la convenzione C e' utile quando non si conosce il numero dei parametri che verranno passati alla funzione, come nel caso di wsprintf(). Nel caso di wsprintf(), la funzione non ha modo di determinare aprioristicamente il numero di parametri che verrano pushati sulla stack, percio' non puo' bilanciare lo stack frame. STDCALL e' un ibrido tra le convenzioni C e PASCAL. Essa passa i parametri da destra a sinistra ma il callee è responsabile per il bilanciamento della stack dopo la call. La piattaforma Win32 usa esclusivamente STDCALL. Eccetto in un caso: wsprintf() dove si usa la convenzione C.

.STACK

Dice al compilatore quanto spazio deve riservare per lo stack. Se viene omesso il compilatore usa per default 400h (1Kb)

La sezione dei dati è divisa in 3 categorie: .DATA, .DATA? e .CONST. Non e' necessario utilizzare tutte e tre le sezioni nel vostro programma. Dichiarate solo la/e sezione/i che volete usare.

.DATA

Inizializza il segmento dati. Dopo questa direttiva si dichiarano le variabili da usare nel programma e le si inizializzano.

Messaggio DB "Salve Mondo",13,10,'$'

Questa istruzione assegna alla variabile Messaggio il valore "Salve Mondo". DB definisce dei Byte in memoria e in questo caso il numero dei byte è 14:
- 11 per la stringa "Salve Mondo"
- 1 per il carattere 13 (CR)
- 1 per il 10 (LF)
- 1 per il terminatore '$', che deve essere sempre presente alla fine di una stringa.
In questo caso la variabile è quindi inizializzata se avessimo voluto solo riservare dello spazio (ad esempio 10 Byte) per riempirlo durante l'esecuzione del programma avremmo dovuto scrivere:

Nome_Variabile DB 10 DUP(?)

Quindi abbiamo visto che la direttiva DB definisce byte , ne esistono altre:

DW - Define Word
DD - Define Double word
DQ - Define Quadword
DF - Define 48-bit (puntatore FAR 6 byte)
DT - Define TenByte

DATA?

Questa sezione contiene i dati NON inizializzati del vostro programma. A volte capita di voler impegnare una parte di memoria (variabli) senza inizializzarla. Questa sezione serve a tale scopo.

.CONST

Questa sezione contiene le costanti usate dal vostro programma. Le costanti in questa sezione non potranno mai essere modificate dal vostro programma. Sono semplicemente costanti.

.CODE

Indica l'inizio del segmento di codice del programma. E' qui dove vengono messe le vostre istruzioni. <etichetta>: end <etichetta> dove <etichetta> e' una qualsiasi etichetta arbitraria sono obbligatorie e sono usate per specificare l'estensione del vostro programma. Entrambe le etichette devono essere identiche. Tutto il vostro codice deve risiedere tra <etichetta>: e end <etichetta>

Dichiarazione Costanti

In assembler la dichiarazione delle costanti avviene mediante un'istruzione dichiarativa  composta dal nome che si intende utilizzare, dalla direttiva EQU e dal valore che si vuole assegnare alla costante.

ESC_ EQU 27 Esempio per il codice del tasto <ESC>
CR EQU 0Dh Esempio per il codice del tasto <CARRIAGE RETURN>
LF EQU 0Ah Esempio per il codice del tasto <LINE FEED>

Dichiarazione Variabili

La dichiarazione delle variabili avviene mediante un'istruzione dichiarativa  composta dal nome che si intende utilizzare, dalla direttiva che riserva il numero di byte e dall'operando che può essere:
- una costante (in questo caso si tratta dell'inizializzazione)
- un punto di domanda che indica che la variabile non è stata inizializzata.
Il nome rappresenta l'indirizzo di offset rispetto all''inizio del segmento dati della variabile appena dichiarata.

I tipi che una variabile può assumere sono quattro e ne definiscono la massima grandezza. Essi sono:
- DB (define byte): la variabile è grande 1 byte;
- DW (define word): la variabile è grande 2 byte;
- DD (define double): la variabile è grande 4 byte;
- DQ (define quad): la variabile è grande 8 byte
- DT (define ten): la variabile è grande 10 byte

Dichiarazione di variabile numeriche intere senza segno:

n db 10 variabile n di un byte inizializzata a 10 (range da 0 a 255)
n dw 20 variabile n di due byte inizializzata a 20 (range da 0 a 65.535)
n dd 30 variabile n di quattro byte inizializzata a 30  (range da 0 a 4.294.967.295=232-1)
n dq 40 variabile n di otto byte inizializzata a 40 (disponibile dal 486) (range: da 0 a 18.446.744.073.709.551.615)
n dt 50 variabile n di dieci byte inizializzata a 50 (range: da 0 a 280-1)

Dichiarazione di variabile numeriche intere con segno: si usa la rappresentazione in complemento a 2. - E' possibile usare le stesse direttive dei numeri senza segno oppure sbyte, sword, sdword:

n db -10
n sbyte -10
variabile n di un byte inizializzata a -10 (range da -128 a 127)
n dw -20
n sword -20
variabile n di due byte inizializzata a -20 (range da -32.768 +32.767)
n dd -30
n sdword -30
variabile n di quattro byte inizializzata a -30  (range da -2.147.483.648 +2.147.483.647 )

Le variabili numeriche con la virgola sono rappresentate in notazione Floating point IEEE 754 - E' possibile usare le stesse direttive dei numeri senza segno oppure real4, real8, real10:

n dd -10
n real4 -10
variabile n di quattro byte inizializzata a -10 (range da 1.18 x 10-38 a 3.40 x 1038 )
n dq -20
n real8 20
variabile n di otto byte inizializzata a -20 (range da 2.23 x 10-308 a 1.79 x 10308 )
n dt -30
n real10 30
variabile n di dieci byte inizializzata a -30  (range da 3.37 x 10-4932 a 1.18 x 104932 )

La dichiarazione di stringhe

lettera db "a"
lettera db 'a'
variabile lettera di un byte inizializzata a "a"
s db "ciao"
s db "c","i","a","o"
invio db 0Dh, 0Ah
variabile s inizializzata a "ciao". Il nome s fa riferimento all'indirizzo del byte contenente il primo carattere. Posso fornire anche la codifica ascii

Gli array possono essere visti come una sequenza di variabili. Una stringa di testo è un esempio di array di byte dove ogni carattere è rappresentato mediante il suo codice ascii. Ecco degli esempi di definizione:

a DB 48h, 65h, 6Ch, 6Ch, 6Fh, 00h
b DB 'Hello', 0

E' possibile accedere al singolo elemento utilizzando le parentesi quadre

MOV AL, a[3]

Spesso a tale scopo vengono utilizzati i registri di memoria BX, SI, DI e BP

MOV SI, 3
MOV AL, a[SI]

Se si vuol dichiarare un array contenente gli stessi valori ripetuti oppure di grandi dimensioni si può utilizzare l'operatore DUP. La sintassi è la seguente:

NUMERO DUP ( Valori )

dove numero indica quanti duplicati devono essere fatti mentre valori indica l'espressione da duplicare

ad esempio:
X DB 5 DUP(9)
equivale a
X DB 9, 9, 9, 9, 9

oppure
X DB 3 DUP(1, 2)
equivale a
X DB 1, 2, 1, 2, 1, 2

Naturalmente si può utilizzare DW al posto di DB se i valori contenuti nel singolo elemento sono maggiori di 255 o inferiori a -128. DW non è usabile per dichiarare una stringa

LE OPERAZIONI FONDAMENTALI

Le operazioni fondamentali come la somma, la differenza, la moltiplica e la divisione richiedono almeno il caricamento di uno degli operandi all'interno dei registri. L'operazione che effettua tale caricamento è il comando MOV.

- Istruzione MOV

L'istruzione MOV copia un dato da una posizione ad un'altra e la sua sintassi è:

       
MOV  <destinazione>, <sorgente>

In pratica, questa istruzione copia il valore sorgente in destinazione. L'operando
<destinazione> può essere un registro o una locazione di memoria, mentre l'operando <sorgente> può essere un registro, una locazione di memoria o un valore immediato; i due operandi devono avere la stessa dimensione. Inoltre i due operandi non possono essere entrambi locazioni di memoria.

- Istruzione ADD

L'istruzione ADD esegue la somma di due numeri con o senza segno e posiziona il risultato della somma nel primo operando. Gli operandi possono essere byte, word e doubleword (nei 386 in su). La sua sintassi è:

       
ADD  <operando1>, <operando2>

Il primo operando può essere un registro o una locazione di memoria, il secondo può essere un registro, una locazione di memoria o un valore immediato; i due operandi devono avere la stessa dimensione. Inoltre i due operandi non possono essere entrambi locazioni di memoria. In base al risultato vengono settati i seguenti flag:
-
ZF : ha il valore 1 se il risultato è zero;
-
SF : ha il valore 1 se il risultato è negativo;
-
CF : ha il valore 1 se si è verificato un riporto (carry) sul bit più significativo.
Corrisponde all'istruzione:
<operando1> = <operando1> + <operando2>

- Istruzione ADC

L'istruzione ADC esegue esegue la somma di due numeri con o senza segno considerando anche il valore del carry flag (CF) e inserendo il risultato della somma nel primo operando. La sua sintassi è:

       
ADC  <operando1>, <operando2>

Questa istruzione richiede le medesime condizione dell'istruzione ADD. Corrisponde all'istruzione: <operando1> = <operando1> + <operando2> + CF

ASSEMBLER (OPER_SOMMA.ASM)

INCLUDE Libreria.inc ; files contenenti le macro generali
INCLUDE Output.inc  
; files contenenti le macro di output
.MODEL tiny         
; Modello di memoria
;--------------------------------------
; segmento dati
;--------------------------------------
.DATA
   X db 120d
; Addendo iniziale
   Y db 110d
; un addendo
   Z db 140d
; altro addendo
   S dw ?   
; Somma
;--------------------------------------
; segmento codice
;--------------------------------------
.CODE
   ORG 100h 
; il prg inizia all'indirizzo 100h
INIZIO:
  
; ESEMPIO 1: somma con risultato inferiore al byte
   MOV AH, 0     
; Azzero la parte alta del registro AX
   MOV AL, X     
; Carico il primo addendo dall'indirizzo X
   ADD AL, Y     
; Aggiungo il secondo addendo dall'indirizzo Y
   MOV S, AX     
; Salvo la somma (inferiore a 256) nella variabile S
   MS_PRINT_INT S
; Stampo la variabile S (vedo 230)
   MS_ACAPO

  
; ESEMPIO 2: somma con risultato superiore a 256 (non mi basta un byte!)
   ;            non considero il riporto

   MOV AH, 0     
; Azzero la parte alta del registro AX
   MOV AL, X     
; Carico il primo addendo dall'indirizzo X
   ADD AL, Z     
; Aggiungo il secondo addendo dall'indirizzo Z
   MOV S , AX    
; Salvo la somma (superiore a 256) nella variabile S
   MS_PRINT_INT S
; Stampo la variabile S (
vedo 4: 00000001 00000100)
   MS_ACAPO

  
; ESEMPIO 3: somma con risultato superiore a 256 (non mi basta un byte!)
   ;            considero il riporto

   MOV AH, 0     
; Azzero la parte alta del registro AX
   MOV AL, X     
; Carico il primo addendo dall'indirizzo X
   ADD AL, Z     
; Aggiungo il secondo addendo dall'indirizzo Z
  
ADC AH, 0      ; Aggiungo il riporto (Carry)
   MOV S , AX    
; Salvo la somma (superiore a 256) nella variabile S
   MS_PRINT_INT S
; Stampo la variabile S (
vedo 260: 00000001 00000100)
   MS_ACAPO

   INT 20h       
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO


- Istruzione SUB

L'istruzione SUB esegue la sottrazione tra due numeri con o senza segno e posiziona il risultato della sottrazione nel primo operando. La sua sintassi è:

       
SUB  <operando1>, <operando2>

Il primo operando può essere un registro o una locazione di memoria, il secondo può essere un registro, una locazione di memoria o un valore immediato; i due operandi devono avere la stessa dimensione. Inoltre i due operandi non possono essere entrambi locazioni di memoria. In base al risultato vengono settati i seguenti flag:
-
ZF : ha il valore 1 se il risultato è zero;
-
SF : ha il valore 1 se il risultato è negativo;
-
CF : ha il valore 1 se si è verificato un riporto sul bit più significativo.

Corrisponde all'istruzione: <operando1> = <operando1> - <operando2>

- Istruzione SBB

L'istruzione SBB esegue la sottrazione di due numeri con o senza segno considerando anche il valore del carry flag CF e inserendo il risultato della sottrazione nel primo operando. La sua sintassi è:

       
SBB  <operando1>, <operando2>

Questa istruzione richiede le medesime condizione dell'istruzione
SUB
Corrisponde all'istruzione:
<operando1> = <operando1> - <operando2> + CF
Lo scopo di questa istruzione è da valutare nel contesto in cui ha avuto origine, cioè quello dell'8086: per via dei limiti della sua ALU (in grado di trattare solo dati a 16 bit). L'istruzione
SBB ha reso possibile la sottrazione di numeri maggiori di 16 bit, con la seguente tecnica (ovviamente superata con l'avvento dei nuovi processori):
- il primo numero a 32 bit viene caricato nei registri AX,BX:
- il sottraendo a 32 bit viene caricato nei registri CX,DX:
- la differenza viene eseguita in 2 tempi, prima sottraendo i 16 bit meno significativi (DX da BX) e poi i rimanenti 16 bit alti (CX da AX) considerando l'eventuale prestito (conservato appunto nella flag di carry) della sottrazione precedente.

ASSEMBLER (OPER_DIFFERENZA.ASM)

INCLUDE Libreria.inc ; files contenenti le macro generali
INCLUDE Output.inc  
; files contenenti le macro di output
.MODEL tiny         
; Modello di memoria
;--------------------------------------
; segmento dati
;--------------------------------------
.DATA
   A dw 32512d  
; Valore iniziale:     0111.1111.0000.0000
   B dw 1d      
; valore da sottrarre: 0000.0000.0000.0001
   R dw ?       
; Risultato - 32.511 : 0111.1110.1111.1111
;--------------------------------------
; segmento codice
;--------------------------------------
.CODE
   ORG 100h 
; il prg inizia all'indirizzo 100h
INIZIO:
  
; ESEMPIO 1: differenza senza considerare il riporto
   MOV AX, A     
; Carico il primo valore (a 32 bit) dall'indirizzo A
   MOV BX, B     
; Carico il secondo valore (a 32 bit) dall'indirizzo B
   SUB AL, BL    
; Sottraggo la parte bassa dei registri AX e BX
   SUB AH, BH    
; Ripeto la sottrazione ma non considero il riporto
   MOV R, AX     
; Salvo la differenza nella variabile R
   MS_PRINT_INT R
; Stampo la variabile R (Errore: vedo 32.767!)
   MS_ACAPO

  
; ESEMPIO 2: differenza considerando il riporto
   MOV AX, A     
; Carico il primo valore (a 32 bit) dall'indirizzo A
   MOV BX, B     
; Carico il secondo valore (a 32 bit) dall'indirizzo B
   SUB AL, BL    
; Sottraggo la parte bassa dei registri AX e BX
  
SBB AH, BH     ; Ripeto la sottrazione ma questa volta considero il riporto
   MOV R, AX     
; Salvo la differenza nella variabile R
   MS_PRINT_INT R
; Stampo la variabile R (vedo 32.511)
   MS_ACAPO

  
; ESEMPIO 3: differenza senza distinguere parte bassa e alta (corretta!)
   MOV AX, A     
; Carico il primo valore (a 32 bit) dall'indirizzo A
  
SUB AX, B      ; Sottraggo da AX il valore all'indirizzo B
   MOV R, AX     
; Salvo la differenza nella variabile R
   MS_PRINT_INT R
; Stampo la variabile R (vedo 32.511)
   MS_ACAPO

   INT 20h        ; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO


- Istruzione INC e DEC

L'istruzione INC incrementa di 1 il valore del suo operando. La sua sintassi è:

       
INC  <operando>

Il suo operando può essere un registro o una locazione di memoria. L'istruzione
DEC decrementa di 1 il valore del suo operando. La sua sintassi è:

       
DEC  <operando>

Il suo operando può essere un registro o una locazione di memoria.

- Istruzione MUL e IMUL

L'istruzione MUL esegue la moltiplicazione tra due numeri senza segno. La sua sintassi è:

       
MUL  <operando>

Corrisponde all'istruzione:
AX = AL*<operando> se l'operando è un byte
AX
= DX:AX=AX*<operando> se l'operando è un word

Quindi a seconda della dimensione dell'operando, l'istruzione si comporta in modo diverso:
- Se l'operando è di tipo byte, l'altro operando deve essere posto in
AL e il risultato viene posto in AX.
- Se l'operando è di tipo word, l'altro operando deve essere posto in
AX e il risultato viene posto nei due registri DX:AX.
Da notare è che l'operando non può essere un valore immediato. L'istruzione
MUL imposta il CF per indicare se la parte alta del risultato è usata o no: il flag è a 1 se viene usata anche la parte alta. Per eseguire moltiplicazioni tra numeri con segno si utilizza l'istruzione IMUL, che prevede le stesse caratteristiche dell'istruzione MUL con l'unica differenza che al posto del flag CF utilizza il flag di overflow OF. Inoltre IMUL può utilizzare anche due o tre argomenti.

ASSEMBLER (OPER_PRODOTTO.ASM)

INCLUDE Libreria.inc ; files contenenti le macro generali
INCLUDE Output.inc  
; files contenenti le macro di output
.MODEL tiny         
; Modello di memoria
;--------------------------------------
; segmento dati
;--------------------------------------
.DATA
   A  db 127d    
; 1° Valore di un byte (da -128 a +127)
   M1 db 127d   
 ; 2° Valore di un byte (da -128 a +127)
   M2 db -128d  
 ; 3° Valore di un byte (da -128 a +127)
   F  dw ?      
 ; Prodotto
;--------------------------------------
; segmento codice
;--------------------------------------
.CODE
   ORG 100h 
     ; il prg inizia all'indirizzo 100h
INIZIO:
  
; ESEMPIO 1: prodotto tra numeri positivi
   MOV AL, A     
; Carico il primo fattore (a 16 bit) dall'indirizzo A
   MOV BL, M1    
; Carico il secondo fattore (a 16 bit) dall'indirizzo M1
   MUL BL        
; Moltiplico BL*AL e scarico in AX
   MOV F, AX     
; Salvo la differenza nella variabile F
   MS_PRINT_INT F
; Stampo la variabile F
   MS_ACAPO

  
; ESEMPIO 2: prodotto con numeri con segno
   MOV AL, A     
; Carico il primo fattore (a 16 bit) dall'indirizzo A
   MOV BL, M2    
; Carico il secondo fattore (a 16 bit) dall'indirizzo M2
   IMUL BL       
; Moltiplico BL*AL e scarico in AX
   MOV F, AX     
; Salvo la differenza nella variabile F
   MS_PRINT_INT F
; Stampo la variabile F
   MS_ACAPO

   INT 20h        ; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO


- Istruzione DIV e IDIV

L'istruzione DIV esegue la divisione intera tra due numeri senza segno. La sua sintassi è:

       
DIV  <operando>

Corrisponde all'istruzione:
- se l'operando è un byte :
AL = AX/<operando> e AH=resto
- se l'operando è un word: AX = DX:AX/<operando> e DX=Resto

Da notare è che il divisore non può essere un valore immediato. L'istruzione
DIV può causare una condizione di overflow se il risultato è troppo grande per essere contenuto nel registro relativo. Si deve porre particolare attenzione alla divisione per 0 e per 1. Per eseguire divisioni tra numeri con segno si utilizza l'istruzione IDIV, che prevede le stesse caratteristiche dell'istruzione DIV.

ASSEMBLER (OPER_DIVISIONE.ASM)

INCLUDE Libreria.inc ; files contenenti le macro generali
INCLUDE Output.inc  
; files contenenti le macro di output
.MODEL tiny         
; Modello di memoria
;--------------------------------------
; segmento dati
;--------------------------------------
.DATA
   A  db 127d    
; 1° Valore di un byte (da -128 a +127)
   M1 db 127d   
 ; 2° Valore di un byte (da -128 a +127)
   M2 db -128d  
 ; 3° Valore di un byte (da -128 a +127)
   F  dw ?      
 ; Prodotto
;--------------------------------------
; segmento codice
;--------------------------------------
.CODE
   ORG 100h 
     ; il prg inizia all'indirizzo 100h
INIZIO:
  
; ESEMPIO 1: prodotto tra numeri positivi
   MOV AL, A     
; Carico il primo fattore (a 16 bit) dall'indirizzo A
   MOV BL, M1    
; Carico il secondo fattore (a 16 bit) dall'indirizzo M1
   MUL BL        
; Moltiplico BL*AL e scarico in AX
   MOV F, AX     
; Salvo la differenza nella variabile F
   MS_PRINT_INT F
; Stampo la variabile F
   MS_ACAPO

  
; ESEMPIO 2: prodotto con numeri con segno
   MOV AL, A     
; Carico il primo fattore (a 16 bit) dall'indirizzo A
   MOV BL, M2    
; Carico il secondo fattore (a 16 bit) dall'indirizzo M2
   IMUL BL       
; Moltiplico BL*AL e scarico in AX
   MOV F, AX     
; Salvo la differenza nella variabile F
   MS_PRINT_INT F
; Stampo la variabile F
   MS_ACAPO

   INT 20h        ; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO


- Operatore PTR

Poiché tutte le istruzioni aritmetiche e di trasferimento richiedono operandi aventi la stessa dimensione, spesso risulta difficile eseguire spostamenti o operazioni. Per permettere il normale svolgimento di tali istruzioni interviene l'operatore PTR che permette di usare un solo byte di un dato definito come word o di indicare una word con un riferimento solo al primo byte.

L’operatore PTR forza l’assemblatore a modificare per l'istruzione corrente il tipo del dato avente come identificatore nome.

Esempio 0):
.DATA
    TOT DW ?
.CODE
    MOV BH, BYTE PTR TOT
    MOV CH, BYTE PTR TOT+1

Esempio 1)
X è una variabile di tipo double (DD). Si vuole spostare nella parte bassa il contenuto del registro AX e nella parte alta il contenuto del registro BX.

MOV Word Ptr X,AX
MOV Word Ptr X+2,BX

Esempio 2)

Si vuole poi spostare il contenuto della parte bassa della variabile double X nel registro CX e il contenuto della parte alta rispettivamente nei registri DL e DH.

MOV CX,Word Ptr X
MOV DL,Byte Ptr X+2
MOV DH,Byte Ptr X+3

Esempio 3)

INC [BX]

La cella da incrementare corrisponde ad una word o ad un byte ? L’assemblatore non può saperlo e genera errore.
La soluzione è:

INC BYTE PTR [BX]

IL CONTROLLO DI FLUSSO

Il controllo di flusso è realizzato mediante istruzioni di salto. Le istruzioni di salto permettono di specificare l'indirizzo dell'istruzione successiva da eseguire, in modo da modificare la normale sequenza di esecuzione. L'istruzione di destinazione può essere specificata mediante un etichetta. Le istruzioni di salto possono essere incondizionate o condizionate: quelle incondizionate vengono sempre eseguite, mentre quelle condizionate vengono eseguite solo al verificarsi o meno di una data condizione

Le istruzioni di salto possono avere un'etichetta come operando. Un'etichetta all'interno del segmento di codice rappresenta l'indirizzo di un'istruzione. Le etichette possono essere specificate con

Nome:


I nomi delle etichette possono essere scelti liberamente, ma naturalmente devono essere unici.

- I salti incondizionati

I salti incondizionati vengono eseguiti con l'istruzione JMP, che ha sintassi:

       
JMP  <operando>

L'istruzione JMP permette di fare un salto a qualsiasi istruzione, e il suo operando può essere un etichetta.

- I salti condizionati

Per fare in modo che il salto venga eseguito solo se si verifica una particolare condizione, si usano le istruzioni di salto condizionato. Le istruzioni di salto condizionato si basano sul valore dei flag impostati dalle operazioni aritmetico-logiche e dall'istruzione CMP

L'istruzione CMP permette di confrontare due dati e ha il formato:

        CMP  <operando1>, <operando2>

Il primo operando può essere un registro o una locazione di memoria, il secondo può essere un registro, una locazione di memoria o un valore immediato; i due operandi devono avere la stessa dimensione. Inoltre i due operandi non possono essere entrambi locazioni di memoria. L'istruzione
CMP si comporta come una sottrazione; sottrae dal primo operando il secondo, trascurando però il risultato. Essa infatti modifica solo i flag affinché possano essere usati correttamente dalla successiva istruzione di salto condizionato.

Ad ogni flag sono associate due istruzioni di salto condizionato: una che salta se il suo valore è a uno, mentre l'altra salta se il valore è a zero (quest'ultima è indicata dalla lettera N che sta ad indicare Not).

Tali istruzioni sono:

-
JZ : salta se il flag Z è a 1 (Zero Flag)
-
JNZ : salta se il flag Z è a 0
-
JS : salta se il flag S è a 1 (Sign Flag)
-
JNS : salta se il flag S è a 0
-
JO : salta se il flag O è a 1 (OverFlow Flag)
-
JNO : salta se il flag O è a 0
-
JC : salta se il flag C è a 1 (Carry Flag)
-
JNC : salta se il flag C è a 0
-
JP : salta se il flag P è a 1 (Parity Flag)
-
JNP : salta se il flag P è a 0
 

Oltre alle istruzioni di salto condizionato che si riferiscono direttamente ai flag, ve ne sono altre, basate sugli stessi flag ma più facili da ricordare e utilizzare.

-
JE : salta se Operatore1=Operatore2
-
JNE : salta se Operatore1<>Operatore2

Confronto tra numeri con segno:

-
JL : salta se Operatore1<Operatore2
-
JG : salta se Operatore1>Operatore2
-
JLE : salta se Operatore1<=Operatore2
-
JGE : salta se Operatore1>=Operatore2

Confronto tra numeri senza segno:
-
JB : salta se Operatore1<Operatore2
-
JA : salta se Operatore1>Operatore2
-
JBE : salta se Operatore1<=Operatore2
-
JAE : salta se Operatore1>=Operatore2

Da notare che, nel processore 8086 le istruzioni di salto richiedono che l'indirizzo di destinazione sia abbastanza vicino: tra -128 byte e +127 byte. Ecco alcuni dei flag coinvolti nelle istruzioni di  salto condizionato

CF

(Carry Flag, flag di Riporto): viene forzata a 1 se una somma/sottrazione ha prodotto riporto/prestito; influenza i seguenti salti condizionati: JC (salta se CF=1),  JNC (salta se CF=0),  JB (salta se CF=1),  JAE (salta se CF=0),  JA (salta se CF=0 e ZF=0),  JBE (salta se CF=1 o ZF=1).
PF (Parity Flag, flag di Parità): viene forzata a 1 se il risultato di un'operazione contiene un numero pari di 1.
AF (Auxiliary Flag, flag di Riporto Ausiliario): viene forzata a 1 se un'operazione ha prodotto riporto tra il primo (bit0÷bit3) e il secondo nibble (bit4÷bit7).
ZF (Zero Flag, flag di Zero): viene forzata a 1 se un'operazione ha dato risultato nullo; influenza i seguenti salti condizionati: JZ (salta se ZF=1),  JNZ (salta se ZF=0),  JE (salta se ZF=1),  JNE (salta se ZF=0),  JG (salta se ZF=0 e SF=OF),  JLE (salta se ZF=1 o SF<>OF),  JA (salta se ZF=0 e CF=0),  JBE (salta se ZF=1 o CF=1).
SF (Sign Flag, flag di Segno): viene forzata al valore corrente del bit più significativo del risultato di un'operazione; come è noto nei numeri con segno 0 significa positivo e 1 significa negativo; influenza i seguenti salti condizionati: JS (salta se SF=1),  JNS (salta se SF=0),  JG (salta se SF=OF e ZF=0), JGE (salta se SF=OF),  JL (salta se SF<>OF),  JLE (salta se SF<>OF o ZF=1).
OF (Overflow Flag, flag di Overflow): viene forzata a 1 se un'operazione ha prodotto trabocco, cioè ha superato il numero massimo per il registro coinvolto (256=100H a 8bit, 65536=10000H a 16bit, ecc); influenza i seguenti salti condizionati: JO (salta se OF=1),  JNO (salta se OF=0).


-
L'istruzione LOOP

L’istruzione LOOP permette di realizzare un ciclo a contatore; il numero di ripetizioni desiderato deve essere posto nel registro contatore CX; ad ogni ciclo il contatore viene decrementato e quando raggiunge lo zero il ciclo viene fermato e l’esecuzione prosegue dall’istruzione successiva al ciclo. Ogni ciclo LOOP presenta una simile sintassi:
...
    Mov CX,<N>
InizioCiclo:
    ...     ; istruzioni
    LOOP InizioCiclo
...

- IF THEN ELSE END IF

La struttura condizionale ha la seguente struttura:

if Condizione then
  IstruzioniV
else
  IstruzioniF
end if

 

Per codificare in assembly questa struttura le istruzioni vanno scritte in sequenza. Se proviamo a deformare la struttura, senza però modificare i punti di giunzione otteniamo questo diagramma equivalente che maggiormente si confà alla struttura sintattica dell'assembler.

oppure

In entrambe i casi si può notare che per implementare questa struttura di controllo sono necessarie due istruzioni di salto: una condizionata e l'altra incondizionata.

Esempio:

si supponga di dover valutare il minimo tra due numeri a, b

MACRO LINGUAGGIO ASSEMBLER (IFTHEN.ASM)

SE (A <B) ALLORA
   Minimo=A
ALTRIMENTI
   Minimo=B
FINESE
Stampa Minimo

INCLUDE Output.inc ; contenenti le macro
.MODEL tiny
.DATA             
; segmento dati
   A dw 4h
   B dw 7h
   Minimo dw ?
.CODE             
; segmento codice
   ORG 100h       
; il prg inizia all'indirizzo 100h
INIZIO:
   MOV AX, A
   CMP AX, B
   JNL _ELSE
   MOV Minimo, AX
   JMP _ENDIF
_ELSE:
   MOV AX, B
   MOV Minimo, AX
_ENDIF:
   MS_PRINT_INT Minimo
   INT 20h
END INIZIO


- CICLO WHILE

L'iterazione è una struttura che permette di ripetere più volte l'esecuzione di un'istruzione strutturata, sotto il controllo di una condizione. La tipica struttura iterativa a controllo in testa è il seguente:

While Condizione
    Istruzioni
Fine While

Questa struttura può essere realizzazata con le istruzioni di salto secondo il seguente schema

...
INIZIO_WHILE:
   SE NON E' VERA LA CONDIZIONI SALTO ALLA FINE_WHILE
   ISTRUZIONI
   SALTA A INIZIO_WHILE
FINE_WHILE:
...

Che in assembler diventa:

...
INIZIO_WHILE:
   CMP ...
   JN Condizione FINE_WHILE
   <ISTRUZIONI>
   JMP INIZIO_WHILE
FINE_WHILE:
...

 

Esempio:

si supponga di dover valutare la somma dei primi N numeri interi

ASSEMBLER (WHILE.ASM)

INCLUDE Output.inc ; contenenti le macro
.MODEL tiny
.DATA             
; segmento dati
   Somma dw 0d
   N     dw 10d
.CODE             
; segmento codice
   ORG 100h       
; il prg inizia all'indirizzo 100h
INIZIO:
   MOV Somma, 0d  
; inizializzazione
   MOV CL, 1d
_INIZIO_WHILE:
   CMP CX, N
   JNBE _FINE_WHILE
   ADD Somma, CX
   INC CX
   JMP _INIZIO_WHILE
_FINE_WHILE:
   MS_PRINT_INT Somma
   INT 20h        
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

LE MACRO ISTRUZIONI E LE PROCEDURE

La stesura di un sorgente software non può fare a meno di ricorrere alla definizione e all'uso di Procedure, qualunque sia l'ambiente di programmazione coinvolto.
Anche se l'obiettivo da raggiungere nello sviluppo di un programma sia limitato e semplice è sempre buona abitudine frazionare il problema in parti piccole, efficienti e specializzate, organizzate da una procedura principale.

In questo ambito rientrano due preziosi strumenti: le Macro e le procedure.

Questa scheda si ripropone di analizzare le due strutture in modo comparativo; il suo compito è quello di aiutarti a distinguere le une dalle altre e a decidere quale delle 2 sarà in grado di assicurare la migliore efficienza.

Macro Istruzioni

Le Macro Istruzioni sono strutture importanti e utili nella programmazione in Assembly

La MacroIstruzione è definita in questo modo:

NomeMacro MACRO arg1, arg2 ....

... codice macro

ENDM

La definizione delle Macro deve essere fatta per tempo, di solito dopo quella delle costanti, all'inizio del nostro programma sorgente; questo fatto da al programmatore la possibilità di scrivere una sola riga di programma, al posto di tutte quelle racchiuse nel corpo della macro.
Spetta poi all'assemblatore sostituire a sua volta questa nostra unica riga con quelle effettive inserite nel corpo della macro associata compilando il codice ad esse relativo. Quindi in realtà, alla fine, il codice macchina è sempre lo stesso!

Il codice macchina prodotto alla fine dall'assemblatore è, dunque, lo stesso; lo scopo della MACRO non è quindi quello di ridurre il numero di bytes del programma finale ma di rendere più leggibile il sorgente, sostituendo le sequenze ripetitive di istruzioni con una unica macro Istruzione. Vediamo il seguente esempio che stampa a video due stringhe

ESEMPIO USO MACRO CON UN SOLO FILE MACRO1.ASM CON DUE FILE: MACRO2.ASM

CR EQU 13d
LF EQU 10d
; PULISCE IL VIDEO
MS_CLRSCR MACRO
    PUSH AX
    MOV AH,00H
    MOV AL,03H
    INT 10h
    POP AX
ENDM

; STAMPA UNA STRINGA
MS_PRINT_STR MACRO stringa
    PUSH AX
    PUSH DX
    LEA DX, stringa
    MOV AH, 9
    INT 21H
    POP AX
    POP DX
ENDM

.MODEL tiny
.DATA
    MSG1 DB "Messaggio 1",CR,LF, "$"
    MSG2 DB "Messaggio 2",CR,LF, "$"
.CODE ; segmento codice
   ORG 100h
INIZIO:
    MS_CLRSCR
    MS_PRINT_STR MSG1
    MS_PRINT_STR MSG2
    INT 20h
END INIZIO

IF1
  INCLUDE Lib_Macro.inc
ENDIF

.MODEL tiny
.DATA
    MSG1 DB "Messaggio 1",CR,LF, "$"
    MSG2 DB "Messaggio 2",CR,LF, "$"
.CODE
    ORG 100h
INIZIO:
    MS_CLRSCR
    MS_PRINT_STR MSG1
    MS_PRINT_STR MSG2
    INT 20h
END INIZIO

LIB_MACRO.INC

CR EQU 13d
LF EQU 10d
; PULISCE IL VIDEO
MS_CLRSCR MACRO
   PUSH AX
   MOV AH,00H
   MOV AL,03H
   INT 10h
   POP AX
ENDM

; STAMPA UNA STRINGA
MS_PRINT_STR MACRO stringa
   PUSHA
   LEA DX, stringa
   MOV AH, 9
   INT 21H
   POPA
ENDM

Disassemblando il programma eseguibile troviamo conferma alle affermazioni fatte relativamente alle macro ovvero che ogni chiamata a macro viene sostituita con il codice contenuto nel corpo della macro stessa.

ANALISI CODICE CON MACRO
C:\trans>debug putciao.com
-
u 100 120
17C7:0100 50      
PUSH AX
17C7:0101 B400    
MOV AH,00
17C7:0103 B003    
MOV AL,03
17C7:0105 CD10    
INT 10
17C7:0107 58      
POP AX

17C7:0108 50      
PUSH AX
17C7:0109 52      
PUSH DX
17C7:010A 8D162201
LEA DX,[0122]
17C7:010E B409    
MOV AH,09
17C7:0110 CD21    
INT 21
17C7:0112 58      
POP AX
17C7:0113 5A      
POP DX

17C7:0114 50      
PUSH AX
17C7:0115 52      
PUSH DX
17C7:0116 8D163001
LEA DX,[0130]
17C7:011A B409    
MOV AH,09
17C7:011C CD21     INT 21
17C7:011E 58      
POP AX
17C7:011F 5A      
POP DX

17C7:0120 CD20     INT 20
-
d 122 le
17C7:0120 4D 65 73 73 61 67-67 69 6F 20 31 0D 0A 24 Messaggio 1..$
-
d 130 le
17C7:0130 4D 65 73 73 61 67 67 69-6F 20 32 0D 0A 24 Messaggio 2..$

Vediamo ora di sottolineare un possibile problema di sintassi: quando nel corpo della macro sono presenti etichette d'indirizzo è necessario dichiararle nel corpo della macro con la pseudoOperazione LOCAL.
Sebbene la riga contenente la Macroistruzione, tra le altre del programma, dia la sensazione di eleganza e compattezza, non bisogna dimenticare l'assemblatore la sostituirà con le istruzioni del suo corpo ogni volta che viene chiamata in causa (anche molte volte nell'ambito del programma sorgente).

Per mostrare questo effetto consideriamo la seguente macro di test.le macro sia chiamata due volte come si vede nel listato:

M_Test MACRO Dato
LOCAL _UgualeAZero
    MOV AL,Dato
    CMP AL,0H
    JZ _UgualeAZero
    DEC AL
    MOV Dato, AL
_UgualeAZero:
ENDM
.MODEL tiny
.DATA
    Numero DB 12
.CODE ; segmento codice
    ORG 100h
INIZIO:
    M_Test Numero
    M_Test Numero
    INT 20h
END INIZIO

Per la presenza di
LOCAL, l'etichetta originaria _UgualeAZero viene sostituita con un nuovo simbolo fatto con 2 punti di domanda e un numero progressivo, ??0000; il numero viene incrementato ogni volta che l'assemblatore deve sostituire un'etichetta locale di una Macro, indipendentemente dal suo nome corrente e dalla Macro che la utilizza.
Così, nell'esempio, l'etichetta
_UgualeAZero viene chiamata ??0000 la prima volta e ??0001 la seconda; in questo modo l'assemblatore rende unici i simboli a riferimenti presenti nelle macro.

Se non avessimo dichiarato
LOCAL _UgualeAZero il compilatore, costretto a riscrivere 2 volte il testo del corpo della Macro avrebbe creato due etichette con lo stesso nome generando nella prima passata l'error A2004 (Redefinition of symbol), mentre nella seconda passata 2 errori (error A2026: Reference to multidefined symbol e error A2005: Symbol is multidefined: MMM1) e questo ad ogni occorrenza dell'etichetta _UgualeAZero (cioè 2 volte).

L'uso delle MacroIstruzioni è molto utile ma spesso la loro definizione, all'inizio di un programma, potrebbe diventare troppo voluminosa. La soluzione sta nell'estrarre tutte le Macro definite, di volta in volta, dai nostri programmi sorgente, raccogliendole in una libreria, partendo dal presupposto che quasi certamente torneranno utili anche in altri programmi.

Naturalmente dopo averle estratte dal programma se non si prendono provvedimenti non saranno più reperibili dal compilatore che, alla prima etichetta di macro non trovata, segnalerà errore (error A2105: Expected: instruction, directive, or label).
Per renderle ancora visibili senza che siano fisicamente presenti si usa la PseudoOperazione
INCLUDE, che si occupa di cercare sul disco fisso il file di testo indicato e di inserirlo pari pari nel punto dove abbiamo l'include. Naturalmente se il file non viene trovato viene segnalato errore (error A2116: Include file not found: PIPPO.MAC) e la direttiva viene ignorata.
 

Certamente ci si chiederà perchè la direttiva INCLUDE sia talvolta racchiusa tra altre 2 PseudoOperazioni: IF1 che obbliga il compilatore a fare l'acquisizione solo durante la prima passata mentre ENDIF è sintatticamente necessaria per chiudere la richiesta condizionata.

La presenza di IF1 evita inutile lavoro al compilatore: durante la prima passata legge il file che raccoglie le nostre MacroIstruzioni, annota le etichette di ciascuna di esse nella Tabella dei Simboli, e sostituisce le chiamate alle Macro con i rispettivi corpi; nella seconda, se non è presente IF1, aggiunge al listato tutto il testo contenuto nel file incluso, marcando con una C ogni riga aggiunta, operazione del tutto inutile, sia perchè allunga a dismisura il listato, sia perchè i compiti indispensabili sono già stati assolti durante la prima passata.

LE PROCEDURE

Una Procedura è un insieme di istruzioni specializzato nel compiere un determinato compito; Quando una sequenza di istruzioni è utilizzata di frequente, può tornar utile inserirla in una procedure che può essere utilizzata addirittura  anche da altri programmi (si pensi ad una procedura Clear_Screen).

Per questa ragione è molto saggio abituarsi ad estrarre queste parti di codice e renderle autonome, dando loro un nome e una struttura; in aggiunta può tornare utile raggrupparle in raccolte, dette Librerie, facilmente visibili da ogni sorgente interessato al loro utilizzo con l'aiuto della direttiva
EXTRN.

Vediamo ora di riassumere le caratteristiche di una Procedura: il progetto di una procedura coinvolge il programmatore in uno sforzo una-tantum, ben ripagato dal fatto che il suo lavoro sarà riutilizzabile in molte altre occasioni, con notevole risparmio di tempo
Per richiamare il servizio fornito da una procedura basta utilizzare una sola istruzione, di norma la
CALL.
Per la sua definizione si utilizzano le direttive
PROC e ENDP, all'interno delle quali sono raccolte le istruzioni chiamate a svolgere il compito affidato al sottoprogramma; ciascuna delle due direttive sarà preceduta dal nome scelto per la procedura:

NomeProcedura PROC [NEAR/FAR]

... codice procedura
   RET

NomeProcedura ENDP

Dopo la direttiva
PROC si specifica il tipo di procedura che si desidera progettare, NEAR o FAR; nel primo caso la procedura può essere chiamata solo all'interno del segmento di codice di definizione, mentre nel secondo la procedura può essere chiamata anche a partire da segmenti di codice diversi da quello di definizione.
Una procedura Near (intrasegmentale) può essere definita anche senza queste formalità, semplicemente aggiungendo : (due punti) dopo il suo nome
Il tipo e il numero di istruzioni che raccoglie è irrilevante; di certo l'ultima dovrà essere l'istruzione RET
la presenza della
RET finale consente al processore di rientrare nel programma chiamante, subito sotto l'istruzione (CALL NOMEPROCEDURA) che ha eseguito la chiamata
Il rientro è reso possibile dal fatto che l'istruzione
CALL ha salvato nello stack l'indirizzo dell’istruzione a sé successiva (la sola parte offset, se di tipo Near, o quello completo segment:offset, se di tipo Far) e che l'istruzione RET lo ripristina nei medesimi registri puntatori di programma (solo IP, se Near, o CS:IP, se Far)

E' possibile differenziare il funzionamento di una procedura passandole dei parametri; un parametro è di norma un valore binario predisposto dal programma chiamante in registri o variabili locali, condivise con la procedura. Inoltre
anche la procedura può lasciare eventuali risultati in registri o variabili locali, condivise con il programma chiamante.

La definizione e l'uso delle Procedure rende i programmi più organizzati, leggibili e fa risparmiare tempo di programmazione. Contrariamente alle MACROISTRUZIONI consente un risparmio di memoria di programma.

DIFFERENZE TRA MACRO E PROCEDURE

Siamo pronti per tirare le somme: abbiamo la consapevolezza che sia le Procedure che le Macroistruzioni sono strutture molto utili. Il problema da risolvere è: quando conviene usare le prime in alternativa alle seconde?

Entrambe rendono più leggibile il codice sorgente (non solo assembly) e riducono gli errori di programmazione, ma sono estremamente diverse tra loro! Vediamo alcune prerogative emerse nei due paragrafi precedenti.

1) L'uso di una procedura è quasi sempre inutile se non si provvede a inizializzare uno o più registri prima della sua chiamata; Il passaggio dei parametri avviene mediante l'opportuna inizializzazione di registri e di variabili di memoria
2) La procedura consente un risparmio in byte nel segmento codice
3) La macro istruzione accetta dei parametri a fianco del nome della macro
4) La macro non comporta alcun risparmio di memoria di programma
Per arrivare alla soluzione conviene rivedere le considerazioni precedenti in una prospettiva di confronto e con spirito di osservazione.
 

Riassumendo:
L'uso corretto di una Macro è quello di organizzare la chiamata di una Procedura: il suo compito è quello di sostituire (con una sola riga di programma) il gruppo di istruzioni necessarie per inizializzarla; la cosa che non fa risparmiare memoria di programma (il codice macchina prodotto dall'assemblatore è sempre lo stesso) ma rende più leggibile il programma.

Quindi quando una procedura non può essere chiamata senza averle anteposte una o più istruzioni di inizializzazione, è il momento di creare una Macro!

In sostanza la presenza di una Macroistruzione in un sorgente ASM non cambia nulla, se non esteticamente e funzionalmente; la quantità di bytes prodotta dal compilatore è esattamente identica (a parte quella risparmiata per la presenza di procedure) ma il codice risulterà più compatto e breve.
Tramite la macro i parametri da associare ai vari registri risultano semplicemente elencati uno dopo l'altro, separati da una virgola, sottolineando una volta di più la maggior leggibilità di questa struttura
 

LE OPERAZIONI FONDAMENTALI

 

LA GESTIONE DELL'OUTPUT

Singolo carattere

 

A) Una prima modalità per scrivere un singolo carattere a video è quella di utilizzare il servizio DOS (Int 21h) di stampa singolo carattere (2H) illustrato nel seguente programma:

- caricare il carattere da stampare nella parte bassa DL del registro DX
- impostare il sottoprogramma DOS di I/O da utilizzare inserendo 02h nel registro AH
- richiamare la funzione DOS mediante l'interrupt 21

ESEMPIO FUNZIONAMENTO CMP CREA CON: DEBUG<PUTC.TXT
C:\trans>debug putciao.com
-
u 100 l13
168E:0100 B402 MOV AH,02
168E:0102 B243 MOV DL,43
168E:0104 CD21 INT 21
168E:0106 B249 MOV DL,49
168E:0108 CD21 INT 21
168E:010A B241 MOV DL,41
168E:010C CD21 INT 21
168E:010E B24F MOV DL,4F
168E:0110 CD21 INT 21
168E:0112 C3 RET
-
t
AX=0200 BX=0000 CX=0013 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0102 NV UP EI PL NZ NA PO NC
168E:0102 B243 MOV DL,43
-
t
AX=0200 BX=0000 CX=0013 DX=0043 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0104 NV UP EI PL NZ NA PO NC
168E:0104 CD21 INT 21
-
p
C
AX=0243 BX=0000 CX=0013 DX=0043 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0106 NV UP EI PL NZ NA PO NC
168E:0106 B249 MOV DL,49
-
t
AX=0243 BX=0000 CX=0013 DX=0049 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0108 NV UP EI PL NZ NA PO NC
168E:0108 CD21 INT 21
-
p
I
AX=0249 BX=0000 CX=0013 DX=0049 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=010A NV UP EI PL NZ NA PO NC
168E:010A B241 MOV DL,41
-
t
AX=0249 BX=0000 CX=0013 DX=0041 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=010C NV UP EI PL NZ NA PO NC
168E:010C CD21 INT 21
-
p
A
AX=0241 BX=0000 CX=0013 DX=0041 SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=010E NV UP EI PL NZ NA PO NC
168E:010E B24F MOV DL,4F
-
t
AX=0241 BX=0000 CX=0013 DX=004F SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0110 NV UP EI PL NZ NA PO NC
168E:0110 CD21 INT 21
-
p
O
AX=024F BX=0000 CX=0013 DX=004F SP=FFFE BP=0000 SI=0000 DI=0000
DS=168E ES=168E SS=168E CS=168E IP=0112 NV UP EI PL NZ NA PO NC
168E:0112 C3 RET
a
MOV AH, 2
MOV DL, 43
INT 21
MOV DL, 49
INT 21
MOV DL, 41
INT 21
MOV DL, 4F
INT 21
RET

n putciao.com
r cx
13
w
q
 
Legenda:
Ricordarsi di aggiungere l'invio nel file

B) Un'altra alternativa per scrivere un carattere a schermo è quello di utilizzare i servizi BIOS relativi al Video (Int 10h). In questo caso occorre seguire questi passaggi:

- caricare il carattere da stampare nella parte bassa AL del registro AX
- impostare il sottoprogramma BIOS (stampa carattere in modo tty) da utilizzare inserendo 0Eh nel registro AH
- richiamare la funzione BIOS mediante l'interrupt 10

ESEMPIO STAMPA SINGOLO CARATTERE CREA CON: DEBUG<PUTC.TXT
C:\trans>debug < putc.txt
-a
166C:0100 MOV AH, E
166C:0102 MOV AL, 43
166C:0104 INT 10
166C:0106 MOV AL, 49
166C:0108 INT 10
166C:010A MOV AL, 41
166C:010C INT 10
166C:010E MOV AL, 4F
166C:0110 INT 10
166C:0112 RET
166C:01
13
-n putciao.com
-r cx
CX 0000
:
13
-w
Scrittura di 00013 byte in corso
-q

C:\trans>putciao
CIAO
C:\trans>
 

a
MOV AH, E
MOV AL, 43
INT 10
MOV AL, 49
INT 10
MOV AL, 41
INT 10
MOV AL, 4F
INT 10
RET

n putciao.com
r cx
13
w
q

 
Legenda:
Ricordarsi di aggiungere l'invio nel file dopo la q

C) Altra modalità è quella di scrivere direttamente nella memoria video. Si consiglia di analizzare l'approfondimento relativo alla scheda video.

Adesso analizziamo un codice di esempio (memvideo_puts.asm) che non fa ricorso ad alcun interrupt ma scrive direttamente nella memoria video il testo da visualizzare.

.MODEL tiny
.CODE ; segmento codice
   ORG 100h      ; il prg inizia all'indirizzo 100h
INIZIO:

   MOV AX,0b800h      
; posiziono il registro ES all'inizio
   MOV ES,AX          
; della memoria relativa al modo testo

   MOV AL, 'A'        
; Scrive la lettera A
   MOV AH, 17h        
; in bianco su sfondo blu
   MOV ES:[00h], AX   
; sulla riga 1 - colonna 1

   MOV AL, 'Z'        
; Scrive la lettera I
   MOV ES:[02h], AL   
; sulla riga 1 - colonna 2
   MOV AH, 79h        
; Coloro di blu su sfondo bianco
   MOV ES:[03h], AL   
; il metodo è equivalente a quello usato per la A

   INT 20h            
; termina il programma - equivalente all'int 21h AL=00h

END INIZIO

ESEMPIO STAMPA STRINGA
AZ   <== si noti che questa AZ è stata scritta dall'eseguibile memvideo_putc.asm
C:\trans>
ml memvideo_putc.asm
Microsoft (R) Macro Assembler Version 6.15.8803
Copyright (C) Microsoft Corp 1981-2000. All rights reserved.

Assembling: memvideo_putc.asm

Microsoft (R) Segmented Executable Linker Version 5.60.339 Dec 5 1994
Copyright (C) Microsoft Corp 1984-1993. All rights reserved.

Object Modules [.obj]: memvideo_putc.obj /t
Run File [memvideo_putc.com]: "memvideo_putc.com"
List File [nul.map]: NUL
Libraries [.lib]:
Definitions File [nul.def]:

C:\TRANS>
memvideo_putc

La macro che consente di stampare a video un singolo carattere che è inclusa nella libreria OUTPUT.INC allegata è

MS_PUTC MACRO char
   PUSH AX
    ; Salvo il valore nel registro
   ; Utilizza le funzioni VIDEO & SCREEN Service (int10 0Eh):
   ; scrive un carattere in modalità teletype
   MOV AL, char
   MOV AH, 0Eh
   INT 10h
   POP AX    
; Ripristina valore nel registro
ENDM


Ecco come utilizzarla (UsoLib_Putc.asm):

INCLUDE output.inc    ; inclusione della libreria contenente le macro
.MODEL tiny
.DATA                
; segmento dati
    Lettera DB 'M'
.CODE                
; segmento codice
    ORG 100h         
; il prg inizia all'indirizzo 100h
INIZIO:
    MS_CLRSCR        
; Macro per la pulizia del video
    MS_PUTC Lettera  
; Macro per la stampa di una variabile carattere
    MS_PUTC "S"      
; Macro per la stampa di una costante carattere 
    INT 20h          
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

Stringa di caratteri

 

A) Un modo per stampare una stringa di caratteri è quello di utilizzare il servizio DOS (Int 21h) relativo alla stampa di una stringa (AH=09h). Un programma di esempio (int21_prints.asm) per la stampa di una stringa è il seguente:

.MODEL tiny
.DATA
   frase DB "Ciao$"
.CODE
   ORG 0100H
INIZIO:
   MOV AH, 09H
   LEA DX, frase
   INT 21H
   RET
END INIZIO

Questo programma d'esempio assemblato mediante ML INT21_PRINTS.ASM fornisce l'eseguibile prints.com

ESEMPIO STAMPA STRINGA CREA CON: DEBUG<PRINTS.TXT
C:\trans>ml Int21_prints.asm
Microsoft (R) Macro Assembler Version 6.15.8803
Copyright (C) Microsoft Corp 1981-2000. All rights reserved.

Assembling: Int21_prints.asm

Microsoft (R) Segmented Executable Linker Version 5.60.339 Dec 5 1994
Copyright (C) Microsoft Corp 1984-1993. All rights reserved.

Object Modules [.obj]: Int21_prints.obj /t
Run File [prints.com]: "Int21_prints.com"
List File [nul.map]: NUL
Libraries [.lib]:
Definitions File [nul.def]:

C:\trans>
Int21_prints
Ciao
C:\TRANS>
debug Int21_prints.com
-
u 100 l9
17C1:0100 B409     MOV AH,09
17C1:0102 8D160A01 LEA DX,[
010A]
17C1:0106 CD21     INT 21
17C1:0108 C3       RET
-d
10A l5
17C1:0100 43 69 61 6F 24 Ciao$
-g
Ciao
L'esecuzione del programma è terminata normalmente
a
MOV AH, 9
LEA DX,[010A]
INT 21
RET

f 10A 10E 'C' 'i' 'a' 'o' '$'
n prints.com
r cx
F
w
q

 
Legenda:
Ricordarsi di aggiungere l'invio nel file dopo la q

La funzione DOS di stampa una stringa richiede necessariamente che questa sia terminata con il carattere '$'.

B) E' possibile visualizzare una stringa anche mediante un loop che stampi ogni singolo carattere della sequenza. (sorgente loop_prints.asm)

.MODEL tiny
.DATA
   frase db "Ciao$"
.CODE
   ORG 0100H
INIZIO:
   LEA SI,frase       ; metto in SI l'indirizzo della frase
_leggiUnChr:
   MOV DL,DS:[SI]     ; carico in DL l'SI-esimo carattere (che è all'indirizzo DS:SI)
   CMP DL,'$'         ; se corrisponde al carattere di stop mi fermo
   JZ _fine

   MOV AH,2H          ; Servizio DOS 2H di stampa singolo carattere
   INT 21H

   INC SI
   JMP SHORT _leggiUnChr
_fine:
   INT 20h          
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

La macro che consente di stampare a video una stringa che è inclusa nella libreria OUTPUT.INC allegata è

MS_PRINT_STR MACRO stringa
    PUSH AX   
; Salvo i valori dei registri
    PUSH DX   
; DX e AX
    ; Utilizza il servizio DOS (Int21) print string (int21 09h)
    ; che scrive una stringa in modalità teletype
    LEA DX, stringa
    MOV AH, 9
    INT 21H
    POP DX    
; Ripristina i valori nei registri
    POP AX    
; AX e DX
ENDM

Ecco un esempio di programma che mostra come utilizzarla (UsoLib_prints.asm):

INCLUDE output.inc     ; inclusione della libreria contenente le macro
.MODEL tiny
.DATA
   Msg DB "Salve a Tutti", 13d, 10d, "$"
.CODE                 
; segmento codice
   ORG 100h           
; il prg inizia all'indirizzo 100h
INIZIO:
   MS_PRINT_STR Msg   
; Macro per la stampa di una stringa
   INT 20h            
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO
 

Numero

 

In assembler i dati vengono interpretati sempre come sequenze di byte pertanto la stampa di un numero richiede una conversione del dato in stringa. Per stampare viene costruito un ciclo che estrae ad ogni iterazione un digit della rappresentazione decimale (resto divisione per 10). Visualizziamo la macro

MS_PRINT_INT MACRO Numero
Local _NonNegativo, _Stampa, _Loop
    PUSHA             
; Salva il contenuto dei registri nello stack
    MOV AX, Numero    
; mette il valore da stampare in AX
    CMP AX, 0
    JNS _NonNegativo  
; salta alla gestione dei positivi se non c'è segno
    NEG AX            
; Se negativo cambio di segno con NEG
    MS_PUTC '-'       
; e stampo il segno

_NonNegativo:
   
; Inizializzazione a 10 per estrarre i singoli numeri
   
; della rappresentazione in base 10
    MOV BX, 10
    MOV CX, 0    
; CX conta il numero di digit presenti nel numero

_Loop:
    MOV DX, 0    
; questa istruzione è necessaria poichè potrei avere un Divide Overflow
    DIV BX       
; effettua la divisione tra il numero nei 32 bit DX:AX e BX (contiene 10d) -
                 
; Essendo BX a 16 bit (1 Word) il resto della divisione va in DX
    ADD DX, 48   
; aggiungo 48 ovvero il codice ASCII di zero
    PUSH DX      
; memorizzo nello stack il resto
    INC CX       
; incremento di CX che conta i digit estratti
    CMP AX, 0
    JNE _Loop    
; se in AX non ho zero ripeto l'operazione

_Stampa:
    POP DX         
; copio il valore in cima allo stack in DX
    MOV AH, 02h    
; stampa di un singolo carattere mediante il
    INT 21h        
; servizio dos INT 21 - 02h
    LOOP _Stampa   
; ripeto CX volte la stampa
    POPA           
; Ripristina i valori nei registri
ENDM

Ecco un esempio di programma che mostra come utilizzarla (UsoLib_printn.asm):

INCLUDE libreria.inc    ; inclusione della libreria contenente le macro
INCLUDE output.inc    
; inclusione della libreria contenente le macro
.MODEL tiny
.DATA                
; segmento dati
    Nr dw 21
.CODE                
; segmento codice
    ORG 100h         
; il prg inizia all'indirizzo 100h
INIZIO:
    MS_PRINT_INT Nr
    MS_ACAPO
    MS_PRINT_INT -3
    MS_ACAPO
    MS_PRINT_INT 0
    INT 20h          
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

LA GESTIONE DELL'INPUT

Un primo metodo di fornire dei dati è quello passarglierli direttamente come i parametri sulla linea di comando.

E' il tipico approccio applicato per i cosiddetti switch. Gli switch sono comandi secchi, di solito costituiti da una sola lettera con davanti uno slash o il segno meno (ad esempio:  /H o -H). Questa tecnica consente di 
creare un eseguibile multifunzionale in grado di funzionare in modalità diverse a seconda della situazione contingente al momento del suo lancio.
 
Prima di procedere vediamo un pò di teoria

 

Il PSP  (prefisso del segmento di programma) è un'area di memoria di 256 bytes (100h) collocata all'inizio del segmento di memoria destinato (dal loader del dos) ad un Programma eseguibile, nel momento del suo caricamento in memoria.
Quando il Caricatore DOS (loader) è chiamato ad allocare in memoria un programma eseguibile riserva per esso la prima zona di memoria Ram libera [almeno un intero segmento (65526 locazioni consecutive)] facendole puntare tutti i registri di segmento, CS, DS, ES e SS.
Nelle prime locazioni di questo segmento, a partire cioè dall'indirizzo di offset 0000H, predispone il PSP, una vera miniera di informazioni destinate alla gestione del programma stesso, ma estremamente utili anche a noi.

In particolare le prime 92 locazioni, al di sotto dell'offset 005CH, contengono dati gestionali molto importanti, come:
- l'indirizzo a cui verrà ceduto il controllo quando il programma ha termine,
- gli indirizzi delle procedure di servizio degli errori critici e della combinazione di tasti Ctrl-C,
- il puntatore alle stringhe Ascii dell'Environment (ambiente) che DOS utilizza per passare informazioni al programma.

Per questa ragione è fortemente consigliato non alterare il contenuto del PSP, almeno al fino a questo indirizzo.

Il contenuto delle rimanenti 164 locazioni non è indispensabile: sono sostanzialmente dei buffer di servizio utilizzabili dal nostro programma per il trasferimento di dati verso o da la memoria di massa, peraltro con tecniche ormai obsolete.
Ciò significa che generalmente questa parte del prefisso può essere manipolata.

Solitamente il programma (codice e dati) viene caricato subito dopo il PSP, a partire dalla locazione 0100H, almeno per gli eseguibili di tipo COM: in questo caso il contenuto dei registri di segmento non viene modificato, cioè puntano ancora tutti l'inizio del PSP. I 4 segmenti sono dunque fisicamente sovrapposti.
In un programma tipo EXE può capitare invece che la sua intestazione (header) suggerisca al loader di cambiare l'indirizzo di partenza del codice (comunque puntato da CS:IP) lasciando in IP ad un valore diverso da 0100H; in questo caso può succedere che CS e SS assumano valori diversi da quelli inizialmente predisposti.
In ogni caso DS e ES continuano dunque a puntare all'inizio del PSP.

Le ultime 128 locazioni del PSP (da 0080H a 00FFH) sono senz'altro riutilizzabili senza problemi dal programma.
Di solito questa zona di memoria è usata dal Dos per 2 funzioni molto specifiche:
- come Buffer temporaneo di default per i dati da trasferire da e verso un disco: quest'area è significativa solo con le (obsolete) funzioni DOS che gestiscono i files mediante la tecnica dei File Control Block; in questo caso questa zona assume il nome di Area di Trasferimento per il Disco (DTA, Disk Transfer Area).
- come area di memoria per la stringa dei parametri eventualmente passati al nostro programma sulla linea di comando;


Per capire questa modalità analizziamo in dettaglio questo esempio. Si consideri un comando Set_Date che rivisualizza la data e l'ora digitate sulla linea di comando

C:\LAVORO>Set_Date 04/01/2010 12:21:32

Vediamo la situazione in memoria, dentro il PSP (dopo le prime 128 / 80h locazioni), dopo aver lanciato il comando suggerito precedentemente:
 

ESEMPIO LETTURA PARAMETRI IN LINEA CREA CON: DEBUG<PRN_DATE.TXT
C:\trans>debug prn_date.com 04/01/2010 12:21:32
-
d 80
17C7:0080 14
20 30 34 2F 30 31 2F-32 30 31 30 20 31 32 3A . 04/01/2010 12:
17C7:0090
32 31 3A 33 32 0D 00 00-
00 00 00 00 00 00 00 00 21:32...........
17C7:00A0
00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
17C7:00B0
00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
17C7:00C0
00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
17C7:00D0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
17C7:00E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
17C7:00F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
-
u 100 12a
17C7:0100 B400     MOV   AH,00
17C7:0102 A08000   MOV   AL,[0080]
17C7:0105 48       DEC   AX
17C7:0106 83F800   CMP   AX,+00
17C7:0109 7C17     JL    0122
17C7:010B FC       CLD
17C7:010C BE8200   MOV   SI,0082
17C7:010F 8D3E2C01 LEA   DI,[012C]
17C7:0113 8BC8     MOV   CX,AX
17C7:0115 F3       REPZ
17C7:0116 A4       MOVSB
17C7:0117 03F9     ADD   DI,CX
17C7:0119 C60524   MOV   BYTE PTR [DI],24
17C7:011C 8D162C01 LEA   DX,[012C]
17C7:0120 EB04     JMP   0126
17C7:0122 8D16AA01 LEA   DX,[01AA]
17C7:0126 B409     MOV   AH,09
17C7:0128 CD21     INT   21
17C7:012A C3       RET
-
q

C:\trans>prn_date
Non hai digitato alcun parametro in linea!
C:\trans>prn_date 04/01/2010 12.21.32
04/01/2010 12.21.32
a
MOV AH,0
MOV AL,[80]
DEC AX
CMP AX,0
JL 0122
CLD
MOV SI,82
LEA DI,[12C]
MOV CX,AX
REPZ
MOVSB
ADD DI,CX
MOV BYTE PTR [DI],24
LEA DX,[012C]
JMP 0126
LEA DX,[1AA]
MOV AH,9
INT 21
RET

f 12c 1a9 20
f 1aa l2b 'Non hai digitato alcun parametro in linea!$'
n prn_date.com
r cx
D5
w
q



 
Legenda:
Ricordarsi di aggiungere l'invio nel file dopo la q


Osserviamo che 21 bytes del PSP sono stati coinvolti ed esattamente 1 in più rispetto al numero di caratteri digitati dopo il nome del programma sul prompt del Dos (la stringa compresa lo spazio iniziale " 04/01/2010 12:21:32" ha 20 caratteri). 
In pratica quella dei parametri è una struttura ben precisa: il primo byte (all'indirizzo 0080H) esprime la lunghezza della coda del comando (cioè della sequenza di parametri). In altre parole è il numero esadecimale dei caratteri digitati dopo il nome dell'eseguibile. L'ultimo byte (dopo l'ultimo carattere della stringa) è sempre 0DH, il valore Ascii che rappresenta la pressione del tasto Invio.
Quindi il testo dei parametri in linea inizia sempre alla locazione 0082H e non può essere più lungo di 126 caratteri; in caso contrario andrebbe a sovrascrivere il codice macchina del nostro eseguibile, senza dubbio presente a partire da 0100H. In ogni caso il Dos previene (ed evita) questa eventualità  limitando d'ufficio il numero di caratteri digitabili in una riga di comando, e segnalando acusticamente con lo speaker di sistema ogni eventuale abuso.

Mostriamo ora in che modo il nostro programma (prn_date.asm) può leggere i parametri passati sul comando di linea (cioè digitati dopo il nome del programma). Prendiamo come riferimento il comando dato all'inizio della pagina per inizializzare l'ora.
 

.MODEL tiny
.DATA                
        ; segmento dati
    Buffer db 126 dup (' ')  
; area per registrare i parametri digitati linea
    Avviso db 'Non hai digitato alcun parametro in linea!$'
.CODE                
        ; segmento codice
    ORG 100h         
        ; il prg inizia all'indirizzo 100h
INIZIO:

    MOV AH, 0        
; Carico in AX la lunghezza della stringa digitata sulla
    MOV AL, DS:[80h] 
; linea di comando, dopo il nome dell'eseguibile sul prompt,
                      ; il cui valore è scritto all'indirizzo DS:[80h]
    DEC AX            ; decremento di 1 tale valore poiché escludo dalla conta l'invio finale

    CMP AX, 0        
; se AX < 0 stamperò che non ho
    JL _noparam      
; argomenti in linea di comando
 

    ; Sequenza per copiare nell'array BUFFER i parametri digitati

    ; sulla linea di comando - si rimanda all'CrossRef relativo al

    ; comando MOVSB
    CLD              
; Setta a 0 il flag DF - DI e SI verranno quindi incrementati
    MOV SI,82H       
; indirizzo stringa sorgente
    LEA DI,buffer    
; indirizzo stringa di destinazione
    MOV CX, AX       
; quante volte ripeto MOVSB
    REP MOVSB

    ADD DI, CX
    MOV byte ptr [di], '$'
; aggiungo in fondo il terminatore di stringa

_siparam:
    LEA DX, buffer
    JMP _stampa
_noparam:             
; carica la stringa che avvisa che non sono digitati
    LEA DX, Avviso    
; parametri di linea
_stampa:
    MOV AH, 09h       
; stampa la stringa il cui indirizzo è in DX
    INT 21h           
; servizio DOS 21h - 09h: stampa stringa
    RET  
END INIZIO


Per funzionare correttamente la zona dati del nostro programma deve prevedere un'adeguata quantità di bytes, identificata con l'etichetta
Buffer, in cui trasferire i caratteri del parametro in linea.
Il codice suggerito deve essere il primo ad essere eseguito, per evitare di perdere la precaria informazione del PSP.

 

La macro che consente di leggere la stringa digitata sulla linea di comando subito dopo il nome del programma, inclusa nella libreria INPUT.INC allegata, è:

MS_GETARGS MACRO Q, Stringa
    PUSHA    
; copia in cima allo stack il contenuto di tutti i registri d'uso generale
             
; (ax, cx, dx, bx, sp, bp, si, di) e decrementa di 16 il valore nel registro SP
    PUSHF    
; decrementa il registro SP di 2 e carica una copia del
             
; registro dei flag in cima allo stack.

    MOV AH, 0        
; Carico in AX la lunghezza della stringa
    MOV AL, DS:[80h] 
; digitata dopo il nome dell'eseguibile sul prompt
    DEC AX           
; del dos - escludo dalla conta l'invio finale
    MOV Q, AX      
; registro in NR il numero di caratteri digitati

    CMP AX, 0        
; se AX < 0 esco dalla macro lasciando NR a 0
    JL _Esci

    CLD              
; Setta a 0 il flag DF - DI e SI verranno quindi incrementati
    MOV SI, 82H      
; indirizzo stringa sorgente
    LEA DI, Stringa  
; indirizzo stringa di destinazione
    MOV CX, AX       
; quante volte ripeto MOVSB
    REP MOVSB

    ADD DI, CX
    MOV byte ptr [DI], '$'
; aggiungo in fondo il terminatore di stringa

_Esci:
    POPF
    POPA
ENDM

Ecco un esempio di programma che mostra come utilizzarla (UsoLib_LeggiParam.asm):

INCLUDE Input.inc              ; inclusione delle librerie
INCLUDE Output.inc    
        ; contenenti le macro  

.MODEL tiny
.DATA                     
    ; segmento dati
    Nr dw 0
    Buffer db 126 dup (' ')   
; area per registrare i parametri digitati linea
    Avviso db 'Non hai digitato alcun parametro in linea!$'
.CODE                         
; segmento codice
    ORG 100h          
        ; il prg inizia all'indirizzo 100h
INIZIO:

    MS_GETARGS Nr, Buffer
    MOV AX, Nr    
 ; ricarico in AX il Nr di caratteri scritto sulla linea di comando
    CMP AX, 0       
 ; se AX < 0 stamperò che non ho
    JL _NoParam     
 ; argomenti in linea di comando
    MS_PRINT_STR Buffer
    JMP _Fine
_NoParam:            
; carica la stringa che avvisa che non sono digitati
    MS_PRINT_STR Avviso
_Fine:
    INT 20h          
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

Singolo carattere

 

Una prima modalità per leggere un singolo carattere da tastiera è quello di utilizzare il servizio DOS (Int 21h) di lettura singolo carattere (1h) con visualizzazione del carattere appena digitato (ECHO). I passaggi da svolgere sono i seguenti:

- impostare il sottoprogramma DOS di I/O da utilizzare inserendo 01h nel registro AH
- richiamare la funzione DOS mediante l'interrupt 21
- caricare in una opportuna locazione di memoria il carattere appena letto salvato nella parte bassa DL del registro DX
 

Una seconda modalità per leggere un singolo carattere da tastiera è quello di utilizzare il servizio DOS (Int 21h) di lettura singolo carattere (8h) senza visualizzare il carattere appena digitato (NO ECHO). I passaggi da svolgere sono i seguenti:

- impostare il sottoprogramma DOS di I/O da utilizzare inserendo 08h nel registro AH
- richiamare la funzione DOS mediante l'interrupt 21
- caricare in una opportuna locazione di memoria il carattere appena letto salvato nella parte bassa DL del registro DX
 

Le macro che consentono questa lettura, incluse nella libreria INPUT.INC allegata, sono:

MS_GETC_ECHO MACRO Char
   PUSH AX     
; Salvo i valori dei registri
   MOV AH, 01h 
; Servizio dos Int 21h - 01h lettura con echo
   INT 21h
   MOV Char, AL
   POP AX      
; Ripristino i valori dei registri
ENDM

MS_GETC_NOECHO MACRO Char
   PUSH AX     
; Salvo i valori dei registri
   MOV AH, 08h 
; Servizio dos Int 21h - 08h lettura senza echo
   INT 21h
   MOV Char, AL
   POP AX      
; Ripristino i valori dei registri
ENDM

Ecco un esempio di programma che mostra come utilizzarla (UsoLib_LeggiChar.asm):

INCLUDE libreria.inc           ; inclusione delle librerie
INCLUDE Input.inc     
        ; contenenti le macro
INCLUDE Output.inc    
          

.MODEL tiny
.DATA                         
; segmento dati
   Carattere DB ?
.CODE                         
; segmento codice
   ORG 100h
INIZIO:
   MS_GETC_NOECHO Carattere
   MS_PUTC Carattere

   MS_GETC_ECHO Carattere
   INT 20h                    
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO
 

Viene proposta un'ulteriore macro che legge un singolo carattere ma consente anche di annullarlo mediante l'uso del backspace. Il carattere viene confermato quando batto invio (sempre inclusa nella libreria INPUT.INC allegata)

MS_GETC MACRO Char
LOCAL _Lettura1Lettera, _Check_AltriChar, _LetturaSuccLettere ,_FineLettura
   PUSHA

_Lettura1Lettera:
   MOV CL, 0h             
; Inizializzo come se non avessi letto niente
   MOV AH, 01h            
; Servizio dos Int 21h - 01h lettura con echo
   INT 21h
   MOV CL, AL             
; Salvo in CL la lettera appena letta
   CMP AL, BackSpace      
; Se scrivo subito BackSpace
   JE _Lettura1Lettera    
; lo ignoro e riparto con la lettura
   CMP AL, CarriageReturn 
; considero confermata la lettura con l'invio
   JE _FineLettura        
; e salto alla fine della macro

_LetturaSuccLettere:
   MOV AH, 08h            
; Servizio dos Int 21h - 01h lettura senza echo
   INT 21h                
; Aanalizzo eventuali caratteri successivi
   CMP AL, BackSpace      
; Se scrivo BackSpace cancello
   JNE _Check_AltriChar   
; Se non ho scritto backspace analizzo eventuali char nel buffer
   MS_ERASECHAR           
; Se scrivo backspace elimino l'ultimo carattere
   JMP _Lettura1Lettera

_Check_AltriChar:
   CMP AL, CarriageReturn
   JE _FineLettura
   MS_BEEP                
; emetto il BEEP se non è un invio o il primo char scritto
   JMP _LetturaSuccLettere

_FineLettura:
   MS_ACAPO
   MOV Char,CL
   POPA
ENDM

Ecco un esempio di programma che mostra come utilizzarla (UsoLib_AdvReadChar.asm):
 

INCLUDE libreria.inc           ; inclusione delle librerie
INCLUDE Input.inc     
        ; contenenti le macro
INCLUDE Output.inc    
          

.MODEL tiny
.DATA ; segmento dati
   Carattere DB ?
.CODE ; segmento codice
   ORG 100h
INIZIO:
   MS_GETC A_Char
   MS_GETC B_Char
   MS_PUTC A_Char
   MS_PUTC B_Char
   INT 20h                    
; Termina il programma - equivalente all'int 21h AL=00h
END INIZIO

;-----------------------------------------------------------------------------;
;		WRITE_STRING(DS:SI Text)                                      ;
;-----------------------------------------------------------------------------;
;	Writes string from DS:SI until character #0 is met		      ;
Write_String:		
	mov ah, 0xE	
	xor bh, bh	
	mov bl, 0x7
.nextchar	
	lodsb		
	or al,al
	jz .return
	int 10h	
	jmp .nextchar
.return		
	ret	

 

BiosCls :MOV AH,00H ;Pulisci lo schermo
         MOV AL,03H ;(ClearScreen)
         INT 10H
         RET

KeyWait :MOV    AH,00H    ;Aspetta la pressione
         INT    16H       ;di un tasto
         RET

_prog   SEGMENT BYTE PUBLIC 'CODE'
        ASSUME CS:_prog,DS:_prog
        ORG    0100H
INIZIO: JMP    Main

KeyWait:MOV    AH,00H    ;Aspetta la pressione
        INT    16H       ;di un tasto
        RET

BiosCls:MOV    AH,00H    ;Pulisci lo schermo
        MOV    AL,03H    ;(ClearScreen)
        INT    10H
        RET

Main:   CALL   BiosCls
        CALL   KeyWait
        MOV    AH,4CH     ;Torna al dos
        INT    21H

_prog   ENDS
------------------------------
; Ricerca del massimo in un vettore (array), questa volta uso scasb per 
; scandire il vettore array, inoltre uso un trucco per definire in 
; modo automatico la lunghezza del vettore. 
;
;
; Per compilare usare tasm  /Zi nomefile.asm
;		      tlink /v  nomefile
; Quindi per eseguire in debug  td nomefile.exe
;	
.MODEL  SMALL    ; uso della memoria
.STACK  100h     ; profondita' stack
.386             ; uso anche istruzioni del 386 e successivi
;
; segmento dati
.DATA
array            DB  5, 4, 88, -6, 23, 1, 2, -36, 100, -120
                 db  8, 11, 0, -33, 44
len              equ $ - array    ; $ indica l'indirizzo corrente, 
                                  ; quindi calcolo quanto e' lungo array
add_arr          dd  array        ; far address of array, cioe' cs:indirizzo

ciao             DB  13,10,'Esempio di programma in Assembler',13,10,'$'
risp             DB  13,10,'Il massimo : $'
endmes           db  13,10,'- Fine',13, 10,'$'

n                dw  len                ;lunghezza array 
max              db  ?                  ; la risposta

; segmento codice
.CODE
inizio:                              ; comincia qui l'esecuzione
  mov     ax,@data
  mov     ds,ax                      ;set DS to point to the data segment
  mov     dx,OFFSET ciao             ;point to the time prompt
  mov     ah,9                       ;DOS: print string
  int     21h                        ;display the time prompt
  
  xor ax,ax                      ;   azzeriamo tutti i registri
  xor bx, bx                     ;   (solo per chiarezza, e' inutile)
  xor cx, cx
  xor dx, dx

  mov al, array                  ; primo elemento del vettore       
  mov max, al                    ; lo salvo, caso mai fosse il massimo
  les di,add_arr                 ; indirizzo d'inizio del vettore
  mov cx, n                      ; numero di elementi
  cld                            ; clear direction flag for scasb

ciclo:                           ; inizia la ricerca
  scasb                          ; al > es:[di] ?
  jg salta                       ; no, al prossimo
  dec di                         ; si decrementiamo puntatore
  mov al, es:[di]                ; sostituiamo il massimo
  inc di                         ; incrementiamo il puntatore
salta:
  loop ciclo                     ; controllo se e' l'ultimo e decremento cx


fine:
  mov max, al                    ; salviamo il risultato  in max
; ora stampo la risposta, prima un messaggio...                                              
  mov     dx,OFFSET risp             ;point to the time prompt
  mov     ah,9                       ;DOS: print string
  int     21h                        ;display the prompt

; quindi max  
  mov al, max
  shr al,4                       ; isoliamo una cifra esadecimale
  call print                     ; stampiamo
  mov al, max                    ; riprendo risultato
  and al, 0fH                    ; isolo l'altra cifra
  call print

  mov     dx,OFFSET endmes           ;messaggio finale
  mov     ah,9                       ;DOS: print string
  int     21h                        ;display the prompt
  
  mov     ah,4ch                  ;DOS: terminate program
  mov     al,0                    ;return code will be 0  
  int     21h                     ;terminate the program

; procedura ausiliaria per trasformare una cifra esadecimale in carattere
; e stamparla.
print:
      add al, 30h               ;converto le cifre 1-9 in caratteri
      cmp al, 39H               ; A-F hanno bisogno di un ritocco
      jbe cambia
      add al,7
cambia:
      mov dl, al               ; uso servizio del bios per stampare
      mov ah, 2
      int 21h
      ret
      end inizio               ; fine dell'assembler, l'esecuzione
;                                  dovra' cominciare da inizio
------------------------------
; Programma di esempio in assembler: 
; Scopo: Ricerca del massimo nel vettore "array"
;
; Le prime righe contengono alcuni comandi per indicare all'assemblatore 
; il tipo di programma e come verra' gestita la memoria
;
; Per compilare usare tasm  /Zi nomefile.asm
;		      tlink /v  nomefile
; Quindi per eseguire in debug  td nomefile.exe
;
	
.MODEL  SMALL    ; uso della memoria
.STACK  100h     ; profondita'dello  stack  (256 decimale)
.386             ; uso anche istruzioni del 386
;
.DATA           ; I dati, contenuti nel loro segmento 
array            DB  5, 4, 88, -6, 23, 1, 2, -36, 100, -120
ciao             DB  13,10,'Esempio di programma in Assembler',13,10,'$'
risp             DB  13,10,'Il massimo : $'
endmes           db  13,10,'- Fine',13, 10,'$'
n                dw  10                 ;lunghezza array 
max              db  ?                  ; la risposta

.CODE            ; Qui inizia il vero programma nel suo segmento codice
inizio:                              
  mov     ax,@data
  mov     ds,ax                  ; DS punta al data segment
  mov     dx,OFFSET ciao         ; messaggio iniziale 
  mov     ah,9                   ; DOS: print string
  int     21h                     

  mov al, array                  ; primo elemento del vettore       
  mov max, al                    ; lo salvo, caso mai fosse il massimo
  mov bx, offset array           ; indirizzo d'inizio del vettore
  mov cx, bx                     ; ne faccio una copia
  add cx, n                      ; indirizzo finale + 1
  dec cx                         ; indirizzo finale

ciclo:                           ; inizia la ricerca
  inc bx                         ; punto all'elemento successivo
  cmp bx, cx                     ; e' l'ultimo ?
  ja  fine                       ; si
  cmp al, [bx]                   ; no, allora lo confronto, e' maggiore ?
  jg  ciclo                      ; no, vado al prossimo
  mov al, [bx]                   ; si, lo sostituisco
  jmp ciclo                      ; e passo al prossimo

fine:
; ora stampo il contenuto di al
  mov max, al                    ; salviamo il risultato  in max
  mov     dx,OFFSET risp         ; il  messaggio che precede il risultato
  mov     ah,9                   ; DOS: print string
  int     21h                    
  
  mov al, max
  shr al,4                       ; isoliamo una cifra esadecimale
  call print                     ; stampiamo
  mov al, max                    ; riprendo risultato
  and al, 0fH                    ; isolo l'altra cifra
  call print
  mov     dx,OFFSET endmes       ; messaggio di fine
  mov     ah,9                   ; DOS: print string
  int     21h                        
  mov     ah,4ch                 ; DOS: terminate program
  mov     al,0                    
  int     21h                    

print:
      add al, 30h               ; converto le cifre 0-9 in caratteri
      cmp al, 39H               ; A-F hanno bisogno di un ritocco
      jbe cambia
      add al,7
cambia:
      mov dl, al                ; uso servizio del bios per stampare
      mov ah, 2
      int 21h
      ret
      end inizio




 

INT 10h / AH = 13h - write string.

 
input:
AL = write mode:
    bit 0: update cursor after writing;
    bit 1: string contains attributes.
BH = page number.
BL = attribute if string contains only characters (bit 1 of AL is zero).
CX = number of characters in string (attributes are not counted).
DL,DH = column, row at which to start writing.
ES:BP points to string to be printed.
example:

	mov al, 1
	mov bh, 0
	mov bl, 0011_1011b
	mov cx, msg1end - offset msg1 ; calculate message size. 
	mov dl, 10
	mov dh, 7
	push cs
	pop es
	mov bp, offset msg1
	mov ah, 13h
	int 10h
	jmp msg1end
	msg1 db " hello, world! "
	msg1end: