Assembly Base con MASM
Capitolo 31: Interfaccia tra Pascal e Assembly
In questo capitolo vengono descritte le convenzioni che permettono di interfacciare il
linguaggio Pascal con il linguaggio Assembly; si presuppone che chi legge
abbia una adeguata conoscenza della programmazione in Pascal.
Il linguaggio Pascal è stato creato nel 1968 dal Prof. Niklaus Wirth
alla Eidgenossische Technische Hochschule di Zurigo; l'obiettivo di
Wirth era quello di creare un linguaggio di programmazione di uso generale
(general purpose) finalizzato esplicitamente all'insegnamento dell'arte della
programmazione ai principianti.
La maggiore preoccupazione di Wirth fu quella di eliminare tutte le terrificanti
caratteristiche che resero tristemente famoso il BASIC (nato anch'esso come
linguaggio didattico); tra gli aspetti più negativi delle prime versioni del
BASIC si possono ricordare lo scarso risalto dato alla tipizzazione dei dati e
l'impossibilità di poter creare programmi decentemente strutturati.
Il risultato ottenuto da Wirth fu un linguaggio di programmazione che ancora oggi
può essere definito come uno dei più semplici ed eleganti mai realizzati; in particolare,
il Pascal presenta una robustissima tipizzazione dei dati e una sintassi che
favorisce uno sviluppo fortemente strutturato dei programmi.
Queste caratteristiche fecero subito conquistare al Pascal il ruolo di linguaggio
di programmazione più usato nelle scuole e, in particolare, nelle Università di tutto il
mondo; inizialmente, il Pascal ottenne anche un notevole successo tra i
programmatori professionisti.
A differenza di quanto accade per il C, non esiste alcuno standard di linguaggio
per il Pascal; proprio per questo motivo è possibile trovare in circolazione diverse
varianti di questo linguaggio come, ad esempio, l'IBM Pascal, il Microsoft Quick
Pascal, il Borland Turbo Pascal, etc.
Molto spesso tali varianti del Pascal possono differire tra loro per alcuni
dettagli sintattici, per la presenza di estensioni del linguaggio e per il procedimento
che porta alla generazione dei programmi eseguibili; in ogni caso, come molti sanno, il
Borland Turbo Pascal può essere definito il vero responsabile dell'enorme successo
ottenuto da questo linguaggio di programmazione, tanto da meritarsi il titolo di standard
"virtuale" di riferimento del linguaggio Pascal.
Con il passare degli anni, il Pascal ha subito un lento declino dovuto, in particolare,
all'avvento del linguaggio C; sul finire degli anni 80 il C ha iniziato
a prendere il sopravvento sul Pascal, sia negli ambienti universitari, sia soprattutto
tra gli sviluppatori professionisti. Ciò è accaduto in quanto, come è stato spiegato nel
precedente capitolo, le rigidissime regole sintattiche del Pascal possono andare bene
per i principianti, ma finiscono per creare notevoli problemi a quegli sviluppatori
professionisti che spesso ricorrono a tecniche di programmazione piuttosto smaliziate.
La situazione comunque è in continua evoluzione e il Pascal conta ancora numerosi
appassionati in tutto il mondo, tanto che è possibile trovare compilatori Pascal
destinati ai più diffusi SO; inoltre, bisogna anche osservare che le limitazioni
del Pascal possono essere facilmente aggirate interfacciando tale linguaggio con
l'Assembly.
Tutte le considerazioni esposte in questo capitolo si riferiscono al compilatore Borland
Turbo Pascal versione 7.0, scaricabile dalla sezione
Compilatori assembly, c++ e altri dell’
Area Downloads di questo sito;
tale compilatore permette lo sviluppo di programmi
Pascal destinati all'ambiente operativo a 16 bit del DOS.
Come è stato spiegato in precedenza, il Turbo Pascal viene considerato, di fatto, lo
standard di riferimento per questo linguaggio di programmazione; l'implementazione del
Turbo Pascal, pur presentando numerose innovazioni, rispetta quasi interamente la
struttura sintattica originale del linguaggio, così come è stata definita dal suo creatore
Niklaus Wirth.
31.1 Tipi di dati del Pascal
Il Pascal attribuisce una notevole importanza alla tipizzazione dei dati e obbliga
il programmatore a tenere ben distinti quei dati che non appartengono allo stesso insieme
numerico o alla stessa categoria; i tipi di dati interi forniti dal Pascal sono
elencati in Figura 31.1.
Questi tipi di dati interi vengono chiamati ordinali in quanto tra due qualsiasi
numeri interi può essere stabilita una relazione d'ordine (maggiore, minore, uguale);
i tipi ordinali comprendono anche il tipo Boolean che può assumere uno tra
i due valori costanti False (cioè, 0) e True (cioè, 1).
I tipi Enumerated e Subrange appartengono anch'essi alla categoria dei tipi
ordinali.
Anche il Turbo Pascal supporta i tre tipi fondamentali di dati in floating
point espressi nel formato standard IEEE; tali tre tipi sono il single
a 32 bit, il double a 64 bit e l'extended a 80 bit.
A questi tre tipi si aggiungono anche il tipo real a 48 bit e il tipo
comp a 64 bit; le caratteristiche di questi tipi di dati sono elencate in
Figura 31.2.
Come al solito, 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.
Il tipo comp è un tipo intero con segno a 64 bit che può essere gestito
solo attraverso la FPU o attraverso il software di emulazione della FPU.
Il Pascal dispone anche del tipo stringa che, come già sappiamo, permette di
definire vettori di BYTE dove ogni BYTE contiene un codice
ASCII; in un precedente capitolo
abbiamo anche visto che il Pascal utilizza il BYTE di indice 0 della
stringa per contenere la lunghezza della stringa stessa. Possiamo affermare quindi che la
lunghezza massima di una stringa Pascal è pari al valore massimo rappresentabile
con 8 bit e cioè, 255 caratteri.
Se scriviamo:
const str1[4] = 'ciao';
il compilatore assegna a str1 un vettore di 5 BYTE formato da un BYTE
di valore 4 (lunghezza della stringa) e da 4 BYTE contenenti i codici
ASCII dei 4 caratteri della
stringa; se, invece, scriviamo:
const str1 = 'ciao';
il compilatore assegna a str1 un vettore di 256 BYTE formato da un
BYTE di valore 4 (lunghezza della stringa), da 4 BYTE contenenti i
codici ASCII dei 4 caratteri
della stringa e da altri 251 BYTE vuoti (i quali, in genere, contengono il valore
0).
Il programma mostrato in Figura 31.3 permette di verificare in pratica le caratteristiche dei
vari tipi di dati del Pascal.
La funzione SizeOf restituisce la dimensione in byte del suo argomento; la funzione
Ord restituisce il valore numerico (ordinale) del suo argomento (il quale
deve appartenere ai tipi ordinali).
31.2 Convenzioni per i compilatori Pascal
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 Pascal.
31.2.1 Segmenti di programma
I compilatori Pascal capaci di generare il codice oggetto (come l'IBM
Pascal), seguono per i segmenti di programma le stesse convenzioni illustrate
nei precedenti capitoli; la possibilità di avere a disposizione il codice oggetto
di un modulo Pascal facilita notevolmente il lavoro di interfacciamento con
l'Assembly.
Il compilatore Turbo Pascal, invece, produce un codice intermedio chiamato
P-Code, che viene tenuto nascosto al programmatore; questa situazione rende
più difficoltose le comunicazioni tra moduli Turbo Pascal e moduli
Assembly.
Si tenga presente, inoltre, che il Turbo Pascal utilizza una segmentazione
la quale segue solo in parte le convenzioni che già conosciamo; per rendersene conto
basta richiedere al compilatore la generazione del map file del programma.
A tale proposito, bisogna selezionare il menu
Options - Linker - Map File
Attraverso il map file si può constatare quanto segue:
Esiste un unico blocco per i dati statici comprendente quindi i dati inizializzati,
non inizializzati e costanti; tale blocco presenta le seguenti caratteristiche:
Esiste un unico blocco stack che naturalmente viene predisposto dal compilatore e
presenta le seguenti caratteristiche:
Ciascun modulo appartenente ad un programma Pascal utilizza un proprio
segmento di codice; i vari segmenti di codice differiscono tra loro solo per il
nome.
Il segmento di codice relativo al modulo principale di un programma Pascal
contiene l'entry point e quindi ricopre il ruolo di segmento principale di
codice; il nome utilizzato da questo segmento è legato alla presenza o meno della
parola riservata Program all'inizio del modulo principale.
Se non utilizziamo tale parola riservata, il blocco di codice principale assume le
seguenti caratteristiche:
Se, come nell'esempio DATATYPE.PAS di Figura 31.3, il programma inizia con:
Program DataTypes;
allora il blocco di codice principale assume le seguenti caratteristiche:
Ogni unit del Turbo Pascal utilizza un segmento di codice che ha il nome
della unit stessa; nel caso, ad esempio, della unit Crt della libreria
Pascal, viene utilizzato il seguente segmento di codice:
Da queste considerazioni emerge chiaramente il fatto che un programma Turbo Pascal
è dotato di un unico segmento di dati, un unico segmento di stack e due o più segmenti di
codice; le procedure presenti nelle unit si trovano in segmenti di codice diversi
da quello in cui si trova il caller e quindi possono essere chiamate solo attraverso
FAR call.
Nel caso dei moduli Pascal tutti questi aspetti vengono gestiti dal compilatore;
nel caso dei moduli Assembly, invece, tutto è nelle mani del programmatore.
In particolare, il programmatore deve indicare chiaramente al compilatore Pascal
il tipo NEAR o FAR delle procedure definite nei moduli Assembly; il
procedimento da seguire viene illustrato nel seguito del capitolo.
Il compilatore Pascal, subito dopo l'entry point, genera tutto il codice
necessario affinché, al momento di caricare il programma in memoria, il SO ponga:
SS = STACK
Lo stesso SO inizializza il registro CS con il segmento principale di
codice che contiene, ovviamente, l'entry point; nel caso del precedente esempio
DATATYPE.PAS, si ha:
CS = DataTypes
Da parte sua, il compilatore genera il codice macchina che pone automaticamente:
DS = DATA
Esistendo un unico blocco di dati, non viene creato alcun gruppo DGROUP.
31.2.2 Stack frame
Il Pascal mette a disposizione due tipi di procedure definibili attraverso le
parole riservate procedure e function; a differenza di quanto accade con
una procedure, la function ha sempre un valore di ritorno.
Nel seguito del capitolo viene utilizzato il termine procedura per indicare genericamente,
sia una procedure, sia una function.
La convenzione Pascal prevede che gli eventuali argomenti da passare ad una procedura
vengano 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, come sappiamo, alla procedura
chiamata; questo lavoro viene generalmente effettuato attraverso l'istruzione:
RET n
dove n indica il numero di byte da sommare a SP.
Come al solito, l'accesso ai parametri di una procedura avviene attraverso BP;
questa volta, però, bisogna ricordarsi 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.
Nel caso di NEAR call l'ultimo parametro si trova a BP+4; nel caso, invece,
di FAR call l'ultimo parametro si trova a BP+6.
31.2.3 Valori di ritorno
In relazione, infine, alle locazioni utilizzate per contenere gli eventuali valori di
ritorno delle funzioni, valgono tutte le convenzioni illustrate nella Figura 25.10 del
Capitolo 25; bisogna prestare attenzione al fatto che il tipo comp, pur
appartenendo agli interi, viene gestito attraverso la FPU e quindi viene
restituito nel registro ST0.
31.3 Le classi di memoria del Pascal
In relazione alle classi di memoria bisogna premettere che nel caso più generale, un
programma Pascal è formato da un modulo principale Pascal, da una o più
unit Pascal e da uno o più moduli Assembly; si tenga presente che nel
caso del Turbo Pascal, la unit System contenente le procedure di sistema
(come New, Dispose, Write, Sin, Cos, etc), viene
incorporata automaticamente nel modulo principale e in tutte le eventuali altre
unit del programma.
Osservando, infatti, il map file di un qualunque programma Turbo Pascal, si
nota sempre la presenza di un blocco codice:
Il Pascal permette di creare procedure innestate all'interno di altre procedure;
una procedura A innestata in una procedura B è visibile solamente in
B e quindi non può essere chiamata da altre procedure.
In sostanza, i compilatori Pascal permettono di trattare le procedure innestate
come se fossero delle variabili locali create nello stack; è ovvio però che in realtà,
una qualunque procedura innestata o non innestata, viene sempre creata in un segmento di
codice del programma.
Tutte le procedure non innestate presenti nel modulo principale Pascal hanno
automaticamente linkaggio esterno; queste procedure quindi sono visibili anche negli
eventuali moduli Assembly facenti parte del programma.
In relazione alle unit il discorso è analogo al caso delle procedure static
e non static del C; tutte le procedure dichiarate nella sezione
interface hanno automaticamente linkaggio esterno, mentre tutte le procedure
dichiarate solo nella sezione implementation hanno automaticamente linkaggio
interno.
Questa distinzione tra procedure pubbliche e private di una unit viene gestita
dal compilatore e quindi vale solamente per i moduli Pascal che fanno uso della
unit stessa.
Un modulo Assembly, invece, è anche in grado di vedere tutte le procedure non
innestate presenti nella sezione implementation di una unit; se vogliamo
attenerci alle regole di visibilità del Pascal, nei moduli Assembly
possiamo evitare di dichiarare EXTRN le procedure private di una unit.
Come è stato spiegato in precedenza, l'eccezione è rappresentata dalla unit System;
questa unit speciale viene automaticamente collegata al modulo principale e a tutte
le altre unit attraverso un procedimento che viene nascosto al programmatore (si
può anche constatare che non esiste alcun file SYSTEM.TPU).
La conseguenza è che le procedure della unit System non risultano visibili nei
moduli Assembly; nei moduli Pascal, inoltre, è proibito passare queste
procedure come argomenti di altre procedure.
Gli identificatori delle procedure innestate presenti in un qualunque modulo Pascal
hanno linkaggio interno e non risultano visibili negli altri moduli, compresi i moduli
Assembly.
Le etichette dichiarate con label in una procedura sono visibili solo all'interno
della procedura stessa e il loro identificatore può essere quindi ridefinito in altri
punti del programma; le etichette dichiarate con label al di fuori di qualsiasi
procedura sono visibili in tutto il modulo di appartenenza e il loro identificatore può
essere quindi ridefinito solo in altri moduli.
Per quanto riguarda le variabili globali dei moduli Pascal e cioè, le variabili
definite al di fuori di qualsiasi procedura, valgono le stesse regole esposte per le
procedure; tutte le variabili definite al di fuori di qualsiasi procedura del modulo
principale Pascal hanno automaticamente linkaggio esterno e risultano quindi
visibili anche in eventuali moduli Assembly facenti parte del programma.
In relazione alle unit, la distinzione tra variabili globali pubbliche e private
viene gestita dal compilatore e quindi vale solamente per i moduli Pascal che
fanno uso della unit stessa; in pratica, un modulo Pascal che fa uso di una
unit è in grado di vedere solamente le variabili globali presenti nella sezione
interface della unit stessa.
Un modulo Assembly, invece, è in grado di vedere tutte le variabili globali presenti,
sia nella sezione interface, sia nella sezione implementation di una
unit; anche in questo caso, se vogliamo attenerci alle regole di visibilità del
Pascal, nei moduli Assembly possiamo evitare di dichiarare EXTRN le
variabili globali private di una unit.
I moduli Assembly non sono, invece, in grado di vedere le costanti prive di tipo
dichiarate in un modulo Pascal come, ad esempio:
const ALTEZZA = 10;
Come è stato spiegato in precedenza, tutte le variabili globali di un programma Pascal
(pubbliche o private), vengono sistemate in un unico segmento dati definito come:
Il compilatore assegna automaticamente il valore 0 a tutte le variabili globali non
inizializzate; analogamente, le stringhe non inizializzate vengono riempite con zeri.
Tutte le variabili definite in una procedura Pascal sono considerate variabili
locali; le variabili locali vengono create nello stack e risultano visibili solo
all'interno della procedura di appartenenza.
Le variabili locali possono essere inizializzate, sia con valori costanti, sia con il
contenuto di altre variabili; come al solito, in assenza di inizializzazione il contenuto
di una variabile locale è casuale.
31.4 Gestione degli indirizzamenti in Pascal
Anche il Pascal supporta i puntatori che, come sappiamo, sono delle variabili
intere senza segno il cui contenuto rappresenta un indirizzo di memoria; il problema che
si presenta è dato dal fatto che il Pascal è un linguaggio di programmazione
destinato ai principianti, per cui tende a nascondere tutti i dettagli relativi alla
gestione a basso livello di un programma.
La conseguenza pratica è che in Pascal la gestione dei puntatori è piuttosto
contorta e tende a creare parecchi problemi ai programmatori provenienti dal C o
dall'Assembly; il modo migliore per aggirare questi problemi consiste naturalmente
nel ricorrere, in caso di necessità, all'interfacciamento con l'Assembly o all'uso
dell'Assembly inline supportato in modo molto efficiente dal Turbo Pascal.
Il primo aspetto da osservare riguarda il fatto che nel Turbo Pascal i puntatori
sono sempre di tipo FAR e rappresentano quindi coppie Seg:Offset da
16+16 bit; come al solito, in base alla convenzione Intel la componente
Seg deve sempre trovarsi nei 16 bit più significativi del puntatore.
Tutti gli aspetti relativi ai puntatori del Pascal coincidono con quanto esposto
nel precedente capitolo a proposito della gestione dei puntatori in C; per maggiore
chiarezza, consideriamo il seguente blocco di variabili globali di un modulo principale
Pascal:
Quando un compilatore Pascal incontra questo blocco dati:
- assegna a c1 una locazione di memoria da 8 bit e carica in tale
locazione il valore 12
- assegna a i1 una locazione di memoria da 16 bit e carica in tale
locazione il valore 1350
- assegna a l1 una locazione di memoria da 32 bit e carica in tale
locazione il valore 186900
- assegna a s1 256 locazioni di memoria da 8 bit ciascuna e carica
nella prima locazione il valore 14 (lunghezza della stringa), nelle
successive 14 locazioni i codici
ASCII dei 14 caratteri
della stringa e nelle restanti 241 locazioni il valore 0
- assegna a ptSho una locazione di memoria da 16+16 bit e carica in
tale locazione la coppia 0000h:0000h (nil)
- assegna a ptInt una locazione di memoria da 16+16 bit e carica in
tale locazione la coppia 0000h:0000h (nil)
- assegna a ptLon una locazione di memoria da 16+16 bit e carica in
tale locazione la coppia 0000h:0000h (nil)
- assegna a ptStr una locazione di memoria da 16+16 bit e carica in
tale locazione la coppia 0000h:0000h (nil)
All'interno del blocco di codice principale e cioè, all'interno del blocco delimitato da
begin e end, procediamo alla inizializzazione dei puntatori; in presenza
dell'istruzione:
ptSho^:= c1;
il compilatore carica in ptSho l'indirizzo Seg:Offset di c1.
In presenza dell'istruzione:
ptInt^:= i1;
il compilatore carica in ptInt l'indirizzo Seg:Offset di i1.
In presenza dell'istruzione:
ptLon^:= l1;
il compilatore carica in ptLon l'indirizzo Seg:Offset di l1.
In presenza dell'istruzione:
ptStr^:= s1;
il compilatore carica in ptStr l'indirizzo Seg:Offset del primo elemento
di s1.
Come si può notare, la sintassi utilizzata dal Pascal con i puntatori lascia molto
a desiderare e tende spesso a creare una certa confusione; molti programmatori provenienti
dal C, ad esempio, seguendo la logica sono portati a scrivere:
ptInt:= ^i1;
Nel tentativo di rendere meno confusa la gestione dei puntatori, il Turbo Pascal
ha introdotto un nuovo operatore rappresentato dal simbolo @ (chiocciola); questo
operatore equivale al simbolo & del C e deve essere letto come
"indirizzo di ...".
Impiegando questo nuovo operatore possiamo scrivere assegnamenti del tipo:
ptInt:= @i1;
Come si può notare, questa nuova sintassi appare molto più chiara e logica di quella
utilizzata dal Pascal classico; la precedente istruzione, infatti, indica
chiaramente che stiamo assegnando a ptInt l'indirizzo di i1.
Il Turbo Pascal mette a disposizione anche i puntatori generici rappresentati
dal tipo Pointer; tali puntatori equivalgono ai puntatori a void del
C.
Un puntatore di tipo Pointer può puntare a qualsiasi variabile di qualsiasi tipo;
nella sezione var del blocco dati illustrato in precedenza, possiamo scrivere, ad
esempio:
ptVoid: Pointer;
A questo punto, nel blocco di codice principale possiamo scrivere:
ptVoid:= @c1;
In qualsiasi altro punto del programma possiamo anche far puntare ptVoid altrove;
possiamo scrivere, ad esempio:
ptVoid:= @ptStr;
Come accade per i puntatori a void del C, anche i Pointer del Turbo
Pascal non possono essere dereferenziati; il loro scopo è solamente quello di memorizzare
un indirizzo che spesso viene poi passato ad una procedura Assembly la quale può
gestirlo senza avere tra i piedi lo stretto controllo di tipo dei compilatori Pascal.
Il Turbo Pascal mette a disposizione anche altre procedure di basso livello per i
puntatori; in particolare, possono tornale utili le function come Seg,
Ofs, Ptr e Addr.
Seg richiede un solo argomento che deve essere l'identificatore pubblico di una
variabile, di una function o di una procedure; il valore restituito da
Seg è una Word contenente la componente Seg dell'indirizzo
dell'argomento.
Ofs richiede un solo argomento che deve essere l'identificatore pubblico di una
variabile, di una function o di una procedure; il valore restituito da
Ofs è una Word contenente la componente Offset dell'indirizzo
dell'argomento.
Addr richiede un solo argomento che deve essere l'identificatore pubblico di una
variabile, di una function o di una procedure; il valore restituito da
Addr è un Pointer contenente la coppia Seg:Offset dell'indirizzo
dell'argomento.
Ptr richiede due argomenti di tipo Word che devono rappresentare una coppia
Seg:Offset; il valore restituito da Ptr è un Pointer contenente
la coppia Seg:Offset costituita dalle due Word passate come argomenti.
Tutte queste estensioni del Pascal sono state introdotte con l'intento di rendere
il linguaggio più potente e più flessibile; molto spesso, però, si ottiene come risultato
un notevole aumento della confusione.
Consideriamo, ad esempio, la seguente procedura Pascal che si serve del blocco dati
visto in precedenza:
Questa procedura richiede un Integer passato per indirizzo; se vogliamo passare a
questa procedura la variabile i1 per indirizzo, dobbiamo scrivere:
WriteInteger(i1);
Il Pascal, come al solito, tende a nascondere tutti i dettagli relativi alla gestione
a basso livello del programma; la variabile i1 ci appare quindi come se venisse
passata per valore.
Un programmatore abituato a lavorare con il C, dopo aver fatto puntare ptInt
a i1 cercherebbe di scrivere:
WriteInteger(ptInt);
Il compilatore genera, però, un messaggio di errore per indicare che WriteInteger
richiede un Integer e non un puntatore ad Integer; se vogliamo utilizzare
per forza ptInt dobbiamo scrivere allora:
WriteInteger(ptInt^);
Secondo la sintassi del Pascal, infatti, ptInt^ è la variabile puntata da
ptInt e cioè, i1 (che è un Integer); queste assurdità sono legate
in parte agli stretti vincoli sulla compatibilità dei tipi di dati del Pascal
e in parte al fatto che il linguaggio Pascal è stato progettato proprio per
impedire ai programmatori di ricorrere a questi "giochetti".
La procedura WriteInteger può essere resa più flessibile in questo modo:
In questo caso, supponendo che ptInt stia puntando a i1, possiamo effettuare
la seguente chiamata:
WriteInteger(ptInt); {passaggio indiretto dell'indirizzo di i1}
Alternativamente, grazie alle estensioni del Turbo Pascal si può anche scrivere:
WriteInteger(@i1); {passaggio diretto dell'indirizzo di i1}
oppure:
WriteInteger(Addr(i1)); {passaggio diretto dell'indirizzo di i1}
Appare chiaro, però, che per poter scrivere codice Pascal di questo genere è
necessario avere una adeguata conoscenza dell'Assembly; in altre parole, il
programmatore deve conoscere tutti i dettagli sul funzionamento a basso livello di un
programma Pascal.
Come è stato spiegato in precedenza, per aggirare tutti questi problemi conviene spesso
ricorrere all'interfacciamento con l'Assembly in modo da avere a disposizione una
sintassi molto più chiara e semplice, soprattutto per i puntatori; come vedremo nel seguito
del capitolo, si rivela estremamente efficace anche il ricorso all'Assembly inline.
Una procedura Assembly che riceve come argomento un puntatore Pascal, si
trova ad avere a che fare con una normalissima coppia Seg:Offset; a questo punto,
la gestione di questa coppia Seg:Offset si svolge nell'identico modo già illustrato
nei precedenti capitoli.
È importante solo ricordare che tutti i puntatori del Turbo Pascal sono di tipo
FAR; di conseguenza, una procedura Assembly che restituisce un puntatore ad
un modulo Pascal, deve restituire la coppia completa Seg:Offset (in
DX:AX).
Un'ultima considerazione riguarda il fatto che anche il Pascal permette di passare
una procedura A come argomento di un'altra procedura B; come abbiamo visto
nei precedenti capitoli, in un caso del genere viene passato come argomento l'indirizzo di
memoria da cui inizia il corpo della procedura A e cioè, l'indirizzo della prima
istruzione della procedura A.
Per gestire questa situazione, la sintassi utilizzata dal Turbo Pascal si discosta
da quella definita nel Pascal classico; vediamo a tale proposito un esempio pratico.
Supponiamo di voler scrivere una procedura che riceve come argomento un'altra procedura
avente le caratteristiche della WriteInteger vista in precedenza; prima di tutto
dobbiamo creare un apposito tipo di dato e quindi nella sezione type del programma
possiamo scrivere, ad esempio:
ptProcInt = procedure(var i: Integer);
In questo modo abbiamo dichiarato un nuovo tipo di dato ptProcInt che rappresenta
un tipo puntatore a una procedure la quale richiede un argomento di tipo
Integer da passare per indirizzo; a questo punto possiamo creare la nostra
procedura che avrà una intestazione del tipo:
procedure ProcTest(pp1: ptProcInt, var i: Integer);
Nel blocco delle istruzioni principali possiamo ora effettuare chiamate del tipo:
ProcTest(WriteInteger, i1);
In un caso del genere quindi, la procedura ProcTest riceve come primo argomento
la coppia Seg:Offset che rappresenta l'indirizzo di memoria da cui inizia il corpo
di WriteInteger; come secondo argomento ProcTest riceve in modo "occulto"
l'indirizzo Seg:Offset di i1 e non il valore di i1.
31.5 Protocollo di comunicazione tra Pascal e Assembly
Analizziamo ora le regole che permettono a due o più moduli Pascal e Assembly
di comunicare tra loro; l'insieme di queste regole definisce il protocollo di comunicazione
tra Pascal e Assembly.
Un modulo Assembly che fa parte di un programma Pascal può definire le sue
eventuali variabili globali in uno o più blocchi di dati che possono avere caratteristiche
scelte a piacere dal programmatore; tali blocchi, infatti, vengono totalmente ignorati dal
compilatore Pascal.
La conseguenza pratica di tutto ciò è data dal fatto che le variabili globali presenti in
un modulo Assembly risultano invisibili nei moduli Pascal; se proviamo a
dichiarare PUBLIC tali variabili, otteniamo un messaggio di errore da parte del
compilatore Pascal!
Un modulo Assembly che fa parte di un programma Pascal può definire le sue
procedure in uno o più blocchi di codice che possono avere caratteristiche scelte a
piacere dal programmatore; anche in questo caso, però, tali blocchi di codice vengono
ignorati dal compilatore Pascal e non possono dichiarare procedure PUBLIC.
Se vogliamo che le procedure presenti in un modulo Assembly risultino visibili
anche nel modulo principale Pascal o nelle unit Pascal, dobbiamo inserire
le definizioni di tali procedure in un blocco di codice che deve chiamarsi
obbligatoriamente CODE; in generale, le caratteristiche di questo segmento di
codice sono le seguenti:
Tutte le procedure dichiarate PUBLIC in questo blocco di codice, risultano
visibili anche nei moduli Pascal; alternativamente è possibile definire il
blocco CODE codice anche in questo modo:
Ciò significa che se vogliamo utilizzare le caratteristiche avanzate di MASM,
possiamo servirci della direttiva semplificata .CODE; in ogni caso, si tenga
presente che è fondamentale l'utilizzo del nome CODE o _TEXT.
Queste strane regole sono una diretta conseguenza del fatto che il compilatore Turbo
Pascal non genera alcun object file; questo fatto ci obbliga ad effettuare
il linking di un modulo Assembly secondo uno schema imposto dal compilatore stesso.
Il Turbo Pascal utilizza due soli modelli di memoria che equivalgono al modello
SMALL e al modello LARGE; per abilitare il modello LARGE è necessario
selezionare il menu:
Options - Compiler - Force far calls
Alternativamente è anche possibile inserire la direttiva {$F+} all'inizio di tutti
i moduli Pascal; analogamente, la direttiva {$F-} disabilita le FAR call.
Se scriviamo un programma Pascal che fa un uso massiccio dei puntatori, possiamo
imbatterci in messaggi di errore del tipo:
Invalid procedure or function reference
Questo messaggio di errore è legato spesso al fatto che abbiamo disabilitato le FAR
call; per evitare qualsiasi problema, si raccomanda vivamente quindi di abilitare
sempre le FAR call, soprattutto quando si interfaccia il Pascal con
l'Assembly.
Di conseguenza, è importantissimo che tutte le procedure PUBLIC presenti in un modulo
Assembly siano di tipo FAR; se stiamo utilizzando le caratteristiche avanzate
di MASM, dobbiamo servirci necessariamente della direttiva:
.MODEL LARGE, PASCAL
I moduli Pascal che intendono utilizzare le procedure PUBLIC definite nel
blocco CODE di un modulo Assembly, devono dichiarare tali procedure con il
qualificatore external; la sintassi del Turbo Pascal prevede, inoltre, che
sia utilizzata la direttiva {$L nomefile.obj} per indicare in quale object
file si trova la procedura Assembly esterna.
Per ogni object file è necessaria una sola direttiva {$L}; l'object
file deve essere in formato OMF (Object Module Format).
Supponiamo, ad esempio, di aver creato in un modulo ASMMOD.ASM una procedura
PUBLIC denominata AreaCerchio ed avente il seguente prototipo Pascal:
function AreaCerchio(r: double): double;
Nel modulo Pascal che intende utilizzare questa function dobbiamo prima
di tutto inserire la direttiva:
{$L asmmod.obj}
A questo punto, nello stesso modulo Pascal, dobbiamo dichiarare AreaCerchio
come:
function AreaCerchio(r: double): double; external;
Nel caso di una unit Pascal, la precedente dichiarazione deve trovarsi nella sezione
implementation; se vogliamo che AreaCerchio sia visibile anche all'esterno della
unit dobbiamo inserire nella sezione interface la dichiarazione:
function AreaCerchio(r: double): double;
Come si può notare, la dichiarazione inserita nella sezione interface è priva del
qualificatore external.
Tutte le procedure contenute nei moduli Assembly da linkare al Pascal,
devono preservare rigorosamente il contenuto dei registri CS, DS, SS,
SP e BP; se si utilizzano le caratteristiche avanzate di MASM,
all'interno delle procedure dotate di stack frame il contenuto dei registri
SP e BP viene automaticamente preservato dall'assembler.
Tutti gli altri registri sono a completa disposizione del programmatore; per tali registri
quindi non è necessario preservarne il contenuto.
Il Pascal è un linguaggio case-insensitive e quindi i compilatori Pascal
non distinguono tra maiuscole e minuscole; possiamo affermare quindi che in Pascal i
nomi come varword1, VarWord1, VARWORD1, etc, rappresentano tutti lo
stesso identificatore. Se in un modulo Pascal definiamo una variabile chiamata
varword1 e poi proviamo a definire una seconda variabile chiamata VARWORD1,
otteniamo quindi un messaggio di errore da parte del compilatore; in osservanza a queste
regole, al momento di assemblare un modulo Assembly da linkare ad un modulo
Pascal, non dobbiamo assolutamente utilizzare le opzioni come /Cp per il
MASM.
31.6 Esempi pratici
In base alle considerazioni appena esposte e a ciò che abbiamo visto nei precedenti
capitoli, possiamo facilmente scrivere programmi di qualunque complessità, formati da un
numero arbitrario di moduli Pascal e moduli Assembly; vediamo allora un
unico esempio formato complessivamente da 4 moduli distinti denominati:
PASMOD.PAS, ASMMODA.ASM, UNITMOD.PAS e ASMMODB.ASM.
Il modulo PASMOD.PAS ricopre il ruolo di modulo principale del programma e contiene
quindi anche l'entry point e il blocco begin end che rappresenta il blocco
di codice principale; inoltre, il modulo PASMOD.PAS si serve delle procedure definite
nel modulo ASMMODA.ASM.
Il modulo UNITMOD.PAS è una unit contenente una serie di procedure utilizzate
anch'esse da PASMOD.PAS; la unit UNITMOD.PAS a sua volta si serve delle
procedure definite nel modulo ASMMODB.ASM.
Il modulo principale PASMOD.PAS assume l'aspetto mostrato in Figura 31.4.
Prima di tutto notiamo le direttive {$F+} (FAR call abilitate), {SG+}
(set di istruzioni 286), {$N+} (set di istruzioni 287); è presente
anche una direttiva {$L} che richiede il linking di PASMOD.PAS con il modulo
ASMMODA.OBJ.
Il modulo PASMOD.PAS contiene l'intestazione:
Program PascalModule;
Si può affermare quindi che tutto il codice di questo modulo sarà inserito in un segmento
di programma avente le seguenti caratteristiche:
Nella sezione uses vengono incluse le unit Crt e UnitMod; come
vedremo in seguito, l'inclusione di Crt è necessaria per permettere anche al modulo
ASMMODA.ASM di utilizzare le procedure definite in tale unit.
Nella sezione type viene dichiarato un tipo di dato vStrType che rappresenta
un vettore di 10 stringhe formate ciascuna da 8 BYTE; il compilatore, come
sappiamo, assegna in realtà 9 BYTE ad ogni stringa in quanto il primo BYTE
è destinato a contenere la lunghezza della stringa stessa.
Complessivamente, quindi, ogni variabile di tipo vStrType richiede:
10 * 9 = 90 byte di memoria
Successivamente viene dichiarato un tipo di dato recCliente che è un record
formato da un campo codice a 16 bit, da un campo telefono a 32
bit e da un campo nome che è una stringa da 42 BYTE (cioè, 41 BYTE
più il BYTE iniziale contenente la lunghezza della stringa stessa); complessivamente,
una variabile di tipo recCliente richiede:
2 + 4 + 42 = 48 byte di memoria
Le sezioni const e var dichiarano una serie di variabili che verranno
utilizzate dal programma; come sappiamo, tutte queste variabili sono visibili anche
all'esterno del modulo PASMOD.PAS.
Subito dopo l'entry point, viene chiamata una procedura InitScreen definita
nel modulo ASMMODA.ASM; questa procedura richiede due parametri di tipo Byte
che vengono utilizzati per inizializzare lo schermo con un colore di sfondo (bgColor)
e un colore di primo piano (fgColor).
Le costanti predefinite come Yellow, Blue, etc, vengono create nella unit
Crt; a tale proposito, si può consultare il file CRT.INT presente nella cartella
DOC del Turbo Pascal.
Il compito successivo svolto dal modulo principale consiste nell'inizializzare una variabile
recCli2 di tipo recCliente; questa variabile viene poi utilizzata per
inizializzare un'altra variabile recCli1 sempre di tipo recCliente.
A tale proposito, viene chiamata una procedura InitRecCli definita nel modulo
ASMMODA.ASM; come si può notare, InitRecCli richiede due argomenti di tipo
puntatore a recCliente.
Il parametro prc1 punta al record destinazione, mentre il parametro prc2
punta al record sorgente; per dimostrare che InitRecCli ha veramente copiato
recCli2 in recCli1, vengono visualizzati tutti i campi dello stesso record
recCli1.
Nella chiamata:
InitRecCli(@recCli1, @recCli2);
vengono passati direttamente gli indirizzi dei due argomenti; tutto ciò equivale a
scrivere:
InitRecCli(Addr(recCli1), Addr(recCli2));
Se vogliamo passare indirettamente gli indirizzi dei due argomenti, dobbiamo servirci
delle variabili puntatore; definendo due variabili Ptr1 e Ptr2 di tipo
Pointer, dopo aver fatto puntare Ptr1 a recCli1 e Ptr2 a
recCli2 possiamo scrivere semplicemente:
InitRecCli(Ptr1, Ptr2);
L'ultimo compito svolto dal modulo principale consiste nel chiamare le due procedure
UnitProcedure e AreaCerchio definite nella unit UnitMod; la procedura
UnitProcedure richiede due argomenti di tipo puntatore a vStrType.
Il contenuto della locazione puntata dal secondo puntatore (sorgente) viene copiato nella
locazione puntata dal primo puntatore (destinazione); anche in questo caso, per dimostrare
che UnitProcedure ha effettivamente copiato vColors in vCol, vengono
visualizzate tutte le 10 stringhe che formano lo stesso vCol.
La procedura AreaCerchio richiede un Double come argomento e restituisce un
altro Double; il valore restituito rappresenta l'area di un cerchio avente come raggio
l'argomento passato a AreaCerchio.
Per sapere come lavorano le due procedure InitScreen e InitRecCli, esaminiamo
il modulo ASMMODA.ASM che assume l'aspetto mostrato in Figura 31.5.
Bisogna ribadire che è fondamentale l'utilizzo del nome CODE o _TEXT per
il blocco di codice contenente le procedure da rendere visibili nei moduli Pascal;
se non si utilizzano questi nomi, si ottiene un messaggio di errore da parte del
compilatore.
Nel blocco delle direttive EXTRN osserviamo che ASMMODA.ASM è in grado di
vedere tutte le variabili e le procedure globali definite in PASMOD.PAS; inoltre,
ASMMODA.ASM può anche vedere tutte le variabili e le procedure globali definite
nelle varie units del Turbo Pascal.
Notiamo, infatti, che ASMMODA.ASM utilizza tre procedure appartenenti alla unit
Crt; a tale proposito, il modulo PASMOD.PAS deve richiedere, nella sezione
uses, l'inclusione di Crt nel programma.
Come abbiamo visto in precedenza, il primo compito svolto dal modulo PASMOD.PAS
consiste nell'effettuare la chiamata:
InitScreen(Blue, Yellow);
Ricordando che il Pascal passa gli argomenti a partire dal primo e tenendo conto
della FAR call, possiamo affermare che il secondo parametro fgColor viene
a trovarsi a BP+6, mentre il primo parametro bgColor viene a trovarsi a
BP+8; questi due parametri sono di tipo BYTE ma, ovviamente, ciascuno di
essi è inserito nello stack negli 8 bit meno significativi di una WORD.
La procedura InitScreen svolge il proprio lavoro chiamando in successione le tre
procedure TextBackground, TextColor e ClrScr definite nella unit
Crt; per conoscere i prototipi di queste procedure si può consultare l'help in linea
del Turbo Pascal.
Nel caso, ad esempio, di TextBackground abbiamo:
procedure TextBackground(Color: Byte);
L'unico argomento richiesto da TextBackground è di tipo Byte; naturalmente,
in un modulo Assembly è compito del programmatore passare a TextBackground
un argomento a 16 bit con gli 8 bit più significativi che, in genere,
valgono 0.
Bisogna anche ricordare che, nel rispetto delle convenzioni Pascal, la pulizia
dello stack viene delegata alla procedura chiamata; nel nostro caso, TextBackground
provvederà a togliere 2 byte dallo stack.
Prima di terminare, InitScreen visualizza la stringa strPas attraverso la
procedura WriteString; sia strPas che WriteString vengono definite
nel modulo PASMOD.PAS.
Il prototipo di WriteString è:
procedure WriteString(var s: String; d: Word);
Il parametro s è l'indirizzo di una stringa, mentre il parametro d è la
colonna dello schermo nella quale verrà stampato l'ultimo carattere della stringa; nel
nostro caso la stringa viene visualizzata in modo che termini a 62 caratteri di
distanza dal bordo sinistro dello schermo.
Siccome stiamo lavorando con i puntatori FAR, la procedura WriteString deve
ricevere l'indirizzo completo Seg:Offset di strPas; come al solito, è
importantissimo ricordare che la componente Seg deve essere inserita per prima nello
stack.
Osserviamo, inoltre, che nel rispetto delle convenzioni Pascal, gli argomenti da
passare a WriteString vengono inseriti nello stack a partire dal primo; infatti,
prima viene inserito l'indirizzo FAR di strPas e poi viene inserito il valore
immediato 62 (a 16 bit).
La seconda procedura chiamata da PASMOD.PAS è InitRecCli che richiede come
argomenti due puntatori a record di tipo recCliente; questa procedura copia
il record puntato da prc2 (sorgente) nel record puntato da prc1
(destinazione).
Anche in questo caso osserviamo subito che abbiamo a che fare con puntatori FAR
contenenti ciascuno una coppia Seg:Offset da 16+16 bit; di conseguenza, il
parametro prc2 si viene a trovare a BP+6, mentre il parametro prc1
si viene a trovare a BP+10.
Per gestire questi due parametri vengono utilizzati i registri DS:SI come sorgente
e ES:DI come destinazione; in questo modo possiamo utilizzare REP MOVSB per
effettuare la copia ad altissima velocità.
È importante ricordarsi di utilizzare l'istruzione CLD in modo da abilitare
l'incremento automatico dei puntatori SI e DI; il contatore CX
contiene il valore 48 che, come abbiamo visto in precedenza, è la dimensione in byte
dei record di tipo recCliente.
Notiamo che 48 è divisibile per 4 (48 / 4 = 12) per cui, all'interno
della procedura InitRecCli possiamo caricare il valore 12 in CX e
scrivere l'istruzione:
rep movsd
Questa istruzione, come sappiamo, è mediamente 4 volte più veloce dell'analoga
istruzione che utilizza MOVSB!
Tutto ciò dimostra che nonostante il Turbo Pascal sia un compilatore a 16 bit,
nei moduli Assembly possiamo sfruttare ugualmente il set di istruzioni a 32
bit; questo è un ulteriore motivo per interfacciare il Turbo Pascal con
l'Assembly.
Per concludere l'analisi di InitRecCli, osserviamo che questa procedura modifica il
registro DS, per cui è importantissimo preservarne il contenuto originale; in caso
contrario, al termine di InitRecCli il programma si pianta.
La procedura InitRecCli ha ricevuto due argomenti da 4 byte ciascuno e quindi
deve terminare rimuovendo 8 byte dallo stack.
Le ultime due procedure chiamate dal modulo principale PASMOD.PAS, sono
UnitProcedure e AreaCerchio; queste procedure vengono definite nel modulo
UNITMOD.PAS. Per capire il funzionamento di UnitProcedure e di
AreaCerchio analizziamo, in Figura 31.6, il contenuto della unit UnitMod.
Anche UNITMOD.PAS specifica le direttive {$F+}, {$G+} e {$N+};
è presente, inoltre, la direttiva {$L} che richiede il linking con il modulo
ASMMODB.OBJ il quale contiene diverse procedure utilizzate da UNITMOD.PAS.
L'intestazione della unit è:
unit UnitMod;
Come sappiamo, il nome della unit deve coincidere con il nome del file contenente
la unit stessa; nel nostro caso il compilatore produrrà una unit memorizzata
in un file chiamato UNITMOD.TPU.
In base all'intestazione che abbiamo utilizzato, tutto il codice di UNITMOD.PAS
viene inserito in un segmento di programma avente le seguenti caratteristiche:
La sezione interface contiene le definizioni delle variabili globali pubbliche e
le dichiarazioni di tutte le procedure pubbliche fornite da questa unit; la sezione
implementation contiene le definizioni delle variabili globali private e le
definizioni di tutte le procedure pubbliche e private fornite da questa unit.
Come già sappiamo, i moduli Pascal che usano UnitMod sono in grado di vedere
solo gli identificatori presenti nella sezione interface di questa unit; i
moduli Assembly facenti parte del programma possono, invece, vedere tutti gli
identificatori globali, pubblici e privati presenti in UnitMod.
La prima procedura di UNITMOD.PAS chiamata da PASMOD.PAS è
UnitProcedure; il prototipo di questa procedura è il seguente:
procedure UnitProcedure(vs1, vs2: ptvStrType);
Questa procedura richiede quindi due argomenti di tipo puntatore a vStrType; il
contenuto dell'area di memoria puntata da vs2 (sorgente), viene copiato nell'area
di memoria puntata da vs1 (destinazione).
Per svolgere tale lavoro viene chiamata una apposita procedura InitvColors che viene
definita nel modulo ASMMODB.ASM; prima di chiamare InitvColors, la procedura
UnitProcedure mostra in pratica un esempio di chiamata ad una procedura che riceve
come argomento un'altra procedura.
A tale proposito, viene chiamata la procedura StringLength definita sempre nel modulo
ASMMODB.ASM; si tratta di una function che riceve come argomento l'indirizzo
di una procedura di tipo ptPrcType e restituisce poi la lunghezza della stringa
strUnit definita in UNITMOD.PAS.
Per analizzare il funzionamento di StringLength e di InitvColors dobbiamo
fare riferimento al modulo ASMMODB.ASM; questo modulo presenta l'aspetto mostrato
in Figura 31.7.
La procedura StringLength presenta il seguente prototipo:
function StringLength(ptp: ptPrcType): Integer;
Questa procedura riceve l'indirizzo Seg:Offset di un'altra procedura; in particolare,
nel modulo PASMOD.PAS notiamo la chiamata:
StringLength(WriteString2);
All'interno di StringLength il parametro ptp rappresenta quindi l'indirizzo
Seg:Offset di WriteString2; la chiamata di WriteString2 è di tipo
indiretto intersegmento, per cui è rappresentata dall'istruzione:
call dword ptr ptp
Come si può notare, WriteString2 riceve come argomento la coppia Seg:Offset
della stringa strUnit definita in PASMOD.PAS; a questo punto WriteString2
provvede a visualizzare strUnit chiamando WriteLn.
Il compito successivo svolto da StringLength consiste nel caricare in AX il
suo valore di ritorno rappresentato dalla lunghezza di strUnit e cioè, dal valore a
8 bit contenuto in strUnit[0].
Come si può notare, l'istruzione MOVZX azzera gli 8 bit più significativi di
AX; prima di terminare, StringLength toglie dallo stack 4 byte che
rappresentano la dimensione della coppia Seg:Offset ricevuta come argomento.
La procedura UnitProcedure riceve in AX la lunghezza di strUnit; per
dimostrarlo notiamo che UnitProcedure chiama WriteLn per visualizzare il
valore di ritorno di StringLength.
La seconda procedura chiamata da UnitProcedure è InitvColors; tale procedura
copia un dato di tipo vStrType in un altro dato di tipo vStrType.
Nel modulo ASMMODB.ASM possiamo notare che InitvColors è del tutto simile
alla procedura InitRecCli definita nel modulo ASMMODA.ASM; ciò dimostra che
in Assembly possiamo gestire allo stesso modo un puntatore a recCliente e un
puntatore a vStrType senza avere tra i piedi il controllo di tipo dei linguaggi di
alto livello.
È chiaro, infatti, che in ogni caso, un puntatore FAR non è altro che una coppia
Seg:Offset; in pratica, potremmo riunire InitvColors e InitRecCli in
un'unica procedura che riceve come terzo argomento la dimensione in byte dei blocchi da
copiare.
In relazione a InitvColors osserviamo, inoltre, che i dati di tipo vStrType
occupano ciascuno 90 byte di memoria; invece di usare MOVSB con CX=90,
possiamo allora utilizzare MOVSW con CX=45.
Il programma termina con la chiamata da parte di PASMOD.PAS della procedura
AreaCerchio dichiarata in UNITMOD.PAS e definita in ASMMODB.ASM; si
tratta di una function che riceve come argomento il raggio r di un cerchio e
restituisce come valore di ritorno l'area del cerchio di raggio r.
Come si può notare in ASMMODB.ASM, questi calcoli vengono effettuati con la
FPU; rispetto all'esempio presentato nel precedente capitolo, l'unica novità è
rappresentata dall'uso delle istruzioni FST op e FLDPI.
L'operando op di FST può essere anche un altro registro della FPU;
il numero irrazionale ℼ = 3.14159267... in ST(0) è in formato
Temporary Real a 80 bit.
Per il calcolo dell'area del cerchio viene utilizzata, ovviamente, la formula:
Area = ℼ * r2
Al termine del calcolo il risultato ottenuto è a disposizione del caller nel registro
ST0 della FPU; per dimostrarlo, nel modulo PASMOD.PAS viene
visualizzato con WriteLn il valore di ritorno di AreaCerchio.
Osserviamo, infine, che AreaCerchio ha ricevuto come argomento un Double il
quale occupa 8 byte nello stack (QWORD); di conseguenza, AreaCerchio
deve terminare passando il valore immediato 8 all'istruzione RET.
31.6.1 Generazione dell'eseguibile
Come è stato spiegato in precedenza, la fase di generazione dell'eseguibile è condizionata
dalle regole imposte dal compilatore Turbo Pascal; in generale, il procedimento da
seguire si sviluppa nelle seguenti fasi:
- assemblaggio dei moduli Assembly
- compilazione delle unit Pascal
- compilazione del modulo principale Pascal
- generazione dell'eseguibile finale
È importantissimo seguire quest'ordine per soddisfare tutte le dipendenze relative agli
identificatori presenti nei vari moduli.
Per quanto riguarda l'assemblaggio dei moduli Assembly con MASM, è
fondamentale ricordarsi che gli object file prodotti dall'assembler devono essere
in formato OMF; con il MASM32 non bisogna utilizzare quindi l'opzione
/coff. Inoltre, l'assemblaggio deve essere case-insensitive e quindi non
bisogna utilizzare opzioni come /Cp.
La fase di compilazione dei moduli Pascal si può svolgere dall'interno
dell'IDE fornito dalla Borland; dal prompt del DOS bisogna
eseguire:
C:\TP\BIN\TURBO.EXE
A questo punto possiamo selezionare il menu:
Compile
Alternativamente è anche possibile utilizzare il compilatore a linea di comando; questo
strumento è disponibile nella cartella BIN del Turbo Pascal ed è
rappresentato dal file TPC.EXE. Digitando TPC dalla linea di comando e
premendo [Invio] si ottiene l'elenco completo delle opzioni di configurazione del
compilatore.
Partendo con la fase di assemblaggio, con il comando:
ml /c asmmoda.asm
generiamo il moduloASMMODA.OBJ;
con il comando:
ml /c asmmodb.asm
generiamo il modulo ASMMODB.OBJ.
A questo punto, dall'interno dell'IDE procediamo alla configurazione del
compilatore; attraverso il menu:
Options - Compiler
dobbiamo abilitare l'uso delle istruzioni dell'80286, l'uso della FPU 80287
e l'uso delle FAR call; tutte queste opzioni possono essere specificate direttamente
nei moduli Pascal grazie, rispettivamente, alle direttive {$G+}, {$N+}
e {$F+}.
Attraverso il menu:
Compiler - Destination Disk
chiediamo al Turbo Pascal di generare l'eseguibile su disco; in caso contrario,
l'eseguibile viene generato solamente in memoria!
Terminata la fase di configurazione, attiviamo con il mouse la finestra contenente il
modulo UNITMOD.PAS e selezioniamo il menu:
Compile - Compile
In questo modo otteniamo una unit memorizzata nel file UNITMOD.TPU.
Attiviamo ora con il mouse la finestra contenente il modulo PASMOD.PAS e
selezioniamo il menu:
Compile - Compile
In questo modo otteniamo un modulo intermedio in formato P-Code che, come è stato
spiegato in precedenza, viene nascosto al programmatore. L'ultima fase consiste nel
selezionare il menu:
Compile - Make
che produce l'eseguibile finale chiamato PASMOD.EXE.
Se vogliamo lavorare solo dalla linea di comando, dopo l'assemblaggio dei due moduli
Assembly possiamo impartire i comandi:
tpc unitmod.pas
e:
tpc -GD pasmod.pas
(l'opzione -GD richiede al Turbo Pascal la generazione di un dettagliato
map file a cui sarà assegnato il nome PASMOD.MAP).
Si tenga presente che, per il corretto svolgimento delle varie fasi, i moduli
ASMMODA.OBJ, ASMMODB.OBJ, UNITMOD.PAS e PASMOD.PAS devono
trovarsi tutti nella directory di lavoro del Turbo Pascal; può essere anche
necessario aggiungere il percorso C:\TP\BIN alla linea PATH del file
C:\AUTOEXEC.BAT.
31.6.2 Allineamento dei dati al BYTE e alla WORD
Anche il Turbo Pascal permette di selezionare l'allineamento dei dati al BYTE
o alla WORD; in questo modo è possibile ottimizzare l'accesso ai dati da parte delle
CPU con Data Bus formato da 16 o più linee.
Per stabilire il tipo di allineamento per i dati è necessario selezionare il menu:
Options - Compiler
Nella finestra che compare bisogna attivare o disattivare la voce Word align data.
In alternativa si può inserire direttamente nel programma la direttiva {$A+} che
abilita l'allineamento dei dati alla WORD; viceversa, la direttiva {$A-}
disabilita ogni forma di allineamento, in modo che i dati vengano disposti in memoria
in modo consecutivo e contiguo.
Consideriamo, ad esempio, il seguente blocco dati di un programma Pascal:
In presenza della direttiva {$A-}, la variabile i1 a 16 bit viene
disposta all'offset 0000h del blocco dati, la variabile b1 a 8 bit
viene disposta all'offset 0002h del blocco dati, mentre il vettore vw1 viene
fatto partire dall'offset 0003h dello stesso blocco dati; in questo caso vw1
parte da un offset dispari e le CPU a 16 bit hanno bisogno di due accessi in
memoria per leggere o scrivere ogni WORD del vettore.
In presenza, invece, della direttiva {$A+}, la variabile i1 a 16 bit
viene disposta all'offset 0000h del blocco dati, la variabile b1 a 8
bit viene disposta all'offset 0002h del blocco dati, mentre il vettore vw1
viene fatto partire dall'offset 0004h dello stesso blocco dati; in questo caso
vw1 parte da un offset pari e le CPU a 16 bit hanno bisogno di un solo
accesso in memoria per leggere o scrivere ogni WORD del vettore.
Nei moduli Pascal tutti questi aspetti vengono gestiti direttamente dal compilatore;
nei moduli Assembly, invece, è compito del programmatore tenere conto dell'attributo
di allineamento che è stato selezionato per i dati EXTRN definiti in un modulo
Pascal.
In generale, è vivamente sconsigliabile scrivere programmi Assembly che cercano di
dedurre in modo empirico l'offset di una variabile; utilizzando, invece, l'istruzione
LEA o l'operatore OFFSET si ottiene questa informazione in modo assolutamente
sicuro ed affidabile.
31.7 Assembly inline
L'enorme successo ottenuto dal Borland Turbo Pascal è dovuto anche alla eccezionale
semplicità ed efficienza che caratterizza il supporto dell'Assembly inline da parte
di questo compilatore; il lavoro del programmatore viene ulteriormente agevolato dal fatto
che il potente editor integrato del Turbo Pascal permette di evidenziare, con una
apposita sintassi a colori, i blocchi di istruzioni Assembly inline inseriti
direttamente nel codice Pascal.
Le istruzioni Assembly inline del Turbo Pascal devono trovarsi all'interno
di un blocco delimitato dalle due parole riservate asm e end; i blocchi di
istruzioni Assembly inline possono comparire anche nel blocco di codice principale
del programma.
Come è stato spiegato nel precedente capitolo, non si può pretendere che l'Assembly
inline abbia la stessa potenza e flessibilità dell'Assembly vero e proprio; in
ogni caso, un programmatore Assembly esperto può fare veramente di tutto, anche
con l'Assembly inline!
Vediamo un semplice esempio pratico rappresentato da una procedura che copia una stringa
sorgente in una stringa destinazione e converte poi la stringa destinazione in maiuscolo;
questo esempio è contenuto nel modulo INLINE.PAS mostrato in Figura 31.8.
La procedura strToUpperCase richiede due argomenti che rappresentano gli indirizzi
Seg:Offset di due dati di tipo String; il primo argomento è la stringa
destinazione, mentre il secondo argomento è la stringa sorgente.
Il lavoro svolto da questa procedura consiste nel copiare s (sorgente) in d
(destinazione); in seguito, la stringa destinazione viene convertita in maiuscolo
attraverso l'algoritmo che già conosciamo. Il valore di ritorno di strToUpperCase è
un Integer che rappresenta la lunghezza della stringa appena convertita.
Come è stato appena spiegato, siccome i due argomenti vengono passati per indirizzo,
d e s rappresentano delle coppie Seg:Offset; l'indirizzo s
(stringa sorgente) viene caricato in DS:SI, mentre l'indirizzo d (stringa
destinazione) viene caricato in ES:DI.
La lunghezza della stringa sorgente e cioè, il contenuto del suo BYTE di indice
0, viene caricato in CX; osserviamo che l'Assembly inline del Turbo
Pascal non supporta le istruzioni a 32 bit, per cui non possiamo utilizzare, ad
esempio, MOVZX per caricare direttamente un valore a 8 bit in CX.
La lunghezza della stringa sorgente viene salvata nella variabile locale Lung che
rappresenta anche il valore di ritorno della procedura; il contenuto di CX viene poi
incrementato di 1 per tenere conto anche del BYTE di indice 0 della
stringa.
A questo punto, la copia di s in d avviene con la solita istruzione REP
MOVSB; nello svolgimento di tale fase è importantissimo ricordarsi di preservare il
contenuto di DS.
Nella fase di conversione di d in maiuscolo possiamo notare che il Turbo Pascal
permette di inserire anche etichette locali all'interno di un blocco asm end;
tali etichette devono essere precedute dal simbolo @@ (doppia chiocciola).
Osserviamo, inoltre, che come al solito, gli eventuali commenti devono seguire la sintassi
del linguaggio di alto livello e non quella dell'Assembly.
Nel precedente capitolo abbiamo visto che in C un qualsiasi vettore viene sempre
passato per indirizzo ad una procedura; il Pascal, invece, permette anche di passare
un intero vettore per valore.
Naturalmente, si tratta di una scelta sconsigliabile in quanto, oltre ad influire
negativamente sull'efficienza di un programma, comporta anche il rischio di saturazione dello
stack; nel caso, ad esempio, del passaggio per valore di un vettore di 200 Integer,
vengono inserite nello stack ben 200 WORD per un totale di 400 byte di dati!
Nel caso della procedura strToUpperCase può capitare che il programmatore voglia
passare la stringa sorgente per valore in modo da evitare che il contenuto originale di
questa stringa possa essere modificato dalla procedura stessa; in un caso del genere il
prototipo di strToUpperCase diventa:
function strToUpperCase(var d: String; s: String): Integer;
Analizziamo ora il significato dei due parametri d e s; il parametro d
rappresenta, come al solito, l'indirizzo completo Seg:Offset della stringa
destinazione (in qualsiasi modello di memoria). La stringa sorgente, invece, è stata passata
per valore, per cui il compilatore ha creato nello stack una copia di tutti i 256 BYTE
della stringa stessa; è chiaro quindi che la copia della stringa sorgente si trova nel
segmento STACK referenziato da SS.
L'offset iniziale di questa stringa è rappresentato, di conseguenza, dall'offset del
parametro s calcolato rispetto a SS; come già sappiamo, per ottenere questa
informazione possiamo usare LEA scrivendo, ad esempio:
lea si, s
A questo punto possiamo accedere alla copia della stringa sorgente tramite SS:SI;
di conseguenza, i vari BYTE della stringa saranno rappresentati da SS:[SI+0],
SS:[SI+1], SS:[SI+2] e così via sino all'ultimo BYTE che sarà
SS:[SI+255].
In sostanza, se la stringa sorgente viene passata per valore, la copia di s in
d può essere ottenuta in questo modo:
All'interno delle procedure che utilizzano l'Assembly inline si sconsiglia vivamente
di utilizzare istruzioni del tipo:
lea si, [bp+6]
Infatti, quando si usa l'Assembly inline, lo stack frame delle procedure viene
gestito direttamente dal compilatore che provvede anche a preservare il contenuto originale
di BP; oltre a preservare BP il compilatore potrebbe anche inserire altre
informazioni nello stack.
Ciò significa che non possiamo essere certi della esatta posizione nello stack dei parametri
e delle variabili locali di una procedura; utilizzando, invece, in modo esplicito i nomi
simbolici associati ai parametri e alle variabili locali di una procedura, possiamo metterci
al riparo da qualsiasi problema.
Se al posto dell'istruzione precedente scriviamo, ad esempio:
lea si, s
siamo sicuri che LEA caricherà in SI l'esatta posizione (offset) di s
all'interno dello stack.
Le stesse considerazioni valgono, ovviamente, per i valori restituiti dalle function;
nella fase di generazione dell'epilog code, infatti, il compilatore potrebbe
sovrascrivere i registri AX e DX che stiamo utilizzando per contenere un
valore di ritorno.
Per evitare problemi, le istruzioni per la restituzione del valore da parte di una
function devono essere scritte in Pascal e non con l'Assembly inline;
nel caso della procedura strToUpperCase possiamo notare che la lunghezza della stringa
da convertire viene salvata nella variabile locale Lung in modo da poter scrivere alla
fine:
strToUpperCase:= Lung;
In questo modo stiamo delegando al compilatore il compito di caricare Lung nel
registro AX.
Un'ultima considerazione riguarda il fatto che anche con l'Assembly inline è proibito
qualsiasi riferimento agli identificatori definiti nella unit System; non è possibile
quindi scrivere istruzioni del tipo:
mov bx, seg WriteLn