Assembly Base con MASM

Capitolo 15: Libreria di procedure per l'I/O


Tutti i linguaggi di alto livello mettono a disposizione dei programmatori sofisticati ambienti di sviluppo che comprendono, in particolare, una numerosa collezione di cosiddetti sottoprogrammi, "pronti per l'uso"; con il termine sottoprogramma si indica un piccolo blocco di istruzioni che, nel loro insieme, formano una sorta di "mini-programma".
Ciascun sottoprogramma è destinato a svolgere un compito ben preciso; possiamo avere, ad esempio, sottoprogrammi che si occupano della gestione della memoria, della gestione dei file su disco, della lettura dell'input dalla tastiera, della scrittura dell'output sullo schermo e così via.
I sottoprogrammi possono essere collegati, direttamente o indirettamente, ad un generico programma che possiamo definire programma principale (o main program); ogni volta che il programma principale deve svolgere un determinato compito, non deve fare altro che chiamare l'opportuno sottoprogramma.
Si ha il collegamento diretto, quando tutti i sottoprogrammi di cui abbiamo bisogno vengono inseriti nello stesso modulo contenente anche il programma principale; esiste però un sistema molto più pratico, rappresentato dal collegamento indiretto. Prima di tutto, in base al compito svolto, i vari sottoprogrammi vengono suddivisi in gruppi e ciascun gruppo viene inserito in un file distinto; ciascuno dei file così ottenuti prende il nome di libreria di sottoprogrammi. Ogni volta che scriviamo un programma che ha bisogno di una serie di sottoprogrammi, non dobbiamo fare altro che linkare le opportune librerie al modulo contenente il programma principale.

Nei linguaggi di alto livello, per indicare i diversi tipi di sottoprogrammi, vengono utilizzati termini come, procedura, funzione, subroutine; in Assembly, i sottoprogrammi vengono chiamati procedure. Nel seguito del capitolo e in tutta la sezione Assembly Base, verrà sempre utilizzato il termine procedura per indicare un generico sottoprogramma; le procedure Assembly verranno trattate in dettaglio in un apposito capitolo.

Come è stato spiegato in precedenza, gli utenti dei linguaggi di alto livello hanno a disposizione numerose librerie di procedure pronte per l'uso, che possono essere utilizzate "a scatola chiusa", senza preoccuparsi di come esse svolgano il proprio lavoro; del resto, uno degli scopi fondamentali dei linguaggi di alto livello, è proprio quello di permettere a chiunque di scrivere programmi senza la necessità di conoscere il funzionamento interno del computer.
Per raggiungere questo obiettivo, i linguaggi di alto livello vengono strutturati in modo da rendere la programmazione indipendente dall'hardware del computer; il compilatore o l'interprete, si occupano di tutti i dettagli relativi alla traduzione del codice sorgente in codice macchina comprensibile dalla piattaforma hardware che si sta utilizzando.
I linguaggi di alto livello rappresentano quindi la soluzione ideale per quei programmatori che non vogliono perdere tempo ad armeggiare, direttamente, con CPU, FPU, Bus PCI, Bus AGP, interfacce IDE, EIDE, SCSI, etc; le comodità offerte dai linguaggi di alto livello, si pagano però a caro prezzo in termini di dimensioni e prestazioni del codice prodotto. È chiaro, infatti, che più la struttura di un linguaggio si allontana dall'hardware del computer, più diventa complesso e contorto il lavoro di traduzione del codice sorgente in codice macchina; tutto ciò porta ad ottenere programmi eseguibili di dimensioni spropositate e con prestazioni spesso insoddisfacenti.

I linguaggi di alto livello mostrano tutti i loro limiti nel momento in cui si presenta la necessità di scrivere programmi potenti ed efficienti, capaci di sfruttare al massimo l'hardware che il computer ci mette a disposizione; i programmatori alle prese con i vari Java, VisualBasic, Visual C++, etc, si trovano davanti ad una vera e propria barriera protettiva, che impedisce loro di accedere in modo efficiente all'hardware sottostante. Se si vuole scavalcare questa barriera, esiste una sola strada da seguire e consiste nell'acquisire una conoscenza approfondita dell'architettura del computer; una volta che questa conoscenza è stata acquisita, si è in grado di dialogare direttamente con il computer, utilizzando il suo stesso linguaggio. Come già sappiamo, il computer parla il linguaggio binario, rappresentato da sequenze di 0 e 1 che, nel loro insieme, formano il codice macchina; per evitare ai programmatori di dover lavorare direttamente con il codice macchina, è stato creato il linguaggio Assembly.

A differenza di quanto accade con i linguaggi di alto livello, l'Assembly non mette a disposizione alcuna procedura predefinita da utilizzare a scatola chiusa per gestire la memoria, i file su disco, l'input dalla tastiera, l'output sullo schermo, etc; tutto il lavoro necessario per realizzare queste procedure, viene lasciato al programmatore. Tutto ciò che l'Assembly mette a disposizione è rappresentato dal set di istruzioni della CPU; mettendo assieme queste semplici istruzioni, è possibile fare qualunque cosa purché, ovviamente, si abbiano le idee chiare.
I programmatori Assembly sparsi in tutto il mondo hanno provveduto a realizzare e a mettere in circolazione un numero enorme di librerie di procedure Assembly, destinate a coprire praticamente tutti gli aspetti delle conoscenze umane; è chiaro però che l'obiettivo fondamentale di chi programma in Assembly, non è quello di utilizzare queste procedure a scatola chiusa, ma è quello di capire come scrivere di persona le procedure di cui si ha bisogno.
Nel capitolo precedente è stato messo in evidenza il problema che si trova davanti chi vuole imparare a programmare in Assembly; se, ad esempio, si vuole stampare una stringa sullo schermo, bisogna scrivere una apposita procedura Assembly, ma per scrivere questa procedura, bisogna già conoscere l'Assembly. Per uscire da questa situazione, l'unica cosa da fare consiste nel servirsi, inizialmente, di apposite librerie di procedure già pronte per l'uso; man mano che si impara a programmare in Assembly, si acquisiscono le conoscenze necessarie per scrivere in proprio queste procedure.

15.1 Le librerie EXELIB e COMLIB

In questo capitolo, viene illustrata una libreria che ci permette di gestire in modo molto semplice, le operazioni per l'input dalla tastiera e per l'output sullo schermo; utilizzando le procedure presenti in questa libreria, possiamo verificare in pratica tutti i concetti esposti nei precedenti capitoli e possiamo poi procedere in modo più rapido, nell'apprendimento del linguaggio Assembly. Decomprimendo nella cartella ASMBASE il file EXELIB.ZIP, si ottengono i seguenti tre file: Analogamente, decomprimendo nella cartella ASMBASE il file COMLIB.ZIP, si ottengono i seguenti tre file: Sia EXELIB.OBJ che COMLIB.OBJ, contengono le stesse procedure; l'unica differenza consiste nel fatto che, come è stato già spiegato, la libreria EXELIB.OBJ è destinata ad essere linkata ai programmi in formato DOS EXE, mentre la libreria COMLIB.OBJ è destinata ad essere linkata ai programmi in formato DOS COM.

15.1.1 Parametri in ingresso e valore in uscita di una procedura

Una procedura, per poter svolgere il proprio lavoro, può richiedere una serie di informazioni, chiamate simbolicamente entry parameters o parametri in ingresso; le informazioni effettive che il programmatore passa alla procedura, come parametri in ingresso, prendono il nome di entry arguments o argomenti in ingresso. Su alcuni libri, i parametri vengono anche chiamati parametri formali, mentre gli argomenti vengono anche chiamati parametri attuali; nella sezione Assembly Base, verrà sempre utilizzato il termine parametri per indicare la lista simbolica di informazioni richieste da una procedura e il termine argomenti per indicare la lista effettiva di informazioni che il programmatore passa alla procedura stessa.
Alcune procedure, dopo aver svolto il proprio lavoro, restituiscono a chi le ha chiamate, una informazione che viene definita exit value o valore in uscita; spesso, il valore in uscita contiene il risultato delle elaborazioni a cui sono stati sottoposti gli argomenti ricevuti in ingresso dalla procedura.
Tutte le procedure contenute in EXELIB.OBJ e COMLIB.OBJ, utilizzano i registri della CPU, sia per ricevere gli eventuali argomenti in ingresso, sia per restituire un eventuale valore in uscita; come vedremo nei capitoli successivi, questo metodo, pur non essendo il più raffinato, è sicuramente il più veloce.

15.1.2 Descrizione delle procedure presenti nella libreria

Esaminiamo ora in dettaglio, l'elenco delle procedure disponibili nella libreria e il compito svolto da ciascuna di esse; nella descrizione che segue, per ogni procedura viene utilizzato il termine Entry per indicare la lista simbolica degli eventuali parametri in ingresso e il termine Exit per indicare l'eventuale valore in uscita.

15.1.3 Ulteriori dettagli sulle librerie EXELIB e COMLIB

Le procedure presenti in EXELIB e COMLIB, sono molto facili da utilizzare; in ogni caso, vediamo alcuni chiarimenti relativi al loro funzionamento.

Le due procedure set80x25 e set80x50, hanno lo scopo di selezionare la modalità testo desiderata per lo schermo; set80x25 attiva la modalità video testo a 25 righe e 80 colonne, mentre set80x50 attiva la modalità video testo a 50 righe e 80 colonne.
Non appena si accende il computer, lo schermo viene inizializzato dal BIOS in modalità testo standard; questa modalità consiste nel gestire lo schermo attraverso una matrice formata da 25 righe (numerate da 0 a 24) e 80 colonne (numerate da 0 a 79), per un totale di 80x25=2000 celle. Verso la fine degli anni 80, la IBM ha introdotto la modalità video grafica VGA standard, che consiste nel gestire lo schermo attraverso una matrice di 640x480 punti (pixel), ciascuno dei quali può assumere uno tra 16 colori differenti; trattandosi di uno standard, questa modalità video viene sicuramente supportata da tutte le schede video VGA e superiori. La modalità grafica VGA, rende disponibile una nuova modalità testo formata da 50 righe e 80 colonne; in questo modo, è possibile visualizzare il doppio delle informazioni rispetto alla modalità testo a 25 righe. Le due procedure set80x25 e set80x50, permettono appunto di selezionare la modalità testo preferita; pur non essendo obbligatorio, si consiglia di utilizzare una di queste due procedure all'inizio di ogni programma che fa uso della libreria EXELIB o COMLIB. La procedura writeString, permette di stampare una stringa sullo schermo, a partire dalla posizione indicata dai due registri DH (riga) e DL (colonna); seguendo la convenzione del linguaggio C (già illustrata in un precedente capitolo), la stringa da stampare deve terminare con un byte di valore zero (NUL). Per fare un esempio, la riga:
stringa_c db 'Linguaggio Assembly', 0
definisce una stringa formata da 20 byte, con il ventesimo byte che vale zero (NUL); è importante che non si faccia confusione tra il simbolo NUL (che ha codice ASCII 0) e il simbolo '0' (che ha codice ASCII 48).
Nel caso della libreria EXELIB, la procedura writeString riceve nei registri ES:DI, l'indirizzo logico Seg:Offset della stringa da stampare; utilizzando la stringa dell'esempio precedente, prima di chiamare writeString possiamo scrivere: In questo modo, la coppia ES:DI punterà all'indirizzo logico Seg:Offset di stringa_c; come vedremo più avanti, nel caso della libreria COMLIB viene richiesto solo il caricamento in DI della componente Offset dell'indirizzo della stringa da stampare.

Tutte le procedure del tipo writeUdec, writeSdec, writeHex e writeBin, permettono di stampare sullo schermo numeri interi nelle principali ampiezze (8, 16 e 32 bit) e nelle principali basi (10, 16 e 2); anche in questo caso il programmatore, utilizzando i registri DH e DL, può stabilire in quale zona dello schermo verranno stampati i numeri.

Tutte le procedure del tipo readUdec, readSdec, readHex e readBin, permettono di inserire numeri interi dalla tastiera, utilizzando le tre ampiezze e le tre basi citate prima; l'input avviene nella sestultima riga dello schermo e si consiglia pertanto di non sovrascrivere questa riga con l'output prodotto dalle procedure writeString, writeHex, etc.

Le procedure del tipo readUdec e readSdec, sono volutamente prive del controllo sui limiti del numero che viene inserito; in questo modo, è possibile verificare quello che succede quando si inseriscono numeri fuori limite. La procedura readUdec8, ad esempio, dovrebbe ricevere in input un numero intero senza segno a 8 bit, compreso quindi tra 0 e 255, ma in realtà, accetta un numero intero senza segno a tre cifre, compreso quindi tra 0 e 999. Analogamente, la procedura readSdec16, dovrebbe ricevere in input un numero intero con segno a 16 bit, compreso quindi tra -32768 e +32767, ma in realtà, accetta un numero intero senza segno a cinque cifre, compreso quindi tra -99999 e +99999.
Per vedere allora quello che accade quando non rispettiamo i limiti, consideriamo il caso della procedura readUdec8; il numero inserito dal programmatore, viene restituito nel registro AL che conterrà quindi un valore binario compreso tra 00000000b e 11111111b. Se proviamo ad inserire il numero 256 e a premere [Invio], la procedura restituisce in AL il valore 0; infatti, 256 (100000000b) è un numero a 9 bit che per essere inserito negli 8 bit di AL, viene troncato a 00000000b con perdita del bit più significativo.
Consideriamo ora il caso della procedura readSdec16; se proviamo ad inserire il numero +32768 e a premere [Invio], verrà restituito in AX il numero binario 1000000000000000b, che rappresenta il valore 32768 per i numeri interi senza segno a 16 bit, e il valore -32768 per i numeri interi con segno a 16 bit in complemento a due. Ricordiamo, infatti, che in complemento a due:
-32768 = 65536 - 32768 = 32768
Passando, infatti, il valore 1000000000000000b alla procedura writeUdec16, verrà stampato il numero 32768; passando, invece, il valore 1000000000000000b alla procedura writeSdec16, verrà stampato proprio il numero -32768.

La procedura showCPU è una delle più importanti della libreria, in quanto permette di visualizzare, in tempo reale, lo stato della CPU, rappresentato dal contenuto di ogni suo registro; questa procedura, stampa le sue informazioni nelle ultime cinque righe dello schermo e quindi, anche in questo caso, bisogna evitare di sovrascrivere quest'area di output.

15.2 Collegamento di una libreria ad un programma Assembly

Vediamo ora come si deve procedere per linkare ai nostri programmi una libreria di procedure come EXELIB o COMLIB; tutti i dettagli su questo argomento, verranno illustrati in modo approfondito in un apposito capitolo.

Supponiamo di scrivere un programma formato dai due file, MAIN.ASM (che contiene il programma principale) e LIB1.ASM (che contiene una libreria di procedure da utilizzare in MAIN.ASM); l'aspetto più importante da chiarire, riguarda il metodo che viene utilizzato per far comunicare tra loro MAIN.ASM e LIB1.ASM.
Come è stato spiegato in un precedente capitolo, in Assembly i nomi delle etichette, delle variabili, delle procedure, etc, hanno lo scopo di individuare la posizione (offset) di queste informazioni, calcolata rispetto al segmento di programma nel quale sono state definite le informazioni stesse; se, ad esempio, la procedura writeString inizia a partire dall'offset 0B00h all'interno di un segmento di programma chiamato CODESEGM, allora, dal punto di vista della CPU, il nome writeString viene associato all'indirizzo logico CODESEGM:0B00h.
Se in MAIN.ASM è presente un'istruzione che chiama la procedura writeString definita in LIB1.ASM, allora è necessario indicare all'assembler qual è l'indirizzo logico Seg:Offset della procedura stessa; infatti, chiamare una procedura significa saltare all'indirizzo Seg:Offset da cui inizia in memoria la procedura stessa.

15.2.1 Le direttive PUBLIC e EXTRN

In sostanza, in fase di assemblaggio di un programma, ogni volta che l'assembler incontra una chiamata ad una procedura, deve essere in grado di convertire il nome di quella procedura, in una coppia Seg:Offset che in fase di esecuzione verrà inserita in CS:IP; per fornire all'assembler tutte le informazioni di cui ha bisogno, vengono utilizzate le due direttive PUBLIC e EXTRN.

Applicando ad un nome la direttiva PUBLIC, si rende pubblico quel nome, cioè, lo si rende visibile anche all'esterno del file nel quale è stato definito; se, ad esempio, in LIB1.ASM definiamo la procedura writeString, preceduta dalla dichiarazione:
PUBLIC writeString
allora la procedura writeString potrà essere chiamata, sia dall'interno del modulo LIB1.ASM, sia da un modulo esterno (come MAIN.ASM).
Tutti i nomi di procedure, etichette, variabili, etc, definiti in LIB1.ASM senza la direttiva PUBLIC, sono invisibili dall'esterno; in questo caso, si dice che questi nomi sono privati, nel senso che sono visibili (e quindi utilizzabili) solo all'interno di LIB1.ASM.

Se nel modulo MAIN.ASM vogliamo chiamare la procedura writeString, definita in LIB1.ASM, allora dobbiamo dire all'assembler che ci stiamo riferendo al nome di una procedura definita in un modulo esterno; per dare questa informazione all'assembler, dobbiamo servirci della direttiva EXTRN. Nel modulo MAIN.ASM dobbiamo quindi scrivere la dichiarazione:
EXTRN writeString
Con le nuove versioni di MASM, per questa direttiva è permessa anche la sintassi EXTERN.

In relazione alla chiamata di una procedura che si trova in memoria all'indirizzo logico Seg:Offset, si possono presentare i seguenti due casi: Risulta evidente che gli indirizzi NEAR, essendo costituiti unicamente da un offset a 16 bit, vengono gestiti dalla CPU più velocemente rispetto agli indirizzi FAR (che comprendono, sia i 16 bit per la componente Offset, sia i 16 bit per la componente Seg); tutti questi concetti, sono stati illustrati in modo molto semplificato in quanto verranno spiegati in dettaglio in altri capitoli.

Per comodità, tutte le necessarie dichiarazioni EXTRN per le procedure esterne da utilizzare in un programma, possono essere inserite in appositi file, chiamati include file; convenzionalmente, per ogni include file relativo ad una libreria, viene utilizzato lo stesso nome della libreria e l'estensione INC. Nel caso, ad esempio, della libreria LIB1.ASM, possiamo creare un apposito include file chiamato LIB1.INC; se il programma MAIN.ASM vuole servirsi delle procedure presenti in LIB1.ASM, deve avere al suo interno una direttiva del tipo:
INCLUDE LIB1.INC
Grazie a questa semplice direttiva, tutte le dichiarazioni EXTRN presenti in LIB1.INC, vengono incorporate nel modulo MAIN.ASM; più avanti vedremo ulteriori dettagli su questo argomento.

15.2.2 Assembling & linking di un programma diviso in due o più moduli

Una volta chiarito il metodo che permette a MAIN.ASM e LIB1.ASM di comunicare tra loro, possiamo passare alle fasi di assembling e linking, che ci permettono di ottenere il programma finale in versione eseguibile; prima di tutto, bisogna procedere all'assemblaggio separato dei due file (è sottinteso che il programmatore si trovi già nella directory C:\MASM\ASMBASE).

Con MASM, dobbiamo impartire i comandi:
..\bin\ml /c main.asm
e:
..\bin\ml /c lib1.asm
Se non si sono verificati errori, abbiamo ottenuto i due file in formato oggetto MAIN.OBJ e LIB1.OBJ e possiamo quindi passare alla fase di linking; con il MASM, dobbiamo impartire il comando:
..\bin\link main.obj + lib1.obj
Il comportamento predefinito del linker, è quello di assegnare all'eseguibile il nome del file più a sinistra tra quelli che il programmatore ha specificato dalla linea di comando; nel nostro caso, si ottiene un eseguibile chiamato MAIN.EXE.
Se avessimo scritto, invece:
..\bin\link lib1.obj + main.obj
il linker avrebbe assegnato all'eseguibile il nome LIB1.EXE; più avanti vedremo ulteriori dettagli sulle fasi di assembling e di linking di un programma suddiviso in due o più moduli.

15.3 L'istruzione CALL

In Assembly, il metodo predefinito per la chiamata di una procedura, consiste nell'uso dell'istruzione CALL, che verrà illustrata in dettaglio in un apposito capitolo; per il momento, ci limitiamo ad una semplice descrizione di questa istruzione, in modo da poterla utilizzare negli esempi presentati più avanti.

Nella sua forma più semplice, la chiamata di una procedura viene definita come direct call within segment (chiamata diretta all'interno del segmento); come è stato spiegato in precedenza, questa situazione si verifica quando la procedura si trova nello stesso segmento dal quale avviene la chiamata. In un caso del genere, la CPU ha bisogno di conoscere solo la componente Offset dell'indirizzo della procedura, in modo da porre IP=Offset; il registro CS non deve essere modificato, in quanto contiene già una componente Seg che è la stessa dell'indirizzo della procedura.
Il codice macchina di questa forma dell'istruzione CALL, è composto dall'Opcode 11101000b (E8h), seguito da un valore a 16 bit; la CPU somma questo valore all'offset dell'istruzione successiva alla CALL e ottiene la componente Offset dell'indirizzo della procedura da chiamare.
Per chiarire questo concetto, vediamo un esempio pratico; supponiamo di avere le seguenti due istruzioni: e supponiamo che la prima istruzione si trovi all'offset 00BBh di un segmento di programma chiamato CODESEGM. Il codice macchina dell'istruzione CALL è formato dall'Opcode E8h (1 byte), seguito da un valore a 16 bit (2 byte), per un totale di 3 byte; di conseguenza, l'istruzione successiva alla CALL si trova all'offset:
00BBh + 0003h = 00BEh
Supponiamo ora che la procedura writeString parta dall'offset 0AC2h nello stesso blocco CODESEGM; di conseguenza, l'assembler tradurrà le due istruzioni precedenti, nel codice macchina: Quando la CPU arriva all'offset 00BBh del blocco CODESEGM, incontra l'istruzione contenente la NEAR call e calcola:
0A04h + 00BEh = 0AC2h
che è proprio l'offset da cui inizia writeString; la CPU quindi carica 0AC2h in IP e salta a CS:IP, cioè a CODESEGM:0AC2h (NEAR call).

Cosa succede quando writeString termina il suo lavoro?
È chiaro che se non si prendono delle precauzioni, la CPU non è in grado di sapere da quale punto riprenderà l'esecuzione del programma; osservando il codice macchina illustrato nell'esempio precedente, risulta evidente che, una volta che writeString ha terminato il suo lavoro, l'esecuzione del programma dovrà riprendere dall'istruzione successiva alla CALL e quindi dall'offset 00BEh. In base a queste considerazioni, la CPU, quando incontra l'istruzione per la NEAR call di writeString, esegue le seguenti operazioni: Come si vedrà in un apposito capitolo, una procedura termina con l'istruzione RET (return from procedure); quando la CPU incontra una istruzione RET (che nel nostro caso è un NEAR return da writeString), esegue in sequenza le seguenti operazioni: Se il programmatore non ha commesso errori nella gestione dello stack (all'interno di writeString), il valore a 16 bit che la CPU estrae dall'indirizzo SS:SP rappresenta proprio l'offset 00BEh salvato in precedenza dalla CPU stessa; di conseguenza, l'indirizzo della prossima istruzione da eseguire, contenuto in CS:IP, sarà proprio quello dell'istruzione successiva alla CALL.

Analizziamo ora un'altra forma della chiamata di una procedura, che viene definita come direct call intersegment (chiamata diretta intersegmento); come è stato spiegato in precedenza, questa situazione si verifica quando la procedura si trova in un segmento diverso da quello nel quale avviene la chiamata. In un caso del genere, la CPU ha bisogno di conoscere l'indirizzo completo Seg:Offset della procedura, in modo da porre CS=Seg e IP=Offset; a causa del salto da un segmento ad un altro, è necessario quindi modificare anche CS.
Il codice macchina di questa forma dell'istruzione CALL è costituito dall'Opcode 10011010b (9Ah), seguito da un valore a 32 bit; questo valore a 32 bit rappresenta l'indirizzo completo Seg:Offset della procedura da chiamare. Per chiarire questo concetto, vediamo un esempio pratico; supponiamo di avere le seguenti due istruzioni: e supponiamo che la prima istruzione si trovi all'offset 00BBh di un segmento di programma chiamato CODESEGM. Il codice macchina dell'istruzione CALL è formato dall'Opcode 9Ah (1 byte), seguito da un valore a 32 bit (4 byte), per un totale di 5 byte; di conseguenza, l'istruzione successiva alla CALL si trova all'offset:
00BBh + 0005h = 00C0h
Supponiamo ora che la procedura writeString parta dall'offset 0AC2h di un blocco codice chiamato CODESEGM2; di conseguenza, l'assembler tradurrà le due istruzioni precedenti nel codice macchina: Notiamo che se l'assembler non è in grado di determinare in anticipo l'indirizzo di una procedura (definita, ad esempio, in un modulo esterno), delega tale compito al linker.
Quando la CPU arriva all'offset 00BBh del blocco CODESEGM, incontra l'istruzione contenente la FAR call, ed esegue le seguenti operazioni: Al termine di writeString, è presente la solita istruzione RET, che in questo caso è un FAR return da writeString; quando la CPU incontra l'istruzione RET, esegue in sequenza le seguenti operazioni: Se il programmatore non ha commesso errori nella gestione dello stack (all'interno di writeString), il valore a 32 bit che la CPU estrae dall'indirizzo SS:SP, rappresenta proprio l'indirizzo CODESEGM:00C0h salvato in precedenza dalla CPU stessa; di conseguenza, l'indirizzo della prossima istruzione da eseguire, contenuto in CS:IP, sarà proprio quello dell'istruzione successiva alla CALL.

È importantissimo ribadire ancora una volta che in Assembly, il programmatore ha il controllo diretto su molti delicati aspetti legati al programma in esecuzione; in particolare, il programmatore ha il dovere di gestire correttamente lo stack, altrimenti la CPU non è in grado di eseguire con successo le fasi descritte negli esempi precedenti. Se, ad esempio, all'interno di writeString il programmatore salva con PUSH il contenuto di un registro e poi dimentica di ripristinare lo stack con POP, ciò che accade è che quando la CPU incontra l'istruzione RET, invece di estrarre dallo stack l'indirizzo della prossima istruzione da eseguire, estrae quella che, in gergo, viene definita "spazzatura"; la CPU carica questa spazzatura in CS:IP e compie un "salto nel buio"!
Generalmente, questa situazione manda in crash il programma in esecuzione; per il momento comunque, non bisogna preoccuparsi di questi aspetti, perché verranno spiegati in dettaglio in altri capitoli.

15.4 Programma di prova EXETEST2.ASM per la libreria EXELIB

Dopo aver esposto tutti i necessari concetti teorici, possiamo passare ad un esempio pratico, rappresentato da un programma che ci permetterà di testare le procedure contenute nella libreria EXELIB.OBJ; a tale proposito, ci serviremo del programma EXETEST2.ASM, illustrato dalla Figura 15.1. Analizzando il listato di EXETEST2.ASM, notiamo che la prima riga contiene la direttiva:
INCLUDE EXELIB.INC
che permette di includere, comodamente, tutte le necessarie direttive EXTRN, relative alle procedure definite nel modulo EXELIB.OBJ.

Tutte le procedure dichiarate nel file EXELIB.INC sono di tipo FAR; infatti, queste procedure vengono definite nel modulo EXELIB.OBJ, all'interno di un apposito segmento di codice, che è diverso dal segmento CODESEGM presente nel modulo EXETEST2.ASM.

Siccome non conosciamo il nome del segmento di codice, interno a EXELIB.OBJ, nel quale sono state definite le varie procedure esterne, è importante che le relative direttive EXTRN vengano poste in un'area del modulo EXETEST2.ASM che deve trovarsi al di fuori di qualsiasi segmento di programma; come si nota in Figura 15.1, il punto più adatto per l'inserimento di queste direttive (poste nel file EXELIB.INC), è rappresentato dalla parte iniziale del modulo che contiene il programma principale. Nel blocco DATASEGM di Figura 15.1, vengono definite due stringhe C, chiamate strTitle e strExit; l'unico aspetto degno di nota, è rappresentato dal metodo che viene utilizzato per definire stringhe molto lunghe. Anziché definire una stringa su una sola gigantesca linea, si può fare ricorso al metodo che è stato seguito in Figura 15.1 per strTitle; come sappiamo, l'assembler provvede a disporre in memoria, in modo consecutivo e contiguo, tutti i byte che formano strTitle.

Nel blocco CODESEGM di Figura 15.1, vengono chiamate quasi tutte le procedure definite in EXELIB.OBJ; in questo modo, è possibile analizzare il loro funzionamento.
Come possiamo notare, vengono inizialmente chiamate, set80x25 (che seleziona la modalità video testo), hideCursor (che nasconde il cursore lampeggiante) e clearScreen (che cancella lo schermo); subito dopo, viene chiamata la procedura clearRegisters, che provvede ad azzerare i registri EAX, EBX, ECX, EDX, ESI e EDI.

Tutte le procedure definite in EXELIB.OBJ, ad eccezione di showCPU, preservano i registri (a 8, 16 e 32 bit) che utilizzano; la procedura clearRegisters non preserva, ovviamente, il contenuto dei 6 registri da azzerare. Questo aspetto diventa molto importante nel momento in cui non vogliamo che la chiamata di una procedura alteri il contenuto di determinati registri che stiamo utilizzando nel programma principale; eventualmente, è opportuno che vengano prese le necessarie precauzioni. Supponiamo, ad esempio, di voler chiamare una procedura Proc1, che altera il registro AX senza preservarne il vecchio contenuto; se, nel programma principale, non vogliamo perdere il vecchio contenuto di AX, dobbiamo scrivere istruzioni del tipo: Subito dopo clearRegisters, viene chiamata la procedura showCPU che visualizza, in tempo reale, il contenuto di tutti i registri della CPU; grazie a showCPU, possiamo analizzare, in particolare, il contenuto iniziale di CS, DS, ES, SS, SP e IP, in modo da conoscere tutti i dettagli relativi all'area della memoria nella quale è stato caricato il nostro programma per la fase di esecuzione.

Proseguendo nell'analisi del listato di Figura 15.1, fissiamo la nostra attenzione sulla chiamata della procedura writeString per la visualizzazione della stringa strTitle; nel caso della libreria EXELIB.OBJ, la procedura writeString si trova in un modulo esterno e non ha la più pallida idea di quali siano le caratteristiche dei segmenti di codice, dati e stack, definiti nel modulo EXETEST2.ASM. Proprio per questo motivo, writeString ha bisogno di conoscere l'indirizzo completo Seg:Offset della stringa da visualizzare; questa informazione, come viene mostrato dalla Figura 15.2, deve essere passata attraverso la coppia ES:DI. Osserviamo il metodo che viene seguito per caricare, in un colpo solo, il valore 00h in DH e il valore 0Eh in DL; in base alle proprietà dei numeri esadecimali, espressi nel sistema posizionale, le due cifre più significative (00h) di 000Eh, si posizionano nei 16 bit più significativi (DH) di DX, mentre le due cifre meno significative (0Eh) di 000Eh, si posizionano nei 16 bit meno significativi (DL) di DX.
La Figura 15.2 ci permette anche di illustrare in pratica, la differenza concettuale che esiste tra i parametri in ingresso e gli argomenti in ingresso; writeString richiede tre parametri in ingresso e cioè, un BYTE che rappresenta l'indice di riga, un BYTE che rappresenta l'indice di colonna e una DWORD che rappresenta un indirizzo FAR. Gli argomenti che il programmatore passa a writeString in ingresso sono, rispettivamente, 00h, 0Eh e la coppia seg(strTitle), offset(strTitle).

Come è stato già detto, tutte le procedure del tipo readUdec, readSdec, readHex e readBin, utilizzano la sestultima riga per mostrare i numeri che l'utente inserisce dalla tastiera; inoltre, la riga precedente a quella di input, mostra una stringa che fornisce informazioni sul tipo e sui limiti inferiore e superiore dei numeri da inserire.

15.4.1 Assembling & Linking del programma EXETEST2

Dopo aver chiarito questi aspetti, possiamo passare alle fasi di assembling e di linking del nostro programma.
Come è stato detto in precedenza, la fase di assembling coinvolge tutti i moduli che contengono il codice sorgente di un programma distribuito su due o più file; nel nostro caso, il modulo EXELIB.OBJ è già in formato object code, per cui, dobbiamo procedere con l'assemblaggio del solo modulo EXETEST2.ASM.

Con il MASM, il comando da impartire dalla directory ASMBASE è:
..\bin\ml /c /Fl exetest2.asm
Se vogliamo un assemblaggio case-sensitive, dobbiamo servirci dell'opzione /Cp e impartire quindi il comando:
..\bin\ml /c /Cp /Fl exetest2.asm
Premendo ora il tasto [Invio], vengono generati i due file EXETEST2.OBJ e EXETEST2.LST; apriamo subito con un editor il file EXETEST2.LST per poter esaminare alcuni aspetti interessanti.
Nel listing file notiamo subito che tutte le direttive EXTRN presenti in EXELIB.INC, sono state incorporate in EXETEST2.LST; inoltre, le stesse direttive EXTRN sono state sistemate al di fuori di qualsiasi segmento di programma presente in EXETEST2.LST. Ciò permette all'assembler di generare l'opportuno codice macchina relativo alla chiamata delle varie procedure; infatti, osserviamo che, ad esempio, in relazione alla chiamata di set80x25, viene generato il codice macchina:
9Ah 00000000    ;FAR call
9Ah è proprio l'Opcode della "chiamata diretta intersegmento; come si può notare, l'assembler sta delegando al linker il compito di calcolare l'indirizzo logico Seg:Offset della procedura esterna set80x25 di tipo Far.

Nella symbol table, la procedura set80x25 viene classificata dall'assembler come:
set80x25 Far ----:---- Extern
Passiamo ora alla fase di linking del nostro programma; con il MASM, il comando da impartire è:
..\bin\link /map exetest2.obj + exelib.obj
Premendo il tasto [Invio], parte la fase di linking che, in assenza di errori, porta alla generazione del file in formato eseguibile EXETEST2.EXE e del map file EXETEST2.MAP; analizziamo il lavoro svolto dal linker per il collegamento dei due moduli EXETEST2.OBJ e EXELIB.OBJ.

Prima di tutto, bisogna premettere che il modulo EXELIB.OBJ contiene un segmento dati del tipo:
LIBIODATA SEGMENT PARA PRIVATE USE16 'DATA'
e un segmento di codice del tipo:
LIBIOCODE SEGMENT PARA PUBLIC USE16 'CODE'
Le considerazioni appena esposte, vengono confermate dal map file di Figura 15.3. Il linker ha calcolato per l'entry point, l'indirizzo logico 0042h:0000h; osserviamo, infatti, che il blocco CODESEGM parte dall'indirizzo fisico 00420h (allineato al paragrafo), a cui corrisponde l'indirizzo logico normalizzato 0042h:0000h. Nel listato di Figura 15.1, notiamo che l'etichetta start si trova all'offset 0000h di CODESEGM; quest'offset non viene rilocato dal linker, per cui l'indirizzo logico dell'entry point è proprio 0042h:0000h.
In Figura 15.3 possiamo anche notare che, grazie all'attributo Class, il linker ha potuto stabilire il criterio di ordinamento per i vari blocchi di memoria; infatti, il nostro programma contiene, prima i blocchi di classe 'DATA', poi i blocchi di classe 'CODE' e infine il blocco di classe 'STACK'.

Cosa succede se effettuiamo il linking con il comando:
..\bin\link /map exelib.obj + exetest2.obj
In tal caso, il linker inizia ad esaminare per primo il modulo EXELIB.OBJ; il primo segmento che viene incontrato è LIBIODATA. Alla fine, si ottiene un eseguibile che viene chiamato EXELIB.EXE e un map file che viene chiamato EXELIB.MAP; la struttura interna di EXELIB.EXE viene illustrata proprio dal map file di Figura 15.4. Come si può notare, questa volta il blocco CODESEGM viene fatto partire dall'indirizzo fisico 01000h, a cui corrisponde l'indirizzo logico normalizzato 0100h:0000h; di conseguenza, l'entry point calcolato dal linker si viene a trovare all'indirizzo logico 0100h:0000h.

15.4.2 Esecuzione del programma EXETEST2.EXE

Passiamo ora alla fase di esecuzione del file EXETEST2.EXE; posizionandoci nella cartella ASMBASE, dal prompt del DOS impartiamo il comando:
exetest2
e premiamo il tasto [Invio].

Appena inizia l'esecuzione di EXETEST2.EXE, vedremo comparire in fondo allo schermo una serie di informazioni come quelle mostrate in Figura 15.5; queste informazioni si riferiscono alla richiesta di input che arriva dalla procedura readUdec8 e allo stato della CPU mostrato dalla procedura showCPU. La procedura showCPU, chiamata proprio all'inizio del nostro programma, mostra il contenuto iniziale dei registri della CPU; analizziamo, in particolare, l'importante contenuto iniziale dei registri CS, DS, ES, SS, SP e IP (si tenga presente che le informazioni contenute nei registri di segmento, possono variare da computer a computer).
Il registro ES non ha subito ancora alcuna modifica, per cui contiene di sicuro la componente Seg dell'indirizzo logico iniziale del program segment assegnato a EXETEST2.EXE; possiamo dire allora che il nostro programma, parte in memoria dall'indirizzo logico 1859h:0000h, a cui corrisponde l'indirizzo fisico 18590h.

I primi 256 byte (0100h byte) del program segment sono occupati dal PSP; in base al map file di Figura 15.3, il primo blocco del nostro programma è DATASEGM, che quindi parte dall'indirizzo fisico:
18590h + 0100h = 18690h
All'indirizzo fisico 18690h corrisponde l'indirizzo logico normalizzato 1869h:0000h; la componente 1869h di questo indirizzo, viene assegnata a DATASEGM. Prima della chiamata di showCPU, il valore DATASEGM è stato assegnato a DS (vedere il listato di Figura 15.1); infatti, in Figura 15.5 vediamo che DS=1869h.

In base al map file di Figura 15.3, il blocco CODESEGM si trova a 00420h byte di distanza dall'inizio di DATASEGM; possiamo dire allora che CODESEGM parte in memoria dall'indirizzo fisico:
18690h + 00420h = 18AB0h
All'indirizzo fisico 18AB0h corrisponde l'indirizzo logico normalizzato 18ABh:0000h, che coincide anche con l'indirizzo logico dell'entry point; la componente 18ABh di questo indirizzo viene assegnata a CODESEGM. Non appena inizia l'esecuzione del nostro programma, il valore CODESEGM viene utilizzato quindi per inizializzare CS; infatti, in Figura 15.5 vediamo che CS=18ABh.
Il valore (0019h) di IP visibile in Figura 15.5, si riferisce al contenuto dell'instruction pointer al momento della prima chiamata di showCPU; infatti, analizzando il file EXETEST2.LST, si scopre che la prima chiamata di showCPU si trova proprio all'indirizzo logico CODESEGM:0019h.

In base al map file di Figura 15.3, il blocco STACKSEGM si trova a 01140h byte di distanza dall'inizio di DATASEGM; possiamo dire allora che STACKSEGM parte in memoria dall'indirizzo fisico:
18690h + 01140h = 197D0h
All'indirizzo fisico 197D0h, corrisponde l'indirizzo logico normalizzato 197Dh:0000h; la componente 197Dh di questo indirizzo, viene assegnata a STACKSEGM. Non appena inizia l'esecuzione del nostro programma, il valore STACKSEGM viene utilizzato quindi per inizializzare SS; infatti, in Figura 15.5 vediamo che SS=197Dh.
Il registro SP viene inizializzato con il valore 0400h, così come avevamo richiesto nel listato di Figura 15.1; è importante notare che in modalità reale a 32 bit, i 16 bit più significativi di ESP valgono 0000h.
Il fatto che il contenuto di SP resti costante nel corso dell'esecuzione del programma, indica che le varie procedure chiamate gestiscono correttamente lo stack (nel senso che, all'interno delle procedure, le istruzioni PUSH sono bilanciate perfettamente dalle istruzioni POP). Se, ad esempio, ci accorgiamo che SP=0400h prima della chiamata di una procedura e SP=03FCH dopo la chiamata, allora c'è sicuramente un errore di gestione dello stack all'interno della procedura stessa; ricordiamoci che in Assembly, lo strumento migliore per scovare gli errori è il nostro cervello!
Per veder variare il contenuto di SP, possiamo scrivere istruzioni del tipo: In questo caso, showCPU mostra per SP il valore 03FEh; infatti, a causa dell'inserimento di una WORD nello stack, si ha:
SP = 0400h - 0002h = 03FEh
Subito dopo la chiamata di showCPU, l'istruzione POP riequilibra lo stack estraendo una WORD dall'indirizzo logico SS:SP che, in quel momento, vale 197Dh:03FEh.

In relazione ai vari flags, in Figura 15.5 notiamo, in particolare, che PF=1 e ZF=1; ciò è dovuto al fatto che prima di showCPU, viene chiamata la procedura clearRegisters, che azzera alcuni registri. In seguito all'azzeramento di un qualsiasi registro, si ottiene, ovviamente, ZF=1; ricordiamo, infatti, che ZF viene posto a livello logico 1 ogni volta che una operazione produce un risultato pari a zero. I primi 8 bit del registro appena azzerato valgono 00h e quindi contengono un numero pari (zero) di bit a livello logico 1 (lo zero viene trattato come numero pari); di conseguenza, il parity flag PF viene posto a livello logico 1.

Nel corso dell'esecuzione del programma, vengono chiamate le procedure del tipo readUdec, readSdec, readHex e readBin, che richiedono all'utente l'inserimento di numeri interi nelle tre ampiezze 8, 16 e 32 bit e nelle tre basi 10, 16 e 2; ogni numero inserito, viene poi visualizzato sullo schermo dalle procedure del tipo writeUdec, writeSdec, writeHex e writeBin.
Per ogni numero inserito, viene chiamata showCPU che mostra la nuova situazione dei registri della CPU; in particolare, si può notare che in AL/AX/EAX sono presenti i numeri appena inseriti, mentre in DH e DL sono presenti le coordinate di schermo che l'utente ha specificato per l'output. Come è stato detto in precedenza, il contenuto del registro IP si riferisce ai vari punti del programma, nei quali viene chiamata la procedura showCPU.

15.5 Programma di prova COMTEST2.ASM per la libreria COMLIB

Le considerazioni appena svolte, sono relative al collegamento di una libreria di procedure ad un programma Assembly destinato ad essere convertito in un eseguibile in formato EXE; nel seguito del capitolo esamineremo, invece, il caso del collegamento di una libreria di procedure ad un programma Assembly destinato ad essere convertito in un eseguibile in formato COM.
Come già sappiamo, un programma in formato COM è costituito da un unico segmento destinato a contenere, codice, dati e stack; questo significa che non possiamo utilizzare la libreria EXELIB, la quale contiene al suo interno due segmenti di programma distinti (uno per il codice e uno per i dati privati).
Proprio per questo motivo, si rende necessaria una versione modificata della libreria EXELIB, destinata esplicitamente ai programmi in formato COM; si tratta della libreria COMLIB che, come è stato già detto, contiene le stesse procedure di EXELIB. Per testare la libreria COMLIB, ci serviamo del file COMTEST2.ASM, illustrato in Figura 15.6; si tratta della versione in formato COM, del file EXETEST2.ASM di Figura 15.1. Notiamo subito che all'interno di COMTEST2.ASM è presente un unico segmento di programma del tipo:
COMSEGM SEGMENT PARA PUBLIC USE16 'CODE'
Il nome e gli attributi di questo segmento sono (e devono essere) identici a quelli dell'unico segmento di programma definito nel modulo COMTEST.OBJ.

Il listato del programma di Figura 15.6 è assolutamente identico al listato del programma di Figura 15.1; l'unica importante eccezione è rappresentata dalla lista di argomenti che vengono passati alla procedura writeString.
Abbiamo visto che, nel caso della libreria EXELIB.OBJ, la procedura writeString richiede l'indirizzo completo Seg:Offset (indirizzo FAR) della stringa da stampare; ciò accade in quanto writeString non ha altro modo per conoscere il segmento di programma nel quale è stata definita la stringa stessa.
Nel caso, invece, della libreria COMLIB.OBJ, la procedura writeString richiede la sola componente Offset (indirizzo NEAR) dell'indirizzo della stringa da stampare; ciò accade in quanto writeString dà per scontato che tutti i dati del programma siano stati definiti all'interno dell'unico blocco COMSEGM e quindi deduce che l'indirizzo logico completo della stringa da stampare, è sicuramente COMSEGM:Offset.

Una diretta conseguenza della presenza di un unico segmento di programma, è data dal fatto che, all'interno del file COMLIB.INC, troviamo dichiarazioni del tipo:
EXTRN set80x25: NEAR
In presenza del solo segmento COMSEGM, le varie procedure della libreria vengono chiamate con una NEAR call; la CPU ha bisogno solamente di caricare in IP la componente Offset dell'indirizzo della procedura da chiamare, in quanto CS contiene già il valore COMSEGM.

15.5.1 Assembling & Linking del programma COMTEST2.ASM

La fase di assemblaggio del programma COMTEST2.ASM è del tutto simile al caso di EXETEST2.ASM; con il MASM, il comando da impartire è:
..\bin\ml /c /Fl comtest2.asm
Premendo ora il tasto [Invio], vengono generati i due file COMTEST2.OBJ e COMTEST2.LST; aprendo con un editor il file COMTEST2.LST, possiamo constatare che, ad esempio, nel caso della chiamata alla procedura set80x25, l'assembler ha generato il codice macchina:
E8h 0000h     ; NEAR call
Inoltre, nella symbol table, la procedura set80x25 viene classificata dall'assembler come:
set80x25 Near ----:---- Extern
Come al solito, l'assembler sta delegando al linker il compito di calcolare l'indirizzo logico Seg:Offset della procedura esterna set80x25 di tipo Near.
In una situazione come quella di Figura 15.6, si può anche inserire la direttiva INCLUDE all'interno del blocco COMSEGM; in tal caso, esaminando il file COMTEST2.LST, si nota che nella symbol table l'assembler ha inserito informazioni del tipo:
set80x25 Near COMSEGM:---- Extern
Passiamo ora alla fase di linking del nostro programma; con il MASM, il comando da impartire è:
c:\masm\bin\link /tiny /map comtest2.obj + comlib.obj
Premendo il tasto [Invio] parte la fase di linking che, in assenza di errori, porta alla generazione del file in formato eseguibile COMTEST2.COM e del map file COMTEST2.MAP; analizziamo il lavoro svolto dal linker per il collegamento dei due moduli COMTEST2.OBJ e COMLIB.OBJ. Le considerazioni appena esposte, vengono confermate dal map file di Figura 15.7. Ovviamente, l'entry point del nostro programma in formato COM si trova all'indirizzo logico 0000h:0100h.

Come si può facilmente immaginare, nel caso dei programmi in formato COM, diventa importante l'ordine con il quale si passano i vari moduli al linker; infatti, se proviamo ad eseguire il comando:
c:\masm\bin\link /tiny /map comlib.obj + comtest2.obj
otteniamo il messaggio di avvertimento:
LINK: warning L4055: start address not equal to 0x100 for /TINY
Il linker ci sta dicendo che è stato generato un eseguibile in formato COM il cui entry point si trova ad un offset diverso da 0100h!
Il perché di questo messaggio è abbastanza chiaro; infatti, il linker inizia ad esaminare per primo il modulo COMLIB.OBJ, dove trova il segmento COMSEGM. A questo segmento, viene aggiunto l'altro COMSEGM presente in COMTEST2.OBJ; in questo modo, l'entry point del programma, non solo si viene a trovare ad un offset maggiore di 0100h, ma viene preceduto dal codice e dai dati definiti nel modulo COMLIB.OBJ.

15.5.2 Esecuzione del programma COMTEST2.COM

Passiamo ora alla fase di esecuzione del file COMTEST2.COM; posizionandoci nella cartella ASMBASE, dal prompt del DOS impartiamo il comando:
comtest2
e premiamo il tasto [Invio].

Appena inizia l'esecuzione di COMTEST2.COM, vedremo comparire in fondo allo schermo una serie di informazioni come quelle mostrate in Figura 15.8; queste informazioni si riferiscono alla richiesta di input che arriva dalla procedura readUdec8 e allo stato della CPU mostrato dalla procedura showCPU. La procedura showCPU, chiamata proprio all'inizio del nostro programma, mostra il contenuto iniziale dei registri della CPU; analizziamo, in particolare, l'importante contenuto iniziale dei registri CS, DS, ES, SS, SP e IP (come è stato già detto, le informazioni contenute nei registri di segmento possono variare da computer a computer).

Come sappiamo, in un programma in formato COM il SO inizializza i registri di segmento CS, DS, ES e SS, in modo che puntino tutti all'unico blocco presente; infatti, nel caso di Figura 15.8 vediamo che a questi quattro registri di segmento è stato assegnato lo stesso valore iniziale 1859h.

In relazione al registro IP, il valore 010Ch si riferisce, come al solito, al contenuto dell'instruction pointer al momento della prima chiamata di showCPU; infatti, analizzando il file COMTEST2.LST, si scopre che la prima chiamata di showCPU si trova proprio all'indirizzo logico COMSEGM:010Ch.

La Figura 15.8 conferma anche il fatto che, in un programma in formato COM, il SO inizializza il registro SP con il valore FFFEh; nel caso quindi del programma COMTEST2.COM, l'indirizzo logico iniziale della cima dello stack (TOS) è 1859h:FFFEh.

15.6 I batch file

In un precedente capitolo è stato spiegato che il DOS fornisce un particolare tipo di file eseguibile, chiamato batch file; si tratta di un normalissimo file di testo, contenente al suo interno una sequenza di comandi DOS eseguibili.
I batch file si rivelano molto utili nel momento in cui abbiamo bisogno di automatizzare alcune operazioni particolarmente fastidiose; pensiamo, ad esempio, alla necessità di riassemblare un intero programma formato da numerosi moduli.
Un batch file può essere reso molto più flessibile, grazie alla possibilità di utilizzare alcune particolari "pseudo-istruzioni", chiamate comandi batch; la Figura 15.9 illustra i comandi batch principali e l'effetto prodotto da ciascuno di essi (per maggiori dettagli, si consiglia di consultare i manuali del DOS). Supponiamo, ad esempio, di voler automatizzare le fasi di assembling e linking del programma EXETEST2.ASM; a tale proposito, possiamo creare un apposito batch file chiamato EXETEST2.BAT. Il nome assegnato ad un batch file deve avere obbligatoriamente l'estensione BAT; la Figura 15.10 illustra la struttura interna (volutamente contorta) di EXETEST2.BAT. Come si può notare, le etichette devono iniziare con il simbolo ':' (due punti); inoltre, è importante osservare che l'istruzione da eseguire nel caso in cui sia verificata la condizione specificata da IF, deve trovarsi sulla stessa riga dello stesso IF.
Grazie al batch file di Figura 15.10, ogni volta che vogliamo ricreare il programma EXETEST2.EXE (ad esempio, dopo una modifica del codice sorgente), non dobbiamo fare altro che impartire il comando:
exetest2.bat
È importante ricordare che, se si impartisce il comando exetest2 senza specificare l'estensione, il DOS esegue il primo che incontra tra i file, EXETEST2.COM, EXETEST2.EXE e EXETEST2.BAT; proprio per questo motivo, nel nostro esempio dobbiamo necessariamente impartire il comando completo exetest2.bat, altrimenti, il DOS esegue exetest2.exe (se esiste).

Naturalmente, non siamo obbligati a scrivere un batch file complesso, come quello di Figura 15.10; possiamo anche limitarci a scrivere i soli comandi visibili in Figura 15.11. Se ora vogliamo creare un batch file per un altro programma (ad esempio, EXETEST3.ASM), siamo costretti a riscrivere completamente l'elenco di istruzioni visibili in Figura 15.10; per evitare questo fastidio, possiamo servirci del comando SET che ci permette di ridurre al minimo il lavoro da svolgere.
Il comando SET crea una variabile simbolica (ad esempio, NomeVar) a cui può essere assegnato un valore; successivamente, tale variabile può essere referenziata nel batch file con la sintassi %NomeVar%.
Per avere allora una versione "riciclabile" del batch file di Figura 15.10, possiamo procedere come si vede in Figura 15.12. Per adattare ora EXETEST2.BAT ad un nuovo programma (ad esempio, EXETEST3.ASM), non dobbiamo fare altro che modificare solamente la riga contenente il comando SET e poi salvare il file assegnandogli il nome EXETEST3.BAT.

Possiamo rendere ancora più generale l'esempio di Figura 15.12 sfruttando il fatto che i batch file sono in grado di gestire una serie di argomenti specificati dall'utente; all'interno di un batch file, tali argomenti sono referenziabili, nell'ordine, dai simboli:
%0, %1, %2, %3, %4, %5, %6, %7, %8, %9
Il simbolo %0 rappresenta il nome dello stesso batch file, per cui il primo argomento passato dall'utente è %1.
Tutto ciò ci permette di creare, ad esempio, un generico batch file attraverso il quale possiamo linkare qualsiasi programma alla libreria EXELIB.OBJ; la Figura 15.13 illustra un esempio pratico, chiamato EXELIB.BAT, che rappresenta la generalizzazione del batch file di Figura 15.12. Se ora vogliamo creare, ad esempio, il programma EXETEST3.EXE, non dobbiamo fare altro che impartire il comando:
exelib.bat exetest3
In questo caso, exetest3 è l'argomento %1 che stiamo passando a EXELIB.BAT; è fondamentale che l'argomento exetest3 venga passato senza l'estensione asm.

15.7 Esercitazioni consigliate

Si consiglia vivamente di sfruttare al massimo le librerie EXELIB.OBJ e COMLIB.OBJ, per effettuare il maggior numero possibile di esperimenti; solo in questo modo si può acquisire la necessaria padronanza di tutti i concetti esposti nei precedenti capitoli.
Con le pochissime istruzioni (MOV, ADD, PUSH, POP) e con i pochissimi operatori (SEG, BYTE, WORD, etc) che già conosciamo, possiamo scrivere un gran numero di programmi; nel seguito, vengono illustrati alcuni semplici esempi che si servono della libreria EXELIB (FAR call per le procedure).

15.7.1 Visualizzazione dell'indirizzo logico di una informazione

Dopo aver definito i dati statici di un programma, possiamo provare a stampare sullo schermo gli indirizzi logici Seg:Offset assegnati in fase di esecuzione ai dati stessi; supponendo, ad esempio, di aver definito un dato chiamato Variabile1, possiamo scrivere: Al posto di Variabile1, possiamo utilizzare anche il nome di una etichetta o di una procedura, definite in un blocco di codice; ad esempio, si può provare a visualizzare l'indirizzo Seg:Offset di una qualsiasi procedura definita nella libreria EXELIB.OBJ o COMLIB.OBJ.
Ricordiamoci che in un eseguibile in formato COM, è proibito scrivere istruzioni del tipo:
mov ax, seg writeString
Al posto di questa istruzione possiamo scrivere, ad esempio:
mov ax, cs
Infatti, in un programma in formato COM è presente un unico segmento, referenziato sicuramente da CS; anche gli altri tre registri DS, ES e SS, se non hanno subito modifiche, referenziano l'unico segmento di programma presente.

15.7.2 Gestione di un dato attraverso un puntatore NEAR o FAR

Dopo aver definito i dati statici di un programma, possiamo provare a gestirli attraverso un puntatore NEAR o FAR; supponiamo, ad esempio, di aver definito in un blocco DATASEGM, il dato:
Variabile1 dw -14532
Dopo aver caricato DATASEGM in DS, possiamo scrivere: È importante ribadire che prima di utilizzare DI come puntatore NEAR, il registro di segmento DS deve essere già stato inizializzato con DATASEGM; in caso contrario, il simbolo [DI] fornisce un valore privo di senso, dovuto al fatto che DS può avere un contenuto casuale.

15.7.3 Accesso allo stack con SS:BP

Come sappiamo, in modalità reale è proibito dereferenziare il registro SP attraverso istruzioni del tipo:
mov ax, [sp] ; ax = ss:[sp]
Al posto di SP dobbiamo allora utilizzare BP; se vogliamo esplorare lo stack con BP, possiamo scrivere, ad esempio: Ricordiamo che ogni istruzione PUSH,con operando di tipo WORD, decrementa SP di 2, per cui le varie PUSH inseriscono i loro operandi ad indirizzi sempre più bassi dello stack (riempimento dello stack); come si nota nella parte finale del precedente listato, se vogliamo ripristinare esattamente il vecchio contenuto di AX e BX, dobbiamo estrarre gli operandi dallo stack in senso inverso rispetto agli inserimenti (infatti, lo stack è una struttura di tipo LIFO).
Naturalmente, nessuno ci impedisce di accedere allo stack con, ad esempio, ES:SI; in questo caso, dobbiamo caricare SP in SI e SS in ES. L'utilizzo di ES:SI richiede il segment override, per cui, dobbiamo scrivere istruzioni del tipo:
mov ax, es:[si+2]

15.7.4 Visualizzazione dei codici ASCII contenuti in una stringa

Supponiamo di aver definito in un blocco DATASEGM, la stringa:
Messaggio db 'Assembly Programming'
Se vogliamo visualizzare i codici ASCII dei vari simboli che formano la stringa, possiamo scrivere: Questo esempio funziona solo se DATASEGM è stato già caricato in DS.
Volendo utilizzare i registri puntatori, possiamo scrivere: Questo esempio funziona solo se DATASEGM è stato già caricato in DS.
Volendo utilizzare BP per indirizzare Messaggio, dobbiamo scrivere, esplicitamente, DS:[BP+0], DS:[BP+1], etc; in caso contrario, la CPU associa automaticamente BP a SS.

15.7.5 Analisi del risultato di una operazione attraverso i flags

Sfruttando la procedura showCPU, possiamo verificare in pratica tutti i concetti sulla matematica del computer, esposti nei precedenti capitoli; per il momento, dobbiamo limitarci a degli esempi che utilizzano la sola istruzione ADD.

Se vogliamo provocare un Carry dal nibble basso al nibble alto di AL, possiamo scrivere: Sommando 1 al valore binario 00001111b si ottiene 00010000b con evidente riporto dal nibble basso al nibble alto di AL; di conseguenza, il flag AF (Auxiliary Carry Flag) si porta a livello logico 1.

Se vogliamo provocare un Carry in una somma con AX, possiamo scrivere: Sommando 1 al valore 65535, otteniamo un valore con ampiezza maggiore di 16 bit, che provoca quindi un riporto; di conseguenza, il flag CF (Carry Flag) si porta a livello logico 1.

Se vogliamo provocare un Overflow in una somma con AX, possiamo scrivere: Sommando 1 al valore +32767, otteniamo 32768 che, per i numeri interi senza segno rappresenta 32768, mentre per i numeri interi con segno rappresenta -32768; di conseguenza, il flag CF (Carry Flag) si porta a livello logico 0, mentre i flag OF (Overflow Flag) e SF (Sign Flag) si portano entrambi a livello logico 1.

15.7.6 Visualizzazione del campo CommandLineParameters del PSP di un programma

Utilizzando le informazioni presenti nella Figura 13.11 del Capitolo 13, possiamo provare a visualizzare la stringa dei parametri che un programma accetta dalla linea di comando; a tale proposito, dobbiamo tenere presente che il campo CommandLineParmLength occupa 1 byte e si trova all'indirizzo logico PSP:0080h, mentre il campo CommandLineParameters occupa sino a 127 byte e si trova all'indirizzo logico PSP:0081h.
Non appena un programma (COM o EXE) viene caricato in memoria, sappiamo che ES=PSP; di conseguenza, prima di chiamare writeString, dobbiamo solo caricare in DI l'offset 0081h. Bisogna anche ricordare che la procedura writeString richiede una stringa C terminata da un byte di valore 0; questo byte lo dobbiamo mettere alla fine della stringa CommandLineParameters e cioè, nella posizione che si ricava da ES:[0080h].
In base a queste considerazioni, possiamo scrivere: Supponiamo che il nome di questo programma sia progtest.exe e proviamo ad eseguire il comando:
progtest mela pera ciliegia
La stringa dei parametri è lunga 19 byte, compresi gli spazi; otteniamo quindi all'indirizzo ES:[0081h]:
CommandLineParameters db ' mela pera ciliegia', 0Dh
e all'indirizzo ES:[0080h]:
CommandLineParmLength db 19
Caricando 0081h in DI, facciamo puntare ES:DI alla stringa dei parametri; a questo punto, dobbiamo mettere un byte di valore zero in posizione 19 della stessa stringa. A tale proposito, carichiamo il byte di valore 19 in BL con l'istruzione:
mov bl, es:[0080h]
Carichiamo ora il valore zero in BH in modo da avere BX=19; a questo punto, possiamo inserire in CommandLineParameters il byte di valore zero con l'istruzione:
mov byte es:[bx+di], 0
Osserviamo che:
BX + DI = 19 + 0081h = 0013h + 0081h = 0094h
A questo punto, la stringa è pronta per essere visualizzata da writeString.

A partire dal prossimo capitolo, verranno illustrate in dettaglio tutte le istruzioni delle CPU 80x86, destinate alla modalità reale; in questo modo, sarà possibile scrivere programmi di esempio molto più complessi rispetto a quelli presentati in questo capitolo.