Assembly Base con NASM

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 NASM 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 NASM sia stato installato nella cartella:
C:\NASM

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 NASM.
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:\NASM\ASMBASE

13.4 La fase di assemblaggio (assembling)

La fase di assemblaggio viene svolta, ovviamente, da uno strumento chiamato assembler (assemblatore); nel caso del NASM, l'assembler si chiama NASM.EXE.
L'assembler NASM 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 NASM, dalla command line possiamo impartire il comando:
c:\nasm\nasm -h
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:\nasm\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 NASM, il comando da impartire è:
c:\nasm\nasm -f obj exetest.asm -l exetest.lst
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:
error: parser: instruction expected
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 NASM, si tratta dell'opzione -l seguita dal nome che vogliamo assegnare al file (la convenzione più seguita consiste nel dare a questo file lo stesso nome del file ASM e l'estensione LST).
L'opzione -l 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 NASM nella fase di assemblaggio di un modulo Assembly. La Figura 13.2 mostra proprio il contenuto del file EXETEST.LST generato da NASM, 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 la numerazione progressiva delle varie linee del codice sorgente; questa numerazione, viene generata a beneficio del programmatore che così può avere dei punti di riferimento all'interno del programma che deve analizzare.
La seconda colonna contiene gli offset delle varie informazioni presenti nei segmenti di programma e nelle eventuali dichiarazioni; più avanti analizzeremo il procedimento seguito dall'assembler per calcolare questi offset.
La terza 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 8, ad esempio, contiene la direttiva CPU 386 che 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.

La riga 12 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.

Tra la riga 14 e la riga 46, notiamo la presenza delle 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 NASM, ad esempio, utilizza il valore simbolico 0000h. 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: Se si sta utilizzando la versione a 32 bit di NASM, la componente Offset di $ può arrivare sino a FFFFFFFFh, superando quindi il valore massimo 0000FFFFh previsto per i segmenti di programma con attributo USE16; è compito del programmatore evitare che ciò possa accadere.

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 terza 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.

Alla riga 285 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 0000h; di conseguenza, l'assembler genera il codice macchina:
10111000b 0000000000000000b = B8h 0000h
In Figura 13.2 notiamo che NASM indica l'operando Imm16 con [0000]; le parentesi quadre indicano che il valore 0000h è 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 (riga 296 di Figura 13.2):
mov word [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
Tutti i valori esadecimali formati da 2 o più byte, vengono mostrati da NASM con i byte disposti al contrario; ad esempio, il valore a 16 bit 33CCh viene disposto come CC33h, mentre il valore a 32 bit AABBCCDDh viene disposto come DDCCBBAAh. Il precedente codice macchina viene quindi rappresentato da NASM come:
C7h 06h 2D00h C21Ah
È importante acquisire una certa pratica anche con questo modo di rappresentare i numeri in quanto molti editor esadecimali utilizzano questa convenzione; nel seguito del capitolo e nei capitoli successivi, i valori esadecimali verranno sempre rappresentati nel modo classico (peso dei byte che cresce da destra verso sinistra).

Nell'istruzione che abbiamo appena esaminato, l'identificatore varRect+p2+x è privo della componente Seg; come già sappiamo, ciò è possibile in quanto, in assenza di diverse indicazioni da parte del programmatore, un Disp16 viene sempre associato dalla CPU alla componente Seg contenuta nel registro DS. Naturalmente, è fondamentale che lo stesso DS sia già stato inizializzato dal programmatore con DATASEGM!
Possiamo dire allora che in questo caso, varRect+p2+x viene gestito 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:
mov word [es:varRect+p2+x], 6850
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+p2+x 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 (linea 429 di Figura 13.2) relativo all'istruzione:
add al, byte [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, possiamo dire all'assembler che vogliamo accedere ad una locazione di memoria da 8 bit. Il codice macchina della precedente 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 (linea 397 di 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 racchiude tra [ ] ogni Disp16 presente nelle istruzioni del segmento di codice; anche in questo caso, queste [ ] 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 (res 00000400); 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.
Tutte queste informazioni vengono poi passate al linker e diventano determinanti nella fase di generazione del programma eseguibile; analizziamo quindi in dettaglio il lavoro svolto proprio dal linker.

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.

Il NASM fornisce unicamente l'assembler, per cui è necessario munirsi di un linker; a tale proposito, possiamo utilizzare un qualunque linker destinato all'ambiente DOS.
Tra le varie scelte possibili si può citare il linker TLINK.EXE fornito con il Borland TASM o il linker LINK.EXE fornito con il Microsoft Quick Basic; nel caso del Microsoft MASM, bisogna munirsi del linker LINK.EXE fornito con le versioni sino alla 6.11 dell'assembler in quanto le versioni superiori sono destinate all'ambiente Windows a 32 bit.
Nella sezione Compilatori assembly, c++ e altri dell’ Area Downloads di questo sito è possibile scaricare il MASM 6.00B che contiene una versione minimale di MASM; il linker presente in tale archivio può essere usato in combinazione con NASM.
Vanno benissimo anche i linker forniti dai compilatori C/C++, Pascal, Fortran, etc, destinati all'ambiente DOS; in alternativa, su Internet si possono reperire anche alcuni linker freeware (un ottimo esempio è VAL).
Nel seguito del capitolo e nei capitoli successivi, si farà riferimento, per semplicità, al linker LINK.EXE di MASM; a tale proposito, si assume che LINK.EXE sia stato installato nella cartella:
C:\MASM\BIN
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. Una caratteristica molto potente di NASM è data dal fatto che questo assembler supporta pienamente i formati OMF per DOS, COFF per Windows, ELF e AOUT per Linux, AOUTB per BSD, etc; 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.

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, se stiamo utilizzando i linker della Microsoft, 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 (terza 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 (riga 372 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: 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. (eventuali segmenti di ampiezza nulla elencati nel map file, sono legati a convenzioni interne di NASM e possono essere ignorati).

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:\NASM
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 Le direttive ALIGN e ALIGNB

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 delle direttive:
ALIGN n
e
ALIGNB n
La direttiva ALIGN richiede obbligatoriamente un primo 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.
Il secondo argomento è facoltativo e rappresenta il valore da 1 byte che l'assembler deve utilizzare per riempire i necessari buchi di memoria, sino ad ottenere l'allineamento richiesto; in assenza del secondo argomento, ALIGN utilizza il valore predefinito 90h che rappresenta il codice macchina dell'istruzione NOP.
Il mnemonico NOP significa no operation (nessuna operazione da eseguire); in presenza del codice macchina 90h, la CPU si limita ad incrementare di 1 il registro IP passando così all'elaborazione dell'istruzione successiva.
In alternativa, possiamo specificare il secondo argomento di ALIGN attraverso le direttive DB o RESB (le quali, ovviamente, devono indicare una sola locazione da 1 byte); possiamo scrivere, ad esempio:
ALIGN 4, db 0
In questo modo, l'assembler allinea il dato successivo ad un offset multiplo intero di 4 (allineamento alla DWORD) e riempie i necessari buchi di memoria con un numero opportuno di byte di valore 00h.
Se scriviamo, invece:
ALIGN 2, resb 1
stiamo dicendo all'assembler di allineare il dato successivo ad un offset multiplo intero di 2 (allineamento alla WORD); i necessari buchi di memoria vengono riempiti dall'assembler con un numero opportuno di byte non inizializzati.

La direttiva ALIGNB è del tutto simile ad ALIGN; l'unica differenza sta nel fatto che, in assenza di diverse indicazioni da parte del programmatore, ALIGNB utilizza come secondo argomento predefinito la direttiva RESB 1.

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 le direttive ALIGN e ALIGNB, raggiungono la massima efficacia quando lavorano 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 (si noti che per il riempimento dei buchi di memoria è stato utilizzato un byte di valore 00h). 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 (inizializzato a 00h) 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 e ALIGNB all'interno delle dichiarazioni di STRUC.
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. La direttiva ALIGNB utilizza RESB 1 come secondo argomento predefinito; l'uso di RESB è necessario in quanto all'interno di un blocco STRUC non possiamo inizializzare i dati.
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.

Quando inseriamo una direttiva ALIGN o ALIGNB in un segmento dati, il valore assegnato all'eventuale secondo argomento non ha alcuna importanza; in generale, la cosa migliore da fare consiste nell'usare DB 0 nei segmenti di dati inizializzati e RESB 1 nei segmenti di dati non inizializzati.
Il discorso cambia radicalmente nel caso di direttive ALIGN o ALIGNB inserite nei segmenti di codice; in una situazione del genere, non possiamo usare queste direttive con un secondo argomento qualunque in quanto rischiamo di trarre in inganno la CPU!
Supponiamo, ad esempio, di utilizzare ALIGN con secondo argomento DB 0 in modo che l'assembler inserisca buchi di memoria di valore 00h; 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 bisogna rigorosamente inserire buchi di memoria rappresentati da dei NOP, equivalenti a byte di valore 90h; a tale proposito, è sufficiente utilizzare ALIGN senza specificare il secondo argomento (il cui valore predefinito è, appunto, 90h)!

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 Calcolo delle componenti Seg e Offset con NASM

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, NASM mette a disposizione una sintassi estremamente semplice.

Nel Capitolo 11 abbiamo visto che in NASM il nome simbolico di una qualsiasi informazione (dato o etichetta), rappresenta la componente Offset dell'indirizzo logico dell'informazione stessa, relativa al segmento di appartenenza; consideriamo, ad esempio, la seguente istruzione riferita al programma di Figura 13.1:
mov bx, 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 un nome simbolico, 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), NASM 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.

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:
resb 0100h
Come vedremo più avanti, NASM permette di generare anche dei file grezzi, chiamati file binari, contenenti esclusivamente il codice macchina di un programma; in base a quanto è stato esposto in precedenza, il formato COM è totalmente compatibile con il formato binario grezzo supportato da NASM (infatti, un file COM contiene solamente il codice macchina da eseguire).
Esclusivamente per il formato binario grezzo, NASM fornisce la direttiva ORG n (che sta per origin); questa direttiva dice all'assembler che la componente Offset del location counter $ deve essere spostata in avanti di n byte.
In modalità reale, quindi, n deve essere un Imm16; nei capitoli successivi vedremo che n può essere persino una espressione complessa.
Supponiamo che, ad esempio, l'assembler incontri la seguente direttiva all'offset 0200h di un blocco CODESEGM:
org 0150h
In tal caso, l'assembler incrementa di 0150h byte la componente Offset del location counter (che in quel punto vale 0200h) portandola quindi a:
0200h + 0150h = 0350h
Tutti i 0150h byte compresi tra CODESEGM:0200h e CODESEGM:034Fh vengono saltati e rimangono privi di inizializzazione; l'assemblaggio di CODESEGM riprende quindi dall'indirizzo logico CODESEGM:0350h.

Si tenga presente che nel caso di MASM, l'omonima direttiva ORG agisce in modo totalmente differente; a tale proposito, si può consultare il tutorial Assembly Base Versione MASM!

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.

Ricordando quanto è stato esposto in relazione agli eseguibili in formato EXE, 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, ..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 ALIGNB all'interno della struttura Automobile e la direttiva ALIGN 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 NASM, dalla cartella ASMBASE, il comando da impartire è:
c:\nasm\nasm -f obj comtest.asm -l comtest.lst
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 è:
c:\masm\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.

13.10.7 Generazione diretta di eseguibili in formato COM

Da quanto è stato esposto, risulta evidente che un file COM contiene esclusivamente la traduzione grezza, in codice macchina, del codice sorgente scritto dal programmatore; ciò significa che nel caso estremamente semplice di un piccolo programma Assembly costituito da un unico file, noi stessi possiamo leggere il relativo codice macchina (fornito dal listing file) e copiarlo direttamente in un altro file il quale assumerà, in tutto e per tutto, le caratteristiche di un eseguibile in formato COM (nel prossimo capitolo vedremo un esempio pratico)!
Lo stesso NASM fornisce un'opzione che permette di ottenere un file binario grezzo contenente la traduzione diretta in codice macchina di un programma Assembly distribuito su un unico file; per quanto è stato appena esposto, il file binario così ottenuto è perfettamente compatibile con la struttura di un eseguibile in formato COM.

La Figura 13.20 illustra la sintassi fornita da NASM per la creazione di file binari grezzi; è necessario ribadire che questa possibilità viene offerta solo per piccoli programmi Assembly distribuiti su un unico file. La suddivisione del programma in tre blocchi (.text, .data e .bss) è assolutamente fittizia; infatti, NASM provvede a creare un unico segmento di programma contenente, nell'ordine, il codice, i dati statici inizializzati, i dati statici non inizializzati e lo stack.
L'attributo ALIGN è opzionale; in ogni caso, il suo impiego è consigliabile soprattutto per far partire i dati da indirizzi adeguati all'architettura della CPU che si sta utilizzando.
Nel caso in cui si voglia generare un eseguibile in formato COM, bisogna ovviamente passare n=0100h alla direttiva ORG.

Supponendo di avere a disposizione il codice sorgente nomefile.asm di un programma Assembly dotato della struttura illustrata in Figura 13.20, possiamo ottenere l'eseguibile nomefile.com attraverso il seguente comando NASM:
c:\nasm\nasm -f bin nomefile.asm -o nomefile.com
Si noti come sia necessario specificare il formato bin attraverso l'apposita opzione -f; l'opzione -o permette di specificare il nome del file eseguibile (file di output).

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.