Barninga Z
a- a+

I Puntatori far e huge

PUNTATORI 

Puntatori far e huge 

Le considerazioni sin qui espresse, però, aprono la via ad alcuni approfondimenti. In primo luogo, va sottolineato ancora una volta che numPtr occupa 16 bit di memoria, cioè 2 byte, proprio come qualsiasi unsigned int. E ciò è valido anche se il tipo di numero, la variabile puntata, è il float, che ne occupa 4. In altre parole, un puntatore occupa sempre lo spazio necessario a contenere l'indirizzo del dato puntato, e non il tipo di dato; tutti i puntatori come numPtr, dunque, occupano 2 byte, indipendentemente che il tipo di dato puntato sia un int, piuttosto che un float, o un double... Una semplice verifica empirica può essere effettuata con l'aiuto dell'operatore sizeof(). 

int unIntero;
    long unLongInt;
    float unFloating;
    double unDoublePrec;

    int *intPtr;
    long *longPtr;
    float *floatPtr;
    double *doublePtr;

    printf("intPtr:    %d bytes (%d)
" ,sizeof(intPtr),sizeof(int *));
    printf("longPtr:   %d bytes (%d)
" ,sizeof(longPtr),sizeof(long *));
    printf("floatPtr:  %d bytes (%d)
" ,sizeof(floatPtr),sizeof(float *));
    printf("doublePtr: %d bytes (%d)
" ,sizeof(doublePtr),sizeof(double *));

Tutte le printf() visualizzano due volte il valore 2, che è appunto la dimensione in byte di un generico puntatore. L'esempio mostra, tra l'altro, come sizeof() possa essere applicato sia al tipo di dato che al nome di una variabile (in questo caso dei puntatori); se ne trae, infine, che il tipo di un puntatore è dato dal tipo di dato puntato, seguito dall'asterisco. 

Tutti i puntatori come numPtr, dunque, gestiscono un offset da un punto di partenza automaticamente fissato dal sistema operativo in base alle caratteristiche del file eseguibile. E' possibile in C, allora, gestire indirizzi lineari, o quanto meno comprensivi di segmento ed offset? La risposta è sì. Esistono due parole chiave, dette modificatori di tipo, che consentono di dichiarare puntatori speciali, in grado di gestire sia la parte segmento che la parte offset di un indirizzo di memoria: si tratta di far e huge

double far *numFarPtr;

La riga di esempio dichiara un puntatore far a un dato di tipo double. Per effetto del modificatore far, numFarPtr è un puntatore assai differente dal numPtr degli esempi precedenti: esso occupa 32 bit di memoria, cioè 2 word, ed è pertanto equivalente ad un long int. Di conseguenza numFarPtr è in grado di esprimere tanto la parte offset di un indirizzo (nei 2 byte meno significativi), quanto la parte segmento (nei  2 byte più significativi[12]). La parte segmento è utilizzata dalla CPU per caricare l'opportuno registro di segmento, mentre la parte offset è gestita come al solito: in tal modo un puntatore far può esprimere un indirizzo completo del tipo segmento:offset e indirizzare dati che si trovano al di fuori dell'area dati assegnata dal sistema operativo al programma. 

Ad esempio, se si desidera che un puntatore referenzi l'indirizzo 596A:074B, lo si può dichiarare ed inizializzare come segue: 

double far *numFarPtr = 0x596A074B;

Per visualizzare il contenuto di un puntatore far con printf() si può utilizzare un formattatore speciale: 

printf("numFarPtr = %Fp
" ,numFarPtr);

Il formattatore %Fp forza printf() a visualizzare il contenuto di un puntatore far proprio come segmento ed offset, separati dai due punti: 

numFarPtr = 596A:074B

è l'output prodotto dalla riga di codice appena riportata. 

Abbiamo appena detto che un puntatore far rappresenta un indirizzo seg:off. E' bene... ripeterlo qui, sottolineando che quell'indirizzo, in quanto seg:off, non è un indirizzo lineare. Parte segmento e parte offset sono, per così dire, indipendenti, nel senso che la prima è considerata costante, e la seconda variabile. Che significa? la riga 

char far *vPtr = 0xB8000000;

dichiara un puntatore far a carattere e lo inizializza all'indirizzo B800:0000; la parte offset è nulla, perciò il puntatore indirizza il primo byte dell'area che ha inizio all'indirizzo lineare B8000 (a 20 bit). Il secondo byte ha offset pari a 1, perciò può essere indirizzato incrementando di 1 il puntatore, portandolo al valore 0xB8000001. Incrementando ancora il puntatore, esso assume valore 0xB8000002 e punta al terzo byte. Sommando ancora 1 al puntatore, e poi ancora 1, e poi ancora... si giunge ad un valore particolare, 0xB800FFFF, corrispondente all'indirizzo B800:FFFF, che è proprio quello del byte avente offset 65535 rispetto all'inizio dell'area. Esso è l'ultimo byte indirizzabile mediante un comune  puntatore near[13]. Che accade se si incrementa ancora vPtr? Contrariamente a quanto ci si potrebbe attendere, la parte offset si riazzera senza che alcun "riporto" venga sommato alla parte segmento. Insomma, il puntatore si "riavvolge" all'inizio dell'area individuata dall'indirizzo lineare rappresentato dalla parte segmento con uno 0 alla propria destra (che serve a costruire l'indirizzo a 20 bit). Ora si comprende meglio (speriamo!) che cosa si intende per parte segmento e parte offset separate: esse sono utilizzate proprio per caricare due distinti registri della CPU e pertanto sono considerate indipendenti l'una dall'altra, così come lo sono tra loro tutti i registri del microprocessore. 

Tutto ciò ha un'implicazione estremamente importante: con un puntatore far è possibile indirizzare un dato situato ad un qualunque indirizzo nella memoria disponibile entro il primo Mb, ma non è possibile "scostarsi" dall'indirizzo lineare espresso dalla parte segmento oltre i 64Kb. Per fare un esempio pratico, se si intende utilizzare un puntatore far per gestire una tabella, la dimensione complessiva di questa non deve eccedere i 64Kb. 

Tale limitazione è superata tramite il modificatore huge, che consente di avere puntatori in grado di indirizzare linearmente tutta la memoria disponibile (sempre entro il primo Mb). La dichiarazione di un puntatore huge non presenta particolarità: 

int huge *iHptr;

Il segreto dei puntatori huge consiste in alcune istruzioni assembler che il compilatore introduce di soppiatto nei programmi tutte le volte che il valore del puntatore viene modificato o utilizzato, e che ne effettuano la normalizzazione. Con tale termine si indica un semplice calcolo che consente di esprimere l'indirizzo seg:off come rappresentazione di un indirizzo lineare: in modo, cioè, che la parte offset sia variabile unicamente da 0 a 15 (F esadecimale) ed i riporti siano sommati alla parte segmento. In pratica si tratta di sommare alla parte segmento i 12 bit più significativi della parte offset. Riprendiamo l'esempio precedente, utilizzando questa volta un puntatore huge

char huge *vhugePtr = 0xB8000000;

L'inizializzazione del puntatore huge, come si vede, è identica a quella del puntatore far. Incrementando di 1 il puntatore si ottiene il valore 0xB8000001, come nel caso precedente. Sommando ancora 1 si ha 0xB8000002, e poi 0xB8000003, e così via. Sin qui, nulla di nuovo. Al quindicesimo incremento il puntatore vale 0xB800000F, come nel caso del puntatore far

Ma al sedicesimo incremento si manifesta la differenza: il puntatore far assume valore 0xB8000010, mentre il puntatore huge vale 0xB8010000: la parte segmento si è azzerata ed il 16 sottratto ad essa ha prodotto  un riporto[14] che è andato ad incrementare di 1 la parte segmento. Al trentunesimo incremento il puntatore far vale 0xB800001F, mentre quello huge è 0xB801000F. Al trentaduesimo incremento il puntatore far diventa 0xB8000020, mentre quello huge vale 0xB8020000

Il meccanismo dovrebbe essere ormai chiaro, così come il fatto che le prime 3 cifre della parte offset di un puntatore huge sono sempre 3 zeri. Fingiamo per un attimo di non vederli: la parte segmento e la quarta cifra della parte offset rappresentano proprio un indirizzo lineare a 20 bit. 

La normalizzazione effettuata dal compilatore consente di gestire indirizzi lineari pur caricando in modo indipendente parte segmento e parte offset in registri di segmento e, rispettivamente, di offset della CPU; in tal modo, con un puntatore huge non vi sono limiti né all'indirizzo di partenza, né alla quantità di memoria indirizzabile a partire da quell'indirizzo. Naturalmente ciò ha un prezzo: una piccola perdita di efficienza del codice eseguibile, introdotta dalla necessità di eseguire la routine di normalizzazione prima di utilizzare il valore del puntatore. 

Ancora una precisazione: nelle dichiarazioni multiple di puntatori far e huge, il modificatore deve essere ripetuto per ogni puntatore dichiarato, analogamente a quanto occorre per l'operatore di indirezione. L'omissione del modificatore determina la dichiarazione di un puntatore "offset" a 16 bit. 

long *lptr, far *lFptr, lvar, huge *lHptr;

Nell'esempio sono dichiarati, nell'ordine, il puntatore a long a 16 bit lptr, il puntatore far a long lFptr, la variabile long lvar e il puntatore huge a long lHptr

E' forse il caso di sottolineare ancora che la dichiarazione di un puntatore riserva spazio in memoria esclusivamente per il puntatore stesso, e non per una variabile del tipo di dato indirizzato. Ad esempio, la dichiarazione 

long double far *dFptr;

alloca, cioè riserva, 32 bit di RAM che potranno essere utilizzate per contenere l'indirizzo di un long double, i cui 80 bit dovranno essere allocati con  un'operazione a parte[15]. 

Tanto per confondere un poco le idee, occorre precisare un ultimo particolare. I sorgenti C possono essere compilati, tramite particolari opzioni riconosciute dal compilatore, in modo da applicare differenti criteri di default alla gestione dei puntatori. In particolare, vi sono modalità di compilazione che trattano tutti i puntatori come variabili a 32 bit, eccetto quelli esplicitamente dichiarati near. Ne riparleremo descrivendo i modelli di memoria. 

Per il momento è il caso di accennare a tre macro, definite in DOS.H, che agevolano in molti casi la manipolazione dei puntatori a 32 bit, siano essi far o huge: si tratta di MK_FP(), che "costruisce" un puntatore a 32 bit dati un segmento ed un offset entrambi a 16 bit, di FP_SEG(), che estrae da un puntatore a 32 bit i 16 bit esprimenti la parte segmento e di FP_OFF(), che estrae i 16 bit esprimenti l'offset. Vediamole al lavoro: 

 

#include
    ....
    unsigned farPtrSeg;
    unsigned farPtrOff;
    char far *farPtr;
    ....
    farPtr = (char far *)MK_FP(0xB800,0);  // farPtr punta a B800:0000
    farPtrSeg = FP_SEG(farPtr);    // farPtrSeg contiene 0xB800
    farPtrOff = FP_OFF(farPtr);    // farPtrOff contiene 0

Le macro testè descritte consentono di effettuare facilmente la normalizzzione di un puntatore, cioè trasformare l'indirizzo in esso contenuto in modo tale che la parte offset non sia superiore a 0Fh

char far *cfPtr;
    char huge *chPtr;
    ....
    chPtr = (char huge *)(((long)FP_SEG(cfPtr)) << 16)+
        (((long)(FP_OFF(cfPtr) >> 4)) << 16)+(FP_OFF(cfPtr) & 0xF);

Come si vede, dalla parte offset sono scartati i 4 bit meno significativi: i 12 bit più significativi sono sommati al segmento; dalla parte offset sono poi scartati i 12 bit più significativi e i 4 bit restanti sono sommati al puntatore. Il significato degli operatori di shift << e >> e dell'operatore & (che in questo caso non ha il significato di  address of, ma di   and su bit) è descritto più avanti. 

L'indirizzo lineare corrispondente all'indirizzo segmentato espresso da un puntatore huge può essere ricavato come segue: 

char huge *chPtr;
    long linAddr;
    ....
    linAddr = ((((((long)FP_SEG(chPtr)) << 16)+(FP_OFF(chPtr) << 12)) >> 12) &
        0xFFFFFL);

Per applicare tale algoritmo ad un puntatore far è necessario che questo sia dapprima normalizzato come descritto in precedenza. 

E' facile notare che due puntatori far possono referenziare il medesimo indirizzo pur contenendo valori a 32 bit differenti, mentre ciò non si verifica con i puntatori normalizzati, nei quali segmento e offset sono sempre gestiti in modo univoco: ne segue che solamente i confronti tra puntatori huge (o normalizzati) garantiscono risultati corretti. 

 



Ti potrebbe interessare anche

commenta la notizia

C'è 1 commento
Lorenzo
Hai qualche domanda da fare?