Win32 Assembly

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 piu' recenti di MASM e TASM. Per quanto riguarda il MASM faremo riferimento alle versioni 6.x o superiori; per quanto riguarda il TASM faremo riferimento alle versioni 5.x o superiori. Tutte le versioni precedenti di MASM e TASM 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 e' 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 verra' analizzato in un apposito capitolo, viene indicato con la sigla PE (Portable Executable); e' fondamentale quindi procurarsi un linker che supporti questo formato. I linker forniti in dotazione con MASM 6.x e TASM 5.x supportano pienamente il formato PE; alternativamente e' possibile servirsi dei linker forniti in dotazione con i vari compilatori per Win32.

4.1 Convenzioni adottate nella sezione Win32 Assembly.

In tutti gli esempi presentati nella sezione Win32 Assembly, 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.
Analogamente, si suppone che l'utente abbia installato il TASM nella cartella:
C:\TASM;
in questo caso tutti gli include files di TASM si troveranno nella cartella:
C:\TASM\INCLUDE,
mentre le librerie di TASM si troveranno nella cartella:
C:\TASM\LIB.
Come al solito e' importante crearsi una cartella di lavoro dove sistemare i files relativi ai vari esempi; in tutti gli esempi presentati nella sezione Win32 Assembly, si fa riferimento ad una cartella di lavoro chiamata win32asm. Se si utilizza MASM questa cartella deve trovarsi in:
C:\MASM32\WIN32ASM;
se invece si utilizza TASM questa cartella deve trovarsi in:
C:\TASM\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. Prima di tutto scarichiamo i files zippati che contengono tutti gli esempi del capitolo in versione MASM e TASM; questi files zippati devono essere scompattati nella cartella win32asm.
Download di tutti gli esempi del capitolo (versione MASM)
Download di tutti gli esempi del capitolo (versione TASM)
La Figura 1 mostra il primo esempio che utilizzeremo in questo capitolo; si tratta della versione MASM di un programma che viene chiamato PRIMO.ASM.

Figura 1 - PRIMO.ASM
Come si puo' notare, rispetto al template presentato nel precedente capitolo sono state rimosse le direttive INCLUDE; questo procedimento si rende necessario perche' 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 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 piu' avanti.
Analizzando il listato di Figura 1, notiamo che nel blocco dati inizializzati vengono definite due stringhe chiamate strTitolo e strMessaggio; si puo' anche notare che queste due stringhe terminano entrambe con uno zero secondo la convenzione del linguaggio C. Questa e' 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 assumera' 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 puo' notare che questa procedura viene definita nella libreria USER32.LIB ed e' dichiarata come:
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
Per una descrizione dettagliata delle caratteristiche di MessageBox e' necessario consultare il solito Win32 Programmer's Reference; analizziamo invece gli aspetti di MessageBox che interessano noi programmatori Assembly. Chiamando la procedura MessageBox viene visualizzata sullo schermo una finestra predefinita chiamata appunto message box (finestra dei messaggi); nella sua forma piu' 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' puo' essere premuto con il pulsante sinistro del mouse oppure attraverso il tasto [Invio]; il codice associato alla pressione del bottone 'OK' e' rappresentato dal valore 00000001h ed e' 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 e' stato spiegato in un precedente capitolo, normalmente ogni applicazione Windows e' 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 e' 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 1 viene dichiarata anche in windows.inc. In sostanza, il tipo HWND indica un tipo di dato intero a 32 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 e' l'indirizzo di una stringa C chiamata lpText; questa stringa deve contenere il messaggio che verra' 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 compatibilita' 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 gia' sappiamo, in Win32 un qualsiasi indirizzo di memoria e' rappresentato da un offset a 32 bit, e cioe' da un numero intero senza segno che puo' andare da 00000000h a FFFFFFFFh; possiamo dire quindi che dal punto di vista dell'Assembly, il tipo LPCTSTR e' 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 e' anch'esso di tipo LPCTSTR; si tratta quindi dell'indirizzo a 32 bit di un'altra stringa C. Questa stringa verra' 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 e' 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 cosi' 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 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 e' 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 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 verra' 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 puo' 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 e' stato gia' detto, il manuale di riferimento reperibile via Internet e' aggiornato al 1996; nel frattempo l'API di Windows e' cresciuta con l'aggiunta di numerose nuove costanti predefinite, nuove strutture, nuovi prototipi di procedure, etc. Un modo efficace per conoscere tutte queste novita' consiste nell'esplorare il contenuto delle versioni piu' 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 e' 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 1 e' 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 piu' esplicite le caratteristiche dei vari parametri.
Un'ultima considerazione relativa alla MessageBox riguarda il fatto che in Figura 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 puo' 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 e' rappresentato da un codice ASCII a 8 bit; le stringhe in formato UNICODE sono vettori di simboli dove ogni simbolo e' 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 e' stata gia' descritta nel precedente capitolo; in Win32 e' 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 pero' cambia radicalmente nel momento in cui si vuole terminare un programma molto piu' complesso; in casi del genere, un programma che termina senza chiamare ExitProcess "scompare" dallo schermo lasciando pero' vari "pezzi" sparpagliati in memoria. Una situazione del genere si verifica ad esempio quando due o piu' applicazioni stanno condividendo la stessa DLL; se tutte le applicazioni terminano chiamando ExitProcess, permettono al SO di sapere quando e' 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 piu' recente di MASM e' 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 e' necessario che il file PRIMO.ASM si trovi nella cartella di lavoro:
c:\masm32\win32asm;
posizionandoci in questa cartella, dal prompt del DOS dobbiamo digitare:
..\bin\ml /c /coff /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 e' il formato oggetto standard utilizzato da molti strumenti di sviluppo Microsoft in ambiente Windows. Il formato COFF e' incompatibile con il formato OMF (Object Module Format) utilizzato invece dagli strumenti di sviluppo della Borland; proprio per questo motivo, non e' possibile utilizzare le librerie MASM con il TASM. Il parametro /Fl infine dice all'assembler di generare il listing file; in assenza di altre indicazioni da parte dell'utente, questo file verra' chiamato PRIMO.LST.

4.4 La fase di assembling con TASM.

Veniamo ora al caso in cui l'utente voglia utilizzare il TASM; installando il TASM nella cartella:
C:\TASM,
tutti gli strumenti di sviluppo vengono a trovarsi nella cartella:
C:\TASM\BIN.
Partiamo dal caso di TASM versione 5.x o superiore; in questo caso, l'assemblatore fornito dal TASM si chiama TASM32.EXE. Sistemiamo come al solito il file PRIMO.ASM nella cartella di lavoro:
C:\TASM\WIN32ASM.
Posizionandoci in questa cartella, dal prompt del DOS dobbiamo digitare:
..\bin\tasm32 /l 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 /l dice all'assembler di generare il listing file; anche in questo caso, in assenza di diverse indicazioni da parte dell'utente, viene generato il file PRIMO.LST. Non sono necessari altri parametri in quanto stiamo utilizzando apposite direttive inserite direttamente nel listato di Figura 1; in particolare, la direttiva:
OPTION CASEMAP: NONE
equivale al parametro /ml.

A differenza del MASM, il TASM e' a pagamento per cui non e' detto che tutti possano disporre della versione piu' recente; se si ha a disposizione una vecchia versione a 32 bit del TASM e non si vuole passare al MASM, e' ugualmente possibile sviluppare applicazioni per Win32, anche se bisogna sopportare qualche sacrificio in piu'. Il problema e' dato principalmente dal fatto che nelle vecchie versioni del TASM, la direttiva .MODEL non supporta l'opzione STDCALL; non potendo utilizzare questa direttiva, dobbiamo scordarci tutte le comodita' legate all'uso dei modelli di memoria. In parole povere, siamo costretti a riscrivere il nostro programma in puro Assembly; la Figura 2 mostra appunto il nuovo aspetto che viene assunto da PRIMO.ASM.

Figura 2 - PRIMO.ASM (vecchio stile)
Come si puo' notare, nella sezione riservata alle direttive per l'assembler sono state eliminate le direttive .MODEL e OPTION; in assenza del modello di memoria, non possiamo utilizzare nemmeno i prototipi delle procedure. Al posto quindi della direttiva PROTO, dobbiamo usare la classica direttiva EXTRN per informare l'assembler che i simboli MessageBoxA e ExitProcess vengono definiti in un'altro modulo; sempre a causa dell'assenza del modello di memoria, siamo costretti a gestire direttamente l'inserimento dei parametri nello stack. Dobbiamo anche ricordarci che le due procedure chiamate seguono la convenzione STDCALL; infatti, come si vede in Figura 2, i parametri vengono passati a partire dall'ultimo, mentre la pulizia dello stack viene delegata alle procedure stesse. Un'altra caratteristica che si nota nel listato di Figura 2, e' la ricomparsa della direttiva ASSUME; infatti, in assenza del modello di memoria, l'associazione tra segmenti di programma e registri di segmento deve essere specificata dal programmatore.
Per un programma molto piccolo come PRIMO.ASM, queste modifiche possono sembrare abbastanza semplici; nel caso pero' di programmi molto complessi la situazione diventa piuttosto impegnativa. Si tenga presente ad esempio che l'impossibilita' di supportare i prototipi delle procedure, ci costringe a riscrivere tutti gli include files; altri problemi possono derivare dalla presenza nei vari include files di nuove direttive non supportate dalle vecchie versioni del TASM.
Veniamo ora alla fase di assemblaggio del programma di Figura 2 con le vecchie versioni del TASM; se si dispone del TASM versione 4.x o 3.x, l'assembler si chiama TASM.EXE. Dal prompt del DOS si deve digitare:
..\bin\tasm /ml /l primo.asm
questa volta bisogna specificare anche il parametro /ml che richiede un assemblaggio case sensitive.
Se si dispone del TASM 2.x, l'assembler si chiama ugualmente TASM.EXE; dal prompt del DOS si deve digitare:
..\bin\tasm /ml /op /l primo.asm
Anche in questo caso notiamo la presenza del parametro /ml; il paramentro /op permette all'assembler di gestire correttamente i segmenti di programma a 32 bit (attributo USE32); si tenga presente che i programmi per Win32 generati dalle vecchie versioni del MASM o del TASM possono anche non funzionare in ambiente Windows NT, Windows XP.

4.5 Il listing file.

Utilizzando il parametro /Fl con il MASM o /l con il TASM, 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 3 mostra in particolare il listing file prodotto da TASM.

Figura 3 - PRIMO.LST
Come e' stato gia' 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 piu' 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 puo' notare, questa volta gli offset sono a 32 bit. La terza colonna contiene la conversione del nostro programma in codice macchina piu' 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, cioe' a 0 byte di distanza dall'inizio di _DATA; questa stringa occupa 15 byte (Fh) per cui l'assembler a partire dall'offset 00000000h crea un vettore di 15 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 0000000Eh, per cui la stringa successiva (strMessaggio) parte dall' offset 0000000Fh; questa seconda stringa occupa 38 byte (26h), per cui l'assembler a partire dall'offset 0000000Fh crea un vettore di 38 byte che vengono inizializzati con i codici ASCII dei caratteri che formano la stringa stessa. Complessivamente il segmnento _DATA occupa in tutto 53 byte (35h); infatti, come si vede in Figura 3 il blocco _DATA termina all'offset 00000035h.
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 puo' notare, grazie alla presenza della direttiva STDCALL, l'assembler e' 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, e' formato dall'opcode 011010_s_0 seguito dal valore immediato; come gia' sappiamo, il bit indicato con s e' il sign bit che consente all'assembler di produrre un codice macchina molto compatto. Il primo parametro da inserire nello stack e' 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 e' 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 e' 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 puo' 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 puo' notare, per entrambe le chiamate l'assembler utilizza l'opcode E8h che come sappiamo e' relativo all'istruzione CALL direct within segment (chiamata diretta all'interno del segmento). Questo aspetto e' 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 Gb. 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 cio' che e' presente in questo supersegmento (codice, dati, stack del programma, librerie dinamiche, etc) e' 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 puo' 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 e' legato al fatto che la Microsoft non vuole rendere pubblici i dettagli relativi al funzionamento di questa direttiva.

4.6 La fase di linking con MASM.

Dopo aver esaminato il listing file PRIMO.LST, possiamo passare alla fase di linking che ci consentira' 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 puo' vedere in Figura 1, con MASM e TASM 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:\masm32\win32asm
dove deve essere presente l'object file PRIMO.OBJ; dal prompt del DOS dobbiamo digitare:
..\bin\link /subsystem:windows /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 /map dice al linker di generare il map file che in assenza di diverse indicazioni da parte dell'utente, viene chiamato PRIMO.MAP.

4.7 La fase di linking con TASM.

Passiamo ora alla fase di linking con il Borland TASM partendo dal caso in cui l'utente sia in possesso del TASM 5.x o superiore; in questo caso il linker si chiama TLINK32.EXE. Osservando il listato di PRIMO.ASM in versione TASM, possiamo notare che e' presente una sola direttiva INCLUDELIB che specifica al linker la richiesta di inclusione dell'unica libreria IMPORT32.LIB; il linker TLINK32.EXE utilizza esclusivamente questa libreria che riunisce in un unico file le informazioni relative alle varie librerie come USER32.LIB, KERNEL32.LIB, etc.
Prima di tutto posizioniamoci nella cartella di lavoro:
c:\tasm\win32asm
dove deve essere presente l'object file PRIMO.OBJ; dal prompt del DOS dobbiamo digitare:
..\bin\tlink32 /c /Tpe /aa /s /V4.0 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 /c abilita l'opzione case sensitive del linker; il parametro Tpe dice al linker di generare un eseguibile in formato PE. Il parametro /aa dice al linker di generare un'applicazione Windows; il parametro /s dice al linker di generare un Map File dettagliato. Il parametro /V4.0 dice al linker che l'applicazione deve poter girare su Windows versione 4.0 o superiore (cioe' su Windows 95 o superiore); la versione di Windows che si sta utilizzando puo' essere individuata lanciando il programma Microsoft System Information, oppure selezionando Risorse del computer + Pannello di controllo + Sistema (questa informazione viene gestita automaticamente dal linker di MASM 6.x). Se si utilizza un numero di versione troppo alto, come ad esempio /V6.0, al momento di eseguire PRIMO.EXE si ottiene un messaggio di errore di Windows; se si utilizza un numero di versione troppo basso, come ad esempio /V3.0, si ottiene un'applicazione priva degli effetti grafici tridimensionali disponibili in Win32.

Passiamo ora al caso in cui l'utente disponga di una vecchia versione del TASM; in questo caso, il linker in dotazione non e' in grado di generare un eseguibile per Win32. Se non si ha intenzione di passare al MASM, l'unica alternativa consiste nel procurarsi uno dei tanti compilatori per Win32 che la Borland distribuisce gratutitamente via Internet o sui CD allegati alle varie riviste di informatica; questi compilatori, come ad esempio il C/C++ Compiler 5.x o il C++ Builder 3.x, sono dotati di linker piu' potenti e aggiornati di TLINK32.EXE. Nel caso ad esempio del Borland C/C++ Compiler 5.x il linker si chiama ILINK32.EXE (Incremental Linker) e viene installato generalmente nella cartella c:\borland\bcc55\bin; se vogliamo generare PRIMO.EXE con questo linker, dopo esserci posizionati nella solita cartella di lavoro dobbiamo digitare:
c:\borland\bcc55\bin\ilink32 /c /Tpe /aa /s /V4.0 primo.obj
(utilizzando questo linker e' possibile specificare sia l'inclusione della sola libreria generale IMPORT32.LIB, sia l'inclusione delle varie librerie USER32.LIB, KERNEL32.LIB, etc). Si tenga presente che le librerie fornite in dotazione con questi compilatori sono estremamente aggiornate; in particolare, la libreria IMPORT32.LIB e' molto piu' aggiornata dell'analoga libreria fornita con il TASM 5.x.

4.8 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, mentre nel caso di TASM si utilizza l'opzione /s (se non vogliamo che il linker del TASM generi il map file dobbiamo utilizzare l'opzione /x). In assenza di diverse indicazioni da parte del programmatore, viene generato un map file chiamato PRIMO.MAP; la Figura 4 mostra il map file generato dal MASM.

Figura 4 - PRIMO.MAP
Con l'ausilio del map file possiamo analizzare il lavoro svolto dal linker; come gia' 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 vediamo che il map file prodotto dal MASM stabilisce che l'applicazione verra' caricata in memoria preferibilmente all'indirizzo lineare 400000h (4 Mb); 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.9 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 campo TI vale 1 (local descriptor table); il campo RPL vale 11b (ring 3).
Il registro FS rappresenta il selettore del TIB (Thread Information Block); il TIB e' 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 Modalita' 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 verra' inizializzato con il valore 2147483648 (2 Gb) che e' il limite superiore dello spazio di indirizzamento della nostra applicazione. Come e' stato detto nel precedente capitolo, il valore iniziale di ESP puo' essere deciso dal programmatore attraverso il campo STACKSIZE del definition file; si raccomanda di non utilizzare dimensioni iniziali inferiori ai 10 Kb. In assenza del definition file abbiamo visto che i vari linker assegnano per lo stack del programma una dimensione predefinita di circa 1 Mb; 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 gia' 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 Gb.

4.10 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. 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 e' 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) e' contenuto nella stringa C lpModuleName. In Win16, ad ogni applicazione in esecuzione viene assegnato un codice numerico univoco chiamato module handle; se si eseguono piu' 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 e' possibile distinguere le diverse applicazioni in esecuzione, mentre attraverso gli instance handle e' 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:
call GetModuleHandleA, offset strName
Se REGVAL.EXE e' 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 e' di tipo DWORD; infatti in windos.inc e' presente la dichiarazione:
HMODULE TYPEDEF DWORD
(se questa dichiarazione non e' 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 e' 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 Gb e 3 Gb; come e' stato spiegato in un precedente capitolo, questo e' 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 e' stato detto in precedenza, questo indirizzo e' generalmente pari a 00400000h (4 Mb). 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 e' presente la chiamata:
call 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 verra' poi passata a MessageBox. Questo lavoro viene svolto da una procedura chiamata wsprintf; la procedura wsprintf fa parte della libreria USER32.LIB ed e' dichiarata nell'API di Windows come:
int wsprintf(LPTSTR lpOut, LPCTSTR lpFmt, ...);
Il primo parametro lpOut e' una stringa destinata a ricevere l'output prodotto da wsprintf; come si puo' notare, questo parametro e' di tipo LPTSTR (puntatore FAR ad una generica stringa di testo) per indicare il fatto che non e' necessario lo zero finale. Il secondo parametro lpFmt e' 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 esadeciamle il contenuto 0F4AB000h del registro EAX; a tale proposito dobbiamo predisporre la stringa di output e la stringa di formato. La stringa di output puo' 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 puo' 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:
call wsprinfA, offset strBuffer, offset strFormat, eax
Come si puo' notare, il valore da inserire nella stringa di output viene passato in EAX come terzo parametro; e' 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 puo' notare, wsprintf ha aggiunto a strBuffer lo zero finale. Se vogliamo visualizzare questa stringa possiamo usare la solita message box con la chiamata:
call MessageBoxA, NULL, strBuffer, strTitolo, MB_OK.
La procedura wsprintf e' 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:
call wsprinfA, offset strBuffer, offset strFormat, eax
nella sequenza di istruzioni:
push     eax
push     offset strFormat
push     offset strBuffer
call     wsprintfA
add      esp, 0Ch
Come si puo' notare, subito dopo la chiamata di wsprintf l'assembler ha aggiuunto 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 e' 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 TASM bisogna scrivere:
wsprintfA PROTO C :DWORD, :DWORD :?
mentre con il MASM bisogna scrivere:
wsprintfA PROTO C :DWORD, :DWORD, :VARARG
Alternativamente, e' sempre possibile l'utilizzo della dichiarazione in vecchio stile Assembly:
EXTRN wsprintfA: PROC
In questo modo pero' non possiamo utilizzare le direttive avanzate CALL e INVOKE.

Tutte queste considerazioni vengono applicate nel programma REGVAL.ASM; esaminiamo in particolare la stringa di formato definita come:
strFormat      db    'Indirizzo di partenza del modulo REGVAL.EXE = %.8Xh', 10
               db    'Contenuto dei registri di segmento:', 10
               db    'CS = %.4Xh', 10
               db    'DS = %.4Xh', 10
               db    'ES = %.4Xh', 10
               db    'SS = %.4Xh', 10
               db    'FS = %.4Xh', 10
               db    'GS = %.4Xh', 10
               db    'Contenuto iniziale di ESP = %.8Xh', 0
Come si puo' 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 e' 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 puo' dedurre che l'output prodotto da wsprintf richiede circa 200 byte di memoria; per questo motivo, e' 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 riuscira' 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 e' il seguente:
Indirizzo di partenza del modulo REGVAL.EXE = 00400000h
Contenuto dei registri di segmento:
CS = 0167h
DS = 016Fh
ES = 016Fh
SS = 016Fh
FS = 139Fh
GS = 0000h
Contenuto iniziale di ESP = 0063FE3Ch
Come si puo' 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 gia' sappiamo, un programma DOS si interfaccia con il SO (cioe' richiede i servizi del SO) attraverso i vettori di interruzione; ad esempio, chiamando l'INT 21h (interrupt dei servizi DOS), un programma puo' richiedere al DOS svariati servizi come la gestione dei files su disco, la gestione della memoria, etc. In ambiente Win32 la situazione e' totalmente diversa; in questo caso il SO carica in memoria le DLL contenenti tutti i servizi richiesti da un'applicazione. Ciascuno di questi servizi, cioe' ciascuna procedura come MessageBoxA, ExitProcess, etc, e' 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 Gb, 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 modalita' reale. Di conseguenza, si deve evitare nella maniera piu' assoluta di chiamere queste ISR da un'applicazione per Win32 che gira invece in modalita' 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 puo' anche mandare in crash l'intero SO.

4.11 I makefile.

La fase di assembling e di linking di un'applicazione Assembly puo' essere automatizzata con l'ausilio di appositi strumenti; uno strumento largamente utilizzato con il TASM e' il cosiddetto makefile. I makefile sono dei files in formato ASCII che contengono una serie di istruzioni scritte con una sorta di linguaggio di programmazione; il makefile viene passato ad un apposito programma chiamato MAKE.EXE, che si trova come al solito nella cartella:
c:\tasm\bin
Questo programma interpreta ed esegue le istruzioni del makefile e ci consente di ottenere l'eseguibile finale. La Figura 5 mostra il makefile relativo al programma PRIMO.ASM; convenzionalmente il makefile ha lo stesso nome del programma seguito dall'estensione MAK.

Figura 5 - PRIMO.MAK
Analizziamo il contenuto di questo makefile; il carattere '#' equivale al punto e virgola dell'Assembly e delimita quindi l'inizio di un commento che termina non appena si va a capo. All'interno del makefile possiamo definire delle vere e proprie macro che ci permettono di specificare ad esempio gli strumenti di sviluppo utilizzati, i parametri da passare a questi strumenti, etc; ad esempio, la macro:
ASM = ..\bin\tasm32
indica il nome dell'assembler che vogliamo utilizzare. Analogamente, la macro:
ASMOPT = /ml /zn /l
indica le opzioni da passare all'assembler; queste macro verranno espanse ed interpretate dal programma MAKE.EXE. Vediamo allora come si gestiscono le fasi di assembling e di linking con l'ausilio dei makefile; nel caso di PRIMO.MAK, posizionandoci nella solita cartella di lavoro, dobbiamo digitare dal prompt del DOS:
..\bin\make /B /fprimo.mak
oppure:
..\bin\make -B -fprimo.mak
A questo punto, premendo il tasto [Invio], entra in azione MAKE.EXE che inizia la fase di espansione e di interpretazione delle varie macro; il parametro /B obbliga MAKE.EXE ad eseguire il makefile anche se PRIMO.OBJ e PRIMO.EXE sono gia' presenti, il parametro /f indica a MAKE.EXE il nome del makefile da eseguire. Ogni volta che MAKE.EXE incontra una macro, la espande e cerca di interpretarla; ad esempio, la macro:
$(ASM) $(ASMOPT) $(NAME).ASM
viene espansa in:
..\bin\tasm32 /ml /zn /l primo.asm
Questa linea viene poi eseguita in modo da ottenere l'object file PRIMO.OBJ; come si puo' notare in Figura 5, le fasi di assembling e di linking devono essere disposte nel makefile in ordine inverso. Tutti gli esempi in versione TASM presentati nella sezione Win32 Assembly, vengono forniti con il relativo makefile.

4.12 L'editor QEDITOR.EXE del MASM32.

Anche 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 e' presente perche' al posto dei makefiles vengono utilizzati i batch files gia' 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 e' presente una riga del tipo:
SET PATH=C:\WINDOWS;C:\WINDOWS\COMMAND ...
Questa riga permette di specificare una serie di cartelle con "visibilita' 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 e' differente in quanto non esiste piu' il file autoexec.bat. Gli utenti di Windows XP devono allora procedere in questo modo:

1 Apripre la finestra Pannello di controllo
2 Selezionare la voce Prestazioni e manutenzione
3 Selezionare la voce Sistema
4 Nella finestra Proprieta' di sistema selezionare Avanzate
5 Premere il pulsante Variabili d'ambiente
6 Evidenziare la riga che inizia con Path e premere il pulsante Modifica
7 Alla fine della riga aggiungere ;c:\masm32 (ricordarsi del punto e virgola)
8 Premere il pulsante Ok per chiudere le finestre aperte in precedenza.

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 e' fatto.
Per la corretta visualizzazione degli esempi proposti nella sezione Win32 Assembly, si consiglia di configurare opportunamente alcune caratteristiche di QEDITOR; in particolare e' 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 disponbili. 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 e' molto semplice; prima di tutto dalla nostra cartella di lavoro:
c:\masm32\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.
Naturalmente possiamo utilizzare qeditor.exe anche per scrivere i programmi Assembly in versione TASM; in questo caso, dopo esserci posizionati nella cartella:
c:\tasm\win32asm
dobbiamo lanciare come al solito l'editor digitando qeditor e premendo [Invio]. Una volta che il codice sorgente del nostro programma Assembly e' pronto, dobbiamo uscire al prompt del DOS attraverso il menu File + Command Prompt, oppure cliccando sull'apposita icona della Tool Bar (barra degli strumenti); dal prompt del DOS dobbiamo eseguire MAKE.EXE dopo di che possiamo lanciare la nostra applicazione sia dal DOS che dall'interno dell'edtor.
Per ulteriori dettagli su qeditor.exe consultare l'help in linea fornito con il programma.

Prima di chiudere questo capitolo, e' 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 potenzialita' 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 complessita' dei programmi scritti in Assembly, ma questa e' 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 piu' 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 e' 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 curiosita', in Figura 6 possiamo vedere l'equivalente di PRIMO.ASM scritto pero' in linguaggio C.
Figura 6 - PRIMO.C
// file primo.c

#include < windows.h >

#pragma argsused

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdParam, int nCmdShow)
{
   MessageBox(NULL, 
              "Il primo programma Assembly per Win32",
              "Win32 Assembly", 
              MB_OK | MB_ICONINFORMATION);
   return 0;
}
Come possiamo notare, la sintassi usata per chiamare MessageBox e' veramente simile a quella utilizzata nella versione Assembly di questo programma; questa e' una diretta conseguenza del fatto che il C e' un parente stretto dell'Assembly. Se proviamo ora a compilare questo programma, il Borland C/C++ Compiler 5.5 genera un eseguibile da quasi 50 Kb; l'analogo eseguibile generato dal TASM o dal MASM occupa invece alcuni Kb. Questo aspetto e' legato al fatto che il compilatore C al momento di produrre l'eseguibile aggiunge una grande quantita' 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.