Header di un file EXE per DOS


Il formato EXE conferisce ai programmi DOS eseguibili, la massima flessibilità ed efficienza; l'altro formato disponibile in ambiente DOS è il formato COM (COre iMage). La struttura di un eseguibile in formato COM è la più semplice possibile; nel caso degli EXE, invece, è possibile realizzare programmi aventi una struttura estremamente complessa.
Per poter garantire queste caratteristiche, i linker creano dei file EXE che iniziano con una intestazione (header) contenente una serie di informazioni destinate al sistema operativo (DOS); la parte successiva del file EXE contiene il cosiddetto modulo caricabile, cioè i blocchi di codice, dati e stack che, al momento dell'esecuzione, verranno caricati in memoria dal SO.

L'header è formato da una parte di lunghezza fissa chiamata struttura di controllo e da una parte di lunghezza variabile; in ogni caso, la dimensione dell'header è un multiplo intero di 16 byte e non deve mai essere inferiore a 512 byte.

Parte di lunghezza fissa dell'header

Analizziamo ora le caratteristiche, in versione Assembly (sintassi MASM), della struttura di controllo dell'header; per comodità, sulla sinistra della struttura sono stati riportati anche gli offset interni dei vari membri. Analizziamo in dettaglio, il significato dei vari membri di questa importante struttura.

Signature (firma)

Questo campo contiene la stringa 'MZ', formata quindi dai due codici ASCII 4Dh e 5Ah; questa stringa rappresenta le iniziali di Mark Zbikowsky, uno dei programmatori che hanno lavorato alla definizione dello standard EXE. Al momento di caricare un eseguibile in memoria, se il DOS trova questa stringa, tratta il programma come un EXE; in caso contrario lo tratta come un COM.

LastPageSize (lunghezza dell'ultima pagina)

Per maneggiare comodamente un file EXE, il DOS lo suddivide in blocchi da 512 byte ciascuno, chiamati pagine; la dimensione dell'ultimo blocco, ovviamente, sarà sempre minore o uguale a 512 byte e per questo motivo, è necessario il campo LastPageSize, che indica appunto la dimensione in byte dell'ultima pagina.

PageCount (numero pagine)

Questo campo, contiene il numero complessivo delle pagine da 512 byte ciascuna, in cui il DOS suddivide i file EXE; tale numero comprende anche l'ultima pagina. Di conseguenza, la dimensione in byte del file EXE, è pari a:
((PageCount - 1) * 512) + LastPageSize

RelocationRecords (numero elementi rilocabili)

Questo campo contiene il numero degli elementi che formano la cosiddetta tabella di rilocazione; le caratteristiche di questa tabella, vengono descritte più avanti.

HeaderParagraphs (dimensione dell'header in paragrafi)

Questo campo contiene la dimensione in paragrafi (blocchi da 16 byte) dell'header del file EXE; questo significa che la parte del file EXE che verrà caricata in memoria (modulo caricabile), si trova a (HeaderParagraphs x 16) byte di distanza dall'inizio del file EXE stesso.

MinimumAllocation (memoria minima allocata per il programma)

Questo campo indica il numero minimo di paragrafi di memoria aggiuntiva, che verranno allocati dal SO per garantire il corretto funzionamento del programma eseguibile; questi paragrafi aggiuntivi, vengono posti alla fine del blocco di memoria contenente il programma eseguibile e il loro scopo è quello di contenere dati non inizializzati del programma stesso (in particolare, lo stack).
Complessivamente, la memoria minima totale in byte, riservata all'intero programma, sarà pari quindi alla somma tra la dimensione in byte del modulo caricabile e il valore (MinimumAllocation x 16).

MaximumAllocation (memoria massima allocata per il programma)

Questo campo indica il numero massimo di paragrafi di memoria aggiuntiva, che verranno allocati dal SO per il programma eseguibile; in genere, i linker inizializzano questa WORD a FFFFh (65536 paragrafi, pari a 65536x16=1 MiB). Non essendo possibile trovare un blocco di memoria da 1 MiB, il DOS riserva ad ogni programma eseguibile il più grande blocco di memoria che riesce a trovare; anche questi paragrafi aggiuntivi, vengono posti a partire dalla fine del blocco di memoria contenente il programma eseguibile.
Complessivamente, la memoria massima totale in byte che verrà occupata dal programma, sarà quindi pari alla somma tra la dimensione in byte del modulo caricabile e il valore (MaximumAllocation x 16).

InitialSS (valore iniziale di SS)

In questo campo, il linker inserisce la componente Seg dell'indirizzo logico da cui inizia il blocco stack; si tratta chiaramente di un valore destinato ad essere rilocato dal SO.
Supponiamo che un programma venga caricato in memoria a partire dal segmento di memoria StartSeg; il SO provvederà allora a rilocare la componente Seg dell'indirizzo di partenza del blocco stack (StackSeg), ponendo:
StackSeg = InitialSS + StartSeg
Il valore così calcolato, viene caricato in SS.

InitialSP (valore iniziale di SP)

In questo campo, il linker inserisce l'offset iniziale che verrà caricato nel registro SP al momento dell'esecuzione del programma; trattandosi di un offset relativo a SS, non è necessaria alcuna rilocazione.

CheckSum (valore di controllo)

Questo campo contiene un codice generato dal linker, che ha lo scopo di "validare" il file EXE; al momento di caricare il programma in memoria, il SO controlla questo valore per sapere se ha a che fare con un eseguibile valido.
Il codice contenuto in CheckSum, viene determinato dal linker in base ad un apposito algoritmo; nel caso degli eseguibili DOS, l'algoritmo consiste innanzi tutto nel suddividere il file EXE in blocchi da 16 bit (WORD). A questo punto, viene calcolata la somma a 16 bit (senza tenere conto dei riporti) dei valori binari contenuti in tutte queste WORD; nel campo CheckSum viene messo il valore che porta questa somma a FFFFh.
Se il file EXE è formato da un numero dispari di byte, il linker calcola il CheckSum ponendo a zero il byte più significativo dell'ultima WORD. Prima di caricare il programma in memoria, il SO ripete questo calcolo e lo confronta con il risultato ottenuto dal linker; se i due valori non coincidono, il file EXE viene considerato non valido (corrupted).
Come si può notare, si tratta di un sistema di validazione piuttosto banale, che non può certo proteggere un eseguibile dai virus; nelle versioni più recenti del DOS, questo campo viene ignorato.

InitialIP (valore iniziale di IP)

In questo campo, il linker inserisce l'offset dell'entry point del programma, che verrà utilizzato per inizializzare IP; anche in questo caso, trattandosi di un offset relativo a CS, non è necessaria alcuna rilocazione.

InitialCS (valore iniziale di CS)

In questo campo, il linker inserisce la componente Seg dell'indirizzo logico da cui inizia il blocco codice che contiene l'entry point del programma; come accade per il blocco stack, anche questa componente Seg è destinata ad essere rilocata dal SO.
In riferimento allo stesso esempio fatto per lo stack, se il programma viene caricato in memoria a partire dal segmento di memoria StartSeg, il SO esegue la rilocazione della componente Seg dell'indirizzo di partenza del blocco codice (CodeSeg) secondo la formula:
CodeSeg = InitialCS + StartSeg
Il valore così calcolato, viene caricato in CS.

StartOfRelocationTable (inizio della tabella di rilocazione)

Questo campo contiene l'offset, calcolato rispetto all'inizio del file EXE, da cui parte la tabella di rilocazione descritta più avanti.

OverlayNumber (numero di overlay)

Questo campo, viene utilizzato dai programmi che gestiscono le overlay; si tratta di un vecchio sistema che, in situazioni di scarsità di memoria, consente ugualmente l'esecuzione di programmi che richiedono più memoria di quella fisicamente presente sul computer. Attraverso questo sistema, il programma viene suddiviso in tante parti, chiamate appunto overlay; in fase di esecuzione, vengono caricate in memoria solo le parti necessarie del programma, mentre le parti non necessarie, rimangono sul disco. Come è stato già detto, si tratta di un vecchio sistema che ormai non viene più utilizzato; per i programmi che non fanno uso di overlay, il valore di questo campo viene posto a 0000h.

Parte di lunghezza variabile dell'header

La parte di lunghezza variabile dell'header, contiene informazioni relative alle overlay e ai relocation records.

Informazioni sulle overlay

Questa parte dell'header, contiene informazioni legate ai programmi che fanno uso delle overlay; quest'area è riservata ai gestori di overlay e le informazioni in essa contenute non hanno niente a che vedere con il SO.

Tabella di rilocazione

In seguito alla rilocazione che il DOS effettua sulle componenti Seg degli indirizzi iniziali dei vari segmenti di programma, si rende necessaria anche la modifica del codice macchina di tutte le istruzioni che contengono riferimenti a queste stesse componenti Seg; la tabella di rilocazione, contiene un elenco di coppie Seg:Offset (chiamate relocation records), ciascuna delle quali si riferisce ad una componente Seg da rilocare.
La tabella di rilocazione si trova a StartOfRelocationTable byte di distanza dall'inizio del file EXE; il numero di relocation records di questa tabella, è contenuto nel campo RelocationRecords della struttura di controllo. In versione Assembly, ogni relocation record della tabella di rilocazione, assume l'aspetto seguente: Supponiamo, ad esempio, che nel blocco codice CODESEGM di un programma, siano presenti 3 istruzioni che fanno riferimento ad un blocco dati chiamato DATASEGM; supponiamo anche che il linker abbia assegnato a DATASEGM il paragrafo 002Bh e a CODESEGM il paragrafo 004Ch. Nell'ipotesi che i tre riferimenti a DATASEGM si trovino agli offset 003Ah, 01F2h e 028Ch di CODESEGM, otteniamo nella struttura di controllo:
RelocationRecords = 3
Inoltre, la relocation table assumerà la seguente struttura:

----------------------------------------

Se la dimensione dell'header è inferiore a 512 byte, il linker aggiunge un numero opportuno di byte di valore 00h, sino ad arrivare appunto a 512 byte; in ogni caso, la dimensione in byte dell'header deve essere sempre un multiplo intero di 16. Subito dopo l'header, inizia il modulo caricabile, contenente il codice, i dati e lo stack del programma.

Attraverso il contenuto dell'header, possiamo ricavare numerose informazioni sul file EXE; per calcolare, ad esempio, la dimensione in byte del solo modulo caricabile, dobbiamo procedere in questo modo: