Assembly Base con MASM

Capitolo 30: Interfaccia tra C e Assembly


In questo capitolo vengono descritte le convenzioni che permettono di interfacciare il linguaggio C con il linguaggio Assembly; naturalmente, si dà per scontato che chi legge abbia una adeguata conoscenza della programmazione in C.

Il linguaggio C è stato creato nel 1972 da Dennis Ritchie presso i laboratori Bell Telephone della AT&T; l'implementazione del C venne effettuata su un computer Digital Equipment PDP-11 gestito da un sistema operativo UNIX.
Sino ad allora erano stati realizzati diversi linguaggi di programmazione, come il BASIC, il Pascal, il FORTRAN, il COBOL, etc; tutti questi linguaggi si rivolgevano ad utenti accomunati dalla necessità di programmare un computer senza avere la minima conoscenza degli aspetti legati all'architettura hardware degli elaboratori elettronici.
Il BASIC ed il Pascal, ad esempio, si proponevano di insegnare l'arte della programmazione ai principianti; il FORTRAN permetteva di risolvere problemi matematici con il computer, mentre il COBOL veniva usato per scrivere programmi destinati al settore finanziario ed alla gestione aziendale.

L'idea di Dennis Ritchie fu quella di creare un linguaggio di programmazione di alto livello e di uso generale (general purpose) rivolto, però, ad utenti esperti in informatica e hardware; a tale proposito, Ritchie prese il linguaggio Assembly e gli aggiunse i costrutti sintattici tipici dei linguaggi di alto livello.
In questo modo ottenne un linguaggio che permetteva di programmare un computer senza gli impedimenti e le limitazioni tipiche dei linguaggi destinati ai non esperti; usando il C quindi, il programmatore aveva la possibilità di fare tutto ciò che era permesso dalla logica del computer.

Queste caratteristiche fecero guadagnare al C il titolo di linguaggio di programmazione per eccellenza e di linguaggio ufficiale del mondo UNIX; in effetti, nel corso della sua storia il C è stato largamente utilizzato per la realizzazione di software che richiedeva le più elevate doti di potenza ed efficienza. In particolare, il C è diventato il linguaggio preferito dai progettisti di SO; infatti, con il C sono stati realizzati i più famosi e diffusi SO, come il DOS, Windows, OS/2, UNIX, Linux, BSD e molti altri.
Un altro settore dominato dal C è quello della progettazione dei compilatori e degli interpreti; con il C sono stati realizzati numerosi compilatori ed interpreti per linguaggi come il C++, Visual Basic, Java e persino assemblatori come NASM, MASM e TASM!
A tutto ciò bisogna anche aggiungere che nel 1983 un apposito comitato dell'ANSI (American National Standard Institute) ha definito lo standard ufficiale per il linguaggio C (ANSI C); l'importantissima conseguenza di tutto ciò sta nel fatto che il codice sorgente di un programma ANSI C può essere ricompilato senza alcuna modifica su qualsiasi piattaforma hardware dotata di un compilatore ANSI C.

Come è stato spiegato in precedenza, la caratteristica rivoluzionaria del C sta nel fatto che questo linguaggio è un vero e proprio Assembly di alto livello grazie al quale si può fare tutto ciò che è permesso dalla logica del computer; per chiarire questo concetto vediamo un semplice esempio pratico.
Supponiamo che nel blocco dati di un programma C siano state dichiarate le seguenti variabili: Un programmatore Assembly vedendo questo blocco dati capisce subito che la prima WORD del vettore v si trova a +0 byte di distanza dall'inizio di v, la seconda WORD si trova a +2 byte di distanza, la terza a +4 byte di distanza e così via sino alla sesta WORD che si trova a +10 byte di distanza dall'offset iniziale assegnato a v; di conseguenza, il BYTE chiamato c1 si trova a +12 byte di distanza dall'inizio di v, il BYTE chiamato c2 si trova a +13 byte di distanza, la WORD chiamata i1 si trova a +14 byte di distanza e la WORD chiamata i2 si trova a +16 byte di distanza.
Tutto ciò significa quindi che in Assembly possiamo scrivere, ad esempio: Come sappiamo, infatti, in Assembly la scrittura v[12] rappresenta il contenuto della locazione di memoria che si trova a +12 byte di distanza dall'offset iniziale assegnato a v (al posto di v[12] si può anche scrivere [v+12]); di conseguenza, l'istruzione:
mov al, byte ptr v[12]
carica in AL il contenuto a 8 bit della locazione di memoria che si trova a +12 byte di distanza dall'inizio del vettore v (l'operatore byte ptr è necessario in quanto v è stato definito come vettore di WORD e non di BYTE).
Come si può notare, l'Assembly si fida del programmatore e non effettua quindi alcuna verifica sulla correttezza dell'indice che abbiamo assegnato al vettore v; in sostanza, l'Assembly presuppone che il programmatore sappia esattamente ciò che sta facendo.
Questa è la stessa filosofia adottata nel linguaggio C; se vogliamo quindi visualizzare sullo schermo il contenuto delle variabili illustrate in precedenza, possiamo scrivere il seguente codice C: Questo esempio ci offre l'occasione per sottolineare ancora una volta una differenza importantissima che esiste tra l'Assembly e i linguaggi di alto livello; in Assembly, se vogliamo accedere ad un elemento di un vettore, siamo tenuti a specificare lo spiazzamento in byte che possiede l'elemento all'interno del vettore stesso. Se, ad esempio, vogliamo accedere alla terza WORD del vettore v definito in precedenza, dobbiamo scrivere v[4]; infatti, la terza WORD di v si trova a +4 byte di distanza dall'inizio dello stesso vettore v.
Nei linguaggi di alto livello, invece, tutti questi calcoli spettano al compilatore o all'interprete; di conseguenza, il programmatore deve indicare tra parentesi quadre l'indice dell'elemento di un vettore e non lo spiazzamento. Nel linguaggio C, ad esempio, gli indici dei vettori partono da 0, per cui se vogliamo accedere alla terza WORD di v dobbiamo scrivere v[2]; quando il compilatore incontra l'indice 2 lo moltiplica per la dimensione in byte (2) di ciascun elemento di v e ottiene lo spiazzamento 4.

Le tecniche di indirizzamento illustrate in precedenza per il linguaggio C, vengono considerate sintatticamente illegali da molti altri linguaggi di programmazione; ciò accade, in particolare, con quei linguaggi che, essendo rivolti ai principianti, impongono rigide regole sintattiche.
Supponiamo, ad esempio, che il blocco dati illustrato in precedenza appartenga ad un programma scritto in Pascal e supponiamo inoltre che il vettore v sia stato definito come:
var v: array[1..6] of Integer;
Se ora proviamo a scrivere v[7], otteniamo un messaggio di errore del compilatore Pascal; questo messaggio ci informa che stiamo utilizzando un indice fuori limite per il vettore v!
In ogni caso, si tenga presente che spesso queste regole sono facilmente aggirabili; osservando, ad esempio, che i1 si trova a 8 WORD di distanza dall'inizio di v, possiamo definire una variabile intera i alla quale assegnamo il valore 8 in modo da poter scrivere il seguente codice Pascal:
WriteLn('i1 = ', v[i]);
Un qualsiasi compilatore o interprete non può effettuare in questo caso alcuna verifica sul contenuto dell'indice del vettore; infatti, l'indice i è una variabile e non una costante, per cui il suo contenuto sarà noto (alla CPU) solo in fase di esecuzione del programma.

Un altro esempio pratico è rappresentato dal fatto che se in C definiamo una variabile x di tipo float (numero reale a 32 bit) e una variabile i3 di tipo long int (numero intero con segno a 32 bit), possiamo scrivere allora il seguente assegnamento:
i3 = x;
Dal punto di vista della CPU questa istruzione è perfettamente legale in quanto consiste nel copiare il contenuto a 32 bit di x (in formato IEEE standard), nel contenuto a 32 bit di i3; di conseguenza, anche i compilatori C considerano legale questo assegnamento.
Un compilatore Pascal, invece, incontrando la precedente istruzione produce un messaggio di errore; questo perché il Pascal impone rigide regole sulla compatibilità tra diversi tipi di dati e quindi non permette di copiare una variabile reale in una variabile intera anche se in memoria entrambe le variabili rappresentano locazioni da 32 bit.

Questo capitolo tratta l'interfacciamento tra C e Assembly nell'ambiente operativo a 16 bit offerto dal DOS; per lo sviluppo dei programmi di esempio presentati nel seguito è necessario quindi munirsi di un compilatore C a 16 bit come il Microsoft C, il Borland Turbo C, etc.
Nel nostro caso, verrà utilizzato il compilatore Borland C++ 3.1 scaricabile dalla sezione Compilatori assembly, c++ e altri dell’ Area Downloads di questo sito.

30.1 Tipi di dati del C

Per interfacciare correttamente l'Assembly con un linguaggio di alto livello è necessario conoscere tutte le informazioni relative ai tipi di dati forniti dal linguaggio stesso; queste informazioni assumono una importanza vitale nell'interscambio di dati tra due o più moduli di un programma e, in particolare, nella fase di gestione dello stack frame di una procedura.
Un compilatore C destinato a lavorare in un ambiente operativo a 16 bit, fornisce una serie di tipi di dati interi aventi le caratteristiche illustrate in Figura 30.1. Nel linguaggio C il tipo int rappresenta il tipo di dato intero di riferimento e, in base a quanto previsto dallo standard ANSI C, la sua ampiezza in bit deve rispecchiare il numero di bit che caratterizza l'ambiente operativo a cui è destinato il compilatore.
Proprio per questo motivo, i compilatori destinati agli ambienti operativi a 16 bit forniscono il tipo di dato int con ampiezza pari a 16 bit; lo standard ANSI C prevede, inoltre, che il tipo short int abbia una ampiezza in bit non superiore a quella di un int e che il tipo long int abbia una ampiezza in bit non inferiore a quella di un int.
Il C fornisce anche il tipo enum che permette di definire enumerazioni di dati di tipo signed int; tali dati hanno quindi la stessa ampiezza in bit dei signed int.

I tipi di dati in virgola mobile (floating point) sono indipendenti dall'architettura del computer in quanto il loro formato standard viene stabilito, come sappiamo, dall'IEEE (Institute of Electrical and Electronics Engineers); i compilatori C che supportano lo standard IEEE forniscono i tre tipi di dati in virgola mobile illustrati in Figura 30.2. Gli estremi Min. e Max. si riferiscono ai numeri reali positivi; per ottenere gli estremi negativi basta mettere il segno meno davanti agli estremi positivi.
Il campo Precisione si riferisce al numero di cifre esatte dopo la virgola; si tenga presente che tanto maggiore è la parte intera del numero reale, tanto minore sarà il numero di cifre esatte dopo la virgola.

Riassumendo, possiamo affermare che tutti questi aspetti dipendono dall'architettura del computer che si sta utilizzando; per convenzione, i limiti minimo e massimo che assumono i tipi interi e in virgola mobile, vengono specificati nei due header limits.h e float.h della libreria standard del C. Il programma C di Figura 30.3 permette di verificare in pratica le considerazioni esposte in precedenza: Compilando questo programma è possibile verificare le diverse ampiezze in bit assunte dai tipi interi; in relazione, invece, ai numeri reali è possibile verificare il differente numero di cifre decimali esatte garantito dai tre tipi float, double e long double.

30.2 Convenzioni per i compilatori C

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

30.2.1 Segmenti di programma

In relazione alla segmentazione standard utilizzata per organizzare i programmi a basso livello, i compilatori C seguono completamente le convenzioni illustrate nel Capitolo 29; si può dire, anzi, che tali convenzioni sono state concepite in modo particolare proprio per i compilatori C!

Possiamo utilizzare quindi tutti i modelli di memoria disponibili (TINY, SMALL, MEDIUM, COMPACT, LARGE e HUGE); inoltre, in base al modello di memoria utilizzato, le informazioni che compongono un programma possono essere distribuite nei blocchi _DATA, CONST, _BSS, STACK, FAR_DATA, FAR_BSS e name_TEXT.

I vari blocchi di dati NEAR vengono raggruppati dai compilatori C con la direttiva:
DGROUP GROUP _DATA, CONST, _BSS, STACK
Il registro DS viene automaticamente inizializzato dal compilatore C con l'assegnamento:
DS = DGROUP
Il comportamento predefinito dei compilatori C prevede che, in qualsiasi modello di memoria, il blocco STACK venga sempre inserito in DGROUP; eventualmente, il programmatore può modificare questa situazione attraverso appositi menu di configurazione del compilatore.
Nel caso, ad esempio, del compilatore Borland C++ 3.1, dall'interno dell'editor integrato bisogna selezionare il menu:
Options - Compiler - Code generation
e disattivare l'opzione Assume SS Equals DS.

Come è stato spiegato nel Capitolo 29, quando si interfaccia MASM con i linguaggi di alto livello è vivamente consigliato l'uso delle direttive semplificate per i segmenti; in questo modo si delega all'assembler il compito di espandere tali direttive in conformità al modello di memoria che si sta utilizzando.
Abbiamo anche visto che in presenza delle direttive semplificate per i segmenti, l'assembler crea automaticamente il gruppo DGROUP; definendo, invece, i segmenti di programma secondo la sintassi classica, tutti questi aspetti sono a carico del programmatore (il quale, in tal caso, deve sapere esattamente come comportarsi).

30.2.2 Stack frame

In relazione allo stack frame, bisogna ricordare innanzi tutto che in C le procedure vengono chiamate funzioni. Nei precedenti capitoli abbiamo visto che gli eventuali argomenti richiesti da una funzione C vengono inseriti nello stack a partire dall'ultimo (destra sinistra); in questo modo, all'interno dello stack, gli argomenti di una funzione si trovano posizionati nello stesso ordine indicato dal prototipo della funzione stessa.
Il compito di ripulire lo stack dagli argomenti spetta, come sappiamo, al caller.

Per accedere ai parametri di una funzione, i compilatori C a 16 bit si servono di BP; abbiamo visto che i parametri si trovano a spiazzamenti positivi da BP, mentre le eventuali variabili locali si trovano a spiazzamenti negativi da BP.

30.2.3 Valori di ritorno

In relazione, infine, alle locazioni utilizzate per contenere gli eventuali valori di ritorno delle funzioni, valgono tutte le convenzioni illustrate nella Figura 25.10 del Capitolo 25.

Sempre in relazione ai valori di ritorno, bisogna ricordare che il C permette di definire funzioni che possono restituire intere strutture, non solo per indirizzo, ma anche per valore!
Ovviamente, il caso di una funzione che restituisce una struttura per indirizzo è molto semplice; infatti, nella Figura 25.10 del Capitolo 25 si vede che un indirizzo NEAR viene restituito in AX, mentre un indirizzo FAR viene restituito in DX:AX.
Il caso interessante riguarda, invece, il procedimento utilizzato dai compilatori C per permettere ad una funzione di restituire una struttura per valore. Una struttura che occupa complessivamente 1, 2 o 4 byte, viene restituita, rispettivamente, nei registri AL, AX, DX:AX; una struttura che occupa complessivamente 3 byte, oppure più di 4 byte, viene restituita al caller attraverso un metodo più complesso che sarà illustrato nel seguito del capitolo attraverso un apposito esempio pratico.

30.3 Le classi di memoria del C

Nella terminologia dei linguaggi di alto livello, con l'espressione classe di memoria si indica l'insieme degli aspetti legati alla visibilità e alla durata di un identificatore; questi aspetti coinvolgono, naturalmente, il metodo attraverso il quale l'identificatore è stato creato.
Come accade nella maggior parte dei linguaggi, anche in C esistono due categorie fondamentali di identificatori e cioè, gli identificatori locali e globali; in generale, gli identificatori locali del C sono quelli definiti all'interno di un blocco funzionale delimitato da una coppia { }, mentre gli identificatori globali sono quelli esterni a qualsiasi blocco { }.

A differenza di quanto accade in Pascal, il C non permette di definire funzioni innestate all'interno di altre funzioni; possiamo dire allora che in C i nomi che identificano le funzioni sono tutti globali e quindi le funzioni esistono per tutta la fase di esecuzione e sono visibili in tutto il programma.

In assenza di altre indicazioni da parte del programmatore, una funzione C definita in un modulo è visibile anche in altri moduli del programma; se in un modulo A definiamo una funzione func1 con prototipo:
int func1(char, char);
allora in un modulo B possiamo rendere visibile func1 attraverso la dichiarazione:
extern int func1(char, char);
Per fare in modo che func1 non sia visibile all'esterno del modulo A, dobbiamo utilizzare la parola chiave static e dichiarare in A il prototipo:
static int func1(char, char);
Le etichette del C possono essere definite solo all'interno di una funzione; la loro visibilità quindi è limitata alla funzione di appartenenza e il loro identificatore può essere ridefinito in altri punti del programma.

Le variabili del C possono essere, sia globali, sia locali; in C le variabili globali vengono anche chiamate statiche, mentre le variabili locali vengono anche chiamate automatiche.
Una variabile è globale quando viene definita all'esterno di qualsiasi blocco funzionale { }; in questo caso il compilatore C inserisce la variabile globale nel blocco dati del programma. Una variabile globale quindi esiste per tutta la fase di esecuzione ed è visibile in tutto il programma.
Le variabili globali possono essere inizializzate solo con valori costanti; in assenza di inizializzazione, lo standard ANSI C prevede che il compilatore assegni ad una variabile globale il valore iniziale 0.

In assenza di altre indicazioni da parte del programmatore, una variabile globale del C definita in un modulo è visibile anche in altri moduli del programma; se in un modulo A definiamo una variabile globale:
float pi_greco = 3.14;
allora in un modulo B possiamo rendere visibile pi_greco attraverso la dichiarazione:
extern float pi_greco;
Per fare in modo che pi_greco non sia visibile all'esterno del modulo A, dobbiamo utilizzare la parola chiave static e riscrivere nel modulo A la definizione:
static float pi_greco = 3.14;
Una variabile del C è locale (o automatica) quando viene definita all'interno di un qualsiasi blocco { }; la visibilità di una variabile locale è limitata al blocco { } di appartenenza e quindi il suo identificatore può essere ridefinito altrove.
Una variabile locale viene creata nello stack quando si entra nel blocco { } di appartenenza e viene poi distrutta quando si esce dal blocco stesso; per fare in modo che una variabile locale non venga distrutta, bisogna utilizzare ancora la parola chiave static. Se la precedente variabile pi_greco viene definita all'interno di un blocco { }, possiamo scrivere:
static float pi_greco = 3.14;
In questa seconda versione la parola chiave static dice al compilatore C che pi_greco non deve essere distrutta all'uscita dal blocco { } di appartenenza; il trucco adottato dal compilatore C consiste nel creare questo tipo di variabili nel blocco dati del programma e non nello stack. È perfettamente lecito quindi il caso di una funzione che restituisce al caller l'indirizzo di una sua variabile locale static; tale variabile, infatti, essendo stata definita nel blocco dati del programma, continua ad esistere anche al di fuori della funzione che l'ha creata.

Questo stesso discorso vale anche se, all'interno di una funzione, definiamo una variabile locale del tipo:
char *loc_string = "Stringa locale";
Quando il compilatore incontra questa definizione, crea la stringa "Stringa locale" nel blocco dati del programma, poi crea nello stack una variabile locale loc_string di tipo puntatore a char e le assegna l'indirizzo iniziale della stringa; al termine della funzione, la variabile locale loc_string viene distrutta, mentre la stringa "Stringa locale" continua ad esistere.
In sostanza, anche in assenza della parola chiave static, le stringhe definite con il precedente metodo all'interno di un blocco { }, vengono ugualmente sistemate nel blocco dati del programma; è lecito quindi scrivere una funzione C che restituisce al caller l'indirizzo (della stringa) contenuto nella precedente variabile puntatore loc_string.

Le variabili locali del C possono essere inizializzate, sia con valori costanti, sia con il contenuto di altre variabili; in assenza di inizializzazione il contenuto di una variabile locale è casuale ("sporco").

30.4 Gestione degli indirizzamenti in C

Nel linguaggio C un puntatore è una variabile contenente un valore intero che rappresenta un indirizzo di memoria; negli ambienti operativi a 16 bit un indirizzo di memoria può essere o di tipo NEAR (formato da un Offset a 16 bit), o di tipo FAR (formato da una coppia Seg:Offset a 16+16 bit).
Come già sappiamo, l'indirizzo di una locazione di memoria da 8 bit, 16 bit, 32 bit, etc, coincide con l'indirizzo del BYTE meno significativo della locazione stessa; un puntatore contiene quindi l'indirizzo NEAR o FAR del BYTE meno significativo della locazione di memoria a cui sta puntando.

Analizziamo in pratica questi concetti premettendo che nel seguito, quando si parla genericamente di compilatore o compilazione, si intende il processo di traduzione del codice C in codice macchina; supponiamo allora di avere il seguente blocco dati di un programma C: Quando un compilatore C incontra questo blocco dati: Supponendo di lavorare con indirizzi di tipo NEAR, in presenza della definizione:
char *pc1 = &c1;
il compilatore assegna a pc1 una locazione di memoria da 16 bit e carica in questa locazione l'Offset iniziale di c1.

In presenza della definizione:
int *pi1 = &i1;
il compilatore assegna a pi1 una locazione di memoria da 16 bit e carica in questa locazione l'Offset iniziale di i1.

In presenza della definizione:
long *pl1 = &l1;
il compilatore assegna a pl1 una locazione di memoria da 16 bit e carica in questa locazione l'Offset iniziale di l1.

Se, invece, stiamo lavorando con indirizzi di tipo FAR, in presenza della definizione:
char far *pc1 = &c1;
il compilatore assegna a pc1 una locazione di memoria da 16+16 bit e carica in questa locazione la coppia Seg:Offset iniziale di c1.

In presenza della definizione:
int far *pi1 = &i1;
il compilatore assegna a pi1 una locazione di memoria da 16+16 bit e carica in questa locazione la coppia Seg:Offset iniziale di i1.

In presenza della definizione:
long far *pl1 = &l1;
il compilatore assegna a pl1 una locazione di memoria da 16+16 bit e carica in questa locazione la coppia Seg:Offset iniziale di l1.

A questo punto, se all'interno di una funzione C scriviamo:
char c2 = *pc1;
il compilatore accede all'indirizzo puntato da pc1, legge un blocco da 8 bit che parte da quell'indirizzo e lo copia nella locazione a 8 bit in cui si trova c2.

Se scriviamo:
int i2 = *pi1;
il compilatore accede all'indirizzo puntato da pi1, legge un blocco da 16 bit che parte da quell'indirizzo e lo copia nella locazione a 16 bit in cui si trova i2.

Se scriviamo:
long l2 = *pl1;
il compilatore accede all'indirizzo puntato da pl1, legge un blocco da 32 bit che parte da quell'indirizzo e lo copia nella locazione a 32 bit in cui si trova l2.

Supponiamo ora di avere una funzione scritta in Assembly che presenta il seguente prototipo C:
extern void func_asm(char c, int i, long l);
Effettuiamo ora, da un modulo scritto in C, la chiamata di func_asm con gli argomenti c1, i1 e l1 definiti in precedenza; la chiamata assume il seguente aspetto:
func_asm(c1, i1, l1);
In questo caso abbiamo a che fare con il passaggio per valore dei tre argomenti c1, i1 e l1, per cui vengono create nello stack le copie dei valori di questi tre argomenti; queste tre copie vengono chiamate simbolicamente c, i e l e rappresentano i tre parametri della funzione func_asm.
All'interno di func_asm il parametro c rappresenta un generico valore intero 12 a 8 bit, il parametro i rappresenta un generico valore intero 1350 a 16 bit, mentre il parametro l rappresenta un generico valore intero 186900 a 32 bit; in base a queste considerazioni, nella funzione func_asm possiamo scrivere quindi istruzioni del tipo: È importante notare che i vari operandi che fanno parte di una istruzione, devono avere dimensioni in bit compatibili con quelle previste dall'istruzione stessa; non avrebbe senso quindi utilizzare MOV per caricare c in AX, o utilizzare ADD per sommare EBX con i.
Grazie al passaggio per valore, qualsiasi modifica apportata ai parametri c, i e l, non ha alcuna ripercussione sugli argomenti c1, i1 e l1; infatti, c, i e l si trovano nel blocco stack, mentre c1, i1 e l1 si trovano nel blocco dati del programma.

Vediamo, invece, come ci si deve comportare quando la funzione func_asm ha il seguente prototipo:
extern void func_asm(char *pc, int *pi, long *pl);
Utilizzando le variabili definite in precedenza, la chiamata di questa funzione diventa:
func_asm(&c1, &i1, &l1);
oppure:
func_asm(pc1, pi1, pl1);
Nel primo caso stiamo passando direttamente gli indirizzi degli argomenti c1, i1 e l1, per cui si parla di passaggio diretto per indirizzo; nel secondo caso stiamo passando indirettamente (tramite cioè i puntatori) gli indirizzi degli argomenti c1, i1 e l1, per cui si parla di passaggio indiretto per indirizzo.
In entrambi i casi, la funzione func_asm riceve tre valori interi a 16 bit chiamati simbolicamente pc, pi e pl, che rappresentano i tre indirizzi NEAR di c1, i1 e l1; attraverso questi tre indirizzi, la funzione func_asm può accedere alle relative locazioni di memoria modificandone il contenuto originale.
Tenendo conto del significato dei parametri pc, pi e pl, all'interno di func_asm possiamo scrivere istruzioni del tipo: Questa volta abbiamo a che fare con indirizzi NEAR formati ciascuno da una componente Offset a 16 bit; questi indirizzi vengono gestiti attraverso i tre registri puntatori SI, DI e BX. Accedendo alle locazioni di memoria relative a questi indirizzi, possiamo modificarne il contenuto originale; infatti, come si vede nell'esempio, tutte le modifiche apportate a [SI], [DI] e [BX] si ripercuotono su c1, i1 e l1.

Da queste considerazioni si intuisce subito che se il parametro pc (che abbiamo caricato in SI) è l'indirizzo iniziale di un vettore di char, allora gli indirizzi dei vari elementi del vettore corrispondono a: SI+0, SI+1, SI+2, etc; di conseguenza, il contenuto dei vari elementi del vettore è rappresentato da: [SI+0], [SI+1], [SI+2], etc.
Se il parametro pi (che abbiamo caricato in DI) è l'indirizzo iniziale di un vettore di int, allora gli indirizzi dei vari elementi del vettore corrispondono a: DI+0, DI+2, DI+4, etc; di conseguenza, il contenuto dei vari elementi del vettore è rappresentato da: [DI+0], [DI+2], [DI+4], etc.
Se il parametro pl (che abbiamo caricato in BX) è l'indirizzo iniziale di un vettore di long, allora gli indirizzi dei vari elementi del vettore corrispondono a: BX+0, BX+4, BX+8, etc; di conseguenza, il contenuto dei vari elementi del vettore è rappresentato da: [BX+0], [BX+4], [BX+8], etc.

Nel caso di indirizzamenti di tipo FAR bisogna solo ricordarsi che in questo caso si devono gestire indirizzi da 16+16 bit; il prototipo della funzione func_asm diventa:
extern void func_asm(char far *pc, int far *pi, long far *pl);
In presenza della chiamata:
func_asm(&c1, &i1, &l1);
il compilatore C provvede automaticamente a passare le coppie Seg:Offset relative agli indirizzi dei vari argomenti; per poter effettuare, invece, la chiamata:
func_asm(pc1, pi1, pl1);
dobbiamo servirci delle definizioni FAR illustrate in precedenza per i tre puntatori pc1, pi1 e pl1.

In entrambi i casi, la funzione func_asm riceve tre valori interi a 16+16 bit chiamati simbolicamente pc, pi e pl, che rappresentano i tre indirizzi FAR di c1, i1 e l1; attraverso questi tre indirizzi, la funzione func_asm può accedere alle relative locazioni di memoria modificandone il contenuto originale.
Tenendo conto del significato dei parametri pc, pi e pl, all'interno di func_asm possiamo scrivere istruzioni del tipo: Queste istruzioni presuppongono l'utilizzo del set di istruzioni a 32 bit in modo da poter disporre dei registri di segmento FS e GS; in caso contrario bisogna riuscire a destreggiarsi con DS e ES, ricordandosi poi di ripristinare il contenuto originale di DS.
Se pc, pi e pl rappresentano gli indirizzi di partenza di tre vettori rispettivamente di char, di int e di long, valgono le stesse considerazioni già esposte per il caso degli indirizzi NEAR; l'unica differenza sta nel fatto che questa volta dobbiamo gestire anche la componente Seg di ogni elemento dei vettori per cui, ad esempio, ES:[SI+4] rappresenta il contenuto del quinto char del vettore puntato da pc.

30.5 Protocollo di comunicazione tra C e Assembly

In base alle considerazioni appena svolte e in base alle cose dette nei precedenti capitoli, possiamo dedurre una serie di regole che permettono a due o più moduli C e Assembly di comunicare tra loro; l'insieme di queste regole definisce quindi il protocollo di comunicazione tra C e Assembly.

I moduli C e Assembly destinati a comunicare tra loro, devono specificare lo stesso modello di memoria; questo aspetto è di vitale importanza in quanto garantisce il corretto scambio di informazioni tra i vari moduli.
In alternativa è possibile seguire una strada molto complessa e pericolosa, che consiste nello specificare esplicitamente il tipo di indirizzamento NEAR o FAR necessario per accedere alle variabili e alle procedure del programma.

Un modulo C che intende utilizzare degli identificatori definiti in un modulo Assembly, deve dichiarare questi identificatori attraverso il qualificatore extern; da parte sua il modulo Assembly contenente le definizioni degli identificatori da utilizzare nel modulo C, deve specificare le necessarie direttive PUBLIC relative agli identificatori stessi.

Un modulo Assembly che intende utilizzare degli identificatori definiti in un modulo C, deve dichiarare questi identificatori attraverso la direttiva EXTRN; da parte sua il modulo C contenente le definizioni degli identificatori da utilizzare nel modulo Assembly, deve provvedere a rendere esternamente visibili gli identificatori stessi.
Tutti gli identificatori visibili anche all'esterno del modulo di appartenenza, vengono chiamati identificatori con linkaggio esterno; gli identificatori visibili, invece, solo all'interno del modulo di appartenenza, vengono chiamati identificatori con linkaggio interno.

In un programma C, tutti gli identificatori con linkaggio esterno devono essere preceduti dal carattere '_' (underscore); il carattere underscore è associato al codice ASCII 95.
I compilatori C aggiungono automaticamente l'underscore all'inizio di ogni identificatore con linkaggio esterno presente nei vari moduli C di un programma; per non generare confusione, nei moduli C è consigliabile quindi evitare l'uso di nomi preceduti dall'underscore.

Nel caso degli identificatori con linkaggio esterno definiti o dichiarati nei moduli Assembly, si possono presentare due casi: Tutte le procedure contenute nei moduli Assembly da linkare al C, devono preservare rigorosamente il contenuto di tutti i registri di segmento (in particolare, CS, DS e SS) e dei registri SP e BP; se si utilizzano le caratteristiche avanzate di MASM, all'interno delle procedure dotate di stack frame il contenuto dei registri SP e BP viene automaticamente preservato dall'assembler.
I registri CS e SS vengono gestiti direttamente dal SO e non necessitano quindi di modifiche da parte del programmatore; in rarissime circostanze si può avere la necessità di modificare SS e SP per poter gestire uno stack personalizzato.
I registri di segmento ES, FS e GS sono a nostra completa disposizione, mentre DS viene inizializzato con DGROUP dal compilatore C; se il programmatore ha la necessità di modificare DS per referenziare altri segmenti di dati, deve preservarne il contenuto originale (che in genere è DGROUP).

I compilatori C utilizzano anche i registri SI e DI per gestire le variabili intere con qualificatore register; molti compilatori C permettono di abilitare o meno questa possibilità. Per non creare problemi al compilatore, si consiglia quindi di preservare sempre SI e DI nelle proprie procedure Assembly che utilizzano questi registri; generalmente, tutte le funzioni delle librerie fornite dai compilatori C preservano SI e DI.

Il C è un linguaggio case-sensitive e quindi i compilatori C distinguono tra maiuscole e minuscole; questo significa che identificatori come varword1, VarWord1, VARWORD1, etc, verranno considerati tutti distinti.
In osservanza a queste regole, al momento di assemblare un modulo Assembly da linkare ad un modulo C è necessario richiedere un assemblaggio case-sensitive; se si sta utilizzando MASM, il parametro da passare all'assembler è /Cp.

30.6 Esempi pratici

Analizziamo ora una serie di esempi pratici che ci permettono di verificare le considerazioni esposte in precedenza; è necessario ribadire che tutti gli esempi che seguono si riferiscono all'ambiente operativo a 16 bit offerto dal DOS.

30.6.1 Chiamate incrociate tra moduli C e moduli Assembly

Iniziamo con un programma formato dai due moduli CMOD1.C e ASMMOD1.ASM; lo scopo del programma è quello di effettuare delle chiamate incrociate tra procedure definite nei due moduli.
Il modulo CMOD1.C chiama una procedura definita nel modulo ASMMOD1.ASM per visualizzare una stringa C definita nello stesso modulo CMOD1.C; il modulo ASMMOD1.ASM chiama una procedura definita nel modulo CMOD1.C per visualizzare una stringa C definita nello stesso modulo ASMMOD1.ASM.
In entrambi i casi la stringa C viene visualizzata attraverso la funzione printf della libreria standard del C; in questo modo possiamo anche vedere come sia possibile chiamare, da un modulo Assembly, le funzioni della libreria standard del C.

CMOD1.C svolge il ruolo di modulo principale del programma e quindi contiene anche l'entry point che, come vedremo in seguito, viene gestito direttamente dal compilatore C; il modulo CMOD1.C assume l'aspetto mostrato in Figura 30.4. Come si può notare, in questo modulo viene definita una stringa strc con linkaggio esterno e viene dichiarata una stringa esterna strasm; inoltre, viene definita una procedura printstr_c con linkaggio esterno e viene dichiarata una procedura esterna printstr_asm. Appena si entra nella funzione principale main, viene chiamata la procedura esterna printstr_asm definita nel modulo ASMMOD1.ASM.

Analizziamo ora proprio il modulo ASMMOD1.ASM che, essendo un modulo secondario, non deve contenere alcun entry point; la prima versione di questo modulo è scritta in stile Assembly classico e non fa uso quindi delle macro presentate nel precedente capitolo.
Come sappiamo, in un caso del genere siamo obbligati a specificare tutti i dettagli relativi ai segmenti standard; inoltre, tutte le procedure devono indicare il loro tipo (NEAR o FAR).
Naturalmente, la gestione dello stack frame è interamente a carico del programmatore; anche la gestione delle direttive ASSUME e del gruppo DGROUP ricade sul programmatore.
Nel caso di un programma con modello di memoria SMALL, il modulo ASMMOD1.ASM assume l'aspetto mostrato in Figura 30.5. La prima cosa da osservare riguarda il fatto che il C è case sensitive, per cui tutti gli identificatori presenti nel modulo ASMMOD1.ASM devono letteralmente coincidere con i corrispondenti identificatori presenti nel modulo CMOD1.C; notiamo poi la presenza del carattere underscore all'inizio di ogni identificatore con linkaggio esterno.
In virtù del fatto che stiamo utilizzando il modello di memoria SMALL, tutte le procedure interne o esterne sono di tipo NEAR; analogamente, l'accesso a tutte le variabili del programma avviene tramite indirizzi di tipo NEAR, costituiti quindi dalla sola componente Offset a 16 bit.

La procedura printstr_asm, chiamata dalla funzione main del modulo CMOD1.C, utilizza la funzione esterna printf della libreria standard del C per visualizzare la stringa strc definita nel modulo CMOD1.C; come al solito, si raccomanda vivamente di inserire tutte le direttive EXTRN al di fuori di qualsiasi segmento di programma.

Il prototipo C della funzione printf è:
int printf(char *format, ...);
Questa funzione richiede quindi un numero variabile di argomenti, di cui il primo è una stringa C denominata stringa di formato; lo scopo della stringa format è quello di formattare l'output prodotto da printf.
Nel blocco _DATA del modulo ASMMOD1.ASM creiamo a tale proposito la stringa di formato strFmt (terminata da uno 0), che chiede a printf di visualizzare una stringa (%s) seguita da un new line (10='\n'); la variabile strFmt ha linkaggio interno per cui non richiede l'underscore.

All'interno di printstr_asm, la chiamata di printf avviene in stile Assembly classico; di conseguenza, dobbiamo inserire gli argomenti nello stack a partire dall'ultimo e alla fine dobbiamo provvedere a ripulire lo stesso stack.
Siccome avevamo inserito due offset da 16 bit ciascuno, la pulizia dello stack consiste nel sommare 4 byte a SP; si tenga anche presente che printf si aspetta che strc sia una stringa C con zero finale (il quale, ovviamente, viene aggiunto dal compilatore C).

Successivamente printstr_asm chiama la procedura printstr_c definita nel modulo CMOD1.C; all'interno del modulo CMOD1.C, la procedura printstr_c utilizza printf per visualizzare la stringa strasm definita nel modulo ASMMOD1.ASM.
Anche in questo caso strasm deve essere una stringa C terminata da uno 0; siccome tale stringa viene definita in un modulo Assembly, il compito di inserire lo zero finale spetta questa volta al programmatore.

30.6.2 Generazione dell'eseguibile

Analizziamo ora le fasi di assembling e di linking dei precedenti programmi di esempio; tali fasi sono estremamente semplici grazie al fatto che i compilatori C producono, per ogni modulo C, il relativo object file. Da queste considerazioni si intuisce subito che per convertire in formato eseguibile il programma precedente, sono necessari i seguenti semplici passi: Naturalmente, la fase di linking coinvolge anche le librerie contenenti le funzioni del C; i dettagli relativi alle varie fasi da svolgere, possono variare da compilatore a compilatore.
Molto spesso i compilatori sono dotati di potenti ambienti integrati di sviluppo (IDE), che permettono di automatizzare tutto questo lavoro; nel caso, ad esempio, del celebre compilatore Borland C++ 3.1 (che tra l'altro contiene anche la versione 3.x del TASM), dall'interno dell'IDE è possibile configurare tutte le opzioni come, ad esempio, il modello di memoria, il set di istruzioni della CPU, l'assembler da utilizzare, etc; una volta effettuate le varie configurazioni, si può aprire un nuovo progetto che nel nostro caso sarà formato dai due moduli CMOD1.C e ASMMOD1.ASM.

Se non si dispone di un IDE, si può sempre ricorrere alla linea di comando che spesso rappresenta la soluzione più semplice ed efficace; in questo caso è necessario inserire manualmente tutte le opzioni di configurazione richieste dal compilatore, dall'assemblatore e dal linker.
In genere, digitando dal prompt del DOS il nome del compilatore e premendo [Invio], si ottiene l'elenco completo di tutte le opzioni di compilazione disponibili; lo stesso discorso vale per l'assembler e per il linker.

Vediamo un caso pratico che si riferisce al compilatore Borland C++ 3.1 a 16 bit; supponiamo a tale proposito che tutti gli strumenti di sviluppo siano stati installati nella cartella predefinita:
c:\borlandc\bin
In questo caso, tutte le librerie del C si trovano nella cartella:
c:\borlandc\lib
mentre gli include file si trovano nella cartella:
c:\borlandc\include
Prima di tutto bisogna creare nella cartella BIN i file di configurazione per il compilatore e per il linker; questi file devono essere in formato ASCII e si devono chiamare TURBOC.CFG e TLINK.CFG.
All'interno del file TURBOC.CFG dobbiamo inserire i percorsi per le librerie, per gli include file e (opzionalmente) per l'assembler che vogliamo utilizzare, più eventuali parametri da passare al compilatore, al linker e all'assembler; l'aspetto generale del file TURBOC.CFG è il seguente: Come si può notare, l'opzione -E permette di specificare l'assembler da utilizzare, mentre ogni opzione -T permette di passare un parametro di configurazione all'assembler stesso; questa tecnica non permette di specificare, con l'opzione -E, un assembler diverso da TASM. Se si intende utilizzare, ad esempio, un compilatore C della Borland in combinazione con il MASM, bisogna separare la fase di compilazione dalla fase di assemblaggio; più avanti viene illustrato in pratica il procedimento da seguire.

L'aspetto generale del file TLINK.CFG è il seguente:
-Lc:\borlandc\lib
Si tenga presente che spesso i due file, TURBOC.CFG e TLINK.CFG, vengono creati automaticamente durante la fase di installazione; in questo caso il programmatore deve solo aggiungere le eventuali altre opzioni di cui ha bisogno.
Numerose altre opzioni possono essere passate anche attraverso la linea di comando; ad esempio, l'opzione -3 abilita il set di istruzioni a 32 bit del compilatore, mentre l'opzione -f287 abilita l'uso della FPU 80287/80387.

A questo punto siamo pronti per la fase di creazione dell'eseguibile CMOD1.EXE; tale fase può essere interamente gestita dal compilatore a linea di comando il cui nome è BCC.EXE.
Per svolgere il proprio lavoro, il compilatore si serve proprio dei file di configurazione descritti in precedenza; è importantissimo passare a BCC.EXE il parametro relativo al modello di memoria che intendiamo utilizzare. Tale parametro è formato dall'opzione -m seguita dalla lettera iniziale del modello di memoria desiderato; se abbiamo deciso, ad esempio, di utilizzare il modello SMALL, dobbiamo eseguire il comando:
c:\borlandc\bin\bcc -ms -3 cmod1.c asmmod1.asm
Se, invece, vogliamo utilizzare il modello LARGE, dobbiamo eseguire il comando:
c:\borlandc\bin\bcc -ml -3 cmod1.c asmmod1.asm
Naturalmente, la struttura del modulo ASMMOD1.ASM deve essere perfettamente compatibile con il modello di memoria selezionato per il modulo C; a questo punto, premendo il tasto [Invio], il controllo passa a BCC.EXE che: Se vogliamo generare anche un dettagliatissimo map file, possiamo passare a BCC.EXE il parametro -M; in questo caso viene creato anche un file chiamato CMOD1.MAP.
Se vogliamo generare il listing file di ASMMOD1.ASM, dobbiamo passare a BCC.EXE il parametro -Tl; in questo caso viene creato anche un file chiamato ASMMOD1.LST.

Un parametro di straordinaria importanza è sicuramente -S che richiede a BCC.EXE la generazione del file CMOD1.ASM; tale file è la traduzione in Assembly di CMOD1.C, effettuata dal compilatore C!
È estremamente istruttivo esaminare il contenuto di CMOD1.ASM in quanto si ha la possibilità di verificare tutto il lavoro compiuto a basso livello dal compilatore C. L'esame di questo file permette anche di conoscere tutti i dettagli sul tipo di segmentazione utilizzato dal compilatore; in presenza del parametro -S, non viene generato alcun eseguibile CMOD1.EXE.

Nel caso in cui si intenda utilizzare un assembler diverso da TASM.EXE (ad esempio, ML.EXE), e/o un compilatore diverso da BCC.EXE, si deve procedere alla separazione delle varie fasi; avendo a disposizione, ad esempio, ML.EXE e BCC.EXE, dobbiamo procedere in questo modo:

1) generazione del file ASMMOD1.OBJ con il comando:
ml /c /Cp asmmod1.asm
2) generazione dell'eseguibile CMOD1.EXE con il comando (caso del modello SMALL):
bcc -ms -3 -M cmod1.c asmmod1.obj
In assenza del parametro /coff il MASM genera un object file in formato OMF (Object Module Format) compatibile con gli object file generati dagli strumenti di sviluppo della Borland.

30.6.3 Riscrittura di ASMMOD1.ASM con l'uso delle caratteristiche avanzate di MASM

Dalle considerazioni esposte in precedenza emerge chiaramente il fatto che, nell'interfacciamento dell'Assembly con i linguaggi di alto livello, il programmatore è chiamato a svolgere un lavoro veramente impegnativo; infatti, oltre a dover scrivere il codice Assembly, il programmatore deve anche gestire una numerosa serie di dettagli legati ai protocolli di comunicazione tra i vari moduli.
In generale, tale modo di procedere è vivamente sconsigliabile in quanto comporta una elevata probabilità di errore; ciò è vero soprattutto quando si devono gestire moduli Assembly ben più complessi di quello mostrato in Figura 30.5.

Bisogna anche ricordare che il ricorso alla segmentazione "vecchio stile" in combinazione con la direttiva GROUP, scarica sul programmatore il compito di gestire il tristemente famoso bug dell'operatore OFFSET di MASM; come è stato spiegato nel precedente capitolo, tale bug scompare quando si utilizzano le direttive avanzate di MASM per la segmentazione standard!

Utilizzando le caratteristiche avanzate di MASM, il modulo ASMMOD1.ASM assume l'aspetto mostrato in Figura 30.6 (modello SMALL). In questa nuova versione del modulo ASMMOD1.ASM notiamo subito la scomparsa dei dettagli relativi alle caratteristiche dei segmenti di programma, del gruppo DGROUP, delle direttive ASSUME e degli underscore; in presenza della direttiva .MODEL, che specifica il modello di memoria e il linguaggio di riferimento, tutti questi aspetti vengono gestiti direttamente dall'assembler!
La presenza della direttiva .MODEL permette di delegare all'assembler anche i dettagli relativi al tipo NEAR o FAR delle procedure; in relazione agli identificatori pubblici, le necessarie direttive PUBLIC possono essere inserite all'inizio del programma, oppure all'interno dei segmenti contenenti le definizioni degli identificatori stessi.
In relazione, invece, agli identificatori esterni, come è stato sottolineato in precedenza, si raccomanda vivamente di inserire le direttive EXTRN all'inizio del programma, al di fuori di qualsiasi segmento; in questo modo lasciamo al linker il compito di individuare la coppia Seg:Offset relativa ad ogni identificatore.

Se al posto di PROTO si utilizza la direttiva EXTRN per le procedure esterne, bisogna indicare il tipo PROC al posto di NEAR o FAR; si può scrivere, ad esempio:
EXTRN printf: PROC, printstr_c: PROC
In ogni caso, si raccomanda vivamente l'utilizzo della direttiva PROTO; in questo modo possiamo anche specificare il prototipo di funzioni C che richiedono un numero variabile di argomenti.
Un esempio pratico è rappresentato dalla funzione di libreria printf; il prototipo di printf in versione MASM è:
printf PROTO :WORD, :VARARG

30.6.4 Ulteriori dettagli sul programma CMOD1.EXE

L'esempio appena presentato, pur essendo molto semplice, ci permette di analizzare alcuni aspetti molto delicati; in particolare, come è stato detto in precedenza, è fondamentale che ci sia una perfetta corrispondenza tra definizioni e dichiarazioni relative agli identificatori con linkaggio esterno presenti nei vari moduli C e Assembly.
Possiamo notare, ad esempio, che nel modulo CMOD1.C la variabile strc è stata definita come vettore di char (cioè, di BYTE); di conseguenza, la direttiva EXTRN presente nel modulo ASMMOD1.ASM deve dichiarare strc come variabile di tipo BYTE, cioè come variabile che permette di accedere a locazioni di memoria da 8 bit ciascuna.
Se nel modulo CMOD1.C proviamo a modificare la definizione di strc scrivendo:
char *strc = "Stringa definita nel modulo CMOD1.C";
possiamo subito constatare che il programma non funziona più (o funziona in modo anomalo); ciò accade in quanto, nel modulo ASMMOD1.ASM, la variabile esterna strc viene gestita come dato di tipo BYTE, mentre nel modulo CMOD1.C la variabile strc viene definita come puntatore a char e cioè, come indirizzo di una locazione di memoria da 8 bit!
Nel modello di memoria SMALL un indirizzo è formato dalla sola componente Offset a 16 bit; di conseguenza, nel modulo ASMMOD1.ASM dobbiamo ridichiarare strc come:
EXTRN strc: WORD
Non solo, ma tenendo anche conto del fatto che questa volta strc rappresenta l'offset iniziale di un vettore di char, all'interno della procedura printstr_asm dobbiamo sostituire l'istruzione:
invoke printf, offset strFmt, offset strc
con l'istruzione:
invoke printf, offset strFmt, strc
Lo stesso discorso vale, naturalmente, per la variabile strasm; se vogliamo che questa variabile sia un puntatore ad un vettore di char, nel blocco .DATA del modulo ASMMOD1.ASM possiamo scrivere: A questo punto, nel modulo CMOD1.C dobbiamo ridichiarare strasm come:
extern char *strasm;
All'interno di printstr_c, la riga:
printf("%s\n", strasm);
non necessita di alcuna modifica in quanto in C, come sappiamo, il nome di un vettore viene trattato come indirizzo iniziale del vettore stesso; di conseguenza, anche se strasm è un vettore di char, la funzione printf riceve in ogni caso l'indirizzo iniziale della stringa.

Un altro aspetto interessante riguarda l'eventualità di dover scavalcare il modello di memoria SMALL per poter utilizzare procedure di tipo FAR; anche in questo caso bisogna ricordarsi che le dichiarazioni e le definizioni nei vari moduli devono coincidere!
Supponiamo, ad esempio, di voler ridefinire printstr_asm come procedura di tipo FAR; a tale proposito, nel modulo ASMMOD1.ASM l'intestazione di printstr_asm diventa:
printstr_asm proc far
Di conseguenza, nel modulo CMOD1.C dobbiamo ridichiarare printstr_asm come:
extern void far printstr_asm(void);
Se non prendiamo questa precauzione, il programma va sicuramente in crash e in certi casi può anche andare in crash l'intero SO!

30.6.5 Programma CMOD1.EXE con modello LARGE

La struttura del precedente programma può variare a seconda del modello di memoria usato; in particolare, il programmatore deve prestare particolare attenzione agli indirizzamenti relativi ai dati del programma.
Nei modelli di memoria che comportano la presenza di un unico segmento di dati, tutti gli indirizzamenti predefiniti per i dati sono di tipo NEAR; in un caso del genere, infatti, per tutta la fase di esecuzione del programma abbiamo DS=DGROUP con DGROUP che raggruppa gli eventuali segmenti di dati _DATA, _BSS, CONST e STACK.
Nei modelli di memoria che comportano la presenza di un unico segmento di codice, tutti gli indirizzamenti predefiniti per le procedure sono di tipo NEAR; in un caso del genere, infatti, per tutta la fase di esecuzione del programma abbiamo CS=_TEXT.
Nei modelli di memoria che comportano la presenza di due o più segmenti di codice, tutti gli indirizzamenti predefiniti per le procedure sono di tipo FAR; in un caso del genere, infatti, il caller può trovarsi in un segmento di programma diverso da quello della procedura chiamata. In generale, quindi, la chiamata di una procedura rende necessaria anche la modifica di CS; questa situazione è stata già illustrata nel precedente capitolo e viene gestita automaticamente dall'assembler grazie all'apposito parametro specificato nella direttiva .MODEL.

Sicuramente, il caso più interessante e delicato riguarda quei modelli di memoria che prevedono la presenza di due o più segmenti di dati; come sappiamo, il compilatore inizializza DS ponendo, in ogni caso, DS=DGROUP.
Si può presentare allora il caso della chiamata di una procedura che deve accedere con DS ad un segmento dati; se tale segmento dati appartiene a DGROUP non ci sono problemi in quanto non c'è bisogno di modificare DS. I problemi, invece, si presentano quando la procedura deve accedere con DS ad un blocco FAR_DATA o FAR_BSS; in tal caso è fondamentale che la procedura restituisca il controllo al caller solo dopo aver ripristinato DS.
Supponiamo, ad esempio, di riscrivere il modulo ASMMOD1.ASM per il modello di memoria LARGE e supponiamo anche di avere in tale modulo il seguente blocco dati: Se la procedura printstr_asm deve accedere per nome a farDword, si presenta il problema della modifica di DS che referenzia DGROUP; per evitare tale modifica, all'interno di printstr_asm potremmo utilizzare ES scrivendo: In questo modo possiamo restituire il controllo alla funzione main del modulo CMOD1.C con DS che continua a referenziare DGROUP; se, però, vogliamo accedere ad una grande quantità di dati presenti in .FARDATA e non vogliamo usare i segment override (che tra l'altro aumentano le dimensioni del codice macchina), possiamo anche servirci di DS scrivendo: Anche in questo caso, il registro DS viene restituito inalterato al caller; se non prendiamo questa precauzione, il crash del programma è assicurato!

In presenza dei modelli di memoria come COMPACT, LARGE e HUGE che prevedono la presenza di due o più segmenti di dati, bisogna ricordarsi che l'accesso ai dati stessi potrebbe richiedere in questo caso indirizzi di tipo FAR; vediamo un esempio pratico che consiste nell'utilizzare il modello LARGE per il programma presentato in precedenza.
Il modulo CMOD1.C rimane inalterato in quanto tutti i dettagli relativi agli indirizzamenti vengono gestiti dal compilatore C; in particolare, la funzione printstr_c diventa automaticamente FAR e, inoltre, la funzione printf, chiamata da printstr_c, oltre a diventare anch'essa FAR, riceve come argomenti gli indirizzi Seg:Offset relativi, sia alla stringa di formato, sia alla stringa strasm da visualizzare.
All'interno del modulo ASMMOD1.ASM, invece, tutti questi aspetti ricadono sul programmatore il quale può semplificarsi la vita grazie alle caratteristiche avanzate; la Figura 30.7 illustra le modifiche apportate al modulo ASMMOD1.ASM (il quale contiene anche il segmento .FARDATA descritto in precedenza). La presenza del parametro LARGE nella direttiva .MODEL, rende automaticamente FAR tutte le procedure prive di modificatore del modello di memoria.
Vediamo ora quello che succede all'interno di printstr_asm. La prima chiamata di printf serve per visualizzare la stringa strc; questa volta, però, printf per gli argomenti di tipo indirizzo richiede coppie Seg:Offset. Siccome gli argomenti strFmt e strc sono definiti sicuramente nel blocco DGROUP referenziato da DS, possiamo scrivere:
invoke printf, offset strFmt, ds, offset strc, ds
È importantissimo ricordare che in base alla convenzione Intel, gli indirizzi FAR formati da 16+16 bit devono contenere la componente Seg nella WORD più significativa; siccome il C inserisce i parametri nello stack da destra verso sinistra, nella chiamata di printf dobbiamo posizionare la componente Seg alla destra della corrispondente componente Offset. In questo modo la precedente direttiva INVOKE viene espansa in questo modo: Come si può notare, per ogni coppia Seg:Offset viene inserita nello stack, prima la componente Seg e poi la componente Offset; all'interno dello stack quindi, la componente Offset precede la relativa componente Seg.

La seconda chiamata di printf serve per visualizzare una variabile farDword di tipo unsigned long, definita nel blocco dati inizializzati .FARDATA; prima di tutto accediamo a questa variabile per nome e, a tale proposito, utilizziamo DS per referenziare .FARDATA.
Dopo aver modificato farDword, memorizziamo questa variabile in EBX e ripristiniamo DS in modo da fargli referenziare di nuovo DGROUP; a questo punto chiamiamo printf per visualizzare il contenuto di EBX.
Per indicare a printf che EBX contiene un intero senza segno a 32 bit, dobbiamo utilizzare la stringa di formato strFmt2; questa stringa viene definita in DGROUP, per cui è fondamentale che printf riceva, attraverso DS, la giusta componente Seg di strFmt2.
In sostanza, se dopo aver modificato farDword dimentichiamo di ripristinare DS, la successiva chiamata di printf potrebbe anche mandare in crash il programma; si tenga presente, inoltre, che al termine di printstr_asm il controllo tornerebbe alla funzione main con DS che referenzia ancora .FARDATA.

L'esempio di Figura 30.7 dimostra chiaramente per quale motivo, le convenzioni C, deleghino al caller il compito di ripulire gli argomenti dallo stack; ciò è dovuto al fatto che, come sappiamo, in C è possibile creare procedure dotate di un numero variabile di parametri.
In Figura 30.7, ad esempio, notiamo che printf viene chiamata una prima volta con 4 argomenti e una seconda volta con 3 argomenti; ovviamente, solo il caller conosce la dimensione totale in byte degli argomenti passati alla procedura, per cui è suo compito gestire manualmente la pulizia dello stack attraverso SP!

La generazione dell'eseguibile con MASM e BCC si ottiene con i comandi:
ml /c /Cp asmmod1c.asm
bcc -ml -3 cmod1.c asmmod1c.obj
(si noti il parametro -ml che richiede a BCC.EXE l'uso del modello di memoria LARGE).

30.6.6 Funzioni che restituiscono strutture per valore

Passiamo ora ad un secondo esempio che illustra il caso di una funzione C la quale restituisce una intera struttura per valore; se la struttura occupa 3 byte o più di 4 byte, viene adottato un procedimento che comporta le fasi seguenti: In sostanza, la funzione chiamata deve riempire una struttura che si trova in memoria all'indirizzo Seg:Offset; indipendentemente dal modello di memoria utilizzato, tale indirizzo deve essere sempre di tipo FAR.
Si può notare che la fase 4 appare superflua in quanto il caller conosce già l'indirizzo Seg:Offset della struttura che esso stesso ha creato; in ogni caso, si tratta di una convenzione del linguaggio C che il programmatore è tenuto a seguire.

Se il caller si trova in un modulo C, le fasi 1 e 2 vengono gestite direttamente dal compilatore; se, invece, il caller si trova in un modulo Assembly, tutto questo lavoro ricade come al solito sul programmatore.

Per illustrare in pratica questi concetti, vediamo un esempio formato dai due moduli CMOD2.C e ASMMOD2.ASM; anche in questo esempio sono presenti chiamate incrociate tra i due moduli.
Il modulo CMOD2.C definisce due strutture dati e chiama il modulo ASMMOD2.ASM per sommarle tra loro; il modulo ASMMOD2.ASM definisce due strutture dati e chiama il modulo CMOD2.C per sommarle tra loro.
Analizziamo prima di tutto la struttura del modulo CMOD2.C che svolge, come al solito, il ruolo di modulo principale del programma; il listato è visibile in Figura 30.8. Questo modulo dichiara un nuovo tipo di dato Point3d, che rappresenta una struttura formata da tre membri di tipo int denominati x, y e z; in totale, ciascun dato di tipo Point3d richiede quindi 6 byte di memoria.
In seguito vengono definite e inizializzate due variabili globali di tipo Point3d, denominate point1 e point2; come si può notare, queste due variabili hanno classe di memoria static per cui risultano invisibili all'esterno del modulo CMOD2.C.
Appena si entra nella funzione principale main, viene creata una nuova variabile di tipo Point3d chiamata psomma; naturalmente, si tratta di una variabile locale che verrà quindi creata nello stack. A questo punto main chiama la funzione esterna sommapoint3d_asm che ha il compito di sommare le due strutture point1 e point2, restituendo poi il risultato alla funzione main che provvederà a salvarlo nella struttura psomma; in questo esempio, per somma di due strutture Point3d si intende la somma dei membri corrispondenti (x con x, y con y e z con z).

In base a quanto detto in precedenza, quando il compilatore C incontra la chiamata di sommapoint3d_asm, la espande nelle seguenti istruzioni (modello di memoria SMALL): Come si può notare, il C inserisce nello stack i membri delle due strutture seguendo il solito ordine da destra a sinistra; alla fine il C inserisce "di nascosto" nello stack anche la coppia Seg:Offset relativa alla struttura psomma che rappresenta il valore di ritorno di sommapoint3d_asm. Osserviamo che psomma è una variabile locale, per cui il suo offset all'interno dello stack viene calcolato con l'istruzione LEA.
Quando sommapoint3d_asm termina, la struttura psomma è stata riempita con la somma tra point1 e point2 e ciò può essere verificato attraverso la printf che visualizza proprio i tre membri di psomma; si noti che il caller effettua la pulizia dello stack sommando ben 16 byte a SP.

Passiamo ora all'analisi del modulo ASMMOD2.ASM; supponendo di utilizzare il modello di memoria LARGE, il modulo ASMMOD2.ASM assumerà l'aspetto mostrato in Figura 30.9. Anche ASMMOD2.ASM dichiara un nuovo tipo di dato Point3d che ha le stesse caratteristiche di quello dichiarato nel modulo CMOD2.C; è importante ricordare che in qualsiasi linguaggio di programmazione, le dichiarazioni non occupano memoria in quanto sono solo dei modelli di dati o di procedure.

In seguito ASMMOD2.ASM definisce e inizializza, nel blocco _DATA, due variabili point3 e point4 di tipo Point3d; in assenza della direttiva PUBLIC, queste due variabili risultano invisibili all'esterno del modulo.
All'interno del blocco _BSS viene, invece, definita la variabile non inizializzata psomma di tipo Point3d; anche in questo caso abbiamo a che fare con una variabile con linkaggio interno, che non ha niente a che vedere quindi con la variabile locale psomma definita nel modulo CMOD2.C.

Per capire meglio quello che succede all'interno della procedura sommapoint3d_asm, è necessario analizzare il significato che viene assunto in Assembly dai nomi dei membri di una struttura; consideriamo a tale proposito la variabile point3 di tipo Point3d. Osserviamo subito che: Quando la CPU incontra una istruzione del tipo:
mov ax, point3.x
trasferisce quindi in AX la WORD che si trova in memoria all'indirizzo che si ottiene sommando 0 byte all'offset iniziale di point3.

Quando la CPU incontra una istruzione del tipo:
mov ax, point3.y
trasferisce quindi in AX la WORD che si trova in memoria all'indirizzo che si ottiene sommando 2 byte all'offset iniziale di point3.

Quando la CPU incontra una istruzione del tipo:
mov ax, point3.z
trasferisce quindi in AX la WORD che si trova in memoria all'indirizzo che si ottiene sommando 4 byte all'offset iniziale di point3.

In sostanza, i membri x, y e z vengono trattati come se fossero variabili definite all'interno di un blocco dati chiamato point3; da queste considerazioni segue, inoltre, che l'istruzione:
mov ax, point3.x
equivale a:
mov ax, point3[x]
oppure a:
mov ax, [point3][x]
oppure a:
mov ax, [point3+x]
oppure a:
mov ax, [point3.x]
Naturalmente, al posto di x, y e z si possono utilizzare direttamente i corrispondenti spiazzamenti 0, 2 e 4; in tal caso è proibito l'uso dell'operatore . (punto).

Se vogliamo accedere per indirizzo ad una variabile di tipo Point3d, siamo tenuti a gestire correttamente lo spiazzamento dei vari membri; se BX punta, ad esempio, all'indirizzo di point3, allora:
mov ax, [bx+0]
carica in AX il membro x di point3.
mov ax, [bx+2]
carica in AX il membro y di point3.
mov ax, [bx+4]
carica in AX il membro z di point3.

Come al solito, sono permesse anche le forme sintattiche esposte in precedenza; possiamo affermare quindi che, ad esempio:
mov ax, [bx+2]
può essere scritto anche come:
mov ax, [bx][2]
oppure:
mov ax, bx[2]
È proibito, invece, scrivere:
mov ax, [bx.2]
Una volta chiariti questi concetti, torniamo al nostro programma e supponiamo che, nel modulo CMOD2.C, la funzione main abbia appena chiamato sommapoint3d_asm passandole, come argomenti, point1 e point2; main passa, inoltre, un ulteriore argomento "nascosto" che rappresenta l'indirizzo Seg:Offset della struttura psomma definita nello stesso modulo CMOD2.C. In Assembly è compito del programmatore tenere conto di tale convenzione; osserviamo, infatti, che la macro arguments specifica anche il primo argomento nascosto (p_addr) inviato da sommapoint3d_c!
Il primo compito svolto da sommapoint3d_asm consiste nel chiamare la funzione sommapoint3d_c, definita in CMOD2.C, per effettuare la somma tra point3 e point4; tale somma verrà poi memorizzata nella struttura psomma definita nel modulo ASMMOD2.ASM.
Questa volta è compito del programmatore passare a sommapoint3d_c la coppia Seg:Offset della "struttura di ritorno" psomma; a tale proposito si può notare che vengono inserite nello stack anche le componenti Seg (DS=DGROUP) e Offset di psomma.

La funzione sommapoint3d_c somma direttamente p2 a p1 e copia poi il contenuto di p1 nella struttura di ritorno psomma (definita in ASMMOD2.ASM). È necessario ricordare che nel passaggio degli argomenti per valore, vengono create nello stack delle copie degli argomenti stessi; tali copie non hanno niente a che vedere con gli originali. Nel nostro caso, il passaggio per valore a sommapoint3d_c degli argomenti point3 e point4, comporta la creazione nello stack delle copie p1 e p2 degli argomenti stessi; sommando p2 a p1, non provochiamo quindi alcuna modifica all'argomento originale point3.

Per dimostrare che sommapoint3d_c ha veramente sommato point3 con point4 restituendo il risultato in psomma, utilizziamo come al solito la printf del C; come si può notare, la printf si serve della stringa di formato strFmt per visualizzare i tre membri di psomma.

Il compito successivo svolto da sommapoint3d_asm consiste nel sommare le copie di point1 e point2 passate per valore da main; prima di tutto carichiamo in ES:BX la coppia Seg:Offset della struttura di ritorno. L'istruzione utilizzata è:
les bx, p_addr
(è una buona abitudine preservare sempre il contenuto dei registri di segmento, come ES, anche se non vengono utilizzati dal compilatore C).

A questo punto effettuiamo le varie somme con istruzioni del tipo: Un'ultima osservazione riguarda il fatto che sommapoint3d_asm, prima di terminare, carica in DX:AX l'indirizzo p_addr della struttura di ritorno; ciò è richiesto dalle convenzioni del linguaggio C.

Come ci dobbiamo comportare se vogliamo creare la struttura psomma all'interno di sommapoint3d_asm (cioè, nello stack)?
Prima di tutto, subito dopo l'intestazione di sommapoint3d_asm dobbiamo scrivere:
LOCAL psomma :Point3d
La chiamata a sommapoint3d_c diventa:
invoke sommapoint3d_c, addr psomma, ss, point3, point4
Per generare l'eseguibile CMOD2.EXE con BCC dobbiamo prima assemblare con MASM il modulo ASMMOD2.ASM; a questo punto dobbiamo impartire il comando:
bcc -ml -3 -M cmod2.c asmmod2.obj

30.6.7 Interscambio di variabili di tipo Floating Point

Analizziamo ora un esempio che illustra l'interscambio di numeri in floating point tra moduli C e moduli Assembly; i numeri in floating point possono essere gestiti, sia via hardware, sia via software.
Per la gestione via hardware è necessario un coprocessore matematico (FPU) che ormai si trova incorporato su tutte le CPU 80486 e superiori; tutti i dettagli sull'uso della FPU vengono esposti in un apposito capitolo della sezione Assembly Avanzato.
Se non si dispone di una FPU è necessario ricorrere ad apposite procedure contenenti algoritmi di simulazione che permettono alla normale CPU di gestire (molto più lentamente) i numeri reali; anche questi aspetti vengono illustrati nella sezione Assembly Avanzato.
I compilatori C forniscono una libreria matematica standard che sfrutta l'eventuale presenza di una FPU; in assenza della FPU viene utilizzata una apposita libreria di emulazione fornita ugualmente dal compilatore.

Per il semplice esempio illustrato più avanti è sufficiente sapere che la FPU dispone di 8 registri a 80 bit che in MASM vengono denominati: ST(0), ST(1), ST(2), ST(3), ST(4), ST(5), ST(6), ST(7); questi 8 registri formano una struttura a stack di cui ST(0) è la cima (TOS).
Ogni volta che si carica un numero (intero o reale) nella FPU, questo numero viene convertito in un formato chiamato temporary real a 80 bit e viene sistemato in ST(0); il vecchio contenuto di ST(0) scala di un posto e si porta in ST(1), il vecchio contenuto di ST(1) scala di un posto e si porta in ST(2) e così via, sino al vecchio contenuto di ST(6) che scala di un posto e si porta in ST(7). Bisogna prestare attenzione al fatto che il vecchio contenuto di ST(7) viene perso; di conseguenza, il programmatore deve tenere traccia dei vari inserimenti in modo da evitare la perdita di dati ancora in uso.

Le istruzioni della FPU iniziano tutte con la lettera F che significa fast (veloce); nel nostro esempio utilizziamo le seguenti istruzioni: L'istruzione FWAIT (che equivale a WAIT) era necessaria per evitare che le vecchie CPU tentassero di accedere ad una locazione di memoria nella quale la FPU doveva ancora salvare il risultato di un calcolo; con le CPU 80486 e superiori, questa istruzione non è più necessaria e il suo utilizzo viene ignorato.
Le istruzioni FLD, FMUL e FSTP presuppongono che l'operando op sia un numero reale in formato IEEE.

Il programma di esempio è formato dai due moduli CMOD3.C e ASMMOD3.ASM; analizziamo prima di tutto il modulo CMOD3.C che ricopre il ruolo di modulo principale del programma. Come si può notare, il modulo CMOD3.C definisce due variabili lato1 e lato2 di tipo double (numero reale a 64 bit); queste due variabili sono static per cui risultano invisibili all'esterno.
All'interno della funzione principale main viene chiamata la funzione esterna diagonale_asm che riceve lato1 e lato2 come argomenti; diagonale_asm utilizza il teorema di Pitagora per calcolare la diagonale del rettangolo che ha lato1 e lato2 come lati. Il risultato ottenuto viene restituito come valore di ritorno; in sostanza, viene effettuato il calcolo:
diagonale = SQRT((lato1 * lato1) + (lato2 * lato2))
(SQRT = radice quadrata)

Per capire come lavora diagonale_asm, dobbiamo analizzare il modulo ASMMOD3.ASM il cui listato è visibile in Figura 30.11. Si noti l'uso della direttiva REAL8 per definire dati a 64 bit in formato IEEE standard; ; ovviamente, dal punto di vista dell'assembler, REAL8 è del tutto equivalente alla direttiva QWORD o DQ. Per definire strFmt viene utilizzata la direttiva BYTE.

Anche nel modulo ASMMOD3.ASM vengono definite due variabili lato3 e lato4 di tipo QWORD (cioè, double a 64 bit); in assenza della direttiva PUBLIC queste due variabili risultano invisibili all'esterno.

La procedura diagonale_asm chiamata da main, chiama a sua volta la funzione esterna diagonale_c definita nel modulo CMOD3.C; questa funzione riceve come argomenti lato3 e lato4 e restituisce la diagonale del corrispondente rettangolo.
Si può notare che diagonale_c utilizza la funzione sqrt appartenente alla libreria matematica del C; notiamo, infatti, che all'inizio del modulo CMOD3.C è presente l'inclusione dell'header standard math.h.

Come è stato detto all'inizio del capitolo, i valori in floating point restituiti da una funzione si devono trovare nel registro ST(0) della FPU; i registri della FPU sono a 80 bit, per cui possono gestire float a 32 bit, double a 64 bit e long double a 80 bit. Nel nostro caso, con l'istruzione FSTP estraiamo un double da ST(0) e lo salviamo nella variabile locale diag a 64 bit creata nello stack da diagonale_asm.
Successivamente, il contenuto di diag viene visualizzato con printf; si noti che la stringa di formato strFmt prevede la visualizzazione di un float con almeno 15 cifre dopo la virgola in modo da dare la possibilità di analizzare la precisione del risultato (il risultato stesso può essere verificato con una calcolatrice scientifica).

Il compito finale svolto da diagonale_asm consiste nel ripetere l'identico calcolo in relazione ai due parametri d1 e d2 ricevuti da main; osserviamo che alla fine del calcolo, il risultato finale è a disposizione di main nel registro ST(0) della FPU.

Come ci si deve comportare quando si ha la necessità di usare direttamente l'istruzione PUSH per inserire nello stack un dato di tipo QWORD?
L'aspetto fondamentale consiste nel fatto che il programmatore deve rispettare la convenzione little endian; nel caso, ad esempio, di un dato come lato3, con il set di istruzioni a 16 bit dobbiamo scrivere: Se abbiamo a disposizione il set di istruzioni a 32 bit possiamo scrivere:

Per generare l'eseguibile CMOD3.EXE con BCC dobbiamo prima assemblare con MASM il modulo ASMMOD3.ASM; a questo punto dobbiamo impartire il comando:
bcc -ms -3 -f287 -M cmod3.c asmmod3.obj
Si noti l'opzione -f287 che richiede al compilatore C l'uso esplicito della FPU.

30.6.8 Allineamento dei dati al BYTE e alla WORD

Molti compilatori C a 16 bit permettono al programmatore di selezionare l'allineamento dei dati al BYTE o alla WORD; chiaramente, lo scopo di tale allineamento è quello di ottimizzare l'accesso ai dati da parte delle CPU con Data Bus formato da 16 o più linee.
Le modalità per la richiesta del tipo di allineamento variano da compilatore a compilatore; nel caso, ad esempio, del Borland C++ 3.1 è necessario selezionare il menu:
Options - Compiler - Code generation - Word alignment
Alternativamente è possibile passare l'opzione -a dalla linea di comando; per gli altri compilatori si possono consultare i relativi manuali (o l'help in linea).

Consideriamo, ad esempio, il seguente blocco dati di un programma C: Se è attivo l'allineamento al BYTE, supponendo che la variabile i1 a 16 bit venga disposta all'offset 0000h del blocco dati, allora la variabile c1 a 8 bit viene disposta all'offset 0002h, mentre la variabile i2 a 16 bit viene disposta all'offset 0003h; in questo caso i2 parte da un offset dispari e le CPU a 16 bit hanno bisogno di due accessi in memoria per leggere o scrivere nella relativa locazione di memoria a 16 bit.
Se, invece, è attivo l'allineamento alla WORD, supponendo che la variabile i1 a 16 bit venga disposta all'offset 0000h del blocco dati, allora la variabile c1 a 8 bit viene disposta all'offset 0002h, mentre la variabile i2 a 16 bit viene disposta all'offset 0004h; in questo caso i2 parte da un offset pari e le CPU a 16 bit hanno bisogno di un solo accesso in memoria per leggere o scrivere nella relativa locazione di memoria a 16 bit.

Nei moduli C tutti questi aspetti vengono gestiti direttamente dal compilatore (abilitando le apposite opzioni); nei moduli Assembly è compito del programmatore tenere conto dell'attributo di allineamento che è stato selezionato per i dati EXTRN definiti in un modulo C.
In generale, è vivamente sconsigliabile scrivere programmi Assembly che cercano di dedurre in modo empirico l'offset di una variabile; utilizzando, invece, il nome simbolico per le variabili statiche e l'istruzione LEA per le variabili dinamiche, si ottiene questa informazione in modo assolutamente sicuro ed affidabile.

30.7 Argomenti dalla linea di comando

Un programma scritto in ANSI C deve contenere una funzione chiamata main che rappresenta il punto in cui il programmatore riceve il controllo; il prototipo standard della funzione main è:
int main(int argc, char *argv[]);
Il parametro argc è un intero che rappresenta il numero di eventuali argomenti passati al programma dalla linea di comando; il parametro argv rappresenta un vettore di puntatori a stringhe, dove ogni stringa è uno degli eventuali argomenti passati al programma dalla linea di comando.
Per convenzione, argv[0] deve puntare al nome del programma (compreso il percorso completo); inoltre, argv[argc] deve puntare a NULL.

Possiamo verificare tutti questi aspetti attraverso il seguente esempio pratico: Per verificare l'output prodotto dal programma ARGVECT.EXE si può eseguire il comando:
argvect arg1 arg2 arg3 arg4
Il fatto che main riceva una lista di argomenti, ci fa capire chiaramente che questo non è il vero entry point del programma; il vero entry point, infatti, si trova in un apposito modulo che in fase di generazione dell'eseguibile verrà collegato dal linker al nostro programma.
Nel caso, ad esempio, del Borland C++ 3.1, questo modulo viene chiamato C0.ASM e si trova nella cartella:
c:\borlandc\lib\startup
Il compito di questo modulo Assembly è quello di effettuare una serie di inizializzazioni che comprendono, ad esempio, la creazione dello stack, la raccolta degli eventuali argomenti passati dalla linea di comando, etc; una volta terminate tutte queste fasi, il modulo C0.ASM chiama una funzione esterna che deve chiamarsi _main.
A questa funzione vengono passati proprio gli argomenti argc e argv descritti in precedenza; per dimostrarlo, riscriviamo l'esempio ARGVECT.C in versione Assembly. Per ottenere l'eseguibile ARGVECT.EXE dobbiamo prima assemblare ARGVECT.ASM con il comando:
ml /c /Cp argvect.asm
e poi chiamare BCC.EXE con il comando:
bcc -ms -3 -M argvect.obj
Come si può notare, è necessario inserire una direttiva EXTRN relativa alla funzione __setargv__ che si occupa dell'inizializzazione di argc e argv; trattandosi di un identificatore FAR, questa direttiva va inserita al di fuori di qualsiasi segmento di programma.
Questi aspetti sono relativi esclusivamente al compilatore Borland C++ 3.1; altri compilatori possono anche richiedere un procedimento differente.

Con il modello di memoria SMALL, all'interno di main troviamo argc a BP+4 e argv a BP+6; il loop arg_loop esegue lo stesso lavoro mostrato nel precedente esempio ARGVECT.C.
Osserviamo che, per definizione, argv è l'indirizzo iniziale di un vettore di indirizzi; in pratica, ogni elemento di argv è l'indirizzo di una stringa. Caricando argv in SI possiamo affermare che: e così via.

È importante notare come la chiamata di printf non danneggia il contenuto di SI; ricordiamo, infatti, che tutte le funzioni del C preservano il contenuto di SI e DI (in ogni caso, sarebbe meglio non fidarsi).

Per maggiori dettagli sui parametri passati ad un programma dalla linea di comando, si può consultare la Figura 13.11 del Capitolo 13; può essere anche interessante rivedere l'esempio presentato nella sezione 15.7.6 del Capitolo 15.

30.8 Assembly inline

I compilatori C più evoluti come il Borland C, il Microsoft C, il Watcom C, etc, supportano una caratteristica molto potente che prende il nome di Assembly inline; tale caratteristica consiste nella possibilità di inserire codice Assembly direttamente nel codice sorgente C!
I compilatori C che supportano l'Assembly inline, sono dotati di appositi assemblatori incorporati; in fase di compilazione del sorgente C, le istruzioni scritte in Assembly inline vengono tradotte direttamente in codice macchina senza effettuare su di esse alcuna ottimizzazione.

La sintassi da utilizzare per la scrittura delle istruzioni Assembly inline varia da compilatore a compilatore; a titolo di esempio, analizziamo il caso del compilatore Borland C++ 3.1. Questo compilatore supporta praticamente l'intero set di istruzioni a 16 bit delle CPU 80286 e l'intero set di istruzioni della FPU 80287.
All'interno di un modulo C, una istruzione Assembly inline deve essere preceduta dalla parola chiave asm; nel caso di un blocco formato da due o più istruzioni, si può inserire il blocco stesso tra la coppia { }.

Analizziamo subito un primo esempio che si riferisce al caso già trattato in precedenza, relativo alla somma tra due strutture dati di tipo Point3d; il listato di questo programma viene illustrato in Figura 30.13. Da questo semplice esempio si intuisce che un programmatore Assembly esperto può fare praticamente di tutto con l'Assembly inline. In Figura 30.14 vediamo un altro esempio che si riferisce al caso, già analizzato, relativo al calcolo della diagonale di un rettangolo. Come si può notare, questa volta il calcolo della diagonale viene effettuato con la FPU senza la necessità di incorporare la libreria matematica del C; si può anche osservare che, all'interno di un blocco asm, gli eventuali commenti devono essere inseriti secondo la sintassi del C.

Non è permesso l'uso di etichette all'interno di un blocco asm; per aggirare questo problema si può scrivere, ad esempio: È importante notare il fatto che la funzione init_vector utilizza il registro SI; di conseguenza, il contenuto originale di SI deve essere preservato.

Naturalmente, non si può pretendere che l'Assembly inline abbia le stesse potenzialità dell'Assembly vero e proprio; non è possibile, ad esempio, utilizzare le macro e non sono supportate diverse direttive.

Per maggiori dettagli sull'Assembly inline si consiglia di consultare i manuali del proprio compilatore C; nel caso del Borland C++ 3.1 si possono reperire tutte le necessarie informazioni attraverso l'help in linea.