Assembly Base con MASM
Capitolo 28: Linking tra moduli Assembly
I numerosi programmi Assembly presentati nei precedenti capitoli, hanno in comune
il fatto che il codice sorgente di ciascuno di essi è interamente contenuto in un unico
file; in questo capitolo, invece, analizzeremo il procedimento da seguire per poter
distribuire il codice sorgente su due o più file.
Nel seguito del capitolo, ciascuno dei file che compongono un programma viene definito
modulo; il problema fondamentale che dobbiamo affrontare consiste allora nello
stabilire i criteri in base ai quali i vari moduli comunicano tra loro.
La possibilità di ripartire un programma su due o più file è molto importante in quanto
ci permette, ad esempio, di creare utili librerie di procedure che possiamo poi linkare
a tutti i nostri programmi che ne hanno bisogno; in questo modo si risparmia parecchio
lavoro in quanto si evita di riscrivere ogni volta tutte le procedure già presenti
all'interno di una libreria.
Un'altra situazione importante è rappresentata dalla eventualità di dover interfacciare
l'Assembly con i linguaggi di alto livello; anche in questo caso, infatti,
dobbiamo essere in grado di far dialogare tra loro moduli Assembly con moduli
C, Pascal, BASIC, etc.
28.1 Programmi Assembly contenuti in un unico file
Per una migliore comprensione dei concetti esposti in questo capitolo, riassumiamo
alcuni importanti aspetti legati alle fasi di assembling e linking di
un programma Assembly il cui codice sorgente è contenuto in un unico file; a
tale proposito, ci serviamo del programma di esempio TESTMAIN.ASM presentato
in Figura 28.1.
Nel blocco CODESEGM di questo programma abbiamo una procedura printStr
la quale utilizza il servizio 09h della INT 21h per visualizzare una
stringa; la procedura printStr segue le convenzioni Pascal e quindi ha
la responsabilità di ripulire lo stack dagli argomenti ricevuti.
Nel blocco DATASEGM vengono definite tre stringhe DOS; nel blocco codice
principale queste tre stringhe vengono passate (per indirizzo) a printStr per
essere visualizzate.
La procedura printStr richiede il solo offset (indirizzo NEAR) della
stringa da visualizzare e presuppone quindi che DS stia referenziando il segmento
di programma in cui è stata definita la stringa stessa; proprio per questo motivo è
importantissimo che il programmatore, prima di chiamare printStr, inizializzi
opportunamente DS. Siccome le tre stringhe vengono definite in DATASEGM,
l'inizializzazione consiste nel porre, ovviamente, DS=DATASEGM.
Osserviamo che nel programma di Figura 28.1 la direttiva ASSUME che associa DS
a DATASEGM è superflua in quanto stiamo accedendo per indirizzo e non per nome ai
dati del programma; bisogna ricordare, infatti, che gli operatori SEG e
OFFSET fanno riferimento direttamente al segmento di appartenenza del loro operando
e non ai SegReg usati per referenziare i segmenti stessi.
Una volta scritto il codice sorgente, possiamo passarlo all'assembler; la prima fase svolta
dall'assembler consiste nel convertire in codice macchina il contenuto di ciascun segmento
di programma. In questa fase, l'assembler determina l'offset e la dimensione in byte di
ogni singolo dato e di ogni singola istruzione; la codifica del blocco DATASEGM,
effettuata dall'assembler, viene illustrata in Figura 28.2.
Convenzionalmente, l'assembler fa partire dal paragrafo 0000h tutti i segmenti
di programma da assemblare; il segmento DATASEGM è allineato al paragrafo per
cui il location counter $ inizia a contare dall'offset 0000h.
L'assembler rileva che il primo dato (stringa1) si trova, appunto, all'offset
0000h ed è un vettore di 20h byte i cui elementi occupano tutti gli offset
compresi tra 0000h e 001Fh; come si può notare, trattandosi di una stringa,
l'assembler converte tutti i caratteri nei corrispondenti codici
ASCII.
Successivamente l'assembler incontra il dato stringa2 e rileva che si tratta di
un vettore di 20h byte che parte dall'offset 0020h; anche stringa2
viene quindi convertita in una sequenza di codici
ASCII. Infine, questo stesso
lavoro viene ripetuto per stringa3 che parte dall'offset 0040h ed è formata
da 20h byte.
L'aspetto importante da ribadire è dato dal fatto che dopo l'assemblaggio, ogni dato
viene convertito in tre parametri fondamentali che sono: l'offset di partenza, il numero
di byte occupati in memoria e il contenuto binario di ogni singolo byte; siccome
DATASEGM è interamente contenuto nel file TESTMAIN.ASM, l'assembler è in
grado di individuare offset, dimensione e contenuto di ogni singolo dato appartenente a
questo blocco. Notiamo, infine, che il blocco DATASEGM occupa, complessivamente,
60h byte di memoria.
Terminato l'assemblaggio di DATASEGM l'assembler incontra il blocco
CODESEGM; l'assemblaggio di CODESEGM produce il risultato mostrato
in Figura 28.3.
Anche in questo caso possiamo notare come l'assembler faccia partire idealmente il blocco
CODESEGM dal paragrafo 0000h della RAM; siccome CODESEGM è
allineato al paragrafo, il location counter $ inizia a contare dall'offset
0000h.
Nella fase di assemblaggio di CODESEGM, l'assembler determina l'offset e il codice
macchina di ciascuna istruzione; ogni riferimento a identificatori di variabili, procedure,
etichette, etc, viene sostituito con il corrispondente indirizzo di memoria. Naturalmente,
tutto ciò è possibile solo se l'assembler conosce già l'indirizzo dell'identificatore; una
situazione del genere si presenta quando l'identificatore viene definito nello stesso file
che l'assembler sta assemblando.
Consideriamo, ad esempio, in Figura 28.3 l'istruzione:
push offset stringa2
In questo caso l'assembler è in grado di determinare che l'offset di stringa2 è
0020h e produce quindi il codice macchina:
68 0020r
La r sta per relocatable (rilocabile) ed è una informazione che l'assembler
passa al linker; come già sappiamo (e come vedremo più avanti), la rilocazione degli
offset si rende necessaria, sia per gestire il corretto allineamento in memoria di un
program segment, sia per gestire il caso di un program segment distribuito
su due o più file.
Consideriamo ora in Figura 28.3 l'istruzione:
mov ax, DATASEGM
per poter convertire questa istruzione in codice macchina l'assembler assegna a
DATASEGM il paragrafo simbolico 0000s. La s sta per segment
e indica il fatto che si tratta, appunto, di un paragrafo simbolico (il MASM
utilizza la r anche per i segmenti rilocabili); il vero paragrafo verrà assegnato
a DATASEGM dal SO al momento di caricare il programma in memoria.
Complessivamente il blocco CODESEGM richiede 2Bh byte di memoria.
L'ultimo segmento di programma incontrato dall'assembler è STACKSEGM; l'assemblaggio
di STACKSEGM produce il risultato mostrato in Figura 28.4.
L'assembler rileva che nel blocco STACKSEGM viene definito un vettore di
400h byte non inizializzati, che occupano tutti gli offset compresi tra
0000h e 03FFh; la dimensione complessiva di STACKSEGM ammonta
quindi a 400h byte (cioè, a 3FFh byte più il byte che si trova
all'offset 0000h).
In seguito alla fase di assemblaggio l'assembler raccoglie tutte le informazioni relative
agli identificatori presenti nel programma; queste informazioni vengono inserite
dall'assembler nella Symbol Table (tabella dei simboli) illustrata in Figura 28.5.
Ad ogni identificatore (symbol) l'assembler associa il tipo e il valore; per le
costanti come STACK_SIZE il tipo è Number. Per i dati come stringa1
il tipo si riferisce ad ogni singolo elemento (Byte, Word, Dword,
etc), mentre il valore indica l'indirizzo logico Seg:Offset del primo elemento;
per le etichette (compresi i nomi di procedure) il tipo si riferisce alla modalità di
indirizzamento (NEAR o FAR), mentre il valore indica l'indirizzo logico
Seg:Offset dell'etichetta stessa. Per le macro alfanumeriche, come strAddr,
viene usato il tipo Text; il valore di una macro alfanumerica non è altro che il
corpo della macro stessa.
La Symbol Table termina con una serie di dettagliate informazioni relative ai vari
segmenti di programma. Nel caso di Figura 28.5 possiamo constatare che, grazie al fatto che
TESTMAIN.ASM è contenuto in un unico file, l'assembler ha potuto determinare in modo
completo tutte le caratteristiche di ogni singolo identificatore.
A questo punto l'assembler genera il file TESTMAIN.OBJ (object file) contenente il
codice macchina di TESTMAIN.ASM e la relativa Symbol Table; tutte le
informazioni contenute in TESTMAIN.OBJ possono essere ora passate al linker.
Nello svolgere il proprio lavoro il linker segue un comportamento ben definito; se
impartiamo, ad esempio, il comando:
link FILE1.OBJ + FILE2.OBJ + FILE3.OBJ
questi tre file vengono esaminati dal linker nello stesso ordine da noi stabilito.
In assenza di diverse indicazioni da parte del programmatore, il linker rispetta la
disposizione dei segmenti da noi stabilita (disposizione sequenziale); più avanti
vengono illustrate alcune tecniche necessarie per stabilire i criteri di ordinamento
dei segmenti di programma.
Il nome assegnato all'eseguibile finale è quello del primo modulo esaminato dal linker;
nel nostro caso otteniamo un eseguibile chiamato FILE1.EXE.
Nell'esempio di Figura 28.1, esiste un unico object file che è TESTMAIN.OBJ; in un
caso del genere il lavoro svolto dal linker viene notevolmente semplificato.
Il linker inizia ad esaminare per primo il blocco DATASEGM che ha combinazione
PUBLIC e classe 'DATA'; non esistendo altri blocchi con queste tre
caratteristiche, il segmento DATASEGM non viene fuso con altri segmenti e rimane
quindi così com'è.
Il linker cerca poi altri blocchi con classe 'DATA'; non esistendo altri blocchi
del genere, il linker passa direttamente al segmento CODESEGM che ha combinazione
PUBLIC e classe 'CODE'. Anche il blocco CODESEGM non subisce alcuna
fusione e non viene neanche raggruppato con altri blocchi di classe 'CODE'; lo
stesso discorso vale anche per l'ultimo segmento esaminato dal linker e cioè, per
STACKSEGM.
I tre segmenti di programma di Figura 28.1 vengono disposti nell'eseguibile finale secondo
l'ordine imposto dal programmatore e nel rispetto dei singoli attributi di allineamento;
il lavoro complessivo svolto dal linker viene rappresentato simbolicamente dal Map
File di Figura 28.6.
Il primo segmento di programma incontrato dal linker è DATASEGM; come indirizzo
fisico di partenza viene utilizzato simbolicamente 00000h. Siccome DATASEGM
ha l'attributo di allineamento PARA, il linker fa partire questo blocco
dall'indirizzo logico 0000h:0000h; di conseguenza si ottiene DATASEGM=0000h,
mentre l'offset iniziale è 0000h.
Come si può notare, la lunghezza complessiva di DATASEGM è di 60h byte,
pari alla lunghezza già calcolata dall'assembler; questo significa che il blocco
DATASEGM di TESTMAIN.OBJ non è stato fuso con nessun altro blocco e
quindi non c'è stata, da parte del linker, alcuna rilocazione degli offset. In base a
queste considerazioni possiamo dire che DATASEGM parte dall'indirizzo fisico
00000h e termina all'indirizzo fisico 0005Fh.
Il secondo segmento di programma incontrato dal linker è CODESEGM per il quale
abbiamo richiesto l'allineamento al paragrafo; l'indirizzo fisico multiplo di 16
immediatamente successivo a 0005Fh è 00060h. Il linker fa partire quindi
CODESEGM dall'indirizzo logico 0006h:0000h; di conseguenza si ottiene
CODESEGM=0006h, mentre l'offset iniziale è 0000h.
Come si può notare, la lunghezza complessiva di CODESEGM è di 2Bh byte,
pari alla lunghezza già calcolata dall'assembler; questo significa che il blocco
CODESEGM di TESTMAIN.OBJ non è stato fuso con nessun altro blocco e
quindi non c'è stata, da parte del linker, alcuna rilocazione degli offset. In definitiva
possiamo dire che CODESEGM parte dall'indirizzo fisico 00060h e termina
all'indirizzo fisico 0008Ah.
Il terzo segmento di programma incontrato dal linker è STACKSEGM per il quale
abbiamo richiesto l'allineamento al paragrafo; l'indirizzo fisico multiplo di 16
immediatamente successivo a 0008Ah è 00090h. Il linker fa partire quindi
STACKSEGM dall'indirizzo logico 0009h:0000h; di conseguenza si ottiene
STACKSEGM=0009h, mentre l'offset iniziale è 0000h.
Come si può notare, la lunghezza complessiva di STACKSEGM è di 400h byte,
pari alla lunghezza già calcolata dall'assembler; questo significa che il blocco
STACKSEGM di TESTMAIN.OBJ non è stato fuso con nessun altro blocco e
quindi non c'è stata, da parte del linker, alcuna rilocazione degli offset. In definitiva
possiamo dire che STACKSEGM parte dall'indirizzo fisico 00090h e termina
all'indirizzo fisico 0048Fh.
Naturalmente, il linker ricava dalla Symbol Table tutte le informazioni relative
ai vari segmenti di programma e agli identificatori; in questo caso particolare, l'intero
programma è contenuto nel file TESTMAIN.OBJ, per cui il linker ha dovuto svolgere
un lavoro relativamente semplice.
Il passo finale compiuto dal linker consiste nella generazione del file eseguibile
TESTMAIN.EXE; come è stato spiegato nel Capitolo 13, un file eseguibile in
formato EXE è composto da una intestazione (header) seguita dal programma
vero e proprio. All'interno dell'intestazione sono presenti importanti informazioni
destinate al SO come, ad esempio, le informazioni di rilocazione relative ai vari
segmenti di programma; in questo modo il linker dice al SO che DATASEGM
parte da 0000h:0000h, CODESEGM parte da 0006h:0000h e STACKSEGM
parte da 0009h:0000h.
Osservando la Figura 28.6 si può notare che l'entry point (etichetta start)
si trova all'indirizzo logico CODESEGM:0000h=0006h:0000h; di conseguenza, il linker
pone nell'header: InitialCS=0006h e InitialIP=0000h.
Il linker nota anche la presenza di un segmento di programma STACKSEGM=0009h con
attributo di combinazione STACK e con lunghezza pari a 400h byte; di
conseguenza, il linker pone nell'header: InitialSS=0009h e
InitialSP=0400h.
Il compito di caricare TESTMAIN.EXE in memoria spetta al loader (caricatore)
del SO; prima di tutto il loader richiede due blocchi di memoria che, come
sappiamo, vengono chiamati Environment Segment e Program Segment. In
particolare, nel Program Segment vengono inseriti i 256 byte del PSP,
immediatamente seguiti dal programma vero e proprio; nel caso di TESTMAIN.EXE il
programma è formato dai tre blocchi visibili in Figura 28.6.
I blocchi di memoria forniti dal DOS sono sempre allineati al paragrafo; supponiamo
allora che il DOS fornisca al loader un Program Segment che parte
dall'indirizzo fisico 0AC20h e cioè, dall'indirizzo logico 0AC2h:0000h.
Il loader procede a questo punto alla rilocazione dei segmenti di programma di
TESTMAIN.EXE; i 256 byte (100h byte) del PSP occupano tutti
gli indirizzi fisici compresi tra 0AC20h e 0AD1Fh, per cui il programma vero
e proprio partirà dall'indirizzo fisico 0AD20h e cioè, dall'indirizzo logico
0AD2h:0000h. I registri DS e ES vengono inizializzati con il paragrafo
di memoria assegnato al PSP, per cui si ottiene:
DS = ES = PSP = 0AC2h
Il registro CS viene inizializzato con il valore rilocato di InitialCS; si
ottiene quindi (Figura 28.5):
CS = InitialCS + 0AD2h = 0006h + 0AD2h = 0AD8h
Il registro IP viene inizializzato con il valore di InitialIP; si ottiene
quindi:
IP = InitialIP = 0000h
Il registro SS viene inizializzato con il valore rilocato di InitialSS; si
ottiene quindi (Figura 28.5):
SS = InitialSS + 0AD2h = 0009h + 0AD2h = 0ADBh
Il registro SP viene inizializzato con il valore di InitialSP; si ottiene
quindi:
SP = InitialSP = 0400h
28.1.1 Eseguibili in formato COM
Per le fasi di assembling e linking di un eseguibile in formato COM contenuto in
un unico file, valgono tutte le considerazioni appena esposte per il formato EXE;
come sappiamo, esistono solo alcune differenze di carattere formale.
Il programma deve avere un unico program segment che, eventualmente, può essere
spezzato in due o più parti; tutte le parti devono condividere lo stesso nome, lo stesso
attributo di combinazione (diverso da PRIVATE) e lo stesso attributo di classe.
L'entry point deve trovarsi rigorosamente all'offset 256 dell'unico
program segment; come sappiamo, il SO inserisce in questi 256 byte
il PSP.
Un file eseguibile in formato COM, generato dal linker, contiene esclusivamente il
programma vero e proprio; non esistendo alcun header, non è permessa la presenza
di istruzioni che fanno riferimento a nomi di segmenti rilocabili.
In fase di caricamento in memoria, il SO assegna ad un eseguibile in formato
COM un intero segmento di memoria (65536 byte); i primi 256 byte
di tale segmento di memoria vengono riempiti con il PSP. I registri CS,
DS, ES e SS vengono inizializzati con il paragrafo di memoria
da cui parte il PSP; il registro IP viene inizializzato con 0100h,
mentre il registro SP viene inizializzato con FFFEh.
28.2 Programmi Assembly distribuiti su due o più file
Nell'esempio TESTMAIN.ASM e nei numerosi altri esempi presentati nei precedenti
capitoli, abbiamo utilizzato molto spesso il servizio n. 09h (Display String)
della INT 21h che ci permette di visualizzare facilmente una stringa DOS; la
Figura 28.7 riassume le caratteristiche di questo servizio.
Anziché riscrivere ogni volta una procedura come la printStr di Figura 28.1 (che
sfrutta proprio il servizio Display String), possiamo pensare di inserire tale
procedura in un file di libreria, linkabile a tutti i programmi che ne hanno bisogno.
A tale proposito, analizziamo un esempio pratico nel quale è presente anche la procedura
setCursor che sfrutta il servizio n. 02h (Set Cursor Position) della
INT 10h (Video BIOS Services); attraverso tale servizio, le cui caratteristiche
vengono illustrate dalla Figura 28.8, possiamo posizionare il cursore (prompt) in un punto
qualsiasi dello schermo.
Per il momento è sufficiente sapere che nel registro BH va inserito il valore
00h che rappresenta la pagina video predefinita; informazioni dettagliate sulle
interruzioni hardware e software vengono esposte nella sezione Assembly Avanzato.
Affinché possano essere linkate a qualsiasi programma, le due procedure printStr
e setCursor devono possedere determinate caratteristiche; in particolare, le
due caratteristiche più importanti sono la generalità e l'indipendenza.
La generalità indica il fatto che una procedura può essere linkata ad un
programma qualsiasi senza che si renda necessaria alcuna modifica alla struttura (corpo
e lista dei parametri) della procedura stessa; l'indipendenza indica il fatto che
il comportamento di una procedura è legato solamente all'algoritmo contenuto nel suo
corpo e non dipende quindi da "fattori esterni" (come, ad esempio, variabili definite
esternamente alla procedura).
Nell'esempio che stiamo per analizzare, il programma principale si trova in un file
chiamato TESTMAIN.ASM; le due procedure printStr e setCursor si
trovano, invece, in un altro file chiamato TESTLIB1.ASM.
In entrambi i file, il codice viene inserito in un apposito blocco chiamato
CODESEGM, con allineamento PARA e classe 'CODE'; in questo modo, le
procedure presenti in TESTLIB1.ASM possono essere chiamate da TESTMAIN.ASM
attraverso NEAR calls.
Partiamo allora dalla procedura setCursor; tale procedura viene organizzata in
modo che richieda due argomenti e cioè, la riga e la colonna in cui posizionare il
cursore. La procedura setCursor assume allora il seguente aspetto:
La procedura setCursor segue le convenzioni Pascal, per cui i suoi
parametri vengono inseriti nello stack a partire dal primo e vengono quindi a trovarsi
disposti in ordine inverso rispetto alla lista mostrata dal prototipo; tali due
parametri occupano 2 byte ciascuno e quindi setCursor, prima di terminare,
deve "restituire" 4 byte allo stack.
I parametri riga e colonna sono entrambi a 16 bit in quanto non
possiamo inserire valori a 8 bit nello stack; naturalmente, all'interno di
setCursor vengono utilizzati solo gli 8 bit meno significativi di ciascun
parametro. Osserviamo, infatti, che una istruzione del tipo:
mov dh, riga
viene espansa dall'assembler in:
mov dh, [bp+6]
Tale istruzione dice chiaramente alla CPU di caricare in DH un valore a
8 bit che si trova all'indirizzo SS:(BP+6); i successivi 8 bit del
parametro riga vengono quindi ignorati.
Passiamo ora alla procedura printStr che è in grado di stampare una stringa in
un punto preciso dello schermo; a tale proposito, printStr si serve proprio di
setCursor. La struttura di printStr, in versione Pascal, assume
allora il seguente aspetto:
Anche printStr segue le convenzioni Pascal; per questa procedura valgono
quindi le stesse considerazioni già esposte per setCursor. In particolare, è
importante notare la chiamata di setCursor effettuata da printStr (chiamata
innestata); gli argomenti vengono passati a partire dal primo, mentre la pulizia dello
stack viene delegata a setCursor.
Osserviamo che printStr richiede solo la componente Offset dell'indirizzo
logico in cui si trova la stringa da visualizzare; ciò significa che printStr si
aspetta che DS contenga già la componente Seg dell'indirizzo stesso!
A questo punto, possiamo procedere con la separazione del programma TESTMAIN.ASM
di Figura 28.1, in due moduli chiamati TESTMAIN.ASM e TESTLIB1.ASM; il file
TESTMAIN.ASM contiene il blocco dati del programma (DATASEGM), il blocco
stack (STACKSEGM) e un blocco codice (CODESEGM) in cui si trova l'entry
point seguito dalle istruzioni principali.
All'interno di TESTLIB1.ASM è presente un altro segmento CODESEGM che
contiene le due procedure setCursor e printStr; queste due procedure
verranno chiamate dal blocco CODESEGM presente in TESTMAIN.ASM.
La situazione appena descritta fa nascere subito un interrogativo: come fanno
TESTMAIN.ASM e TESTLIB1.ASM a comunicare tra loro?
Per rispondere a questa domanda è necessario esporre alcune importanti considerazioni
relative agli identificatori di un programma Assembly.
28.2.1 Le direttive EXTRN e PUBLIC
In assenza di diverse indicazioni fornite dal programmatore, tutti gli identificatori
definiti in un file hanno una visibilità limitata al file stesso; ciò significa che in
un programma formato da due o più moduli, gli identificatori definiti in un modulo non
possono essere visti dagli altri moduli. Di conseguenza, è possibile ridefinire uno
stesso identificatore in moduli diversi senza che si verifichi alcuna interferenza; si
tratta comunque di uno stile di programmazione da evitare in quanto può creare notevole
confusione.
Analizziamo il caso del programma TESTMAIN.ASM mostrato in Figura 28.1; nel blocco
DATASEGM vengono definite staticamente tre stringhe alle quali vengono assegnati
gli identificatori stringa1, stringa2 e stringa3. A ciascuno di
questi tre nomi il linker assegna un indirizzo che rimane fisso (statico) per tutta la
fase di esecuzione di TESTMAIN.EXE; queste tre variabili hanno una visibilità
limitata al file TESTMAIN.ASM ed esistono per tutta la fase di esecuzione di
TESTMAIN.EXE.
Nel blocco CODESEGM vengono definite staticamente due etichette alle quali vengono
assegnati gli identificatori start e printStr; anche questi due
identificatori hanno una visibilità limitata al file TESTMAIN.ASM ed esistono per
tutta la fase di esecuzione di TESTMAIN.EXE.
Nel file TESTMAIN.ASM notiamo anche la presenza degli identificatori STACK_SIZE
(macro numerica creata con =) e strAddr (macro alfanumerica creata con
equ); queste due macro hanno una visibilità limitata al solo file TESTMAIN.ASM
(a partire dal punto in cui vengono create).
Tra l'identificatore di una macro e gli identificatori di variabili, etichette e procedure
esiste però una differenza importante; infatti, come abbiamo visto nel Capitolo 24, una
macro numerica creata con = può essere ridefinita più volte (sempre con la
direttiva =) anche all'interno di uno stesso file. Una macro alfanumerica
definita con equ può essere ridefinita più volte (sempre con la direttiva equ)
all'interno di uno stesso file; una macro numerica definita con equ non
può essere ridefinita all'interno di uno stesso file.
In Figura 28.1, ad esempio, sia la macro numerica STACK_SIZE, sia la macro alfanumerica
strAddr, possono essere ridefinite più volte all'interno di TESTMAIN.ASM; in
un eventuale altro modulo possiamo definire due variabili chiamate STACK_SIZE e
strAddr senza che questi nomi vengano confusi con quelli presenti nel file
TESTMAIN.ASM.
Per chiarire meglio questi importanti concetti passiamo all'implementazione pratica del
nostro programma formato dai due moduli TESTMAIN.ASM e TESTLIB1.ASM;
all'interno del blocco CODESEGM di TESTMAIN.ASM è possibile inserire le
istruzioni per la chiamata (NEAR) delle procedure setCursor e printStr
(definite in un altro blocco CODESEGM del modulo TESTLIB1.ASM).
Per informare l'assembler che queste due procedure si trovano in un file esterno a
TESTMAIN.ASM, dobbiamo utilizzare la direttiva EXTRN; le versioni più recenti
di MASM supportano anche la sintassi EXTERN per questa direttiva.
La direttiva EXTRN richiede una lista di argomenti separati da virgole; ogni
argomento è formato dal nome dell'identificatore esterno, da un due punti (:) e
dal tipo dell'identificatore esterno. Il tipo di un identificatore è lo stesso specificato
dall'assembler nella colonna Type della Symbol Table; ad esempio:
- il tipo di una etichetta esterna o di una procedura esterna non è altro che la
modalità di indirizzamento, NEAR o FAR
- il tipo di una variabile esterna (scalare o vettoriale) è la dimensione di ogni
suo singolo elemento, Byte, Word, Dword, etc.
Nel caso del modulo TESTMAIN.ASM dobbiamo scrivere quindi:
EXTRN setCursor: NEAR, printStr: NEAR
Il punto del programma in cui deve essere inserita la direttiva EXTRN assume
una notevole importanza; infatti, il comportamento di MASM varia a seconda della
posizione di questa direttiva!
Prima di tutto bisogna tenere presente che l'assembler non è in grado, ovviamente, di
determinare l'indirizzo di un identificatore definito esternamente al modulo che sta
assemblando; tale lavoro verrà svolto dal linker durante la fase di unione dei vari
moduli. Di conseguenza, l'assembler assegna ad ogni identificatore esterno un indirizzo
logico simbolico del tipo ----:----; in base, però, alla posizione della direttiva
EXTRN, l'assembler si comporta nel modo seguente:
- se EXTRN è posta all'esterno dei segmenti di programma, l'assembler
delega al linker il compito di individuare l'indirizzo completo ----:----
degli identificatori esterni specificati dalla direttiva
- se EXTRN è posta all'interno di un segmento di programma (ad esempio,
CODESEGM), l'assembler assume che l'indirizzo degli identificatori esterni
specificati dalla direttiva sia del tipo CODESEGM:---- (e lascia quindi al
linker il compito di calcolare la sola componente Offset)
La Figura 28.9 illustra il nuovo aspetto assunto dal modulo TESTMAIN.ASM del nostro
esempio.
Come si può notare, printStr viene chiamata in stile Pascal per cui i tre
argomenti vengono inseriti nello stack a partire dal primo, mentre la pulizia dello stack
spetta alla stessa printStr; è importante osservare anche il fatto che gli argomenti
della direttiva EXTRN, posta all'inizio di CODESEGM, possono essere visti
come veri e propri prototipi delle procedure setCursor e printStr e rendono
quindi superfluo l'uso dell'operatore NEAR PTR nella chiamata delle procedure stesse
(proprio per questo motivo, conviene inserire le direttive EXTRN sempre all'inizio
del segmento di programma di competenza).
Passiamo ora al modulo TESTLIB1.ASM contenente il blocco CODESEGM all'interno
del quale vengono definite le due procedure setCursor e printStr; come è stato
spiegato in precedenza, in assenza di diverse indicazioni da parte del programmatore, questi
due identificatori vengono definiti staticamente in CODESEGM e risultano visibili solo
all'interno del modulo TESTLIB1.ASM.
Per fare in modo che i due identificatori setCursor e printStr risultino visibili
anche all'esterno di TESTLIB1.ASM, dobbiamo provvedere a "renderli pubblici"; a tale
proposito, l'assembler ci mette a disposizione la direttiva PUBLIC.
Questa direttiva richiede una lista di argomenti separati da virgole; ciascun argomento è
costituito dall'identificatore che vogliamo rendere pubblico. Nel caso del modulo
TESTLIB1.ASM dobbiamo scrivere quindi:
PUBLIC setCursor, printStr
Analogamente ad EXTRN, la direttiva PUBLIC può essere posta all'esterno dei
segmenti di programma, oppure all'interno del segmento di programma che contiene le
definizioni degli identificatori da rendere pubblici. Ovviamente, la direttiva
PUBLIC relativa ad un determinato identificatore, deve precedere la definizione
dell'identificatore stesso; in questo modo informiamo l'assembler che un identificatore
definito più avanti deve essere reso pubblico.
In base alle considerazioni appena esposte, il modulo TESTLIB1.ASM assume l'aspetto
mostrato in Figura 28.10.
Analizzando il file TESTLIB1.ASM possiamo notare innanzi tutto che le macro alfanumeriche
riga e colonna vengono prima dichiarate all'interno di setCursor e poi
ridichiarate all'interno di printStr; dall'inizio di setCursor sino all'inizio di
printStr il corpo della macro riga vale [bp+6], mentre da printStr
in poi il corpo della macro riga vale [bp+8].
Se ci si dimentica di ridefinire la macro riga, all'interno di printStr l'assembler
converte il nome riga in [bp+6] e il programma va in crash; ancora una volta quindi
è importante ribadire che in Assembly la minima disattenzione da parte del programmatore
può essere pagata a caro prezzo!
Un altro aspetto importante da notare è che la fine del modulo TESTLIB1.ASM viene
delimitata dalla direttiva END senza alcun argomento aggiuntivo; un eventuale argomento
verrebbe interpretato dall'assembler come l'identificatore di un entry point. In tal caso
l'assembler produce un messaggio di errore per indicare che sono stati individuati due o più
entry point; in sostanza, in un programma formato da due o più moduli, solo uno di essi
(modulo principale) deve contenere l'entry point.
Torniamo per un momento al discorso relativo alla visibilità degli identificatori; nel blocco
CODESEGM del modulo TESTLIB1.ASM potremmo, ad esempio, definire una variabile
denominata stringa1. Questa etichetta non andrebbe in conflitto con l'etichetta
stringa1 di TESTMAIN.ASM in quanto i due identificatori hanno entrambi visibilità
limitata ai moduli di appartenenza; è chiaro che se si fa in modo che l'etichetta stringa1
di TESTLIB1.ASM diventi visibile nel modulo TESTMAIN.ASM, si ottiene un messaggio
di errore da parte dell'assembler.
Ci si può chiedere come facciano l'assembler e il linker a non confondere tra loro questi due
identificatori che, oltretutto, si trovano nello stesso blocco CODESEGM; per ottenere
una risposta a questa domanda è sufficiente analizzare quello che succede nelle fasi di
assembling e di linking del programma.
28.2.2 Assembling di TESTMAIN.ASM e TESTLIB1.ASM
Ora che siamo in possesso dei due moduli TESTMAIN.ASM e TESTLIB1.ASM, possiamo
passare alla fase di assemblaggio; il comando da impartire con il MASM è:
ml /c /Cp /Fl testmain.asm testlib1.asm
L'ordine con il quale i moduli vengono passati all'assembler non ha alcuna importanza; se
viene rilevato un errore in uno dei due moduli, è sufficiente riassemblare solo quel modulo.
L'assemblaggio dei due blocchi DATASEGM e STACKSEGM del modulo TESTMAIN.ASM
produce l'identico risultato visibile in Figura 28.2 e in Figura 28.4; l'assemblaggio del blocco
CODESEGM del modulo TESTMAIN.ASM produce, invece, il risultato visibile in Figura
28.11.
Come si vede in Figura 28.11, l'assembler è in grado di determinare gli offset potenzialmente
rilocabili (r) delle tre variabili stringa1, stringa2 e stringa3;
ciò è possibile in quanto queste tre variabili vengono definite nel modulo TESTMAIN.ASM
appena assemblato.
L'assembler non è, invece, in grado di determinare l'offset di printStr in quanto
questo identificatore non viene definito nel modulo TESTMAIN.ASM; grazie, però, alla
direttiva EXTRN, l'assembler capisce che questo identificatore è una etichetta di
tipo NEAR che viene definita in un altro modulo. Osserviamo, infatti, che nella
chiamata di printStr l'assembler utilizza il codice macchina E8h (chiamata
diretta intrasegmento) seguito dal valore simbolico 0000e; la e significa
proprio extern ed è una informazione che l'assembler passa al linker. In fase di
linking, il linker provvederà a modificare questo valore; tutta questa situazione viene
riassunta dalla Symbol Table visibile in Figura 28.12.
Un dato importante da tenere presente è la dimensione in byte del blocco CODESEGM del
modulo TESTMAIN.ASM; come si vede in Figura 28.11 e in Figura 28.12, tale blocco
richiede 29h byte di memoria.
Notiamo anche gli indirizzi del tipo CODESEGM:---- che l'assembler ha determinato
per printStr e setCursor; come è stato spiegato in precedenza, ciò è una
conseguenza del fatto che abbiamo inserito la direttiva EXTRN in CODESEGM.
Passiamo ora al modulo TESTLIB1.ASM; l'assemblaggio del blocco CODESEGM di
questo modulo produce il risultato visibile in Figura 28.13.
In Figura 28.13 è importante notare i codici macchina delle istruzioni che coinvolgono le due
macro alfanumeriche riga e colonna; questi codici macchina cambiano anche in
base alle varie ridichiarazioni delle macro.
Complessivamente, il blocco CODESEGM del modulo TESTLIB1.ASM richiede 2Ah
byte di memoria; la Figura 28.14 mostra la Symbol Table prodotta dall'assembler per questo
modulo.
Il valore assegnato alle macro alfanumeriche è quello relativo all'ultima ridichiarazione.
28.2.3 Linking di TESTMAIN.OBJ e TESTLIB1.OBJ
A questo punto entra in azione il linker che questa volta deve eseguire un lavoro leggermente
più complesso rispetto al primo esempio del capitolo; in particolare, nel caso di un programma
formato da due o più moduli, diventa importante anche l'ordine con il quale i moduli stessi
vengono passati al linker.
Partiamo dal caso in cui i moduli vengano passati al linker nell'ordine più intuitivo e cioè,
TESTMAIN.OBJ seguito poi da TESTLIB1.OBJ; il comando da impartire con il
MASM è:
link /map testmain.obj + testlib1.obj
Il lavoro svolto dal linker parte con l'analisi dei singoli segmenti di programma; il primo
segmento incontrato dal linker è il blocco DATASEGM di TESTMAIN.OBJ. Siccome
non esiste alcun altro blocco DATASEGM, né in TESTMAIN.OBJ, né in
TESTLIB1.OBJ, il linker produce il seguente risultato:
Questo risultato è identico al caso del blocco DATASEGM di Figura 28.1; naturalmente,
il linker non effettua alcuna rilocazione degli offset presenti in DATASEGM.
Il secondo segmento incontrato dal linker è il blocco CODESEGM di TESTMAIN.OBJ;
il linker trova anche in TESTLIB1.OBJ un blocco avente lo stesso nome, lo stesso
attributo di combinazione e la stessa classe di CODESEGM e procede quindi alla fusione
dei due blocchi.
Il primo blocco CODESEGM occupa 29h byte e richiede un allineamento al paragrafo;
il linker produce quindi il seguente risultato:
Il secondo blocco CODESEGM occupa 2Ah byte e richiede un allineamento al paragrafo;
il linker produce quindi il seguente risultato:
Unendo i due blocchi CODESEGM il linker ottiene un blocco unico CODESEGM che
occupa tutti gli indirizzi fisici compresi tra 00060h e 000B9h; la lunghezza
totale di CODESEGM è quindi:
000B9h - 00060h + 1h = 0005Ah
Come al solito il +1 tiene conto del fatto che gli indici partono da zero; in definitiva,
il blocco finale CODESEGM assume le seguenti caratteristiche:
A questo punto il linker deve procedere alla rilocazione di tutti gli offset presenti nella
seconda parte del blocco CODESEGM; in Figura 28.11 vediamo che la prima parte del blocco
CODESEGM occupa tutti gli indirizzi logici compresi tra 0000h:0000h e
0000h:002Fh.
Dopo 0000h:002Fh, il prossimo indirizzo logico allineato al paragrafo è
0000h:0030h; ciò significa che il linker deve sommare il valore 0030h a tutti
gli offset presenti nella seconda parte del blocco CODESEGM (Figura 28.13). Possiamo
affermare allora che l'offset iniziale 0000h della procedura setCursor diventa
(Figura 28.13):
0000h + 0030h = 0030h
Analogamente, l'offset iniziale 0013h della procedura printStr diventa
(Figura 28.13):
0013h + 0030h = 0043h
Tutto ciò ci permette anche di rispondere alla domanda su come faccia il linker a non
confondere due identificatori definiti con lo stesso nome (visibile localmente) in due
moduli differenti; supponiamo, ad esempio, di definire una seconda etichetta start
all'offset 0000h del blocco CODESEGM di TESTLIB1.ASM. Subito dopo la
fusione dei due blocchi, con la conseguente rilocazione di tutti gli offset del secondo
blocco CODESEGM, le due etichette vengono a trovarsi in due offset differenti e
quindi non possono essere confuse; osserviamo, infatti, che la start di
TESTMAIN.ASM rimane all'offset 0000h, mentre la start di
TESTLIB1.ASM viene rilocata all'offset:
0000h + 0030h = 0030h
In sostanza, dal punto di vista della CPU, la prima start viene vista
come CODESEGM:0000h; la seconda start, invece, viene vista come
CODESEGM:0030h!
Il lavoro svolto dal linker sul blocco finale CODESEGM non è ancora terminato;
rimangono, infatti, da risolvere le istruzioni di chiamata di printStr lasciate
in sospeso dall'assembler. Consideriamo, ad esempio, l'istruzione:
call near ptr printStr
che si trova all'offset 000Ch del blocco CODESEGM di Figura 28.11; il codice
macchina generato dall'assembler è:
E8 0000e
Il linker trova il codice E8h e capisce che si tratta di una chiamata diretta
intrasegmento; subito dopo il linker trova il codice 0000e e dalla e
capisce che questo valore deve essere modificato.
Come già sappiamo, il valore da inserire è quello che sommato all'offset dell'istruzione
successiva alla CALL (indirizzo di ritorno) ci fornisce l'offset della procedura
da chiamare; il linker vede che l'indirizzo di ritorno è 000Fh (Figura 28.11),
mentre l'offset rilocato di printStr è 0043h. Il valore da sostituire a
0000e è quindi:
0043h - 000Fh = 0034h
Di conseguenza, il linker genera il codice macchina definitivo:
E8h 0034h
Il terzo e ultimo segmento di programma incontrato dal linker è il blocco STACKSEGM
di TESTMAIN.OBJ, per il quale abbiamo richiesto un allineamento al paragrafo;
siccome non esiste alcun altro blocco STACKSEGM, né in TESTMAIN.OBJ, né in
TESTLIB1.OBJ, il linker produce il seguente risultato:
Il risultato complessivo prodotto dal linker viene mostrato in Figura 28.15.
Notiamo subito che l'entry point calcolato dal linker è identico a quello di Figura
28.6; questo fatto è abbastanza ovvio in quanto, come si vede in Figura 28.15, il blocco
CODESEGM parte dall'indirizzo fisico 00060h, mentre l'etichetta start
si trova all'offset 0000h di CODESEGM. Il linker, di conseguenza,
nell'header di TESTMAIN.EXE inserisce: InitialCS=0006h e
InitialIP=0000h.
La situazione cambia radicalmente nel momento in cui viene invertito l'ordine di linkaggio dei
due moduli TESTMAIN.ASM e TESTLIB1.ASM; proviamo, infatti, a chiamare il linker
con il comando:
link /map testlib1.obj + testmain.obj
Il nuovo Map File prodotto dal linker viene mostrato in Figura 28.16.
Prima di tutto si nota che il linker questa volta ha prodotto un Map File chiamato
TESTLIB1.MAP e un eseguibile chiamato TESTLIB1.EXE; ciò è dovuto al fatto
che il comportamento predefinito del linker consiste nell'assegnare all'eseguibile il nome
del primo modulo della lista.
Il linker inizia quindi ad esaminare per primo il modulo TESTLIB1.OBJ dove trova il
blocco CODESEGM; questo blocco occupa 2Ah byte di memoria e richiede un
allineamento al paragrafo, per cui si ottiene:
Il linker trova anche in TESTMAIN.OBJ un blocco avente lo stesso nome, lo stesso
attributo di combinazione e la stessa classe di CODESEGM e procede quindi alla
fusione dei due blocchi. Il secondo blocco CODESEGM occupa 2Fh byte e
richiede un allineamento al paragrafo; il linker produce quindi il seguente risultato:
Unendo i due blocchi CODESEGM il linker ottiene un blocco unico CODESEGM che
occupa tutti gli indirizzi fisici compresi tra 00000h e 00058h; la lunghezza
totale di CODESEGM è quindi:
00058h - 00000h + 1h = 00059h
Il +1 tiene conto del fatto che gli indici partono da zero; in definitiva, il blocco
finale CODESEGM assume le seguenti caratteristiche:
A questo punto il linker deve procedere alla rilocazione di tutti gli offset presenti nella
seconda parte del blocco CODESEGM; in Figura 28.13 vediamo che la prima parte del
blocco CODESEGM occupa tutti gli indirizzi logici compresi tra 0000h:0000h e
0000h:002Ah. Dopo 0000h:002Ah, il prossimo indirizzo logico allineato al
paragrafo è 0000h:0030h; ciò significa che il linker deve sommare il valore 0030h
a tutti gli offset presenti nella seconda parte del blocco CODESEGM (Figura 28.11).
Possiamo dire allora che l'offset iniziale 0000h dell'etichetta start diventa
(Figura 28.11):
0000h + 0030h = 0030h
Terminato l'esame di TESTLIB1.OBJ il linker passa a TESTMAIN.OBJ; il primo
blocco che il linker incontra in questo modulo è DATASEGM. Questo blocco occupa
60h byte e richiede un allineamento al paragrafo; siccome non esiste alcun altro
blocco DATASEGM, né in TESTMAIN.OBJ, né in TESTLIB1.OBJ, il linker
produce quindi il seguente risultato:
Il secondo blocco che il linker incontra in TESTMAIN.OBJ è CODESEGM; questo
blocco è stato già fuso con quello presente in TESTLIB1.OBJ per cui il linker passa
direttamente al blocco successivo e cioè a STACKSEGM.
Il blocco STACKSEGM occupa 400h byte e richiede un allineamento al paragrafo;
siccome non esiste alcun altro blocco STACKSEGM, né in TESTMAIN.OBJ, né in
TESTLIB1.OBJ, il linker produce il seguente risultato:
Il risultato complessivo prodotto dal linker e' visibile nel Map File di Figura
28.16; l'inversione dell'ordine di linkaggio dei moduli ha prodotto un eseguibile nel quale
la successione dei vari segmenti di programma è: CODESEGM, DATASEGM,
STACKSEGM.
Un'altra conseguenza è data dal fatto che nel blocco CODESEGM, il codice macchina di
setCursor e di printStr viene inserito prima dell'etichetta start;
infatti, l'offset di start viene rilocato dal linker e diventa 0030h.
Le considerazioni appena esposte giustificano il fatto che questa volta l'entry
point viene posto all'indirizzo logico 0000h:0030h (Figura 28.16); infatti, in base
al lavoro appena svolto dal linker, il blocco CODESEGM parte dall'indirizzo fisico
00000h, mentre l'etichetta start si trova all'offset rilocato 0030h di
CODESEGM. Il linker, di conseguenza, nell'header di TESTLIB1.EXE
inserisce: InitialCS=0000h e InitialIP=0030h.
Se abbiamo la necessità assoluta di disporre i vari segmenti di programma in un determinato
ordine, dobbiamo tenere conto di tutti i concetti appena illustrati; più avanti viene
mostrata una tecnica molto semplice attraverso la quale possiamo imporre al linker la
disposizione dei segmenti di programma.
Un'ultima considerazione riguarda l'eventualità di dover distribuire su più moduli anche
il blocco DATASEGM dell'esempio di Figura 28.9; supponiamo, ad esempio, di lasciare
in un blocco DATASEGM del modulo TESTMAIN.ASM la definizione di
stringa1 spostando, invece, in un blocco DATASEGM del modulo TESTLIB1.ASM
le definizioni di stringa2 e stringa3. Applicando le cose dette in precedenza
possiamo dire che il blocco DATASEGM di TESTMAIN.ASM assume la seguente
struttura:
Il blocco DATASEGM di TESTLIB1.ASM assume, invece, la seguente struttura:
28.3 Programmi in formato COM distribuiti su due o più moduli
Analizziamo ora il caso di un programma in formato COM che vogliamo distribuire su
due o più moduli; in questo caso dobbiamo tenere conto, come sappiamo, di alcune limitazioni
che caratterizzano questo tipo di eseguibili.
Un programma in formato COM è costituito da un unico program segment
contenente codice, dati e stack; questo segmento di programma può essere suddiviso in tante
parti e ciascuna di esse può essere inserita in un modulo differente. In fase di linking del
programma, il linker provvede a fondere queste parti tra loro ottenendo alla fine un segmento
unico; è importante che la dimensione complessiva di questo segmento unico non superi i
65536 byte. Naturalmente, questa limitazione vale anche per i singoli segmenti di
programma di un eseguibile in formato EXE.
L'aspetto più delicato per un programma in formato COM riguarda il fatto che in questo
tipo di eseguibile, l'entry point deve trovarsi tassativamente all'offset 0100h
del segmento unico; tutto ciò significa che nella generazione di un eseguibile in formato
COM, diventa fondamentale l'ordine con il quale i vari moduli vengono passati al linker.
Per chiarire questo aspetto, analizziamo un esempio pratico; in Figura 28.17 vediamo il modulo
principale (semplificato) COMFILE1.ASM di un programma in formato COM.
Come si può notare, il modulo principale COMFILE1.ASM contiene, non solo l'entry
point del programma, ma anche la direttiva ORG; questa direttiva incrementa di
256 byte il location counter $ in modo da creare lo spazio per il PSP
proprio all'inizio di COMSEGM. Di conseguenza, la direttiva start si viene a
trovare all'offset 0100h di COMSEGM.
È importante anche ricordare che in un eseguibile in formato COM il programmatore non
deve inizializzare alcun registro di segmento in quanto questo lavoro spetta al SO;
nel caso del nostro esempio, il SO carica il programma in memoria ponendo:
CS = DS = ES = SS = COMSEGM
(ovviamente, COMSEGM=PSP).
In Figura 28.18 vediamo il modulo COMFILE2.ASM del programma.
Il modulo COMFILE2.ASM non deve contenere alcun entry point, ma è libero
di utilizzare, eventualmente, la direttiva ORG per modificare il location counter
$ all'interno di COMSEGM.
In Figura 28.19 vediamo il modulo COMFILE3.ASM del programma.
Per il modulo COMFILE3.ASM e per altri eventuali moduli valgono le stesse considerazioni
appena esposte per COMFILE2.ASM.
A questo punto possiamo passare alla fase di assemblaggio; come sappiamo, in questa fase
l'ordine con il quale passiamo i vari moduli all'assembler non ha alcuna importanza.
Possiamo impartire, ad esempio, il comando:
ml /c /Cp /Fl comfile3.asm + comfile2.asm + comfile1.asm
In questo modo otteniamo (se non vengono trovati errori) i tre file oggetto COMFILE1.OBJ,
COMFILE2.OBJ e COMFILE3.OBJ; questi tre file devono essere passati al linker per
ottenere l'eseguibile finale in formato COM.
Nella fase di linking, però, diventa importantissimo l'ordine con il quale i vari moduli
vengono passati al linker; come si può facilmente immaginare, siamo obbligati a passare per
primo il modulo principale COMFILE1.OBJ contenente l'entry point all'offset
0100h. Il comando corretto da impartire è quindi:
link /map /tiny comfile1.obj + comfile2.obj + comfile3.obj
oppure:
link /map /tiny comfile1.obj + comfile3.obj + comfile2.obj
Se proviamo a passare per primo COMFILE2.OBJ o COMFILE3.OBJ otteniamo,
ovviamente, un errore del linker; è chiaro, infatti, che in un caso del genere il blocco
COMSEGM contenuto in COMFILE2.OBJ e/o COMFILE3.OBJ verrebbe a
precedere, nell'eseguibile finale, il blocco COMSEGM contenuto in COMFILE1.OBJ.
Di conseguenza, il blocco COMSEGM contenuto in COMFILE1.OBJ subirebbe la
rilocazione di tutti i suoi offset; come risultato finale l'entry point si verrebbe
a trovare ad un offset non valido in quanto (sicuramente) superiore a 0100h.
28.4 Caso generale
Il caso più generale possibile si presenta quando vogliamo distribuire su due o più moduli
un programma dotato di due o più segmenti di dati e/o due o più segmenti di codice; in una
situazione del genere conviene assegnare ad uno dei moduli il ruolo di modulo
principale. Il modulo più adatto per ricoprire questo ruolo è quello che contiene
l'entry point e quindi anche il blocco principale delle istruzioni; oltre
all'entry point, il modulo principale è in genere anche quello che fornisce lo stack
all'intero programma.
Nei moduli secondari vengono distribuite, invece, le varie procedure utilizzate dal modulo
principale; ciascuno dei moduli secondari può essere visto quindi come una libreria di
procedure da linkare al modulo principale.
I moduli secondari più complessi sono spesso dotati di un segmento di codice pubblico, un
segmento di codice privato, un segmento di dati pubblici e un segmento di dati privati;
questo tipo di struttura permette di organizzare al meglio le funzionalità di una libreria.
Il segmento di codice pubblico contiene, ovviamente, le procedure da linkare ai programmi
che ne hanno bisogno; tali procedure, per poter svolgere il proprio lavoro, si servono del
codice e dei dati contenuti nei segmenti privati. I segmenti privati possono essere protetti
attraverso l'attributo di combinazione PRIVATE; questa tecnica permette di nascondere
all'esterno la complessità di una libreria.
I segmenti di dati pubblici di una libreria contengono variabili destinate a fornire al
modulo principale informazioni globali attinenti alla libreria stessa (ad esempio, variabili
destinate a configurare dall'esterno il comportamento di una libreria); tali variabili
prendono il nome di variabili globali della libreria.
Una situazione molto frequente consiste nella eventualità di dover linkare un modulo
principale ad una libreria scritta da un altro programmatore; spesso, questo tipo di
libreria viene fornita sotto forma di object file in quanto lo sviluppatore ha
deciso di non rendere pubblico il codice sorgente.
In un caso del genere, la libreria è sempre accompagnata da una adeguata documentazione;
lo scopo fondamentale della documentazione è quello di fornire tutte le informazioni sul
lavoro svolto dalle procedure della libreria e sull'interfaccia per la chiamata delle
procedure stesse.
Quando una libreria viene fornita solo sotto forma di object code, può capitare
che chi scrive il modulo principale non conosca le caratteristiche dei segmenti di
programma utilizzati dalla libreria stessa; in questo caso, è fondamentale che tutte
le necessarie direttive EXTRN vengano inserite al di fuori di qualsiasi segmento
di programma. Questo argomento viene approfondito nella sezione 28.5
Per illustrare in pratica i concetti appena esposti, riscriviamo l'esempio mostrato nelle
figure 28.9 e 28.10; questa volta utilizziamo le convenzioni del linguaggio C per il
passaggio degli argomenti e per la pulizia dello stack.
Prima di tutto, scriviamo il codice sorgente del modulo secondario chiamato
FILELIB1.ASM; questo modulo contiene la procedura setCursor che può essere
chiamata, se necessario, anche da procedure appartenenti ad altre librerie. Nel modulo
FILELIB1.ASM è presente anche un blocco dati contenente variabili private destinate
all'uso interno della libreria; il modulo FILELIB1.ASM, contenente la procedura
setCursor, viene mostrato in Figura 28.20.
Il modulo FILELIB1.ASM utilizza un blocco dati LIB1DATA contenente variabili
invisibili all'esterno del file in quanto riservate all'uso esclusivo della libreria;
l'attributo di combinazione PRIVATE impedisce che il blocco LIB1DATA venga
fuso con altri blocchi compatibili.
La procedura setCursor questa volta segue le convenzioni C; di conseguenza,
i parametri si trovano disposti nello stack nello stesso ordine indicato dal prototipo. Si
può anche notare che, a causa della FAR call, il primo parametro si trova a
[bp+6]; naturalmente, tutte le procedure di libreria presentate in questo esempio,
sono di tipo FAR in quanto vengono definite in segmenti di codice diversi da quello
utilizzato nel modulo principale.
In Figura 28.21 viene mostrato il modulo secondario FILELIB2.ASM che contiene la
procedura printStr; questa procedura si serve di setCursor per posizionare
il cursore sullo schermo.
Il modulo FILELIB2.ASM contiene un blocco dati LIB2DATA all'interno del
quale vengono definite le tre stringhe strLib2a, strLib2b e strLib2c;
solo la seconda e la terza stringa sono visibili all'esterno in quanto sono state
dichiarate PUBLIC.
Le procedure di FILELIB2.ASM che vogliono accedere per nome alle variabili definite
in LIB2DATA, devono seguire le stesse precauzioni già esposte per setCursor;
in altre parole, tutte le procedure che modificano, ad esempio, DS, ne devono
preservare il contenuto originario.
All'interno della procedura printStr notiamo la chiamata in stile C di
setCursor; infatti, i parametri vengono inseriti nello stack a partire dall'ultimo,
mentre la pulizia dello stack spetta al caller e cioè a printStr.
È importante osservare che il modulo FILELIB2.ASM non conosce le caratteristiche
del segmento di codice di FILELIB1.ASM nel quale viene definita setCursor;
proprio per questo motivo, la necessaria direttiva EXTRN viene posta al di fuori
di qualsiasi segmento di programma (delegando così al linker il compito di determinare
l'indirizzo completo di setCursor).
La Figura 28.22 mostra, infine, il modulo principale del programma; questo modulo prende il
nome di FILEMAIN.ASM e contiene lo stack, l'entry point e il blocco delle
istruzioni principali.
Siccome stiamo lavorando esclusivamente con gli indirizzi logici Seg:Offset delle
stringhe da passare a printStr, l'inizializzazione DS=DATASEGM è superflua;
infatti, gli operatori SEG e OFFSET fanno riferimento direttamente al nome
del segmento di appartenenza del loro operando.
Nel blocco codice principale viene chiamata la procedura printStr per visualizzare
stringhe DOS definite in diversi segmenti di dati; proprio per questo motivo,
printStr ha bisogno dell'indirizzo completo Seg:Offset della stringa da
visualizzare.
Come al solito, è importante ricordare che se si deve passare una coppia Seg:Offset
ad una procedura, la componente Offset deve essere inserita nello stack subito dopo
la componente Seg; in questo modo la coppia Seg:Offset viene disposta nello
stack con la componente Offset che precede la componente Seg (nel rispetto
della convenzione seguita dalle CPU 80x86).
28.5 I file di inclusione (Include file)
Quando si utilizzano le librerie esterne, si presenta spesso una situazione piuttosto
fastidiosa, rappresentata dalla necessità di inserire una valanga di direttive
EXTRN nei programmi che vogliono linkarsi alle librerie stesse; questo problema
può essere risolto in modo efficace, isolando tutte le direttive EXTRN (e
numerose altre direttive) in appositi file, in formato
ASCII, associati alle librerie.
I file così ottenuti vengono poi "inclusi" nel programma che intende linkarsi alle
librerie; proprio per questo motivo, si utilizza la definizione di include file
(file di inclusione).
Analizziamo il caso rappresentato dal precedente esempio FILEMAIN.ASM che comporta
un linking alle due librerie FILELIB1.ASM e FILELIB2.ASM; ciascuna delle
due librerie può essere accompagnata da un apposito include file destinato a
contenere le necessarie direttive EXTRN, ma anche altre informazioni utili (ad
esempio, la documentazione sulle procedure presenti nelle librerie).
Convenzionalmente, un include file utilizza lo stesso nome della libreria a cui è
associato, seguito poi dall'estensione predefinita INC; nel caso allora della
libreria FILELIB1.ASM, si ottiene il file di inclusione mostrato dalla Figura 28.23.
La Figura 28.24, invece, mostra il file di inclusione per la libreria FILELIB2.ASM.
A questo punto, per includere FILELIB1.INC e FILELIB2.INC in FILEMAIN.ASM,
possiamo utilizzare la direttiva INCLUDE già illustrata nel Capitolo 15; nella sezione
di FILEMAIN.ASM riservata alle direttive, possiamo scrivere allora:
Il risultato che si ottiene può essere analizzato attraverso il file FILEMAIN.LST.
Si tenga presente che FILELIB1 e FILELIB2 sono due librerie molto semplici;
nel caso di librerie particolarmente complesse, si può avere a che fare con centinaia di
direttive EXTRN. In una situazione del genere, l'utilizzo dei file di inclusione
si rivela estremamente efficace; infatti, tutto il lavoro che il programmatore deve
svolgere si riduce all'inserimento di una o più direttive INCLUDE nel file che
intende linkarsi alle librerie.
28.5.1 Uso degli include file per l'ordinamento dei segmenti di programma
In fase di linking del programma illustrato nelle figure 28.20, 28.21 e 28.22, l'ordine
con il quale i tre file oggetto FILEMAIN.OBJ, FILELIB1.OBJ e
FILELIB2.OBJ vengono passati al linker non ha alcuna importanza; questo perché
stiamo generando un eseguibile in formato EXE.
Il discorso cambia se abbiamo la necessità di ottenere un eseguibile con i segmenti di
programma disposti in un ben preciso ordine; supponiamo, ad esempio, di impartire il
comando:
link /map filemain.obj + filelib1.obj + filelib2.obj
In questo caso il linker produce un eseguibile chiamato FILEMAIN.EXE ed un Map
File chiamato FILEMAIN.MAP; la Figura 28.25 mostra appunto il Map File
del nostro programma.
Nella Figura 28.25 è importante notare, in particolare, il raggruppamento dei segmenti
che il linker effettua basandosi sull'attributo di classe.
La disposizione dei segmenti di programma diventa importantissima nel momento in cui abbiamo
la necessità di calcolare lo spazio in byte che il nostro programma occupa in memoria;
osservando, ad esempio, la Figura 28.25, possiamo affermare che il Program Segment di
FILEMAIN.EXE richiede come minimo un blocco di memoria da:
((STACKSEGM * 10h) + 0400h) - (PSP * 10h) byte
Tutto ciò è valido solo se STACKSEGM è l'ultimo segmento del nostro programma; in
caso contrario, si ottiene un risultato privo di senso.
L'assembler fornisce diversi metodi che ci permettono di ordinare nel modo desiderato i
vari segmenti di un programma; uno di questi metodi consiste nell'uso della direttiva .ALPHA
per la disposizione in ordine alfabetico dei segmenti di programma.
In un caso del genere, se vogliamo fare in modo che il blocco stack sia l'ultimo segmento del
nostro programma, dovremmo attribuirgli un nome del tipo ZZZZSTACK; naturalmente, bisogna
anche fare in modo che non esistano altri nomi di segmenti alfabeticamente successivi a
ZZZZSTACK.
Un altro metodo consiste nell'uso della direttiva .SEQ per la disposizione in ordine
sequenziale dei segmenti di programma; in assenza di diverse indicazioni da parte del
programmatore, l'assembler e il linker utilizzano .SEQ come direttiva predefinita.
Sicuramente, il metodo più efficace è quello che prevede l'uso combinato della direttiva
.SEQ e degli include file; il trucco consiste nel creare appositi segmenti
vuoti chiamati anche dummy segments (segmenti fantoccio).
Quando il linker incontra i dummy segments, è costretto a seguire lo stesso ordine
con cui i segmenti risultano disposti; in questo modo, possiamo ottenere l'ordinamento dei
segmenti che più si adatta alle nostre esigenze.
Vediamo subito un esempio pratico che si riferisce sempre a FILEMAIN.ASM; supponiamo
di voler disporre STACKSEGM come primo segmento in assoluto, CODESEGM come
ultimo segmento in assoluto e LIB1CODE che precede LIB2CODE. Naturalmente,
per ottenere questo risultato dobbiamo conoscere le caratteristiche complete di tutti i
segmenti di programma; in tal caso, possiamo creare un apposito include file che
assume l'aspetto mostrato in Figura 28.26.
A questo punto, la sezione riservata alle direttive nel file FILEMAIN.ASM, diventa:
Provando ora ad impartire il comando:
link /map filemain.obj + filelib1.obj + filelib2.obj
possiamo constatare che si ottiene il Map File mostrato in Figura 28.27.
La spiegazione di tutto ciò è abbastanza intuitiva; nello svolgere il proprio lavoro,
il linker incontra per primo il modulo FILEMAIN.OBJ. All'interno di questo modulo
viene subito incontrata la direttiva INCLUDE che incorpora il file
ORDSEGM.INC contenente l'ordinamento da noi stabilito per i segmenti di programma
(a tale proposito, si può analizzare il contenuto del file FILEMAIN.LST);
ovviamente, il linker è costretto a seguire l'ordinamento imposto da ORDSEGM.INC
e produce il risultato visibile in Figura 28.27.
Si tenga anche presente che tutti i dummy segments hanno dimensione nulla; di
conseguenza, la fusione effettuata dal linker tra, ad esempio, il dummy STACKSEGM
e il vero STACKSEGM, non altera in alcun modo le caratteristiche del vero
STACKSEGM.
Si osservi, infine, che la tecnica appena descritta non può essere applicata ai
segmenti con attributo di combinazione PRIVATE; nel caso, ad esempio, di
LIB1DATA, si otterrebbero due segmenti distinti, di cui uno (dummy) avente
dimensione nulla!