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: 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: 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!