Assembly Base con MASM

Capitolo 32: Interfaccia tra BASIC e Assembly


In questo capitolo vengono descritte le convenzioni che permettono di interfacciare il linguaggio BASIC con il linguaggio Assembly; si presuppone che chi legge abbia una adeguata conoscenza della programmazione in BASIC.

Il BASIC è stato creato nel 1963 da John G. Kemeny e Thomas E. Kurtz, professori al Dartmouth College (USA), con lo scopo di insegnare la programmazione dei computer ai principianti assoluti, privi di qualsiasi conoscenza nel campo dell'informatica e dell'hardware; infatti, BASIC è l'acronimo di Beginner's All purpose Symbolic Instruction Code (linguaggio di programmazione simbolico e di uso generale per i principianti).
Per raggiungere l'obiettivo di insegnare la programmazione a chiunque, il BASIC inizialmente venne privato di tutti quegli aspetti che avrebbero potuto mettere in difficoltà i principianti; in particolare, con le prime versioni del BASIC era possibile fare a meno di importanti concetti come la tipizzazione dei dati e la programmazione strutturata.
Purtroppo, con il passare degli anni queste caratteristiche si sono rivelate estremamente negative e controproducenti; la quasi totale mancanza di regole da rispettare, ha prodotto intere generazioni di programmatori di livello professionale molto basso.
In particolare, la possibilità di fare a meno della programmazione strutturata e l'uso sconsiderato dell'istruzione GOTO, ha favorito un modo di programmare contorto ed incomprensibile che è stato subito etichettato con la tristemente famosa definizione di spaghetti code; nel tentativo di risolvere questi problemi, nel corso degli anni si è assistito alla comparsa sul mercato di numerose nuove varianti del BASIC. Si è passati così dal BASIC originario, ai vari Turbo Basic della Borland, al QuickBASIC e Visual Basic della Microsoft, etc; in tal modo, anche i programmatori BASIC hanno potuto beneficiare di tutti i vantaggi offerti dalla tipizzazione dei dati e dall'uso delle procedure.

Un altro aspetto legato alla evoluzione del BASIC è rappresentato dal fatto che le prime versioni di questo linguaggio di programmazione erano tutte di tipo interpretato; le versioni attuali (o quelle meno vecchie) del linguaggio BASIC sono, invece, quasi tutte di tipo compilato.
Come è stato spiegato nei precedenti capitoli, l'interfacciamento con l'Assembly ha senso solo per i linguaggi compilati; per questo motivo, tutte le considerazioni esposte in questo capitolo si riferiscono al QuickBASIC 4.x della Microsoft che, nel bene e nel male, rappresenta sicuramente la variante del BASIC che ha avuto maggiore successo in ambiente DOS a 16 bit.
Il compilatore Microsoft QuickBASIC 4.x è scaricabile dalla sezione Compilatori assembly, c++ e altri dell’ Area Downloads di questo sito; tale compilatore permette lo sviluppo di programmi BASIC destinati all'ambiente operativo a 16 bit del DOS.

32.1 Tipi di dati del BASIC

In precedenza è stato affermato che quando si programma in BASIC è possibile fare a meno del concetto di tipizzazione dei dati; ciò significa che in un qualunque punto di un programma BASIC possiamo scrivere:
PesoNetto = 1200
In questo modo abbiamo creato "al volo" una variabile apparentemente di tipo intero, che viene poi inizializzata con il valore 1200; nel seguito del programma possiamo ora scrivere:
PesoNetto = PesoNetto + 4.5
In questo modo stiamo sommando un numero reale ad una variabile che dovrebbe essere di tipo intero; se proviamo a compilare questo programma, possiamo notare che non viene generato alcun messaggio di errore!
A titolo di verifica possiamo usare l'istruzione:
PRINT "PesoNetto ="; PesoNetto
per constatare che effettivamente viene visualizzato sullo schermo il valore reale 1204.5!
Questa situazione appare abbastanza strana in quanto non si capisce bene quale sia il tipo della variabile PesoNetto e quale sia la sua ampiezza in bit; un programmatore Assembly, però, sa benissimo che la CPU ha bisogno di conoscere l'ampiezza in bit di un qualsiasi dato appartenente ad un programma scritto con un qualsiasi linguaggio di programmazione. Per svelare questo mistero è necessario allora analizzare il sistema che permette al BASIC di gestire in modo "occulto" i tipi dei dati di un programma.

La particolarità del BASIC sta nel fatto che in questo linguaggio i tipi dei dati di un programma possono essere gestiti in modo implicito; per capire questa situazione è necessario premettere che il BASIC è un linguaggio case insensitive, che non distingue quindi tra maiuscole e minuscole.
Un identificatore del BASIC e cioè, un nome di una variabile, di una etichetta o di una procedura, deve iniziare obbligatoriamente con una tra le 26 possibili lettere dell'alfabeto inglese. Al suo interno un nome può contenere esclusivamente lettere, cifre e il carattere '.' (punto); è proibito, invece, l'uso di altri simboli come '_', '@', '§', etc. Il programmatore può indicare esplicitamente il tipo di un identificatore di variabile o di funzione, posizionando alla fine del relativo nome uno tra i cinque appositi simboli:
'%', '&', '!', '#', '$'
In relazione al simbolo utilizzato si ha che: In assenza dei simboli che indicano esplicitamente il tipo di un identificatore di variabile o di funzione, il BASIC utilizza un metodo che permette di stabilire implicitamente una relazione tra la lettera iniziale del nome dell'identificatore e il relativo tipo; a tale proposito, ad ogni programma BASIC viene associata una tabella che assume il seguente aspetto: Questa tabella indica il fatto che in assenza di diverse indicazioni da parte del programmatore, tutti gli identificatori privi di tipo, i cui nomi iniziano con una qualsiasi delle 26 lettere dell'alfabeto inglese, vengono considerati implicitamente di tipo SINGLE (reale a 32 bit); il BASIC, in pratica, aggiunge automaticamente il simbolo '!' a tutti i nomi degli identificatori privi di tipo!

Tutto ciò giustifica quello che è successo con la variabile PesoNetto; il BASIC, incontrando la definizione:
PesoNetto = 1200
la converte in:
PesoNetto! = 1200.0
(variabile di tipo SINGLE).

Se vogliamo che PesoNetto sia esplicitamente di tipo INTEGER dobbiamo scrivere quindi:
PesoNetto% = 1200
Le considerazioni appena esposte valgono naturalmente anche per le funzioni che restituiscono un valore; nel caso, ad esempio, della dichiarazione:
DECLARE FUNCTION Pitagora(lato1, lato2)
tenendo conto della precedente tabella si ha che Pitagora è una funzione che richiede due argomenti di tipo SINGLE e restituisce un valore di tipo SINGLE!
Il BASIC, infatti, converte la precedente dichiarazione in:
DECLARE FUNCTION Pitagora!(lato1!, lato2!)
La tabella illustrata in precedenza ha lo scopo di permettere al programmatore di delegare al BASIC il compito di gestire implicitamente i tipi di dati; se il programmatore vuole alterare la situazione predefinita, può servirsi delle istruzioni DEFINT, DEFLNG, DEFSNG, DEFDBL e DEFSTR.
Se in un programma BASIC scriviamo, ad esempio: allora la precedente tabella viene modificata nel modo seguente: In questo caso: Un programmatore proveniente dall'Assembly, dal C o dal Pascal, leggendo queste cose non può che rabbrividire; questo sistema sembra concepito apposta per favorire gli errori di programmazione!
Per rendercene conto, consideriamo il seguente programma BASIC: Se si prova a compilare e ad eseguire questo programma, si può constatare che la PRINT visualizza il valore 0 al posto del previsto 2021.02; ciò che è successo è che il programmatore ha creato ed inizializzato due variabili di tipo esplicito SINGLE, chiamate Raggio! e PiGreco!. In seguito, il programmatore ha creato una nuova variabile di tipo esplicito SINGLE, chiamata AreaCerchio!; per inizializzare questa nuova variabile il programmatore ha utilizzato Raggio e PiGreco dimenticando, però, il simbolo '!'.
In base alla DEFINT inserita nel programma, il BASIC crea automaticamente due nuove variabili di tipo implicito INTEGER, chiamate appunto Raggio e PiGreco; in assenza di inizializzazione, tutte le variabili globali del BASIC vengono inizializzate con il valore 0 e questo giustifica il risultato visualizzato dalla PRINT!
In sostanza, il programmatore a sua insaputa si ritrova con ben 5 variabili che sono: Raggio!, PiGreco!, AreaCerchio!, Raggio% e PiGreco%; per verificarlo possiamo aggiungere altre PRINT al programma precedente in modo da stampare i valori di tutte le variabili.

La situazione è ancora più assurda se si pensa che se commettiamo un errore di sintassi scrivendo, ad esempio:
PRINT "Raggio% ="; Ragio%
allora il BASIC, invece di segnalarci l'errore, crea una nuova variabile Ragio% di tipo esplicito INTEGER e con valore iniziale 0; tutto ciò ci fa capire per quale motivo, anche i programmatori BASIC più esperti, si trovassero continuamente alle prese con stranissimi bug che infestavano praticamente tutti i loro programmi.

Le considerazioni esposte in precedenza ci permettono di affermare che il QuickBASIC fornisce i due tipi di dati interi mostrati in Figura 32.1. Come si può notare, non è presente alcun tipo di dato intero a 8 bit e non vengono neanche supportati gli interi senza segno; viene fornito, invece, il supporto per i due tipi di dati in virgola mobile IEEE mostrati in Figura 32.2. Gli estremi Min. e Max. si riferiscono ai numeri reali positivi; per ottenere gli estremi negativi basta mettere il segno meno davanti agli estremi positivi.

Come è stato spiegato in precedenza, il BASIC offre anche il supporto per le stringhe; una stringa BASIC è, come al solito, un vettore di BYTE consecutivi e contigui. La dimensione di una stringa BASIC non può superare i 32767 byte di memoria.
Proprio il caso delle stringhe ci permette di analizzare una caratteristica molto particolare del BASIC; supponiamo, ad esempio, di scrivere:
Stringa1$ = "Esempio di stringa BASIC"
Incontrando questa definizione il BASIC riserva, nel segmento dati del programma, un blocco da 24 byte di memoria, consecutivi e contigui, per contenere esattamente i 24 caratteri che formano Stringa1$; a questo punto bisogna capire come fa il BASIC a gestire la memoria riservata a questa stringa. Nei precedenti capitoli abbiamo visto, ad esempio, che il linguaggio C aggiunge ad ogni stringa uno 0 finale per delimitare la fine della stringa stessa; il Pascal, invece, utilizza il BYTE di indice 0 di una stringa per memorizzare la lunghezza della stringa stessa.
Il BASIC utilizza un sistema completamente differente che gli permette di gestire dinamicamente la memoria riservata alle stringhe e, in generale, ai dati di tipo vettoriale; per capire il funzionamento di questo sistema è necessario premettere che in un programma BASIC esiste un unico blocco dati chiamato DGROUP e referenziato da DS.
Ad ogni stringa presente nel blocco dati, il BASIC associa un cosiddetto "descrittore di stringa"; il descrittore di stringa viene creato ugualmente nel blocco dati ed è costituito da due WORD. La WORD meno significativa contiene la lunghezza della stringa; la WORD più significativa contiene l'offset della stringa stessa, calcolato rispetto a DS.
Supponendo, ad esempio, che Stringa1$ venga posizionata in memoria all'indirizzo logico DS:00B2h, allora il suo descrittore conterrà la DWORD 00B2h:0018h (18h=24d); come è stato già spiegato, questo sistema permette al BASIC di gestire in modo dinamico la memoria riservata alle stringhe.
Supponiamo, ad esempio, di modificare Stringa1$ in questo modo:
Stringa1$ = "Esempio di stringa BASIC modificata"
In questo caso il BASIC libera i 24 byte riservati a Stringa1$, richiede nel blocco dati altri 35 byte consecutivi e contigui e li usa per contenere i 35 caratteri che formano la nuova Stringa1$; a questo punto il BASIC modifica il descrittore della stringa che (se la componente Offset non cambia) diventa 00B2h:0023h (23h=35d).

Il vantaggio di questo sistema consiste nella possibilità di sfruttare in modo ottimale lo spazio presente nell'unico blocco dati del programma; lo svantaggio è rappresentato, invece, dal fatto che numerose allocazioni e deallocazioni della memoria possono portare alla frammentazione della memoria stessa, proprio come accade nell'hard disk a causa delle frequenti operazioni di creazione, modifica e cancellazione di file.
Per ovviare a questo inconveniente, il memory manager del BASIC supporta una nota caratteristica che prende il nome di garbage collection (raccolta della spazzatura); in sostanza, mentre il programma è in esecuzione, il BASIC all'insaputa del programmatore può decidere ad un certo punto di effettuare un riordino (o deframmentazione) del blocco dati, che coinvolge principalmente i vettori monodimensionali (come le stringhe) e multidimensionali.
In questo modo il blocco dati viene ricompattato con l'eliminazione, nei limiti del possibile, degli eventuali "buchi" presenti tra un dato e l'altro; naturalmente, questa operazione coinvolge anche le eventuali stringhe, per cui il BASIC deve necessariamente aggiornare anche i relativi descrittori!

Dalle considerazioni appena esposte, emergono alcuni aspetti molto delicati che impongono al programmatore Assembly un comportamento ben preciso; in sostanza, è necessario tenere sempre presente che a causa del garbage collection, l'indirizzo di determinati dati del BASIC non è statico, ma può variare durante la fase di esecuzione del programma!
Appare evidente quindi che utilizzando una procedura Assembly per manipolare in modo improprio i dati di un programma BASIC, possiamo danneggiare il sistema che il BASIC stesso utilizza per la gestione dei dati in memoria; nel seguito del capitolo vedremo come ci si deve comportare nei vari casi che si possono presentare.

32.2 Convenzioni per i compilatori BASIC

Nel Capitolo 29 sono state illustrate le convenzioni seguite dai compilatori nella gestione della struttura interna dei programmi; analizziamo ora tali convenzioni in riferimento ai compilatori BASIC.

32.2.1 Segmenti di programma

In relazione al sistema di segmentazione da utilizzare nell'interfacciamento tra BASIC e Assembly, bisogna notare che la situazione viene notevolmente semplificata dal fatto che il compilatore QuickBASIC è in grado di generare gli object file in formato OMF (Object Module Format); ciò permette di far interagire facilmente il QuickBASIC con il MASM, per poter creare programmi estremamente flessibili.
All'interno dei moduli Assembly, inoltre, possiamo creare segmenti di programma aventi attributi scelti liberamente in base alle nostre esigenze; solamente in rare circostanze siamo tenuti a creare segmenti di programma aventi attributi imposti dal BASIC.
La conseguenza più importante di questa flessibilità sta nel fatto che, come accade per il C, anche in questo caso abbiamo la possibilità di condividere dati e procedure tra moduli BASIC e moduli Assembly; nel caso del Turbo Pascal, invece, abbiamo visto che l'impossibilità di avere a disposizione gli object file dei moduli Pascal ci impone alcune limitazioni nell'interfacciamento con l'Assembly.

In generale, un programma interamente scritto in BASIC assume una struttura interna che equivale al modello di memoria MEDIUM; come sappiamo, questo modello di memoria prevede la presenza di un unico blocco dati (comprendente anche lo stack) e di due o più segmenti di codice.
In presenza di un unico blocco di dati, il BASIC gestisce le variabili globali del programma attraverso indirizzamenti di tipo NEAR; in presenza di due o più segmenti di codice, tutte le procedure di un programma BASIC devono essere di tipo FAR.

Tutti i segmenti di dati appartenenti ai moduli BASIC, vengono inseriti dal compilatore in un gruppo chiamato DGROUP che in fase di esecuzione viene referenziato attraverso DS; in questo stesso gruppo viene anche inserito il segmento di stack del programma.

Nei moduli Assembly possiamo attenerci a queste regole creando uno o più segmenti di dati NEAR da inserire in DGROUP; come è stato spiegato in precedenza, gli attributi di questi segmenti di dati possono essere scelti liberamente dal programmatore.
Volendo utilizzare gli attributi standard che già conosciamo, possiamo creare, ad esempio, il seguente segmento di dati NEAR: In questo caso dobbiamo anche ricordarci di inserire la direttiva:
DGROUP GROUP _DATA
Nel blocco codice del modulo Assembly, inoltre, se vogliamo accedere per nome ai dati definiti in _DATA, dobbiamo inserire la direttiva:
ASSUME DS: DGROUP
Tutti i dati globali da condividere tra moduli BASIC e moduli Assembly, devono trovarsi in un segmento di dati NEAR avente i seguenti attributi: In alternativa è possibile scegliere un nome personalizzato; utilizzando, ad esempio, il nome DATICOMUNI, gli attributi di questo segmento diventano: Anche questo blocco deve essere rigorosamente inserito in DGROUP; in presenza, ad esempio, del blocco _DATA e del blocco COMMON, dobbiamo inserire la direttiva:
DGROUP GROUP _DATA, COMMON
Tutti i dettagli relativi al segmento dati condivisi verranno illustrati nel seguito del capitolo.

Eventualmente, possiamo creare anche segmenti di dati FAR da non inserire in DGROUP; in questo caso il programma complessivo assume una struttura interna che equivale al modello di memoria LARGE.
Utilizzando gli attributi standard possiamo creare, ad esempio, il seguente segmento di dati FAR: Se vogliamo accedere ai dati FAR tramite DS, dobbiamo preservare rigorosamente il contenuto di questo registro di segmento; inoltre, in base alle considerazioni appena esposte, è importantissimo restituire sempre il controllo al BASIC con DS=DGROUP.

Come è stato affermato in precedenza, ciascun modulo Assembly collegato ad un programma BASIC può utilizzare un proprio segmento di codice; le procedure presenti in tali segmenti devono essere quindi tutte di tipo FAR.
Utilizzando gli attributi standard, in ogni modulo Assembly possiamo allora creare segmenti di codice del tipo: Avendo a disposizione le versioni più recenti di MASM, possiamo risparmiare parecchio lavoro attraverso le caratteristiche avanzate di tale assembler; nel caso più generale possiamo inserire, all'inizio di ogni modulo, la direttiva:
.MODEL LARGE, BASIC
In questo modo possiamo creare i vari segmenti di programma attraverso le direttive semplificate che conosciamo (.DATA, .FARDATA e .CODE); l'assembler provvederà poi a creare automaticamente il gruppo DGROUP e ad inserire le necessarie direttive ASSUME.
Osserviamo, però, che in presenza del blocco dati COMMON, tutti i relativi dettagli devono essere gestiti direttamente dal programmatore; il blocco COMMON, infatti, presenta caratteristiche non standard che non sono quindi supportate da MASM.

Al momento di caricare il programma in memoria, il SO inizializza CS con il segmento principale di codice BASIC che contiene, ovviamente, l'entry point; da parte sua, il compilatore genera il codice macchina che pone automaticamente DS=SS=DGROUP; a causa della presenza del gruppo DGROUP, tutte le procedure Assembly che modificano DS sono tenute a preservare rigorosamente il contenuto originale di tale registro di segmento.

32.2.2 Stack frame

Le convenzioni seguite dal BASIC per la gestione dello stack frame di una procedura, sono del tutto simili a quelle utilizzate dal Pascal; inoltre, è necessario ribadire che le chiamate alle procedure esterne sono tutte di tipo FAR.

Gli eventuali argomenti da passare ad una procedura vengono inseriti nello stack a partire dal primo (sinistra destra); all'interno dello stack gli argomenti di una procedura vengono quindi posizionati in ordine inverso rispetto a quello indicato dalla intestazione della procedura stessa.
Il compito di ripulire lo stack dagli argomenti spetta alla procedura chiamata; tale lavoro viene generalmente effettuato attraverso l'istruzione:
RET n
dove n indica il numero di byte da sommare a SP.

L'accesso ai parametri di una procedura avviene attraverso BP; è necessario ricordare che i parametri sono posizionati in ordine inverso nello stack e quindi, muovendoci in avanti rispetto a BP, incontreremo per primo l'ultimo parametro ricevuto dalla procedura. A causa delle FAR call, l'ultimo parametro si trova sempre a BP+6.

32.2.3 Valori di ritorno

In relazione alle locazioni utilizzate per contenere gli eventuali valori di ritorno interi delle FUNCTION del BASIC, valgono tutte le convenzioni illustrate nella Figura 25.10 del Capitolo 25; nel caso particolare delle FUNCTION che restituiscono valori in virgola mobile IEEE di tipo SINGLE e DOUBLE, il BASIC utilizza, invece, le convenzioni del FORTRAN.
Il procedimento per la restituzione di tali valori comporta i seguenti passi:

32.3 Le classi di memoria del BASIC

Il QuickBASIC fornisce una serie di caratteristiche che permettono di scrivere programmi adeguatamente strutturati; in particolare, vengono messi a disposizione diversi tipi di procedure che comprendono le FUNCTION e le SUB.
La FUNCTION rappresenta la generalizzazione del concetto di operatore matematico e quindi fornisce sempre un valore di ritorno; la SUB rappresenta la generalizzazione del concetto di istruzione e non ha quindi alcun valore di ritorno.

Tutte le FUNCTION e le SUB definite in un modulo BASIC risultano automaticamente visibili anche nei moduli esterni; gli eventuali moduli Assembly possono quindi chiamare (con FAR call) tutte le FUNCTION e le SUB presenti in un modulo BASIC.
Tutte le procedure definite in un modulo Assembly e dichiarate PUBLIC, come già sappiamo, risultano automaticamente visibili anche nei moduli esterni; queste procedure possono essere quindi chiamate (con FAR call), sia da altri moduli Assembly, sia dal modulo principale BASIC.

Anche in relazione alle variabili di un programma, il QuickBASIC ha introdotto una serie di innovazioni che permettono di creare variabili, sia locali, sia globali; nelle prime versioni del BASIC, invece, tutte le variabili (anche quelle create all'interno di una procedura) erano visibili in qualsiasi punto del programma.
Tutte le variabili globali definite in un modulo BASIC e non associate ad una direttiva COMMON, risultano visibili solo all'interno del modulo stesso; tali variabili globali possono essere paragonate alle variabili static del C.
Tutte le variabili globali definite in un modulo BASIC e associate ad una direttiva COMMON risultano, invece, visibili anche nei moduli esterni; tali variabili quindi possono essere condivise tra moduli BASIC e moduli Assembly.

Sempre in relazione alle variabili globali di un programma BASIC, analizziamo il metodo seguito dal compilatore per l'allocazione della relativa memoria; come è stato già spiegato, un programma BASIC è dotato di un unico blocco dati DGROUP che comprende: dati inizializzati, dati non inizializzati, dati costanti e stack.
La dimensione del blocco DGROUP, ovviamente, non può superare i 65536 byte; vediamo allora come sia possibile creare, in un programma BASIC, una quantità di dati (in particolare, grossi vettori multidimensionali), la cui dimensione complessiva può superare abbondantemente i 65536 byte!

Tutte le variabili numeriche semplici e cioè, le variabili di tipo INTEGER, LONG, SINGLE e DOUBLE, vengono create nel blocco DGROUP; l'accesso a queste variabili avviene esclusivamente con indirizzamenti di tipo NEAR in quanto la loro componente Seg è implicitamente DS=DGROUP.

Tutte le variabili di tipo STRING vengono create nel blocco DGROUP e vengono associate, come già sappiamo, al relativo descrittore creato sempre in DGROUP; l'offset di una stringa, contenuto nel relativo descrittore, è riferito quindi sempre a DS=DGROUP.
Abbiamo anche visto che a causa del garbage collection, l'indirizzo delle stringhe del BASIC può variare dinamicamente in fase di esecuzione del programma; è pericolosissimo quindi accedere alle stringhe BASIC in modo improprio.
Se abbiamo, ad esempio, due stringhe BASIC chiamate Stringa1$ e Stringa2$ e vogliamo copiare Stringa2$ in Stringa1$, possiamo scrivere:
Stringa1$ = Stringa2$
Utilizzando, invece, metodi impropri come, ad esempio, una procedura Assembly che accede direttamente a queste due stringhe, rischiamo di danneggiare il sistema di gestione della memoria del BASIC; in questi casi normalmente il programma viene interrotto e viene visualizzato il messaggio:
Spazio stringhe alterato
La memoria necessaria per i vettori del BASIC viene allocata in base al metodo che utilizziamo per specificare le dimensioni del vettore stesso; in questo modo è possibile creare vettori gestiti staticamente o dinamicamente.
Un vettore le cui dimensioni sono rappresentate da valori immediati, viene creato nel blocco DGROUP; questo caso si presenta, ad esempio, quando si scrive:
DIM Matr1%(5, 3)
In questo modo stiamo definendo un vettore Matr1% formato da 6 righe (indici da 0 a 5) e 4 colonne (indici da 0 a 3) per un totale di:
6 * 4 = 24
elementi di tipo INTEGER che richiedono complessivamente:
24 * 2 = 48
byte di memoria.

Questo blocco da 48 byte viene creato in DGROUP.

L'accesso ad un vettore statico avviene con indirizzamenti di tipo NEAR in quanto la componente Seg è implicitamente DS=DGROUP; un vettore BASIC creato in questo modo non può essere ridimensionato con l'istruzione REDIM.

Supponiamo ora di aver definito le variabili dim1% = 49 e dim2% = 99; a questo punto possiamo scrivere la definizione:
DIM Matr2!(dim1%, dim2%)
In questo modo stiamo definendo un vettore Matr2! formato da 50 righe (indici da 0 a 49) e 100 colonne (indici da 0 a 99), per un totale di:
50 * 100 = 5000
elementi di tipo SINGLE che richiedono complessivamente:
5000 * 4 = 20000
byte di memoria.

Questo blocco da 20000 byte viene creato nel global heap del programma, al di fuori di DGROUP!

Il global heap di un programma rappresenta tutta la memoria convenzionale disponibile; per conoscere questa informazione, dal prompt del DOS si può digitare il comando:
mem /c
L'accesso ad un vettore dinamico avviene con indirizzamenti di tipo FAR formati quindi da una coppia Seg:Offset; un vettore BASIC creato in questo modo può essere anche ridimensionato con l'istruzione REDIM.

Con il metodo appena descritto è possibile persino creare vettori dinamici di tipo HUGE la cui dimensione complessiva può superare i 65536 byte; tali vettori vengono ovviamente creati nel global heap e vengono gestiti dal BASIC attraverso gli indirizzi logici normalizzati.

Anche i vettori del BASIC sono soggetti al garbage collection; proprio per questo motivo, in analogia con i dati di tipo STRING, il BASIC associa un descrittore ad ogni vettore che abbiamo definito nel programma. Come al solito, il descrittore viene creato nel blocco DGROUP; ciò accade anche quando il vettore si trova nel global heap.

32.4 Gestione degli indirizzamenti in BASIC

Il BASIC è un linguaggio di programmazione rivolto ai principianti assoluti, per cui tende a nascondere tutti i dettagli relativi agli indirizzamenti; contemporaneamente, però, questo linguaggio fornisce anche una serie di procedure di basso livello che permettono di operare direttamente con gli indirizzi di memoria.

Analizziamo innanzi tutto il sistema che il BASIC utilizza per il passaggio degli argomenti alle procedure; il comportamento predefinito del BASIC consiste nel passare gli argomenti sempre per indirizzo!
Questo discorso vale quindi anche quando gli argomenti sono rappresentati da variabili semplici di tipo INTEGER, LONG, SINGLE e DOUBLE; vediamo un esempio pratico riferito alla dichiarazione:
DECLARE FUNCTION AddIntegers%(a%, b%)
Questa procedura richiede due argomenti di tipo INTEGER e termina restituendo in AX un INTEGER che rappresenta la somma tra a% e b%; per chiamare questa procedura utilizziamo le tre variabili globali varint1% = 2000, varint2% = 5000 e varint3%.
La chiamata di AddIntegers% è rappresentata quindi dall'istruzione:
varint3% = AddIntegers%(varint1%, varint2%)
La procedura AddIntegers% riceve in questo caso, non i due valori 2000 e 5000, ma le componenti Offset di varint1% e varint2%; trattandosi di variabili semplici di tipo INTEGER, la loro componente Seg è implicitamente DS=DGROUP.
Se questa procedura è scritta in Assembly, supponendo DS=DGROUP, avremo: Come già sappiamo, il passaggio degli argomenti per indirizzo è particolarmente veloce, soprattutto nel caso di argomenti di grosse dimensioni come le stringhe, i vettori, i record, etc; l'aspetto negativo è rappresentato dal fatto che una procedura come AddIntegers% può inavvertitamente modificare il contenuto originale degli argomenti ricevuti.
Se vogliamo servirci allora del passaggio per valore, dobbiamo indicarlo esplicitamente inserendo la parola chiave BYVAL nella dichiarazione della procedura; nel caso di AddIntegers% la dichiarazione diventa:
DECLARE FUNCTION AddIntegers%(BYVAL a%, BYVAL b%)
In questo modo AddIntegers% riceve i valori dei due argomenti e non gli indirizzi; la parola chiave BYVAL non può essere utilizzata nel caso di argomenti di tipo STRING o, più in generale, di tipo vettore.
Nel caso di un argomento di tipo STRING il BASIC passa obbligatoriamente la componente Offset del descrittore della stringa; anche in questo caso, la componente Seg è implicitamente DS=DGROUP.

Vediamo un esempio pratico che si riferisce ad una procedura Assembly che converte una stringa BASIC in maiuscolo; prima di tutto definiamo la seguente stringa:
Stringa1$ = "Stringa BASIC da convertire"
Supponiamo che questa stringa venga sistemata all'offset 3500 del blocco DGROUP; di conseguenza, il suo descrittore (creato anch'esso in DGROUP) sarà formato dalla DWORD 3500:27. Questa DWORD indica che Stringa1$ è lunga 27 caratteri e si trova in memoria all'indirizzo logico DGROUP:3500; passando Stringa1$ come argomento di una procedura, ciò che viene passato è l'offset del suo descrittore calcolato rispetto a DGROUP.
La dichiarazione BASIC della procedura del nostro esempio è la seguente:
DECLARE SUB StrToUpper(s$)
La chiamata di StrToUpper con Stringa1$ come argomento è rappresentata dall'istruzione:
CALL StrToUpper(Stringa1$)
Sempre nell'ipotesi DS=DGROUP, l'implementazione Assembly di questa procedura è la seguente: Come è stato già spiegato, questa procedura presuppone DS=DGROUP; in questo caso, gli offset contenuti in BX vengono correttamente calcolati rispetto a DGROUP.
Grazie al passaggio dell'argomento per indirizzo, la procedura StrToUpper può accedere direttamente al blocco di memoria contenente Stringa1$ modificandone il contenuto originale; naturalmente, il programmatore deve evitare nella maniera più assoluta di alterare il contenuto del descrittore della stringa!

Analizziamo, infine, il caso degli argomenti di tipo vettore; anche in questo caso il BASIC passa obbligatoriamente l'indirizzo del vettore e, a differenza del Pascal, non permette quindi il passaggio per valore di un intero vettore.
In precedenza abbiamo visto che i vettori del BASIC possono essere creati staticamente in DGROUP e dinamicamente nel global heap; in questo secondo caso, la componente Seg del vettore è ovviamente diversa da DGROUP.
Per gestire questa situazione, conviene scrivere procedure che ricevono come argomenti, sia la componente Seg, sia la componente Offset del vettore; in questo modo possiamo operare indifferentemente su vettori statici e dinamici.

Per ottenere le componenti Seg e Offset di una qualunque variabile globale, il BASIC ci mette a disposizione le due funzioni VARSEG e VARPTR; la funzione VARSEG restituisce la componente Seg (a 16 bit) del suo argomento, mentre la funzione VARPTR restituisce la componente Offset (a 16 bit) del suo argomento.
Supponiamo, ad esempio, di aver definito una variabile globale vardouble1# che si trova all'offset 14 del blocco DGROUP, con DS=DGROUP=3500; in questo caso: Questo sistema funziona solo con il QuickBASIC 4.x; nelle vecchie versioni del QuickBASIC bisogna utilizzare, invece, la funzione VARPTR in combinazione con la procedura di libreria PTR86.
Nella vecchia versione, la funzione VARPTR applicata ad una variabile, restituisce un valore a 32 bit che rappresenta la distanza (in byte) della variabile da DS; questo valore viene poi passato a PTR86 che attraverso due altri argomenti passati per indirizzo, restituisce la coppia Seg:Offset della variabile.
Nel caso, ad esempio, di vardouble1#, dopo aver definito le due variabili intere seg% e offs% dobbiamo scrivere:
CALL PTR86(seg%, offs%, VARPTR(vardouble1#))
Torniamo ora al caso del QuickBASIC 4.x e vediamo come operano le due funzioni VARSEG e VARPTR nel caso di argomenti di tipo stringa e di tipo vettore; nel caso delle stringhe, queste due funzioni restituiscono le componenti Seg e Offset non della stringa ma, come al solito, del descrittore della stringa stessa.
Nel caso dei vettori, VARSEG e VARPTR restituiscono, invece, le componenti Seg e Offset dell'elemento specificato del vettore; nella maggior parte dei casi abbiamo bisogno di conoscere la coppia Seg:Offset del primo elemento di un vettore.
Supponiamo allora di aver definito le due variabili dim1% = 15 e dim2% = 29; a questo punto possiamo definire il seguente vettore bidimensionale di INTEGER:
DIM Matr1%(dim1%, dim2%)
Questo vettore è formato da 16 righe e 30 colonne per un totale di 480 elementi; ogni elemento richiede due byte per cui Matr1% occupa in memoria 960 byte.
In base alla precedente definizione, questi 960 byte verranno allocati dinamicamente nel global heap al di fuori quindi di DGROUP; supponiamo ora di voler scrivere una procedura Assembly che inizializza un generico vettore multidimensionale di INTEGER, con un determinato valore iniziale.
Prima di tutto inseriamo nel modulo BASIC una dichiarazione del tipo:
DECLARE SUB InitVector(BYVAL vseg%, BYVAL voff%, BYVAL n%, BYVAL i%)
I parametri vseg%, voff%, n% e i% rappresentano, rispettivamente: la componente Seg del vettore, la componente Offset, il numero di elementi e il valore iniziale da assegnare ad ogni elemento; come si può notare, tutti gli argomenti vengono passati questa volta per valore.
A questo punto, nel modulo Assembly possiamo scrivere la seguente procedura: Osserviamo che l'istruzione:
les di, dword ptr voff
trasferisce la coppia vseg:voff in ES:DI; questa istruzione lavora in modo corretto solo se vseg è stata inserita nello stack prima di voff.
Siccome il BASIC passa gli argomenti da sinistra a destra, nella lista dei parametri di InitVector è necessario posizionare il parametro vseg prima di voff; utilizzando, invece, le convenzioni del C avremmo dovuto invertire le posizioni di vseg e voff.

Se ora vogliamo inizializzare con il valore 10 i 480 INTEGER di Matr1%, dobbiamo utilizzare la seguente chiamata BASIC:
CALL InitVector(VARSEG(Matr1%(0, 0)), VARPTR(Matr1%(0, 0)), 480, 10)
Come è stato spiegato in precedenza, quando si utilizzano VARSEG e VARPTR con argomenti di tipo vettore, è importantissimo indicare a quale elemento del vettore stesso ci si vuole riferire; nel nostro caso viene specificato naturalmente il primo elemento Matr1%(0, 0) del vettore.

Un altro aspetto che assume una importanza fondamentale è rappresentato dal fatto che, come è stato spiegato in precedenza, sia le stringhe, sia i vettori vengono associati ai rispettivi descrittori e sono soggetti quindi al garbage collection da parte del BASIC; tutto ciò significa che in fase di esecuzione di un programma, l'area di memoria riservata a questi dati non è fissa, ma può cambiare da un momento all'altro.
Questa situazione impone al programmatore un comportamento ben preciso; in particolare, è importante ricordare sempre che le informazioni restituite da funzioni come VARSEG e VARPTR devono essere utilizzate immediatamente.
Tali informazioni, infatti, possono cambiare senza preavviso e non avrebbe senso quindi memorizzarle per poi utilizzarle in un secondo momento; in sostanza, VARSEG e VARPTR devono essere chiamate solo nel preciso momento in cui ci servono le informazioni restituite da queste due funzioni.

Un'altra conseguenza legata a questa delicata situazione è data dal fatto che un argomento di tipo stringa o vettore passato ad una procedura BASIC o Assembly, deve essere elaborato immediatamente; una procedura che riceve uno di questi argomenti e prima di elaborarlo lo passa ad un'altra procedura, sta seguendo un comportamento molto pericoloso!
È chiaro, infatti, che se la seconda procedura esegue, ad esempio, un'istruzione REDIM su un vettore ricevuto come argomento, restituisce alla prima procedura un vettore con differenti dimensioni e con differente indirizzo di memoria; se non si seguono queste precauzioni, si ottiene sicuramente un programma che funziona in modo anomalo.

Prima di concludere questa parte relativa agli indirizzamenti è necessario menzionare due istruzioni che i programmatori BASIC conoscono molto bene; si tratta, naturalmente, delle famigerate istruzioni POKE e PEEK, attraverso le quali è possibile accedere in modo indiscriminato a qualsiasi area della memoria del computer, sia in lettura, sia in scrittura.

PEEK è una funzione che richiede un argomento Offset e restituisce un valore byte; l'argomento Offset rappresenta un offset di memoria dal quale PEEK legge un valore a 8 bit. Il valore di ritorno byte rappresenta il valore a 8 bit letto da PEEK; possiamo affermare quindi che PEEK ci fornisce il contenuto a 8 bit della locazione di memoria che si trova all'indirizzo Seg:Offset.

POKE è un'istruzione che richiede due argomenti offset e byte; l'argomento offset rappresenta un offset di memoria nel quale POKE scrive un valore a 8 bit. L'argomento byte rappresenta il valore a 8 bit che POKE deve scrivere; possiamo affermare quindi che POKE scrive il valore byte a 8 bit nella locazione di memoria che si trova all'indirizzo Seg:Offset.

A questo punto resta da capire rispetto a quale componente Seg venga calcolata la componente Offset utilizzata da POKE e PEEK; in assenza di altre indicazioni da parte del programmatore, la componente Offset viene calcolata rispetto a DGROUP.
Se il programmatore vuole alterare questa situazione, deve utilizzare l'istruzione DEF SEG; questa istruzione permette di stabilire la componente Seg che verrà utilizzata dalle istruzioni POKE, PEEK, BLOAD, BSAVE e CALL ABSOLUTE.

Vediamo, ad esempio, come sia possibile inizializzare con il valore 10 (&H000A) tutti gli elementi del precedente vettore Matr1%; abbiamo visto che i 480 INTEGER di Matr1% vengono allocati dinamicamente nel global heap al di fuori di DGROUP.
Utilizzando POKE possiamo scrivere allora: Come si può notare, dopo aver utilizzato POKE, PEEK, BLOAD, etc, è una buona regola ripristinare il segmento predefinito DGROUP per queste istruzioni; l'istruzione DEF SEG senza argomento ripristina appunto DGROUP come segmento predefinito per POKE, PEEK, BLOAD, etc.

32.5 Protocollo di comunicazione tra BASIC e Assembly

Analizziamo ora le regole che permettono a due o più moduli BASIC e Assembly di comunicare tra loro; l'insieme di queste regole definisce il protocollo di comunicazione tra BASIC e Assembly.

In relazione alle procedure di un programma, tutte le FUNCTION e le SUB definite in un modulo BASIC e associate alle rispettive DECLARE, risultano visibili anche nei moduli esterni come procedure di tipo FAR; un modulo Assembly che intende servirsi di queste procedure, le deve dichiarare EXTRN e le può così chiamare attraverso FAR call.

Tutte le procedure definite in un blocco codice di un modulo Assembly e dichiarate PUBLIC, risultano visibili anche nei moduli esterni; queste procedure per essere compatibili con le convenzioni del BASIC devono essere di tipo FAR.
Un modulo BASIC che intende servirsi di queste procedure, le deve dichiarare con le apposite direttive DECLARE e le può così chiamare attraverso FAR call; naturalmente, in questo caso le FAR call vengono gestite direttamente dal compilatore.

In relazione alle variabili di un programma, tutte le variabili globali definite in un modulo BASIC e non associate ad una direttiva COMMON, risultano invisibili nei moduli esterni; analogamente, tutte le variabili globali definite in un blocco dati di un modulo Assembly non compatibile con il blocco COMMON del BASIC, risultano invisibili nei moduli BASIC esterni.

Tutte le variabili globali definite o solo dichiarate in un modulo BASIC e associate ad una direttiva COMMON, risultano visibili anche nei moduli esterni; questa situazione si verifica, ad esempio, nel caso di un modulo BASIC che presenta la seguente dichiarazione:
COMMON num1%, num2%, num3!, num4!
In questo caso il BASIC crea il seguente segmento dati: Un modulo Assembly che intende condividere queste variabili, deve definire un segmento dati avente gli identici attributi del segmento appena illustrato; all'interno di questo segmento devono essere definite le identiche variabili, nell'identico ordine specificato dalla direttiva COMMON del modulo BASIC.
Questo blocco, inoltre, deve essere categoricamente inserito nel gruppo DGROUP; i dati in esso contenuti possono essere gestiti quindi con indirizzi di tipo NEAR.

Come si può notare, il precedente blocco COMMON presenta l'attributo di combinazione COMMON; nel Capitolo 12 abbiamo visto che tutti i segmenti di programma aventi lo stesso nome, la stessa classe e lo stesso attributo di combinazione COMMON, vengono sovrapposti tra loro in modo che condividano tutti lo stesso indirizzo fisico iniziale in memoria.
Nel nostro caso, il blocco COMMON del BASIC e il blocco COMMON dell'Assembly vengono sovrapposti tra loro in modo da ottenere un unico blocco COMMON contenente le 4 variabili num1%, num2%, num3! e num4!; si tenga presente che in fase di compilazione, il BASIC rimuove dagli identificatori i simboli come '%' '!', etc.

Il programmatore ha anche la possibilità di assegnare un nome personalizzato al blocco COMMON appena descritto; nel modulo BASIC possiamo scrivere ad esempio:
COMMON /DATICOMUNI/ num1%, num2%, num3!, num4!
In questo caso il BASIC crea il seguente segmento dati: Anche in questo caso, un modulo Assembly che intende condividere queste variabili, deve definire un segmento dati identico a quello appena illustrato; come al solito, questo segmento deve poi essere inserito in DGROUP.

Tutte le procedure contenute nei moduli Assembly da linkare al BASIC, devono preservare rigorosamente il contenuto dei registri CS, DS, SS, SP e BP; tutti gli altri registri sono a completa disposizione del programmatore.

Il BASIC è un linguaggio case-insensitive e quindi i compilatori BASIC non distinguono tra maiuscole e minuscole; possiamo affermare quindi che in BASIC i nomi come varword1%, VarWord1%, VARWORD1%, etc, rappresentano tutti lo stesso identificatore.
A differenza, però, di quanto accade in Pascal, bisogna sempre ricordare che a causa della gestione implicita dei tipi dei dati, in un modulo BASIC la contemporanea presenza dei nomi varword1% e VARWORD1% non produce alcun messaggio di errore; il BASIC, infatti, considera i due nomi come identificatori della stessa variabile di tipo INTEGER!

In osservanza alle regole appena illustrate, al momento di assemblare un modulo Assembly da linkare ad un modulo BASIC, non dobbiamo assolutamente utilizzare le opzioni come /Cp per il MASM.

32.6 Esempi pratici

Nella sezione 32.4 di questo capitolo abbiamo analizzato alcuni esempi che si riferiscono a procedure Assembly chiamabili da un modulo BASIC; vediamo ora un esempio completo che mostra anche come condividere variabili tra BASIC e Assembly e come chiamare, da un modulo Assembly, una serie di FUNCTION e SUB scritte in BASIC.

Il programma di esempio è formato dai due moduli BASASM.BAS e ASMBAS.ASM; il modulo BASASM.BAS ricopre il ruolo di modulo principale del programma ed è illustrato in Figura 32.3. Le varie DECLARE ci permettono di dichiarare, sia le procedure esterne definite nel modulo Assembly, sia le procedure interne definite nel modulo BASIC; naturalmente, tutte le procedure esterne che vogliamo chiamare dal modulo BASIC, devono essere dichiarate PUBLIC nel modulo Assembly di appartenenza.

La direttiva COMMON, presente nel modulo BASASM.BAS, elenca una serie di variabili da condividere con gli altri moduli del programma; è importante ricordare che in un modulo BASIC, la direttiva COMMON deve precedere qualunque istruzione eseguibile, comprese le definizioni delle variabili globali.

Il primo compito svolto dal modulo BASASM.BAS consiste nel chiamare la SUB InitCommonVars per inizializzare le variabili condivise; tale SUB è definita nel modulo ASMMOD.ASM. Subito dopo la loro inizializzazione, le variabili condivise vengono poi visualizzate attraverso una serie di PRINT.

Successivamente, il modulo BASASM.BAS chiama la FUNCTION Pitagora! definita nel modulo ASMBAS.ASM; questa FUNCTION utilizza il teorema di Pitagora per calcolare la diagonale di un rettangolo avente per lati i due SINGLE passati come argomenti.
In questo caso, la chiamata di Pitagora! viene gestita dal compilatore BASIC che quindi provvede anche a predisporre lo stack per ricevere il valore di tipo SINGLE restituito dalla FUNCTION; se, invece, la chiamata avviene da un modulo Assembly, questi dettagli sono a carico del programmatore.

L'ultimo compito svolto dal modulo BASASM.BAS consiste nella chiamata della SUB AsmProcedure definita nel modulo ASMBAS.ASM; questa SUB ha lo scopo di illustrare il metodo di chiamata di una serie di FUNCTION e SUB del BASIC che visualizzano valori di tipo INTEGER, SINGLE e STRING.

Per chiarire meglio questi dettagli, analizziamo il modulo ASMBAS.ASM illustrato in Figura 32.4. Il modulo ASMBAS.ASM inizia con una serie di dichiarazioni esterne per le procedure definite in BASASM.BAS; queste procedure esterne possono essere quindi chiamate da ASMBAS.ASM attraverso apposite FAR call.

Subito dopo troviamo un blocco COMMON contenente le variabili NEAR condivise tra i vari moduli del programma; è importante notare che nel blocco COMMON del modulo ASMBAS.ASM, queste variabili vengono definite con lo stesso nome, la stessa ampiezza in bit e la stessa disposizione specificata dalla direttiva COMMON del modulo BASASM.BAS.

Il modulo ASMBAS.ASM definisce anche un blocco riservato _DATA di variabili globali; questo blocco viene poi inserito insieme a COMMON in DGROUP e può essere quindi gestito attraverso indirizzi di tipo NEAR.

Analizziamo ora il blocco codice _TEXT che contiene le definizioni di tutte le procedure chiamabili dal modulo BASIC; questo blocco specifica le necessarie direttive PUBLIC per queste procedure e le direttive ASSUME che associano CS a _TEXT e DS a DGROUP.
La prima procedura chiamata da BASASM.BAS è InitCommonVars che inizializza le variabili condivise; come si può notare, i due SINGLE condivisi varsingle1 e varsingle2 vengono inizializzati con i due SINGLE chiamati, rispettivamente, varfloat1 e varfloat2, definiti nel blocco _DATA.

La seconda procedura chiamata da BASASM.BAS è Pitagora! che calcola la diagonale del rettangolo che ha per lati i due argomenti di tipo SINGLE ricevuti dalla procedura stessa; come già sappiamo, il comportamento predefinito del BASIC consiste nel passare l'offset degli argomenti e non il loro valore. Possiamo affermare quindi che Pitagora! riceve gli offset sng1 e sng2 di due SINGLE; in entrambi i casi, la componente Seg è implicitamente DGROUP in quanto i tipi numerici semplici del BASIC vengono sempre creati in tale gruppo.
Siccome Pitagora! restituisce un SINGLE, il BASIC prima di chiamare questa procedura predispone nello stack un'area da 4 byte per contenere il valore di ritorno; come già sappiamo, l'offset di quest'area viene passato come ultimo argomento a Pitagora!.
Anche in questo caso, la componente Seg predefinita è DGROUP; infatti, in fase di inizializzazione del programma, il BASIC pone DS=SS=DGROUP.
Tenendo conto delle convenzioni BASIC per il passaggio degli argomenti, troveremo quindi nello stack il parametro retOffs a BP+6, il parametro sng2 a BP+8 e il parametro sng1 a BP+10; questi tre parametri rappresentano degli offset, per cui vengono gestiti dalla procedura attraverso i registri puntatori BX, DI e SI.
Come al solito, il calcolo della diagonale viene effettuato attraverso le istruzioni della FPU; nel rispetto delle convenzioni del BASIC, la procedura Pitagora! prima di terminare restituisce SS in DX e retOffs (cioè, BX) in AX.

Passiamo, infine, all'ultima procedura AsmProcedure chiamata da BASASM.BAS; questa procedura chiama a sua volta diverse altre procedure del modulo BASIC che svolgono svariati compiti, compresa la visualizzazione di dati di tipo INTEGER, SINGLE e STRING.
Prima di tutto AsmProcedure chiama PrintInteger per visualizzare dei dati di tipo INTEGER; come si nota dal prototipo, PrintInteger richiede l'offset di un dato di tipo INTEGER.
Il primo INTEGER che viene visualizzato è vint che è stato ricevuto da AsmProcedure come parametro; questo parametro non è altro che l'offset di un INTEGER, per cui può essere inserito direttamente nello stack e passato a PrintInteger.
Per visualizzare, invece, asmint1 definito nel blocco _DATA, dobbiamo calcolare necessariamente il suo offset; come al solito, per prevenire il bug dell'operatore OFFSET, dobbiamo inserire il segment override DGROUP:asmint1.

La procedura AsmProcedure visualizza in seguito una serie di valori di tipo SINGLE; a tale proposito, viene chiamata la procedura PrintSingle che in base al prototipo richiede l'offset di un SINGLE.
Il primo SINGLE che viene visualizzato è vsng che è stato ricevuto da AsmProcedure come parametro; questo parametro non è altro che l'offset di un SINGLE, per cui può essere inserito direttamente nello stack e passato a PrintSingle.
Per visualizzare, invece, asmsng1 definito nel blocco _DATA, dobbiamo calcolare necessariamente il suo offset; anche in questo caso, per prevenire il bug dell'operatore OFFSET, dobbiamo inserire il segment override DGROUP:asmsng1.

AsmProcedure chiama PrintSingle anche per visualizzare un SINGLE restituito dalla procedura Pitagora2! definita nel modulo BASASM.BAS e identica alla procedura Pitagora! definita nel modulo ASMBAS.ASM; in questo modo abbiamo la possibilità di analizzare la gestione dei valori di ritorno di tipo IEEE da parte di una procedura Assembly.
Prima di tutto, AsmProcedure crea nello stack un'area di 4 byte per contenere il SINGLE restituito da Pitagora2!; quest'area viene identificata dalla macro retOffs.
La procedura Pitagora2! riceve quindi i tre argomenti rappresentati dall'offset di varfloat1, dall'offset di varfloat2 e dall'offset di retOffs; naturalmente, per calcolare l'offset di retOffs utilizziamo l'istruzione LEA.
La procedura Pitagora2! termina restituendo in DX:AX l'indirizzo SS:retOffs che contiene il valore di ritorno di tipo SINGLE; siccome DX=SS e SS=DGROUP, possiamo chiamare PrintSingle passandole AX che contiene l'offset del SINGLE da visualizzare.

L'ultimo compito svolto da AsmProcedure consiste nel chiamare PrintString per visualizzare delle stringhe BASIC; la procedura PrintString, in base al suo prototipo, richiede come sappiamo l'offset del descrittore della stringa da visualizzare.
Il parametro vstr ricevuto da AsmProcedure è già l'offset del descrittore di una stringa, per cui lo possiamo passare direttamente a PrintString; in seguito AsmProcedure chiama di nuovo PrintString per visualizzare una stringa BASIC "simulata" nel modulo Assembly.
Come si può notare, nel blocco _DATA viene creato un "finto" descrittore chiamato asmstr1 di tipo DWORD; in questo descrittore carichiamo la lunghezza della stringa strAsm e il relativo offset calcolato rispetto a DGROUP.
A questo punto, l'offset del descrittore asmstr1 viene passato a PrintString; si consiglia in ogni caso di definire stringhe e vettori esclusivamente nei moduli scritti in BASIC in modo da garantire la corretta gestione del garbage collection da parte del BASIC stesso.

Un'ultima importantissima considerazione riguarda il fatto che tutti gli indirizzamenti relativi ai dati NEAR di un programma BASIC, si svolgono correttamente solo se DS=SS=DGROUP; nelle procedure Assembly è importantissimo quindi preservare sempre il contenuto originale di questi registri di segmento.

32.6.1 Generazione dell'eseguibile

Come è stato già spiegato in precedenza, la generazione dell'eseguibile è resa molto semplice dal fatto che il QuickBASIC è in grado di produrre gli object file dei moduli BASIC; le fasi da svolgere sono quindi le seguenti: L'assemblaggio deve essere case insensitive e quindi non bisogna utilizzare opzioni come /Cp con MASM.

Il compilatore a linea di comando del QuickBASIC si chiama BC.EXE, mentre il linker a 16 bit si chiama LINK.EXE e può essere rimpiazzato da un qualunque altro linker a 16 bit della Microsoft (come quello fornito dal MASM16 o dal FORTRAN77).
Non è possibile, invece, utilizzare i linker a 16 bit della Borland o di altre marche; questo perché gli object file prodotti dal QuickBASIC contengono particolari caratteristiche interne che vengono supportate solo dai linker della Microsoft.

Vediamo ora come dobbiamo procedere per convertire BASASM.BAS e ASMBAS.ASM nell'eseguibile BASASM.EXE; supponiamo a tale proposito di aver installato il MASM nella cartella C:\MASM e il QuickBASIC nella cartella C:\QB45, con la cartella di lavoro in C:\QB45\ASMBASE.
Prima di tutto ci dobbiamo posizionare nella cartella C:\QB45\ASMBASE dove abbiamo salvato i due moduli del programma; a questo punto, con il comando:
c:\masm\ml /c asmbas.asm
generiamo il modulo ASMBAS.OBJ.

Con il comando:
..\bc basasm.bas
generiamo il modulo BASASM.OBJ.

Con il comando:
..\link basasm.obj + asmbas.obj
generiamo l'eseguibile finale BASASM.EXE.

Quando il linker mostra la stringa Libraries [.LIB]:, bisogna digitare ..\brun45.lib; si tratta della libreria del QuickBASIC necessaria per creare file eseguibili "stand alone", cioè indipendenti dall'interprete BASIC.

Per automatizzare tutto questo lavoro possiamo crearci un apposito batch file; a tale proposito, nella cartella C:\QB45\ASMBASE creiamo, ad esempio, il seguente batch file denominato BASASM.BAT: Grazie a questo batch file, possiamo eseguire le varie fasi digitando semplicemente BASASM.BAT dal prompt del DOS; anche in questo caso è necessario posizionarsi nella cartella C:\QB45\ASMBASE.

Se nel modulo BASIC vengono definiti vettori dinamici di tipo HUGE è necessario passare l'opzione /ah a BC.EXE; se vogliamo generare il map file del programma possiamo passare l'opzione /map a LINK.EXE.