![]() |
(Incompleto) MINI CORSO DI ASSEMBLER 8086 |
![]() |
NOTE: DEVE ESSERE APPROFONDITO
TINY: E' il modello che consente la creazione di file .COM.
Tutti i registri di segmento (CS, SS, DS ed ES) contengono lo stesso indirizzo,
quello del Program Segment Prefix del programma. Quando il programma è un .COM,
il registro IP è sempre inizializzato a 100h (256 decimale) e, dal momento che
il PSP occupa proprio 256 byte, l'entry point del programma coincide col primo
byte del file: i conti tornano.
Tanto nei file .COM che nei file .EXE, codice, dati e stack non possono superare
i 64 Kb e tutti i puntatori sono, per default, near. La memoria è dunque gestita
secondo una "mappa" analoga a quella presentata nella figura sottostante
.
Vale la pena di sottolineare che dati globali e statici,
stack e heap condividono il medesimo segmento di memoria: un utilizzo "pesante"
dell'allocazione dinamica della memoria riduce quindi lo spazio disponibile per
le variabili locali e per i dati globali, e viceversa.
SMALL: Nel modello small il segmento del codice è separato da
quello per i dati. I programmi generati con questo modello possono avere fino a
64 Kb di codice eseguibile, ed altri 64 Kb condivisi tra dati statici e globali,
heap e stack. Come si vede nella figura sottostante, anche nei programmi
compilati in modalità small lo spazio utilizzato dai dati globali riduce heap e
stack, e viceversa, ma il valore iniziale di DS ed SS non coincide con quello di
CS, in quanto viene stabilito in base ai parametri presenti nella relocation
table, generata dal linker. E' inoltre disponibile il far heap, nel quale è
possibile allocare memoria da gestire mediante puntatori far.
MEDIUM: Il modello medium è adatto ai programmi di
grosse dimensioni che gestiscono piccole quantità di dati:
infatti, i puntatori per il codice sono tutti a 32 bit (le chiamate a funzione
sono tutte far), mentre i puntatori per i dati, per default, sono a 16 bit come
nel modello small. Analogamente a quest'utlimo, perciò, il modello medium
gestisce un segmento di 64 Kb per dati statici e globali, heap e stack separato
dagli indirizzi del codice, che può invece raggiungere la dimensione (teorica)
di 1 Mb.
Si noti che il codice eseguibile, qualora superi la dimensione di 64 Kb, deve
essere "spezzato" in più moduli .OBJ, ognuno dei quali deve essere di dimensioni
non superiori ai 64 Kb. La generazione di più moduli oggetto presuppone che il
sorgente sia suddiviso in più file, ma è appena il caso di rimarcare che la
dimensione di ogni singolo sorgente non ha alcuna importanza: i limiti accennati
valgono per il codice già compilato. La figura 3 evidenzia che il registro CS è
inizializzato per puntare ad uno dei moduli oggetto.
Nel modello medium, le
funzioni dichiarate esplicitamente near sono richiamabili solo dall'interno
dello stesso modulo oggetto nel quale esse sono definite, in quanto una chiamata
near, gestita con un indirizzo a soli 16 bit, non può gestire "salti"
intersegmento.
COMPACT: Il modello compact può
essere considerato il complementare del modello medium, in quanto genera per
default chiamate near per le funzioni e indirizzamenti far per i
dati: in pratica esso si addice a programmi piccoli, che gestiscono
grandi moli di dati. Il codice non può superare i 64
Kb, come nel modello small, mentre per i dati può essere utilizzato fino ad 1 Mb
(tale limite è teorico, in quanto ogni programma, in ambiente DOS, si scontra
con l'infame "barriera" dei 640 Kb).
La figura successiva evidenzia che, a differenza di quanto avviene nei modelli
tiny, small e medium, DS e SS sono inizializzati con valori differenti: il
programma ha perciò un segmento di 64 Kb dedicato ai dati statici e globali, ed
un altro, distinto, per la gestione dello stack. Lo heap (cioè l'area di RAM
allocabile dinamicamente) occupa tutta la rimanente memoria disponibile ed è
indirizzato per default con puntatori far. Proprio per questa caratteristica
esso è definito heap e non far heap, come avviene invece nel modello small, nel
quale è necessario dichiarare esplicitamente far i puntatori al far heap e si
deve utilizzare farmalloc() per allocarvi memoria.
LARGE: Il modello large genera per
default indirizzamenti far sia al codice che ai dati e si rivela perciò
adatto a programmi di notevoli dimensioni che
gestiscono grandi quantità di dati.
Esso è, in pratica, un ibrido tra i modelli medium (per quanto riguarda la
gestione del codice) e compact (per l'indirizzamento dei dati); codice e dati
hanno quindi entrambi a disposizione (in teoria) 1 Mb.
Il modello large, per le sue
caratteristiche di indirizzamento, è probabilmente il più flessibile, anche se
non il più efficiente. Le funzioni contenute in una libreria compilata per il
modello large possono essere utilizzate senza problemi anche da programmi
compilati per altri modelli: è sufficiente ricordarsi che tutti i puntatori
parametri delle funzioni sono far e che le funzioni devono essere prototipizzate
anch'esse come far: se questi non sono i default del modello di memoria
utilizzato occorre agire di conseguenza. Esempio: abbiamo un sorgente, PIPPO.C,
da compilare con il modello small, nel quale deve essere inserita una chiamata a
funzStr() (che accetta un puntatore a carattere quale parametro e restituisce un
puntatore a carattere) disponibile nella libreria LARGELIB.LIB, predisposta per
il modello large. Alla libreria è associato uno header file, LARGELIB.H, che
contiene il seguente prototipo di funzStr():
char *funz(char *string);
La funzione e i puntatori (il parametro e quello restituito) non sono dichiarati
far, perché nel modello large tutti i puntatori e tutte le funzioni lo sono per
default. Se non si provvede ad informare il compilatore che, pur essendo il
modello di memoria small, funzStr(), i suoi parametri e il valore restituito
sono far, si verificano alcuni drammatici problemi: in primo luogo, lo stack è
gestito come se entrambi i puntatori fossero near. Ciò significa che a funzStr(),
in luogo di un valore a 32 bit, ne viene passato uno a 16; il codice di funzStr(),
però, lavora comunque su 32 bit, prelevando dallo stack 16 bit di "ignota
provenienza" in luogo della vera parte segmento del puntatore. La funzStr(),
inoltre, restituisce un valore a 32 bit utilizzando la coppia di registri DX:AX,
ma la funzione chiamante, aspettandosi un puntatore a 16 bit, ne considera solo
la parte in AX, cioè l'offset. Ma ancora non basta: la chiamata a funzStr()
generata dal compilatore è near, secondo il default del modello small, perciò, a
runtime, la CALL salva sullo stack solo il registro IP (e non la coppia CS:IP).
Quando funzStr() termina, la RETF (far return) estrae dello stack 32 bit e
ricarica con essi la coppia CS:IP; anche in questo caso, 16 di quei 32 bit sono
di "ignota provenienza". Ce n'è quanto basta per bloccare la macchina alla prima
chiamata. E' indispensabile correre ai ripari, modificando come segue il
prototipo in LARGELIB.H
char far * far funzStr(char
far *string);
e dichiarando esplicitamente far i puntatori coinvolti nella chiamata a funzStr():
....
char far *parmPtr, far *retPtr;
....
retPtr = funzStr(parmPtr);
....
A dire il vero, si può evitare di dichiarare parmPtr esplicitamente far, perché
il compilatore, dall'esame del prototipo, è in grado di stabilire quale tipo di
puntatore occorre passare a funzStr() e provvede da sé copiando sullo stack un
puntatore far costruito come DS:parmPtr; la dichiarazione far, comunque, non
guasta, purché ci si ricordi di avere a che fare con un puntatore a 32 bit anche
laddove ciò non è richiesto.
Per facilitare l'uso dei puntatori far nei modelli di memoria tiny, small e
medium sono state di recente aggiunte alle librerie standard nuove versioni
(adatte a puntatori a 32 bit) di alcune funzioni molto usate: accanto a strcpy()
troviamo perciò _fstrcpy(), e così via.
HUGE:
FLAT: Usando questo modello, un programma vede la memoria come un singolo e continuo spazio di indirizzi chiamato linear address space. Il modello base di flat nasconde i meccanismi di segmentazione dell'architettura anche al programmatore applicativo. Per implementare un modello di memoria flat di base devono essere creati almeno 2 segment descriptor, uno per referenziare un segmento codice, l'altro per un segmento dati. Entrambi questi segmenti comunque, sono mappati sull'intero Linear Address Space, ovvero entrambi i segmenti hanno lo stesso valore di base address (cioè 0) e lo stesso valore di limite di 4 Gb. Questo modello di memoria NON segmentato è disponibile su sistemi operativi a 32bit, diciamo, simile al modello Tiny per il fatto che tutto il codice e i dati stanno in un unico segmento di 32 bit. Questo spazio di indirizzi (fisico) non segmentato può essere mappato come memoria read-only, read-write e memory mapped I/O. Per usare il modello Flat prima di usare la direttiva .MODEL bisogna esplicitare .386 o .486 (indovinate un po', indica la modalità del processore che intendiamo usare nel nostro programma, è il set di direttive per selezionare il processore e il coprocessore). Il sistema operativo automaticamente inizializza i registri di segmento al momento del caricamento. CS, DS, ES ed SS occupano tutti il supergruppo FLAT.