Assembly Base con MASM

Capitolo 13: Assembling & Linking


Lo scopo di questo capitolo è quello di illustrare il procedimento che bisogna seguire per convertire in formato eseguibile un programma scritto in linguaggio Assembly; a tale proposito, ci serviremo del modello presentato nel precedente capitolo.

13.1 Programma Assembly di esempio

La Figura 13.1 illustra un programma di esempio chiamato EXETEST.ASM, a partire dal quale otterremo l'eseguibile finale; questo programma fa riferimento ai vari dati, elementari e complessi, illustrati nel precedente capitolo. Il contenuto del programma di Figura 13.1 è estremamente semplice e non necessita di ulteriori spiegazioni; inoltre, attraverso i vari commenti, si possono ricavare tutte le informazioni necessarie per capire lo scopo delle varie dichiarazioni, definizioni, linee di codice, etc.

Osserviamo che, per il segmento di codice CODESEGM, è stato scelto un allineamento di tipo DWORD (ALIGN=4); più avanti vedremo che questa scelta ci permetterà di analizzare alcuni aspetti molto importanti.

13.2 Installazione dell'assembler

L'installazione dell'assembler MASM sul computer, non presenta alcuna difficoltà; a tale proposito, tutte le necessarie informazioni sono disponibili Nella sezione Compilatori assembly, c++ e altri dell’ Area Downloads di questo sito.

Per comodità, in tutti gli esempi presentati nella sezione Assembly Base, si suppone che il MASM sia stato installato nella cartella:
C:\MASM
In questo caso, tutti gli strumenti di sviluppo (assembler, linker, debugger, etc), vengono sistemati nella cartella:
C:\MASM\BIN

13.3 Scrittura del codice sorgente

Come già sappiamo, per la scrittura del codice sorgente dobbiamo munirci di un apposito editor capace di gestire file rigorosamente in formato testo semplice ASCII; qualsiasi altro formato di file, non viene accettato da MASM.
La scelta dell'editor è una questione di gusti personali; in generale, è consigliabile l'uso di un editor rivolto esplicitamente allo sviluppo dei programmi. Infatti, questi particolari editor sono dotati di caratteristiche che si rivelano di grande aiuto per il programmatore; tra le caratteristiche più utili si possono citare, il supporto del copia/incolla, la gestione delle tabulazioni (per l'indentazione del codice), la possibilità di interagire facilmente con il prompt del DOS, etc.
Si possono utilizzare, ad esempio, gli editor forniti in dotazione ai compilatori e agli interpreti C, Pascal, BASIC, etc; bisogna però prestare particolare attenzione al fatto che alcuni di questi editor (come quelli del Visual Basic o del Quick Basic), salvano il codice sorgente in un formato binario che risulta illeggibile con altri editor di testo.

Un'altra possibilità consiste nello scaricare da Internet uno dei numerosi editor per programmatori; molti di questi editor sono scaricabili e utilizzabili con licenza freeware.
In caso di necessità, si rivela validissimo anche l'editor EDIT.COM (o EDIT.EXE) fornito con il DOS; tale editor è presente anche in FreeDOS.

In ambiente Linux sono disponibili diversi ottimi editor, come GEdit, KWrite, Kate, KDevelop, etc; tali editor supportano la cosiddetta "sintassi a colori" che permette di evidenziare con colori differenti le varie componenti del codice sorgente.

Una volta che abbiamo l'editor a disposizione, possiamo procedere con la scrittura del codice sorgente; a tale proposito, è sufficiente eseguire un copia/incolla dalla Figura 13.1 all'editor stesso (tale metodo potrebbe non funzionare con alcuni editor).
Il codice sorgente così ottenuto, deve essere salvato con il nome EXETEST.ASM; per convenzione, un file con estensione ASM rappresenta un modulo contenente codice sorgente Assembly. Ogni linguaggio di programmazione, utilizza alcune estensioni predefinite per i file che contengono il codice sorgente; nel linguaggio C si utilizza l'estensione C, nel C++ si utilizza l'estensione CPP, nel Pascal si utilizza l'estensione PAS e così via. L'estensione predefinita per i file che contengono codice sorgente Assembly è appunto ASM; queste regole non sono obbligatorie, ma è sempre meglio rispettarle per poter individuare facilmente i vari tipi di file.

Per comodità, è opportuno crearsi una cosiddetta cartella di lavoro, nella quale disporre il codice sorgente dei propri programmi Assembly; in tutti gli esempi presentati nella sezione Assembly Base, si suppone che la cartella di lavoro sia:
C:\MASM\ASMBASE

13.4 La fase di assemblaggio (assembling)

La fase di assemblaggio viene svolta, ovviamente, da uno strumento chiamato assembler (assemblatore); nel caso del MASM, l'assembler si chiama ML.EXE.
L'assembler MASM funziona dal prompt del DOS o command line (linea di comando); a seconda dei gusti, si tratta quindi di lanciare FreeDOS da VirtualBox o il Prompt di MS-DOS dal menù Start di Windows o DOSEmu da un terminale di Linux (da ora in poi si parlerà genericamente di "linea di comando" o "command line").

Il compito fondamentale svolto dall'assembler consiste nel convertire il codice sorgente di un modulo Assembly, in un formato binario che prende il nome di object code (codice oggetto); come vedremo tra breve, il codice oggetto contiene, oltre al codice macchina, anche una numerosa serie di informazioni necessarie al linker per poter svolgere la fase successiva.
Per prendere confidenza con l'assembler MASM, dalla command line possiamo impartire il comando:
c:\masm\bin\ml /?
In questo modo, si ottiene la lista delle opzioni che si possono passare all'assembler; attraverso l'uso di queste opzioni, è possibile pilotare il comportamento dell'assembler in base alle nostre specifiche esigenze.

Procediamo ora con la fase di assemblaggio del modulo EXETEST.ASM che, in precedenza, abbiamo salvato nella cartella ASMBASE; prima di tutto dobbiamo posizionarci nella stessa cartella ASMBASE impartendo dalla command line il comando:
cd c:\masm\asmbase
Una volta che ci siamo posizionati nella cartella di lavoro, possiamo finalmente impartire i comandi per l'assemblaggio del nostro programma; nel caso del MASM, il comando da impartire è:
..\bin\ml /c /Fl exetest.asm
Premendo ora il tasto [Invio], parte la fase di assemblaggio del modulo EXETEST.ASM; in questa fase, l'assembler verifica anche la presenza di eventuali errori nel codice sorgente. Per ogni eventuale errore che viene individuato, l'assembler genera una serie di messaggi che indicano, il tipo di errore e in quale linea del codice sorgente si è verificato il problema; in questo caso, bisogna tornare nell'editor per apportare le necessarie correzioni al codice sorgente.
L'assembler individua principalmente due tipi di errore e cioè, errori sintattici ed errori semantici; gli errori sintattici sono in pratica gli errori di battitura che vengono commessi dal programmatore. Ad esempio, per sbaglio si può scrivere MOZ invece di MOV; in questo caso, l'assembler genera un messaggio di errore del tipo:
Syntax error
Gli errori semantici, invece, si riferiscono a parti del codice sorgente prive di significato logico; ad esempio, una istruzione di trasferimento dati da BL ad AX (da Reg8 a Reg16), può essere valida dal punto di vista sintattico, ma semanticamente è priva di senso.

Se il nostro codice sorgente non presenta errori, la fase di assemblaggio si conclude con la generazione del file contenente il codice oggetto; nel caso del nostro esempio, in assenza di diverse indicazioni da parte del programmatore, questo file verrà chiamato EXETEST.OBJ, dove l'estensione OBJ significa appunto object file.
Se si prova a caricare questo file binario in un editor di testo, verrà visualizzata una sequenza incomprensibile di simboli appartenenti al set di codici ASCII; gli editor di testo, infatti, cercheranno di convertire in stringhe di testo quelli che, in realtà, sono i codici macchina delle istruzioni del nostro programma.

13.4.1 Il Listing File

Impartendo ora dal prompt il comando:
dir
verrà mostrata la lista dei file presenti nella cartella ASMBASE; in questo modo si scopre che, oltre a EXETEST.ASM e EXETEST.OBJ, è presente anche un terzo file chiamato EXETEST.LST. L'assembler ha generato questo file, perché noi glielo abbiamo chiesto attraverso una apposita opzione di assemblaggio; nel caso di MASM, si tratta dell'opzione /Fl (l'opzione /c = compile only, indica al MASM di limitarsi al solo assemblaggio del file EXETEST.ASM).
L'opzione /Fl significa generate listing e indica all'assembler di generare un particolare file in formato testo ASCII, chiamato listing file; questo file ci offre l'eccezionale opportunità di analizzare in dettaglio tutto il lavoro svolto da MASM nella fase di assemblaggio di un modulo Assembly. La Figura 13.2 mostra proprio il contenuto del file EXETEST.LST generato da MASM, che possiamo visualizzare con un normale editor di testo. Analizziamo in dettaglio l'importantissimo contenuto di questo file, che ci permette di verificare in pratica, le considerazioni teoriche svolte nei precedenti capitoli.
Notiamo subito che il listing file contiene (nella parte destra) anche il codice sorgente del nostro programma; si tratta di una grande comodità che ci permette di confrontare facilmente ogni linea del nostro programma, con il corrispondente codice ricavato dall'assembler.
Nella parte sinistra del listing file, troviamo una serie di informazioni molto utili; queste informazioni sono disposte su varie colonne. La colonna più a sinistra contiene gli offset delle varie informazioni (codice o dati); nel caso delle dichiarazioni di strutture, viene indicata la dimensione della struttura e l'offset dei vari membri rispetto all'inizio della struttura stessa.
La seconda colonna del listing file è sicuramente la più importante, in quanto contiene il codice macchina generato dall'assembler; analizziamo più da vicino le informazioni contenute in queste colonne.

Come si vede in Figura 13.2, le varie direttive e dichiarazioni, non hanno alcun codice macchina e quindi non provocano nessuna allocazione di memoria; come già sappiamo, infatti, si tratta di semplici disposizioni che il programmatore impartisce all'assembler. La riga contenente la direttiva .386, ad esempio, ha il solo scopo di informare l'assembler sul fatto che vogliamo utilizzare il set di istruzioni a 32 bit della CPU 80386; questa direttiva (come qualsiasi altra direttiva) non ha quindi alcun offset o codice macchina.

Consideriamo ora la riga che contiene la dichiarazione della costante simbolica STACK_SIZE; l'assembler si limita a rilevare che questo nome simbolico è associato al valore costante 0400h. Ogni volta che l'assembler incontra questo nome simbolico in una istruzione o in una espressione costante, lo sostituisce con il valore 0400h che verrà quindi incorporato direttamente nel codice macchina.

Consideriamo poi l'area contenente le varie dichiarazioni di STRUC; anche in questo caso si vede che l'assembler non genera alcun codice macchina e si limita solo a prendere nota delle caratteristiche di ogni singola STRUC.
In particolare, notiamo che per ogni STRUC l'assembler calcola gli offset dei vari membri; è importante ricordare che questi offset, sono calcolati rispetto all'inizio della struttura di appartenenza. In relazione, ad esempio, alla struttura Automobile, l'assembler rileva che: Complessivamente, le istanze di tipo Automobile, richiedono una quantità di memoria pari a:
2 + 2 + 2 + (10 * 4) = 6 + 40 = 46 = 2Eh byte
È importante notare che tutti i codici numerici generati dall'assembler (compresi gli offset), sono espressi implicitamente in esadecimale; ciò dimostra l'importanza per il programmatore Assembly, della conoscenza di questo particolare sistema di codifica.

13.4.2 Assemblaggio di DATASEGM

A questo punto, l'assembler arriva alla linea 50 del listing file, dove incontra l'inizio del segmento di dati DATASEGM; per ciascun dato presente in questo segmento, l'assembler procede al calcolo delle tre informazioni fondamentali che sono, come sappiamo, l'offset, la dimensione in byte e il contenuto iniziale.
Il primo aspetto importante da osservare, riguarda il fatto che l'assembler non è ovviamente in grado di determinare in anticipo l'indirizzo fisico di memoria da cui partirà un determinato segmento di programma; tutto ciò che riguarda il corretto allineamento in memoria dei segmenti di programma, è di competenza del linker. Proprio per questo motivo, l'assembler assegna ad ogni segmento di programma, un indirizzo fisico iniziale simbolico, che è sempre allineato al paragrafo; questo indirizzo fisico è quindi del tipo XXXX0h e può essere associato all'indirizzo logico normalizzato XXXXh:0000h. In fase di assemblaggio, il valore XXXXh assunto dalla componente Seg di questo indirizzo, è del tutto irrilevante; il MASM, ad esempio, utilizza il valore simbolico ----. Ciò che conta, è che l'assembler fa partire sempre da 0000h gli offset relativi alle informazioni contenute in un qualunque segmento di programma; in questa fase, infatti, lo scopo dell'assembler è quello di determinare lo spiazzamento che assume ogni informazione, all'interno del segmento di appartenenza. Ogni volta che l'assembler incontra l'inizio di un nuovo segmento di programma, inizializza $ in modo che la sua componente Offset valga 0000h; a questo punto inizia il calcolo degli offset relativi alle informazioni presenti nel segmento stesso. Nel caso di DATASEGM, il location counter viene inizializzato con DATASEGM:0000h (cioè XXXXh:0000h); a questo punto, l'assembler, come si vede in Figura 13.2, rileva quanto segue: In un segmento di programma con attributo USE16, la componente Offset di un indirizzo logico è un valore a 16 bit; in questo caso, la componente Offset di $ può assumere tutti i valori compresi tra 0000h e FFFFh. È chiaro che, se la componente Offset di $ supera FFFFh, ricomincia a contare da 0000h; in tal caso, l'assembler genera un messaggio di errore del tipo:
Location counter overflow
Analizzando i vari offset assegnati ai dati definiti in DATASEGM, si può notare che l'assembler compatta al massimo le varie informazioni presenti nei segmenti di programma, in modo da non sprecare neanche un byte; in base allora al fatto che vettAuto parte dall'offset 00AFh e occupa 0398h byte, possiamo dire che la dimensione totale di DATASEGM è pari a:
00AFh + 0398h = 0447h byte = 1095 byte
Nella seconda colonna di Figura 13.2, relativamente al blocco DATASEGM, l'assembler mostra gli eventuali valori iniziali assegnati a ciascun dato; tutte queste informazioni ricavate dall'assembler, vengono inserite nel codice oggetto e messe a disposizione del linker.

Esclusivamente in fase di assemblaggio di un programma, il location counter $ è accessibile in lettura al programmatore; nei capitoli successivi vedremo degli esempi pratici sull'utilizzo di questo strumento.

13.4.3 Assemblaggio di CODESEGM

Terminato l'assemblaggio di DATASEGM, l'assembler cerca altri segmenti DATASEGM eventualmente presenti nel modulo EXETEST.ASM; se la ricerca fornisce esito positivo, l'assembler provvede ad assemblare e ad unire tra loro, tutti i DATASEGM aventi gli stessi identici attributi. Nel nostro caso, non essendo presente alcun altro DATASEGM, l'assembler passa al segmento successivo e cioè, a CODESEGM; il location counter viene nuovamente inizializzato con la coppia CODESEGM:0000h. A questo punto, parte l'assemblaggio del blocco CODESEGM; come al solito, se vogliamo verificare il lavoro svolto dall'assembler, possiamo analizzare il listing file di Figura 13.2.

Notiamo subito in Figura 13.2 che, in corrispondenza della direttiva ASSUME, non viene associato alcun offset o codice macchina; è necessario ribadire ancora una volta che le direttive non devono essere confuse con le istruzioni e quindi non comportano nessuna allocazione di memoria.
Successivamente, viene incontrata l'etichetta start; in questo punto, la componente Offset di $ vale 0000h; questo è proprio l'offset che viene assegnato a start. Anche in questo caso, è necessario ricordare che le etichette sono dei semplici "marcatori" che hanno lo scopo di individuare un offset all'interno di un segmento di programma; infatti, in Figura 13.2 notiamo che l'assembler, in corrispondenza di start, non genera alcun codice macchina. Tutto ciò significa che le etichette non occupano memoria (osserviamo che, in questo caso particolare, l'etichetta start indica l'entry point del nostro programma); a dimostrazione del fatto che start non occupa memoria, possiamo notare che subito dopo è presente l'istruzione:
mov ax, DATASEGM
alla quale l'assembler assegna l'offset 0000h, che è lo stesso di start; questa istruzione è la prima in assoluto che verrà elaborata dalla CPU non appena inizierà la fase di esecuzione del nostro programma.
Si tratta chiaramente di un trasferimento dati da Imm16 a Reg16; il codice macchina di questa istruzione, è formato dall'Opcode 1011_w_Reg, seguito da un Imm16. Nel nostro caso, w=1 (operandi a 16 bit) e Reg=AX=000b; si ottiene quindi:
Opcode = 10111000b = B8h
Il valore immediato è DATASEGM, che simbolicamente vale ---- R; di conseguenza, l'assembler genera il codice macchina:
10111000b 0000000000000000b = B8h ---- R
In Figura 13.2 notiamo che MASM indica l'operando Imm16 con ---- R; la R indica che il valore ---- è destinato ad essere modificato dal linker e/o dal SO. Tutte le componenti Seg e Offset destinate ad essere modificate dal linker e/o dal SO, vengono definite relocatables (rilocabili); più avanti vedremo come avviene la fase di rilocazione.

L'istruzione appena esaminata, ha un codice macchina da 3 byte; di conseguenza, l'assembler incrementa di 0003h la componente Offset del location counter e ottiene:
Offset = 0000h + 0003h = 0003h
Quest'offset viene assegnato all'istruzione successiva, che è:
mov ds, ax
Si tratta chiaramente di un trasferimento dati da Reg16 a SegReg; il codice macchina di questa istruzione, è formato dall'Opcode 10001110b=8Eh e dal campo mod_0_SegReg_r/m. Nel nostro caso, mod=11b (entrambi gli operandi sono di tipo registro), r/m=AX=000b e SegReg=DS=11b; si ottiene quindi:
mod_0_SegReg_r/m = 11011000b = D8h
L'assembler genera allora il codice macchina:
10001110b 11011000b = 8Eh D8h
Questo codice macchina occupa 2 byte, per cui, l'assembler incrementa di 0002h la componente Offset del location counter e ottiene:
Offset = 0003h + 0002h = 0005h
Quest'offset viene assegnato all'istruzione successiva.

Nella parte seguente di CODESEGM, sono presenti una serie di trasferimenti di dati e addizioni, che coinvolgono, registri, valori immediati e variabili statiche definite in DATASEGM; lo scopo di queste istruzioni è quello di mostrare che, simboli apparentemente complessi come, ad esempio:
VettAuto[0].Revisioni[8]
non sono altro che banalissimi Disp16.
Consideriamo, ad esempio, l'istruzione di Figura 13.2:
mov varRect.p2.x, 6850
Si tratta di un trasferimento dati da Imm16 a Mem16; il codice macchina di questa istruzione, è formato dall'Opcode 1100011w, seguito dal campo mod_000_r/m (il sottocampo reg è superfluo e vale 000b), da Disp16 e da Imm16. Nel nostro caso, w=1 (operandi a 16 bit), per cui:
Opcode = 11000111b = C7h
La destinazione è di tipo Disp16, per cui mem=00b e r/m=110b; otteniamo quindi:
mod_000_r/m = 00000110b = 06h
Nel segmento DATASEGM di Figura 13.2 si nota che varRect si trova all'offset 0029h; considerando il fatto che ogni Point2d occupa 4 byte, possiamo dire che varRect+p2+x si trova all'offset:
Disp16 = 0029h + 0004h + 0000h = 002Dh = 0000000000101101b
Il valore immediato è:
Imm16 = 6850 = 1AC2h = 0001101011000010b
In definitiva, l'assembler genera il codice macchina:
11000111b 00000110b 0000000000101101b 0001101011000010b = C7h 06h 002Dh 1AC2h
Nell'istruzione che abbiamo appena esaminato, l'identificatore varRect.p2.x è privo della componente Seg; come già sappiamo, ciò è possibile grazie alla precedente direttiva:
ASSUME DS: DATASEGM
In questo modo, l'assembler sa che tutti gli identificatori definiti in DATASEGM (come varRect.p2.x), hanno un offset che deve essere associato alla componente Seg contenuta in DS; naturalmente, è fondamentale che lo stesso DS sia già stato inizializzato con DATASEGM!
Possiamo dire allora che in questo caso, varRect.p2.x viene gestita con un indirizzo di tipo NEAR, formato dalla sola componente Offset (cioè dal Disp16=002Dh); in fase di elaborazione dell'istruzione, la CPU provvede ad associare automaticamente il Disp16 a DS.
Se avessimo scritto, invece: allora l'assembler, all'inizio del precedente codice macchina, avrebbe inserito il prefisso 26h relativo al registro ES (segment override); ciò avviene perché, come sappiamo, ES non è il SegReg predefinito per i segmenti di dati. In un caso del genere, la variabile varRect viene gestita attraverso un indirizzo di tipo FAR, formato da una coppia completa Seg:Offset (cioè ES:002Dh); ogni indirizzo FAR aumenta di 1 byte le dimensioni del codice macchina e, inoltre, essendo formato da 32 bit, comporta un tempo di elaborazione leggermente più lungo rispetto al caso degli indirizzi NEAR.

Vediamo un altro esempio in Figura 13.2, relativo all'istruzione:
add al, byte ptr vettAuto[46].Revisioni[8+3]
Si tratta di una addizione tra Reg8 (destinazione) e Mem8 (sorgente); il nome Revisioni indica un dato a 32 bit, ma grazie all'operatore BYTE PTR, possiamo dire all'assembler che vogliamo accedere ad una locazione di memoria da 8 bit.
Il codice macchina di questa istruzione è formato dall'Opcode 000000dw, seguito dal campo mod_reg_r/m e da un Disp16; nel nostro caso, d=1 (destinazione registro) e w=0 (operandi a 8 bit), per cui:
Opcode = 00000010b = 02h
Il registro destinazione è reg=AL=000b; la sorgente è di tipo Disp16, per cui mem=00b e r/m=110b. Otteniamo quindi:
mod_reg_r/m = 00000110b = 06h
Nel segmento DATASEGM di Figura 13.2 si nota che vettAuto si trova all'offset 00AFh, mentre il membro Revisioni si trova all'offset 0006h (rispetto all'inizio di ogni struttura Automobile); tenendo conto del fatto che 46=002Eh, possiamo dire allora che vettAuto[46].Revisioni[8+3] si trova all'offset:
Disp16 = 00AFh + 002Eh + 0006h + 0008h + 0003h = 00EEh = 0000000011101110b
In definitiva, l'assembler genera il codice macchina:
00000010b 00000110b 0000000011101110b = 02h 06h 00EEh
Vediamo infine un esempio in Figura 13.2, relativo all'istruzione:
mov eax, varDword
Questa volta abbiamo a che fare con un trasferimento dati a 32 bit da Mem32 a Reg32; grazie alla presenza dell'accumulatore, viene utilizzato un codice macchina compatto formato dal solo Opcode 1010000w seguito dal Disp16. Nel modo 32 bit, il bit w=1 indica che gli operandi sono a 32 bit (full size); otteniamo allora:
Opcode = 10100001b = A1h
In Figura 13.2 si nota che varDword si trova all'offset 0003h del segmento DATASEGM; otteniamo quindi:
Disp16 = 0003h = 0000000000000011b
L'assembler genera anche il prefisso 66h (01100110b) che rappresenta l'operand size prefix; alla fine, otteniamo il codice macchina:
01100110b 10100001b 0000000000000011b = 66h A1h 0003h
Come si può notare in Figura 13.2, complessivamente, l'intero contenuto di CODESEGM occupa 0145h+2h=147h byte di memoria, cioè 327 byte.

Un'ultima considerazione riguarda il fatto che, come si vede in Figura 13.2, l'assembler aggiunge una r ad ogni Disp16 presente nelle istruzioni del segmento di codice; anche in questo caso, queste r stanno per relocatable e indicano il fatto che, tutti questi Disp16, sono soggetti ad una eventuale rilocazione da parte del linker. Più avanti verrà chiarito questo importantissimo aspetto.

13.4.4 Assemblaggio di STACKSEGM

Terminato l'assemblaggio di CODESEGM, l'assembler cerca altri segmenti CODESEGM eventualmente presenti nel modulo EXETEST.ASM; se la ricerca fornisce esito positivo, l'assembler provvede ad assemblare e ad unire tra loro tutti i CODESEGM aventi gli stessi identici attributi. Nel nostro caso, non essendo presente alcun altro CODESEGM, l'assembler passa al segmento successivo e cioè, a STACKSEGM; il location counter viene nuovamente inizializzato con la coppia STACKSEGM:0000h. A questo punto, parte l'assemblaggio del blocco STACKSEGM; trattandosi di un segmento contenente esclusivamente dati, la situazione è perfettamente analoga al caso di DATASEGM.
Come possiamo notare in Figura 13.2, l'assembler assegna l'offset 0000h all'unico dato presente, che è un vettore formato da 0400h byte non inizializzati (0400 [00]); di conseguenza, la dimensione complessiva del segmento STACKSEGM è pari proprio a 0400h byte, cioè 1024 byte.

Terminato l'assemblaggio di STACKSEGM, l'assembler cerca altri segmenti STACKSEGM eventualmente presenti nel modulo EXETEST.ASM; se la ricerca fornisce esito positivo, l'assembler provvede ad assemblare e ad unire tra loro, tutti gli STACKSEGM aventi gli stessi identici attributi. Nel nostro caso, non essendo presente alcun altro STACKSEGM, l'assembler passa al segmento successivo; non essendo presente nessun segmento successivo, la fase di assemblaggio può considerarsi terminata.

13.4.5 La Symbol Table

Il lavoro dell'assembler si conclude con la creazione della cosiddetta Symbol Table (tavola dei simboli); la Symbol Table ha una importanza enorme in quanto rappresenta un riassunto di tutte le informazioni fondamentali che l'assembler ha ricavato dal codice sorgente del nostro programma.

Come si può notare in Figura 13.2, la parte iniziale della Symbol Table contiene una lista chiamata Symbols; si tratta dell'elenco di tutti gli identificatori presenti nel nostro programma. Alcune di queste informazioni indicano, la data e l'ora di assemblaggio, il nome del modulo sorgente appena assemblato, la versione dell'assembler, etc; queste informazioni variano da assembler ad assembler e sono riservate all'uso interno dello stesso assembler e del linker.
La parte più importante di questa lista, contiene tutti i nomi simbolici dei dati che abbiamo definito nel nostro programma; a ciascun nome, l'assembler associa due informazioni chiamate, Type e Value. Nel caso, ad esempio, delle costanti simboliche come STACK_SIZE, l'assembler raccoglie le seguenti informazioni:
STACK_SIZE Number 0400h
Ad ogni costante simbolica, viene quindi associato il tipo Number e il valore numerico della costante stessa.

Nel caso dei dati elementari come varFloat64, l'assembler raccoglie le seguenti informazioni:
varFloat64 QWord 001D DATASEGM
Ad ogni dato elementare, viene quindi associata la dimensione in byte, e l'indirizzo logico completo Seg:Offset.

Nel caso dei dati complessi come FiatPanda, l'assembler raccoglie le seguenti informazioni:
FiatPanda Automobile 0081 DATASEGM
Ad ogni dato strutturato, viene quindi associato l'identificatore della dichiarazione di struttura e l'indirizzo logico completo Seg:Offset.

Nel caso delle etichette come start, l'assembler raccoglie le seguenti informazioni:
start L Near 0000 CODESEGM
Ad ogni etichetta, viene quindi associato il tipo di indirizzo (NEAR o FAR) individuato e l'indirizzo logico completo Seg:Offset; nel caso di start, il tipo NEAR indica che per accedere a questa etichetta dall'interno di CODESEGM, basta specificare un indirizzo formato dalla sola componente Offset (la componente Seg è implicitamente CS=CODESEGM).

Come si nota in Figura 13.2, la Symbol Table contiene anche l'elenco delle varie dichiarazioni di struttura e record. Per ciascuna struttura o record, vengono descritte in dettaglio le caratteristiche interne; queste informazioni comprendono, i nomi dei membri, l'ampiezza in byte di ciascun membro e l'offset interno di ciascun membro (espresso in byte).

La Symbol Table contiene anche una lista chiamata Segments and Groups; questa lista comprende le informazioni generali relative a tutti i segmenti di programma presenti nel modulo EXETEST.ASM. Nel caso di Figura 13.2, la lista ricavata dall'assembler è la seguente: Il campo Bit si riferisce alla dimensione in bit degli offset utilizzati per accedere ai vari segmenti di programma; nel nostro caso, questa dimensione è 16 in quanto abbiamo utilizzato per tutti i segmenti l'attributo USE16.

Un'ultima considerazione riguarda il fatto che, come si nota in Figura 13.2, tutti i nomi simbolici presenti nella Symbol Table (compresi i nomi dei segmenti di programma), risultano disposti in ordine alfabetico; questa disposizione è solo una convenzione formale usata dell'assembler e non ha niente a che vedere con la disposizione in memoria dei dati o dei segmenti di programma.

13.5 La fase di linkaggio (linking)

Come molti avranno intuito, il file EXETEST.OBJ, pur contenendo il codice macchina del nostro programma, non è ancora in formato eseguibile; il compito di convertire il codice oggetto in codice eseguibile, spetta ad un altro strumento chiamato linker (collegatore), che è proprio il destinatario delle informazioni generate dall'assembler.

Teoricamente, il contenuto di un file oggetto destinato all'ambiente DOS dovrebbe rispecchiare una struttura standard, indicata con la sigla OMF (Relocatable Object Module Format); in realtà, i vari assembler producono dei file oggetto che, pur seguendo questo standard, si differenziano tra loro per alcuni dettagli (spesso non rilevanti).
La Microsoft ha definito un nuovo standard per gli object file, indicato dalla sigla COFF (Common Object File Format); il formato COFF viene utilizzato da tutti gli strumenti di sviluppo della Microsoft (MASM32, Visual C++, etc) per la realizzazione di applicazioni Windows. Chi volesse studiare in dettaglio gli standard OMF e COFF, può scaricare la documentazione ufficiale nella sezione Documentazione tecnica di supporto al corso assembly dell’ Area Downloads di questo sito.

Un altro aspetto importante, riguarda il fatto che spesso i linker generano eseguibili destinati ad uno specifico SO; nel nostro caso quindi, dobbiamo munirci di un linker capace di generare eseguibili destinati a girare sotto DOS. Nel caso del MASM, bisogna distinguere tra le varie versioni di questo assembler; infatti, il linker (LINK.EXE) fornito con il MASM32 è in grado di generare eseguibili destinati esplicitamente all'ambiente Windows a 32 bit. I linker (LINK.EXE) forniti con le versioni di MASM precedenti la 6.2, sono tutti in grado di generare eseguibili per il DOS. Se si ha a disposizione solo il MASM32, si può scaricare il linker per il DOS nella sezione Compilatori assembly, c++ e altri dell’ Area Downloads di questo sito; nella stessa pagina, sono presenti anche le istruzioni d’installazione.
Alternativamente, è anche possibile utilizzare i linker dei vari compilatori C/C++, Pascal, FORTRAN, BASIC, etc, destinati all'ambiente DOS.

Come è stato spiegato nei precedenti capitoli, nel caso più generale un programma Assembly è formato da due o più moduli di codice sorgente; per ciascuno di questi moduli, l'assembler crea il relativo object file. Il compito del linker è innanzi tutto quello di collegare tra loro i vari moduli; da ciò deriva appunto il nome "linker" (collegatore). In questo modo il linker ottiene un unico modulo, all'interno del quale i vari segmenti di programma vengono combinati tra loro secondo le disposizioni impartite dal programmatore; i segmenti risultanti dalle varie combinazioni, vengono disposti secondo i criteri di ordinamento e allineamento in memoria stabiliti ugualmente dal programmatore.
Come si può facilmente intuire, in seguito alle varie combinazioni e ai successivi allineamenti, può rendersi necessaria la modifica delle componenti Seg:Offset relative alle informazioni presenti nei segmenti di programma; questo lavoro di modifica prende il nome di rilocazione e rappresenta sicuramente il compito più importante e delicato svolto dal linker.
Una volta che il linker ha portato a termine tutto questo lavoro, viene prodotto un file in formato eseguibile; come vedremo tra breve, nel caso più generale un file eseguibile è costituito da due parti principali chiamate, header (intestazione) e modulo caricabile (codice, dati e stack).

Dopo queste importanti precisazioni, possiamo procedere con la generazione del formato eseguibile del nostro programma; a tale proposito, prima di tutto dobbiamo posizionarci nella cartella di lavoro ASMBASE. A questo punto, dobbiamo impartire il seguente comando:
c:\masm\bin\link /map exetest.obj
Premendo il tasto [Invio], parte la fase di linking del file EXETEST.OBJ; anche in questa fase, è possibile che vengano generati dei messaggi di errore (errori del linker), che verranno analizzati nel seguito del capitolo e nei capitoli successivi.
Nel caso dei linker Microsoft, viene chiesto al programmatore di inserire i nomi da assegnare ai vari file che verranno prodotti; premendo [Invio] per ogni richiesta, si accettano i nomi predefiniti proposti dal linker stesso.

Nel dare inizio alla fase di linking, il linker non è ovviamente in grado di conoscere in anticipo l'indirizzo fisico di memoria da cui verrà fatto partire il nostro programma; questa informazione, infatti, verrà decisa dal SO al momento di caricare ed eseguire il programma stesso. Proprio per questo motivo, il linker utilizza simbolicamente l'indirizzo fisico iniziale 00000h; a questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 0000h:0000h. Possiamo dire quindi che, al primo segmento del nostro programma, viene assegnato l'indirizzo logico iniziale 0000h:0000h; osserviamo che questo indirizzo, è compatibile con qualsiasi attributo di allineamento.
Il linker esamina i vari segmenti di programma, nello stesso ordine stabilito dal programmatore; nel caso del nostro esempio (ordinamento predefinito di tipo sequenziale), il linker esamina, prima DATASEGM, poi CODESEGM e infine STACKSEGM.

13.5.1 Linking di DATASEGM

Il primo segmento esaminato dal linker è DATASEGM; a questo segmento viene assegnato l'indirizzo fisico iniziale 00000h (compatibile con l'attributo PARA), a cui corrisponde l'indirizzo logico normalizzato 0000h:0000h. Di conseguenza, il linker ricava:
DATASEGM = 0000h
Gli offset all'interno di DATASEGM, partono dal valore iniziale 0000h, proprio come era stato determinato dall'assembler; di conseguenza, tutti i calcoli effettuati dall'assembler vengono confermati (non è necessaria alcuna rilocazione degli offset dei dati).
Il linker va alla ricerca di altri DATASEGM presenti in altri moduli; nel nostro caso, il programma è formato da un unico modulo, per cui il linker genera la struttura finale di DATASEGM. In base alle informazioni ricavate dall'assembler in Figura 13.2, questa struttura è rappresentata da un blocco da 0447h byte (1095 byte); i vari byte, occupano tutti gli indirizzi fisici compresi tra 00000h e:
00000h + (0447h - 1h) = 00446h
In termini di indirizzi logici, il blocco DATASEGM viene inserito, teoricamente, nel segmento di memoria n. 0000h e occupa tutti gli offset di questo segmento, compresi tra 0000h e:
0000h + (0447h - 1h) = 0446h
In sostanza, DATASEGM è compreso tra gli indirizzi logici 0000h:0000h e 0000h:0446h; all'interno di questo blocco, il linker inserisce eventuali valori iniziali che abbiamo assegnato ai dati del programma (seconda colonna di Figura 13.2 nel blocco DATASEGM). La Figura 13.3, illustra una parte della struttura finale di DATASEGM. Naturalmente, quando DATASEGM verrà caricato in memoria, i vari dati risulteranno disposti in modo consecutivo e contiguo, secondo lo schema di Figura 13.4: In questo schema, gli indirizzi di memoria crescono da sinistra verso destra; di conseguenza, i dati di tipo WORD, DWORD etc, appaiono disposti al contrario rispetto alla loro rappresentazione con il sistema posizionale. Ad esempio, nel caso del dato ABCDh, vediamo che il BYTE meno significativo CDh, si trova alla sinistra del BYTE più significativo ABh; è importante abituarsi anche a questo tipo di rappresentazione, in quanto, molti debugger e editor esadecimali, visualizzano la memoria con gli indirizzi crescenti da sinistra verso destra.
La CPU non incontra alcuna difficoltà nel gestire la situazione, apparentemente incomprensibile, di Figura 13.4; infatti, ogni istruzione che accede a questi dati, codifica l'indirizzo Seg:Offset e l'ampiezza in bit dei dati stessi.

13.5.2 Linking di CODESEGM

Terminato il linking di DATASEGM, il linker va alla ricerca di altri segmenti di classe 'DATA'; nel nostro caso, non esistono altri segmenti di classe 'DATA', per cui il linker passa al segmento CODESEGM.
Per CODESEGM abbiamo richiesto un allineamento di tipo DWORD; di conseguenza, il linker calcola un indirizzo fisico iniziale successivo al blocco DATASEGM e multiplo intero di 4. Abbiamo visto che il blocco DATASEGM termina all'indirizzo fisico 00446h; l'indirizzo fisico successivo a 00446h e multiplo intero di 4 è 00448h. Il linker allora, inserisce un byte di valore 00h (buco di memoria) all'indirizzo fisico 00447h in modo da far partire CODESEGM da 00448h; a questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 0044h:0008h. Di conseguenza, il linker ricava:
CODESEGM = 0044h
Gli offset all'interno di CODESEGM, partono dal valore iniziale 0008h; questo significa che gli offset calcolati dall'assembler, per tutte le istruzioni di Figura 13.2, vengono traslati in avanti di 8 byte dal linker. Ad esempio, l'istruzione alla linea 288 di Figura 13.2:
mov ds, ax
passa dall'offset 0003h all'offset:
0003h + 0008h = 000Bh
Se al posto di CODESEGM ci fosse stato DATASEGM, sarebbe successa la stessa cosa; in sostanza, se DATASEGM fosse partito dall'indirizzo logico normalizzato 0044h:0008h, allora: e così via.

In una situazione del genere, il linker avrebbe dovuto modificare tutti i Disp16 presenti nelle istruzioni del programma; nel caso, ad esempio, dell'istruzione di Figura 13.2:
mov al, [varByte]
il codice macchina:
A0h 0000h
sarebbe stato trasformato dal linker in:
A0h 0008h
Le considerazioni appena esposte illustrano il concetto importantissimo di rilocazione degli offset.

Tornando al linking di CODESEGM, il linker va alla ricerca di altri CODESEGM presenti in altri moduli; nel nostro caso, il programma è formato da un unico modulo, per cui il linker genera la struttura finale di CODESEGM. In base alle informazioni ricavate dall'assembler in Figura 13.2, questa struttura è rappresentata da un blocco da 0147h byte (327 byte); i vari byte, occupano tutti gli indirizzi fisici compresi tra 00448h e:
00448h + (0147h - 1h) = 0058Eh
In termini di indirizzi logici, il blocco CODESEGM viene inserito nel segmento di memoria n. 0044h e occupa tutti gli offset di questo segmento, compresi tra 0008h e:
0008h + (0147h - 1h) = 014Eh
In sostanza, CODESEGM è compreso tra gli indirizzi logici 0044h:0008h e 0044h:014Eh; all'interno di questo blocco, il linker inserisce tutti i codici macchina della seconda colonna di Figura 13.2, relativi al blocco CODESEGM. La Figura 13.5, illustra una parte della struttura finale di CODESEGM. Naturalmente, quando CODESEGM verrà caricato in memoria, i vari codici risulteranno disposti in modo consecutivo e contiguo, secondo lo schema di Figura 13.6. Come al solito, se la memoria viene disegnata con gli indirizzi crescenti da sinistra verso destra, i dati di tipo WORD, DWORD etc, appaiono disposti al contrario rispetto alla loro rappresentazione con il sistema posizionale.
Anche in questo caso, la CPU non ha alcuna difficoltà a gestire tutti questi codici binari; la CPU, infatti, dopo aver letto e decodificato il primo byte di una istruzione, è in grado di sapere se il codice macchina è terminato o se bisogna procedere con la lettura e la decodifica di ulteriori byte. Non appena l'istruzione è stata decodificata, il registro IP (Instruction Pointer) viene incrementato di un numero di byte pari a quello che rappresenta la dimensione dell'istruzione stessa; in questo modo, IP si trova posizionato sempre sulla prossima istruzione da eseguire.

In base al fatto che CODESEGM parte dall'indirizzo logico 0044h:0008h, il linker è ora in grado di determinare l'indirizzo logico Seg:Offset dell'entry point del programma; questo indirizzo verrà poi assegnato dal DOS alla coppia CS:IP. Nel nostro caso, si tratta ovviamente dell'indirizzo logico assegnato all'etichetta start; per questa etichetta, l'assembler aveva calcolato (in Figura 13.2) una componente Offset pari a 0000h. Il linker ha incrementato di 8 byte quest'offset, per cui, l'indirizzo logico di start: è proprio 0044h:0008h; di conseguenza, il linker ottiene:
Entry Point = 0044h:0008h
Più avanti verrà illustrato il metodo che permette al linker di passare l'entry point al DOS.

13.5.3 Linking di STACKSEGM

Terminato il linking di CODESEGM, il linker va alla ricerca di altri segmenti di classe 'CODE'; nel nostro caso, non esistono altri segmenti di classe 'CODE', per cui il linker passa al segmento STACKSEGM.
Per STACKSEGM abbiamo richiesto un allineamento di tipo PARA; di conseguenza, il linker calcola un indirizzo fisico iniziale successivo al blocco CODESEGM e multiplo intero di 16. Abbiamo visto che il blocco CODESEGM termina all'indirizzo fisico 0058Eh; l'indirizzo fisico successivo a 0058Eh e multiplo intero di 16 è 00590h. Il linker allora, inserisce un byte di valore 00h (buco di memoria) all'indirizzo fisico 0058Fh in modo da far partire STACKSEGM da 00590h; a questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 0059h:0000h. Di conseguenza, il linker ricava:
STACKSEGM = 0059h
Gli offset all'interno di STACKSEGM, partono dal valore iniziale 0000h, proprio come era stato determinato dall'assembler; di conseguenza, tutti i calcoli effettuati dall'assembler vengono confermati (non è necessaria alcuna rilocazione degli offset dei dati).
Il linker va alla ricerca di altri STACKSEGM presenti in altri moduli; nel nostro caso, il programma è formato da un unico modulo, per cui il linker genera la struttura finale di STACKSEGM. In base alle informazioni ricavate dall'assembler in Figura 13.2, questa struttura è rappresentata da un blocco da 0400h byte (1024 byte); i vari byte, occupano tutti gli indirizzi fisici compresi tra 00590h e:
00590h + (0400h - 1h) = 0098Fh
In termini di indirizzi logici, il blocco STACKSEGM viene inserito nel segmento di memoria n. 0059h e occupa tutti gli offset di questo segmento, compresi tra 0000h e:
0000h + (0400h - 1h) = 03FFh
In sostanza, STACKSEGM è compreso tra gli indirizzi logici 0059h:0000h e 0059h:03FFh; all'interno di questo blocco, il linker inserisce tutti i valori della terza colonna, riga 447 di Figura 13.2. Si tratta in pratica di 1024 byte non inizializzati; quest'area da 1024 byte è destinata ai dati temporanei del programma.

A questo punto, il linker nota la presenza dell'attributo di combinazione STACK per il segmento STACKSEGM; di conseguenza, il linker procede al calcolo dell'indirizzo logico iniziale Seg:Offset che il DOS assegnerà poi alla coppia SS:SP. Abbiamo visto in precedenza, che il blocco STACKSEGM parte dall'indirizzo logico 0059h:0000h ed occupa 0400h byte; in base a queste informazioni, il linker ricava facilmente l'indirizzo logico 0059h:0400h da passare al DOS.
Se non utilizziamo l'attributo STACK per STACKSEGM, il compito di inizializzare SS:SP (in fase di esecuzione) spetta a noi; in tal caso, dobbiamo scrivere subito dopo l'entry point: La direttiva:
assume ss: STACKSEGM
non è necessaria, a meno che si voglia accedere per nome a eventuali dati statici definiti nel blocco STACKSEGM; come vedremo nei capitoli successivi, è sconsigliabile definire dati statici nel blocco stack di un programma.

Terminato il linking di STACKSEGM, il linker va alla ricerca di altri segmenti di classe 'STACK'; nel nostro caso, non esistono altri segmenti di classe 'STACK', per cui, la fase di linking dei vari segmenti di programma può considerarsi terminata.

13.5.4 Il Map File

Una volta che la fase di linking è terminata, digitando dalla command line il comando:
dir
e premendo [Invio], viene visualizzato, come al solito, l'elenco di tutti i file presenti nella cartella ASMBASE; in questo modo, si scopre che, oltre ai 3 file generati dall'assembler, ci sono anche due nuovi file generati dal linker, con i nomi EXETEST.EXE e EXETEST.MAP.
Naturalmente, EXETEST.EXE rappresenta la versione eseguibile del nostro programma; l'estensione EXE sta, infatti, per executable. Il file EXETEST.MAP, invece, rappresenta il cosiddetto map file; il suo scopo è quello di mostrare un riassunto semplificato di tutto il lavoro che il linker ha compiuto sul file oggetto EXETEST.OBJ.
Per generare il map file con i linker della Microsoft bisogna utilizzare l'opzione /map.
Il map file ci permette di verificare la correttezza di tutti i calcoli che abbiamo effettuato in precedenza; trattandosi di un file di testo, lo possiamo visualizzare con un normale editor ASCII. La Figura 13.7 illustra il map file generato dal linker del MASM. L'esame del file EXETEST.MAP conferma l'esattezza di tutti i calcoli relativi agli indirizzi fisici e logici, che abbiamo effettuato in precedenza; osserviamo, inoltre, che il linker ha rispettato tutte le direttive che avevamo impartito all'assembler in relazione all'ordine con il quale disporre in memoria i vari segmenti di programma.
In Figura 13.7 si nota che anche il linker, come l'assembler, mostra tutte le informazioni numeriche, in formato esadecimale; a maggior ragione, quindi, è importantissimo che i programmatori Assembly abbiano grande dimestichezza con la rappresentazione dei numeri in base 16.

13.6 Struttura di un file EXE

Tra i vari formati eseguibili disponibili per il DOS, il formato EXE è in assoluto il più potente e flessibile; la caratteristica più evidente del formato EXE, è rappresentata dalla possibilità di inserire al suo interno un numero arbitrario di segmenti di programma. In caso di necessità quindi, si possono scrivere programmi EXE formati da numerosi segmenti di dati e di codice; il numero dei vari segmenti presenti in un programma, può crescere sino ad occupare tutta la memoria convenzionale disponibile.

Naturalmente, per poter garantire tutta questa flessibilità, il DOS ha bisogno di conoscere una serie di importanti informazioni, relative alla struttura interna del programma EXE; in particolare, a causa della presenza di due o più segmenti di programma, il DOS deve sapere quale di questi segmenti contiene l'entry point (per poter inizializzare la coppia CS:IP) e quale di questi segmenti verrà usato come stack (per poter inizializzare la coppia SS:SP).
Come abbiamo appena visto, gli indirizzi logici iniziali da assegnare alle coppie CS:IP e SS:SP, sono già stati calcolati dal linker; non bisogna dimenticare però che il linker, assegna a qualsiasi programma un indirizzo fisico iniziale simbolico, pari a 00000h. Come si vede nella Figura 9.7 del Capitolo 9, i primi KiB della RAM sono riservati a svariate informazioni di sistema (vettori di interruzione, area dati del BIOS, kernel del DOS, etc); è assolutamente impossibile quindi, che un programma possa essere caricato in memoria a partire dall'indirizzo fisico 00000h!
Questo significa che anche il DOS dovrà procedere alla rilocazione degli indirizzi logici calcolati dal linker; questa volta, la rilocazione riguarda solo le componenti Seg assegnate a questi indirizzi. Infatti, le componenti Offset delle informazioni presenti nei segmenti di programma, non sono altro che degli spiazzamenti calcolati rispetto all'inizio dei segmenti stessi; questi spiazzamenti sono già stati calcolati (ed eventualmente rilocati) in modo definitivo dal linker e non devono essere più modificati.
Consideriamo, ad esempio, una informazione che si trova all'indirizzo logico 03B2h:0008h; se ora rilochiamo la componente Seg sommandogli il valore 210Ah, otteniamo il nuovo indirizzo 24BCh:0008h. Come si può notare, la componente Offset rimane invariata; in sostanza, l'informazione dell'esempio è passata dall'offset 0008h del segmento di memoria n. 03B2h, all'offset 0008h del segmento di memoria n. 24BCh.

Come conseguenza della rilocazione dei segmenti, il DOS ha anche bisogno di poter modificare il codice macchina di eventuali istruzioni del tipo:
mov ax, DATASEGM
È chiaro, infatti, che, se DATASEGM viene rilocato dal DOS, è anche necessario modificare il codice macchina della precedente istruzione; per poter svolgere questo lavoro, il DOS ha bisogno di conoscere il cosiddetto relocation record di ciascuna istruzione che fa riferimento ad un segmento rilocabile. Il relocation record è l'indirizzo Seg:Offset di un Imm16 che, in una istruzione, rappresenta una componente Seg rilocabile; come esempio pratico, consideriamo proprio la precedente istruzione, alla quale corrisponde, in Figura 13.2, il codice macchina:
B8h 0000h
L'assembler ha assegnato a questo codice macchina, l'offset 0000h calcolato rispetto all'inizio di CODESEGM; il linker ha assegnato a DATASEGM il valore 0000h, a CODESEGM il valore 0044h e ha rilocato (definitivamente) l'offset della precedente istruzione, portandolo a 0008h.
In seguito alle modifiche apportate dal linker, il codice macchina di questa istruzione rimane invariato, mentre il suo indirizzo logico iniziale diventa 0044h:0008h; il valore che il DOS deve rilocare nel codice macchina, è 0000h, che si trova chiaramente all'indirizzo logico 0044h:0009h (1 byte dopo il codice B8h). Di conseguenza, il relocation record relativo a questa istruzione è 0044h:0009h.
Nel momento in cui EXETEST.EXE viene caricato in memoria, il DOS procede alla rilocazione dei segmenti DATASEGM, CODESEGM e STACKSEGM (ovviamente, vengono rilocate anche le componenti Seg dei vari relocation records); subito dopo, il DOS accede alle istruzioni puntate dai vari relocation records e modifica ogni riferimento agli stessi DATASEGM, CODESEGM e STACKSEGM.

Tutte le informazioni di cui il DOS ha bisogno per poter gestire un file EXE, vengono generate dal linker; a tale proposito, come è stato già anticipato, il linker suddivide un file EXE in due parti chiamate, header (intestazione), e loadable module (modulo caricabile, contenente il programma vero e proprio). All'interno dell'header, vengono infilate tutte le informazioni richieste dal DOS; la Figura 13.8 contiene la descrizione dell'header di un file EXE (per maggiori dettagli, si consiglia di consultare un manuale per programmatori DOS). Nel prossimo capitolo, analizzeremo alcuni metodi di disassemblaggio di un programma eseguibile; in questo modo, sarà possibile esaminare in pratica l'header di un file EXE. La Figura 13.9, visualizza una serie di informazioni su EXETEST.EXE, facilmente deducibili anche senza consultare il contenuto dell'header.

13.7 Esecuzione di un file EXE

Una volta che abbiamo a disposizione il nostro programma EXETEST.EXE, possiamo procedere con la sua esecuzione; a tale proposito, dobbiamo posizionarci nella cartella ASMBASE e impartire il comando:
exetest
Premendo ora il tasto [Invio], cediamo il controllo all'interprete dei comandi COMMAND.COM; l'interprete dei comandi, si accorge subito che exetest non è un comando DOS e si mette allora alla ricerca, nella cartella ASMBASE, di un eseguibile di nome EXETEST.
In base alle convenzioni seguite dal DOS, COMMAND.COM cerca un file chiamato EXETEST.COM; se questo file non viene trovato, COMMAND.COM cerca un file chiamato EXETEST.EXE. Se questo file non viene trovato, COMMAND.COM cerca un file chiamato EXETEST.BAT; i file con estensione BAT vengono chiamati batch file e sono dei semplici file di testo contenenti una sequenza di comandi DOS. Se il file EXETEST.BAT non viene trovato, COMMAND.COM genera un messaggio di errore del tipo:
Comando o nome file non valido
Nel nostro caso, nella cartella ASMBASE, l'interprete dei comandi trova il file EXETEST.EXE; supponendo che si tratti di un file eseguibile, COMMAND.COM cede il controllo ad un apposito programma del SO, chiamato loader (caricatore). Come si intuisce dal nome, il compito del loader è quello di caricare in memoria i programmi eseguibili; a tale proposito, il loader consulta l'eventuale header del file da eseguire. Se viene trovata la stringa iniziale 'MZ', il file viene trattato come EXE; in caso contrario, il loader cerca di trattare EXETEST come un eseguibile di tipo COM. È chiaro che, se proviamo ad eseguire un "finto" file EXE o COM, provochiamo una situazione di errore che può anche determinare un crash (Windows visualizza una finestra di errore).
Nel caso di EXETEST.EXE, il loader trova la stringa 'MZ' e quindi, avvia le procedure standard per il caricamento in memoria di un programma EXE; per ogni programma, EXE o COM, da caricare in memoria, vengono predisposti due blocchi di memoria chiamati environment segment e program segment. Come è stato spiegato in un precedente capitolo, tutti i blocchi di memoria forniti dal DOS, sono sempre allineati al paragrafo; inoltre, la dimensione in byte di un qualunque blocco di memoria DOS, è sempre un multiplo intero di 16.

13.7.1 L'environment segment

L'environment segment o segmento d'ambiente di un programma, è un blocco di memoria DOS destinato a contenere una sequenza di stringhe, chiamate variabili d'ambiente; ogni stringa assume la forma:
'NOME=contenuto', 0
Come si può notare, queste stringhe seguono le convenzioni del linguaggio C; una stringa C è un vettore di codici ASCII terminato da un byte di valore 0 (il codice ASCII 0 viene anche chiamato NULL).
Subito dopo l'ultima stringa della sequenza, è presente un byte di valore 0, che rappresenta quindi una stringa C vuota.
Nelle versioni più recenti del DOS, subito dopo questo byte, è presente una WORD contenente il numero di eventuali stringhe extra; nel caso generale, è presente una sola stringa extra che contiene il nome dell'eseguibile, completo di percorso (path).

Le variabili d'ambiente di un programma, vengono passate da chi ha richiesto l'esecuzione del programma stesso; nel caso più frequente, l'esecuzione di un programma viene richiesta da COMMAND.COM. Anche nell'esempio presentato in questo capitolo, è proprio COMMAND.COM che ha richiesto l'esecuzione di EXETEST.EXE; si dice allora che COMMAND.COM è padre di EXETEST.EXE, oppure che EXETEST.EXE è figlio di COMMAND.COM. A sua volta, EXETEST.EXE potrebbe richiedere l'esecuzione di un altro programma chiamato, ad esempio, TEST2.EXE; in questo caso, EXETEST.EXE, come padre di TEST2.EXE, deve provvedere a passare le variabili d'ambiente allo stesso TEST2.EXE.
La Figura 13.10, mostra un esempio in versione Assembly, della struttura del segmento d'ambiente, che COMMAND.COM crea per il programma figlio EXETEST.EXE. La variabile COMSPEC viene inizializzata dal SO e viene usata dai programmi, per invocare l'interprete dei comandi COMMAND.COM; questo è proprio ciò che accadeva, quando si lanciava la DOS Box dall'interno del vecchio Windows98.
Le due variabili PATH e PROMPT di Figura 13.10 vengono, invece, configurate dall'utente attraverso il file AUTOEXEC.BAT; a tale proposito, all'interno del file C:\AUTOEXEC.BAT, si può utilizzare il comando SET per scrivere, ad esempio:
SET PATH=C:\DOS;C:\WINDOWS;C:\MASM\BIN
Per approfondire questi concetti, si consiglia di consultare un manuale per programmatori DOS.

Chi conosce il linguaggio C, sarà interessato a sapere che l'unica stringa extra presente nel segmento d'ambiente, rappresenta il famoso argomento argv[0] passato (dal compilatore C) alla funzione:
int main(int argc, char *argv[])
Un segmento d'ambiente, può arrivare a contenere sino a 32 KiB di informazioni; bisogna prestare quindi particolare attenzione al fatto che, numerose richieste di esecuzione a catena, da un programma ad un altro, possono occupare una notevole quantità di memoria convenzionale.

13.7.2 Il program segment

Il program segment o segmento di programma (da non confondere con i segmenti che formano un programma), è un blocco di memoria DOS destinato a contenere il PSP o Program Segment Prefix (prefisso del segmento di programma) e il programma eseguibile vero e proprio; la Figura 13.11 illustra le caratteristiche del PSP (per maggiori dettagli, si consiglia di consultare un manuale per programmatori DOS).

13.7.3 Caricamento in memoria di EXETEST.EXE

Supponiamo ora che il DOS abbia creato l'environment segment e il program segment per EXETEST.EXE; supponiamo anche che il program segment parta dall'indirizzo fisico (sempre allineato al paragrafo) 024B0h. A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 024Bh:0000h; possiamo dire quindi che il program segment destinato a contenere il modulo caricabile di EXETEST.EXE, inizia dall'offset 0000h del segmento di memoria n. 024Bh.
Nei primi 256 byte (0100h byte) del program segment, viene sistemato il PSP; possiamo dire quindi che il PSP, occupa tutti gli indirizzi fisici del program segment, compresi tra 024B0h e:
024B0h + (0100h - 1h) = 025AFh
In termini di indirizzi logici, il PSP viene inserito nel segmento di memoria n. 024Bh e occupa tutti gli offset di questo segmento, compresi tra 0000h e:
0000h + (0100h - 1h) = 00FFh
In sostanza, il PSP è compreso tra gli indirizzi logici 024Bh:0000h e 024Bh:00FFh.

L'inizio del PSP coincide con l'inizio del program segment; per convenzione, si utilizza allora la sigla PSP, per indicare la componente Seg dell'indirizzo logico iniziale (024Bh:0000h) dello stesso program segment. Possiamo dire quindi che:
PSP = 024Bh
Immediatamente dopo il PSP, viene sistemato il modulo caricabile prelevato dal file EXETEST.EXE; siccome il PSP termina all'indirizzo fisico 025AFh, possiamo dire che il modulo caricabile parte dall'indirizzo fisico 025B0h, allineato al paragrafo. A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 025Bh:0000h; la componente Seg di questo indirizzo logico, è proprio quella che viene utilizzata dal DOS, per rilocare i segmenti di EXETEST.EXE. Indicando simbolicamente questa componente Seg con il nome StartSeg, possiamo scrivere:
StartSeg = 025Bh
Come si può notare, StartSeg si trova 16 paragrafi più avanti di PSP; a questo punto, con l'aiuto dell'header di Figura 13.9, il DOS procede con la rilocazione dei segmenti di programma di EXETEST.EXE.

Il linker aveva assegnato a DATASEGM l'indirizzo fisico iniziale 00000h; il DOS somma questo indirizzo a 025B0h e ottiene:
00000h + 025B0h = 025B0h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 025Bh:0000h; di conseguenza:
DATASEGM = 025Bh = StartSeg
Come si può notare, gli offset interni a DATASEGM non subiscono alcuna rilocazione e continuano a partire da 0000h (confermando i calcoli del linker); osserviamo anche che l'indirizzo fisico iniziale 025B0h di DATASEGM, è compatibile con l'attributo di allineamento PARA.

Il linker aveva assegnato a CODESEGM l'indirizzo fisico iniziale 00448h; il DOS somma questo indirizzo a 025B0h e ottiene:
00448h + 025B0h = 029F8h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 029Fh:0008h; di conseguenza:
CODESEGM = 029Fh = InitialCS + StartSeg
Come si può notare, gli offset interni a CODESEGM non subiscono alcuna rilocazione e continuano a partire da 0008h (confermando i calcoli del linker); osserviamo anche che l'indirizzo fisico iniziale 029F8h di CODESEGM, è compatibile con l'attributo di allineamento DWORD.

Il linker aveva assegnato a STACKSEGM l'indirizzo fisico iniziale 00590h; il DOS somma questo indirizzo a 025B0h e ottiene:
00590h + 025B0h = 02B40h
A questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 02B4h:0000h; di conseguenza:
STACKSEGM = 02B4h = InitialSS + StartSeg
Come si può notare, gli offset interni a STACKSEGM non subiscono alcuna rilocazione e continuano a partire da 0000h (confermando i calcoli del linker); osserviamo anche che l'indirizzo fisico iniziale 02B40h di STACKSEGM, è compatibile con l'attributo di allineamento PARA.

Nella relocation table (Figura 13.9), il DOS trova un solo relocation record rappresentato dalla coppia 0044h:0009h; la componente Seg di questa coppia, viene ovviamente rilocata, e si ottiene:
(0044h + StartSeg):0009h = 029Fh:0009h
A questo indirizzo di CODESEGM, è presente il valore da rilocare 0000h (DATASEGM), relativo al codice macchina:
B8h 0000h
dell'istruzione:
mov ax, DATASEGM
Il valore 0000h viene rilocato (sommato a StartSeg) e il precedente codice macchina diventa:
B8h 025Bh
Il DOS passa ora alla inizializzazione dei registri della CPU; per la coppia CS:IP, il DOS pone:
CS:IP = CODESEGM:InitialIP = 029Fh:0008h
Per la coppia SS:SP, il DOS pone:
SS:SP = STACKSEGM:InitialSP = 02B4h:0400h
Per i due registri di segmento DS e ES, il DOS pone per convenzione:
DS = ES = PSP = 024Bh
A questo punto, il programma è pronto per l'esecuzione; in seguito a tutto questo lavoro svolto dal DOS, il programma EXETEST.EXE assume in memoria la disposizione illustrata in Figura 13.12. In un programma EXE, possono essere presenti, eventualmente, diversi segmenti di dati; proprio per questo motivo, il SO non può sapere quale di questi segmenti di dati deve essere utilizzato per inizializzare DS. Questo compito quindi, viene lasciato al programmatore.
Convenzionalmente, il DOS inizializza DS e ES, con la componente Seg (che abbiamo chiamato PSP) dell'indirizzo iniziale del program segment; questo significa che se vogliamo accedere al contenuto del PSP, possiamo servirci di DS o ES e delle informazioni visibili in Figura 13.11.
In genere, subito dopo l'entry point di un programma, DS viene inizializzato dal programmatore con un segmento di dati, per cui ci rimane a disposizione ES; in questo caso, il PSP è accessibile a partire dall'indirizzo logico ES:0000h.
Il campo CommandLineParmLength, ad esempio, si trova all'indirizzo logico ES:0080h; analogamente, il campo CommandLineParameters, si trova all'indirizzo logico ES:0081h.
L'environment segment del programma in esecuzione, si trova in memoria a partire dall'indirizzo logico EnvironmentSegment:0000h; a sua volta, il campo EnvironmentSegment si trova all'indirizzo logico ES:002Ch.
Molto spesso, si rende necessario anche il ricorso a ES per la gestione dei dati di un programma; proprio per questo motivo, l'elaborazione delle informazioni presenti nel PSP deve essere effettuata prima che il contenuto iniziale di DS e ES venga sovrascritto.

13.7.4 Esecuzione di EXETEST.EXE

Una volta che il DOS ha inizializzato i necessari registri della CPU, può partire la fase di esecuzione di EXETEST.EXE; il controllo passa quindi alla stessa CPU che trova la coppia CS:IP inizializzata con l'entry point 029Fh:0008h. A questo indirizzo interno a CODESEGM, è presente il codice macchina:
B8h 025Bh
La CPU quindi, decodifica ed esegue questa istruzione; nel frattempo, IP viene incrementato in modo che la coppia CS:IP punti alla prossima istruzione da eseguire.
La precedente istruzione, ha un codice macchina da 3 byte; di conseguenza, la CPU pone:
IP = IP + 0003h = 0008h + 0003h = 000Bh
All'indirizzo 029Fh:000Bh, interno a CODESEGM, è presente il codice macchina:
8Eh D8h
La CPU quindi, decodifica ed esegue questa istruzione; nel frattempo, IP viene incrementato in modo che la coppia CS:IP punti alla prossima istruzione da eseguire.
La precedente istruzione, ha un codice macchina da 2 byte; di conseguenza, la CPU pone:
IP = IP + 0002h = 000Bh + 0002h = 000Dh
e così via.

L'esecuzione prosegue in questo modo, finché non si arriva al blocco di istruzioni che terminano il programma e restituiscono il controllo al DOS.

13.8 La direttiva ALIGN

Il programma di esempio EXETEST.EXE presentato in questo capitolo, ha una finalità puramente didattica e non pone quindi alcuna attenzione al problema del corretto allineamento delle informazioni in memoria; è chiaro però che nei programmi reali, questo aspetto non può essere trascurato.

Come è stato ampiamente detto nei precedenti capitoli, il programmatore Assembly ha la possibilità di gestire in modo diretto, tutti i dettagli relativi al tipo di allineamento da assegnare, sia ai segmenti di programma, sia alle informazioni contenute nei segmenti stessi; codice e dati correttamente allineati in memoria, vengono acceduti alla massima velocità possibile dalla CPU.
Nel capitolo precedente, abbiamo visto che grazie all'attributo Allineamento, possiamo richiedere al linker che un determinato segmento di programma venga disposto in memoria a partire da un indirizzo fisico avente particolari requisiti di allineamento; questo attributo agisce quindi esclusivamente sull'indirizzo fisico iniziale dei segmenti di programma.
Se vogliamo imporre anche l'allineamento delle informazioni contenute all'interno di un segmento di programma, dobbiamo servirci della direttiva:
ALIGN n
La direttiva ALIGN richiede un argomento che deve essere un numero intero positivo n, potenza intera di 2 (quindi, 2, 4, 8, 16, etc); in questo modo diciamo all'assembler che la successiva informazione deve partire da un offset multiplo intero di n.
Per motivi abbastanza ovvi, non avrebbe senso specificare un valore di n che supera quello dell'attributo Allineamento; nel caso, ad esempio, di un segmento con attributo PARA (16 byte), il valore di n non può superare 16.
Naturalmente, nel caso di CPU con architettura a 8 bit, tutto ciò che riguarda l'allineamento dei segmenti di programma e del loro contenuto, non ha alcun significato; il discorso cambia, invece, nel momento in cui abbiamo a che fare con CPU a 16 bit, 32 bit, 64 bit, etc.
Appare ovvio il fatto che la direttiva ALIGN raggiunge la massima efficacia quando lavora in combinazione con l'attributo Allineamento; allineando, ad esempio, un segmento dati ad un indirizzo fisico pari, possiamo facilmente allineare ad indirizzi pari anche i dati contenuti nel segmento stesso.
Per capire meglio questi importanti concetti, analizziamo in dettaglio i metodi da utilizzare per allineare in memoria, il codice, i dati statici e i dati temporanei del programma EXETEST.EXE presentato in questo capitolo.

13.8.1 Allineamento in memoria dei dati statici

Osservando il file EXETEST.LST di Figura 13.2, ci accorgiamo che nel segmento DATASEGM, l'allineamento dei dati appare piuttosto insoddisfacente; si nota, infatti, che il dato varByte si trova ad un offset pari (0000h), mentre gli 11 dati successivi, si trovano tutti ad offset dispari.
Questa situazione, è dovuta al fatto che varByte, che parte appunto da un offset pari, ha una ampiezza dispari (1) in byte, mentre gli 11 dati successivi, hanno tutti una ampiezza pari in byte; di conseguenza, varByte costringe gli 11 dati successivi, a posizionarsi tutti ad un offset dispari.
Se stiamo lavorando con CPU a 16 bit o superiori, si tratta di una situazione veramente pessima; infatti, questa situazione provoca un enorme aumento del numero di accessi in memoria, necessari alla CPU per leggere o scrivere i dati presenti in DATASEGM.

Per evitare questo problema, possiamo servirci di una semplice regola generale, valida per una qualsiasi CPU avente un Data Bus con ampiezza pari a n byte; questa regola consiste nell'inserire una direttiva ALIGN n, prima di qualsiasi dato il cui allineamento non è un multiplo intero di n e dopo qualsiasi dato la cui ampiezza in byte non è un multiplo intero di n.

Per dimostrare questa regola, consideriamo una CPU con architettura a 16 bit, cioè con Data Bus avente ampiezza pari a 2 byte; osservando il listing file di Figura 13.2, possiamo notare che, ad eccezione di varByte che ha allineamento pari e dimensione dispari, tutti gli altri 11 dati hanno allineamento dispari e ampiezza pari. Proviamo allora ad apportare una semplicissima modifica che consiste nell'inserire un buco di memoria da 1 byte tra varByte e varWord; la porzione di DATASEGM interessata da questa modifica, viene mostrata in Figura 13.13. Osserviamo subito che varByte continua a partire dall'offset 0000h; subito dopo varByte, troviamo un buco di memoria da 1 byte che si trova all'offset 0001h. Questo buco di memoria, fa scalare in avanti di 1 byte gli offset di tutti gli altri 11 dati; inoltre, questi 11 dati hanno tutti una ampiezza pari in byte.
Riassemblando ora il programma di Figura 13.1 e consultando il nuovo listing file, possiamo constatare che con questa semplice modifica, tutti i dati presenti in DATASEGM si vengono a trovare allineati ad indirizzi pari!
Possiamo ottenere lo stesso risultato, attraverso la direttiva ALIGN 2; in questo modo si perviene alla situazione illustrata in Figura 13.14. Quando l'assembler esamina DATASEGM, assegna a varByte l'offset 0000h; subito dopo, l'assembler incontra la direttiva ALIGN, attraverso la quale stiamo richiedendo un allineamento ad un offset pari per varWord. L'offset pari successivo a 0000h è ovviamente 0002h; di conseguenza, l'assembler inserisce un buco di memoria da 1 byte subito dopo varByte e fa così scalare in avanti di 1 byte gli offset di tutti i successivi 11 dati. Questi 11 dati hanno tutti una ampiezza pari in byte, per cui si troveranno tutti allineati ad un offset pari; alla fine si ottiene quindi l'identica situazione mostrata in Figura 13.13.

Veniamo ora al caso molto più frequente di una CPU con architettura a 32 bit, cioè con Data Bus avente ampiezza pari a 4 byte; osservando il listing file di Figura 13.2, possiamo notare che questa volta, non basta inserire una direttiva ALIGN 4 tra varByte e varWord. Questa direttiva, fa scalare in avanti di 3 byte l'offset di varWord, che si viene così a trovare all'offset 0004h; a causa però dell'ampiezza pari a 2 byte della stessa varWord, la successiva variabile varDword si viene a trovare all'offset 0006h che non è un multiplo intero di 4.
È necessaria quindi una nuova direttiva ALIGN 4 tra varWord e varDword; in questo modo, varDword, varQword e varTbyte si vengono a trovare ad offset multipli interi di 4.
La variabile varTbyte ha una ampiezza pari a 10 byte, per cui provoca il disallineamento di varFloat32; è necessaria quindi una nuova direttiva ALIGN 4 tra varTbyte e varFloat32, in modo che, varFloat32, varFloat64, dataViaggio, varRect, vettRect e FiatPanda si vengano a trovare ad offset multipli interi di 4.
La variabile FiatPanda ha una ampiezza pari a 46 byte, per cui provoca il disallineamento di vettAuto; è necessaria quindi un'ultima direttiva ALIGN 4 tra FiatPanda e vettAuto. Si perviene in questo modo alla situazione finale mostrata in Figura 13.15. Se vogliamo svolgere un lavoro più meticoloso, possiamo ricordare che, con le CPU a 32 bit, i dati di tipo WORD non hanno problemi di allineamento, purché si trovino ad indirizzi pari; sfruttando questo aspetto, possiamo inserire una direttiva ALIGN 2 tra varByte e varWord. Questa modifica ci permette di eliminare la direttiva ALIGN 4 tra varWord e varDword; in questo modo, inoltre, riduciamo anche le dimensioni complessive di DATASEGM, in quanto le direttive ALIGN, comportano un certo spreco di memoria. La Figura 13.15 ci permette anche di osservare che dati come FiatPanda, a causa della loro ampiezza in byte, creano problemi di allineamento ai dati successivi; possiamo dire allora che lavorando con una CPU avente Data Bus a n byte, sarebbe meglio definire dati statici con ampiezza multipla intera di n byte.
Per risolvere questo problema, possiamo inserire, ad esempio, opportuni buchi di memoria all'interno delle dichiarazioni dei dati; possiamo anche sfruttare il fatto che l'assembler ci permette di inserire le direttive ALIGN all'interno delle dichiarazioni di STRUC e UNION.
Supponiamo, ad esempio, di voler ottimizzare la struttura Automobile per le CPU a 32 bit; possiamo apportare allora le modifiche mostrate in Figura 13.16. Ricordiamo ancora una volta che tutte le WORD con allineamento pari comportano un unico accesso in memoria da parte delle CPU con architettura a 32 bit; dalla Figura 13.16 si ricava allora quanto segue: Allineando allora un dato di tipo Automobile ad un indirizzo fisico multiplo intero di 4, ottimizziamo al massimo l'accesso al dato stesso da parte di una CPU a 32 bit; osserviamo inoltre che, grazie alla direttiva ALIGN, la struttura di Figura 13.16 assume una ampiezza in byte pari a 48 (multipla intera di 4), che tiene allineato alla DWORD anche il dato successivo.

13.8.2 Allineamento in memoria dei dati temporanei

Come è stato già detto nel capitolo 10, un programma in esecuzione può fare un uso veramente massiccio dello stack; inoltre, lo stack di un programma viene pesantemente utilizzato anche dalle ISR chiamate dalla CPU in risposta alle richieste di interruzione software e hardware. Il programmatore è tenuto quindi a curare con grande attenzione il corretto allineamento delle informazioni presenti nello stack; vediamo allora come ci si deve comportare nel caso delle CPU con architettura a 16 e a 32 bit.

Nei precedenti capitoli è stato detto che le CPU con architettura a 16 bit, per la gestione dello stack forniscono le due istruzioni PUSH e POP, che accettano esclusivamente operandi di tipo WORD; in questo caso, il corretto allineamento dei dati temporanei, può essere ottenuto in modo estremamente semplice.
In base alle considerazioni svolte in precedenza (in relazione al blocco DATASEGM), dobbiamo innanzi tutto far partire il segmento di stack da un indirizzo fisico multiplo intero di 2; è necessario quindi evitare l'uso dell'attributo di allineamento BYTE. A questo punto, dobbiamo assegnare a SP un offset iniziale multiplo intero di 2; con questi semplici accorgimenti, tutte le WORD inserite nello stack si verranno a trovare ad indirizzi fisici pari e verranno accedute alla massima velocità possibile dalla CPU.

Nel caso delle CPU con architettura a 32 bit, la situazione è più delicata; come già sappiamo, queste particolari CPU permettono di utilizzare le istruzioni PUSH e POP con operandi, sia di tipo WORD, sia di tipo DWORD. La cosa migliore da fare, consiste allora nell'utilizzare PUSH e POP, esclusivamente con operandi di tipo DWORD; questa tecnica viene largamente impiegata nei SO a 32 bit come Windows e Linux.
Ripetendo quindi il ragionamento precedente, dobbiamo innanzi tutto allineare il segmento di stack ad un indirizzo fisico multiplo intero di 4 (attributo DWORD o PARA); successivamente, dobbiamo assegnare a SP un offset iniziale multiplo intero di 4. In questo modo, tutte le DWORD presenti nello stack, si vengono a trovare ad indirizzi fisici allineati alla DWORD.

Se vogliamo usare PUSH e POP con operandi di tipo WORD e DWORD, la situazione diventa più complicata; in questo caso, infatti, dobbiamo fare in modo che gli operandi di tipo WORD si trovino sempre ad indirizzi multipli interi di 2 e che gli operandi di tipo DWORD si trovino sempre ad indirizzi multipli interi di 4. Una tale situazione è però troppo difficile da gestire; in casi del genere, si può anche tollerare un piccolo aumento di accessi in memoria, dovuto a qualche dato di tipo DWORD allineato ad un indirizzo che, pur essendo pari, non è un multiplo intero di 4.

13.8.3 Allineamento in memoria delle istruzioni

Come sappiamo, nelle CPU di classe CISC, vengono utilizzate delle istruzioni codificate con un numero variabile (e spesso anche dispari) di byte; ciò significa che in un segmento di codice, moltissime istruzioni si vengono a trovare fuori allineamento. È chiaro però che sarebbe assurdo pensare di allineare ogni singola istruzione di un blocco codice; in questo modo, infatti, si verificherebbe un enorme aumento delle dimensioni del programma.
Proprio per questo motivo, la strada che si segue consiste nell'allineare solamente quelle istruzioni che si trovano in posizioni particolarmente critiche; nel caso più frequente, si tratta di quelle istruzioni che vengono raggiunte dalla CPU attraverso un salto (con conseguente svuotamento della prefetch queue). Questi aspetti sono stati già anticipati nel capitolo 10 (sezione 10.6); nei capitoli successivi, verranno analizzati diversi accorgimenti che permettono di ottenere un certo miglioramento nella velocità di esecuzione del codice.

In presenza di una direttiva ALIGN in un segmento dati, l'assembler inserisce eventualmente dei buchi di memoria rappresentati da un opportuno numero di byte, ciascuno dei quali vale 00h; questo sistema non può essere però utilizzato nei segmenti di codice, in quanto si rischia di trarre in inganno la CPU!
Supponiamo, ad esempio, che l'assembler inserisca buchi di memoria di valore 00h in un segmento di codice; in tal caso, la CPU viene tratta in inganno in quanto scambia il valore 00h per il codice macchina di una addizione (il codice 00h indica, infatti, l'Opcode di una addizione tra sorgente Reg8/Mem8 e destinazione Mem8/Reg8).
Per evitare questo problema, nei segmenti di codice l'assembler inserisce buchi di memoria rappresentati da dei byte di valore 90h; questo valore da 1 byte è il codice macchina completo dell'istruzione NOP. Il mnemonico NOP significa no operation (nessuna operazione da svolgere); quando la CPU incontra il codice macchina 90h, si limita ad incrementare di 1 il contenuto del registro IP e passa quindi all'elaborazione dell'istruzione successiva.

Nel caso in cui sia necessario inserire buchi di memoria da più di 1 byte ciascuno, possiamo servirci anche di istruzioni del tipo:
mov ax, ax
Come si può facilmente constatare, questa istruzione lascia invariato il contenuto di AX; il suo unico scopo è quello di occupare memoria (in questo caso, il codice macchina 89h C0h, occupa 2 byte).

13.9 Gli operatori SEG e OFFSET

Sulla base di ciò che è stato esposto in questo capitolo in relazione alla rilocazione degli indirizzi da parte del linker e del SO, si intuisce subito il fatto che sarebbe assolutamente folle l'idea di scrivere programmi nei quali si tenti di determinare empiricamente le componenti Seg e Offset dell'indirizzo logico di una qualsiasi informazione; è chiaro, infatti, che un qualunque indirizzo logico Seg:Offset che nel codice sorgente assume un determinato aspetto, è destinato a subire profonde modifiche ad opera del linker e del SO.
È necessario quindi ribadire che bisogna evitare nella maniera più assoluta di scrivere programmi che tentino di calcolare indirizzi in modo empirico; per permettere al programmatore di svolgere questi calcoli in assoluta sicurezza, MASM mette a disposizione due potenti operatori chiamati SEG e OFFSET.

In riferimento al programma di Figura 13.1, consideriamo l'istruzione:
mov bx, OFFSET varDword
Questa istruzione, trasferisce in BX la componente Offset dell'indirizzo logico del dato varDword; per capire bene questa istruzione, è necessario ricordare che quando un dato viene caricato in memoria, il suo indirizzo logico Seg:Offset viene assegnato in modo definitivo (statico) e rimane fisso per tutta la fase di esecuzione. Tutto ciò significa che le componenti Seg e Offset dell'indirizzo logico di una informazione, non sono altro che due semplici valori immediati del tipo Imm16; ne consegue che la precedente istruzione, rappresenta un trasferimento dati da Imm16 a Reg16.
Come già sappiamo, il codice macchina di questa istruzione è formato dall'Opcode 1011_w_Reg, seguito dall'Imm16; nel nostro caso, w=1 (operandi a 16 bit) e Reg=BX=011b. Otteniamo allora:
Opcode = 10111011b = BBh
Dal listing file di Figura 13.2, si rileva che l'offset di varDword è:
Disp16 = 0003h = 0000000000000011b
In definitiva, otteniamo il codice macchina:
10111011b 0000000000000011b = BBh 0003h
L'assembler, "marca" 0003h come un offset rilocabile; in questo modo, il linker potrà apportare tutte le necessarie modifiche al precedente codice macchina. In base all'esempio su EXETEST.EXE presentato in questo capitolo, abbiamo visto che l'indirizzo logico che l'assembler assegna a varDword è 0000h:0003h; questo indirizzo logico viene trasformato dal linker in 0000h:0003h e dal SO in 025Bh:0003h. In fase di esecuzione del programma, la componente Offset assegnata a varDword diventa definitiva (0003h); il programmatore quindi non deve fare altro che gestire questa informazione attraverso l'operatore OFFSET, lasciando che tutto il complesso lavoro di rilocazione venga gestito dall'assembler, dal linker e dal SO.

Per determinare la componente Seg dell'indirizzo logico di una qualsiasi informazione (dato o etichetta), MASM mette a disposizione l'operatore SEG; sempre in riferimento al programma di Figura 13.1, consideriamo la seguente istruzione:
MOV DX, SEG start
Questa istruzione, trasferisce in DX la componente Seg dell'indirizzo logico dell'etichetta start:; anche in questo caso, abbiamo a che fare con un trasferimento dati da Imm16 a Reg16. L'Opcode è sempre 1011_w_Reg; nel nostro caso, w=1 (operandi a 16 bit) e Reg=DX=010b. Otteniamo allora:
Opcode = 10111010b = BAh
Dal listing file di Figura 13.2, si rileva che start si trova nel blocco CODESEGM, a cui l'assembler assegna il valore simbolico 0000h (assolutamente privo di significato); in definitiva, otteniamo il codice macchina:
10111010b 0000000000000000b = BAh 0000h
L'assembler, "marca" 0000h come un segmento rilocabile; in questo modo, il linker potrà apportare tutte le necessarie modifiche al precedente codice macchina. In base all'esempio su EXETEST.EXE presentato in questo capitolo, abbiamo visto che l'indirizzo logico che l'assembler assegna a start è 0000h:0000h; questo indirizzo logico viene trasformato dal linker in 0044h:0008h e dal SO in 029Fh:0008h. In fase di esecuzione del programma, la componente Seg assegnata a start diventa definitiva (029Fh); il programmatore quindi non deve fare altro che gestire questa informazione attraverso l'operatore SEG, lasciando che tutto il complesso lavoro di rilocazione venga gestito dall'assembler, dal linker e dal SO.

Gli operatori SEG e OFFSET non vengono assolutamente influenzati da eventuali direttive ASSUME; è chiaro, infatti, che l'assembler, incontrando l'istruzione:
MOV BX, OFFSET varDword
capisce benissimo che gli stiamo chiedendo di calcolare lo spiazzamento di varDword all'interno del segmento di appartenenza, che è ovviamente DATASEGM. Per scrivere questa istruzione quindi, non è necessaria una direttiva ASSUME che associa DATASEGM ad un qualsiasi registro di segmento.

Un altro aspetto importante da capire, riguarda il fatto che gli operatori SEG e OFFSET, lavorano solamente in fase di assemblaggio di un programma (assembler time operators); il loro scopo, è quello di generare dei valori immediati (rilocabili), che vengono poi inseriti dall'assembler, direttamente nel codice macchina.

13.10 Programmi eseguibili in formato COM

Come già sappiamo, il SO CP/M può essere definito l'antenato del DOS; ai tempi del CP/M, i computer disponevano di quantità piuttosto limitate di memoria, in genere 32 KiB, 64 KiB o, al massimo, 128 KiB come nel caso del celebre Commodore 128 (che utilizzava proprio questo SO).
In una situazione del genere, si rendeva necessario limitare al massimo le esigenze di memoria dei programmi; proprio per questo motivo, il CP/M prevedeva un formato eseguibile che permetteva di realizzare programmi piuttosto piccoli e con ridottissime esigenze di memoria, a scapito ovviamente della flessibilità del programma stesso.
Con l'arrivo del DOS, si rese necessaria la conversione verso questo nuovo SO, della grande quantità di software scritto per il CP/M; per facilitare questa conversione, il DOS mise a disposizione un apposito formato per i programmi eseguibili, chiamato COM (COre iMage). Il formato COM è disponibile ancora oggi in ambiente DOS e pur essendo stato soppiantato quasi del tutto dal formato EXE, viene ancora utilizzato per scrivere piccoli programmi, molto compatti ed efficienti.

13.10.1 Struttura di un eseguibile in formato COM

In pratica, un programma COM viene ottenuto riducendo all'essenziale la struttura di un programma EXE; per raggiungere questo obiettivo, si elimina innanzi tutto la possibilità di definire più di un segmento di programma. In un programma COM quindi, è presente un unico segmento, all'interno del quale trovano posto, il codice, i dati e lo stack; in presenza di una struttura così semplice, molte delle informazioni che il linker inserisce nell'header diventano superflue.
Spingendo al massimo la semplificazione, si arriva ad eliminare del tutto la necessità, da parte del linker, di generare l'header; di conseguenza, i file COM generati dal linker, contengono al loro interno solo ed esclusivamente il modulo caricabile (da cui il nome "core image").
In assenza dell'header, tutte le fasi di caricamento in memoria e inizializzazione di un programma COM, vengono gestite dal SO attraverso un procedimento standard; questo procedimento quindi, è identico per qualsiasi file COM.

Nello scrivere un programma Assembly in formato COM, il programmatore ha l'obbligo di lasciare liberi i primi 256 byte (0100h byte) dell'unico segmento presente; come si può facilmente intuire, in questi primi 256 byte il SO inserisce il PSP. Ne consegue che l'entry point di un programma COM deve trovarsi obbligatoriamente all'offset 0100h dell'unico segmento presente!

13.10.2 La direttiva ORG

L'area da 256 byte riservata al PSP in un eseguibile in formato COM, deve essere rigorosamente non inizializzata; possiamo facilmente ottenere questo risultato attraverso la direttiva:
db 0100h dup (?)
Un metodo più semplice ed efficace, consiste nel servirsi della direttiva:
ORG n
Quando l'assembler incontra questa direttiva mentre sta assemblando un segmento di programma, assegna il valore n alla componente Offset del location counter ($); l'argomento n, deve essere quindi un valore immediato di tipo Imm16.
Supponiamo che, ad esempio, l'assembler incontri la seguente direttiva all'offset 0200h di un blocco CODESEGM:
org 0250h
In quel punto, $ vale CODESEGM:0200h; in seguito a questa direttiva, l'assembler assegna a $ il valore CODESEGM:0250h. Tutto lo spazio da 50h byte compreso tra gli offset 0200h e 0250h (estremi esclusi), viene riempito con dei buchi di memoria; l'assemblaggio di CODESEGM riprende quindi a partire dall'indirizzo logico CODESEGM:0250h.

In generale, l'argomento n specificato da ORG può essere una qualunque espressione, formata però da operandi immediati; in riferimento al programma di Figura 13.1, in un punto del blocco CODESEGM possiamo scrivere, ad esempio:
ORG STACK_SIZE - ((OFFSET start) + 00B2h)
L'assembler, deve avere la possibilità di risolvere questa espressione, in modo da ottenere un valore finale di tipo Imm16.

13.10.3 Modello di programma Assembly in formato COM

Prima di illustrare la struttura generale di un programma Assembly in formato COM, rimane ancora da chiarire un aspetto importante, relativo al punto in cui inserire le definizioni dei dati statici; in presenza, infatti, di un unico segmento, c'è il rischio che la CPU possa finire per sbaglio nel bel mezzo di questi dati, scambiandoli per codici macchina da eseguire!
Per risolvere questo problema è necessario ribadire che, a differenza di quanto accade con i programmi in formato EXE, in un programma in formato COM è proibito inserire qualsiasi dato inizializzato prima dell'entry point (cioè prima dell'offset 0100h); vediamo allora quali altre strade si possono seguire, per poter individuare un'area adatta a contenere la definizione dei dati statici di un programma in formato COM.
Una prima soluzione, consiste nel definire tutti i dati statici, subito dopo l'entry point; in questo caso, prima delle varie definizioni, bisogna inserire una istruzione di salto, che permetta alla CPU di scavalcare i dati e di raggiungere la prima istruzione da eseguire. Le istruzioni di salto, verranno illustrate in un capitolo successivo.
In questo capitolo, viene utilizzato un altro metodo, molto semplice, che consiste nell'inserire le definizioni dei dati statici, subito dopo le istruzioni di terminazione del programma; quest'area, non potrà mai essere raggiunta (in modo involontario) dalla CPU.

Sulla base delle considerazioni appena esposte, siamo finalmente in grado di definire la struttura generale di un programma Assembly, destinato ad essere convertito in un eseguibile in formato COM; la Figura 13.17, illustra un modello che verrà utilizzato in tutta la sezione Assembly Base, per scrivere esempi di programma Assembly in formato COM. Il modello presentato in Figura 13.17, utilizza un segmento unico chiamato COMSEGM; per la classe del segmento, è stata utilizzata la stringa 'CODE', ma naturalmente, è possibile utilizzare anche altre stringhe come 'UNICO', 'COMCLASS', etc.
Ovviamente, in presenza di un unico segmento, un programma in formato COM non può superare la dimensione massima prevista per i segmenti di memoria e cioè, 64 KiB; questa dimensione massima, comprende anche i 256 byte riservati al PSP. In realtà, sarebbe anche possibile superare questa barriera; in tal caso però, si andrebbe incontro a complicazioni tali da rendere preferibile la scelta del formato EXE.
Anche nel caso dei programmi in formato COM, è possibile spezzare l'unico segmento presente, in due o più parti che possono trovarsi distribuite, nello stesso modulo o anche in moduli differenti; tutte queste parti però, devono avere lo stesso nome e gli stessi identici attributi, in modo che il linker ottenga alla fine un unico segmento di programma (ovviamente, l'attributo Combinazione non deve essere PRIVATE). Se non si seguono queste regole, si ottiene un errore del linker; la suddivisione dei programmi Assembly in due o più moduli, verrà analizzata in un capitolo successivo.

Tornando alla Figura 17, notiamo la presenza della direttiva:
assume cs: COMSEGM, ds: COMSEGM, es: COMSEGM, ss: COMSEGM
Con questa direttiva, stiamo dicendo all'assembler che in presenza di istruzioni del tipo:
mov ax, varWord
l'identificatore varWord rappresenta una componente Offset che deve essere associata ad una componente Seg contenuta indifferentemente in uno qualunque dei registri di segmento CS, DS, ES e SS; infatti, come vedremo tra breve, in presenza di un unico segmento di programma, tutti i registri di segmento verranno inizializzati con la stessa componente Seg.
Come conseguenza della precedente direttiva ASSUME, subito dopo l'entry point dovrebbero essere presenti le classiche istruzioni per l'inizializzazione di DS e ES; se proviamo però a scrivere, ad esempio:
mov ax, COMSEGM
otteniamo un errore del linker, rappresentato da un messaggio del tipo:
Cannot generate COM file: segment-relocatable items present in module nomefile.com
Con questo messaggio, il linker ci sta dicendo che in un programma in formato COM, è proibita la presenza di istruzioni che fanno riferimento ad una qualsiasi componente Seg rilocabile.
Il perché di questo messaggio è abbastanza ovvio; in assenza dell'header, non esiste nemmeno la relocation table. Di conseguenza, il SO non è in grado di modificare il codice macchina di una istruzione che fa riferimento ad una componente Seg rilocabile; è proibito quindi scrivere anche istruzioni del tipo:
mov dx, seg start
È perfettamente lecito, invece, scrivere istruzioni del tipo:
mov bx, offset start ; carica in BX l'offset di start
oppure:
mov ax, ds
Come vedremo tra breve, in un programma in formato COM tutte le inizializzazioni dei registri di segmento CS, DS, ES e SS, spettano rigorosamente al SO; le considerazioni appena esposte, ci danno un'idea delle notevoli limitazioni presenti in questo tipo di programma eseguibile.

Un altro aspetto abbastanza evidente nel segmento COMSEGM di Figura 13.17, riguarda l'apparente mancanza di un'area riservata allo stack; anche a questo proposito, vedremo che l'inizializzazione standard dello stack di un programma COM, spetta al SO.

13.10.4 Esempio di programma Assembly in formato COM

Come esempio pratico, trasformiamo in formato COM il programma di Figura 13.1; il risultato di questa trasformazione, ci porta ad ottenere il file COMTEST.ASM illustrato in Figura 13.18. Notiamo subito la presenza di alcune differenze importanti rispetto al codice sorgente di Figura 13.1; in Figura 13.18, è stata utilizzata la direttiva ALIGN, sia per i membri della struttura Automobile, sia per l'allineamento alla DWORD dei dati statici del programma.
Notiamo anche la presenza di una direttiva ALIGN 4, che precede l'inizio delle definizioni dei dati statici; questa direttiva è necessaria in quanto, molto probabilmente, l'ultima istruzione del blocco codice principale termina ad un offset che provoca il disallineamento dell'informazione successiva. Per i programmi in formato COM, si consiglia di ricorrere sempre a questo accorgimento.

13.10.5 Assembling & Linking di un programma in formato COM

L'assemblaggio di COMTEST.ASM avviene nel solito modo; con il MASM, dalla cartella ASMBASE, il comando da impartire è:
..\bin\ml /c /Fl comtest.asm
Il comando appena impartito, genera un object file chiamato COMTEST.OBJ e anche un listing file chiamato COMTEST.LST; grazie al listing file, possiamo constatare che il blocco COMSEGM, occupa complessivamente 0694h+(4*10)=06BCh byte (1724 byte) e comprende anche i 256 byte del PSP.
Sempre attraverso il listing file, si può notare che l'entry point, rappresentato dall'etichetta start, si trova all'offset 0100h; inoltre, tutti i dati definiti in COMSEGM si trovano allineati alla DWORD (grazie anche al fatto che questa volta, ogni struttura Automobile occupa 48 byte).

Passiamo ora alla fase di linking; il comando da impartire con i linker Microsoft è:
..\bin\link /map /tiny comtest.obj
In assenza dell'opzione /tiny per i linker Microsoft, il linker genera un normale eseguibile in formato EXE, chiamato COMTEST.EXE; in questo caso, viene generato anche un messaggio di avvertimento (warning) che ci informa dell'assenza dello stack.
In presenza, invece, dell'opzione /tiny per i linker Microsoft, il linker genera un file eseguibile chiamato COMTEST.COM; in questo caso, non viene generato alcun warning relativo all'assenza dello stack. Oltre all'eseguibile in formato COM, viene generato anche un map file chiamato COMTEST.MAP, che ci fornisce un riassunto semplificato del lavoro svolto dal linker.

13.10.6 Caricamento in memoria ed esecuzione di un programma in formato COM

Vediamo ora quello che succede, quando dal prompt del DOS, si impartisce il comando:
comtest
Dopo aver seguito tutta la procedura già descritta a proposito dell'esempio EXETEST.EXE, il loader del SO trova un file chiamato COMTEST.COM; non trovando nessuna stringa 'MZ' all'inizio di questo file, il loader avvia le procedure standard per il caricamento in memoria di un eseguibile in formato COM.

Come al solito, il DOS alloca due blocchi di memoria, uno per l'environment segment e l'altro per il program segment; la domanda che ci poniamo è: in base a quali criteri il DOS determina la dimensione del program segment?
La tecnica utilizzata dal DOS è molto semplice; al program segment di un qualunque programma in formato COM, viene riservato un intero segmento di memoria che, come sappiamo, occupa 64 KiB ed è sempre allineato al paragrafo.
Supponiamo allora che il DOS abbia riservato a COMTEST.COM, il segmento di memoria che parte dall'indirizzo fisico 01BC0h; a questo indirizzo fisico, corrisponde l'indirizzo logico normalizzato 01BCh:0000h.
A questo punto, il DOS legge dal disco il file COMTEST.COM (che contiene il solo modulo caricabile) e lo copia tale e quale nel program segment; nei primi 256 byte (0100h) che precedono l'entry point, viene sistemato il PSP. Possiamo dire quindi che il PSP occupa tutti gli indirizzi fisici del program segment, compresi tra 01BC0h e:
01BC0h + (0100h - 1h) = 01CBFh
In termini di indirizzi logici, il PSP viene inserito nel segmento di memoria n. 01BCh e occupa tutti gli offset di questo segmento, compresi tra 0000h e:
0000h + (0100h - 1h) = 00FFh
In sostanza, il PSP è compreso tra gli indirizzi logici 01BCh:0000h e 01BCh:00FFh.

L'inizio del PSP coincide con l'inizio del program segment; anche nei programmi COM quindi, per convenzione, si utilizza la sigla PSP per indicare la componente Seg dell'indirizzo logico iniziale (01BCh:0000h) dello stesso program segment. Possiamo scrivere quindi:
PSP = 01BCh
Il DOS passa ora alla inizializzazione standard dei registri della CPU; per i registri di segmento CS, DS, ES e SS, il DOS pone ovviamente:
CS = DS = ES = SS = PSP = 01BCh
In sostanza, il blocco codice, il blocco dati e il blocco stack, partono tutti dallo stesso indirizzo fisico 01BC0h.
Il PSP parte dall'indirizzo logico 01BCh:0000h, per cui l'entry point del programma, si viene a trovare all'indirizzo logico 01BCh:0100h; il DOS pone quindi:
CS:IP = 01BCh:0100h
Il registro SP viene inizializzato con l'offset FFFEh (che è il più grande offset di indice pari, all'interno del segmento di memoria n. 01BCh da 64 KiB); per la coppia SS:SP, il DOS pone quindi:
SS:SP = 01BCh:FFFEh
Successivamente, il DOS inserisce una WORD di valore 0000h all'indirizzo logico SS:SP (cioè 01BCh:FFFEh); il perché di questa WORD verrà chiarito in un prossimo capitolo.

A questo punto, il programma è pronto per l'esecuzione; in seguito a tutto questo lavoro svolto dal DOS, il programma COMTEST.COM assume in memoria la disposizione illustrata in Figura 13.19. Come possiamo notare, il programma COMTEST.COM ha a sua disposizione una notevole quantità di memoria per lo stack; in base al fatto che il PSP, il codice e i dati statici di questo programma occupano 06BCh byte, possiamo dire che lo spazio riservato allo stack è pari a:
FFFEh - 06BCh = 65534 - 1724 = 63810 byte
È necessario ricordare che, man mano che lo stack si riempie, il contenuto del registro SP viene via via decrementato; se lo spazio riservato allo stack è troppo piccolo, c'è il rischio che SP invada l'area riservata al codice e ai dati statici, i quali verrebbero così sovrascritti dai dati temporanei. La Figura 13.19, permette anche di capire come ci si deve regolare, nel momento in cui si vuole creare un segmento misto dati + stack; in sostanza, è fondamentale che l'area riservata allo stack venga disposta sempre nella parte finale del segmento misto.

Il PSP, il codice, i dati statici e lo stack di COMTEST.COM, occupano l'intero segmento di memoria n. 01BCh; un segmento di memoria è formato da 65536 byte (10000h byte). Possiamo dire quindi che l'indirizzo fisico iniziale del segmento di memoria successivo al nostro programma, sarà:
01BC0h + 10000h = 11BC0h
Una volta che il DOS ha inizializzato i necessari registri della CPU, può partire la fase di esecuzione di COMTEST.COM; il controllo passa quindi alla stessa CPU che trova la coppia CS:IP inizializzata con l'entry point 01BCh:0100h. Da questo punto in poi, la fase di esecuzione si svolge nello stesso modo già descritto per EXETEST.EXE.

Nel prossimo capitolo, verranno illustrati diversi strumenti che ci permetteranno di verificare in pratica tutto ciò che è stato appena esposto in relazione alla struttura interna di un file eseguibile e al comportamento in memoria di un programma in esecuzione.