Assembly Windows 32bit con MASM

Capitolo 4: Assembling & linking


In questo capitolo vedremo come si deve procedere per convertire un programma Assembly in un eseguibile per Win32; tutte le considerazioni che verranno svolte si riferiscono alle versioni più recenti di MASM; per quanto riguarda il MASM faremo riferimento alle versioni 6.x o superiori. Tutte le versioni precedenti di MASM non sono in grado di generare eseguibili per Win32; il problema non riguarda l'assembler ma il linker. Teoricamente, un qualsiasi assembler a 32 bit è in grado di convertire in formato oggetto un programma Assembly scritto per Win32; il linker invece deve essere in grado di generare un eseguibile a 32 bit avente un particolare formato utilizzato da Win32. Questo formato che verrà analizzato in un apposito capitolo, viene indicato con la sigla PE (Portable Executable); è fondamentale quindi procurarsi un linker che supporti questo formato. I linker forniti in dotazione con MASM 6.x supportano pienamente il formato PE; alternativamente è possibile servirsi dei linker forniti in dotazione con i vari compilatori per Win32.

4.1 Convenzioni adottate nella sezione Assembly Windows 32bit

In tutti gli esempi presentati nella sezione Assembly Windows 32bit, si suppone che l'utente abbia installato il MASM nella cartella:
C:\MASM32
in questo caso, tutti gli include files di MASM si troveranno nella cartella:
C:\MASM32\INCLUDE
mentre le librerie di MASM si troveranno nella cartella:
C:\MASM32\LIB
Come al solito è importante crearsi una cartella di lavoro dove sistemare i files relativi ai vari esempi; in tutti gli esempi presentati nella sezione Assembly Windows32bit, si fa riferimento ad una cartella di lavoro chiamata win32asm. Se si utilizza MASM questa cartella può trovarsi in:
C:\WIN32ASM
Ricordiamoci che a differenza di quanto accade in ambiente Unix, in ambiente DOS/Windows il SO non distingue tra lettere maiuscole e minuscole utilizzate per i nomi e per i percorsi dei vari files; non esiste nessuna differenza quindi tra c:\masm32 e C:\MASM32.

4.2 Il primo programma Assembly per Win32

Per illustrare le fasi di assembling e di linking di un programma Assembly per Win32 utilizzeremo in questo capitolo alcuni esempi molto semplici; si tratta di piccolissime applicazioni Win32 ridotte quasi al minimo indispensabile. La Figura 4.1 mostra il primo esempio che utilizzeremo in questo capitolo; si tratta della versione MASM di un programma che viene chiamato PRIMO.ASM. Come si può notare, rispetto al template presentato nel precedente capitolo sono state rimosse le direttive INCLUDE; questo procedimento si rende necessario perché in seguito, in fase di assemblaggio dovremo generare il listing file. In presenza dei vari include files, l'assembler genera un listing file gigantesco contenente una marea di simboli che non dobbiamo utilizzare; per evitare questo inconveniente, rimuoviamo le direttive INCLUDE e inseriamo nel nostro programma solamente le costanti e i prototipi di procedure di cui abbiamo bisogno. La Figura 4.1 mostra appunto la presenza nel nostro programma di due sezioni contenenti queste informazioni copiate direttamente dai vari include files; il contenuto di queste due sezioni viene descritto più avanti.
Analizzando il listato di Figura 4.1, notiamo che nel blocco dati inizializzati vengono definite due stringhe chiamate strTitolo e strMessaggio; si può anche notare che queste due stringhe terminano entrambe con uno zero secondo la convenzione del linguaggio C. Questa è una situazione molto frequente in Windows dove spesso si ha a che fare con procedure che manipolano stringhe in versione C; nel gergo di Windows queste stringhe vengono chiamate Zero Terminated Strings (stringhe terminate da uno zero) o C Strings (stringhe C). Se definiamo una stringa C e dimentichiamo di specificare lo zero finale, la procedura che riceve questa stringa come parametro assumerà un comportamento imprevedibile; ricordiamo ancora una volta che quando si parla di zero finale di una stringa C, ci si riferisce al valore numerico 0 e non al codice ASCII del simbolo '0'.
Il programma PRIMO quando viene eseguito mostra sullo schermo la stringa strMessaggio; un metodo molto semplice per raggiungere questo scopo consiste nell'utilizzare la procedura predefinita MessageBox. Consultando il Win32 Programmer's Reference si può notare che questa procedura viene definita nella libreria USER32.LIB ed è dichiarata come:
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
Per una descrizione dettagliata delle caratteristiche di MessageBox è necessario consultare il solito Win32 Programmer's Reference; analizziamo invece gli aspetti di MessageBox che interessano a noi programmatori Assembly. Chiamando la procedura MessageBox viene visualizzata sullo schermo una finestra predefinita chiamata appunto message box (finestra dei messaggi); nella sua forma più semplice questa procedura richiede quattro parametri e quando termina restituisce un valore intero a 32 bit (tipo int del linguaggio C). Le convenzioni seguite dai linguaggi di alto livello per la restituzione di valori da parte delle procedure, vengono dettagliatamente descritte nella sezione Assembly Base e sono valide anche per Win32; anche in questo caso quindi i valori interi a 8 bit vengono restituiti in AL, i valori interi a 16 bit vengono restituiti in AX, i valori interi a 32 bit vengono restituiti in EAX, i valori in virgola mobile vengono restituiti nel registro ST(0) della FPU, etc. Il valore intero a 32 bit restituito in EAX da MessageBox codifica una determinata azione compiuta dall'utente; nella gran parte dei casi l'azione consiste nella pressione del bottone 'OK' presente nella finestra dei messaggi. Il bottone 'OK' può essere premuto con il pulsante sinistro del mouse oppure attraverso il tasto [Invio]; il codice associato alla pressione del bottone 'OK' è rappresentato dal valore 00000001h ed è dichiarato in windows.inc come:
IDOK = 00000001h
I codici associati ad altre azioni verranno esaminati al momento opportuno; analizziamo ora i parametri richiesti dalla procedura MessageBox. Il primo parametro (hWnd) consente di passare alla finestra dei messaggi il codice numerico della finestra principale che ha effettuato la chiamata di MessageBox; come è stato spiegato in un precedente capitolo, normalmente ogni applicazione Windows è dotata di una finestra chiamata main window (finestra principale). La finestra principale viene individuata da Windows attraverso un codice identificativo chiamato handle to window o hwnd (handle = maniglia); quando la finestra principale chiama una finestra che potremmo definire "finestra figlia", deve passarle il suo hwnd. Se, come nel nostro caso, non esiste una finestra principale, il parametro hwnd da passare alla finestra figlia (che in questo caso è la MessageBox) deve valere zero; come al solito, invece di utilizzare valori numerici espliciti, conviene sempre servirsi di costanti simboliche. Nel nostro caso utilizziamo la costante simbolica NULL che rappresenta appunto un valore nullo; la costante NULL presente in Figura 4.1 viene dichiarata anche in windows.inc. In sostanza, il tipo HWND indica un tipo di dato intero a 32 bit che corrisponde quindi al tipo DWORD dell'Assembly; se consultiamo infatti il file windows.inc, troviamo la dichiarazione:
HWND TYPEDEF DWORD
Il secondo parametro richiesto dalla procedura MessageBox è l'indirizzo di una stringa C chiamata lpText; questa stringa deve contenere il messaggio che verrà mostrato dalla message box. Come si nota dal prototipo di MessageBox, il parametro lpText viene dichiarato di tipo LPCTSTR; questo mnemonico sta per Long Pointer to C Text STRing (puntatore FAR ad una stringa di testo terminata da uno zero). I puntatori NEAR e FAR esistono anche in Win32 per garantire la compatibilità con i vecchi programmi scritti per Win16; siccome noi stiamo scrivendo una applicazione "pura" a 32 bit per Win32, dobbiamo ignorare totalmente questa distinzione tra diversi tipi di puntatori che riguarda esclusivamente Win16. Come già sappiamo, in Win32 un qualsiasi indirizzo di memoria è rappresentato da un offset a 32 bit, e cioè da un numero intero senza segno che può andare da 00000000h a FFFFFFFFh; possiamo dire quindi che dal punto di vista dell'Assembly, il tipo LPCTSTR è perfettamente equivalente al tipo DWORD. Anche in questo caso, consultando il file windows.inc troviamo proprio la dichiarazione:
LPCTSTR TYPEDEF DWORD
Il terzo parametro richiesto da MessageBox viene chiamato lpCaption ed è anch'esso di tipo LPCTSTR; si tratta quindi dell'indirizzo a 32 bit di un'altra stringa C. Questa stringa verrà visualizzata nell'area riservata al titolo della finestra dei messaggi; nel gergo di Windows l'area riservata al titolo di una finestra viene chiamata Title Bar (barra del titolo).
Il quarto parametro richiesto da MessageBox viene chiamato uType; si tratta di un valore intero senza segno a 32 bit che permette di gestire numerosi dettagli relativi alla finestra dei messaggi. Attraverso questo parametro, possiamo richiedere la visualizzazione nella message box di bottoni predefiniti e icone predefinite; possiamo anche specificare quale bottone è attivo e quale comportamento deve assumere la message box. Per gestire tutti questi dettagli, si utilizzano delle bit mask predefinite; a ciascuna categoria di dettagli viene riservata una parte dei bit del parametro uType. Per specificare il tipo desiderato di bottoni si utilizza il primo nibble di uType, per specificare il tipo desiderato di icona si utilizza il secondo nibble di uType, per specificare il bottone attivo si utilizza il terzo nibble di uType e così via; per combinare tra loro le diverse categorie di dettagli dobbiamo combinare le relative bit mask attraverso l'operatore OR dell'Assembly. Nel listato di Figura 4.1 sono presenti alcune di queste bit mask copiate direttamente dal file windows.inc; analizziamo in particolare le due bit mask chiamate MB_OK e MB_ICONINFORMATION. La costante MB_OK vale 00000000h e rappresenta il codice del bottone predefinito 'OK' (Okey Button); passando MB_OK come quarto parametro di MessageBox, otteniamo la visualizzazione del bottone predefinito 'OK'. La costante MB_ICONINFORMATION vale 00000040h e rappresenta il codice dell'icona predefinita 'ICONINFORMATION' (Icona Info); passando MB_ICONINFORMATION come quarto parametro di MessageBox, otteniamo la visualizzazione dell'icona predefinita 'ICONINFORMATION'. Osserviamo che il nibble meno significativo di MB_ICONINFORMATION vale 0; passando questa bit mask si ottiene quindi la visualizzazione della relativa icona e anche del bottone 'OK' che è il bottone predefinito della message box. Per rendere esplicito il fatto che vogliamo visualizzare contemporaneamente il bottone 'OK' e l'icona 'ICONINFORMATION', dobbiamo passare come quarto parametro di MessageBox la combinazione:
MB_OK OR MB_ICONINFORMATION
Questa situazione viene mostrata proprio nel listato di Figura 4.1.
Ricordiamo che non bisogna confondere gli operatori dell'Assembly (come OR) con le analoghe istruzioni della CPU; gli operatori dell'Assembly devono comparire in espressioni contenenti esclusivamente operandi costanti. Queste espressioni vengono analizzate e risolte dall'assembler in fase di assemblaggio del programma; osservando ad esempio che MB_OK vale 00000000h e MB_ICONINFORMATION vale 00000040h, l'espressione precedente verrà convertita dall'assembler nel valore esplicito:
00000000h OR 00000040h = 00000040h
(gli operatori dell'Assembly vengono trattati in dettaglio nella sezione Assembly Base).
Consultando il Win32 Programmer's Reference si può trovare l'elenco delle costanti predefinite associate ai vari dettagli della MessageBox; i valori numerici associati a queste costanti vengono dichiarati in windows.inc. Come è stato già detto, il manuale di riferimento reperibile via Internet è aggiornato al 1996; nel frattempo l'API di Windows è cresciuta con l'aggiunta di numerose nuove costanti predefinite, nuove strutture, nuovi prototipi di procedure, etc. Un modo efficace per conoscere tutte queste novità consiste nell'esplorare il contenuto delle versioni più recenti degli header files forniti in dotazione con i vari compilatori per Win32; altre informazioni importanti possono essere reperite attraverso la documentazione on-line fornita dalla Microsoft.
Tornando al quarto parametro di MessageBox, possiamo dire quindi che anche in questo caso abbiamo a che fare con un intero a 32 bit; nel file windows.inc è presente infatti la dichiarazione:
UINT TYPEDEF DWORD
In sostanza, la procedura MessageBox richiede quattro parametri di tipo DWORD e quando termina restituisce in EAX un valore di tipo DWORD; in base a tutte queste considerazioni, il prototipo di MessageBox mostrato in Figura 4.1 è proprio:
MessageBoxA PROTO :DWORD, :DWORD, :DWORD, :DWORD
Naturalmente, sfruttando le dichiarazioni presenti in windows.inc possiamo anche scrivere:
MessageBoxA PROTO :HWND, :LPCTSTR, :LPCTSTR, :UINT
In questo modo rendiamo più esplicite le caratteristiche dei vari parametri.
Un'ultima considerazione relativa alla MessageBox riguarda il fatto che in Figura 4.1 questa procedura viene chiamata MessageBoxA con la A finale; si tratta di un'aspetto che in Win32 assume una notevole importanza. Tutte le procedure che manipolano stringhe, sono disponibili in Win32 in due differenti versioni chiamate versione ASCII e versione UNICODE; come si può facilmente intuire, questa distinzione si riferisce al fatto che esistono stringhe in formato ASCII e stringhe in formato UNICODE. Le stringhe in formato ASCII sono vettori di simboli dove ogni simbolo è rappresentato da un codice ASCII a 8 bit; le stringhe in formato UNICODE sono vettori di simboli dove ogni simbolo è rappresentato da un codice UNICODE a 16 bit. I simboli ASCII vengono anche chiamati Char (caratteri), mentre i simboli UNICODE vengono anche chiamati Wide Char (caratteri larghi); le procedure che manipolano stringhe in formato ASCII hanno dei nomi che terminano con A, mentre le procedure che manipolano stringhe in formato UNICODE hanno dei nomi che terminano con W. Nel nostro caso, stiamo utilizzando la procedura MessageBoxA che richiede quindi stringhe in formato ASCII che terminano con un byte che vale zero; l'analoga procedura MessageBoxW richiede stringhe in formato UNICODE che terminano con una word che vale zero. Maggiori dettagli sulla realizzazione di applicazioni Win32 con supporto UNICODE sono disponibili nel Win32 Programmer's Reference.

Passiamo ora alla procedura ExitProcess che è stata già descritta nel precedente capitolo; in Win32 è importantissimo terminare un'applicazione attraverso la chiamata di ExitProcess. In questo modo infatti si aiuta il SO ad effettuare correttamente tutte le necessarie operazioni di "pulizia"; un programmino come PRIMO.ASM termina in modo pulito anche se non si chiama ExitProcess. La situazione però cambia radicalmente nel momento in cui si vuole terminare un programma molto più complesso; in casi del genere, un programma che termina senza chiamare ExitProcess "scompare" dallo schermo lasciando però vari "pezzi" sparpagliati in memoria. Una situazione del genere si verifica ad esempio quando due o più applicazioni stanno condividendo la stessa DLL; se tutte le applicazioni terminano chiamando ExitProcess, permettono al SO di sapere quando è il momento di rimuovere la DLL dalla memoria.

A questo punto il funzionamento del programma PRIMO.ASM appare abbastanza chiaro; subito dopo l'entry point (start), viene chiamata la MessageBox che mostra un messaggio sullo schermo; non appena l'utente preme il bottone 'OK', la finestra dei messaggi viene chiusa. Subito dopo viene chiamata ExitProcess che "notifica" al SO la terminazione del nostro programma.

4.3 La fase di assembling con MASM

Vediamo ora come si deve procedere per convertire PRIMO.ASM in formato oggetto; cominciamo dal caso in cui l'utente voglia utilizzare MASM. Visto e considerato che la versione più recente di MASM è scaricabile gratuitamente da Internet, non avrebbe senso pretendere di utilizzare una vecchia versione di questo assembler; faremo riferimento quindi alla versione 6.x o superiore. Installando il MASM nella cartella:
C:\MASM32
tutti gli strumenti di sviluppo vengono a trovarsi nella cartella:
C:\MASM32\BIN
l'assemblatore fornito dal MASM si chiama ML.EXE. Prima di tutto è necessario che il file PRIMO.ASM si trovi nella cartella di lavoro:
c:\win32asm
posizionandoci in questa cartella, dal prompt del DOS dobbiamo digitare:
c:\masm32\bin\ml /c /coff /I c:\masm32\include /Fl primo.asm
Premendo ora il tasto [Invio] parte la fase di assemblaggio del programma; se non vengono trovati errori, viene generato il file PRIMO.OBJ che contiene il codice oggetto del nostro programma. Il parametro /c dice all'assembler di limitarsi alla sola fase di assemblaggio; in assenza di questo parametro, parte automaticamente anche la fase di linking con la generazione dell'eseguibile. Il parametro /coff dice all'assembler di generare il file PRIMO.OBJ nel formato COFF (Common Object File Format); questo è il formato oggetto standard utilizzato da molti strumenti di sviluppo Microsoft in ambiente Windows. Il formato COFF è incompatibile con il formato OMF (Object Module Format) utilizzato invece dagli strumenti di sviluppo della Borland; proprio per questo motivo, non è possibile utilizzare il TASM per creare applicazioni per Win32. Il parametro /I c:\masm32\include dice all'assembler dove cercare gli include files per risolvere i simboli e i prototipi di procedure utilizzati dal programma; il parametro /Fl infine dice all'assembler di generare il listing file; in assenza di altre indicazioni da parte dell'utente, questo file verrà chiamato PRIMO.LST.

4.4 Il listing file

Utilizzando il parametro /Fl con il MASM, abbiamo chiesto all'assembler di generare il listing file del nostro programma; dopo la fase di assemblaggio quindi, abbiamo a disposizione non solo l'object file PRIMO.OBJ ma anche il listing file PRIMO.LST. Questo file ci permette di analizzare in dettaglio il lavoro svolto dall'assembler; la Figura 4.2 mostra in particolare il listing file prodotto da MASM. Come è stato già spiegato nella sezione Assembly Base, la parte iniziale del listing file mostra sulla destra il listato del nostro programma, e sulla sinistra una serie di informazioni inserite dall'assembler e comprendenti anche il codice macchina; la colonna più a sinistra contiene la numerazione delle linee che formano il listato del programma. La seconda colonna a partire da sinistra contiene gli offset relativi al contenuto di ciascun segmento di programma; come si può notare, questa volta gli offset sono a 32 bit. La terza colonna contiene la conversione del nostro programma in codice macchina più altre informazioni inserite dall'assembler; in particolare, notiamo che l'assembler prende nota dei valori numerici espliciti associati a ciascuna costante dichiarata nel programma. Il blocco dati inizializzati _DATA inizia con la stringa strTitolo che si trova ovviamente all'offset 00000000h, cioè a 0 byte di distanza dall'inizio di _DATA; questa stringa occupa 23 byte (17h) per cui l'assembler a partire dall'offset 00000000h crea un vettore di 23 byte che vengono inizializzati con i codici ASCII dei caratteri che formano la stringa stessa. La stringa strTitolo occupa tutti gli offset che vanno da 00000000h a 00000016h, per cui la stringa successiva (strMessaggio) parte dall'offset 00000017h; questa seconda stringa occupa 48 byte (30h), per cui l'assembler a partire dall'offset 00000017h crea un vettore di 48 byte che vengono inizializzati con i codici ASCII dei caratteri che formano la stringa stessa. Complessivamente il segmento _DATA occupa in tutto 71 byte (47h); infatti, come si vede in Figura 4.2 il blocco _DATA termina all'offset 00000047h.
Il blocco codice _TEXT inizia con l'entry point rappresentato dall'etichetta start che si trova quindi all'offset 00000000h rispetto all'inizio di questo segmento; subito dopo l'entry point troviamo il codice macchina prodotto dall'assembler relativamente alla chiamata di MessageBoxA. Come si può notare, grazie alla presenza della direttiva STDCALL, l'assembler è in grado di stabilire l'ordine di inserimento nello stack, dei parametri da inviare alla procedura; analizziamo ora i codici macchina delle varie istruzioni PUSH. Il codice macchina dell'istruzione PUSH con operando immediato, è formato dall'opcode 011010_s_0 seguito dal valore immediato; come già sappiamo, il bit indicato con s è il sign bit che consente all'assembler di produrre un codice macchina molto compatto. Il primo parametro da inserire nello stack è il risultato dell'espressione:
MB_OK OR MB_ICONINFORMATION
L'assembler converte quest'espressione nel valore esplicito 40h che occupa un solo byte, contro i quattro byte occupati da 00000040h; l'assembler pone s=1 e ottiene il codice macchina:
6A 40h
formato da due soli byte; quando la CPU incontra questo codice macchina, capisce che deve ampliare 40h a 32 bit con estensione del bit di segno. Siccome 40h=01000000b, la CPU converte 40h in 00000040h, decrementa ESP di 4 byte e inserisce 00000040h nello stack. Il secondo parametro da inserire nello stack è l'offset 00000000h di srtTitolo; trattandosi di un numero positivo a 32 bit, l'assembler pone s=0 e ottiene il codice macchina:
68 00000000h
Quando la CPU incontra questo codice macchina, decrementa ESP di 4 byte e inserisce 00000000h nello stack; lo stesso procedimento viene seguito per l'offset 0000000Fh di strMessaggio (terzo parametro). Il quarto parametro da inserire nello stack è il valore immediato 00000000h (NULL); con lo stesso procedimento usato per il primo parametro, l'assembler pone s=1 e ottiene il codice macchina:
6A 00h
Quando la CPU incontra questo codice macchina, decrementa ESP di 4 byte, converte 00h in 00000000h e inserisce questo valore nello stack; a questo punto tutti i parametri sono stati inseriti nello stack per cui si può procedere alla chiamata di MessageBoxA che provvede a visualizzare la stringa strMessaggio. Dopo la chiamata di MessageBoxA troviamo il codice macchina relativo alla chiamata di ExitProcess; come si può notare, per entrambe le chiamate l'assembler utilizza l'opcode E8h che come sappiamo è relativo all'istruzione CALL direct within segment (chiamata diretta all'interno del segmento). Questo aspetto è molto importante in quanto ci fa capire bene il concetto di modello di memoria FLAT; in sostanza, al momento di avviare la fase di esecuzione, Win32 carica in memoria il nostro programma inserendolo in un "supersegmento" virtuale da 4 Gib. All'interno di questo supersegmento vengono caricate anche le librerie dinamiche (DLL) contenenti i servizi del SO richiesti dal nostro programma (come MessageBoxA, ExitProcess, etc); tutto ciò che è presente in questo supersegmento (codice, dati, stack del programma, librerie dinamiche, etc) è indirizzabile in modo lineare attraverso un semplice offset a 32 bit.
La seconda parte del listing file contiene come al solito la symbol table (tavola dei simboli); in quest'area l'assembler raccoglie informazioni dettagliate su tutti i nomi simbolici presenti nel programma (nomi di procedure, di variabili, di etichette, etc). La parte finale del listing file contiene tutte le informazioni relative ai segmenti di programma; in quest'area possiamo vedere come vengono raggruppati i vari segmenti di programma e quali nomi vengono assegnati ai gruppi. In particolare, si può notare che il blocco _DATA viene inserito nel gruppo DGROUP di cui fa parte anche lo STACK del programma; in questo gruppo vengono inseriti anche gli eventuali blocchi _BSS e CONST.
Nel caso del listing file generato dal MASM, si nota l'assenza delle informazioni relative al codice macchina prodotto dalle varie chiamate delle procedure con le direttive invoke; evidentemente questo aspetto è legato al fatto che la Microsoft non vuole rendere pubblici i dettagli relativi al funzionamento di questa direttiva.

4.5 La fase di linking con MASM

Dopo aver esaminato il listing file PRIMO.LST, possiamo passare alla fase di linking che ci consentirà di ottenere l'eseguibile finale chiamato PRIMO.EXE; nella fase di linking viene anche effettuato il collegamento tra PRIMO.OBJ e le necessarie librerie di Win32. Le librerie da collegare sono USER32.LIB (che contiene la procedura MessageBoxA) e KERNEL32.LIB (che contiene la procedura ExitProcess); come si può vedere in Figura 4.1, con MASM queste librerie possono essere specificate direttamente nel codice sorgente attraverso le direttive INCLUDELIB.
Cominciamo come al solito dal caso in cui l'utente stia utilizzando MASM 6.x o superiore; in questo caso il linker si chiama LINK.EXE. Prima di tutto posizioniamoci nella cartella di lavoro:
c:\win32asm
dove deve essere presente l'object file PRIMO.OBJ; dal prompt del DOS dobbiamo digitare:
c:\masm32\bin\link /subsystem:windows /libpath:c:\masm32\lib /map primo.obj
Premendo ora il tasto [Invio] parte la fase di linking di PRIMO.OBJ; se non vengono trovati errori, viene generato il file PRIMO.EXE che contiene il codice eseguibile del nostro programma. Il parametro /subsystem:windows dice al linker di generare un eseguibile in formato PE per Win32; il parametro /libpath:c:\masm32\lib dice all'assembler dove cercare le librerie necessarie per collegare le procedure dei servizi di Windows utilizzate dal programma. Il parametro /map dice al linker di generare il map file che in assenza di diverse indicazioni da parte dell'utente, viene chiamato PRIMO.MAP.

4.6 Il Map File

Nella fase di linking del nostro programma, abbiamo chiesto al linker di generare il Map File; nel caso di MASM si utilizza l'opzione /map. In assenza di diverse indicazioni da parte del programmatore, viene generato un map file chiamato PRIMO.MAP; la Figura 4.3 mostra il map file generato dal MASM. Con l'ausilio del map file possiamo analizzare il lavoro svolto dal linker; come già sappiamo, questo lavoro consiste principalmente nella determinazione di tutte le caratteristiche dei vari segmenti che formano il nostro programma e nella verifica di tutti i riferimenti a simboli definiti in altri moduli. I segmenti di programma vengono poi fusi con i corrispondenti segmenti predisposti dal SO; infine il linker provvede ad ordinare i vari segmenti secondo le convenzioni stabilite sempre dal SO. In Figura 4.3 vediamo che il map file prodotto dal MASM stabilisce che l'applicazione verrà caricata in memoria preferibilmente all'indirizzo lineare 400000h (4 Mib); successivamente troviamo una serie di informazioni relative alle caratteristiche dei vari segmenti di programma, come ad esempio l'indirizzo iniziale, la dimensione in byte, il tipo di segmento, la classe del segmento, etc. Il valore a 16 bit presente nel campo Start (indirizzo iniziale), rappresenta simbolicamente il selettore di quel blocco di programma; oltre ai segmenti appartenenti al nostro programma, notiamo la presenza di altri segmenti appartenenti al SO. Il blocco successivo contiene informazioni relative alla posizione dei vari simboli nei rispettivi segmenti di programma; come possiamo notare, sono presenti anche le informazioni relative ai simboli MessageBoxA e ExitProcess. L'ultima informazione del map file si riferisce all'entry point del nostro programma, rappresentato dall'indirizzo 0001:00000000h; il valore 0001h si riferisce simbolicamente al selettore del blocco codice _TEXT.

4.7 La fase di esecuzione di PRIMO.EXE

Per eseguire PRIMO.EXE possiamo digitare PRIMO dal prompt del DOS premendo successivamente [Invio], oppure possiamo fare doppio click con il pulsante sinistro del mouse sull'icona associata al file eseguibile; a questo punto entra in gioco il loader del SO che esegue tutte le necessarie inizializzazioni. In particolare analizziamo il lavoro svolto dal loader sui registri di segmento della CPU.
Il registro CS rappresenta il selettore del segmento di codice; il campo index punta quindi al descrittore del blocco codice. Il campo TI (table indicator) vale 1 (local descriptor table); il campo RPL (request privilege level) vale 11b (ring 3).
I registri DS, ES, SS vengono inizializzati con lo stesso valore a 16 bit; questo significa che il blocco codice e il blocco stack del nostro programma vengono fusi in un unico blocco il cui descrittore viene puntato dal campo index di questi tre registri. Il registro FS rappresenta il selettore del TIB (Thread Information Block); il TIB è un blocco dati predisposto dal SO e contenente importanti informazioni relative all'applicazione che stiamo eseguendo. In particolare il TIB contiene le informazioni relative ai gestori delle eccezioni per l'applicazione in esecuzione; il campo TI di FS vale 1 (local descriptor table), mentre il campo RPL vale 11b (ring 3).
Il registro GS rappresenta il selettore nullo (null selector) e viene ovviamente inizializzato con il valore 0000h; per maggiori informazioni sul selettore nullo, vedere la sezione Modalità Protetta.
Il registro ESP (Extended Stack Pointer) viene fatto puntare all'indirizzo piu alto del blocco di memoria assegnato dal SO alla nostra applicazione proprio come accade per gli eseguibili in formato COM del DOS; questo non significa che ESP verrà inizializzato con il valore 2147483648 (2 Gib) che è il limite superiore dello spazio di indirizzamento della nostra applicazione. Come è stato detto nel precedente capitolo, il valore iniziale di ESP può essere deciso dal programmatore attraverso il campo STACKSIZE del definition file; si raccomanda di non utilizzare dimensioni iniziali inferiori ai 10 Kib. In assenza del definition file abbiamo visto che i vari linker assegnano per lo stack del programma una dimensione predefinita di circa 1 Mib; in ogni caso, questi dettagli possono variare da linker a linker.
Una volta terminate tutte le inizializzazioni, il SO predispone una nuova macchina virtuale e carica la nostra applicazione in memoria; come già sappiamo, all'interno di questa macchina virtuale la nostra applicazione parte dall'indirizzo lineare 4194304d = 400000h e crede di avere a disposizione uno spazio di indirizzamento complessivo di 4 Gib.

4.8 La procedura wsprintf

Per verificare in pratica le considerazioni appena esposte, scriviamo un apposito programma che rappresenta il secondo esempio di questo capitolo; il programma, che si chiama REGVAL.ASM, mostra in una message box l'indirizzo di memoria da cui parte l'applicazione REGVAL.EXE e il contenuto dei principali registri della CPU, nella figura 4.4 viene mostrato il listato del programma regval.asm. A tale proposito, utilizziamo due nuove procedure chiamate GetModuleHandle e wsprintf; entrambe le procedure maneggiano stringhe, per cui esistono come al solito sia in versione ASCII sia in versione UNICODE. La procedura GetModuleHandle fa parte della libreria KERNEL32.LIB ed è dichiarata nell'API di Windows come:
HMODULE GetModuleHandle(LPCTSTR lpModuleName)
Questa procedura restituisce in EAX l'handle di un modulo EXE o DLL il cui nome (completo di estensione) è contenuto nella stringa C lpModuleName. In Win16, ad ogni applicazione in esecuzione viene assegnato un codice numerico univoco chiamato module handle; se si eseguono più copie (istanze) della stessa applicazione, ad ogni copia viene associato un codice numerico univoco chiamato instance handle. Questi accorgimenti sono necessari in quanto in Win16 le varie applicazioni condividono tutte lo stesso spazio di indirizzamento; attraverso i module handle è possibile distinguere le diverse applicazioni in esecuzione, mentre attraverso gli instance handle è possibile distinguere le varie istanze di una applicazione in esecuzione. Se vogliamo ottenere ad esempio l'handle del modulo REGVAL.EXE, possiamo definire la stringa:
strName db 'REGVAL.EXE', 0
(si possono usare indifferentemente le maiuscole o le minuscole). A questo punto possiamo effettuare la chiamata:
invoke GetModuleHandleA, offset strName
Se REGVAL.EXE è in esecuzione, otteniamo in EAX il suo module handle, in caso contrario la procedura GetModuleHandle restituisce in EAX il valore zero (NULL). Possiamo dire quindi che il valore restituito da GetModuleHandle è di tipo DWORD; infatti in windos.inc è presente la dichiarazione:
HMODULE TYPEDEF DWORD
(se questa dichiarazione non è presente, possiamo sempre inserirla noi).
In ambiente Win32 tutte queste considerazioni perdono significato; ciascuna applicazione Win32 infatti, gira in una macchina virtuale privata e crede di essere l'unica applicazione in esecuzione. Lo stesso discorso vale anche per le istanze multiple della stessa applicazione; se ad esempio si eseguono contemporaneamente quattro istanze della stessa applicazione, ciascuna di esse è inconsapevole della presenza delle altre istanze. Per questo motivo in Win32 non esiste distinzione tra module handle e instance handle; in Win32 la procedura GetModuleHandle restituisce l'indirizzo di memoria da cui parte il modulo specificato nel parametro lpModuleName. Nel caso di moduli come USER32.DLL, KERNEL32.DLL, etc, si ottengono indirizzi compresi tra 2 Gib e 3 Gib; come è stato spiegato in un precedente capitolo, questo è lo spazio di indirizzamento condiviso tra le varie applicazioni Win32. Nel caso di moduli .EXE si ottiene l'indirizzo di memoria da cui parte il modulo stesso all'interno del suo spazio di indirizzamento privato; come è stato detto in precedenza, questo indirizzo è generalmente pari a 00400000h (4 Mib). Se chiamiamo GetModuleHandle con il parametro NULL (puntatore nullo), ci viene restituito l'handle dello stesso modulo che ha effettuato la chiamata; per questo motivo nel programma REGVAL.ASM è presente la chiamata:
invoke GetModuleHandleA, NULL
L'informazione desiderata viene restituita nel registro EAX e viene quindi copiata nella variabile a 32 bit hInstance; questa variabile non ha un valore iniziale per cui viene definita nel segmento dati non inizializzati (_BSS).

A questo punto dobbiamo visualizzare attraverso una message box il contenuto di hInstance, il contenuto dei vari registri di segmento e il contenuto di ESP; per fare questo, dobbiamo inserire tutte queste informazioni in una stringa che verrà poi passata a MessageBox. Questo lavoro viene svolto da una procedura chiamata wsprintf; la procedura wsprintf fa parte della libreria USER32.LIB ed è dichiarata nell'API di Windows come:
int wsprintf(LPTSTR lpOut, LPCTSTR lpFmt, ...);
Il primo parametro lpOut è una stringa destinata a ricevere l'output prodotto da wsprintf; come si può notare, questo parametro è di tipo LPTSTR (puntatore FAR ad una generica stringa di testo) per indicare il fatto che non è necessario lo zero finale. Il secondo parametro lpFmt è una stringa C chiamata stringa di formato, contenente le direttive per la formattazione dell'output; in base a queste direttive wsprintf determina l'output da inviare a lpOut. Al posto del terzo parametro troviamo tre puntini ... che nel linguaggio C indicano il fatto che wsprintf accetta un numero variabile di altri argomenti; la procedura wsprintf quando termina restituisce in EAX un valore intero che (in assenza di errori) rappresenta il numero di caratteri inseriti in lpOut.
Per una descrizione dettagliata della procedura wsprintf si veda il solito Win32 Programmer's Reference; vediamo ora un breve esempio che chiarisce il principio di funzionamento di wsprintf. Supponiamo di voler stampare in esadecimale il contenuto 0F4AB000h del registro EAX; a tale proposito dobbiamo predisporre la stringa di output e la stringa di formato. La stringa di output può essere definita come:
strBuffer db 40 dup (0)
Bisogna prestare particolare attenzione al fatto che questa stringa deve essere in grado di contenere tutto l'output prodotto da wsprintf. La stringa di formato può essere definita come:
strFormat db 'EAX = %.8Xh', 0
Attraverso la stringa di formato, stiamo dicendo a wsprintf di inserire in strBuffer la stringa:
'EAX = '
seguita da un valore esadecimale formato da almeno 8 cifre. Gli eventuali posti vuoti alla sinistra del valore esadecimale devono essere riempiti con degli zeri; subito dopo il valore esadecimale deve essere inserita la lettera h. A questo punto possiamo procedere con la chiamata:
invoke wsprinfA, offset strBuffer, offset strFormat, eax
Come si può notare, il valore da inserire nella stringa di output viene passato in EAX come terzo parametro; è importantissimo che ci sia un perfetto equilibrio tra il numero di direttive inserite nella stringa di formato e il numero di parametri aggiuntivi passati a wsprintf. Se tutto fila liscio, la procedura wsprintf restituisce in strBuffer l'output:
'EAX = 0F4AB000h', 0
Come si può notare, wsprintf ha aggiunto a strBuffer lo zero finale. Se vogliamo visualizzare questa stringa possiamo usare la solita message box con la chiamata:
invoke MessageBoxA, NULL, strBuffer, strTitolo, MB_OK
La procedura wsprintf è una delle rarissime procedure di Win32 che seguono le convenzioni del linguaggio C; in sostanza, i parametri vengono passati a wsprintf a partire dall'ultimo, e lo stack viene ripulito da chi ha chiamato la procedura. Nel caso dell'esempio precedente, l'assembler espande l'istruzione:
invoke wsprinfA, offset strBuffer, offset strFormat, eax
nella sequenza di istruzioni: Come si può notare, subito dopo la chiamata di wsprintf l'assembler ha aggiunto un'istruzione che somma il valore 0Ch = 12d al registro ESP; infatti, prima della chiamata di wsprintf abbiamo inserito nello stack tre parametri da 4 byte ciascuno. Come fa l'assembler a sapere che wsprintf è una procedura C e non STDCALL e che richiede un numero variabile di argomenti? Questa informazione gliela dobbiamo dare noi attraverso il prototipo della procedura; con il MASM bisogna scrivere:
wsprintfA PROTO C :DWORD, :DWORD, :VARARG
Alternativamente, è sempre possibile l'utilizzo della dichiarazione in vecchio stile Assembly:
EXTRN wsprintfA: PROC
In questo modo però non possiamo utilizzare le direttive avanzate INVOKE.

Tutte queste considerazioni vengono applicate nel programma REGVAL.ASM; esaminiamo in particolare la stringa di formato definita come: Come si può notare, grazie alla convenzione C (zero finale) possiamo definire stringhe molto lunghe; questa stringa di formato dice a wsprintf che l'output deve contenere 8 numeri esadecimali, con il primo e l'ultimo formati entrambi da 8 cifre e gli altri 6 formati da 4 cifre. Di conseguenza, la procedura wsprintf si aspetta di trovare 8 parametri aggiuntivi contenenti gli 8 numeri da inviare alla stringa di output; se si verifica una discordanza tra le direttive della stringa di formato e i parametri aggiuntivi, la procedura wsprintf restituisce un codice di errore. I byte di valore 10d presenti in questa stringa rappresentano il codice ASCII che simula la nuova linea della macchina da scrivere (line feed); sia wsprintf che MessageBox interpretano correttamente questo byte. In pratica, quando MessageBox incontra nella stringa un byte che vale 10d, va a capo in modo che l'output della stringa stessa riprenda dall'inizio di una nuova linea; nel linguaggio C questo stesso risultato viene ottenuto inserendo nella stringa il simbolo \n che rappresenta ugualmente il codice ASCII 10d.
Come è stato spiegato in un precedente capitolo, tutti gli operandi di PUSH e POP devono essere a 32 bit; per questo motivo, il contenuto a 16 bit dei registri di segmento viene passato a wsprintf attraverso apposite variabili a 32 bit.
Analizzando la stringa di formato, si può dedurre che l'output prodotto da wsprintf richiede circa 200 byte di memoria; per questo motivo, è necessario predisporre per l'output un buffer di dimensioni adeguate. La stringa destinata a ricevere l'output di wsprintf viene infatti definita come:
strBuffer db 256 dup (0)
In questo modo siamo sicuri che strBuffer riuscirà a contenere tutto l'output prodotto da wsprintf.
Dopo aver proceduto con le fasi di assembling e di linking otteniamo l'applicazione REGVAL.EXE; eseguendo questa applicazione compare una message box che visualizza il contenuto della variabile hInstance, il contenuto dei registri di segmento e il contenuto dello stack pointer ESP.
Alcune di queste informazioni possono variare da computer a computer; un esempio di output prodotto dal programma REGVAL.EXE è il seguente: Come si può notare, il contenuto della variabile hInstance restituitoci da GetModuleHandle vale 00400000h e rappresenta l'indirizzo lineare da cui inizia il nostro programma in memoria; osserviamo anche che DS, ES e SS referenziano lo stesso descrittore di segmento. Lo stack pointer viene inizializzato con il valore 0063FE3Ch,; questo significa che il programma REGVAL.EXE occupa in memoria:
0063FE3Ch - 00400000h = 0023FE3Ch = 2358844 byte
Il programma REGVAL.EXE utilizza le procedure wsprintfA, MessageBoxA, GetModuleHandleA e ExitProcess; le prime due procedure vengono definite nella libreria USER32, mentre le altre due procedure vengono definite nella libreria KERNEL32. Quando REGVAL.EXE chiama una di queste procedure non sta facendo altro che richiedere un servizio al SO; di conseguenza, al momento di eseguire REGVAL.EXE, il SO carica in memoria anche queste due librerie che devono fornire i servizi richiesti dal nostro programma. In sostanza, le librerie di Win32 vengono collegate dinamicamente alle applicazioni che le utilizzano e per questo motivo si parla anche di librerie a collegamento dinamico o DLL; questi concetti sono molto importanti per capire il meccanismo attraverso il quale un'applicazione Win32 si interfaccia al SO.
Come già sappiamo, un programma DOS si interfaccia con il SO (cioè richiede i servizi del SO) attraverso i vettori di interruzione; ad esempio, chiamando l'INT 21h (interrupt dei servizi DOS), un programma può richiedere al DOS svariati servizi come la gestione dei files su disco, la gestione della memoria, etc. In ambiente Win32 la situazione è totalmente diversa; in questo caso il SO carica in memoria le DLL contenenti tutti i servizi richiesti da un'applicazione. Ciascuno di questi servizi, cioè ciascuna procedura come MessageBoxA, ExitProcess, etc, è associata ad un ben preciso codice numerico chiamato ordinale; possiamo dire quindi che ciascuna procedura di Win32 viene identificata attraverso una coppia NOME_DLL:ordinale. La componente NOME_DLL rappresenta il nome della libreria che contiene la procedura associata a ordinale; supponendo ad esempio che MessageBoxA abbia ordinale = 0004h, possiamo dire che questa procedura viene identificata attraverso la coppia USER32:0004h. All'interno del file REGVAL.EXE viene inserita una tabella chiamata Import Table, contenente l'elenco completo delle coppie NOME_DLL:ordinale richieste dall'applicazione; non appena REGVAL.EXE viene caricato in memoria (insieme alle varie DLL), tutte le coppie NOME_DLL:ordinale presenti nella Import Table, vengono convertite in indirizzi relativi alla posizione in memoria delle corrispondenti procedure. Tenendo presente che sia il nostro programma che le varie DLL si trovano all'interno di un unico segmento virtuale da 4 Gib, possiamo dire che le chiamate alle procedure saranno tutte di tipo diretto o indiretto intrasegmento; spesso si parla anche di chiamate NEAR, dove il termine NEAR in questo caso si riferisce ad un indirizzo formato semplicemente da un offset a 32 bit.
Un'ultima considerazione riguarda il fatto che ovviamente anche sotto Win32 i primi 1024 byte della RAM sono riservati ai 256 vettori di interruzione; questi vettori di interruzione puntano a ISR concepite espressamente per la modalità reale. Di conseguenza, si deve evitare nella maniera più assoluta di chiamare queste ISR da un'applicazione per Win32 che gira invece in modalità protetta; se si prova ad eseguire una INT XXh dall'interno di un'applicazione per Win32, si provoca come minimo la terminazione forzata dell'applicazione stessa, ma in molti casi si può anche mandare in crash l'intero SO.

4.9 L'editor QEDITOR.EXE del MASM32

Con le vecchie versioni del MASM vengono largamente utilizzati i makefile; in questo caso, l'interprete dei makefiles si chiama NMAKE.EXE. In MASM32 questo programma non è presente perché al posto dei makefiles vengono utilizzati i batch files già illustrati nella sezione Assembly Base; MASM32 installa una serie di batch files predefiniti che vengono utilizzati da un potente editor fornito in dotazione e chiamato QEDITOR.EXE. Questo editor che si trova nella cartella c:\masm32, permette di gestire dal suo interno tutte le fasi di assembling e di linking; per poterlo sfruttare al massimo, dobbiamo adottare una serie di accorgimenti. La prima cosa da fare consiste nell'inserire il percorso c:\masm32 nel file c:\autoexec.bat che viene eseguito all'avvio del computer; all'interno di questo file è presente una riga del tipo:
SET PATH=C:\WINDOWS;C:\WINDOWS\COMMAND ...
Questa riga permette di specificare una serie di cartelle con "visibilità globale"; alla fine di questa riga dobbiamo aggiungere la cartella C:\MASM32 (le varie cartelle sono separate tra loro da un punto e virgola). Per rendere attiva questa modifica, dobbiamo salvare il file autoexec.bat e riavviare il computer; a questo punto, dal prompt del DOS possiamo eseguire QEDITOR.EXE da qualunque altra cartella.
Il procedimento appena descritto vale solo per Windows 9x; nel caso in cui si disponga di Windows XP, il procedimento è differente in quanto non esiste più il file autoexec.bat. Gli utenti di Windows XP devono allora procedere in questo modo: Un'altro passo importante da compiere consiste nel rendere QEDITOR.EXE l'applicazione predefinita per i files con estensione .ASM, .INC, .MAK, .LST, .MAP, etc; per fare questo basta aprire Esplora Risorse, cliccare con il tasto destro del mouse su un qualunque file avente queste estensioni e selezionare Apri con .... A questo punto compare una finestra che ci permette di scegliere l'applicazione predefinita per questi files; naturalmente dobbiamo scegliere l'applicazione qeditor.exe e il gioco è fatto.
Per la corretta visualizzazione degli esempi proposti nella sezione Assembly Windows 32bit, si consiglia di configurare opportunamente alcune caratteristiche di QEDITOR; in particolare è necessario modificare le opzioni per la tabulazione e per l'indentazione del testo. A tale proposito, bisogna selezionare il menu Tools + Change Editor Settings; in questo modo compare un'apposita finestra contenente l'elenco delle opzioni disponibili. Con un doppio click sull'opzione Set Tab Size compare una finestra per configurare il numero di spazi di tabulazione; inserire il valore 3 e premere OK. Con un doppio click sull'opzione Set Indent Left compare una finestra per configurare il numero di spazi di indentazione verso sinistra; inserire il valore 3 e premere OK. Con un doppio click sull'opzione Set Indent Right compare una finestra per configurare il numero di spazi di indentazione verso destra; inserire il valore 3 e premere OK. A questo punto bisogna premere il bottone Save per salvare la configurazione; per rendere attive le modifiche bisogna chiudere QEDITOR e riavviarlo.

Il procedimento che bisogna seguire per generare un'applicazione Win32 con qeditor.exe è molto semplice; prima di tutto dalla nostra cartella di lavoro:
c:\win32asm
digitiamo qeditor e premiamo [Invio]. Dall'interno dell'editor selezioniamo il menu File + Open e carichiamo il programma Assembly desiderato; selezioniamo quindi il menu Project + Build All. A questo punto partono le fasi di assemblig e di linking che portano alla generazione dell'eseguibile; per poter lanciare questo eseguibile selezioniamo il menu Project + Run Program.
Dopo di che possiamo lanciare la nostra applicazione sia dal DOS che dall'interno dell'editor.
Per ulteriori dettagli su qeditor.exe consultare l'help in linea fornito con il programma.

Prima di chiudere questo capitolo, è importante fare qualche considerazione sulla programmazione in ambiente Win32 con un linguaggio di basso livello come l'Assembly; anche leggendo le poche cose esposte in questo capitolo, ci si rende subito conto dell'enorme potenza che l'Assembly offre ai programmatori. Nel caso ad esempio della procedura MessageBox, abbiamo visto che ci basta agire direttamente sui bit del parametro uType per definire in modo dettagliato tutte le caratteristiche della finestra; queste potenzialità vengono offerte solo dai linguaggi di medio/basso livello come il C e l'Assembly. I linguaggi di programmazione di alto livello, permettono al programmatore di gestire queste situazioni solo attraverso apposite procedure; utilizzare un'apposita procedura per modificare un banale dettaglio di una finestra significa scrivere programmi ingombranti e lenti. Utilizzando l'Assembly invece, il programmatore ha il controllo diretto su ogni singolo bit del programma che sta scrivendo; naturalmente il prezzo da pagare consiste nella maggiore complessità dei programmi scritti in Assembly, ma questa è solo una questione di punti di vista. I moderni linguaggi di alto livello, nati con l'intento di semplificare la vita ai programmatori, sono diventati talmente contorti da risultare più complessi dell'Assembly; non parliamo poi del fatto che oggi l'Assembly appare come l'unico linguaggio capace di sfruttare l'enorme potenza dell'hardware fornito con gli attuali computers.
In sostanza, utilizzando l'Assembly è possibile scrivere programmi compatti, efficienti e veloci che riescono a fare in modo semplicissimo cose che con i linguaggi di alto livello appaiono molto complesse se non impossibili da realizzare; a titolo di curiosità, in Figura 4.5 possiamo vedere l'equivalente di PRIMO.ASM scritto però in linguaggio C. Come possiamo notare, la sintassi usata per chiamare MessageBox è veramente simile a quella utilizzata nella versione Assembly di questo programma; questa è una diretta conseguenza del fatto che il C è un parente stretto dell'Assembly. Se proviamo ora a compilare questo programma, il Microsoft Visual C genera un eseguibile da quasi 50 Kib; l'analogo eseguibile generato dal MASM occupa invece alcuni Kib. Questo aspetto è legato al fatto che il compilatore C al momento di produrre l'eseguibile aggiunge una grande quantità di codice che effettua determinate inizializzazioni; la situazione tende a diventare assurda con altri compilatori che spesso producono eseguibili caratterizzati da dimensioni spropositate e prestazioni scadenti.

Codice sorgente per MASM: Esempi capitolo 4 MASM