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:
- un identificatore il cui nome termina con il simbolo '%' è considerato
esplicitamente di tipo INTEGER (intero con segno a 16 bit)
- un identificatore il cui nome termina con il simbolo '&' è considerato
esplicitamente di tipo LONG (intero con segno a 32 bit)
- un identificatore il cui nome termina con il simbolo '!' è considerato
esplicitamente di tipo SINGLE (reale IEEE a 32 bit in precisione
singola)
- un identificatore il cui nome termina con il simbolo '#' è considerato
esplicitamente di tipo DOUBLE (reale IEEE a 64 bit in precisione
doppia)
- un identificatore il cui nome termina con il simbolo '$' è considerato
esplicitamente di tipo STRING (vettore di codici
ASCII da 8 bit
ciascuno)
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 identificatore privo di tipo il cui nome inizia con le lettere comprese
tra A e G è implicitamente di tipo INTEGER
- un identificatore privo di tipo il cui nome inizia con le lettere comprese
tra H e M è implicitamente di tipo DOUBLE
- un identificatore privo di tipo il cui nome inizia con le lettere comprese
tra N e T è implicitamente di tipo STRING
- un identificatore privo di tipo il cui nome inizia con le lettere comprese
tra U e Z è implicitamente di tipo SINGLE
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:
- il caller crea nello stack un'area di memoria sufficiente a contenere il valore
IEEE restituito dalla FUNCTION
- il caller chiama la FUNCTION passandole come ultimo argomento l'offset
(che chiamiamo simbolicamente OFFS) dell'area di memoria appena creata
nello stack (la componente Seg è, ovviamente, SS)
- la FUNCTION, a causa della FAR call, trova OFFS a BP+6
e gli eventuali altri parametri a partire da BP+8
- la FUNCTION memorizza in SS:OFFS il valore IEEE da restituire
- la FUNCTION prima di terminare carica SS in DX e OFFS
in AX
- il caller riottiene il controllo e trova all'indirizzo DX:AX il valore
IEEE restituito dalla FUNCTION
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:
- VARSEG(vardouble1#) restituisce il valore 3500
- VARPTR(vardouble1#) restituisce il valore 14
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:
- assemblaggio dei moduli Assembly
- compilazione del modulo principale BASIC
- generazione dell'eseguibile finale
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.