Sei sulla pagina 1di 138

Win32 Assembly

Capitolo 1: Strumenti necessari per la


sezione Win32 Assembly
Nella sezione Win32 Assembly vengono illustrate le tecniche generali
che si utilizzano per lo sviluppo dei programmi Assembly destinati a
"girare" in ambiente Windows; il termine Win32 indica tutte le
versioni a 32 bit del sistema operativo Windows, come Windows 95,
Windows 98, Windows ME, Windows NT, Windows 2000, Windows XP (che
esiste anche in versione a 64 bit).
Naturalmente si dà per scontato il fatto che chi legge questa sezione
abbia una buona conoscenza generale della programmazione in Assembly;
in particolare, è importante avere una certa dimestichezza con il set
di istruzioni a 32 bit e soprattutto con gli indirizzamenti a 32 bit.
Bisogna osservare inoltre che il sistema operativo Win32 sfrutta la
modalità protetta delle CPU 80386 e superiori; è importante quindi
avere anche una conoscenza generale dei concetti che stanno alla base
di questa modalità operativa delle CPU. Ricordiamo le principali
novità che caratterizzano la modalità operativa protetta a 32 bit:

* I segmenti di programma possono avere l'attributo SIZE di tipo


USE32.
* Gli indirizzamenti predefiniti sono di tipo NEAR a 32 bit.
* È possibile utilizzare qualsiasi registro generale a 32 bit come
registro puntatore.
* Lo stack viene gestito dalla CPU attraverso ESP, e dal
programmatore attraverso EBP.
* La dimensione predefinita per gli operandi immediati è di 32 bit.

Ulteriori dettagli vengono esposti nel capitolo successivo.


In ogni caso, per scrivere programmi Assembly in ambiente Win32 non è
necessario destreggiarsi tra descrittori di segmento, selettori, task
switching, etc; tutti questi aspetti vengono infatti gestiti dal
sistema operativo e non dal programmatore. È chiaro però che se si è
interessati agli aspetti più nascosti di Windows è necessaria una
conoscenza piuttosto approfondita della programmazione in modalità
protetta e dei concetti che stanno alla base della progettazione dei
sistemi operativi (da ora in poi chiamati SO); le sezioni Assembly
Base, Assembly Avanzato e Modalità Protetta rappresentano una buona
base di partenza per chi fosse interessato a questi argomenti.

1.1 Strumenti di sviluppo

Per poter sviluppare programmi Windows in Assembly è necessario


dotarsi di una serie di adeguati strumenti; la dotazione minima
comprende:

1
* un editor di testo
* un assembler a 32 bit
* un linker a 32 bit
* un compilatore di risorse
* un editor di icone
* il Windows SDK

L'editor di testo ci serve per creare, modificare, salvare su disco o


leggere dal disco i files contenenti il codice sorgente dei nostri
programmi Assembly; come al solito, questi files devono essere
rigorosamente in formato ASCII perché questo è ciò che viene
richiesto dall' assembler. Per motivi di chiarezza e di stile, si
consiglia di rispettare tutte le convenzioni per le estensioni dei
files contenenti programmi Assembly; i files contenenti il codice
sorgente dovrebbero utilizzare l'estensione ASM, i files contenenti
direttive, dichiarazioni, prototipi di procedure, etc, dovrebbero
utilizzare l'estensione INC, i files contenenti macro dovrebbero
utilizzare l'estensione MAC, e così via.

L' assembler ha il compito di convertire il codice sorgente del


nostro programma in codice oggetto; come al solito, se il codice
sorgente è distribuito su più files, verranno generati altrettanti
files con estensione OBJ contenenti il codice macchina del nostro
programma. È chiaro che per poter sviluppare programmi per Win32 è
necessario un assembler in grado di supportare il set di istruzioni a
32 bit; se poi si vogliono utilizzare istruzioni disponibili solo in
modalità protetta (come LLDT), è necessario un assembler che accetti
direttive del tipo .386p (set di istruzioni a 32 bit in modalità
protetta).

Il linker ha il compito di collegare tra loro i vari moduli oggetto e


di generare il programma finale in formato eseguibile (estensione
EXE); il linker deve essere capace di generare eseguibili a 32 bit
compatibili con Windows. Infatti, il formato EXE utilizzato da
Windows è differente dall'analogo formato utilizzato in ambiente DOS;
la struttura degli eseguibili per Windows viene analizzata in un
apposito capitolo della sezione Win32 Assembly.

Il compilatore di risorse è uno strumento di sviluppo tipico


dell'ambiente Windows; questo particolare compilatore opera sui
cosiddetti Resource Files (files di risorse) di Windows. Come si
intuisce dal nome, un file di risorse è destinato a contenere una
serie di risorse che verranno utilizzate da una applicazione Windows;
tra le risorse più conosciute possiamo citare i menu, le icone, le
immagini in formato bitmap, etc. I files di risorse utilizzano
l'estensione predefinita RC e anche in questo caso devono essere in
formato ASCII; il compilatore di risorse compila il FILE.RC e lo
converte in un FILE.RES che è compatibile con il formato oggetto
(OBJ). A questo punto il FILE.RES viene passato al linker che
provvede a collegarlo ai vari moduli OBJ per poter ottenere il
programma finale in versione EXE.

L'editor di icone è un altro strumento di sviluppo tipico dei SO o


ambienti operativi che utilizzano interfacce grafiche; uno degli
2
oggetti più caratteristici delle interfacce grafiche è rappresentato
proprio dall'icona che è una sorta di bitmap formata in genere da 16
x 16 o 32 x 32 pixel. Nell'interfaccia grafica di Windows, di Linux,
di OS/2, etc, ad ogni programma viene associata un'icona chiamata
icona del programma; l'editor di icone ci permette proprio di creare
o modificare icone, e di salvarle su disco nel tipico formato di
immagine ICO utilizzato da Windows. A rigore bisogna dire che
l'editor di icone non è uno strumento indispensabile; infatti, se il
programmatore non specifica una icona personalizzata, Windows
provvederà ad assegnare al programma una icona predefinita. Su
Internet sono reperibili numerosi editor di icone, dai più semplici
ai più sofisticati; per trovarli ci si può servire di un motore di
ricerca e utilizzare un criterio di ricerca del tipo windows AND
iconedit.

Il Windows SDK rappresenta uno strumento senza il quale non sarebbe


possibile sviluppare programmi per Windows; la sigla SDK sta per
Software Development Kit e cioè kit software per lo sviluppo dei
programmi Windows. L'SDK rappresenta una collezione di librerie
contenenti tutti gli strumenti che Windows mette a disposizione dei
programmatori per la gestione dei files, gestione della memoria,
gestione della grafica, multimedia, telecomunicazioni, etc; l'SDK
occupa su disco diversi Mb, ed è importante procurarsi la versione
più aggiornata possibile per poter usufruire di tutte le nuove
funzionalità messe a disposizione dalle versioni più recenti di
Windows. L'SDK di Windows è scritto principalmente in linguaggio C
standard (ANSI C), mentre le parti più critiche, strettamente legate
alla piattaforma hardware utilizzata, sono state scritte guarda caso
in Assembly; queste considerazioni ci fanno capire che i programmi
Windows scritti in C o Assembly risultano più semplici, compatti e
veloci degli analoghi programmi scritti con qualunque altro
linguaggio di programmazione.

1.2 Assembler disponibili

L' assembler che offre il supporto più completo per lo sviluppo di


applicazioni Windows è sicuramente il Microsoft Macro Assembler
(MASM); l'aspetto più interessante è dato dal fatto che la Microsoft
da un po’ di tempo ha deciso di distribuirlo "gratis" via Internet
(con la solita chilometrica licenza d'uso che tutti evitano di
leggere premendo direttamente il pulsante [Accept]). Grazie ad un
rapporto di collaborazione tra la Microsoft e un gruppo di
programmatori, è nato il progetto MASM32 che ha lo scopo di
promuovere l'uso dell'Assembly per lo sviluppo di applicazioni Win32;
MASM32 fornisce ai programmatori interessati un vasto insieme di
strumenti di sviluppo che comprende tra l'altro: l' assembler, il
linker, il compilatore di risorse, l'SDK, un ottimo editor di testo
(Quick Editor), etc. Nella sezione Links Utili è presente un link al
sito ufficiale del progetto MASM32; accedendo a questo sito è
possibile scaricare MASM32 (circa 4 Mb) insieme a diversi esempi di
applicazioni Windows scritte in Assembly. Tra l'altro, lo stesso
MASM32 contiene una notevole quantità di eccezionali esempi pratici
scritti dagli stessi programmatori della Microsoft; nella sezione
Downloads, premendo il pulsante [Info] si ottengono le informazioni
3
necessarie per installare MASM32.

Un altro assembler professionale che permette di sviluppare


applicazioni Windows è il Borland Turbo Assembler (TASM) che a
differenza del MASM è però a pagamento; si tratta comunque di un
assembler nettamente più affidabile del MASM, tanto che la stragrande
maggioranza dei programmatori Assembly lo considera il miglior
assembler disponibile nel mondo dei PC. Del resto la Borland è una
software house resa celebre da mitici compilatori e interpreti come
il Turbo Basic, il Turbo Pascal, il C/C++, Delphi, etc, che si
differenziano dagli analoghi prodotti Microsoft per la quasi totale
assenza di bugs; si può dire che grazie alla Borland, milioni di
programmatori in tutto il mondo hanno appreso l'arte della
programmazione.
La versione più aggiornata del TASM è la 5.0 che mette a disposizione
l’ assembler a 32 bit (TASM32.EXE), il linker a 32 bit (TLINK32.EXE)
in grado di generare eseguibili per Windows, il compilatore di
risorse e numerosi altri strumenti. Per i fortunati possessori del
TASM, si presenta un problema legato al fatto che l'SDK fornito con
la versione 5.0 dell’ assembler risale all'era paleozoica di Windows
95; si tratta quindi di un SDK molto vecchio e pressoché
inutilizzabile. Un vero programmatore Assembly però non può certo
fermarsi davanti a questi ostacoli; vediamo allora come può essere
risolto il problema in modo da poter utilizzare non solo TASM 5.0 ma
persino le versioni precedenti. La prima idea che viene in mente può
essere quella di installare MASM32 per utilizzare poi il relativo SDK
con TASM; questa idea però non funziona in quanto le librerie usate
dal MASM contengono codice oggetto in formato COFF, mentre il TASM e
tutti i vari compilatori Borland, accettano solo codice oggetto in
formato standard OMF (vedere la sezione Downloads per la
documentazione ufficiale su questi due formati). La strada che
dobbiamo seguire consiste allora nel procurarci la versione più
recente possibile dell'SDK che la Borland distribuisce insieme ai
suoi compilatori; questa opportunità ci viene offerta dal fatto che
da tempo la Borland distribuisce gratuitamente le versioni a linea di
comando dei suoi potentissimi compilatori C/C++ come il Borland C/C++
compiler 5.x e il Borland C++ Builder 5.x (o versioni superiori). Si
possono seguire due strade in quanto questi prodotti sono disponibili
sia sui CD allegati alle varie riviste di programmazione, sia via
Internet nel sito della Borland (vedere la sezione Links Utili); in
entrambi i casi, bisogna registrarsi gratuitamente on-line. Una volta
installato il compilatore C/C++ abbiamo finalmente a disposizione
l'SDK, che generalmente viene sistemato in una sottocartella chiamata
Lib; se lo si desidera si può anche copiare l'SDK nella cartella dove
abbiamo installato il TASM (creando un'analoga sottocartella Lib).
Osserviamo che il compilatore C/C++ ci mette a disposizione anche il
linker e il compilatore di risorse; tutti questi strumenti possono
essere utilizzati al posto degli analoghi strumenti forniti con il
Turbo Assembler. A questo punto siamo "quasi" pronti per utilizzare
TASM nello sviluppo di applicazioni Win32; l'ultimo passo da compiere
viene descritto nel seguito del capitolo.

Tra i vari assembler che si possono utilizzare per lo sviluppo di


applicazioni Windows, una menzione speciale spetta al prodotto
4
freeware NASM; la versione a 32 bit di NASM supporta il formato
oggetto COFF e può essere quindi usata in combinazione con l'SDK
fornito con MASM32. Chi volesse usare NASM per sviluppare
applicazioni Windows, può reperire tutta la documentazione necessaria
via Internet; tutti gli esempi proposti nella sezione Win32 Assembly
si riferiscono esclusivamente a MASM e TASM.

1.3 Gli include files

Dopo aver installato MASM32 sul disco fisso (generalmente nella


cartella masm32), si nota la presenza di due sottocartelle chiamate
Lib e Include; la sottocartella Lib contiene ovviamente la collezione
di librerie dell'SDK, mentre la sottocartella Include contiene gli
include files. Come già sappiamo, gli include files dell'Assembly
equivalgono agli header files del C/C++ e sono destinati a contenere
prototipi di procedure, dichiarazioni di strutture, di union, di
costanti simboliche, etc; in questo caso tutti questi prototipi e
dichiarazioni servono per poter interfacciare i nostri programmi
Assembly con l'SDK di Windows.
Nel caso del TASM, dobbiamo tener presente che stiamo utilizzando
l'SDK fornito in dotazione ai compilatori C/C++ della Borland; nella
sottocartella Include del compilatore troveremo quindi non gli
include files ma gli header files necessari per interfacciare i
programmi C/C++ con l'SDK. Fortunatamente, le versioni più recenti
del TASM, supportano la sintassi avanzata imposta dalla Microsoft per
le dichiarazioni presenti negli include files del MASM32 (vedere il
Capitolo 28 della sezione Assembly Base); questo significa che nella
gran parte dei casi, gli include files di MASM32 possono essere
utilizzati con il TASM. Possiamo allora copiare gli include files di
MASM32 in una apposita sottocartella Include di TASM. Purtroppo, come
abbiamo visto nella sezione Assembly Base, esistono delle differenze
tra MASM e TASM per quanto riguarda in particolare la visibilità dei
membri di strutture e union; tanto per citare un esempio, in MASM il
nome di un membro di una struttura è locale alla struttura stessa e
può essere quindi riutilizzato per identificare un membro di un'altra
struttura, un parametro di una procedura, una variabile locale, etc.
In TASM invece il nome di un membro di una struttura è visibile anche
all'esterno della struttura stessa e non può quindi essere
ridefinito; in caso contrario TASM genera un errore. Nei vari esempi
che vengono presentati nella sezione Win32 Assembly, vengono
illustrati i rimedi che bisogna adottare per eliminare queste
situazioni di errore.

1.4 L'include file Windows.inc

Tra tutti gli include files, il più importante di tutti è sicuramente


Windows.inc che equivale all'header file Windows.h usato dai
programmi C/C++ per Windows e viene chiamato include file principale;
il file Windows.inc contiene al suo interno una autentica marea di
dichiarazioni di procedure, di strutture, di union, di costanti, di
tipi di dati (typedef), etc. Queste dichiarazioni sono indispensabili
per poter sviluppare qualunque applicazione per Windows; se si
osserva il codice sorgente del modulo principale di un programma per

5
Windows, si nota che all'inizio è sempre presente una direttiva del
tipo:
INCLUDE ..\Include\Windows.inc
All'interno di Windows.inc si possono trovare ad esempio
dichiarazioni del tipo:
UINT TYPEDEF DWORD
Questa dichiarazione crea un nuovo tipo di dato UINT che rappresenta
il tipo intero senza segno a 32 bit. È importantissimo che il
programmatore dia un'occhiata a questo include file per farsi un'idea
precisa del suo contenuto; si tenga presente infatti che in fase di
sviluppo dei programmi Assembly per Windows, la consultazione di
Windows.inc diventa una pratica frequentissima.
Il file Windows.inc relativo a MASM32 viene tenuto costantemente
aggiornato da diversi programmatori; nella sezione Downloads è
possibile scaricare l'aggiornamento più recente curato da Iczelion.
Purtroppo questo file è inutilizzabile con TASM a causa delle
incompatibilità descritte in precedenza; per questo motivo, chi
utilizza TASM è tenuto a scaricare un apposito include file
Windows.inc. Nella sezione Downloads è presente oltre ad un generico
file Windows.inc per TASM, anche un analogo file Win32.inc; il file
Win32.inc viene costantemente aggiornato da Ra.M. Software.

1.5 Documentazione

Per poter utilizzare l'SDK il programmatore deve conoscere i nomi con


i quali vengono chiamate le funzioni, procedure, macro, strutture,
union, costanti simboliche, etc, presenti nell'SDK stesso; tutte
queste dichiarazioni utilizzano la classica sintassi del linguaggio
C, e nel loro insieme formano la cosiddetta Windows API. La sigla API
sta per Application Programming Interface (interfaccia per la
programmazione delle applicazioni) e indica appunto l'interfaccia che
bisogna utilizzare per accedere agli strumenti dell'SDK; si tratta di
centinaia e centinaia di nomi che un programmatore non può certo
ricordare a memoria. È molto importante quindi procurarsi un manuale
di riferimento sull'API di Windows, che deve essere il più possibile
aggiornato; fortunatamente grazie a Internet è possibile scaricare un
file generalmente chiamato win32.zip e contenente un documento
chiamato Win32 Programmer's Reference in formato HLP (Help File di
Windows). Questo file, una volta decompresso occupa su disco oltre 24
Mb, ma nonostante le dimensioni consistenti, contiene una versione
dell'API aggiornata al 1996; in ogni caso si tratta di un documento
che si rivela di estrema utilità per il programmatore. La
documentazione completa dell'API di Windows occupa diversi CD-ROM ed
è ottenibile a pagamento dalla Microsoft; in alternativa, la versione
completa ed aggiornata dell'API è consultabile gratuitamente via
Internet nel sito della stessa Microsoft. Nella sezione Links Utili è
presente un link al sito web di MrCrimson che oltre al file win32.zip
contiene anche una eccezionale collezione di documenti sull'API di
Windows sempre in formato HLP.

Nota importante
Si tenga presente che diversi siti web citati nella sezione Links
Utili vengono gestiti da programmatori e appassionati; la
sopravvivenza di tali siti (e quindi anche di questo) è legata spesso
6
alle decisioni di chi li ospita o di chi li gestisce. È del tutto
normale quindi che questi siti possano scomparire improvvisamente per
poi riapparire magari ad un altro indirizzo Internet; nei limiti del
possibile la sezione Links Utili viene costantemente aggiornata in
modo da garantire il corretto funzionamento dei vari links.

7
Win32 Assembly

Capitolo 2: Introduzione
Nell'estate del 1981 inizia l'era dei Personal Computers con
l'entrata in commercio del mitico PC IBM XT 8086; si tratta di un
computer basato sulla CPU Intel 8086 con architettura a 16 bit. Come
sappiamo, questa CPU è dotata di registri a 16 bit, Data Bus a 16 bit
e Address Bus a 20 bit; con un Address Bus a 20 bit questa CPU è in
grado di gestire sino a 220 byte di memoria RAM (1 Mb), cioè tutti gli
indirizzi fisici che vanno da 0 sino a 1048575. Con i suoi registri a
16 bit però, l'8086 è in grado di gestire solo numeri interi compresi
tra 0 e 65535; per permettere all'8086 di accedere a tutto il Mb di
RAM, si rappresentano gli indirizzi fisici sotto forma di indirizzi
logici. Prima di tutto, il Mb di RAM del computer viene suddiviso in
blocchi da 65536 byte ciascuno (64 Kb) chiamati segmenti; un
qualunque indirizzo fisico del computer viene allora rappresentato
sotto forma di indirizzo logico costituito da una coppia seg:offset.
La componente seg di questa coppia è un numero a 16 bit che indica il
numero di segmento della RAM a cui vogliamo accedere (0, 1, 2, 3,
..., 65535); la componente offset di questa coppia è un numero a 16
bit che indica uno spiazzamento all'interno di seg (0, 1, 2, 3, ...,
65535). Si tratta dello stesso sistema che usa un postino per
suddividere la città in vie (seg) e numeri civici (offset); in questo
modo il postino può distinguere tra il n.40 di via Pascoli e il n.40
di via Carducci. Per poter indirizzare esattamente 1048576 byte di
RAM, si è deciso di far partire i vari segmenti da indirizzi fisici
multipli di 16 byte (paragrafi); in questo modo il segmento n.0 parte
dall'indirizzo fisico esadecimale 00000h, il segmento n.1 parte da
00010h, il segmento n.2 parte da 00020h e così via sino all'ultimo
segmento n.65535 che parte da FFFF0h. In pratica è come avere 65536
segmenti da 16 byte ciascuno per un totale di:
65536 x 16 = 1048576 byte
Grazie a questo accorgimento, la CPU converte facilmente un indirizzo
logico in un indirizzo fisico. La formuletta che consente alla CPU di
convertire un indirizzo logico seg:offset in un indirizzo fisico a 20
bit è:
(seg x 16) + offset
Questo modo di accedere alla RAM prende il nome di modalità di
indirizzamento reale 8086. Il termine "reale" si riferisce al fatto
che gli indirizzi logici specificati dal programmatore corrispondono
ad indirizzi fisici realmente esistenti in memoria; per parecchio
tempo la modalità reale è stata un vero e proprio incubo per i
programmatori costretti a spezzettare i loro programmi in tanti
blocchi da 64 Kb ciascuno.
L'IBM XT 8086 è un computer destinato non solo alle università o ai
grandi centri di ricerca, ma anche ad un mercato più vasto formato
soprattutto dalle piccole e medie aziende; l'obiettivo fondamentale
8
quindi è quello di diffondere l'uso del computer anche tra un
pubblico privo di conoscenze di informatica. Per consentire a
chiunque di utilizzare un computer senza conoscerne il funzionamento
interno, la IBM equipaggia l'XT 8086 con un apposito Sistema
Operativo (SO); il SO è un software che ha lo scopo di creare
un'interfaccia tra l'utente e il computer. L'utente dialoga con il
computer impartendogli dei comandi formati da termini come DIR,
MKDIR, COPY, DEL, etc; il SO riceve questi comandi, li traduce nel
linguaggio binario del computer e li fa eseguire dall'hardware. Un
altro compito fondamentale di un SO è quello di far girare i
programmi; in questo modo gli utenti possono anche scrivere o
acquistare i programmi più adatti alle loro esigenze, facendoli
eseguire dal computer.
La scelta dell'IBM cade sul SO proposto da Bill Gates e Paul Allen
che poco tempo prima avevano creato una piccola software house
chiamata Microsoft; il SO di Gates e Allen viene chiamato MS-DOS
(Microsoft Disk Operating System). Questo nome deriva dal fatto che
all'epoca, la configurazione standard dell'XT 8086 era formata dal PC
con 1 Mb di RAM, scheda video in modalità testo (alfanumerica) e
memoria permanente formata da un lettore di floppy disk da 5"1/4, o
al massimo "ben" due lettori di floppy disk; gli hard disk da pochi
Mb rappresentavano ancora un lusso che solo pochi fortunati potevano
permettersi. Il DOS organizza il disco come se fosse un armadio da
ufficio suddiviso in tanti ripiani; ogni ripiano rappresenta una
cartella (directory). Le varie cartelle contengono al loro interno
svariati documenti ciascuno dei quali viene chiamato file (archivio);
in questo modo l'utente può organizzare il proprio lavoro come se
avesse a che fare con l'archivio di un ufficio.
Dal punto di vista dei programmi (e dei programmatori), il DOS
organizza la RAM suddividendola in due blocchi principali; il primo
blocco occupa i primi 640 Kb e rappresenta la memoria convenzionale
(Conventional Memory), mentre i restanti 384 Kb formano la memoria
superiore (Upper memory). La memoria superiore è riservata al SO che
la utilizza in particolare per accedere alle memorie periferiche;
bisogna ricordare infatti che l'8086 con un Address Bus a 20 linee
può vedere solo 1 Mb di RAM e quindi qualsiasi altra memoria
periferica deve essere mappata in questo stesso Mb (I/O Memory
Mapped). Il DOS crea nella memoria superiore diverse "finestre"
(Frame Windows) attraverso le quali può accedere ad altre memorie
esterne come la memoria video (VRAM), la ROM-BIOS, etc; a causa dei
registri a 16 bit della CPU 8086, queste finestre non possono essere
più grandi di 64 Kb, e questo significa che il problema della
segmentazione della memoria si ripercuote anche sulle memorie
esterne. Per lasciare più spazio ai normali programmi, la memoria
superiore viene anche utilizzata dai Device Drivers (piloti di
dispositivo); i piloti di dispositivo sono dei programmi residenti in
memoria che permettono al DOS di pilotare diverse periferiche
collegate al computer. Per quanto riguarda la memoria convenzionale,
i primi Kb sono occupati dai vettori di interruzione e dall'area dati
del BIOS; subito dopo troviamo alcune decine di Kb riservati alla
parte del DOS residente in memoria. In definitiva, per i normali
programmi rimangono a disposizione circa 600 Kb di memoria
convenzionale; questo fatto spinge Bill Gates a pronunciare una delle
sue frasi più celebri: "640 Kb rappresentano una quantità enorme di
9
memoria, più che sufficiente per qualunque applicazione presente e
futura!"
Uno degli aspetti più scomodi del DOS è rappresentato dalla sua
interfaccia in modalità testo; dopo aver acceso il computer l'utente
si trova davanti uno schermo nero con un cursore bianco lampeggiante
che viene chiamato prompt del DOS. Lo schermo viene trattato dal DOS
come se fosse il foglio di una macchina da scrivere, o per meglio
dire, il foglio di una telescrivente (l'antenata della stampante); il
prompt rappresenta la testina della telescrivente che può muoversi in
orizzontale e andare a capo grazie ad una sorta di carrello virtuale.
Non a caso, il tasto [Invio] (o [Enter]) viene chiamato "ritorno
carrello" perché premendolo si manda a capo il prompt posizionandolo
all'inizio di una nuova linea; questo tipo di interazione tra utente
e computer è piuttosto grossolano ed ha anche il difetto di
costringere l'utente stesso a leggere grossi manuali per poter
imparare tutti i comandi del DOS. Questo problema riguarda
praticamente tutti i SO dell'epoca anche perché l'hardware
disponibile non consente ancora l'applicazione di una soluzione
alternativa ideata ben dieci anni prima; questa soluzione alternativa
si chiama Interfaccia Grafica Utente o GUI (Graphic User Interface).
Nei primi anni 70 un gruppo di ricercatori dei laboratori Xerox di
Palo Alto (California) mette a punto un progetto destinato a
sconvolgere completamente il mondo dei computers; secondo questo
progetto l'utente interagisce con il computer attraverso uno schermo
grafico che mostra tutta una serie di oggetti tra i quali figurano
finestre, menu di scelta, pulsanti, icone, immagini, etc. L'oggetto
più importante di una GUI è sicuramente la finestra (window) che
rappresenta una sorta di "schermo virtuale"; un SO capace di far
girare più programmi contemporaneamente, associa una finestra a
ciascun programma in modo che ogni programma abbia a disposizione il
suo schermo virtuale attraverso il quale dialogare con l'utente. I
ricercatori della Xerox mettono a punto anche un dispositivo di
puntamento corrispondente all'attuale mouse, che permette all'utente
di muoversi agevolmente tra gli oggetti dello schermo; tutti questi
concetti oggi possono apparire abbastanza ovvi, ma negli anni 70
vengono accolti come una vera e propria rivoluzione.
La prima applicazione pratica di queste idee che ottiene un enorme
successo commerciale è la GUI che viene fornita con i celebri Apple
Macintosh; anche la Microsoft comincia a studiare una propria GUI e
nel 1985 mette in commercio Windows 1.0. Si tratta non di un SO ma di
una semplice GUI che si appoggia sul DOS; l'utente infatti avvia
Windows dal DOS digitando WIN.COM e premendo [Invio] come se avesse a
che fare con un qualunque altro programma DOS. In sostanza l'unico
miglioramento apportato da Windows 1.0 è rappresentato dall'utilizzo
di una interfaccia grafica che consente all'utente di dialogare con
il computer in modo molto più semplice ed intuitivo; per il resto
Windows 1.0 mantiene tutti i pregi e i difetti del DOS legati anche
alla modalità di indirizzamento reale 8086.
L'irruzione della grafica nel mondo dei PC mette in risalto non solo
le scarse prestazioni delle CPU 8086 ma anche l'assoluta
insufficienza dell'unico Mb di memoria disponibile; la caratteristica
fondamentale degli oggetti grafici come le icone, i menu e
soprattutto le immagini, è proprio quella di richiedere notevoli
quantità di memoria. Per cercare di risolvere questi problemi,
10
cominciano a comparire sul mercato costosissime schede hardware
chiamate espansioni di memoria; grazie a queste schede anche le CPU a
16 bit possono disporre di ben 32 Mb di memoria oltre al solito Mb di
RAM; per poter accedere a questa memoria esterna chiamata memoria
espansa, la CPU utilizza la solita tecnica dell'I/O memory mapped. In
sostanza, viene creata nella RAM una frame window da 64 Kb che può
essere fatta "puntare" dai programmi alla zona desiderata della
memoria espansa; anche in questo caso quindi si nota che i programmi
possono vedere al massimo 64 Kb di memoria espansa alla volta. Per
mettere ordine nel mondo delle schede di espansione di memoria, nasce
un consorzio di aziende formato da Lotus, Intel e Microsoft (LIM); il
lavoro svolto dal consorzio LIM porta alla nascita dello standard EMS
(Expanded Memory Specifications). Lo standard EMS definisce
l'interfaccia di programmazione (API) attraverso la quale i programmi
possono accedere alla memoria espansa; per maggiori dettagli sullo
standard EMS si veda la sezione Assembly Avanzato.
Nel 1987 la Microsoft annuncia l'uscita di Windows 2.0 che oltre a
presentare diversi miglioramenti estetici rispetto a Windows 1.0,
offre anche il supporto completo della memoria espansa; in questo
modo, le applicazioni per Windows 2.0 possono beneficiare di una
notevole quantità di memoria rispetto all'unico Mb di RAM sfruttabile
con Windows 1.0. Ogni applicazione riceve da Windows 2.0 un'area di
memoria espansa riservata in esclusiva all'applicazione stessa e
protetta quindi da tentativi di "intrusione" da parte di altre
applicazioni; pur permanendo il problema della segmentazione a 64 Kb
della memoria, bisogna dire che l'avvento della memoria espansa
rappresenta un notevole passo avanti per il mondo dei PC.
La memoria espansa risolve parzialmente il problema della penuria di
memoria, ma non può certo offrire nessun miglioramento sul piano
delle prestazioni del PC; per migliorare le prestazioni del PC è
necessario disporre di CPU molto più potenti dell'8086 e di un metodo
per superare il problema della segmentazione a 64 Kb della memoria.
Consapevole di questo fatto la Intel scatena un autentico uragano sul
mondo dei PC producendo in rapida sequenza tre nuove CPU chiamate
80186, 80286 e 80386; l'80186 passa quasi inosservata in quanto si
tratta di una versione più sofisticata dell'8086. La vera novità
arriva invece con l'80286 che è una CPU dotata di registri a 16 bit,
Data Bus a 16 bit e soprattutto Address Bus a 24 bit; con un Address
Bus a 24 bit l'80286 può gestire sino a 224 byte di memoria RAM
fisica, pari a 16777216 byte (16 Mb). L'aspetto importante è dato dal
fatto che non si tratta di memoria periferica distinta dal solito Mb
di memoria base, ma di un vero e proprio prolungamento della memoria
RAM che viene chiamato memoria estesa; questo significa che una CPU
80286 accede a questi 16 Mb specificando l'indirizzo lineare a 24 bit
0, 1, 2, ..., sino a 16777215. L'obiettivo fondamentale della Intel è
quello di mantenere la compatibilità con l'enorme quantità di
software scritta per le CPU 8086; per questo motivo, appena si
accende il computer l'80286 viene inizializzata in modalità di
emulazione dell'8086. Questa modalità viene ottenuta disabilitando la
linea A20 dell'Address Bus; in questo modo si impedisce ai programmi
di accedere agli indirizzi fisici superiori a FFFFFh, proprio come
accade con l'8086. In alternativa alla modalità reale la CPU 80286
dispone anche di una nuova modalità operativa chiamata modalità
protetta; questo nome deriva dal fatto che in questa modalità l'80286
11
permette ad un SO di far girare "contemporaneamente" più programmi
con meccanismi di protezione che impediscono ai programmi stessi di
interferire tra loro. In modalità protetta i programmi hanno la
possibilità di sfruttare tutti i 16 Mb di memoria fisica
indirizzabile dalla CPU superando in questo modo il limite dei 640 Kb
imposto dalla modalità reale; questo obiettivo viene raggiunto grazie
ad un uso differente dei registri di segmento.

In modalità protetta i registri di segmento vengono chiamati


selettori e il loro contenuto assume la struttura mostrata in Figura
1. I 13 bit più significativi del registro di segmento contengono un
numero compreso tra 0 e 8191. Questo numero rappresenta un indice che
seleziona uno tra gli 8192 possibili elementi presenti in una tabella
chiamata descriptor table (tavola dei descrittori); ciascun elemento
di questa tabella viene chiamato appunto descrittore e contiene la
descrizione completa del segmento di programma puntato dal registro
di segmento. Questa descrizione comprende il tipo di segmento
(scrivibile, leggibile, eseguibile), le dimensioni del segmento
(segment limit) e soprattutto l'indirizzo di memoria da cui parte il
segmento stesso (base address); nel caso delle CPU 80286 questo
indirizzo è formato da 24 bit. Ogni volta che la CPU deve accedere ad
un offset all'interno di un segmento di programma, somma quest'offset
al base address del segmento stesso per ottenere l'indirizzo lineare
a 24 bit; se la CPU incontra ad esempio il codice macchina di una
istruzione del tipo:
mov ax, [bx]
somma l'offset contenuto in BX al base address del segmento di
programma puntato da DS ottenendo un indirizzo fisico a 24 bit; a
questo punto la CPU accede a quell'indirizzo, legge 16 bit di dati e
li trasferisce in AX. Come si può facilmente intuire, l'80286 con i
suoi registri a 16 bit permette di specificare offset compresi tra 0
e 65535; questo significa che neanche questa CPU riesce a risolvere
il problema della segmentazione a 64 Kb della memoria. In pratica,
quando si opera in modalità protetta, la CPU 80286 permette di
scavalcare la barriera dei 640 Kb e di accedere ad un massimo di 16
Mb di RAM; questi 16 Mb vengono però suddivisi in tanti blocchi da 64
Kb analogamente a quanto succede con l'8086.
Un'altra importante caratteristica dell'80286 è il supporto per la
cosiddetta memoria virtuale; in pratica, anche se il computer non
dispone fisicamente di tutti i 16 Mb di RAM, l'80286 permette
ugualmente di simulare la memoria sfruttando l'hard disk. In questo
modo è possibile eseguire programmi che richiedono più memoria di
quella fisicamente disponibile; il trucco consiste nello scaricare
sull'hard disk quei segmenti di programma che in un dato momento non
sono necessari per l'esecuzione del programma stesso. Per sapere se
un segmento di programma si trova in memoria o sull'hard disk si
utilizza un bit del descrittore di segmento; questo bit viene
indicato con P e prende il nome di present bit (bit di presenza). Se
P=1, il corrispondente segmento di programma è in memoria; se invece
P=0 allora il corrispondente segmento di programma è sull'hard disk.
Se si prova ad accedere ad un segmento di programma che ha P=0, la
12
CPU genera un errore (eccezione di segmento non presente); il SO
intercetta questo errore, scarica sull'hard disk un segmento di
programma non più necessario e carica in memoria il segmento di
programma richiesto dal precedente tentativo di accesso.
Tutte queste caratteristiche della CPU 80286 sono state concepite
espressamente per favorire la realizzazione di SO capaci di far
girare più programmi contemporaneamente; i SO di questo tipo vengono
definiti multitask per indicare la capacità di eseguire più compiti
(task) alla volta. Naturalmente con un'unica CPU a disposizione è
possibile eseguire un solo programma alla volta; per ottenere il
multitasking si utilizza un noto espediente chiamato time sharing
(ripartizione del tempo). In sostanza, il SO fa girare uno alla volta
tutti i programmi assegnando a ciascuno di essi un piccolo intervallo
di tempo di esecuzione; in questo modo l'utente ha l'impressione che
i vari programmi stiano girando contemporaneamente.
I SO multitasking sfruttano ampiamente gli altri due campi presenti
nel selettore di segmento descritto in Figura 1; il bit in posizione
2 del selettore viene indicato con TI e rappresenta il Table
Indicator (indicatore del tipo di tabella). Se TI=0 allora il
selettore punta ad una Global Descriptor Table (GDT); se invece TI=1
allora il selettore punta ad una Local Descriptor Table (LDT).
Generalmente il SO riserva per se stesso la GDT ed assegna ad ogni
programma in esecuzione una propria LDT; queste tabelle vengono
inizializzate dal SO e caricate in memoria con le apposite istruzioni
LGDT (Load GDT) e LLDT (Load LDT) disponibili solo in modalità
protetta.
I due bit in posizione 0 e 1 nel selettore di Figura 1, vengono
indicati con RPL e rappresentano il Request Privilege Level (livello
di privilegio); l'RPL indica il livello di privilegio

assegnato al segmento di programma puntato dal campo indice del


selettore. In questo modo, il SO può implementare un meccanismo che
protegge determinati segmenti di programma dai tentativi di
intrusione da parte di altri segmenti di programma meno privilegiati;
infatti, se da un segmento di programma si prova ad accedere ad un
altro segmento di programma più privilegiato, la CPU genera un errore
(violazione di privilegio). Con i due bit del campo TI possiamo
formare i quattro numeri 0, 1, 2 e 3; per convenzione, il livello 0
rappresenta il privilegio più alto (ring 0) mentre il livello 3
rappresenta il privilegio più basso (ring 3). Per questo motivo i SO
riservano per se stessi i livelli di privilegio più alti e assegnano
ai normali programmi i livelli di privilegio più bassi; in questo
modo si evita che un normale programma possa danneggiare il SO. La
Figura 2 mostra un esempio sulla ripartizione dei livelli di
privilegio in un SO; la parte più privilegiata è quella colorata in
rosso che rappresenta il kernel. Il termine kernel deriva dal tedesco
e significa 'nucleo’ per indicare la parte più interna del SO che
13
gestisce direttamente l'hardware del computer; si tratta chiaramente
della componente più delicata di un SO e deve quindi godere del
massimo livello di protezione dalle interferenze esterne. Il livello
di privilegio 1 viene in genere assegnato ai servizi di sistema; si
tratta dell'insieme di servizi (gestione files, gestione memoria,
etc) che i SO mettono a disposizione dei normali programmi. Il
livello di privilegio 2 viene in genere assegnato alle estensioni del
SO; si tratta di programmi che pur non facendo parte del SO ne
estendono le capacità fornendo ulteriori servizi per le normali
applicazioni. Un esempio pratico è rappresentato dai device drivers
attraverso i quali i SO possono pilotare periferiche collegate al
computer; il livello di privilegio più basso (3) viene naturalmente
assegnato ai normali programmi utilizzati dall'utente.
In base alle considerazioni appena svolte, si deduce che l'80286 è in
grado di indirizzare sino a 1 Gb di memoria virtuale; osserviamo
infatti che con i 13 bit del campo indice del selettore di segmento
possiamo rappresentare 213=8192 descrittori di segmento differenti.
Attraverso il campo TI del selettore possiamo specificare se i
descrittori si trovano nella GDT o nella LDT; con queste due tabelle
possiamo gestire in totale:
2 x 8192 = 16384
descrittori differenti, e siccome ogni segmento di programma è grande
65536 byte, otteniamo una memoria virtuale totale pari a:
16384 x 65536 = 1073741824 byte (1 Gb).

Per superare anche il problema della segmentazione a 64 Kb della


memoria è stato necessario attendere l'arrivo della CPU 80386; si
tratta di una CPU dotata di registri a 32 bit, Data Bus a 32 bit e
Address Bus a 32 bit. Con un Address Bus a 32 bit l'80386 può
indirizzare sino a 232 byte di memoria fisica (4 Gb); questa CPU
estende e potenzia la modalità protetta dell'80286 ed apre veramente
una nuova era per i PC. In modalità protetta il base address di un
descrittore di segmento dell'80386 è un indirizzo a 32 bit, ma la
cosa più importante è che anche i registri dell'80386 sono a 32 bit;
con un registro a 32 bit possiamo specificare un offset che può
assumere tutti i valori compresi tra 0 e 4294967295. Tutto ciò
significa che anche l'80386 gestisce la memoria in modo segmentato,
con la differenza però che con questa CPU i segmenti sono
virtualmente da ben 4 Gb ciascuno! Un'altra caratteristica che
testimonia l'abisso che esiste tra l'80286 e l'80386 è che l'80386
sfruttando l'hard disk e i segmenti da 232 byte è in grado di gestire
sino a:
2 x 213 x 232 = 246
byte di memoria virtuale pari a 64 Tb (tetra byte)!
La memoria virtuale viene gestita anche attraverso un sofisticato
meccanismo chiamato paginazione della memoria; in pratica lo spazio
di indirizzamento virtuale dell'80386 viene suddiviso in tanti
blocchi da 4 Kb chiamati pagine di memoria. Così come accade per i
segmenti virtuali dell'80286, anche le pagine dell'80386 possono
trovarsi sull'hard disk; attraverso il bit di presenza (P bit) il SO
può scambiare pagine in memoria con pagine sull'hard disk.
Come accade per tutte le CPU Intel e compatibili, anche l'80386
appena si accende il computer viene inizializzata in modalità reale;
in questo modo viene garantita la compatibilità con tutto il software
14
scritto per le CPU precedenti. Proprio per quanto riguarda la
compatibilità con i vecchi programmi, l'80386 offre una
caratteristica veramente rivoluzionaria rappresentata dalla
cosiddetta modalità virtuale 8086 (V86) che permette a questa CPU di
eseguire programmi DOS senza uscire dalla modalità protetta; nel caso
dell'80286, se ci troviamo in modalità protetta e vogliamo eseguire
un programma DOS, dobbiamo prima tornare in modalità reale. La
modalità V86 dell'80386 crea una macchina virtuale 8086 costituita da
un'area di memoria virtuale da 1 Mb; un programma DOS gira in questo
Mb virtuale credendo di trovarsi in un tipico ambiente DOS. Il Mb
virtuale creato dalla modalità V86 può trovarsi in qualunque area
della memoria; l'aspetto straordinario della modalità V86 è che un SO
multitask può creare più macchine virtuali facendo girare più
programmi DOS contemporaneamente.

Per poter sfruttare tutta la potenza delle nuove CPU la Microsoft nel
1990 annuncia l'uscita di Windows 3.0; oltre ai soliti miglioramenti
estetici, questa nuova versione di Windows mette a disposizione
dell'utente ben tre differenti modalità operative chiamate modo
reale, modo standard e modo avanzato. Nel modo reale Windows funziona
come i suoi predecessori comportandosi come una normale GUI che si
appoggia sul DOS; in questa modalità quindi è possibile eseguire un
programma alla volta (in modalità reale) ed è possibile indirizzare
solo 1 Mb di memoria RAM fisica più l'eventuale memoria espansa.
Il modo standard equivale alla modalità protetta dell'80286; in
questa modalità Windows prende il controllo del computer e si
comporta come se fosse un vero SO mentre in realtà non lo è. Grazie
alla modalità protetta dell'80286, Windows è in grado di indirizzare
16 Mb di memoria fisica e 1 Gb di memoria virtuale; in questo modo
diventa possibile far girare più programmi contemporaneamente come in
un vero SO multitask. Il problema principale del modo standard è
legato al fatto che se tra i vari programmi in esecuzione ci sono
anche programmi DOS, allora Windows deve continuamente commutare tra
la modalità reale e la modalità protetta con notevole calo delle
prestazioni; inoltre se un programma DOS va in crash, manda in crash
tutta la sessione Windows facendoci perdere tutto il lavoro che
stavamo svolgendo e che, come al solito, ci eravamo dimenticati di
salvare.
Il modo avanzato equivale alla modalità protetta dell'80386; in
questo caso la situazione migliora enormemente grazie alle
sofisticate caratteristiche di questa CPU. Nel modo avanzato Windows
è in grado di indirizzare 4 Gb di memoria fisica e 64 Tb di memoria
virtuale; grazie anche alla tecnica della paginazione della memoria
il multitasking diventa molto più affidabile perché può beneficiare
di meccanismi di protezione più severi. La modalità V86 inoltre,
permette l'esecuzione di più programmi DOS contemporaneamente
eliminando la necessità di commutazioni continue tra modalità reale e
modalità protetta; in questo modo un programma DOS che va in crash
viene chiuso da Windows senza danneggiare gli altri programmi in
esecuzione.

Tutte queste considerazioni sono vere solo da un punto di vista


teorico; gli utenti e soprattutto i programmatori che hanno
conosciuto Windows 3.x lo considerano come una sorta di entità aliena
15
scesa sulla terra per impadronirsi dei nostri computers e spargere il
terrore. In effetti le vecchie versioni di Windows sono diventate
tristemente famose per i continui blocchi di sistema annunciati dalla
classica schermata blu; i motivi di tutta questa instabilità sono
numerosi. La prima cosa da osservare è che tutte le versioni di
Windows dalla 1.x alla 3.x vengono definite con il termine Win16 per
indicare il fatto che si tratta di "ambienti operativi" a 16 bit;
questo aspetto è legato alla continua ossessione della Microsoft di
voler garantire a tutti i costi la compatibilità con il vecchio
software e con il vecchio hardware. Basti pensare al fatto che
Windows 3.x potrebbe girare persino su un 8086 dotato di un solo
lettore di floppy disk; la conseguenza di tutto ciò è che all'interno
di Win16 è presente una autentica bolgia infernale fatta di programmi
DOS, programmi Windows, modalità reale, modalità protetta e chissà
cos'altro. Un altro difetto di Win16 è rappresentato dal meccanismo
rudimentale che viene utilizzato per il multitasking; questo
meccanismo viene chiamato multitasking cooperativo per indicare il
fatto che Win16 si affida al "buon comportamento" dei vari programmi
in esecuzione. In sostanza Win16 fa girare a turno i vari programmi
sperando che ciascuno di essi gli restituisca il controllo nel più
breve tempo possibile; se tutti i programmi si comportano
"educatamente", allora questo meccanismo funziona discretamente bene.
Se uno dei programmi impiega un quarto d'ora prima di restituire il
controllo a Win16, allora tutto il sistema rimane bloccato per un
quarto d'ora; come molti ricorderanno, in un caso del genere l'unica
cosa da fare consisteva nel riavviare il computer perdendo come al
solito tutto il lavoro che non era stato ancora salvato.

Tra il 1990 e il 1995 la situazione in casa Microsoft comincia


decisamente ad evolversi nella direzione dei SO a 32 bit; il primo
passo consiste nell'arrivo di Windows NT che è un SO destinato
all'utenza professionale e al mondo dei server. In termini di qualità
la situazione non sembra però migliorare; in breve tempo Windows NT
balza al primo posto nella poco invidiabile classifica dei SO più
colpiti dai virus. Per quanto riguarda il mondo di Win16, la
Microsoft inizia la distribuzione di uno strumento di sviluppo
software chiamato win32s; attraverso questa libreria è possibile
iniziare a sviluppare applicazioni a 32 bit destinate a girare nella
modalità avanzata di Windows 3.x. L'obiettivo della Microsoft è
chiaramente quello di dare inizio alla migrazione degli utenti e dei
programmatori verso il mondo dei SO a 32 bit; verso la fine del 1995
infatti, esce sul mercato Windows 95 che viene presentato dalla
Microsoft come un vero SO a 32 bit destinato al grande pubblico.
Passano pochi mesi e alcuni hackers dimostrano in modo inconfutabile
che in realtà Windows 95 continua ad essere una GUI che si appoggia
sul DOS; infatti Windows 95 installa sul disco una sorta di versione
a 32 bit del DOS che rappresenta il vero SO su cui si appoggia
l'interfaccia grafica. La conseguenza pratica è che anche Windows 95
in breve tempo diventa tristemente famoso per i continui crash; a
tale proposito appare molto eloquente il giudizio di Matt Pietrek
(probabilmente il massimo esperto mondiale di programmazione in
ambiente Windows) che vedendo in azione Windows 95 afferma: "Windows
95 è un' ottimo Sistema Operativo per giocare a Solitario!"
Successivamente la situazione subisce un certo miglioramento con
16
l'arrivo di Windows 98 e Windows ME; in questi casi si può parlare di
veri SO decentemente stabili anche se dietro le quinte continua a
farsi notare la presenza del DOS.
Passando a questioni più tecniche, la prima cosa da dire è che tutte
queste versioni di Windows vengono classificate con il termine Win32
per indicare il fatto che si tratta di SO a 32 bit; in realtà per la
solita compatibilità verso il basso, al loro interno continuano a
convivere (con molte difficoltà) porzioni di codice a 16 e a 32 bit.
Con l'arrivo di Win32 scompaiono sia il modo reale che il modo
standard; questo significa che Win32 può funzionare solo in modo
avanzato e quindi richiede un PC dotato di CPU 80386 o superiore.
Tutto ciò non può che garantire una maggiore stabilità del SO; un
altro miglioramento apportato da Win32 è rappresentato dal meccanismo
usato per il multitasking. Questa volta si tratta di un multitasking
non cooperativo che non fà affidamento sulla "buona educazione" dei
vari programmi in esecuzione; in sostanza Win32 cede il controllo ad
un programma e dopo un breve periodo di tempo glielo toglie per
passarlo ad un altro programma. Anche questo aspetto produce un
notevole aumento della stabilità e dell'efficienza del SO; infatti un
programma che perde molto tempo non provoca nessun rallentamento del
sistema.
Uno degli obiettivi fondamentali di Win32 consiste nell'eliminare un
grave problema presente in Win16; in Win16 i vari programmi in
esecuzione possono vedersi l'uno con l'altro e per risparmiare
memoria condividono tra loro molte risorse. Questa situazione crea
due gravi conseguenze; la prima conseguenza è legata al fatto che i
vari programmi potendosi vedere tra loro, possono anche danneggiarsi
l'uno con l'altro compromettendo così il meccanismo di protezione. La
seconda conseguenza è legata al fatto che la condivisione delle
risorse crea il fenomeno degli orfani; con questo termine si indicano
quelle risorse che rimangono in memoria anche quando non vengono più
utilizzate. Questo fenomeno può verificarsi sia quando un programma
termina dimenticandosi di rimuovere le risorse caricate in memoria,
sia quando un programma va in crash lasciando la memoria piena di
"rifiuti". A causa della confusione che regna al suo interno, Win16
non è assolutamente in grado di individuare il programma che non ha
ripulito la memoria; anche se individuasse il "responsabile", Win16
non potrebbe fare niente perché le risorse rimaste in memoria
potrebbero essere condivise con altri programmi ancora in esecuzione.
Tra le varie risorse si possono citare in particolare le DLL (Dynamic
Link Libraries); le DLL sono librerie software che invece di essere
collegate al programma in fase di linking, vengono collegate in fase
di esecuzione rendendo così più "snello" il programma stesso. Se due
programmi devono utilizzare la stessa DLL, invece di caricare in
memoria due copie della DLL ne viene caricata una sola da condividere
tra i due programmi; Win16 utilizza un contatore per sapere quanti
programmi stanno utilizzando la stessa DLL. Se uno dei programmi
termina, il contatore viene decrementato di 1; quando il contatore

17
diventa 0, Win16 scarica la DLL dalla memoria. Non ci vuole molto a
capire che se uno dei programmi va in crash, il contatore non viene
aggiornato e la DLL rimane in memoria anche quando non viene più
utilizzata; una conseguenza pratica di tutti questi problemi è che in
Win16 si verifica
spesso il fenomeno
della
saturazione della
memoria.

Per cercare di
eliminare tutti
questi aspetti negativi di Win16, Win32 si basa sul fatto che, al
giorno d'oggi, i PC non hanno certo
problemi di penuria di RAM; non sussiste più quindi la necessità di
risparmiare memoria attraverso la condivisione delle risorse.
Eliminando la condivisione delle risorse si elimina anche la
necessità di consentire alle varie applicazioni in esecuzione di
potersi vedere l'una con l'altra; in questo modo si garantisce un
meccanismo di protezione molto più efficiente in quanto in Win32 ogni
applicazione ha il suo spazio di indirizzamento privato nel quale
vengono caricate anche le risorse necessarie. La Figura 3 mostra la
"realtà virtuale" che Win32 crea per ogni programma in esecuzione;
questo significa che indipendentemente dalla configurazione fisica
del computer, una applicazione in esecuzione sotto Win32 crede di
vedere la situazione di Figura 3. Come si può notare, i primi 4 Mb
vengono riservati al codice per il supporto dei programmi DOS; ogni
programma DOS gira all'interno di una macchina virtuale V86. Il DOS è
un SO monotask che consente cioè di eseguire un solo programma alla
volta; i programmi DOS hanno quindi tutto il computer a loro completa
disposizione e sono liberi di accedere direttamente all'hardware del
computer senza il rischio di entrare in conflitto con altri
programmi. Tutto ciò non avrebbe nessun senso in un SO multitask dove
girano contemporaneamente più programmi che devono condividere lo
stesso hardware; uno dei compiti fondamentali della macchina virtuale
V86 è proprio quello di intercettare i tentativi di accesso diretto
all'hardware da parte dei programmi DOS. Il SO riceve queste
richieste e le esaudisce al momento opportuno in modo da non creare
conflitti tra i vari programmi in esecuzione; tutti questi aspetti
vengono quindi gestiti con l'ausilio del codice di supporto per i
programmi DOS.
Subito dopo questo blocco da 4 Mb troviamo una vasta area di memoria
virtuale riservata a ciascuna applicazione Win32 "pura"; in pratica
ogni applicazione scritta espressamente per Win32, una volta che
viene caricata in memoria crede di avere a disposizione un vasto
spazio di indirizzamento che va dall'indirizzo fisico 4194304 (4 Mb)
all'indirizzo fisico 2147483648 (2 Gb). Quest'area di memoria
contiene il blocco dati, il blocco codice e il blocco stack del
programma in esecuzione, più le varie risorse necessarie; se si
18
eseguono due istanze dello stesso programma, ciascuno dei due
programmi vede la situazione di Figura 3 credendo quindi di essere
l'unico programma in esecuzione con le sue risorse private che non
vengono quindi condivise con nessuno. Se due o più programmi Win32
vogliono condividere delle risorse, lo devono richiedere
esplicitamente; a tale proposito Win32 mette a disposizione lo spazio
di indirizzamento virtuale compreso tra 2 Gb e 3 Gb. In quest'area
troviamo ad esempio le DLL esplicitamente condivise dalle
applicazioni Win32; sempre in quest'area sono presenti gli eventuali
memory mapped files (files mappati in memoria). Si tratta dello
strumento che Win32 mette a disposizione per consentire a due o più
programmi di condividere tra loro la stessa area di memoria; i memory
mapped files vengono illustrati in un apposito capitolo.
Proprio l'area di memoria compresa tra 2 e 3 Gb è la più delicata per
Win32; uno degli obiettivi fondamentali di Win32 è quello di
garantire la compatibilità con le vecchie applicazioni scritte per
Win16. In ambiente Win16, ogni programma che vuole allocare
dinamicamente memoria, ha a disposizione due aree di memoria chiamate
Local Heap e Global Heap; il Local Heap è una piccola area di memoria
riservata alle singole applicazioni nel senso che ogni applicazione
ha il suo Local Heap privato. Il Global Heap invece è una vasta area
di memoria a disposizione di tutte le applicazioni; si tratta quindi
di un'area di memoria visibile a qualunque applicazione Win16. Per
garantire la compatibilità con queste vecchie applicazioni, Win32
ricrea lo stesso ambiente di Win16 disponendo il Global Heap proprio
nell'area compresa tra 2 e 3 Gb; tutte le applicazioni Win16 che
girano sotto Win32 condividono questo spazio di memoria e sono quindi
in grado di vedersi l'una con l'altra. La conseguenza di tutto ciò è
che in quest'area della memoria si ricrea la classica bolgia tipica
di Win16; tanto maggiore è il numero di applicazioni Win16 in
esecuzione tanto più cresce l'instabilità di Win32.
Passiamo infine all'area compresa tra 3 e 4 Gb che è la più
importante di tutte in quanto contiene la parte residente in memoria
del kernel di Win32 (codice di ring 0); con il meccanismo della
paginazione della memoria, il codice di ring 0 crea per ogni
applicazione in esecuzione la "realtà virtuale" descritta dalla
Figura 3 e chiamata Virtual Machine (macchina virtuale). Ogni
applicazione Win32 gira quindi in una propria macchina virtuale
gestita da un Virtual Machine Manager o VMM; il compito del VMM
quindi è quello di far credere ad ogni applicazione di essere l'unica
in esecuzione. Un altro componente importante del codice di ring 0 è
rappresentato dai Virtual Device Drivers o VxDs; il compito dei VxDs
è quello di far credere ad ogni applicazione di avere il controllo
totale su una determinata periferica. È chiaro che in un SO multitask
non è possibile concedere alle varie applicazioni in esecuzione
l'accesso diretto alle periferiche del computer; attraverso i VxDs
due o più applicazioni possono accedere alla stesa periferica senza
entrare in conflitto tra loro.

Abbiamo visto che la necessità di garantire la compatibilità con le


applicazioni Win16 crea parecchi problemi a Win32; in particolare
Win32 è costretto a definire una vasta area di memoria virtuale da
condividere tra le varie applicazioni. Tutto ciò si ripercuote
negativamente sul meccanismo di protezione che dovrebbe impedire alle
19
applicazioni di vedersi tra loro; tanto è vero che come molti hanno
avuto modo di constatare, le schermate blu continuano ad imperversare
anche su Windows 9x. Il SO Windows NT e i suoi successori Windows
2000 e Windows XP cercano di risolvere questo problema attraverso il
controllo totale sui 4 Gb di memoria virtuale; di conseguenza, la
condivisione della memoria subisce in questi SO drastiche
limitazioni. Proprio per questo motivo, le applicazioni scritte
esplicitamente per Windows NT, 2000 e XP possono anche non funzionare
in Windows 9x; fortunatamente però grazie alla compatibilità verso il
basso, le applicazioni scritte per Windows 9x girano benissimo (o
quasi) anche sulle versioni superiori di Windows.
In conclusione comunque bisogna dire che nonostante i grandi sforzi
compiuti dalla Microsoft, Windows XP e compagnia non sono ancora in
grado di competere con la proverbiale affidabilità dei SO della
famiglia Unix; basti pensare al fatto che esistono server Unix che
ormai funzionano ininterrottamente da venti anni! Proprio per questo
motivo, i SO basati su Unix (come ad esempio Linux) vengono
largamente impiegati in quei settori nei quali, non essendo possibile
l'intervento umano, viene richiesta una grandissima affidabilità
operativa per lunghissimi periodi di tempo; un esempio pratico
abbastanza eloquente è rappresentato dall'utilizzo di Unix/Linux per
la gestione dei computers di bordo dei satelliti artificiali e delle
sonde spaziali (meditate gente, meditate)!

20
Win32 Assembly

Capitolo 3: Struttura di un programma


Assembly per Win32
Sin dalle prime versioni il SO Windows è stato scritto in C standard
(ANSI C); le parti più critiche del SO, legate alla piattaforma
hardware di destinazione, sono state scritte invece in Assembly. Di
conseguenza, un programma per Windows assume una struttura interna
legata alle convenzioni che sono state esposte nel Capitolo 28 e nel
Capitolo 29 della sezione Assembly Base; nel caso di Win32, tutte
queste convenzioni vengono applicate in un ambiente operativo basato
sulla modalità protetta a 32 bit delle CPU 80386 e superiori.
Nel precedente Capitolo abbiamo visto che al momento dell'esecuzione,
un'applicazione Win32 viene caricata in memoria a partire
dall'indirizzo lineare a 32 bit 4194304d (4 Mb); una volta caricata
in memoria, l'applicazione ha a sua disposizione un vasto spazio di
indirizzamento virtuale che arriva sino all'indirizzo lineare a 32
bit 2147483648d (2 Gb). All'interno di questo spazio sono presenti il
blocco codice, il blocco dati e il blocco stack dell'applicazione; in
sostanza, questi tre blocchi occupano tre aree distinte di uno stesso
segmento di memoria a 32 bit. Ricordiamo che, contrariamente a quanto
molti pensano, anche nella modalità protetta delle CPU 80386 e
superiori esiste la segmentazione della memoria tipica della modalità
reale a 16 bit; la differenza sta nel fatto che mentre in modalità
reale a 16 bit i segmenti sono da 64 Kb ciascuno, in modalità
protetta a 32 bit ciascun segmento può arrivare sino a 4 Gb.
La struttura di un programma Assembly per Win32 è molto simile alla
struttura di un programma Assembly per DOS con modello di memoria
SMALL; in base a quanto è stato detto nella sezione Assembly Base, un
programma che gira in modalità reale con modello di memoria SMALL è
costituito da un blocco codice referenziato da CS, un blocco dati
referenziato da DS e un blocco stack referenziato da SS, con ciascun
blocco che può raggiungere al massimo la dimensione di 64 Kb
(attributo USE16). Esistendo un solo blocco codice, un solo blocco
dati e un solo blocco stack, nella fase di esecuzione del programma
il contenuto dei tre registri di segmento CS, DS e SS non cambia mai;
tutto ciò porta ad una gestione velocissima degli indirizzamenti in
quanto per accedere al codice, ai dati e allo stack la CPU utilizza
indirizzi formati dal solo offset (indirizzi NEAR). Se vogliamo
accedere ad esempio ad una variabile del blocco dati, ci basta
specificare solo il suo offset a 16 bit; in assenza di altre
informazioni la CPU associa quest'offset a DS che è il registro di
segmento predefinito per i dati. Se la somma delle dimensioni del
blocco codice e del blocco stack non supera i 64 Kb, i compilatori
dei linguaggi di alto livello pongono SS=DS raccogliendo quindi dati
e stack in un unico blocco chiamato convenzionalmente DGROUP; in
questo modo si ottiene una gestione più efficiente e veloce dei dati

21
e dello stack del programma.
La stessa situazione si verifica nel caso di un'applicazione per
Win32 che è formata da un solo blocco codice referenziato dal
selettore CS, un solo blocco dati referenziato dal selettore DS e un
solo blocco stack referenziato dal selettore SS; anche in questo caso
quindi per accedere al codice, ai dati e allo stack si utilizzano
indirizzi formati dal solo offset. Questa volta però stiamo lavorando
in modalità protetta a 32 bit e questo significa che gli offset sono
numeri a 32 bit che teoricamente possono spaziare da 00000000h a
FFFFFFFFh; nel capitolo successivo vedremo che anche nel caso di
un'applicazione per Win32 si ha SS=DS, per cui questi due selettori
referenziano la stessa area di memoria.

In base alle considerazioni appena esposte, possiamo dire che un


programma Assembly per Win32 dovrà essere dotato di appositi segmenti
di programma destinati a contenere il codice, i dati e lo stack; come
è stato spiegato nel Capitolo 28 della sezione Assembly Base, i nomi
e gli attributi di questi segmenti di programma devono rispettare
rigorosamente una serie di convenzioni imposte dalla Microsoft,
chiamate anche convenzioni MASM. Naturalmente queste convenzioni
valgono sia per l'Assembly che per qualsiasi altro linguaggio di alto
livello; nel caso dei linguaggi di alto livello questi aspetti
vengono gestiti dal compilatore, mentre nel caso dell'Assembly come
al solito l'onore e l'onere di gestire ogni minimo dettaglio ricade
sul programmatore.
Passiamo ora all'analisi dettagliata delle caratteristiche dei vari
blocchi che formano la struttura di un programma Assembly per Win32.

3.1 Le direttive per l’ assembler

Come al solito, un programma Assembly inizia con una serie di


direttive destinate all’ assembler; le direttive possono essere
inserite dappertutto, ma quelle che vengono poste all'inizio sono le
più importanti in quanto definiscono le caratteristiche generali del
nostro programma. La Figura 1 mostra le direttive fondamentali che
sono sempre presenti in un programma Assembly destinato a girare
sotto Win32.
Figura 1 - Direttive per l’ assembler

.386 ; set di istruzioni a 32 bit


.MODEL FLAT, STDCALL ; memory model & calling conventions
OPTION CASEMAP: NONE ; case-sensitive on symbols

INCLUDE ..\include\windows.inc ; include file principale di Win32


INCLUDE ..\include\user32.inc ; interfaccia per USER32.LIB
INCLUDE ..\include\kernel32.inc ; interfaccia per KERNEL32.LIB

INCLUDELIB ..\lib\user32.lib ; libreria servizi GUI


INCLUDELIB ..\lib\kernel32.lib ; libreria servizi kernel
Analizziamo in dettaglio le varie direttive presenti in questa
sezione; come è stato già detto, si tratta delle direttive minime
necessarie per generare un programma Assembly per Win32.

La direttiva .386,
22
indica all’ assembler che vogliamo utilizzare il set di istruzioni a
32 bit delle CPU 80386 e superiori; come sappiamo Win32 lavora in
modalità protetta a 32 bit, per cui richiede come minimo una CPU di
classe 80386. Come è stato spiegato nella sezione Assembly Base, è
perfettamente inutile ricorrere a direttive del tipo .486, .586, etc,
a meno che non si vogliano utilizzare istruzioni specifiche di queste
CPU, come ad esempio CPUID che richiede almeno la direttiva .586
(naturalmente in questo caso bisogna possedere un PC con CPU di
classe Pentium o superiore); lo stesso discorso vale per le direttive
del tipo .386p, .486p, etc, che sono necessarie solo se vogliamo
utilizzare le istruzioni per la modalità protetta.

La direttiva .MODEL FLAT, STDCALL,


dice all’ assembler quale modello di memoria viene utilizzato dal
nostro programma e quale convenzione viene adottata per il passaggio
dei parametri alle procedure e per la pulizia dello stack al termine
delle procedure stesse; come si vede in Figura 1, il modello di
memoria utilizzato dalle applicazioni Win32 è FLAT. In base a quanto
è stato detto in precedenza, il modello FLAT può essere paragonato ad
una sorta di modello SMALL per la modalità protetta a 32 bit; in
sostanza, il nostro programma è costituito da tre blocchi
referenziati da CS, DS e SS. Il selettore CS punta al descrittore del
blocco codice, il selettore DS punta al descrittore del blocco dati e
il selettore SS punta al descrittore del blocco stack; questi tre
blocchi vengono inseriti in un gigantesco spazio di indirizzamento
virtuale a 32 bit. L'aspetto più importante da considerare è che
all'interno di ciascun blocco, gli indirizzamenti si svolgono in modo
lineare grazie proprio agli offset a 32 bit; per chi proviene dal
mondo della modalità reale 8086 il modello FLAT rappresenta la fine
di un incubo. In modalità reale, se il nostro programma ha bisogno al
massimo di 64 Kb di codice, 64 Kb di dati e 64 Kb di stack, possiamo
ritenerci fortunati; in questo caso infatti questi tre blocchi
vengono referenziati da CS, DS e SS e il contenuto di questi tre
registri di segmento rimane invariato per tutta la fase di esecuzione
del programma. Tutti gli indirizzamenti si svolgono in modo lineare a
16 bit, nel senso che la CPU lavora con indirizzi formati dal solo
offset a 16 bit (indirizzi NEAR); ogni volta che la CPU si imbatte in
un offset a 16 bit, è in grado di associarlo al corrispondente
registro di segmento grazie al fatto che il nostro programma ha un
solo segmento di codice, un solo segmento di dati e un solo segmento
di stack. Se invece il nostro programma ha bisogno di più di 64 Kb di
codice e/o più di 64 Kb di dati, allora si entra nell'inferno della
segmentazione a 64 Kb della memoria; tutti gli indirizzamenti in
questo caso sono formati da una coppia seg:offset (indirizzi FAR), in
quanto la CPU ha bisogno di sapere non solo a quale offset di memoria
vogliamo accedere, ma anche a quale segmento di programma appartiene
quell'offset. Lo stesso problema si presenta nel momento in cui
vogliamo allocare dinamicamente un blocco di memoria più grande di 64
Kb; anche in questo caso siamo costretti a richiedere al SO due o più
blocchi di memoria.
Per superare questo problema bisogna ricorrere alla modalità protetta
delle CPU 80386 e superiori; in questo modo possiamo sfruttare gli
indirizzamenti lineari a 32 bit che ci permettono di muoverci
virtualmente all'interno di segmenti di programma da 4 Gb ciascuno.
23
Il problema che si presenta è dato dal fatto che in ambiente DOS, se
vogliamo scrivere programmi che girano in modalità protetta a 32 bit,
siamo costretti a scrivere anche le procedure per l'accesso alle
varie periferiche (dischi, tastiera, mouse, stampante, etc); non
bisogna dimenticare infatti che i vari servizi offerti dal DOS, dal
BIOS, dai Device Drivers, etc, sono concepiti espressamente per la
modalità reale.
Il discorso cambia radicalmente nel momento in cui scriviamo
applicazioni per Win32; in questo caso, non solo possiamo sfruttare
gli indirizzamenti lineari a 32 bit, ma abbiamo anche a disposizione
un vero SO a 32 bit che ci fornisce una serie enorme di procedure
anch'esse a 32 bit, attraverso le quali possiamo accedere a tutto ciò
che è collegato al nostro computer. Il modello di memoria FLAT
significa tutto questo; prepariamoci quindi a mettere da parte i vari
segment overrides, le direttive ASSUME, i puntatori NEAR, i puntatori
FAR, etc. Nel modello di memoria FLAT di Win32 un indirizzo è formato
da un solo offset a 32 bit; se ad esempio vogliamo trasferire in AX
un dato a 16 bit puntato da EBX, dobbiamo semplicemente scrivere:
mov ax, [ebx].
Tutti gli altri dettagli relativi al contenuto dei vari registri di
segmento (selettori) vengono gestiti direttamente dal SO; se si prova
a modificare il contenuto di questi registri, si provoca l'intervento
del SO che chiude forzatamente il nostro programma e mostra un
messaggio di errore (errore di pagina non valida).
Per quanto riguarda il passaggio degli argomenti alle procedure,
Win32 segue la convenzione STDCALL; come abbiamo visto nella sezione
Assembly Base, le due convenzioni più importanti sono la convenzione
C e la convenzione Pascal. Secondo la convenzione C, gli argomenti da
passare ad una procedura vengono inseriti nello stack a partire
dall'ultimo; inoltre, al termine della procedura il compito di
ripulire lo stack spetta al caller. Secondo la convenzione Pascal
invece, gli argomenti da passare ad una procedura vengono inseriti
nello stack a partire dal primo; inoltre, al termine della procedura
il compito di ripulire lo stack spetta alla procedura stessa. La
convenzione C è preferibile per quanto riguarda il passaggio degli
argomenti, in quanto ci permette di implementare procedure che
richiedono un numero variabile di argomenti; la convenzione Pascal è
più veloce nella pulizia dello stack. La convenzione STDCALL seguita
da Win32 è un misto tra queste due; in sostanza, quando si chiama una
procedura in Win32, si passano gli argomenti secondo la convenzione C
e si ripulisce lo stack secondo la convenzione Pascal. L'unica
eccezione è rappresentata dal caso in cui si voglia effettuare da
Windows la chiamata di procedure appartenenti alle librerie standard
del C; molte di queste procedure sono infatti disponibili anche sotto
Windows. La procedura sprtintf ad esempio è presente in Windows con
il nome wsprintf; solo in questi casi si deve seguire la convenzione
C sia per il passaggio degli argomenti che per la pulizia dello
stack.

La direttiva OPTION CASEMAP: NONE,


dice all’ assembler che nella fase di individuazione dei nomi (di
variabili, di procedure, di etichette, etc) da inserire nella Symbol
Table è necessario distinguere tra lettere maiuscole e lettere
minuscole; questo ci permette ad esempio di definire due variabili
24
chiamate var1 e VAR1 che verranno considerate distinte dall’
assembler (si tratta comunque di un pessimo stile di programmazione).
Ricordiamoci che Windows è stato scritto prevalentemente in C, e cioè
con un linguaggio che distingue tra lettere maiuscole e lettere
minuscole (case-sensitive); in sostanza questo significa che in C il
nome var1 è diverso dal nome VAR1. Il Pascal invece è un linguaggio
case-insensitive che non distingue quindi tra lettere maiuscole e
lettere minuscole; questo significa che se proviamo a definire in
Pascal le due variabili precedenti, otteniamo un messaggio di errore
del compilatore. Questa direttiva può essere sostituita anche
dall'opzione /ml da passare direttamente al TASM; nel caso del MASM
si deve utilizzare invece l'opzione /Cp.

La direttiva include ..\include\windows.inc,


permette al nostro programma di accedere al contenuto dell'include
file windows.inc; come già sappiamo questo è l'include file
principale dell'SDK di Win32 e contiene un'autentica marea di
dichiarazioni di costanti, di nuovi tipi di dati, di strutture, etc,
che sono essenziali per lo sviluppo di applicazioni Win32. È molto
importante che il programmatore dia uno sguardo approfondito a questo
file per farsi un'idea precisa del suo contenuto; in ogni caso, nei
capitoli successivi avremo modo di prendere confidenza con le
informazioni contenute in windows.inc. Come è stato detto nel
Capitolo 1 di questa sezione, man mano che escono le nuove versioni
di Windows, il contenuto di windows.inc (o di files simili come
win32.inc) viene continuamente aggiornato da diversi programmatori;
questo lavoro consiste nell'aggiunta di nuove costanti simboliche,
nuove strutture, etc, introdotte dalle versioni più recenti di
Windows.
Subito dopo la direttiva che include windows.inc troviamo due
analoghe direttive che includono gli altri due files user32.inc e
kernel32.inc; nell'SDK di Win32 ad ogni libreria di procedure è
associato un include file che contiene l'interfaccia della libreria
stessa. Una libreria contiene il codice delle varie procedure, e cioè
le definizioni delle procedure; il corrispondente include file
contiene i prototipi di queste procedure, e cioè le dichiarazioni
delle procedure, più eventuali dichiarazioni di costanti, strutture,
etc, relative a quella particolare libreria. I due include files
user32.inc e kernel32.inc contengono le dichiarazioni delle procedure
definite nelle corrispondenti librerie user32.lib e kernel32.lib;
come si vede in Figura 1, queste due librerie vengono collegate al
nostro programma attraverso le direttive INCLUDELIB.
La libreria user32.lib contiene una serie di procedure attraverso le
quali possiamo accedere ai servizi dell'interfaccia utente di
Windows; la libreria kernel32.inc contiene una serie di procedure
attraverso le quali possiamo accedere ai servizi a basso livello che
ci vengono messi a disposizione dal kernel di Win32.

3.2 Il segmento dati inizializzati

Cominciamo ora ad analizzare i vari segmenti di programma presenti in


una applicazione Win32; la Figura 2 mostra le caratteristiche del
segmento dati inizializzati di un programma Assembly per Win32.

25
Figura 2 - Segmento dati inizializzati

_DATA SEGMENT DWORD PUBLIC USE32 'DATÀ

_DATA ENDS
In questo blocco dobbiamo inserire tutti i dati inizializzati del
nostro programma, cioè tutti quei dati che vengono inizializzati al
momento della loro definizione; scrivendo ad esempio:
varWord1 dw 12800,
stiamo definendo una variabile chiamata varWord1 che occupa in
memoria 16 bit e viene inizializzata con il valore 12800d.
Come è stato detto all'inizio di questo capitolo, quando scriviamo un
programma Assembly per Win32 abbiamo l'obbligo di seguire le
convenzioni MASM relative ai nomi e agli attributi dei segmenti di
programma; come si vede in Figura 2, il segmento dati inizializzati
deve chiamarsi obbligatoriamente _DATA e deve avere un attributo di
classe 'DATÀ. L'attributo di allineamento è DWORD, e questo significa
che il segmento _DATA deve partire da un' indirizzo di memoria
multiplo di 4 byte; come sappiamo, questo è l'allineamento ottimale
per il Data Bus a 32 bit delle CPU 80386 e 80486. Per consentire alla
CPU di accedere ai dati alla massima velocità possibile, dobbiamo
fare in modo che i dati stessi si trovino correttamente allineati
all'interno del segmento _DATA; i dati di tipo BYTE possono trovarsi
a qualunque indirizzo di memoria, i dati di tipo WORD devono trovarsi
possibilmente ad indirizzi pari, mentre i dati di tipo DWORD, QWORD
etc, devono trovarsi possibilmente ad indirizzi multipli di 4 byte.
Per quanto riguarda gli altri attributi, osserviamo in particolare
che l'attributo SIZE viene impostato a USE32; come già sappiamo
questo significa che il segmento _DATA viene gestito attraverso gli
offset a 32 bit.
Grazie alla presenza della direttiva .MODEL usata per definire il
modello di memoria del nostro programma, possiamo anche servirci
delle direttive semplificate per la creazione dei segmenti di
programma; nel caso del segmento dati inizializzati, possiamo
sostituire tutto ciò che si vede in Figura 2 con la direttiva
semplificata .DATA.

3.3 Il segmento dati non inizializzati

Il segmento dati inizializzati precedentemente descritto, può anche


contenere la definizione di dati privi di valore iniziale; un esempio
pratico può essere rappresentato dalla definizione:
varDword1 dd ?.
Inserendo i dati non inizializzati nel segmento dati inizializzati,
otteniamo naturalmente un programma perfettamente funzionante; se
però vogliamo aiutare il SO ad ottimizzare al massimo l'utilizzo
della memoria, allora è preferibile inserire la definizione dei dati
non inizializzati in un apposito segmento di programma. La Figura 3
mostra proprio le caratteristiche di questo blocco dati chiamato
segmento dati non inizializzati.

26
Figura 3 - Segmento dati non inizializzati

_BSS SEGMENT DWORD PUBLIC USE32 'BSS'

_BSS ENDS
Questo blocco deve chiamarsi obbligatoriamente _BSS come previsto
dalle convenzioni MASM; gli attributi sono identici a quelli del
segmento dati inizializzati, con la sola eccezione dell'attributo di
classe che deve essere 'BSS'. È importante che in questo blocco non
vengano inseriti dati inizializzati; in tal caso il programma
funziona ugualmente bene, ma costringe il SO ad utilizzare più
memoria di quella necessaria.
Se preferiamo servirci delle direttive semplificate per i segmenti,
possiamo sostituire tutto ciò che si vede in Figura 3 con la
direttiva .DATA?.

3.4 Il segmento dati costanti

Come abbiamo visto nel Capitolo 28 della sezione Assembly Base, i


compilatori e gli interpreti per poter organizzare i programmi nel
modo più efficiente possibile, definiscono anche un blocco riservato
ai dati costanti; in questo blocco vengono sistemati tutti quei dati
di tipo numerico o di tipo stringa che non sono associati
necessariamente ad un nome di variabile. Un esempio pratico è
rappresentato dalla seguente istruzione C:
printf("Premere un tasto per continuare");
La stringa "Premere un tasto per continuare" viene utilizzata come
argomento da passare alla funzione printf, ma non è associata a
nessuna variabile; questa stringa rappresenta un classico esempio di
dato costante che i compilatori C inseriscono proprio nel blocco per
i dati costanti. La Figura 4 illustra le caratteristiche di questo
particolare blocco dati.
Figura 4 - Segmento dati costanti

_CONST SEGMENT DWORD PUBLIC USE32 'CONST'

_CONST ENDS
In presenza della direttiva .MODEL, questo blocco può essere creato
con l'ausilio della direttiva semplificata .CONST.

3.5 Il segmento di stack

Ai tempi di Win16 il programmatore era tenuto a specificare la


dimensione in byte da assegnare al segmento di stack del programma;
questa informazione veniva passata al linker attraverso un apposito
file chiamato Definition File. La Figura 5 mostra un esempio pratico
di un classico definition file per un'applicazione Win16 chiamata
Win16App.

27
Figura 5 - Definition File

NAME Win16App
DESCRIPTION 'Applicazione per Win16'
EXETYPE WINDOWS
STUB 'WINSTUB.EXÈ
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE
HEAPSIZE 1024
STACKSIZE 8192
Il definition file è un file in formato ASCII che reca l'estensione
DEF; convenzionalmente, il nome del definition file è lo stesso nome
usato per il modulo principale dell'applicazione che stiamo
scrivendo. Nel caso di Figura 5, l'applicazione Win16 si chiama
Win16App, per cui il definition file verrà salvato sul disco con il
nome Win16App.def; esaminiamo ora i vari campi presenti all'interno
di questo file.
Il campo NAME specifica il nome che verrà assegnato all'eseguibile
generato dal linker; nel nostro caso, l'eseguibile verrà chiamato
Win16App.exe.
Il campo DESCRIPTION specifica una stringa contenente una breve
descrizione del nostro programma; questa stringa viene inserita
nell'header dell'eseguibile, per cui la possiamo utilizzare per
"firmare" il nostro programma.
Il campo EXETYPE specifica il tipo di eseguibile che verrà generato
dal linker; nel nostro caso il tipo WINDOWS indica che Win16App.exe
sarà in formato eseguibile per Windows.
Il campo STUB indica un programma che verrà chiamato automaticamente
nel caso in cui si tenti di eseguire Win16App.exe dal prompt del DOS;
in questo caso, il programma predefinito winstub.exe interviene e
mostra il messaggio:
Questo programma richiede Microsoft Windows
Il campo CODE elenca una serie di attributi assegnati al segmento di
codice del nostro programma; l'attributo PRELOAD indica che il blocco
codice verrà automaticamente caricato in memoria al momento
dell'avvio del nostro programma, l'attributo MOVEABLE indica che
all'occorrenza Windows potrà spostare blocchi di codice in un'altra
zona della RAM in modo da ottimizzare l'uso della memoria,
l'attributo DISCARDABLE indica che all'occorrenza Windows potrà
scaricare blocchi di codice sull'Hard Disk per fronteggiare
situazioni di scarsità di memoria.
Il campo DATA elenca una serie di attributi assegnati al segmento
dati del nostro programma; gli attributi PRELOAD e MOVEABLE hanno il
solito significato, mentre l'attributo MULTIPLE indica che se vengono
eseguite due o più istanze dello stesso programma, ogni istanza avrà
la sua copia privata del segmento dati.
Il campo HEAPSIZE indica la dimensione iniziale in byte che verrà
assegnata al Local Heap della nostra applicazione; nel caso di Figura
5, viene assegnata una dimensione iniziale di 1024 byte che se
necessario verrà modificata da Windows.
Il campo STACKSIZE indica la dimensione in byte da assegnare allo
stack del nostro programma; in Win16 la dimensione minima
raccomandata è di 8192 byte (8 Kb), mentre in Win32 è di circa 10 Kb.
28
Già con l'arrivo di Windows 95 alcuni campi del definition file hanno
cominciato a perdere importanza; in particolare, un'applicazione
"pura" per Windows 95 non ha nessun Local Heap, per cui il campo
HEAPSIZE anche se è presente viene ignorato dal linker. I campi CODE
e DATA diventano importanti solo se si installa Windows in un
computer con scarse risorse di memoria RAM; inoltre l'attributo
MULTIPLE del campo DATA è superfluo in quanto in Win32 ogni
applicazione ha il suo spazio di indirizzamento privato. In generale
quando si sviluppano applicazioni per Win32 è anche possibile
omettere il definition file; in questo caso i vari linker utilizzano
una serie di impostazioni predefinite che spesso sono molto più
efficienti rispetto a quelle specificate dal programmatore. In
particolare il linker fornisce al SO tutte le indicazioni per la
corretta inizializzazione dei registri SS e ESP utilizzati per la
gestione dello stack; grazie all'abbondante disponibilità di spazio
virtuale, in assenza del definition file un'applicazione Win32 riceve
generalmente 1 Mb di stack. Per tutti questi motivi, la tendenza che
si segue in Win32 è quella di evitare del tutto l'uso del definition
file da associare alle applicazioni.

3.6 Il segmento di codice

Nella sezione Assembly Base abbiamo visto che un programma Assembly


destinato a girare in ambiente DOS ha un blocco codice principale che
inizia con un'etichetta che rappresenta l'entry point del programma,
e termina con una serie di istruzioni che restituiscono il controllo
al DOS. Qualcuno potrebbe restare sorpreso nell'apprendere che in
Win32 succede esattamente la stessa cosa; la Figura 6 mostra proprio
l'estrema semplicità dello scheletro del segmento di codice di
un'applicazione Assembly per Win32.
Figura 6 - Segmento di codice

_TEXT SEGMENT DWORD PUBLIC USE32 'CODÈ

start: ; entry point del programma

; -------------- inizio blocco istruzioni --------------

; --------------- fine blocco istruzioni ---------------

push dword ptr 0 ; exit code = 0


call ExitProcess ; termina il programma

_TEXT ENDS
Il primo aspetto da osservare è che il segmento di codice deve
chiamarsi obbligatoriamente _TEXT; l' attributo di classe inoltre
deve essere obbligatoriamente 'CODÈ. Appena il programma viene
caricato in memoria, l' instruction pointer EIP viene inizializzato
proprio con l'offset a 32 bit dell'etichetta che abbiamo indicato
come entry point; il nome di questa etichetta deve essere
naturalmente lo stesso indicato dalla direttiva END che chiude il
modulo Assembly. Nel caso di Figura 6 viene utilizzato il nome start,
ma siamo liberi di utilizzare qualsiasi altro nome come startWin32,
29
main, etc; i linguaggi di alto livello invece impongono nomi
obbligatori per l'entry point del programma.
Per indicare a Win32 che il nostro programma è giunto al termine,
dobbiamo chiamare la procedura ExitProcess; questa procedura viene
definita nella libreria kernel32.lib e rappresenta quindi uno dei
tanti servizi a basso livello che ci vengono messi a disposizione dal
kernel di Win32. Se consultiamo il Win32 Programmer's Reference,
possiamo notare che questa procedura viene dichiarata come:
void ExitProcess(UINT uExitCode);
Come è stato spiegato in un precedente capitolo, il Win32
Programmer's Reference contiene tutta la documentazione relativa
all'API di Win32; questa documentazione comprende la descrizione
completa delle procedure, delle strutture, delle costanti
predefinite, delle varie TYPEDEF, etc, che fanno parte dell'API di
Win32. In virtù del fatto che Windows è scritto in C, anche i manuali
di riferimento sull'API di Windows utilizzano la sintassi del
linguaggio C; chi conosce bene questo linguaggio si troverà quindi a
proprio agio anche nella programmazione Assembly in ambiente Windows.
Analizzando la precedente dichiarazione di ExitProcess (prototipo di
funzione), possiamo notare che questa procedura richiede un unico
parametro uExitCode di tipo UINT, e quando termina non restituisce
nessun valore (void); il tipo UINT rappresenta in Windows il tipo di
dato intero senza segno a 32 bit. Questo tipo di dato viene creato
nell'include file windows.inc con la dichiarazione:
UINT TYPEDEF DWORD,
che in vecchio stile Assembly è perfettamente equivalente alla
dichiarazione:
UINT equ DD;
ogni volta che l’ assembler incontra il nome simbolico UINT, lo
sostituisce con DD (Define Double Word). Il nome uExitCode ha il solo
scopo di ricordarci che questo parametro contiene un valore numerico
a 32 bit che il nostro programma restituisce a Windows prima di
terminare (exit code); secondo la convenzione Unix, un valore di
ritorno uguale a zero indica la terminazione corretta del nostro
programma. Si tenga presente comunque che questo exit code viene
totalmente ignorato da Windows.
La Figura 6 ci permette di vedere come avviene la chiamata di
ExitProcess in vecchio stile Assembly e come viene applicata in
pratica la convenzione STDCALL; prima di tutto dobbiamo inserire
nello stack i parametri da passare alla procedura. Secondo la
convenzione C questi parametri vengono inseriti nello stack a partire
dall'ultimo; nel nostro caso esiste un solo parametro a 32 bit che
viene inserito nello stack con l'istruzione:
push dword ptr 0.
Il type override dword ptr serve solo per rendere più chiara
l'istruzione; ricordiamoci che in modalità protetta a 32 bit lo stack
viene gestito attraverso SS:ESP. L'istruzione PUSH accetta quindi
operandi a 16 bit e a 32 bit; se questi operandi sono di tipo mem o
reg, allora PUSH è in grado di determinare la loro dimensione in bit.
Se l'operando è di tipo imm, allora PUSH in assenza del type override
converte sempre il valore immediato in un numero a 32 bit.

Nota importante. I manuali tecnici per i programmatori Win32


raccomandano vivamente di utilizzare per PUSH e POP, esclusivamente
30
operandi a 32 bit, in modo che ESP rimanga sempre allineato alla
DWORD; è importantissimo quindi evitare di utilizzare il type
override word ptr per costringere queste istruzioni a lavorare con
operandi a 16 bit. Nel caso di operandi immediati a 8 o 16 bit, l’
assembler provvede ad effettuare la loro conversione a 32 bit; non è
necessario quindi inserire il type override dword ptr. Nel caso di
operandi di tipo reg o mem a 8 o 16 bit, tutte le conversioni
necessarie sono invece a carico del programmatore; se ad esempio
vogliamo salvare nello stack i 16 bit di AX, dobbiamo passare a PUSH
l'operando EAX, azzerando se necessario i suoi 16 bit più
significativi.

Una volta che abbiamo inserito i parametri nello stack, possiamo


chiamare la procedura ExitProcess con l'istruzione:
call ExitProcess;
prima di terminare, ExitProcess nel rispetto della convenzione Pascal
ripulisce lo stack con l'istruzione:
ret (4) (1 DWORD = 4 BYTE).
Se Win32 avesse seguito la convenzione C anche per la pulizia dello
stack, questo compito sarebbe spettato a noi; in questo caso, subito
dopo la chiamata di ExitProcess avremmo dovuto inserire l'istruzione:
add esp, 4,
o anche ad esempio:
pop eax.
Grazie alla presenza della direttiva .MODEL, possiamo evitare tutto
questo lavoro sfruttando le istruzioni avanzate di TASM e MASM; nel
caso di TASM possiamo scrivere:
call ExitProcess, 0,
mentre nel caso di MASM possiamo scrivere:
invoke ExitProcess, 0.
Quando l’ assembler incontra queste istruzioni, utilizza le
informazioni specificate dalla direttiva .MODEL per sapere come si
deve comportare; tutto il lavoro svolto dall’ assembler può essere
analizzato attraverso il Listing File.

Un' aspetto importantissimo da analizzare è legato al fatto che anche


all'interno del segmento di codice del nostro programma possiamo
definire delle variabili; come al solito, è necessario fare in modo
che la CPU non tenti di eseguire queste variabili scambiandole per
codici macchina di qualche istruzione. Una soluzione a questo
problema può essere quella mostrata in Figura 7.
Figura 7 - Definizione di dati nel blocco codice

_TEXT SEGMENT DWORD PUBLIC USE32 'CODE’

start: ; entry point del programma

jmp short start_code: ; salta le definizioni dei dati

varCode1 dw 13500
varCode2 dd 400000
varCode3 dw 3FA0h

start_code:

31
; -------------- inizio blocco istruzioni --------------

; --------------- fine blocco istruzioni ---------------

push 0 ; exit code = 0


call ExitProcess ; termina il programma

_TEXT ENDS
In pratica, subito dopo l'entry point è presente un'istruzione JMP
che esegue un salto incondizionato all'etichetta start_code; in
questo modo la CPU salta le definizioni dei vari dati presenti
all'interno del blocco codice. Il problema che si presenta è dato dal
fatto che ci troviamo in modalità protetta; se vogliamo modificare il
contenuto di una variabile definita nel segmento di codice, dobbiamo
avere il permesso di scrittura su questo segmento. Nell'ambiente
operativo a 16 bit predisposto dal DOS, qualunque segmento di
programma (codice, dati o stack), è accessibile sia in lettura che in
scrittura; questo significa che possiamo tranquillamente leggere o
modificare il contenuto di eventuali variabili definite anche nel
segmento di codice. In modalità protetta invece tutto dipende dai
vari attributi assegnati ai descrittori dei vari segmenti di
programma; nel caso di Win32 questi attributi vengono decisi
naturalmente dal SO. In una applicazione Win32, i segmenti di stack,
di dati inizializzati e di dati non inizializzati sono ovviamente
accessibili sia in lettura che in scrittura; il segmento di codice
invece (che è ovviamente un segmento eseguibile), è accessibile solo
in lettura. Questo significa che possiamo tranquillamente leggere i
dati che abbiamo definito in Figura 7, ma non possiamo modificare il
loro contenuto; se proviamo ad accedere in scrittura a questi dati,
il SO chiude forzatamente il nostro programma e mostra un messaggio
di errore.

Un'ultima cosa da dire è legata al fatto che come al solito, grazie


alla presenza nel nostro programma della direttiva .MODEL, possiamo
servirci delle direttive semplificate per i segmenti di programma;
nel caso di Figura 6 o di Figura 7, l'inizio del segmento di codice
può essere indicato attraverso la direttiva semplificata .CODE.

3.7 Il gruppo DGROUP

In fase di assemblaggio di un programma per Win32, l’ assembler in


presenza della direttiva .MODEL, raggruppa tutti i blocchi di dati in
un gruppo chiamato convenzionalmente DGROUP; come abbiamo visto nella
sezione Assembly Base e come viene mostrato anche nel capitolo
successivo, l’ assembler inserisce automaticamente nel programma la
direttiva:
DGROUP GROUP _DATA, _CONST, _STACK, _BSS
Lo scopo di questa direttiva è quello di permettere una gestione più
semplice ed efficiente dei dati di un programma; tutti i dettagli
relativi alla inizializzazione dei registri di segmento (compreso DS)
spettano al SO. Il programmatore quindi deve evitare nella maniera
più assoluta di modificare il contenuto di questi registri (come ad
esempio, tentare di caricare DGROUP in DS); d'altra parte abbiamo
32
anche visto che in modalità protetta i registri di segmento svolgono
un ruolo completamente diverso da quello svolto in modalità reale.
Nella sezione Assembly Base abbiamo visto che in presenza della
direttiva .MODEL, il comportamento del TASM in relazione alla
creazione del gruppo DGROUP differisce dal comportamento del MASM; il
MASM crea automaticamente il gruppo DGROUP sia in presenza delle
direttive semplificate per i segmenti, sia in presenza delle
direttive classiche per i segmenti stessi. Il TASM invece crea
correttamente il gruppo DGROUP solo in presenza delle direttive
semplificate per i segmenti; in presenza invece delle direttive
classiche per i segmenti, il lavoro di creazione del gruppo DGROUP
viene lasciato al programmatore. Se il programmatore sa esattamente
quello che sta facendo, può usare tranquillamente le direttive
classiche per i segmenti di programma; nel caso generale invece si
consiglia vivamente di utilizzare le direttive semplificate per i
segmenti.

3.8 Template di un programma Assembly per Win32

Raccogliendo tutte le considerazioni esposte in questo capitolo,


possiamo definire lo scheletro di una applicazione Assembly per
Win32; la Figura 8 mostra un esempio con le chiamate delle procedure
in puro stile Assembly compatibile con tutte le versioni di TASM e di
MASM.
Figura 8 - Template di un programma Assembly per Win32
;------------------------------------------------;
; nome del modulo Assembly ;
; descrizione del programma ;
;------------------------------------------------;

; ################# direttive per l’ assembler #################

.386 ; set di istruzioni a 32 bit


.MODEL FLAT, STDCALL ; memory model & calling conventions
OPTION CASEMAP: NONE ; case sensitive on symbols

INCLUDE ..\include\windows.inc ; include file principale di Win32


INCLUDE ..\include\user32.inc ; interfaccia per USER32.LIB
INCLUDE ..\include\kernel32.inc ; interfaccia per KERNEL32.LIB

INCLUDELIB ..\lib\user32.lib ; libreria servizi GUI


INCLUDELIB ..\lib\kernel32.lib ; libreria servizi kernel

; ################ segmento dati inizializzati #################

_DATA SEGMENT DWORD PUBLIC USE32 'DATÀ

_DATA ENDS

; ############## segmento dati non inizializzati ###############

_BSS SEGMENT DWORD PUBLIC USE32 'BSS'

_BSS ENDS

; ################## segmento dati costanti ####################

_CONST SEGMENT DWORD PUBLIC USE32 'CONST'


33
_CONST ENDS

; ####################### gruppo DGROUP ########################

DGROUP GROUP _DATA, _BSS, _CONST

; ##################### segmento di codice #####################

_TEXT SEGMENT DWORD PUBLIC USE32 'CODÈ

ASSUME cs: _TEXT, ds: _DGROUP, ss: _DGROUP

start: ; entry point del programma

push 0 ; exit code = 0


call ExitProcess ; termina il programma

_TEXT ENDS

; ##############################################################

END start ; fine del modulo


A lungo andare, l'uso delle istruzioni PUSH per il passaggio degli
argomenti alle procedure può diventare abbastanza fastidioso;
possiamo servirci allora delle direttive avanzate di TASM e MASM. Se
stiamo utilizzando TASM, la chiamata di ExitProcess diventa:
call ExitProcess, 0;
se invece stiamo utilizzando MASM, dobbiamo scrivere:
invoke ExitProcess, 0
(ricordiamo che per poter usare queste direttive avanzate, è
necessaria come al solito la presenza della direttiva .MODEL). Le
direttive avanzate CALL e INVOKE lavorando in combinazione con i
prototipi delle procedure, sono in grado di rilevare eventuali errori
che possiamo commettere nel passaggio degli argomenti alle procedure
stesse; l'unico (grave) problema è dato dal fatto che MASM non
supporta la direttiva avanzata CALL e TASM ricambia il favore non
supportando la direttiva avanzata INVOKE. Per i piccoli programmi,
questo problema può essere facilmente superato con l'utilizzo delle
direttive condizionali; nel caso del template di Figura 8, nel blocco
delle direttive per l’ assembler possiamo inserire la dichiarazione:
TASM = 1.
A questo punto, la chiamata della procedura ExitProcess può essere
riscritta come:
IF TASM
call ExitProcess, 0
ELSE
invoke ExitProcess, 0
ENDIF
Se la costante TASM ha un valore diverso da zero, allora verrà
eseguita la direttiva avanzata CALL; se invece la costante TASM vale
zero, allora verrà eseguita la direttiva avanzata INVOKE. Ricordiamo
che l'utilizzo delle direttive condizionali, non ha nessuna influenza
ne sulle dimensioni ne sulle prestazioni del programma; l’ assembler
esamina il codice precedente e alla fine elimina tutte le parti
superflue (vedere a tale proposito il Listing File).
Quando si scrivono programmi piuttosto grandi, questo procedimento
34
diventa piuttosto impegnativo perché il programmatore è costretto
praticamente a scrivere due programmi in uno; se si ha la necessità
di assemblare con TASM un programma scritto per il MASM contenente
quindi parecchie direttive INVOKE, si può tentare di utilizzare la
seguente macro:
invoke equ call.
È chiaro però che se nella lista degli argomenti passati tramite
INVOKE è presente l'operatore ADDR, la precedente macro produce un
errore del TASM; in questo caso il programmatore deve sostituire
l'operatore ADDR secondo il metodo descritto nel Capitolo 28 della
sezione Assembly Base. In ogni caso, tutti gli esempi presentati
nella sezione Win32 Assembly verranno scritti separatamente per il
MASM e per il TASM.

Il template di Figura 8 deve essere necessariamente utilizzato quando


il programmatore dispone di vecchie versioni di MASM e TASM a 32 bit
che non supportano la direttiva .MODEL o il modello di memoria FLAT;
in un caso del genere si è costretti a specificare in dettaglio tutti
i nomi e gli attributi di ogni segmento di programma. Il
programmatore deve inoltre creare il gruppo DGROUP specificando anche
tramite la direttiva ASSUME tutte le necessarie associazioni tra
segmenti di programma e registri di segmento; naturalmente, in un
caso del genere bisogna programmare in stile Assembly classico in
quanto non è possibile utilizzare le direttive avanzate di MASM e
TASM.
Avendo a disposizione le ultime versioni di MASM e TASM non c'è
nessuna ragione per utilizzare il template di Figura 8; come già
sappiamo infatti, nell'ambiente operativo fornito da Win32 il
programmatore ha a disposizione esclusivamente i segmenti di
programma _DATA, _BSS, _CONST, _STACK e _TEXT. Tutti questi segmenti
hanno nomi e attributi standard che il programmatore non può
assolutamente modificare; tutte le associazioni tra segmenti di
programma e registri di segmento devono rigorosamente seguire un
preciso schema imposto dal SO. In una situazione di questo genere è
vivamente consigliabile quindi l'uso delle direttive semplificate per
i segmenti di programma; in questo modo, grazie ai parametri
specificati nella direttiva .MODEL, si delega all’ assembler il
compito di gestire correttamente tutta la situazione. In particolare,
l’ assembler è in grado in questo caso di creare automaticamente il
gruppo DGROUP e di stabilire le opportune associazioni tra segmenti
di programma e registri di segmento; la Figura 9 illustra il nuovo
aspetto assunto dal template di Figura 8 riscritto con l'ausilio
delle direttive semplificate per i segmenti di programma (versione
MASM):
Figura 9 - Template di un programma Assembly per Win32
;------------------------------------------------;
; nome del modulo Assembly ;
; descrizione del programma ;
;------------------------------------------------;

; ################# direttive per l’ assembler #################

.386 ; set di istruzioni a 32 bit


.MODEL FLAT, STDCALL ; memory model & calling conventions
OPTION CASEMAP: NONE ; case sensitive on symbols

35
INCLUDE ..\include\windows.inc ; include file principale di Win32
INCLUDE ..\include\user32.inc ; interfaccia per USER32.LIB
INCLUDE ..\include\kernel32.inc ; interfaccia per KERNEL32.LIB

INCLUDELIB ..\lib\user32.lib ; libreria servizi GUI


INCLUDELIB ..\lib\kernel32.lib ; libreria servizi kernel

; ################ segmento dati inizializzati #################

.DATA

; ############## segmento dati non inizializzati ###############

.DATA?

; ################## segmento dati costanti ####################

.CONST

; ##################### segmento di codice #####################

.CODE

start: ; entry point del programma

invoke ExitProcess, 0 ; fine programma con exit code 0

; ##############################################################

END start ; fine del modulo

3.9 Programmazione in modalità protetta a 32 bit

Prima di iniziare a scrivere programmi Assembly per Win32, bisogna


ricordare che in questo caso ci troviamo in un ambiente operativo
basato sulla modalità protetta a 32 bit delle CPU 80386 e superiori;
è necessario quindi riassumere brevemente le principali differenze
che esistono tra la modalità protetta a 32 bit e la modalità reale a
16 bit. Gli assembler come MASM e TASM sono in grado di rilevare
automaticamente la presenza di un ambiente operativo a 16 o a 32 bit;
in presenza di un ambiente operativo a 32 bit il comportamento
predefinito dell’ assembler si basa principalmente sulle seguenti
regole:

Tutti gli indirizzamenti sono di tipo NEAR e sono costituiti quindi


dalla sola componente offset che questa volta però è un numero a 32
bit; con un offset a 32 bit possiamo spostarci teoricamente da
00000000h a FFFFFFFFh. Quest'offset è riferito al base address, cioè
all'indirizzo lineare a 32 bit da cui parte il segmento di programma
in cui ci troviamo; nel caso dei SO come Win32, il base address di
ogni segmento di programma viene ovviamente definito dallo stesso SO.

Tutti i registri generali EAX, EBX, ECX, EDX, e tutti i registri


speciali ESI, EDI, ESP, EBP, possono essere utilizzati come registri
puntatori; ricordiamo che negli indirizzamenti che comprendono
registro base, registro indice e spiazzamento, il registro ESP può

36
ricoprire solo il ruolo di registro base. In modalità protetta a 32
bit possiamo quindi scrivere istruzioni del tipo:
mov dx, [eax + edi + 120500]
È importante anche ricordare che in assenza di segment override,
tutti gli indirizzamenti che hanno ESP o EBP come registro base
rappresentano degli offset a 32 bit calcolati rispetto al base
address referenziato da SS; in tutti gli altri casi gli
indirizzamenti rappresentano degli offset a 32 bit calcolati rispetto
al base address referenziato da DS.

Le istruzioni per la manipolazione delle stringhe, utilizzano


automaticamente ESI come puntatore sorgente e EDI come puntatore
destinazione; in presenza inoltre dei prefissi REP, REPZ, REPNZ, etc,
viene utilizzato ECX come contatore. Analogamente, il controllo dei
loop da parte delle istruzioni LOOP, LOOPZ, LOOPNZ, etc, si basa sul
contenuto del registro ECX.

Per ottimizzare al massimo i tempi di accesso allo stack da parte


della CPU, è importante tenere ESP e EBP sempre allineati alla DWORD;
a tale proposito è necessario utilizzare le istruzioni PUSH e POP
sempre con operandi a 32 bit. Se si deve inserire nello stack un
valore formato da meno di 32 bit, è necessario estendere il valore
stesso a 32 bit; se ad esempio vogliamo inserire nello stack il
contenuto a 8 bit del registro AL, dobbiamo passare all'istruzione
PUSH l'operando a 32 bit EAX azzerando i suoi 24 bit più
significativi. Se si passa a PUSH un operando immediato, la
dimensione in bit di questo operando viene automaticamente estesa a
32 bit; le regole per l'estensione del bit di segno sono state
esposte nel Capitolo 15 della sezione Assembly Base.

Lo stack frame delle procedure viene gestito attraverso ESP e EBP; i


parametri di una procedura si trovano a spiazzamenti positivi
rispetto a EBP, mentre le variabili locali si trovano a spiazzamenti
negativi rispetto a EBP. Per fare posto alle variabili locali bisogna
sottrarre l'opportuno numero di byte a ESP; le procedure dotate di
stack frame devono rigorosamente preservare il contenuto originale di
ESP e EBP. In modalità protetta a 32 bit, la chiamata di una
procedura comporta da parte della CPU l'inserimento nello stack
dell'indirizzo di ritorno formato dalla sola componente offset a 32
bit; all'interno della procedura, la gestione dello stack frame
comporta da parte del programmatore il salvataggio nello stack del
contenuto originale del registro EBP. Tenendo conto di queste
considerazioni, nel caso delle convenzioni C per il passaggio degli
argomenti possiamo dire che il primo parametro di una procedura si
viene a trovare nello stack a EBP+8; al termine della procedura, il
caller deve ripulire lo stack sommando l'opportuno numero di byte a
ESP. Nel caso invece delle convenzioni Pascal per il passaggio degli
argomenti possiamo dire che l'ultimo parametro di una procedura si
viene a trovare nello stack a EBP+8; la procedura stessa prima di
terminare deve ripulire lo stack passando un opportuno valore
immediato all'istruzione RET. In Win32 come sappiamo vengono
utilizzate le convenzioni miste STDCALL; in questo caso una procedura
trova il suo primo parametro a EBP+8, ed ha anche la responsabilità
di ripulire lo stack.
37
Se si utilizzano le caratteristiche avanzate di MASM e TASM, tutti i
dettagli appena esposti vengono automaticamente gestiti dall’
assembler che provvede anche a preservare il contenuto originale di
ESP e di EBP; naturalmente, l'uso delle caratteristiche avanzate di
MASM e TASM non esime il programmatore dall'avere una conoscenza
approfondita di tutti gli aspetti relativi alla gestione dello stack
frame di una procedura.

Sempre in relazione alle procedure bisogna anche ricordare che negli


ambienti operativi a 32 bit le convenzioni seguite dai linguaggi di
alto livello prevedono che i valori di ritorno a 32 bit vengano
restituiti in EAX e non in DX:AX; lo stesso discorso vale quindi
anche per gli indirizzi NEAR formati dalla sola componente offset a
32 bit.

In modalità protetta bisogna evitare nella maniera più assoluta di


chiamare porzioni di codice scritte espressamente per la modalità
reale; questo discorso vale in particolare per le ISR che gestiscono
i vettori di interruzione presenti nei primi 1024 byte della RAM.
Queste ISR sono rivolte alla modalità reale, e la loro chiamata
quindi manda in crash un programma che gira in modalità protetta; si
deve anche tenere presente che una applicazione Win32 si interfaccia
con il SO in modo completamente differente rispetto a quanto accade
con il DOS. Una applicazione che gira sotto DOS si interfaccia al SO
attraverso una serie di ISR richiamabili con l'istruzione INT; come
viene spiegato nei capitoli successivi, una applicazione Win32 invece
si interfaccia con il SO attraverso la chiamata diretta di una serie
di procedure fornite dallo stesso SO.

Esempio pratico

Vediamo un esempio pratico relativo ad una procedura copiaStringa che


copia una stringa C sorgente in una stringa C destinazione
restituendo alla fine la lunghezza della stringa copiata; questa
procedura utilizza le convenzioni STDCALL compatibili con Win32. Se
non abbiamo a disposizione le caratteristiche avanzate di MASM e
TASM, siamo costretti a gestire personalmente tutti i dettagli
relativi al prolog code e all'epilog code della procedura; in
ambiente Win32 dobbiamo quindi attenerci al modello di memoria FLAT e
alle convenzioni STDCALL. Come già sappiamo, il modello FLAT consiste
in sostanza nell'uso degli indirizzamenti di tipo NEAR a 32 bit; le
convenzioni STDCALL consistono invece nel passare gli argomenti in
stile C e nel ripulire lo stack in stile Pascal.
In base a queste considerazioni, la procedura copiaStringa scritta in
Assembly classico assume il seguente aspetto:
; int copiaStringa(char *strTo, char *strFrom);

copiaStringa proc

strTo equ [ebp+8] ; parametro strTo


strFrom equ [ebp+12] ; parametro strFrom
strCount equ [ebp-4] ; var. locale strCount

push ebp ; preserva ebp

38
mov ebp, esp ; ss:ebp = ss:esp
sub esp, 4 ; spazio per strCount

mov dword ptr strCount, -1 ; strCount = -1


mov esi, strFrom ; esi = sorgente
mov edi, strTo ; edi = destinazione
strCopyLoop:
mov al, [esi] ; copia da sorgente
mov [edi], al ; a destinazione
inc esi ; incremento puntatore
inc edi ; incremento puntatore
inc dword ptr strCount ; incremento contatore
test al, al ; fine stringa C ?
jnz strCopyLoop ; controllo loop

mov eax, strCount ; valore di ritorno

mov esp, ebp ; ripristina esp


pop ebp ; ripristina ebp
ret 8 ; pulizia stack e return

copiaStringa endp
Prima di tutto osserviamo che questa procedura riceve due argomenti
di tipo puntatore a stringa, e cioè due indirizzi NEAR a 32 bit;
siccome gli argomenti vengono inseriti nello stack a partire
dall'ultimo, incontreremo il parametro strTo a EBP+8 e il parametro
strFrom a EBP+12, cioè 4 byte più avanti. La variabile locale
strCount a 32 bit viene utilizzata come contatore e si trova a EBP-4;
naturalmente sarebbe meglio utilizzare un registro, ma la procedura
copiaStringa ha solamente uno scopo didattico e quindi è volutamente
non ottimizzata.
Con le prime tre istruzioni copiaStringa preserva il contenuto di
EBP, copia ESP in EBP e sottrae 4 byte a ESP per fare posto a
strCount nello stack; come si può notare, si tratta dello stesso
procedimento già illustrato nella sezione Assembly Base, adattato in
questo caso all'ambiente operativo a 32 bit.
La variabile locale strCount viene inizializzata con -1 per tener
conto dello zero finale della stringa C che non deve essere
conteggiato; il registro ESI contiene l'indirizzo della stringa
sorgente, mentre il registro EDI contiene l'indirizzo della stringa
destinazione. All'interno del loop possiamo notare che tutto si
svolge nel modo che già conosciamo; l'unica differenza è
rappresentata dall'uso dei puntatori a 32 bit.
Al termine del loop, il contenuto di strCount viene copiato in EAX e
rappresenta il valore di ritorno destinato al caller; successivamente
incontriamo le istruzioni che ripristinano ESP e EBP. Prima di
terminare, la procedura copiaStringa nel rispetto delle convenzioni
STDCALL deve ripulire lo stack; siccome la procedura ha ricevuto due
argomenti da 4 byte ciascuno, la pulizia dello stack consiste nel
passare il valore immediato 8 all'istruzione RET.

La fase di chiamata di copiaStringa deve ugualmente adattarsi al


modello di memoria FLAT e alle convenzioni STDCALL; in Win32, in
presenza dell'unico segmento di codice _TEXT, la chiamata di una
procedura può essere o diretta intra segmento (salto ad una etichetta

39
NEAR), o indiretta intra segmento (salto ad un indirizzo NEAR a 32
bit).
Supponendo ora di aver definito nel blocco dati del programma le due
stringhe strSource e strDest (con strDest che deve essere in grado di
contenere strSource), la chiamata di copiaStringa si svolge in questo
modo:
push offset strSource
push offset strDest
call copiaStringa
Come si può notare, gli argomenti vengono inseriti nello stack
partire dall'ultimo; il valore immediato restituito dall'operatore
OFFSET è naturalmente l'indirizzo 32 bit del relativo operando.
Appena copiaStringa restituisce il controllo al caller, il valore di
ritorno della procedura è disponibile in EAX; possiamo anche notare
che la pulizia dello stack viene delegata alla procedura stessa.

Vediamo ora quello che succede in presenza delle caratteristiche


avanzate di MASM e TASM; prima di tutto, all'inizio del programma
dobbiamo inserire le seguenti direttive:
.386 ; set di istruzioni a 32 bit
.MODEL FLAT, STDCALL ; memory model & calling conventions
Come è stato detto in precedenza, la direttiva .386 rappresenta in
termini di CPU il requisito minimo per programmare in Win32; in
presenza di questa direttiva e del parametro FLAT l’ assembler
capisce che deve lavorare in un ambiente operativo a 32 bit.
Il passo successivo consiste nella dichiarazione del prototipo di
copiaStringa; questa dichiarazione assume il seguente aspetto:
copiaStringa PROTO :DWORD, :DWORD
All'interno del blocco _TEXT, la definizione di copiaStringa è la
seguente:
; int copiaStringa(char *strTo, char *strFrom);

copiaStringa proc strTo :DWORD, strFrom :DWORD

LOCAL strCount :DWORD ; contatore

mov dword ptr strCount, -1 ; strCount = -1


mov esi, strFrom ; esi = sorgente
mov edi, strTo ; edi = destinazione
strCopyLoop:
mov al, [esi] ; copia da sorgente
mov [edi], al ; a destinazione
inc esi ; incremento puntatore
inc edi ; incremento puntatore
inc dword ptr strCount ; incremento contatore
test al, al ; fine stringa C ?
jnz strCopyLoop ; controllo loop

mov eax, strCount ; valore di ritorno

ret ; return

copiaStringa endp
Come si può notare, tutta la gestione dello stack frame, compresa la
pulizia dello stack, viene delegata all’ assembler; le modalità che

40
regolano questa gestione vengono stabilite dai parametri che abbiamo
specificato nella direttiva .MODEL.
A questo punto, possiamo procedere con la chiamata di copiaStringa;
grazie alla sintassi avanzata di TASM questa chiamata diventa:
call copiaStringa, strDest, strSrc
Con la sintassi avanzata di MASM la chiamata invece diventa:
invoke copiaStringa, strDest, strSrc
Quando l’ assembler incontra questa chiamata, segue un comportamento
determinato dai parametri passati alla direttiva .MODEL; in questo
modo l’ assembler è in grado di sapere in particolare come vengono
inseriti gli argomenti nello stack e a chi spetta la pulizia finale
dello stesso stack. Il parametro STDCALL fa in modo che l’ assembler
inserisca nello stack prima l'argomento strSource e poi l'argomento
strDest; a questo punto, grazie al parametro FLAT l’ assembler
capisce che la chiamata a copiaStringa è di tipo diretto
intrasegmento (in sostanza, copiaStringa è un'etichetta NEAR definita
nel blocco _TEXT e rappresentata quindi da un offset a 32 bit).
Sempre in base al parametro STDCALL, l’ assembler genera il codice
macchina necessario per la pulizia dello stack in stile Pascal; come
al solito, quando il controllo viene restituito al caller, il
registro EAX contiene il valore di ritorno della procedura
copiaStringa.

41
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 più
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 è 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 e TASM 5.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 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 è 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:
42
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
;--------------------------------------------------------;
; File primo.asm ;
; Il primo programma Assembly per Win32 (versione MASM). ;
; Mostra un messaggio attraverso la finestra predefinita ;
; MessageBox. ;
;--------------------------------------------------------;

; ################# direttive per l’ assembler #################

.386 ; set di istruzioni a 32 bit


.MODEL FLAT, STDCALL ; memory model & calling conventions
OPTION CASEMAP: NONE ; case sensitive on symbols

; ############### dichiarazione tipi e costanti ###############

NULL = 00000000h ; valore nullo


MB_OK = 00000000h ; codice bottone 'OK'
MB_ICONINFORMATION = 00000040h ; codice icona 'ICONINFORMATION'

; ################# prototipi delle procedure #################

MessageBoxA PROTO :DWORD, :DWORD, :DWORD, :DWORD


ExitProcess PROTO :DWORD

; #################### inclusione librerie ####################

INCLUDELIB ..\lib\user32.lib ; libreria servizi GUI


INCLUDELIB ..\lib\kernel32.lib ; libreria servizi kernel

; ################ segmento dati inizializzati #################

_DATA SEGMENT DWORD PUBLIC USE32 'DATÀ

strTitolo db 'Win32 Assembly', 0


strMessaggio db 'Il primo programma Assembly per Win32', 0
43
_DATA ENDS

; ##################### segmento di codice #####################

_TEXT SEGMENT DWORD PUBLIC USE32 'CODÈ

start: ; entry point del programma

invoke MessageBoxA, NULL, offset strMessaggio, offset strTitolo, MB_OK OR MB_ICONINFORMATION

invoke ExitProcess, 0 ; termina con exit code = 0

_TEXT ENDS

; ##############################################################

END start ; fine del modulo


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 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 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 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
44
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 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 è
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
45
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 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 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).
46
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 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 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
47
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:\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 è 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
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 verrà chiamato
PRIMO.LST.

4.4 La fase di assembling con TASM.


48
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 è a pagamento per cui non è detto che
tutti possano disporre della versione più recente; se si ha a
disposizione una vecchia versione a 32 bit del TASM e non si vuole
passare al MASM, è ugualmente possibile sviluppare applicazioni per
Win32, anche se bisogna sopportare qualche sacrificio in più. Il
problema è 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
comodità 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)
;--------------------------------------------------------;
; File primo.asm ;
; Il primo programma Assembly per Win32 (versione TASM). ;
; Questa versione assembla anche con TASM 2.x e 3.x. ;
; Mostra un messaggio attraverso la finestra predefinita ;
; MessageBox. ;
;--------------------------------------------------------;

; ################# direttive per l’ assembler #################

.386 ; set di istruzioni a 32 bit

; ############### dichiarazione tipi e costanti ###############

NULL = 00000000h ; valore nullo


MB_OK = 00000000h ; codice bottone 'OK'
MB_ICONINFORMATION = 00000040h ; codice icona 'ICONINFORMATION'

; ################# prototipi delle procedure #################

49
EXTRN MessageBoxA: PROC
EXTRN ExitProcess: PROC

; #################### inclusione librerie ####################

INCLUDELIB ..\lib\import32.lib ; libreria generale

; ################ segmento dati inizializzati #################

_DATA SEGMENT DWORD PUBLIC USE32 'DATÀ

strTitolo db 'Win32 Assembly', 0


strMessaggio db 'Il primo programma Assembly per Win32', 0

_DATA ENDS

; ##################### segmento di codice #####################

_TEXT SEGMENT DWORD PUBLIC USE32 'CODÈ

ASSUME cs: _TEXT, ds: _DATA ; assegnamento segmenti

start: ; entry point del programma

push MB_OK OR MB_ICONINFORMATION


push offset strTitolo
push offset strMessaggio
push NULL
call MessageBoxA

push 0
call ExitProcess ; termina con exit code = 0

_TEXT ENDS

; ##############################################################

END start ; fine del modulo

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
Turbo Assembler Version 5.0 06-07-02 15:52:12 Page 1
primo.ASM

1
2 ;--------------------------------------------------------;
3 ; File primo.asm ;
4 ; Il primo programma Assembly per Win32 (versione TASM).
;

50
5 ; Mostra un messaggio attraverso la finestra predefinita ;
6 ; MessageBox.
7 ;--------------------------------------------------------;
8
9 ; ################# direttive per l’ assembler
#################
10
11 .386 ; set di
istruzioni a 32 bit
12 00000000 .MODEL FLAT, STDCALL ; memory model & calling
conventions
13 OPTION CASEMAP: NONE ; case
sensitive on symbols
14
15 ; ############### dichiarazione tipi e costanti
###############
16
17 =0000 NULL = 00000000h ; valore nullo
18 =0000 MB_OK = 00000000h ; codice
bottone 'OK'
19 =0040 MB_ICONINFORMATION = 00000040h ; codice icona
'ICONINFORMATION'
20
21 ; ################# prototipi delle procedure
#################
22
23 MessageBoxA PROTO :DWORD, :DWORD,
:DWORD, :DWORD
24 ExitProcess PROTO :DWORD
25
26 ; #################### inclusione librerie
####################
27
28 INCLUDELIB ..\lib\import32.lib ; libreria
generale
29
30 ; ################ segmento dati inizializzati
#################
31
32 00000000 _DATA SEGMENT DWORD PUBLIC USE32 'DATÀ
33
34 00000000 57 69 6E 33 32 20 41+ strTitolo db 'Win32 Assembly', 0
35 73 73 65 6D 62 6C 79+
36 00
37 0000000F 49 6C 20 70 72 69 6D+ strMessaggio db 'Il primo programma
Assembly per Win32', 0
38 6F 20 70 72 6F 67 72+
39 61 6D 6D 61 20 41 73+
40 73 65 6D 62 6C 79 20+
41 70 65 72 20 57 69 6E+
42 33 32 00
43
44 00000035 _DATA ENDS
45
46 ; ##################### segmento di codice
#####################
47
48 00000000 _TEXT SEGMENT DWORD PUBLIC USE32 'CODÈ
49
50 00000000 start: ; entry point del
programma

51
51
52 call MessageBoxA, NULL, offset strMessaggio, offset
strTitolo, MB_OK OR +
53 MB_ICONINFORMATION
1 54 00000000 6A 40 PUSH MB_OK OR
MB_ICONINFORMATION
1 55 00000002 68 00000000r PUSH offset strTitolo
1 56 00000007 68 0000000Fr PUSH offset strMessaggio
1 57 0000000C 6A 00 PUSH NULL
1 58 0000000E E8 00000000e CALL MessageBoxA
59
60 call ExitProcess, 0 ; termina con exit code
=0
1 61 00000013 6A 00 PUSH 0
1 62 00000015 E8 00000000e CALL ExitProcess
63
64 0000001A _TEXT ENDS
65
66 ;
##############################################################
67
68 END start ; fine del
modulo

Symbol Table

Symbol Name Type Value

??date Text "06-07-02"


??filename Text "primo "
??time Text "15:52:12"
??version Number 0500
@32Bit Text 1
@CodeSize Text 0
@Cpu Text 0F0FH
@DataSize Text 0
@FileName Text primo
@Interface Text 003h
@Model Text 1
@Type_ExitProcess Proctyp
@Type_MessageBoxA Proctyp
@WordSize Text 4
@code Text FLAT
@curseg Text _TEXT
@data Text FLAT
@stack Text FLAT
ExitProcess Struct ----:---- Extern @Type_ExitProcess
MB_ICONINFORMATION Number 0040
MB_OK Number 0000
MessageBoxA Struct ----:---- Extern @Type_MessageBoxA
NULL Number 0000
start Near32 FLAT:0000
strMessaggio Byte FLAT:000F
strTitolo Byte FLAT:0000

Groups & Segments Bit Size Align Combine Class

DGROUP Group
_DATA 32 0035 Dword Public DATA
FLAT Group
_TEXT 32 001A Dword Public CODE

52
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 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 segmento _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 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
53
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 "super segmento"
virtuale da 4 Gb. All'interno di questo super segmento 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 super segmento (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.6 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 1, con MASM e TASM queste librerie

54
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 è 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 (cioè su Windows 95 o superiore); la
versione di Windows che si sta utilizzando può 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 è in grado di
55
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 gratuitamente 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 più 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 è 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 è
molto più 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
primo

Timestamp is 3d03c4b6 (Sun Jun 09 23:12:22 2002)

Preferred load address is 00400000

Start Length Name Class


0001:00000000 00000026H .text CODE
0002:00000000 00000010H .idata$5 DATA
0002:00000010 00000028H .idata$2 DATA
0002:00000038 00000014H .idata$3 DATA
0002:0000004c 00000010H .idata$4 DATA
0002:0000005c 00000036H .idata$6 DATA
0002:00000092 00000000H .edata DATA
0003:00000000 00000035H .data DATA

Address Publics by Value Rva+Base Lib:Object

0001:00000000 _start 00401000 primo.obj


0001:0000001a _MessageBoxA@16 0040101a f user32:USER32.dll
0001:00000020 _ExitProcess@4 00401020 f kernel32:KERNEL32.dll
0002:00000000 __imp__ExitProcess@4 00402000 kernel32:KERNEL32.dll
0002:00000004 \177KERNEL32_NULL_THUNK_DATA 00402004 kernel32:KERNEL32.dll
0002:00000008 __imp__MessageBoxA@16 00402008 user32:USER32.dll
0002:0000000c \177USER32_NULL_THUNK_DATA 0040200c user32:USER32.dll
0002:00000010 __IMPORT_DESCRIPTOR_USER32 00402010 user32:USER32.dll
0002:00000024 __IMPORT_DESCRIPTOR_KERNEL32 00402024 kernel32:KERNEL32.dll
0002:00000038 __NULL_IMPORT_DESCRIPTOR 00402038 user32:USER32.dll

56
entry point at 0001:00000000

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 vediamo che il
map file prodotto dal MASM stabilisce che l'applicazione verrà
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 è 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
57
informazioni sul selettore nullo, vedere la sezione Modalità
Protetta.
Il registro ESP (Extended Stack Pointer) viene fatto puntare
all'indirizzo più 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 Gb) 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 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 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
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 è
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.EXÈ, 0
(si possono usare indifferentemente le maiuscole o le minuscole). A
questo punto possiamo effettuare la chiamata:
call 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
58
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 Gb e 3 Gb; 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 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 è 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 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
59
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:
call 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:
call 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:
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 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 TASM bisogna
scrivere:
wsprintfA PROTO C :DWORD, :DWORD :?
mentre con il MASM bisogna scrivere:
wsprintfA PROTO C :DWORD, :DWORD, :VARARG
Alternativamente, è sempre possibile l'utilizzo della dichiarazione
in vecchio stile Assembly:
60
EXTRN wsprintfA: PROC
In questo modo però 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 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:
Indirizzo di partenza del modulo REGVAL.EXE = 00400000h

61
Contenuto dei registri di segmento:
CS = 0167h
DS = 016Fh
ES = 016Fh
SS = 016Fh
FS = 139Fh
GS = 0000h
Contenuto iniziale di ESP = 0063FE3Ch
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 Gb, possiamo dire che le chiamate alle
procedure saranno tutte di tipo diretto o indiretto intra segmento;

62
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.11 I makefile.

La fase di assembling e di linking di un'applicazione Assembly può


essere automatizzata con l'ausilio di appositi strumenti; uno
strumento largamente utilizzato con il TASM è 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
###############################################
# Make file per l'applicazione primo #
# syntax: make -B -fprimo.mak #
###############################################

# macro per gli strumenti utilizzati

ASM = ..\bin\tasm32
LNK = ..\bin\tlink32

# macro per i files

NAME = primo
OBJS = $(NAME).obj

# macro per le opzioni dell’ assembler e del linker

ASMOPT = /ml /zn /l


LNKOPT = /c /Tpe /aa /V4.0 /s

############# generazione exe file ############

$(NAME).EXE: $(OBJS)
$(LNK) $(LNKOPT) $(OBJS), $(NAME)

########### generazione object file ###########

$(NAME).OBJ: $(NAME).ASM
$(ASM) $(ASMOPT) $(NAME).ASM

63
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 già 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 può 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 è 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
64
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:

1 Aprire la finestra Pannello di controllo


2 Selezionare la voce Prestazioni e manutenzione
3 Selezionare la voce Sistema
4 Nella finestra Proprietà 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 è fatto.
Per la corretta visualizzazione degli esempi proposti nella sezione
Win32 Assembly, 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:\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
65
punto partono le fasi di assembling 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 è 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, è 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 6 possiamo vedere l'equivalente di PRIMO.ASM scritto però in
linguaggio C.
Figura 6 - PRIMO.C
// file primo.c

#include < windows.h >

#pragma argsused

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpszCmdParam,


66
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 è
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 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 è 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.

67
Win32 Assembly

Capitolo 5: La Main Window.


Nel precedente capitolo sono state presentate alcune piccolissime
applicazioni per Win32 come ad esempio PRIMO.ASM; l'estrema
semplicità di un programma come PRIMO.ASM è legata principalmente al
fatto che abbiamo utilizzato un servizio come la finestra dei
messaggi (message box), interamente gestito da Win32. In sostanza,
tutta la reale complessità del programma è nascosta all'interno della
procedura MessageBox; questa procedura esegue un lavoro piuttosto
impegnativo che consiste nell'inizializzare, registrare, attivare e
gestire una finestra chiamata appunto finestra dei messaggi. Nel caso
più generale, quando si realizza una applicazione standard per Win32,
tutto questo lavoro spetta invece al programmatore; in particolare,
in questo capitolo viene esaminato il procedimento che bisogna
seguire per realizzare una applicazione per Win32 dotata di una
propria finestra. Come è stato già spiegato nei precedenti capitoli,
ogni applicazione standard per Win32 è dotata di una finestra
principale chiamata appunto main window; la main window rappresenta
simbolicamente un vero e proprio schermo virtuale attraverso il quale
un'applicazione interagisce con l'utente. D'altra parte, lo scopo
fondamentale delle interfacce grafiche (GUI) è proprio quello di
realizzare un tipo di interazione visuale tra computer e utente; per
raggiungere questo obiettivo si utilizzano appunto le finestre che al
loro interno possono contenere diversi oggetti come le icone, i
bottoni, i menu, le immagini, etc. In commercio esistono
numerosissimi libri che illustrano in dettaglio questi concetti;
nella sezione Win32 Assembly ci occuperemo invece degli aspetti
pratici che riguardano noi programmatori Assembly.

Nella realizzazione di un'applicazione standard per Win32 un


programmatore Assembly deve gestire principalmente le seguenti fasi:

1) Inizializzazione della main window.


2) Registrazione della main window.
3) Attivazione della main window.
4) Gestione della main window.

La fase più importante è rappresentata sicuramente dalla gestione


della main window; questa fase infatti ci permette di capire il
principio fondamentale su cui si basano le GUI dei SO come Windows,
OS/2, Unix/Linux, MacOS, etc.

5.1 I messaggi di Windows.

68
All'interno di Win32, qualunque evento viene convertito in un codice
numerico chiamato messaggio; lo spostamento del mouse, la pressione o
il rilascio di un pulsante del mouse, la pressione o il rilascio di
un tasto della tastiera, il trascinamento di una finestra, la
chiusura di una finestra, etc, sono tutti eventi che Win32 converte
in appositi messaggi. Non appena un'applicazione Win32 viene caricata
in memoria per l'esecuzione, inizia da parte del SO un vero e proprio
bombardamento di messaggi destinato all'applicazione stessa; la
gestione della main window da parte del programmatore consiste
proprio nel decidere quali messaggi accettare e quali messaggi
restituire invece al mittente (e cioè al SO).
La ricezione e l'elaborazione dei messaggi destinati ad una finestra
avviene all'interno di un'apposita procedura chiamata window
procedure (procedura di finestra); la window procedure ha
un'importanza enorme in quanto rappresenta di fatto il cuore di
un'applicazione Win32. Un'applicazione Win32 può essere dotata di una
o più finestre; una di esse rappresenta la finestra principale (main
window), mentre le altre svolgono il ruolo di finestre secondarie (o
finestre figlie). Qualsiasi finestra, principale o secondaria che
sia, deve essere dotata di una propria window procedure; la window
procedure di una finestra deve essere notificata al SO durante la
fase di registrazione della finestra stessa. Il SO si serve proprio
delle window procedure per inviare i messaggi alle corrispondenti
finestre; le procedure di questo tipo, e cioè le procedure che
vengono utilizzate dal SO per inviare indirettamente messaggi ad una
applicazione, vengono chiamate procedure di callback. Il
programmatore Assembly è libero di assegnare qualsiasi nome ad una
window procedure; tradizionalmente la window procedure associata alla
main window viene chiamata WndProc.

Nota importante.
I manuali di programmazione per win32, raccomandano che tutte le
procedure di callback preservino il contenuto originario dei registri
EBX, ESI e EDI; quindi, se una procedura di callback deve utilizzare
questi registri, deve prima salvare il loro contenuto originario
nello stack, in modo da poter procedere successivamente alla fase di
ripristino.

Cominciamo quindi l'analisi dettagliata delle quattro fasi che ci


portano a realizzare una applicazione standard per Win32; prima di
tutto procediamo al download del codice sorgente relativo all'esempio
mostrato in questo capitolo:
Download di tutti gli esempi del capitolo (versione MASM)
Il file in formato ZIP deve essere scompattato come al solito nella
cartella di lavoro win32asm; a questo punto lanciamo il nostro editor
preferito e apriamo il file MAINWIN.ASM che contiene il programma di
esempio che stiamo per analizzare. Si può notare che il codice
sorgente ha dimensioni ben più consistenti rispetto a quelle degli
esempi mostrati nel precedente capitolo; si tenga presente in ogni
caso che MAINWIN.ASM contiene la struttura generale di
un'applicazione standard per Win32, per cui il 100% di questo codice
può essere riciclato e impiegato per scrivere altre applicazioni.

5.2 La procedura WinMain.


69
Analizzando il listato di MAINWIN.ASM si nota che le fasi di
inizializzazione, registrazione, attivazione e gestione della main
window sono state inserite all'interno di una procedura chiamata
WinMain; in questo modo stiamo simulando la classica struttura di una
applicazione Win32 scritta in linguaggio C.
Un programma in linguaggio C per l'ambiente DOS o per la console di
Unix/Linux, deve contenere una procedura che deve chiamarsi
obbligatoriamente main (in C le procedure vengono chiamate funzioni);
questa procedura ha un ruolo molto importante in quanto rappresenta
la procedura principale del programma e cioè il punto in cui il
programmatore riceve il controllo. Il vero entry point del programma
si trova invece in un apposito modulo gestito direttamente dal
compilatore; nel caso ad esempio del Borland C/C++ 3.1, questo modulo
si chiama C0.ASM ed è scritto chiaramente in Assembly. All'interno di
C0.ASM troviamo l'entry point del programma, chiamato STARTX più una
serie di importanti inizializzazioni che nel loro insieme formano lo
startup code; tra le varie inizializzazioni si possono citare la
creazione del segmento di stack del programma e l'individuazione di
eventuali parametri passati al programma dalla linea di comando
(command line). Supponiamo ad esempio di avere un programma C
chiamato TESTC.EXE; digitando:
TESTC param1 param2 param3 param4
e premendo [Invio], stiamo avviando l'esecuzione di TESTC.EXE e
stiamo passando al programma i quattro parametri param1, param2,
param3, param4. La stringa dei parametri viene inserita nel campo
CommandLineParameters del PSP del programma (vedere il Capitolo 13
della sezione Assembly Base); questo campo conterrà quindi la stringa
ASCII:
' param1 param2 param3 param4', 0Dh
Il campo CommandLineParmLength del PSP contiene invece la lunghezza
in byte della stringa dei parametri (nel nostro caso 28); il
compilatore C elabora questi due campi per individuare le
informazioni da inviare alla procedura main. Viene creata una
variabile (tipo intero) chiamata arguments counter (contatore del
numero di argomenti) contenente il numero di argomenti passati al
programma dalla linea di comando (nel nostro caso 4); viene poi
creata una seconda variabile (tipo vettore di puntatori a stringhe)
chiamata arguments vector (vettore degli argomenti) contenente una
sequenza di puntatori ai parametri. Una volta che la fase di
inizializzazione è terminata, dal modulo C0.ASM viene chiamata la
procedura main; il prototipo C di questa procedura è:
int main(int argc, char *argv[])
In sostanza il compilatore utilizza la procedura main per cedere il
controllo al programmatore; nel caso del nostro esempio (TESTC.EXE),
il parametro argc contiene il valore 4 (numero di parametri passati a
TESTC.EXE), mentre il parametro argv è un vettore di 4 puntatori a
stringhe, con argv[0] che punta a 'param1', argv[1] che punta a
'param2', argv[2] che punta a 'param3', argv[3] che punta a 'param4'
e argv[4] che per convenzione deve contenere il valore NULL. Una
volta che main ha terminato il suo lavoro cede nuovamente il
controllo al modulo C0.ASM; all'interno di questo modulo vengono
effettuati tutti i necessari ripristini dei valori e infine viene
terminato il programma con la conseguente restituzione del controllo
al SO (nel caso del DOS viene chiamato come al solito il servizio 4Ch
70
dell'INT 21h).

Un programma in linguaggio C per l'ambiente Win32 deve contenere una


procedura che deve chiamarsi obbligatoriamente WinMain; la procedura
WinMain ricopre in Win32 lo stesso ruolo di procedura principale
ricoperto da main in ambiente DOS. Anche in questo caso quindi, il
vero entry point del programma si trova in un altro modulo gestito
direttamente dal compilatore; nel caso ad esempio del Borland C/C++
Compiler 5.x, questo modulo si chiama C0W32. All'interno di questo
modulo vengono effettuate come al solito le necessarie
inizializzazioni che prevedono in particolare l'acquisizione dei
parametri da passare alla procedura WinMain; terminata la fase di
inizializzazione il modulo C0W32 chiama la procedura WinMain e cede
il controllo al programmatore. Quando WinMain ha esaurito il suo
compito restituisce il controllo al modulo C0W32; questo modulo, dopo
aver effettuato i vari ripristini dei valori, richiede a Win32 la
terminazione del nostro programma.

Un programma in linguaggio Assembly per l'ambiente Win32 non è


obbligato a seguire il procedimento appena descritto; in Assembly
siamo liberi di seguire lo stile che più si adatta ai nostri gusti.
Naturalmente però siamo tenuti a rispettare le regole che definiscono
la struttura di un'applicazione Win32 standard; questa struttura può
essere schematizzata attraverso tre blocchi fondamentali:

1) Il primo bocco contiene la cosiddetta fase di startup


dell'applicazione; in questa fase dobbiamo reperire in particolare
alcune informazioni molto importanti.
2) Il secondo blocco contiene le quattro fasi che portano alla
creazione della main window del programma; come già sappiamo queste
quattro fasi consistono nell'inizializzazione, registrazione,
attivazione e gestione della main window.
3) Il terzo blocco contiene la fase di ripristino dei valori
dell'applicazione; in questa fase in particolare dobbiamo richiedere
al SO la terminazione del nostro programma.

In base a queste considerazioni risulta evidente che un'applicazione


Assembly per Win32 contiene il vero entry point e il vero exit point
dell'applicazione stessa; questo significa che le fasi di
inizializzazione e ripristino del programma spettano a noi.
Quando si utilizza un linguaggio criptico come l'Assembly, è molto
importante seguire uno stile di programmazione chiaro e ordinato; per
raggiungere questo obiettivo si possono utilizzare ad esempio le
procedure, che hanno il pregio di rendere evidente la suddivisione
del nostro programma in tanti blocchi funzionali. In questo modo tra
l'altro, si rende molto più semplice la verifica e la manutenzione
del codice sorgente; proprio per questo motivo, tutti gli esempi
presentati nella sezione Win32 Assembly, seguendo le convenzioni dei
linguaggi di alto livello utilizzano una procedura che ricopre il
ruolo di procedura principale dell'applicazione. In particolare, in
base alle convenzioni del linguaggio C utilizzeremo proprio la
procedura WinMain; naturalmente siamo liberi anche di adottare le
convenzioni seguite da altri linguaggi come Visual Basic, Delphi,
Java, etc., e siamo anche liberi di non seguire nessuna convenzione.
71
A questo punto possiamo procedere con l'analisi delle caratteristiche
della procedura WinMain; consultando il Win32 Programmer's Reference
possiamo notare che questa procedura viene dichiarata con il seguente
prototipo C:
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine,
int nCmdShow);
Il termine WINAPI indica le convenzioni seguite da una procedura per
il passaggio dei parametri e per la pulizia dello stack, ed è
perfettamente equivalente a STDCALL; infatti nel file windows.inc è
presente una dichiarazione del tipo:
WINAPI equ STDCALL
Come si può notare, la procedura WinMain termina restituendo un
valore di tipo int (intero con segno a 32 bit); questo valore è
l'exit code che verrà restituito al SO dopo la terminazione della
nostra applicazione. Come è stato detto nei precedenti capitoli,
l'exit code viene totalmente ignorato da Win32; in ogni caso, il
Win32 Programmer's Reference, a proposito dell'exit code, fornisce
alcuni utili consigli che vengono illustrati più avanti.
Passiamo ora all'analisi dettagliata dei parametri richiesti da
WinMain.

hInst è l'handle che identifica l'istanza corrente della nostra


applicazione, e cioè il codice numerico che il SO assegna alla nostra
applicazione al momento del caricamento in memoria per la fase di
esecuzione; come è stato spiegato nel precedente capitolo, in Win32
questo codice vale sempre 00400000h e rappresenta l'offset a 32 bit
da cui parte l'applicazione in memoria. Si tratta in sostanza del
base address di un'applicazione all'interno del segmento virtuale da
4 Gbyte.

hPrevInst è l'handle che identifica eventuali altre istanze della


nostra applicazione già in fase di esecuzione; come sappiamo, questa
informazione è importante solo in ambiente Win16. In Win32 il
concetto di istanze multiple di una applicazione non ha nessun
significato; se eseguiamo contemporaneamente 10 copie di
un'applicazione, otteniamo 10 applicazioni che girano in 10 macchine
virtuali indipendenti tra loro. Ogni applicazione è convinta quindi
di essere l'unica in esecuzione e di avere tutto il computer a sua
disposizione; per questo motivo in Win32 il parametro hPrevInst di
WinMain deve valere sempre NULL.

lpCmdLine è un puntatore FAR ad una stringa C contenente gli


eventuali parametri passati alla nostra applicazione dalla command
line; come già sappiamo in Win32 la distinzione tra puntatori NEAR o
FAR non ha significato. In sostanza il tipo LPSTR (long pointer to
string) rappresenta un offset a 32 bit, e cioè un dato di tipo DWORD;
il parametro lpCmdLine non viene quasi mai utilizzato in quanto è
rarissimo che un'applicazione Win32 riceva l'input dalla linea di
comando.

nCmdShow è un codice numerico a 32 bit che ci permette di specificare


lo stato iniziale della main window della nostra applicazione; i
codici più utilizzati vengono indicati con i nomi simbolici
SW_SHOWMAXIMIZED, SW_SHOWMINIMIZED e SW_SHOWNORMAL dichiarati come al
72
solito in windows.inc (SW = Show Window). Se si utilizza
SW_SHOWMAXIMIZED la nostra applicazione parte con la main window
massimizzata; se si utilizza SW_SHOWMINIMIZED la nostra applicazione
parte con la main window minimizzata (cioè ridotta ad icona nella
barra delle applicazioni). Nel nostro esempio utilizziamo
SW_SHOWNORMAL che lascia a Win32 il compito di stabilire le
dimensioni iniziali e la posizione iniziale della main window del
programma; per conoscere gli altri codici si consiglia di consultare
il Win32 Programmer's Reference.

In base a tutte le considerazioni appena esposte, il prototipo di


WinMain puo essere scritto come:
WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD
o se preferiamo anche come:
WinMain PROTO :HINSTANCE, :HINSTANCE, :LPSTR, :SDWORD

Appare evidente inoltre che subito dopo l'entry point (start) del
nostro programma, dobbiamo effettuare le necessarie inizializzazioni
che consistono nell'acquisizione delle informazioni da passare a
WinMain; le due informazioni da reperire sono l'handle
dell'applicazione e l'indirizzo della stringa contenente la command
line.
Per reperire l'handle dell'applicazione ci serviamo della procedura
GetModuleHandleA già descritta nel precedente capitolo; questa
procedura restituisce in EAX l'informazione richiesta. Il valore
restituito in EAX viene salvato nella variabile hInstance definita
nel segmento dati non inizializzati del nostro programma.
Per reperire l'indirizzo della command line ci serviamo invece della
procedura GetCommandLineA definita nella libreria KERNEL32; il
prototipo di questa procedura è:
LPTSTR GetCommandLine(void);
Come si può notare, GetCommandLineA non richiede nessun parametro e
quando termina restituisce in EAX l'offset a 32 bit della command
line; questa informazione viene salvata nella nella variabile
commandLine definita nel segmento dati non inizializzati del nostro
programma.

A questo punto abbiamo a disposizione tutte le informazioni


necessarie per poter chiamare WinMain; la parte iniziale del nostro
programma assume quindi la seguente struttura (in versione TASM):
call GetModuleHandleA, NULL ; trova l'handle del modulo MAINWIN.EXE
mov hInstance, eax ; e lo salva in hInstance

call GetCommandLineA ; trova l'indirizzo della command line


mov commandLine, eax ; e lo salva in commandLine

call WinMain, hInstance, NULL, commandLine, SW_SHOWNORMAL

call ExitProcess, eax ; exit code restituito da WinMain


Come si può notare, grazie all'uso di WinMain il codice appare
estremamente chiaro e ordinato; i tre blocchi evidenti sono:

a) acquisizione di hInstance e commandLine;


b) chiamata di WinMain;
c) terminazione dell'applicazione.
73
L'applicazione viene terminata come al solito da ExitProcess che
utilizza il contenuto di EAX come exit code; naturalmente EAX in quel
preciso istante contiene il valore di ritorno restituito da WinMain.
Passiamo ora all'analisi del codice racchiuso dalla procedura
WinMain; come è stato detto in precedenza, questo codice ci permette
di creare la Main Window della nostra applicazione.

5.3 Inizializzazione della Main Window.

Il primo passo da compiere consiste nell'inizializzazione della Main


Window; questa fase consiste in una vera e propria progettazione
della finestra principale della nostra applicazione. Progettare la
Main Window significa definire le caratteristiche generali della
finestra, come ad esempio, il colore di sfondo, l'icona associata, la
window procedure, etc; tutte queste informazioni devono essere
passate al SO attraverso un'apposita struttura dichiarata con il nome
WNDCLASSEX. Per analizzare questa struttura, apriamo con un editor
l'include file principale windows.inc (o win32.inc), e chiediamo
all'editor stesso di cercare il nome WNDCLASSEX; ad esempio, con
QEDITOR.EXE dobbiamo selezionare il menu Edit + Find Text.
Informazioni dettagliate su questa struttura possono essere reperite
nel Win32 Programmer's Reference; la struttura WNDCLASSEX assume in
Assembly il seguente aspetto:
WNDCLASSEX STRUC

cbSize UINT ? ; dimensione struttura in byte


style UINT ? ; stile della classe finestra
lpfnWndProc WNDPROC ? ; puntatore alla window procedure
cbClsExtra LONG ? ; extra byte per la classe finestra
cbWndExtra LONG ? ; extra byte per la finestra
hInstance HANDLE ? ; handle dell'applicazione
hIcon HICON ? ; handle dell'icona;
hCursor HCURSOR ? ; handle del cursore del mouse
hbrBackground HBRUSH ? ; colore di sfondo
lpszMenuName LPCTSTR ? ; puntatore alla risorsa menu
lpszClassName LPCTSTR ? ; puntatore alla stringa di classe
hIconSm HICON ? ; handle dell'icona piccola

WNDCLASSEX ENDS
Prima di tutto nel nostro programma definiamo un'istanza wc di questa
struttura; questa istanza può essere definita nel blocco dati non
inizializzati scrivendo:
wc WNDCLASSEX < ? >
Alternativamente, seguendo lo stile dei linguaggi di alto livello,
possiamo definire wc come variabile locale di WinMain; in questo caso
la memoria per wc viene allocata nello stack scrivendo:
LOCAL wc :WNDCLASSEX
Analizziamo in dettaglio il significato dei singoli campi di questa
struttura.

cbSize è una DWORD destinata a contenere la dimensione in byte della


struttura; per inizializzare questo campo possiamo utilizzare gli
operatori SIZE del TASM e SIZEOF del MASM. Con il TASM possiamo
scrivere:
74
mov wc.cbSize, SIZE WNDCLASSEX
Con il MASM possiamo scrivere:
mov wc.cbSize, SIZEOF WNDCLASSEX

style è una DWORD che ci permette di definire alcuni aspetti


stilistici della finestra; nel nostro esempio utilizzeremo i due
codici CS_HREDRAW e CS_VREDRAW. Il codice CS_HREDRAW determina
l'aggiornamento della finestra in caso di modifica della sua
larghezza; il codice CS_VREDRAW determina l'aggiornamento della
finestra in caso di modifica della sua altezza. I due codici devono
essere combinati tra loro attraverso l'operatore OR dell'Assembly; in
definitiva possiamo scrivere:
mov wc.style, CS_HREDRAW OR CS_VREDRAW
Nei capitoli successivi utilizzeremo anche altri codici di stile.

lpfnWndProc è l'indirizzo della window procedure associata alla


nostra finestra; come è stato detto in precedenza, il SO utilizza la
window procedure di una finestra per inviare i messaggi alla finestra
stessa. Nel caso del nostro esempio la window procedure si chiama
WndProc e viene descritta più avanti; il tipo di dato WNDPROC non è
altro che una DWORD contenente l'offset di WndProc. Possiamo scrivere
quindi:
mov wc.lpfnWndProc, offset WndProc

cbClsExtra contiene il numero extra di byte da riservare per la


classe finestra; questo campo (tipo DWORD) verrà illustrato in altri
capitoli. Nel nostro caso non ci serve nessun byte extra per cui
scriviamo:
mov wc.cbClsExtra, 0

cbWndExtra contiene il numero extra di byte da riservare per


l'istanza della finestra; anche questo campo (tipo DWORD) verrà
illustrato in altri capitoli. Nel nostro caso non ci serve nessun
byte extra per cui scriviamo:
mov wc.cbWndExtra, 0

hInstance rappresenta l'handle della nostra applicazione; in


precedenza, con la chiamata di GetModuleHandleA avevamo già reperito
questa informazione che era stata salvata nella variabile hInstance.
Possiamo scrivere quindi:
mov wc.hInstance, hInstance
Naturalmente questa istruzione produce un errore dell’ assembler in
quanto non è possibile effettuare un trasferimento dati da memoria a
memoria; per evitare il fastidio di dover effettuare ogni volta due
trasferimenti (da SRC a reg e da reg a DEST), possiamo utilizzare la
seguente macro:
MOV32 MACRO Destinazione, Sorgente

push dword ptr Sorgente


pop dword ptr Destinazione

ENDM
A questo punto possiamo scrivere:
MOV32 wc.hInstance, hInstance
La macro MOV32 potrebbe essere scritta anche come:
75
MOV32 MACRO Destinazione, Sorgente

mov eax, Sorgente


mov Destinazione, eax

ENDM
Questo metodo però è sconsigliabile in quanto il registro EAX (o
qualsiasi altro registro generale) potrebbe essere già in uso e
quindi non utilizzabile.

hIcon contiene l'handle dell'icona (32x32 pixel) associata alla


nostra applicazione; il tipo HICON equivale come al solito a DWORD.
Per reperire questa informazione dobbiamo servirci della procedura
LoadIconA definita nella libreria USER32.LIB; il prototipo di questa
procedura è:
HICON LoadIconA(HINSTANCE hInst, LPCTSTR lpIconName);
Il parametro hInst rappresenta l'handle della nostra applicazione,
mentre il parametro lpIconName rappresenta il nome o il codice che
identifica l'icona. Queste informazioni sono necessarie solo quando
vogliamo utilizzare un'icona personalizzata; nel nostro caso invece
viene utilizzata l'icona predefinita IDI_WINLOGO che mostra il
classico logo di Windows. Quando si utilizza un'icona predefinita il
parametro hInst deve valere NULL; la procedura LoadIconA termina
restituendo in EAX l'handle richiesto. In definitiva possiamo
scrivere (in versione MASM):
invoke LoadIconA, NULL, IDI_WINLOGO
mov wc.hIcon, eax
I codici delle altre icone predefinite sono reperibili nel Win32
Programmer's Reference; utilizzando i vari codici disponibili
possiamo sperimentare i diversi tipi di icone predefinite.

hCursor contiene l'handle del cursore del mouse associato alla nostra
applicazione; il tipo HCURSOR equivale a DWORD. Per reperire questa
informazione dobbiamo servirci della procedura LoadCursorA definita
nella libreria USER32.LIB; il prototipo di questa procedura è:
HCURSOR LoadCursorA(HINSTANCE hInst, LPCTSTR lpCursorName);
Il parametro hInst rappresenta l'handle della nostra applicazione,
mentre il parametro lpCursorName rappresenta il nome o il codice che
identifica il cursore. Queste informazioni sono necessarie solo
quando vogliamo utilizzare un cursore personalizzato; nel nostro caso
invece viene utilizzato il cursore predefinito IDC_ARROW che mostra
la classica freccia. Quando si utilizza un cursore predefinito il
parametro hInst deve valere NULL; la procedura LoadCursorA termina
restituendo in EAX l'handle richiesto. In definitiva possiamo
scrivere (in versione MASM):
invoke LoadCursorA, NULL, IDC_ARROW
mov wc.hCursor, eax
Anche in questo caso possiamo divertirci a sperimentare altri codici
che ci permettono di utilizzare cursori predefiniti a forma di
clessidra, di punto interrogativo, etc.

hbrBackground contiene il colore di sfondo della finestra; il tipo di


dato HBRUSH equivale a DWORD e rappresenta l'handle del pennello da
utilizzare per colorare uno sfondo. Anche in questo caso Win32
76
fornisce una numerosa serie di colori predefiniti; ciascun colore
predefinito è rappresentato da un codice numerico al quale bisogna
sommare 1. Utilizzando ad esempio il colore predefinito COLOR_WINDOW
(colore di sistema per lo sfondo delle finestre), possiamo scrivere:
mov wc.hbrBackground, COLOR_WINDOW + 1

lpszMenuName è un codice numerico o un puntatore ad una stringa C che


identifica la risorsa menu da collegare alla nostra applicazione; se
l'applicazione non ha un menu questo campo deve valere NULL. Nel
nostro caso quindi possiamo scrivere:
mov wc.lpszMenuName, NULL

lpszClassName è un puntatore ad una stringa C che contiene il nome da


assegnare alla classe della finestra (window class) che stiamo
inizializzando; il termine window class si riferisce alla categoria a
cui appartiene la finestra da definire. Ricorrendo ad una pratica
molto diffusa tra i programmatori, utilizziamo lo stesso nome
dell'applicazione; nel blocco dati inizializzati viene definita la
stringa:
className db 'MainWin', 0
Possiamo scrivere quindi:
mov wc.lpszClassName, offset className

hIconSm contiene l'handle dell'icona piccola (16x16 pixel) associata


alla nostra applicazione; si tratta della piccola icona che compare
all'estremità sinistra della barra del titolo di una finestra.
Cliccando su questa icona con il pulsante sinistro del mouse compare
il menu di sistema (system menu) che Win32 assegna automaticamente ad
ogni finestra; un doppio click su questa icona provoca invece una
richiesta di chiusura della finestra. Se non vogliamo utilizzare
nessuna icona piccola, questo campo deve valere NULL; in questo caso
il SO utilizza come icona piccola una versione rimpicciolita
dell'icona individuata dal campo hIcon. Nel nostro caso possiamo
scrivere:
mov wc.hIconSm, NULL

A questo punto abbiamo esaminato tutti i campi della struttura


WNDCLASSEX, per cui possiamo dire che la fase di inizializzazione
della Main Window è terminata; il passo successivo da compiere
consiste nell'inviare al SO una richiesta di registrazione della
finestra appena inizializzata.

5.4 Registrazione della Main Window.

Per effettuare la richiesta di registrazione di una finestra dobbiamo


servirci della procedura RegisterClassExA definita nella libreria
USER32.LIB; questa procedura ha il seguente prototipo:
ATOM RegisterClassExA(CONST WNDCLASSEX *lpwcx);
Come si può notare, il parametro lpwcx deve contenere l'indirizzo di
una struttura di tipo WNDCLASSEX; il qualificatore CONST del
linguaggio C impone che lpwcx sia un puntatore costante, nel senso
che non è possibile modificare l'indirizzo a cui punta lpwcx. Nel
caso del nostro esempio dobbiamo passare a RegisterClassExA l'offset
di wc; il procedimento che dobbiamo seguire varia a seconda che wc
77
sia stata definita nel segmento dati del programma (variabile
globale) o nello stack (variabile locale). Vediamo come ci si deve
comportare nel caso del TASM; se wc è stata definita nel segmento
dati, allora questa variabile è dotata di un offset ben preciso che
può essere calcolato dall' assembler in fase di assemblaggio del
programma. In questo caso possiamo scrivere:
call RegisterClassExA, offset wc
Se invece wc è stata definita nello stack, allora l' assembler
ovviamente non può conoscere in anticipo l'offset di questa
variabile; ricordiamoci infatti che, come è stato spiegato nel
Capitolo 24 della sezione Assembly Base, lo stack è un'area di
memoria gestita dinamicamente. Le variabili locali (così come gli
argomenti) di una procedura, vengono create nello stack in fase di
chiamata della procedura, e distrutte al termine della procedura
stessa; può capitare che ad ogni chiamata queste variabili vengano
ricreate nello stack sempre ad offset differenti. In questo caso, al
posto dell'operatore OFFSET dobbiamo utilizzare l'istruzione LEA
(load effective address) che è in grado di calcolare un offset in
fase di esecuzione del programma (run time); in definitiva, se wc è
stata creata nello stack, dobbiamo scrivere:
lea eax, wc
call RegisterClassExA, eax
Queste regole devono essere rispettate in modo rigoroso e valgono
ovviamente per tutte quelle istruzioni che operano sull'indirizzo di
una variabile locale; la non osservanza di queste regole può portare
alla scrittura di programmi contenenti bugs molto pericolosi e
difficili da scovare.
Per quanto riguarda il MASM, valgono ovviamente le stesse
considerazioni; questo assembler però ci mette a disposizione
l'operatore ADDR che utilizzato in combinazione con la direttiva
INVOKE permette la scrittura di codice più compatto. L'operatore ADDR
applicato ad una locazione di memoria, si comporta come OFFSET per le
variabili globali, e come LEA per le variabili locali; nel nostro
caso possiamo scrivere quindi:
invoke RegisterClassExA, ADDR wc
Indipendentemente dagli aspetti formali, è ovvio che alla fine sia
TASM che MASM produrranno un codice macchina equivalente; questo
codice macchina corrisponde alla sequenza di istruzioni:
lea eax, wc
push eax
call RegisterClassExA
La procedura RegisterClassExA termina restituendo in EAX un valore di
tipo ATOM che equivale a DWORD; se la registrazione ha successo, EAX
contiene un codice numerico che identifica in modo univoco la window
class appena registrata. Se la registrazione fallisce EAX vale zero;
in questo caso il Win32 Programmer's Reference consiglia di terminare
il programma proprio con exit code uguale a zero. In definitiva, il
codice relativo alla registrazione della main window assume il
seguente aspetto (versione MASM):
invoke RegisterClassExA, ADDR wc ; richiesta di registrazione
test eax, eax ; errore ?
jz exitWinMain ; termina con eax = 0
In caso di errore la procedura WinMain termina con EAX=0; in una
eventualità del genere sarebbe opportuno informare l'utente con
78
un'apposita MessageBox. Il lettore può provare per esercizio ad
inserire il codice necessario per la gestione degli errori; questo
codice può essere inserito prima della chiamata di ExitProcess.

5.5 Attivazione della Main Window.

Se la fase di registrazione è terminata con successo, possiamo


passare alla fase di attivazione della main window; questa fase si
svolge fondamentalmente in tre passi:

1) Creazione della finestra.


2) Visualizzazione della finestra.
3) Aggiornamento dell'area di output della finestra.

La creazione materiale della finestra avviene attraverso la procedura


CreateWindowExA definita nella libreria USER32.LIB; l'impressionante
prototipo di questa procedura è il seguente:
HWND CreateWindowEx(
DWORD dwExStyle, /* extended window style */
LPCTSTR lpClassName, /* pointer to registered class name */
LPCTSTR lpWindowName, /* pointer to window name */
DWORD dwStyle, /* window style */
int x, /* horizontal position of window */
int y, /* vertical position of window */
int nWidth, /* window width */
int nHeight, /* window height */
HWND hWndParent, /* handle to parent or owner window */
HMENU hMenu, /* handle to menu, or child-window identifier */
HINSTANCE hInstance, /* handle to application instance */
LPVOID lpParam /* pointer to window-creation data */
);
Tutti i parametri richiesti da questa procedura sono di tipo DWORD;
analizziamo in dettaglio il loro significato.

dwExStyle è un codice numerico che rappresenta lo stile esteso della


finestra; modificando questo codice è possibile alterare
completamente l'aspetto esteriore della finestra. Nel nostro esempio
viene utilizzato il codice WS_EX_OVERLAPPEDWINDOW (WS = Window Style)
che crea una finestra con bordi tridimensionali; questo codice è
formato da un OR tra WS_EX_CLIENTEDGE (area utente incavata) e
WS_EX_WINDOWEDGE (bordo sporgente). Per gli altri numerosi codici si
veda il Win32 Programmer's Reference o l'include file principale
windows.inc (o win32.inc).

lpClassName è l'indirizzo di una stringa C contenente il nome


assegnato alla window class; si tratta dello stesso parametro già
utilizzato per inizializzare la struttura wc.

lpWindowName è l'indirizzo di una stringa C che verrà visualizzata


nella barra del titolo della finestra; nel nostro esempio utilizziamo
la stringa winTitle.

dwStyle è un codice numerico che rappresenta lo stile ordinario della


finestra; nel nostro esempio utilizziamo lo stile WS_OVERLAPPEDWINDOW

79
che è formato da una combinazione attraverso l'operatore OR dei
codici WS_BORDER (finestra con bordo), WS_CAPTION (finestra con title
bar), WS_SYSMENU (finestra con menu di sistema), WS_TICKFRAME
(finestra dimensionabile), WS_MINIMIZEBOX (finestra con bottone
minimizza) e WS_MAXIMIZEBOX (finestra con bottone massimizza).

x è l'ascissa iniziale del vertice superiore sinistro della finestra;


nel nostro esempio utilizziamo il codice predefinito CW_USEDEFAULT
che lascia la scelta di x al SO.

y è l'ordinata iniziale del vertice superiore sinistro della


finestra; nel nostro esempio utilizziamo il codice predefinito
CW_USEDEFAULT che lascia la scelta di y al SO.

nWidth è la larghezza iniziale della finestra; nel nostro esempio


utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta
di nWidth al SO.

nHeight è l'altezza iniziale della finestra; nel nostro esempio


utilizziamo il codice predefinito CW_USEDEFAULT che lascia la scelta
di nHeight al SO.

hWndParent è l'handle della finestra "madre" di cui è "figlia" la


nostra finestra; nel nostro caso stiamo creando proprio la finestra
madre (main window), per cui questo parametro deve valere NULL.

hMenu è un codice numerico che identifica un menu o una finestra


figlia; nel nostro caso non esistono ne menu ne finestre figlie per
cui questo parametro deve valere NULL.

hInstance è l'handle della nostra applicazione; questa informazione è


già stata descritta in precedenza.

lpParam è l'indirizzo di una struttura di tipo CREATESTRUCT


contenente informazioni ulteriori per la fase di creazione della
finestra; quando si chiama con successo la procedura CreateWindowEx,
il SO invia alla nostra window procedure, il messaggio WM_CREATE (WM
= Window Message). Se intendiamo gestire questo messaggio, dobbiamo
inizializzare il parametro lpParam facendolo puntare ad una struttura
contenente le necessarie informazioni; nel nostro esempio, il
messaggio WM_CREATE viene rispedito al mittente per cui il parametro
lpParam deve valere NULL.

Se la procedura CreateWindowEx termina con successo, ci restituisce


in EAX l'handle che il SO ha assegnato alla finestra che abbiamo
appena creato; questa informazione può servirci anche nel seguito del
programma per cui la salviamo nella variabile hWindow. Se invece
CreateWindowEx fallisce il suo compito, ci restituisce in EAX il
valore NULL; in questo caso dobbiamo terminare il programma con exit
code zero.
Se tutto è filato liscio possiamo compiere i due passi successivi che
consistono nella visualizzazione della finestra e nell'aggiornamento
del suo contenuto; la visualizzazione della finestra viene ottenuta
attraverso la procedura ShowWindow definita nella libreria
80
USER32.LIB. Il prototipo di questa procedura è il seguente:
BOOL ShowWindow(HWND hWnd, int nCmdShow);
Il parametro hWnd è l'handle della finestra che in precedenza avevamo
salvato in hWindow; il parametro nCmdShow individua lo stato iniziale
della finestra ed è stato già descritto in precedenza per la
procedura WinMain. La procedura ShowWindow termina restituendo in EAX
un valore booleano TRUE (non zero) o FALSE (zero); il valore
restituito dipende dallo stato iniziale (visibile/invisibile) della
finestra. Questo significa che FALSE non deve essere interpretato
come un codice di errore.
A questo punto dobbiamo procedere con l'aggiornamento dell'area di
output della finestra; nel gergo di Windows quest'area viene chiamata
client area. L'aggiornamento della client area viene svolto dalla
procedura UpdateWindow definita nella libreria USER32.LIB. Il
prototipo di questa procedura è il seguente:
BOOL UpdateWindow(HWND hWnd);
Il parametro hWnd è il solito handle della finestra già descritto in
precedenza; questa procedura invia alla nostra window procedure il
messaggio WM_PAINT. Intercettando questo messaggio possiamo eseguire
il codice necessario per aggiornare la client area della nostra
finestra; questo stesso messaggio viene inviato automaticamente dal
SO ogni volta che la client area della nostra finestra viene
"sporcata". Anche UpdateWindow termina restituendo in EAX un valore
booleano TRUE (non zero) o FALSE (zero); questa volta però il valore
FALSE (zero) rappresenta un codice di errore. In questo caso dobbiamo
terminare la nostra applicazione con exit code zero; in definitiva,
la fase di attivazione della main window assume la seguente struttura
(versione TASM):
call CreateWindowExA, WS_EX_OVERLAPPEDWINDOW, \
offset className, \
offset winTitle, \
WS_OVERLAPPEDWINDOW, \
CW_USEDEFAULT, \
CW_USEDEFAULT, \
CW_USEDEFAULT, \
CW_USEDEFAULT, \
NULL, \
NULL, \
hInstance, \
NULL

test eax, eax ; errore ?


jz exitWinMain ; termina con eax = 0
mov hWindow, eax ; salva l'handle della finestra
call ShowWindow, hWindow, nCmdShow ; visualizza la finestra
call UpdateWindow, hWindow ; aggiorna l'area client
test eax, eax ; errore ?
jz exitWinMain ; termina con eax = 0
A questo punto se tutto è filato liscio possiamo passare alla fase
più importante del nostro programma, e cioè alla fase di gestione
della main window.

5.6 Gestione della Main Window.

81
La gestione della main window consiste principalmente in un loop
chiamato message loop (loop dei messaggi); all'interno di questo loop
si sviluppa il flusso dei messaggi che il SO invia alla window
procedure della main window. Possiamo dire quindi che in questa fase
il protagonista principale è il messaggio; le caratteristiche dei
messaggi di Win32 vengono specificate dalla struttura MSG dichiarata
in windows.inc. Questa struttura (che viene riempita dal SO) presenta
il seguente aspetto:
MSG STRUC

hwnd HWND ? ; handle della finestra destinataria


message UINT ? ; codice messaggio
wParam WPARAM ? ; informazioni addizionali
lParam LPARAM ? ; informazioni addizionali
time DWORD ? ; ora esatta di invio del messaggio
pt POINT < ? > ; posizione del cursore del mouse

MSG ENDS
Analizziamo in dettaglio il significato dei vari campi che formano
questa struttura.

hwnd è l'handle della finestra destinataria del messaggio.

message è il codice numerico a 32 bit che identifica il messaggio;


nel file windows.inc è presente un vasto elenco di costanti
simboliche che rappresentano i vari messaggi. I nomi di queste
costanti iniziano generalmente con WM_ (Window Message).

wParam e lParam sono due DWORD contenenti informazioni addizionali


sul messaggio; la natura di queste informazioni è legata al codice
presente nel campo messaggio.

time rappresenta l'ora esatta in cui il SO ha inviato il messaggio.

pt è una struttura di tipo POINT contenente la posizione (x, y) del


cursore del mouse al momento dell'invio del messaggio; la struttura
POINT viene dichiarata in windows.inc come:
POINT STRUC

x LONG ? ; ascissa
y LONG ? ; ordinata

POINT ENDS
Tutti i messaggi che il SO invia alle applicazioni in esecuzione,
vengono inseriti in un'apposita coda di attesa; il nostro compito
consiste nell'attivare un loop all'interno del quale questi messaggi
vengono estratti in sequenza dalla coda, decodificati e smistati ai
vari destinatari (finestre). Possiamo individuare quindi tre passi
fondamentali che dobbiamo compiere all'interno del loop:

1) Estrazione del prossimo messaggio dalla coda di attesa.


2) Decodifica del messaggio.
3) Invio del messaggio al destinatario.

A ciascuno di questi tre passi è associata una ben precisa procedura.


82
Per l'estrazione dei messaggi dalla coda dobbiamo utilizzare la
procedura GetMessage definita nella libreria USER32.LIB; il prototipo
di questa procedura è il seguente:
BOOL GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT
wMsgFilterMax);

lpMsg è una DWORD contenente l'indirizzo di una struttura di tipo


MSG; i campi che formano questa struttura vengono riempiti da
GetMessage.

hWnd è l'handle della finestra destinataria del messaggio; quando


questo parametro vale NULL la procedura GetMessage estrae i messaggi
legati esclusivamente all'applicazione che ha chiamato la procedura
stessa. Nel caso del nostro esempio è fondamentale che il parametro
hWnd valga NULL.

wMsgFilterMin e wMsgFilterMax rappresentano rispettivamente il codice


più piccolo e il codice più grande dei messaggi che bisogna estrarre
dalla coda di attesa; assegnando il valore zero ad entrambi i
parametri si indica a GetMessage di estrarre messaggi aventi
qualsiasi codice.

La procedura GetMessage termina restituendo in EAX un valore non


nullo; l'unica eccezione è rappresentata dall'estrazione dalla coda
del messaggio WM_QUIT che indica l'imminente chiusura della nostra
applicazione. In questo caso GetMessage restituisce EAX=0; questa è
proprio la condizione di uscita dal loop dei messaggi. L'uscita dal
loop determinata dalla ricezione di WM_QUIT indica la terminazione
regolare del nostro programma; in questo caso il Win32 Programmer's
Reference consiglia di utilizzare il campo wParam della struttura MSG
come exit code.
Se nell'estrazione di un messaggio dalla coda si verifica un errore,
GetMessage restituisce il numero negativo -1; si tratta di
un'eventualità estremamente improbabile, ma conoscendo Windows è
meglio non sottovalutare la situazione.

Per la decodifica del messaggio appena estratto da GetMessage si


utilizza la procedura TranslateMessage definita nella libreria
USER32.LIB; il prototipo di questa procedura è il seguente:
BOOL TranslateMessage(CONST MSG *lpMsg);

lpMsg è l'indirizzo della struttura di tipo MSG appena restituitaci


da GetMessage; la procedura TranslateMessage provvede alla decodifica
del messaggio corrente. Ad esempio, se si preme un tasto della
tastiera, Win32 associa questo evento ad un messaggio che rappresenta
in forma "criptata" il codice del tasto appena premuto; questo codice
viene chiamato virtual key (tasto virtuale). La procedura
TranslateMessage provvede a "decriptare" il virtual key ricavandone
una serie di informazioni tra le quali c'è anche il codice ASCII del
tasto; tutte queste informazioni vengono a loro volta convertite in
messaggi e inserite nella coda privata dell'applicazione
destinataria.

Per l'invio dei messaggi appena decodificati da TranslateMessage si


83
utilizza la procedura DispatchMessage definita nella libreria
USER32.LIB; il prototipo di questa procedura è il seguente:
BOOL DispatchMessage(CONST MSG *lpMsg);

lpMsg è l'indirizzo della struttura di tipo MSG appena decodificata


da TranslateMessage; la procedura DispatchMessage provvede ad inviare
materialmente il messaggio alla window procedure della finestra
destinataria. Il valore che DispatchMessage restituisce in EAX
coincide con il valore di ritorno della window procedure.

A questo punto abbiamo a disposizione tutti gli elementi necessari


per scrivere il codice relativo al loop dei messaggi; prima di tutto
dobbiamo definire un'istanza della struttura MSG. Come al solito
questa definizione può essere inserita sia nel blocco dati che nello
stack; nel nostro esempio definiamo l'istanza msg nello stack
scrivendo:
LOCAL msg :MSG
In questo modo msg assume l'aspetto di una variabile locale di
WinMain; a questo punto possiamo scrivere (in versione MASM):
MessageLoop:
invoke GetMessage, ADDR msg, NULL, 0, 0 ; estrazione prossimo
messaggio
test eax, eax ; eax = 0 ?
jz exitMessageLoop ; fine del loop (WM_QUIT)
invoke TranslateMessage, ADDR msg ; decodifica messaggio
invoke DispatchMessage, ADDR msg ; invio messaggio alla
WinProc
jmp MessageLoop ; ripeti
exitMessageLoop:
mov eax, msg.wParam ; exit code = OK
exitWinMain: ; exit code = 0 (errore)
Come si può notare, se il programma termina regolarmente, WinMain a
sua volta termina con il valore msg.wParam in EAX; questo valore
viene quindi passato a ExitProcess che lo restituisce al SO. Se
invece si è verificato un errore (causato ad esempio dal fallimento
di RegisterClassEx), l'esecuzione salta all'etichetta exitWinMain con
EAX=0.

5.7 La Window Procedure.

Non appena il nostro programma viene caricato in memoria per


l'esecuzione, entra in azione il message loop che "pompa"
continuamente messaggi dalla coda di attesa, li decodifica e li
smista alle window procedure delle finestre destinatarie. Nel caso
del nostro esempio, DispatchMessage invia i messaggi alla procedura
WndProc associata alla main window; il SO conosce la posizione in
memoria di WndProc in quanto gli abbiamo passato questa informazione
attraverso il campo lpfnWndProc della struttura wc.
Siamo liberi di assegnare qualsiasi nome ad una window procedure; la
lista dei parametri però deve rispettare rigorosamente il seguente
prototipo:
LRESULT NomeWinProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

hWnd è l'handle della finestra destinataria.

84
uMsg, wParam e lParam equivalgono ai campi message, wParam e lParam
della struttura MSG.

Una window procedure termina restituendo in EAX un codice che


rappresenta il risultato dell'elaborazione del messaggio;
generalmente questo codice viene totalmente ignorato in Win32.
Nel nostro esempio la window procedure viene chiamata WndProc;
all'interno di WndProc avviene l'elaborazione dei messaggi che
intendiamo accettare. È fondamentale che tutti i messaggi che non
intendiamo accettare vengano rispediti al mittente; a tale proposito
ci dobbiamo servire della window procedure predefinita di Win32,
chiamata DefWindowProc e definita nella libreria USER32.LIB. Questa
procedura naturalmente ha lo stesso prototipo mostrato in precedenza;
DefWindowProc riceve tutti i messaggi che abbiamo rifiutato ed
effettua su di essi una serie di elaborazioni predefinite. In base a
tutte queste considerazioni, possiamo definire il seguente scheletro
in linguaggio C della procedura WndProc:
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_AAAA) /* se il messaggio è WM_AAAA */
...................... /* elabora il messaggio WM_AAAA */
else if (uMsg == WM_BBBB) /* se invece il messaggio è WM_BBBB */
...................... /* elabora il messaggio WM_BBBB */
.........................
......................
else /* altrimenti chiama DefWindowProc */
return DefWindowProc(hWnd, uMsg, wParam, lParam);

return 0; /* valore di ritorno di WndProc */


}
In sostanza, se rifiutiamo un messaggio, lo passiamo a DefWindowProc
e poi usciamo da WndProc con il valore di ritorno restituito in EAX
dalla stessa DefWindowProc; se invece accettiamo il messaggio, lo
elaboriamo e usciamo da WndProc con EAX che convenzionalmente ha
valore zero. Come è stato detto in precedenza, questo valore di
ritorno rimane inutilizzato.

Nel caso del nostro esempio, si nota che la procedura WndProc


gestisce solamente i due messaggi WM_CLOSE e WM_DESTROY che sono
fondamentali per la corretta terminazione di un'applicazione; il
messaggio WM_CLOSE viene generato da Win32 ogni volta che si cerca di
chiudere una finestra. Questo evento si verifica quando premiamo il
bottone Chiudi all'estremità destra della barra del titolo, quando
selezioniamo Chiudi dal menu di sistema della finestra, quando
eseguiamo un doppio click sull'icona di sistema della finestra (icona
piccola), etc; in tutti questi casi la procedura WndProc si vede
arrivare il codice WM_CLOSE nel parametro uMsg. Nel nostro esempio
intercettiamo il messaggio WM_CLOSE e apriamo una MessageBox per
chiedere conferma all'utente; rispetto agli esempi del precedente
capitolo, la MessageBox usata dall'applicazione MAINWIN presenta
alcune caratteristiche interessanti. Prima di tutto osserviamo che
questa volta la MessageBox viene generata dalla main window del
nostro programma; questo significa che la MessageBox è figlia della
main window e quindi deve ricevere nel primo parametro l'handle hWnd
della madre e cioè della main window. Un altro aspetto interessante
85
riguarda la presenza di due bottoni nella MessageBox; si tratta dei
due bottoni YES e NO che possiamo attivare con il codice MB_YESNO. Se
si preme YES la MessageBox restituisce in EAX il codice IDYES; se si
preme NO la MessageBox restituisce in EAX il codice IDNO.
Se premiamo NO saltiamo all'etichetta exitWndProc, usciamo da WndProc
con EAX=0 e l'esecuzione della nostra applicazione continua; se
premiamo YES, stiamo confermando la chiusura dell'applicazione. In
questo caso dobbiamo seguire rigorosamente un procedimento ben
preciso che deve garantire una corretta chiusura dell'applicazione;
in particolare, è fondamentale che WndProc dopo aver "risposto" a
WM_CLOSE passi questo messaggio alla DefWindowProc. In questo modo
anche DefWindowProc può rispondere a WM_CLOSE eseguendo una serie di
operazioni predefinite che precedono la chiusura della finestra; al
termine di queste operazioni, DefWindowProc spedisce il messaggio
WM_DESTROY per richiedere la "distruzione" fisica della finestra. A
questo punto la nostra WndProc deve intercettare WM_DESTROY e in
risposta a questo messaggio deve chiamare la procedura
PostQuitMessage.
La procedura PostQuitMessage viene definita nella libreria
USER32.LIB; il prototipo di questa procedura è il seguente:
VOID PostQuitMessage(int nExitCode);

nExitCode deve contenere l'exit code con il quale vogliamo uscire


dalla nostra applicazione; generalmente i programmatori passano NULL
in questo parametro, anche se in questo modo l'exit code può essere
confuso con un codice di errore (zero). La procedura PostQuitMessage
non restituisce nessun valore di ritorno (void), ma prima di
terminare inserisce nella coda di attesa il messaggio WM_QUIT; alla
successiva iterazione del message loop, la procedura GetMessage
riempie nuovamente la struttura msg inserendo il codice WM_QUIT nel
campo message e il codice nExitCode nel campo wParam. In presenza del
messaggio WM_QUIT la procedura GetMessage termina con valore di
ritorno EAX=0; come già sappiamo questa è proprio la condizione di
uscita dal loop dei messaggi.
Subito dopo essere usciti dal loop, carichiamo in EAX il codice
contenuto in msg.wParam (cioè nExitCode) e quindi usciamo da WinMain;
a questo punto viene chiamata ExitProcess che termina l'applicazione
con l'exit code contenuto in EAX.
È importante sottolineare il fatto che se non seguiamo il
procedimento descritto prima per la gestione di WM_CLOSE può
verificarsi una terminazione anomala del nostro programma; in
particolare, può capitare che la main window scompaia dallo schermo
lasciando però vari "pezzi" sparpagliati in memoria. Per verificare
questa eventualità, possiamo premere la sequenza di tasti
[Ctrl][Alt][Canc] (o [Ctrl][Alt][Del]) per visualizzare il Task
Manager di Win32, cioè la finestra che mostra la lista delle
applicazioni in esecuzione; se il nostro programma è terminato in
modo anomalo, pur essendo scomparso dallo schermo potrebbe essere
ancora presente in questa lista.

5.8 Esecuzione di MAINWIN.EXE.

Eseguendo MAINWIN.EXE compare sullo schermo una classica finestra di


Win32 dotata di sfondo standard, bordi tridimensionali, menu di
86
sistema, bottoni Riduci a icona, Ingrandisci/Ripristina e Chiudi,
barra del titolo, etc; ogni volta che si cerca di chiudere questa
finestra, compare una MessageBox che ci chiede di confermare la
nostra scelta.
È importante che il programmatore faccia degli esperimenti provando a
modificare alcuni codici passati alla struttura WNDCLASSEX o alla
procedura CreateWindowEx; nel caso della struttura WNDCLASSEX si può
provare a modificare i campi hIcon, hCursor e hbrBackground inserendo
i codici predefiniti presenti nell'include file windows.inc o nel
Win32 Programmer's Reference. Nel caso della procedura CreateWindowEx
si può provare a modificare i parametri dwExStyle, dwStyle, x, y,
nWidth e nHeight; anche in questo caso, si consiglia di consultare il
Win32 Programmer's Reference per avere maggiori informazioni su
questi parametri.

5.9 Note importanti sulla generazione dell'eseguibile


MAINWIN.EXE.

Se stiamo utilizzando MASM32, la generazione di MAINWIN.EXE avviene


in modo estremamente semplice sia dalla linea di comando che
dall'interno di QEDITOR.EXE; in particolare, dall'interno di
QEDITOR.EXE ci basta selezionare il menu Project + Build All. Se non
ci sono errori possiamo eseguire MAINWIN.EXE selezionando il menu
Project + Run Program.
Tutto si svolge in modo molto semplice grazie al fatto che MASM32
offre il pieno supporto per lo sviluppo di applicazioni in Assembly
per Win32; in particolare questo assembler fornisce l'SDK completo di
Win32, compresi gli include files associati alle varie librerie.

Se invece vogliamo usare TASM, dobbiamo procurarci la versione più


aggiornata possibile dell'SDK in quanto quella fornita con TASM 5.x è
ormai obsoleta; per quanto riguarda gli include files possiamo
utilizzare quelli del MASM32 con l'eccezione di windows.inc che usa
una sintassi incompatibile con TASM. Come è stato detto nei
precedenti capitoli, il problema più grave è legato al fatto che i
nomi usati per i campi di una struttura sono considerati locali da
MASM e globali da TASM; questo significa che con MASM possiamo
riutilizzare questi nomi all'interno di altre strutture, mentre con
TASM questo non è possibile. Per questo motivo è necessario
procurarsi un apposito file windows.inc scritto espressamente per il
TASM; alternativamente, con un po’ di olio di gomito è possibile
modificare il file windows.inc di MASM adattandolo al TASM.
Le modifiche da apportare sono relative fondamentalmente ai nomi
usati per i campi delle varie strutture predefinite di Win32; lo
scopo di queste modifiche è quello di eliminare la possibilità che il
nome di un campo di una struttura venga ridefinito altrove provocando
un messaggio di errore di TASM. Vediamo ad esempio il caso del campo
wParam della struttura MSG; questo nome entra in conflitto con il
nome del terzo parametro utilizzato per la procedura WndProc.
Come si può facilmente intuire, si tratta di una situazione veramente
assurda che costringe il programmatore a compiere veri e propri salti
mortali; è un vero peccato che la Borland non abbia ancora risolto
questo problema visto che il TASM per qualità, efficienza e

87
affidabilità è nettamente superiore al MASM.
Attraverso Internet è possibile reperire numerose versioni del file
windows.inc per il TASM; è fondamentale procurarsi una versione che
si attenga nel modo più fedele possibile ai nomi imposti dalla
Microsoft per le varie dichiarazioni.
Per venire incontro ai numerosi estimatori del TASM, è possibile
scaricare da questo sito una versione di windows.inc curata da Ra.M.
Software; il file si chiama win32.zip ed è prelevabile dalla sezione
Downloads. Questo file una volta scompattato diventa win32.inc e deve
essere spostato nella cartella c:\tasm\include; il file win32.inc
viene costantemente aggiornato sulla base delle esigenze legate ai
programmi di esempio presentati nei vari capitoli della sezione Win32
Assembly. Tutti gli esempi per TASM presentati in questa sezione
utilizzano l'include file win32.inc; le dichiarazioni presenti in
win32.inc presuppongono che il programmatore disponga del TASM 5.x.
Le versioni precedenti del TASM non supportano il modello di memoria
FLAT per cui non è possibile utilizzare neanche la sintassi avanzata
per la chiamata delle procedure; in questo caso è necessario
programmare in puro Assembly.
Se si intende utilizzare win32.inc è consigliabile aprire questo file
con un editor per prendere confidenza in particolare con le modifiche
apportate ai membri delle strutture WNDCLASSEX, MSG, etc; ad esempio,
il membro wParam di MSG è stato modificato in ms_wParam. È chiaro che
il programmatore è libero di modificare il contenuto di win32.inc (o
di qualsiasi versione di windows.inc) in base ai gusti personali; in
particolare, è importante che l'include file principale venga
continuamente aggiornato con le informazioni più recenti reperite ad
esempio attraverso Internet.
Se si decide invece di scaricare wintasm.zip dalla sezione Downloads,
si ha a disposizione una delle tante versioni di windows.inc
reperibile via Internet; se si vuole utilizzare questo file è
necessario inserire nei propri programmi la dichiarazione:
UNICODE = 0
Naturalmente questa dichiarazione deve precedere l'inclusione di
windows.inc.

5.10 Note importanti sulle versioni ASCII e UNICODE delle


procedure di Win32.

Nell'esempio presentato in questo capitolo e in tutti gli esempi dei


capitoli successivi vengono utilizzati gli include files associati
all'SDK di Win32; in questi include files sono presenti naturalmente
i prototipi delle varie procedure dell'SDK. Come già sappiamo, tutte
le procedure che gestiscono stringhe sono presenti sia in versione
ASCII che in versione UNICODE; le procedure in versione ASCII hanno
nomi che terminano con la lettera A (MessageBoxA), mentre le
procedure in versione UNICODE hanno nomi che terminano con la lettera
W (MessageBoxW). Se si consulta ad esempio il file USER32.INC si nota
che tutti i prototipi delle procedure in versione ASCII, vengono
ridichiarati nel seguente modo:
MessageBoxA PROTO :DWORD, :DWORD, :DWORD, :DWORD
MessageBox equ < MessageBoxA >

88
In sostanza, grazie a queste ri-dichiarazioni, ogni volta che
omettiamo la lettera A finale, viene chiamata la versione ASCII della
procedura; in questo modo si evita il fastidio di dover specificare
ogni volta la lettera finale. A partire dagli esempi del prossimo
capitolo utilizzeremo quindi i prototipi delle procedure ASCII privi
della A finale.

89
Win32 Assembly

Capitolo 6: La gestione del testo.


In questo capitolo vengono analizzati i principali strumenti che
win32 ci mette a disposizione per l'output delle stringhe di testo
sullo schermo; per verificare in pratica i vari concetti esposti nel
capitolo, viene utilizzato un apposito programma denominato
FONTWIN.ASM.
Come al solito, per seguire meglio le spiegazioni teoriche, conviene
scaricare il codice sorgente e visualizzarlo con l'editor preferito;
il codice sorgente è disponibile nei seguenti files zippati:
Download di tutti gli esempi del capitolo (versione MASM)
Download di tutti gli esempi del capitolo (versione TASM)
Analizzando il contenuto del file FONTWIN.ASM, si può subito notare
che, come era stato anticipato nel precedente capitolo, è stato
riciclato praticamente il 100% del codice sorgente presente nel file
MAINWIN.ASM; questa caratteristica dei programmi per Windows, viene
largamente sfruttata dai programmatori che vogliono risparmiare
parecchio tempo nella scrittura del codice sorgente. Supponiamo ad
esempio di avere a disposizione il codice sorgente di MAINWIN.ASM e
di voler scrivere un nuovo programma chiamato FONTWIN.ASM; invece di
ricominciare da zero, conviene aprire con un editor il file
MAINWIN.ASM, salvarlo sul disco con il nuovo nome FONTWIN.ASM e
apportare in seguito tutte le necessarie modifiche.

Cominciamo allora con l'analizzare proprio le principali modifiche


apportate al file FONTWIN.ASM; prima di tutto, notiamo che le
procedure dell'SDK che gestiscono stringhe ASCII, vengono chiamate
senza la A finale (LoadIcon, LoadCursor, MessageBox, etc). Come è
stato detto nel precedente capitolo, nei vari include files dell'SDK,
tutte le procedure che gestiscono stringhe ASCII hanno nomi che
vengono ridichiarati senza la A finale, come ad esempio:
LoadIconA PROTO :DWORD, :DWORD
LoadIcon equ < LoadIconA >
In questo modo, se dimentichiamo di scrivere la A finale, viene
chiamata la versione ASCII della procedura; da questo capitolo in
poi, la A finale di questi nomi verrà sempre omessa.

Il blocco dati inizializzati (_DATA) e il blocco dati non


inizializzati (_BSS) di FONTWIN.ASM, assumono l'aspetto mostrato in
Figura 1.
Figura 1 - Segmento dati di FONTWIN.ASM
_DATA SEGMENT DWORD PUBLIC USE32 'DATÀ

className db 'FontWin', 0
winTitle db 'Win32 Assembly - Applicazione FontWin', 0

90
msgTitolo db 'Win 32 Assembly', 0
msgCreate db 'Procedura FontWin_OnCreate.', 10
db 'Messaggio WM_CREATE ricevuto!', 10
db 'Inizializzazioni effettuate.', 0

msgClose db 'Procedura FontWin_OnClose.', 10


db 'Messaggio WM_CLOSE ricevuto!', 10
db 'Chiudere FONTWIN.EXE ?', 0

strOut1 db 'Output di stringhe di testo in Win32'


LEN_STROUT1 = $ - offset strOut1
strOut2 db 'True Typè
LEN_STROUT2 = $ - offset strOut2
strOut3 db 'Testo centrato nella finestrà, 0

fontName db 'Times New Roman', 0


fontHandle dd 0

_DATA ENDS

_BSS SEGMENT DWORD PUBLIC USE32 'BSS'

hInstance dd ? ; handle dell'applicazione


commandLine dd ? ; puntatore alla command line
hWindow dd ? ; handle della main window

fontStruct LOGFONT ; struttura LOGFONT

_BSS ENDS
Come si può notare, per le variabili principali come className,
winTitle, etc, si è proceduto semplicemente a sostituire la stringa
MainWin con la stringa FontWin; tutte le altre variabili definite nel
blocco _DATA e nel blocco dati non inizializzati _BSS, vengono
descritte nel seguito del capitolo.

Passiamo ora alle modifiche apportate nella fase di inizializzazione


della main window; come già sappiamo, questa fase consiste nel
riempimento di una struttura di tipo WNDCLASSEX.
Per il campo hIcon (handle dell'icona), viene utilizzata l'icona
predefinita individuata dal codice IDI_INFORMATION; questa icona è la
stessa che viene mostrata in una MessageBox con il codice
MB_ICONINFORMATION.
Per il campo hCursor (handle del cursore del mouse), viene utilizzato
il cursore predefinito individuato dal codice IDC_HAND; in questo
modo, il cursore del mouse assume la forma di una mano.
Indubbiamente, le modifiche più interessanti riguardano il campo
hbrBackground che come sappiamo, ci permette di assegnare il colore
di sfondo alla finestra che stiamo inizializzando; nel precedente
capitolo abbiamo visto che questo campo è una DWORD che contiene un
codice di tipo HBRUSH. Questo codice rappresenta l'handle del
pennello (brush = spazzola, pennello) che verrà utilizzato per
colorare lo sfondo; se non abbiamo esigenze particolari, possiamo
utilizzare uno dei tanti colori predefiniti di Windows. Nel
precedente capitolo ad esempio, abbiamo utilizzato il colore
predefinito COLOR_WINDOW+1 che rappresenta il colore standard per lo
sfondo delle finestre; questo colore di sfondo viene assegnato da
91
Windows a tutte le finestre che non fanno richiesta di colori
personalizzati. Per modificare il colore per lo sfondo standard,
bisogna cliccare sul desktop con il pulsante destro del mouse e
selezionare Proprietà; nella finestra che compare bisogna poi
selezionare Aspetto.
Nel caso dell'applicazione FONTWIN, viene utilizzato un colore
personalizzato per lo sfondo della main window; prima di analizzare
il procedimento da seguire, è necessario illustrare il metodo che
viene impiegato da Windows per la gestione dei colori.
Come si sa dalla fisica ottica, è possibile ottenere un qualsiasi
colore a partire dai tre colori rosso, verde e blu; per questo
motivo, questi tre colori vengono definiti colori fondamentali. Per
ottenere un determinato colore, bisogna miscelare tra loro i tre
colori fondamentali dopo aver scelto opportunamente la tonalità di
rosso, la tonalità di verde e la tonalità di blu; nel mondo del
computer, i colori ottenuti in questo modo vengono chiamati colori
RGB. La sigla RGB significa Red Green Blue (rosso, verde blu); si usa
anche la definizione di terna RGB.
In Windows le terne RGB vengono gestite attraverso il tipo di dato
COLORREF; un valore di tipo COLORREF è una DWORD che assume la
struttura mostrata dalla Figura 2.

Come possiamo notare, i bit da 0 a 7 contengono la tonalità di rosso,


i bit da 8 a 15 contengono la tonalità di verde, i bit da 16 a 23
contengono la tonalità di blu, mentre i bit da 24 a 31 contengono il
valore del "canale alfa" (Alpha Channel); il canale alfa viene
utilizzato dai programmi di grafica per gestire l'effetto trasparenza
delle immagini. Nel caso dei colori di sfondo delle finestre, il
canale alfa non viene utilizzato, e quindi i bit da 24 a 31 di un
COLORREF dovrebbero valere sempre zero; ci restano quindi a
disposizione 24 bit (da 0 a 23) per definire numerosissimi colori
personalizzati. Osserviamo che per definire la tonalità di ciascuno
dei tre colori fondamentali, abbiamo a disposizione 8 bit; con 8 bit
possiamo rappresentare 28=256 valori differenti. In totale quindi con
3*8=24 bit possiamo rappresentare:
28 * 28 * 28 = 28+8+8 = 224 = 16777216 colori = 16M colori
Naturalmente, sarà possibile visualizzare tutti i 16M di colori solo
se abbiamo a disposizione una scheda video che gestisce 16M colori;
in questo caso, tecnicamente si parla di schede video a 24 bit o
anche di colori a 24 bit. Se oltre ai 16M di colori vogliamo gestire
anche il canale alfa, dobbiamo disporre ovviamente di una scheda
video a 32 bit; se il nostro computer è dotato di scheda video con
caratteristiche inferiori (64K colori, 32K colori, etc), allora i
colori non disponibili verranno "approssimati" da Windows. In
pratica, se selezioniamo un colore non supportato dalla nostra scheda
video, Windows ci fornisce il colore più somigliante a quello
richiesto; è chiaro quindi che tanto maggiore è il numero di colori
supportato dalla nostra scheda video, tanto migliore sarà la qualità
delle immagini gestite attraverso Windows.

92
Per creare facilmente un dato di tipo COLORREF, possiamo scrivere ad
esempio la seguente macro:
MAKE_COLORREF MACRO intRed, intGreen, intBlue

xor eax, eax


mov ah, intBlue
shl eax, 8
mov ah, intGreen
mov al, intRed

ENDM
Questa macro non fa altro che disporre nell'ordine giusto i tre
valori intRed, intGreen e intBlue; alla fine il registro EAX contiene
un dato in formato COLORREF.
Osserviamo che:

Le terne RGB di tipo (r, 0, 0), con r compreso tra 0 e 255, ci


forniscono 256 possibili tonalità di rosso.
Le terne RGB di tipo (0, g, 0), con g compreso tra 0 e 255, ci
forniscono 256 possibili tonalità di verde.
Le terne RGB di tipo (0, 0, b), con b compreso tra 0 e 255, ci
forniscono 256 possibili tonalità di blu.
Le terne RGB di tipo (r, g, b), con r = g = b compresi tra 0 e 255,
ci forniscono 256 possibili tonalità di grigio, dal nero (0, 0, 0) al
bianco (255, 255, 255).

Una volta chiariti questi aspetti, vediamo come si deve procedere per
assegnare ad esempio il colore (0, 150, 150) allo sfondo della main
window dell'applicazione FONTWIN; il problema da affrontare riguarda
il fatto che il campo hbrBackground della struttura WNDCLASSEX
richiede un HBRUSH e non un COLORREF. Per risolvere questo problema
ci viene incontro la procedura CreateSolidBrush definita nella
libreria GDI32; il prototipo di questa procedura è:
HBRUSH CreateSolidBrush(COLORREF crColor);
In pratica questa procedura riceve in input un argomento crColor di
tipo COLORREF e lo converte in un HBRUSH che ci consente di
"dipingere" con il colore richiesto; il codice di tipo HBRUSH come al
solito viene restituito in EAX. In base a queste considerazioni
possiamo scrivere (versione MASM):
MAKE_COLORREF 0, 150, 150 ; crea una terna RGB
invoke CreateSolidBrush, eax ; e la converte in HBRUSH
mov wc.hbrBackground, eax ; colore di sfondo della finestra
A questo punto possiamo divertirci a colorare lo sfondo della
finestra con tutti i colori disponibili; naturalmente, il colore di
sfondo va ad interessare solamente la client area della finestra,
mentre le altre parti (bordo, barra del titolo, etc) vengono colorate
da Windows con i colori di sistema.
A titolo di curiosità possiamo fare degli esperimenti anche con la
procedura CreateHatchBrush definita sempre nella libreria GDI32; il
prototipo di questa procedura è:
HBRUSH CreateHatchBrush(int fnStyle, COLORREF crColor);
Questa procedura converte un COLORREF in un HBRUSH che ha la
caratteristica di essere non un pennello "solido" (solid brush), ma
un pennello "retinato" (hatch brush); il parametro fnStyle è una

93
DWORD che codifica appunto lo stile di retinatura. Per conoscere i
vari stili disponibili possiamo consultare il Win32 Programmer's
Reference o gli include files windows.inc e win32.inc (i vari codici
iniziano per HS = hatch style). Per ottenere ad esempio una
retinatura incrociata a 45 gradi, possiamo scrivere:
invoke CreateHatchBrush, HS_DIAGCROSS, eax
(con EAX che deve contenere come al solito un COLORREF); nei capitoli
successivi vengono illustrate anche altre procedure che permettono di
creare sfondi più sofisticati.

Nota importante.
A rigore, per poter utilizzare un pennello non basta ottenere il suo
handle; come vedremo più avanti, una volta ottenuto un HBRUSH da
CreateSolidBrush, dobbiamo abilitare il corrispondente pennello e
dobbiamo dire a Windows in quale contesto vogliamo utilizzare questa
risorsa. Una volta che abbiamo finito di utilizzare il pennello,
dobbiamo restituirlo a Windows in modo che questa risorsa venga resa
disponibile anche ad altri programmi in esecuzione; nel caso però del
pennello utilizzato per definire il colore di sfondo di una client
area, il programmatore deve fornire solo un HBRUSH in quanto tutti
gli altri dettagli vengono gestiti da Windows.

Esaminiamo ora le modifiche apportate nella fase di creazione della


main window; questa fase come sappiamo, viene gestita attraverso la
procedura CreateWindowEx. Rispetto al caso di MAINWIN.ASM, le uniche
modifiche apportate riguardano i parametri x, y, nWidth e nHeight che
specificano le coordinate iniziali del vertice in alto a sinistra
della main window e le dimensioni orizzontale e verticale della main
window stessa; come si può notare, vengono utilizzati i valori x=10,
y=10, nWidth=600 e nHeight=550. Nel precedente capitolo, per questi
parametri abbiamo utilizzato la costante simbolica CW_USEDEFAULT che
lascia la scelta a Windows; se non si hanno esigenze particolari,
conviene sempre utilizzare CW_USEDEFAULT che permette a Windows di
stabilire i valori più opportuni in base alla risoluzione grafica
dello schermo.
Come è stato spiegato nel precedente capitolo, se la procedura
CreateWindowEx termina con successo, ci restituisce in EAX l'handle
della finestra appena creata; inoltre, prima di terminare,
CreateWindowEx invia alla window procedure il messaggio WM_CREATE.
L'applicazione FONTWIN intercetta questo messaggio per effettuare
determinate inizializzazioni; tutti i dettagli relativi a questo
aspetto vengono illustrati più avanti.

6.1 Modifiche apportate alla Window Procedure.

Nel precedente capitolo abbiamo visto che il cuore di un'applicazione


Windows è rappresentato sicuramente dalla procedura di finestra
(window procedure); ogni finestra di un programma è dotata di
un'apposita window procedure. Attraverso la window procedure, Windows
invia i messaggi alla relativa finestra; il nostro compito
all'interno della window procedure, è quello di decidere quali
messaggi accettare e quali invece rifiutare restituendoli al
mittente. Restituire un messaggio al mittente significa passarlo alla
procedura DefWindowProc che è la window procedure predefinita di
94
Windows; all'interno di DefWindowProc vengono effettuate tutte le
elaborazioni predefinite relative ai vari messaggi in arrivo. Abbiamo
anche visto che esistono particolari messaggi che dopo essere stati
intercettati ed elaborati dalla nostra window procedure, devono
essere passati ugualmente a DefWindowProc; questo è ad esempio il
caso dei due messaggi WM_CREATE e WM_CLOSE.
Per applicazioni relativamente semplici, tutto il codice per
l'elaborazione dei vari messaggi può essere inserito all'interno
della window procedure; nel caso dell'esempio MAINWIN.ASM è stata
seguita proprio questa strada. Quando però l'applicazione che stiamo
scrivendo comincia ad assumere una certa complessità, conviene
decisamente cambiare tattica scrivendo una apposita procedura per
ogni messaggio che intendiamo elaborare; questo è proprio il metodo
seguito nell'esempio FONTWIN.ASM. Per chiarire la situazione, vediamo
in Figura 3 l'aspetto assunto dalla window procedure WndProc
dell'applicazione FONTWIN (versione TASM).
Figura 3 - Window procedure WndProc
WndProc proc hWnd :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD

messaggio_WM_CREATE:
cmp uMsg, WM_CREATE ; (uMsg == WM_CREATE) ?
jne messaggio_WM_PAINT
call FontWin_OnCreate, hWnd, NULL
jmp messaggio_restituito ; passa WM_CREATE a DefWindowProc
messaggio_WM_PAINT:
cmp uMsg, WM_PAINT ; (uMsg == WM_PAINT) ?
jne messaggio_WM_CLOSE
call FontWin_OnPaint, hWnd
jmp exitWndProc
messaggio_WM_CLOSE:
cmp uMsg, WM_CLOSE ; (uMsg == WM_CLOSE) ?
jne messaggio_WM_DESTROY
call FontWin_OnClose, hWnd
cmp eax, IDYES
jne exitWndProc
jmp messaggio_restituito ; passa WM_CLOSE a DefWindowProc
messaggio_WM_DESTROY:
cmp uMsg, WM_DESTROY ; (uMsg == WM_DESTROY) ?
jne messaggio_restituito
call FontWin_OnDestroy, hWnd
jmp exitWndProc
messaggio_restituito:
call DefWindowProc, hWnd, uMsg, wParam, lParam
ret
exitWndProc:

xor eax, eax


ret

WndProc endp
Come si può notare, l'applicazione FONTWIN elabora i 4 messaggi
WM_CREATE, WM_PAINT, WM_CLOSE e WM_DESTROY; osserviamo anche che come
è stato detto in precedenza, i due messaggi WM_CREATE e WM_CLOSE dopo
essere stati intercettati ed elaborati da WndProc, vengono passati
anche a DefWindowProc attraverso un salto all'etichetta
messaggio_restituito. Per motivi di stile e di chiarezza, nella
95
sequenza dei messaggi intercettati dalla window procedure, conviene
mettere per primo il messaggio WM_CREATE; per gli stessi motivi, i
messaggi WM_CLOSE e WM_DESTROY dovrebbero occupare rispettivamente la
penultima e l'ultima posizione (vedi Figura 3).
Ciascuno dei messaggi intercettati viene elaborato in un'apposita
procedura; come accade per WndProc, possiamo scegliere liberamente i
nomi di queste procedure, mentre la lista dei parametri viene imposta
obbligatoriamente da Windows. La Microsoft consiglia di utilizzare
nomi del tipo Cls_OnMessage, dove Cls sta per class name (nome della
classe finestra); al posto di Cls bisogna quindi scrivere il nome
della classe finestra, mentre al posto di Message si deve scrivere il
nome del messaggio. In questo modo, nel caso dell'applicazione
FONTWIN, si ottengono nomi del tipo: FontWin_OnCreate,
FontWin_OnClose, FontWin_OnDestroy, etc; in tutti gli esempi
presentati nella sezione Win32 Assembly, viene seguito proprio questo
procedimento (vedi Figura 3).
Il problema fondamentale da affrontare, consiste nel reperire
informazioni relative al tipo dei parametri e degli eventuali valori
di ritorno di queste procedure; il consiglio migliore che si può dare
è quello di procurarsi una copia dell'header file windowsx.h,
distribuito insieme ai compilatori C/C++ per Windows. Questo header
file è stato introdotto dalla Microsoft ai tempi di Windows95; al suo
interno sono presenti i cosiddetti message crackers. Si tratta di una
serie di macro attraverso le quali tutto il codice mostrato in Figura
3, nel caso di un programma C/C++ può essere semplificato come si
vede in Figura 4.
Figura 4 - Struttura di WndProc in C/C++
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
HANDLE_MSG(hWnd, WM_CREATE, FontWin_OnCreate);
HANDLE_MSG(hWnd, WM_PAINT, FontWin_OnPaint);
HANDLE_MSG(hWnd, WM_CLOSE, FontWin_OnClose);
HANDLE_MSG(hWnd, WM_DESTROY, FontWin_OnDestroy);
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
Come si può notare, la macro HANDLE_MSG ha tre parametri che
rappresentano l'handle della finestra (hWnd), il messaggio da
elaborare, e l'indirizzo della funzione che elabora il messaggio;
ricordiamo che in C/C++ il nome di una funzione rappresenta
l'indirizzo della funzione stessa. All'interno dell'header file
windowsx.h è presente la dichiarazione:
#define HANDLE_MSG(hwnd, message, fn) \
case (message): return HANDLE_##message((hwnd), (wParam), (lParam),
(fn))
Nel caso ad esempio del messaggio WM_CREATE, attraverso l'operatore
di preprocessor ## del C, avviene il concatenamento delle due
stringhe HANDLE_ e WM_CREATE in modo da ottenere HANDLE_WM_CREATE;
l'espansione della macro produce quindi:
case WM_CREATE: return HANDLE_WM_CREATE((hwnd), (wParam), (lParam), (fn))
In sostanza, la macro HANDLE_MSG chiama a sua volta la macro
96
HANDLE_WM_CREATE. Cercando all'interno del file windowsx.h, ci
imbattiamo nella dichiarazione di questa seconda macro che assume la
seguente struttura:
/* BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) */

#define HANDLE_WM_CREATE(hwnd, wParam, lParam, fn) \


((fn)((hwnd), (LPCREATESTRUCT)(lParam)) ? 0L : (LRESULT)-1L)
L'espansione di questa macro porta alla chiamata:
FontWin_OnCreate(hwnd, lParam);
Dal commento presente prima della dichiarazione di HANDLE_WM_CREATE,
possiamo ricavare tutte le informazioni relative ai parametri e ai
valori di ritorno della procedura Cls_OnCreate; attraverso ulteriori
ricerche possiamo trovare i prototipi di tutte le altre procedure che
elaborano i vari messaggi. Analizziamo in particolare i prototipi
delle procedure relative ai quattro messaggi intercettati da WndProc
(Figura 3); osserviamo che queste procedure vengono utilizzate solo
dalla window procedure, per cui i loro prototipi sono stati
dichiarati prima della definizione di WndProc.

6.2 Gestione del messaggio WM_CREATE.

La procedura FontWin_OnCreate che elabora il messaggio WM_CREATE, ha


il seguente prototipo:
BOOL FontWin_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct);
Il parametro hwnd rappresenta l'handle della finestra per la quale è
stata richiesta la creazione, mentre il parametro lpCreateStruct è
l'indirizzo di una struttura di tipo CREATESTRUCT contenente una
serie di informazioni addizionali per la creazione della finestra;
nel precedente capitolo abbiamo visto che se vogliamo ricorrere a
questa struttura, dobbiamo specificare il suo indirizzo attraverso il
parametro lpParam di CreateWindowEx. In fase di creazione della
finestra, CreateWindowEx spedisce un messaggio WM_CREATE; se
decidiamo di intercettare questo messaggio, possiamo reperire
l'indirizzo della struttura CREATESTRUCT attraverso il parametro
lpParam di WndProc (Figura 3). Nel caso dell'applicazione FONTWIN,
questa struttura non viene utilizzata, per cui la procedura
FontWin_OnCreate viene chiamata con il secondo argomento che vale
NULL; la struttura CREATESTRUCT verrà descritta al momento opportuno
in un prossimo capitolo.
L'invio del messaggio WM_CREATE da parte della procedura
CreateWindowEx assume una notevole importanza; infatti, intercettando
questo messaggio possiamo effettuare una serie di inizializzazioni
prima che la finestra compaia sullo schermo. Utilizzando la
terminologia dei linguaggi ad oggetti come il C++, possiamo dire che
la procedura che elabora WM_CREATE rappresenta il costruttore della
classe finestra che stiamo creando.
Una osservazione importante riguarda il fatto che il messaggio
WM_CREATE viene inviato dall'interno della procedura CreateWindowEx;
questo significa che ancora non abbiamo a disposizione il valore di
ritorno di CreateWindowEx (handle della finestra) da memorizzare
nella variabile globale hWindow definita nel segmento _BSS. La
procedura FontWin_OnCreate deve utilizzare quindi l'argomento hWnd
inviato a WndProc da CreateWindowEx; in sostanza, all'interno di
FontWin_OnCreate, la variabile hWindow non ha ancora nessun
97
significato e non deve quindi essere usata.
Le vecchie versioni di Windows, richiedevano che la procedura
Cls_OnCreate dovesse terminare restituendo in EAX il valore zero per
indicare il successo della fase di inizializzazione; nelle attuali
versioni di win32 questo valore di ritorno viene ignorato, per cui
possiamo simbolicamente caricare in EAX il valore TRUE.

6.3 Gestione del messaggio WM_CLOSE.

Il messaggio WM_CLOSE viene elaborato dalla procedura


FontWin_OnClose; questa procedura ha il seguente prototipo:
void FontWin_OnClose(HWND hwnd);
Il messaggio WM_CLOSE viene inviato dal SO ogni volta che cerchiamo
di chiudere una finestra; molte applicazioni hanno la necessità di
intercettare questo messaggio per evitare situazioni spiacevoli.
Pensiamo ad esempio al caso di un elaboratore di testi
(wordprocessor) che contiene informazioni non ancora salvate sul
disco; se chiudiamo l'applicazione senza intercettare il messaggio
WM_CLOSE, tutte le informazioni non salvate vengono perse senza che
l'utente abbia la possibilità di intervenire. Le situazioni di questo
genere possono essere evitate intercettando ed elaborando il
messaggio WM_CLOSE; l'elaborazione di questo messaggio consiste in
genere nella visualizzazione di un messaggio di avvertimento che
fornisce all'utente la possibilità di confermare o meno la richiesta
di chiusura dell'applicazione. Nel caso in cui la chiusura venga
confermata, è importantissimo passare il messaggio WM_CLOSE anche
alla window procedure predefinita DefWindowProc; in questo modo il SO
può effettuare le necessarie deinizializzazioni e può inviare infine
il messaggio WM_DESTROY che autorizza la terminazione
dell'applicazione.
Come si nota dal prototipo, la procedura Cls_OnClose richiede un
argomento di tipo HWND che rappresenta l'handle della finestra per la
quale è stata richiesta la chiusura; la procedura Cls_OnClose termina
senza nessun valore di ritorno (void).

6.4 Gestione del messaggio WM_DESTROY.

Il messaggio WM_DESTROY viene elaborato dalla procedura


FontWin_OnDestroy; questa procedura ha il seguente prototipo:
void FontWin_OnDestroy(HWND hwnd);
Il messaggio WM_DESTROY viene inviato dal SO per autorizzare la
chiusura di una finestra; in sostanza, con il messaggio WM_DESTROY il
SO ci sta informando del fatto che le varie deinizializzazioni
predefinite sono state portate a termine con successo, per cui la
chiusura della finestra può avvenire in modo corretto. Intercettando
questo messaggio, possiamo effettuare anche noi le nostre
deinizializzazioni; questa fase consiste in genere nella restituzione
al SO delle varie risorse utilizzate dalla finestra che vogliamo
chiudere. Come già sappiamo, win32 e in particolare Windows2000 e
WindowsXP, sono in grado di effettuare automaticamente questo lavoro;
in ogni caso, per questioni di chiarezza e di stile, è meglio
svolgere questa fase in modo esplicito.
Dalle considerazioni appena esposte, risulta evidente il ruolo svolto

98
dalle due procedure Cls_OnCreate e Cls_OnDestroy; così come
Cls_OnCreate rappresenta il costruttore della classe finestra che
vogliamo creare, allo stesso modo Cls_OnDestroy rappresenta il
distruttore della classe finestra che vogliamo "distruggere".
Prima di terminare, Cls_OnDestroy deve chiamare la procedura
PostQuitMessage; come già sappiamo, questa procedura inserisce il
messaggio WM_QUIT nella coda dei messaggi della finestra che
intendiamo chiudere. Nel precedente capitolo abbiamo anche visto che
il parametro nExitCode richiesto da PostQuitMessage, rappresenta il
codice di uscita (exit code) della nostra applicazione; all'interno
del loop dei messaggi, la successiva chiamata di GetMessage determina
l'estrazione del messagio WM_QUIT. In questo caso, la procedura
GetMessage restituisce zero in EAX, determinando in questo modo la
condizione di uscita dal loop dei messaggi; il contenuto di nExitCode
può essere reperito nel campo wParam della struttura di tipo MSG
passata a GetMessage.
Come si nota dal prototipo, la procedura Cls_OnDestroy richiede un
argomento di tipo HWND che rappresenta l'handle della finestra per la
quale è stata richiesta la chiusura; la procedura Cls_OnDestroy
termina senza nessun valore di ritorno (void).

6.5 Gestione del messaggio WM_PAINT.

Il messaggio WM_PAINT viene elaborato dalla procedura


FontWin_OnPaint; questa procedura ha il seguente prototipo:
void FontWin_OnPaint(HWND hwnd);
In Windows il messaggio WM_PAINT assume una importanza fondamentale;
questo messaggio viene inviato automaticamente dal SO ogni volta che
la client area di una finestra ha bisogno di un aggiornamento. Questa
situazione si verifica ad esempio quando modifichiamo le dimensioni
orizzontale e verticale di una finestra, quando ripristiniamo una
finestra dopo averla ridotta ad icona, quando una finestra viene
portata in primo piano dopo essere stata parzialmente coperta da
altre finestre, etc; in tutti questi casi, si dice tecnicamente che
il contenuto della finestra si "sporca" e deve quindi essere
ripristinato. Per poter procedere a questa fase di ripristino,
dobbiamo chiaramente intercettare il messaggio WM_PAINT;
l'elaborazione del messaggio WM_PAINT ci offre l'opportunità di
conoscere alcune importanti caratteristiche di Windows.
In Windows, prima di poter visualizzare qualsiasi cosa nella client
area di una finestra, dobbiamo richiedere al SO un cosiddetto Device
Context; per capire il significato del Device Context possiamo
servirci della Figura 5. Supponiamo che il blocco Device
(dispositivo), rappresenti la scheda video del nostro computer; in
questo caso, il blocco Device Driver (pilota di dispositivo)
rappresenta il software sviluppato per Windows dal produttore della
scheda video. Attraverso questo software, Windows può accedere
direttamente all'hardware della scheda video sfruttandone tutte le
caratteristiche; il Device Driver permette quindi a Windows di
selezionare le varie modalità video (CGA, EGA, VGA, SVGA, etc), di
selezionare il numero di colori da utilizzare, di visualizzare
materialmente sullo schermo l'output del nostro programma, e così
via. In ambiente DOS, un programma che vuole inviare il suo output
allo schermo deve gestire in proprio tutti questi dettagli hardware;
99
in ambiente Windows invece, le applicazioni delegano tutto questo
lavoro al Device Context (contesto di dispositivo). Possiamo dire
quindi che il Device Context rappresenta un'interfaccia tra le
applicazioni Windows e il Device Driver di un dispositivo di output
che può essere lo schermo, la stampante, il plotter, etc; in questo
modo si ottiene una enorme semplificazione nella scrittura delle
applicazioni. L'aspetto negativo è rappresentato dal fatto che tutti
questi passaggi, portano ad una inevitabile diminuzione delle
prestazioni generali dell'interfaccia grafica di Windows; d'altra
parte, in assenza di questi accorgimenti, una applicazione Windows
sarebbe libera di mostrare il suo output in qualunque punto dello
schermo, anche al di fuori della client area.

In definitiva, se decidiamo di intercettare il messaggio WM_PAINT,


prima di "scrivere" qualsiasi cosa sulla client area della nostra
finestra dobbiamo procurarci un Device Context; a tale proposito,
Windows ci mette a disposizione diverse procedure come BeginPaint e
GetDC. È fondamentale che in relazione al messaggio WM_PAINT venga
utilizzata esclusivamente la procedura BeginPaint; questa procedura
infatti è stata concepita esplicitamente per questo particolare
messaggio. La procedura BeginPaint è definita nella libreria USER32;
il suo prototipo è il seguente:
HDC BeginPaint(HWND hwnd, LPPAINTSTRUCT lpPaint);
Il parametro hwnd rappresenta come al solito l'handle della finestra
nella quale vogliamo scrivere; il parametro lpPaint rappresenta una
DWORD contenete l'indirizzo di una struttura di tipo PAINTSTRUCT i
cui campi verranno riempiti da BeginPaint. Nell'include file
windows.inc o win32.inc, troviamo la seguente dichiarazione della
struttura PAINTSTRUCT:
PAINTSTRUCT STRUC

hdc HDC ? ; handle del DC da usare


fErase BOOL ? ; flag aggiornamento sfondo
rcPaint RECT < ? > ; area di output
fRestore BOOL ? ; riservato a Win32
fIncUpdate BOOL ? ; riservato a Win32
rgbReserved BYTE 32 dup (?) ; riservato a Win32

PAINTSTRUCT ENDS
Il campo hdc è una DWORD contenente un codice numerico di tipo HDC
(handle to device context); questo campo viene quindi riempito da
BeginPaint con l'handle del device context che utilizzeremo per
disegnare.
Il campo fErase è un valore booleano che indica se deve essere
aggiornato anche lo sfondo della finestra; se fErase vale TRUE, verrà
aggiornato anche lo sfondo.
Il campo rcPaint è una struttura di tipo RECT (rettangolo) che indica
il rettangolo della client area interessato all'aggiornamento; in
questo modo Windows ha la possibilità di migliorare le prestazioni
del programma in quanto viene aggiornata solo la porzione della
finestra che è stata sporcata.
La struttura RECT viene dichiarata nel file windows.inc o win32.inc
nel seguente modo:
RECT STRUC

100
left LONG ? ; ascissa P1
top LONG ? ; ordinata P1
right LONG ? ; ascissa P2
bottom LONG ? ; ordinata P2

RECT ENDS
In pratica, left e top rappresentano il vertice in alto a sinistra
del rettangolo; right e bottom rappresentano invece il vertice in
basso a destra del rettangolo.
I campi fRestore, fIncUpdate e rgbReserved della struttura
PAINTSTRUCT sono riservati a Windows e non devono essere
assolutamente modificati dall'utente.

Nota importante.
Come al solito, gli utilizzatori del TASM devono affrontare il
problema della visibilità globale dei nomi usati per i vari campi
delle strutture; in sostanza, se ad esempio nel nostro programma
definiamo una variabile chiamata left, questo nome entra in conflitto
con il campo left della struttura RECT. Per evitare questo problema,
bisogna modificare i nomi dei campi delle varie strutture predefinite
di Windows; se si osserva il file win32.inc si nota che ad esempio,
nel caso della struttura RECT, i vari campi sono stati rinominati in
rc_left, rc_top, rc_right, rc_bottom.
Naturalmente, le stesse considerazioni valgono per la struttura
PAINTSTRUCT e per molte altre strutture predefinite di Windows; il
programmatore è libero di modificare a sua volta i nomi di questi
campi adattandoli alle proprie esigenze o ai propri gusti.

La procedura BeginPaint termina restituendo in EAX il device context


che avevamo richiesto; se la richiesta fallisce, il registro EAX
contiene il valore NULL. Una volta che abbiamo ottenuto il nostro
device context, siamo pronti per disegnare nella client area della
finestra da aggiornare; in sostanza, elaborare il messaggio WM_PAINT
significa dire a Windows in cosa consiste l'output che il nostro
programma visualizza nella client area della sua finestra.
Per indicare a Windows che l'elaborazione del messaggio WM_PAINT è
terminata, dobbiamo utilizzare la procedura EndPaint definita
anch'essa nella libreria USER32; il prototipo di questa procedura è
il seguente:
BOOL EndPaint(HWND hwnd, CONST LPPAINTSTRUCT lpPaint);
Il parametro hwnd indica l'handle della finestra appena aggiornata;
il parametro lpPaint è una DWORD contenente l'indirizzo della
struttura di tipo PAINTSTRUCT precedentemente riempita da BeginPaint.
La procedura EndPaint, termina sempre con un valore non nullo in EAX;
questo valore può essere tranquillamente ignorato.

Per elaborare il messaggio WM_PAINT utilizziamo quindi la procedura


FontWin_OnPaint che, come si nota dal prototipo, richiede come
argomento l'handle della finestra da aggiornare, e termina senza
nessun valore di ritorno (void); dalle considerazioni appena esposte,
possiamo definire il seguente scheletro di una generica procedura
Cls_OnPaint (versione MASM):
Cls_OnPaint proc hwnd :HWND

101
LOCAL paintStruct :DWORD
LOCAL paintDC :DWORD

invoke BeginPaint, hwnd, addr paintStruct


mov paintDC, eax

; elaborazione di WM_PAINT

invoke EndPaint, hwnd, addr paintStruct

ret

Cls_OnPaint endp

Dopo aver illustrato le caratteristiche generali delle procedure che


elaborano i quattro messaggi intercettati dalla window procedure di
FONTWIN, possiamo passare all'analisi delle due principali procedure
che vengono utilizzate in Windows per l'output di stringhe di testo
sullo schermo.

6.6 La procedura TextOut.

La prima procedura che andiamo ad esaminare viene chiamata TextOut;


questa procedura che viene definita nella libreria GDI32, presenta il
seguente prototipo C:
BOOL TextOut(HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int
cbString);
Il parametro hdc rappresenta l'handle del device context che vogliamo
usare per disegnare.
Il parametro nXStart è una DWORD che rappresenta l'ascissa del punto
della client area dal quale inizia l'output.
Il parametro nYStart è una DWORD che rappresenta l'ordinata del punto
della client area dal quale inizia l'output.
Il parametro lpString è una DWORD che rappresenta l'indirizzo di una
stringa ASCII, non necessariamente terminata da uno zero; questa è
ovviamente la stringa che verrà stampata sullo schermo.
Il parametro cbString è una DWORD che rappresenta il numero di byte
(caratteri) che formano la stringa.
Se TextOut termina con successo, restituisce un valore non nullo in
EAX; se invece TextOut fallisce, restituisce zero in EAX.

Supponiamo ad esempio di voler stampare la stringa strOut1 definita


nel blocco _DATA mostrato in Figura 1; approfittando del fatto che
questa stringa viene definita staticamente, possiamo determinare
facilmente la sua lunghezza con il solito metodo del location
counter.
strOut1 db 'Output di stringhe di testo in Win32'
LEN_STROUT1 = $ - offset strOut1
A questo punto possiamo visualizzare la stringa strOut1 scrivendo:
invoke TextOut, paintDC, 10, 10, offset strOut1, LEN_STROUT1
In questo modo, la stringa strOut1 viene stampata a partire dal punto
(10, 10) della client area; le coordinate sono espresse in pixel e
sono riferite al vertice in alto a sinistra della client area. È
necessario ricordare che il sistema di riferimento cartesiano dello
102
schermo del computer, ha l'origine nel vertice in alto a sinistra; le
ascisse crescono da sinistra verso destra, mentre le ordinate
crescono dall'alto verso basso (in sostanza, l'asse y punta verso il
basso).
Analizzando gli effetti prodotti da questa istruzione, ci accorgiamo
che l'area della finestra interessata dall'output della stringa, ha
subito una modifica dello sfondo; quest'area, prende il nome di
sfondo della stringa. In particolare, si osserva che Windows stampa
le stringhe utilizzando un colore predefinito per il testo, chiamato
foreground color (colore di primo piano); analogamente, lo sfondo
della stringa viene riempito con un colore predefinito chiamato
background color (colore di sfondo). Generalmente, in assenza di
indicazioni da parte del programmatore, Windows utilizza il bianco
per lo sfondo, e il nero per il primo piano; naturalmente Windows ci
mette a disposizione tutti gli strumenti necessari per modificare sia
il colore di sfondo, sia il colore di primo piano.
Per modificare il colore di sfondo possiamo utilizzare la procedura
SetBkColor; questa procedura viene definita nella libreria GDI32, ed
ha il seguente prototipo:
COLORREF SetBkColor(HDC hdc, COLORREF crColor);
Il parametro hdc rappresenta l'handle del device context che stiamo
usando per scrivere.
Il parametro crColor rappresenta il colore in formato COLORREF che
vogliamo usare per lo sfondo della stringa da visualizzare.
Se SetBkColor termina con successo, restituisce in EAX il precedente
colore di sfondo in formato COLORREF; se SetBkColor fallisce,
restituisce in EAX la costante simbolica CLR_INVALID.
Volendo definire ad esempio uno sfondo nero per l'output, possiamo
scrivere:
MAKE_COLORREF 0, 0, 0
invoke SetBkColor, paintDC, eax
Per modificare il colore di primo piano, possiamo utilizzare la
procedura SetTextColor. Questa procedura viene definita nella
libreria GDI32, ed ha il seguente prototipo:
COLORREF SetTextColor(HDC hdc, COLORREF crColor);
Il significato dei parametri è identico al caso di SetBkColor.
Se SetTextColor termina con successo, restituisce in EAX il
precedente colore di primo piano in formato COLORREF; se SetTextColor
fallisce, restituisce in EAX la costante simbolica CLR_INVALID.
Volendo usare ad esempio un colore rosso intenso per il testo,
possiamo scrivere:
MAKE_COLORREF 200, 0, 0
invoke SetTextColor, paintDC, eax
Windows ci permette anche di definire con precisione la modalità di
modifica dello sfondo dell'area interessata dall'output; a tale
proposito possiamo servirci della procedura SetBkMode. Questa
procedura viene definita nella libreria GDI32, e presenta il seguente
prototipo:
int SetBkMode(HDC hdc, int iBkMode);
Il parametro hdc è il solito handle del device context che stiamo
utilizzando per scrivere nella client area della nostra finestra.
Il parametro iBkMode è una DWORD che codifica la modalità di modifica
dello sfondo; le due modalità più utilizzate sono espresse dalle due
costanti simboliche OPAQUE e TRANSPARENT. La modalità OPAQUE indica
103
che allo sfondo verrà assegnato il colore predefinito di Windows o il
colore che eventualmente abbiamo scelto con SetBkColor; la modalità
TRANSPARENT indica che l'output non provocherà nessuna modifica dello
sfondo (output trasparente).
Se SetBkMode termina con successo, restituisce in EAX il codice della
precedente modalità di modifica dello sfondo; se SetBkMode fallisce,
restituisce in EAX il valore zero.
Osserviamo che utilizzando SetBkMode con la costante TRANSPARENT, non
abbiamo la necessità di preoccuparci del colore di sfondo
dell'output; in definitiva, se vogliamo stampare la stringa strOut1
con il colore (0, 250, 0) e senza alterare lo sfondo, possiamo
scrivere (versione MASM):
invoke SetBkMode, paintDC, TRANSPARENT
MAKE_COLORREF 0, 250, 0
invoke SetTextColor, paintDC, eax
invoke TextOut, paintDC, 10, 10, offset strOut1, LEN_STROUT1

6.7 La procedura DrawText.

Se abbiamo bisogno di una gestione più sofisticata dell'output,


possiamo servirci della procedura DrawText; questa procedura viene
definita nella libreria USER32 e presenta il seguente prototipo:
int DrawText(HDC hdc, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT
uFormat);
Il parametro hdc è l'handle del device context che vogliamo
utilizzare per l'output.
Il parametro lpString è una DWORD contenete l'indirizzo di una
stringa ASCII, non necessariamente terminata da uno zero; questa è la
stringa che verrà stampata sullo schermo da DrawText.
Il parametro nCount è una DWORD che indica la lunghezza in byte della
stringa da stampare; se lpString contiene l'indirizzo di una stringa
C, e vogliamo delegare a DrawText il calcolo della lunghezza della
stringa, dobbiamo passare nCount = -1.
Il parametro lpRect è una DWORD contenente l'indirizzo di una
struttura di tipo RECT; la stringa da stampare, verrà formattata
all'interno di questo rettangolo.
Il parametro uFlags, è una DWORD contenente una serie di flags che
indicano il tipo di formattazione che DrawText applicherà alla
stringa; i vari flags possono essere combinati tra loro con
l'operatore OR dell'Assembly.
Per conoscere i numerosi flags disponibili, conviene consultare il
Win32 Programmer's Reference; nell'esempio FONTWIN, viene utilizzata
la combinazione di flags:
DT_SINGLELINE OR DT_CENTER OR DT_VCENTER
In base a questa combinazione, all'interno dell'area rettangolare
passata a DrawText attraverso lpRect, verrà stampata una stringa su
una sola linea (DT_SINGLELINE); questa stringa verrà inoltre centrata
orizzontalmente (DT_CENTER) e verticalmente (DT_VCENTER) all'interno
del rettangolo.

Supponiamo ad esempio di voler stampare la stringa strOut3 definita


nel blocco _DATA di Figura 1; come si può notare, si tratta di una
stringa C (zero terminated string). Supponiamo inoltre di voler
104
stampare questa stringa esattamente al centro della client area della
main window dell'applicazione FONTWIN; per individuare il rettangolo
che rappresenta l'intera client area, possiamo utilizzare la
procedura GetClientRect. Questa procedura viene definita nella
libreria USER32 e presenta il seguente prototipo:
BOOL GetClientRect(HWND hwnd, LPRECT lpRect);
Il parametro hwnd rappresenta l'handle della finestra di cui vogliamo
determinare le coordinate della client area.
Il parametro lpRect rappresenta una DWORD contenente l'indirizzo di
una struttura di tipo RECT che verrà riempita da GetClientRect con le
informazioni richieste; considerando il fatto che le coordinate sono
riferite al vertice in alto a sinistra della client area (0, 0), si
otterrà ovviamente left=0, top=0, mentre right e bottom conterranno
rispettivamente la larghezza e l'altezza della client area.
La prima cosa da fare consiste quindi nel definire una struttura di
tipo RECT chiamata ad esempio paintRect; a questo punto, se vogliamo
stampare la stringa con il colore (150, 0, 0), possiamo scrivere
(versione MASM):
invoke GetClientRect, hwnd, addr paintRect
MAKE_COLORREF 150, 0, 0
invoke SetTextColor, paintDC, eax
invoke DrawText, paintDC, addr strOut3, -1, addr paintRect, DT_SINGLELINE
OR DT_CENTER OR DT_VCENTER
Come possiamo notare, per quanto riguarda il colore di sfondo, il
colore di primo piano e la modalità di modifica dello sfondo, valgono
tutte le considerazioni già svolte per TextOut; in sostanza, anche
con DrawText possiamo utilizzare le procedure SetBkMode, SetBkColor e
SetTextColor.

Se DrawText termina con successo, restituisce in EAX l'altezza del


testo appena stampato; in caso di fallimento DrawText restituisce in
EAX il valore zero.

6.8 I font di Windows.

Abbiamo visto quindi che in assenza di indicazioni da parte del


programmatore, le due procedure TextOut e DrawText utilizzano valori
predefiniti per il colore di sfondo, per il colore di primo piano e
per la modalità di modifica dello sfondo dell'output; un altro
aspetto importante, è dato dal fatto che queste due procedure
stampano il loro output utilizzando un tipo di carattere avente uno
stile grafico predefinito. L'aspetto grafico (estetico) di un
carattere prende il nome di font; Windows dispone di un numerosissimo
insieme di font utilizzabili per conferire alle stringhe un aspetto
grafico di elevata qualità.
Se non selezioniamo nessun font particolare, Windows utilizza un font
predefinito chiamato font di sistema; generalmente il font
predefinito è un tipo di carattere a spaziatura fissa simile a quello
utilizzato dal DOS. Se ci serve invece un font più elegante, dobbiamo
richiederlo a Windows attraverso l'invio di una serie di opportune
informazioni.
Il primo passo da compiere consiste nel definire tutte le
caratteristiche grafiche del font che vogliamo selezionare; a tale
proposito dobbiamo riempire i campi di una struttura di tipo LOGFONT.
105
Nel file windows.inc o win32.inc troviamo la seguente dichiarazione
di questa struttura:
LOGFONT STRUC

lfHeight LONG ? ; altezza cella carattere


lfWidth LONG ? ; larghezza cella carattere
lfEscapement LONG ? ; inclinazione del testo in gradi * 10
lfOrientation LONG ? ; inclinazione dei char in gradi * 10
lfWeight LONG ? ; peso del font (0 - 1000)
lfItalic BYTE ? ; TRUE = font italic
lfUnderline BYTE ? ; TRUE = font sottolineato
lfStrikeOut BYTE ? ; TRUE = font sbarrato
lfCharSet BYTE ? ; set di caratteri
lfOutPrecision BYTE ? ; precisione nell’ output del font
lfClipPrecision BYTE ? ; precisione nel clipping del font
lfQuality BYTE ? ; qualità dell'output
lfPitchAndFamily BYTE ? ; tipo e famiglia del font
lfFaceName TCHAR LF_FACESIZE dup (?) ; nome del font

LOGFONT ENDS
Il campo lfHeight indica l'altezza del font.
Il campo lfWidth indica la larghezza media del font; si tratta di un
valore medio in quanto esistono caratteri (come la 'm') che sono
molto più larghi di altri (come la 'ì). Specificando il valore zero
in questo campo, lasciamo che la larghezza più adatta venga calcolata
da Windows.
Il campo lfEscapement indica l'angolo che la stringa deve formare
rispetto alla linea orizzontale che rappresenta la base dello
schermo; questo angolo è espresso in gradi e deve essere moltiplicato
per 10. Per richiedere ad esempio un'inclinazione del testo di 45
gradi, dobbiamo caricare nel campo lfEscapement il valore 450.
Il campo lfOrientation indica l'angolo che ogni carattere della
stringa deve formare rispetto alla linea orizzontale che rappresenta
la base dello schermo; anche questo valore deve essere moltiplicato
per 10. Attualmente questa caratteristica non è ancora supportata da
Windows.
Il campo lfWeight indica il peso del font, cioè la sua consistenza;
il peso deve essere un valore compreso tra 0 e 1000 e può variare
solo a salti di 100 unità. Il valore 400 viene indicato come normal
(testo normale); il valore 700 viene indicato come bold (testo in
grassetto).
Il campo lfItalic è un valore booleano che indica se il testo deve
essere stampato in stile italic (inclinato verso destra); se questo
campo vale TRUE (non-zero), verrà attivato lo stile italic.
Il campo lfUnderline è un valore booleano che indica se il testo deve
essere sottolineato; se questo campo vale TRUE (non-zero), il testo
verrà sottolineato.
Il campo lfStrikeOut è un valore booleano che indica se il testo deve
essere sbarrato; se questo campo vale TRUE (non-zero), il testo verrà
sbarrato.
Il campo lfCharSet indica la nazionalità del set di caratteri da
utilizzare; generalmente questo campo vale zero per indicare il set
di caratteri ANSI (si può usare anche la costante simbolica
ANSI_CHARSET).
Il campo lfOutPrecision indica con quale precisione Windows
106
rispetterà le richieste del programmatore relative ai campi lfHeight,
lfWidth, lfEscapement e lfOrientation; il valore di questo campo
indica anche a Windows a quale famiglia deve appartenere il font di
riserva da utilizzare se il font che abbiamo richiesto non è
disponibile. La famiglia di appartenenza del font viene specificata
dal campo lfPitchAndFamily; per obbligare ad esempio Windows ad usare
solo font True Type (ad alta qualità), dobbiamo specificare nel campo
lfOutPrecision il valore rappresentato dalla costante simbolica
OUT_TT_ONLY_PRECIS.
Il campo lfClipPrecision indica con quale precisione Windows
"taglierà" la porzione di testo che sconfina dalla client area o che
viene coperta da un'altra finestra; il taglio delle porzioni di
finestra non visibili, viene chiamato tecnicamente clipping.
Generalmente per questo campo si può utilizzare la costante simbolica
CLIP_DEFAULT_PRECIS.
Il campo lfQuality indica la qualità generale dell'output; possiamo
utilizzare ad esempio le costanti simboliche DEFAULT_QUALITY,
DRAFT_QUALITY, PROOF_QUALITY, etc.
Il campo lfPitchAndFamily indica il tipo e la famiglia di
appartenenza del font che vogliamo selezionare; come è stato detto in
precedenza, Windows utilizza queste informazioni per poter reperire
eventuali sostituti del font che abbiamo richiesto. I primi 4 bit di
questo campo indicano il tipo di font (fisso o variabile); i
successivi bit indicano la famiglia di appartenenza (modern, roman,
swiss, etc). Come al solito, per combinare tra loro il tipo e la
famiglia di un font, si può utilizzare l'operatore OR dell'Assembly;
volendo utilizzare ad esempio un font di tipo variabile della
famiglia roman, dobbiamo caricare nel campo lfPitchAndFamily il
valore:
VARIABLE_PITCH OR FF_ROMAN
Il campo lfFaceName deve contenere il nome utilizzato da Windows per
identificare il font da selezionare; tra i vari nomi si possono
citare ad esempio Times New Roman, Arial, Courier New, etc (la
distinzione tra maiuscole e minuscole non è importante). Come si può
notare, questo campo è un vettore formato da LF_FACESIZE byte (TCHAR
= BYTE). La costante LF_FACESIZE vale 32, e questo significa che il
nome del font non può superare i 32 byte di lunghezza; in questi 32
byte deve trovare posto anche lo zero finale in quanto il nome di un
font deve essere espresso sotto forma di stringa C.

Per conoscere l'enorme numero di costanti simboliche utilizzabili con


questi campi, si consiglia di consultare il Win32 Programmer's
Reference o gli include files windows.inc e win32.inc; è importante
che il programmatore utilizzi queste costanti simboliche per
sperimentare in pratica l'effetto che esse producono.

Una volta che abbiamo riempito i vari campi della struttura di tipo
LOGFONT, dobbiamo procedere con la creazione del font avente le
caratteristiche richieste; a tale proposito, viene utilizzata la
procedura CreateFontIndirect. Questa procedura viene definita nella
libreria GDI32, e presenta il seguente prototipo:
HFONT CreateFontIndirect(CONST LPLOGFONT lplf);
Il parametro lplf è una DWORD contenente l'indirizzo di una struttura
di tipo LOGFONT; naturalmente questa struttura deve essere stata già
107
riempita con tutte le informazioni descritte in precedenza.
Se CreateFontIndirect termina con successo, restituisce in EAX
l'handle del font (tipo HFONT) appena installato; se
CreateFontIndirect fallisce, restituisce in EAX il valore NULL.
Un concetto molto importante in Windows è rappresentato dal fatto che
i vari oggetti grafici come i font, le icone, i cursori, le bitmap,
etc, non sono disponibili in numero illimitato; non bisogna
dimenticare inoltre che in un SO multitasking come Windows, ci
possono essere più programmi contemporaneamente in esecuzione,
ciascuno dei quali può aver bisogno di un certo numero di oggetti
grafici. Tutto ciò significa che le applicazioni Windows devono
comportarsi in modo responsabile, evitando di sprecare inutilmente
queste importanti risorse; in sostanza, ogni applicazione deve
richiedere un determinato oggetto grafico nel momento in cui ne ha
effettivamente bisogno, e deve poi restituirlo quando non gli serve
più.
Vediamo allora come ci si deve comportare nel caso della risorsa di
tipo font; prima di tutto definiamo una variabile globale destinata a
contenere l'handle di un font. In Figura 1 notiamo appunto la
presenza di una variabile chiamata fontHandle; questa variabile viene
inizializzata con il valore zero per indicare che inizialmente non
contiene nessun handle. Ogni volta che vogliamo utilizzare un nuovo
font, dobbiamo restituire a Windows l'eventuale handle del font
precedentemente utilizzato; a tale proposito possiamo servirci della
procedura DeleteObject che provvede a liberare tutte le risorse
associate ad un determinato handle. Questa procedura viene definita
nella libreria GDI32, e presenta il seguente prototipo:
BOOL DeleteObject(HGDIOBJ hObject);
Il parametro hObject rappresenta l'handle di una risorsa grafica che
può essere un font, una bitmap, un pennello, etc; la procedura
DeleteObject restituisce in EAX un valore non nullo quando termina
con successo.
Definiamo poi nel segmento dati non inizializzati (_BSS) una
struttura di tipo LOGFONT, che nel caso del nostro esempio viene
chiamata fontStruct (Figura 1); questa struttura naturalmente deve
essere riempita con le necessarie informazioni. A questo punto, per
creare un nuovo font avente le caratteristiche che abbiamo richiesto,
possiamo scrivere (versione MASM):
cmp fontHandle, 0
je short continue
invoke DeleteObject, fontHandle
continue:
invoke CreateFontIndirect, addr fontStruct
mov fontHandle, eax
In questo modo otteniamo nella variabile fontHandle, l'handle del
nuovo font che abbiamo appena creato; prima di poter utilizzare
fisicamente questo font, dobbiamo selezionarlo attraverso la
procedura SelectObject. Il compito di SelectObject è quello di dire
al nostro device context che il font corrente verrà sostituito da un
nuovo font; la procedura SelectObject viene definita nella libreria
GDI32, e presenta il seguente prototipo:
HGDIOBJ SelectObject(HDC hdc, HGDIOBJ hgdiobj);
Il parametro hdc è l'handle del device context che vogliamo
utilizzare per l'output.

108
Il parametro hgdiobj è l'handle dell'oggetto che vogliamo
selezionare; nel nostro caso si tratta dell'handle di un font
(fontHandle).
Se la procedura SelectObject fallisce, termina restituendo in EAX il
valore NULL; se la procedura SelectObject termina con successo,
restituisce in EAX l'handle dell'oggetto appena sostituito (nel
nostro caso si tratta dell'handle del precedente font). È
fondamentale che l'handle restituito in EAX da SelectObject, venga
salvato da qualche parte; infatti, quando abbiamo finito il nostro
lavoro, dobbiamo chiamare nuovamente SelectObject per ripristinare il
font precedente. Supponiamo ad esempio di voler utilizzare TextOut
per visualizzare la stringa strOut1 con il nuovo font che abbiamo
creato; prima di tutto definiamo una variabile locale chiamata ad
esempio oldFont e destinata a contenere l'handle del font corrente
che dovremo poi ripristinare. A questo punto possiamo scrivere
(versione MASM):
invoke SelectObject, paintDC, fontHandle
mov oldFont, eax
invoke TextOut, paintDC, 10, 10, addr strOut1, LEN_STROUT1
invoke SelectObject, paintDC, oldFont
Come si può notare, prima di tutto selezioniamo il nuovo font con
SelectObject; l'handle del vecchio font, restituitoci da SelectObject
in EAX, viene salvato nella variabile locale oldFont. In seguito
viene chiamata TextOut che stampa la stringa strOut1 con il nuovo
font; al termine di questa fase, procediamo a ripristinare il vecchio
font attraverso una nuova chiamata a SelectObject.

6.9 Implementazione di FONTWIN.ASM.

Dopo aver illustrato gli aspetti teorici relativi alla gestione del
testo in Windows, possiamo passare all'implementazione pratica di
questi concetti nell'applicazione FONTWIN; il programma può essere
suddiviso in tre blocchi fondamentali che gestiscono le fasi di
inizializzazione, invio dell'output alla client area e ripristino dei
valori.
È chiaro che il luogo più adatto nel quale inserire la fase di
inizializzazione, è rappresentato dalla procedura FontWin_OnCreate
che viene chiamata da WndProc in risposta al messaggio WM_CREATE; in
particolare, all'interno di questa procedura possiamo inserire anche
l'inizializzazione della struttura di tipo LOGFONT. Nel blocco dati
non inizializzati _BSS, definiamo quindi la struttura fontStruct;
bisogna tenere presente che non è obbligatorio riempire tutti i campi
di questa struttura (come ad esempio lfWidth). È importante però che
tutti i campi non inizializzati, vengano riempiti con degli zeri; in
questo modo Windows può inizializzare questi campi con dei valori
predefiniti. Il procedimento più sicuro consiste allora
nell'inizializzare tutta la struttura fontStruct riempiendo tutti i
suoi campi con degli zeri; a tale proposito possiamo scrivere
un'apposita procedura MemSet che assume il seguente aspetto:
MemSet proc ptrMem :DWORD, fillValue :DWORD, fillSize :DWORD

push edi ; salva il contenuto di edi

mov edi, ptrMem ; edi punta al blocco di memoria da inizializzare


109
mov eax, fillValue ; al = byte ptr fillValue[0] (inizializzatore)
mov ecx, fillSize ; ecx = n. byte da riempire (contatore)
cld ; DF = 0 (incremento automatico puntatori)
rep stosb ; riempie [ptrMem] con fillValue byte

pop edi ; ripristina il vecchio contenuto di edi


ret

MemSet endp
Il parametro ptrMem rappresenta l'indirizzo del blocco di memoria da
inizializzare; il parametro fillValue rappresenta il valore di
inizializzazione, mentre il parametro fillSize indica la dimensione
in byte del blocco di memoria da riempire. Come si può notare, MemSet
utilizza l'istruzione STOSB; in modalità protetta a 32 bit, questa
istruzione utilizza AL come operando sorgente, e [EDI] come operando
destinazione. MemSet preserva il contenuto originale di EDI in
quanto, come è stato detto in un precedente capitolo, in win32, le
procedure di callback devono sempre preservare il contenuto dei
registri EBX, ESI e EDI; osserviamo che (come viene mostrato in
seguito), MemSet viene chiamata da FontWin_OnCreate che a sua volta
viene chiamata da WndProc che è una procedura di callback. I
parametri di MemSet sono tutti di tipo DWORD perché, come già
sappiamo, in win32 le istruzioni PUSH e POP devono lavorare
esclusivamente con operandi a 32 bit; naturalmente, nel caso del
parametro fillValue viene sfruttato solo il suo byte meno
significativo. La procedura MemSet può essere utilizzata per
inizializzare qualunque blocco di memoria; è consigliabile
inizializzare con MemSet tutte le strutture utilizzate da una
applicazione (come ad esempio la struttura di tipo WNDCLASSEX).
Una volta definiti questi aspetti, possiamo procedere con
l'implementazione della procedura FontWin_OnCreate; l'aspetto assunto
da questa procedura è il seguente (versione TASM):
FontWin_OnCreate proc hwnd :DWORD, lpCreateStruct :DWORD

call MemSet, offset fontStruct, 0, SIZE LOGFONT

mov fontStruct.lfHeight, 50
mov fontStruct.lfWeight, 700
mov fontStruct.lfEscapement, 0
mov fontStruct.lfItalic, TRUE
mov fontStruct.lfUnderline, TRUE
mov fontStruct.lfCharSet, ANSI_CHARSET
mov fontStruct.lfOutPrecision, OUT_TT_ONLY_PRECIS
mov fontStruct.lfClipPrecision, CLIP_DEFAULT_PRECIS
mov fontStruct.lfQuality, DEFAULT_QUALITY
mov fontStruct.lfPitchAndFamily, VARIABLE_PITCH OR FF_ROMAN
call StrCpy, offset fontStruct.lfFaceName, offset fontName

call MessageBox, hwnd, offset msgCreate, offset msgTitolo, MB_OK OR


MB_ICONINFORMATION

mov eax, TRUE


ret

FontWin_OnCreate endp

110
Nel caso del MASM bisogna ricordarsi di utilizzare l'operatore SIZEOF
al posto di SIZE; in MASM l'operatore SIZE applicato ad un dato,
restituisce un codice numerico che identifica il tipo di dato.
Come è stato detto in questo capitolo, il campo lfFaceName della
struttura fontStruct, è un vettore di 32 byte che deve contenere
sotto forma di stringa C, il nome del font che vogliamo creare; nel
caso del nostro esempio, viene definita un'apposita variabile
fontName che punta alla stringa:
'Times New Roman', 0 (Figura 1)
Questa stringa C deve essere copiata nel vettore lfFaceName; a tale
proposito, possiamo scriverci un'apposita procedura StrCpy che assume
il seguente aspetto:
StrCpy proc strTo :DWORD, strFrom :DWORD

mov ecx, strFrom ; ecx punta a strFrom


mov edx, strTo ; edx punta a strTo
strcpy_loop:
mov al, [ecx] ; trasferisce un byte
mov [edx], al ; da [ecx] in [edx]
inc ecx ; prossimo byte sorgente
inc edx ; prossimo byte destinaz.
test al, al ; fine stringa ?
jnz strcpy_loop

ret

StrCpy endp
Il parametro strFrom rappresenta l'indirizzo della stringa sorgente;
il parametro strTo rappresenta l'indirizzo della stringa
destinazione. Ricordiamo che in modalità protetta a 32 bit, qualunque
registro generale può essere utilizzato come registro puntatore; il
test di fine stringa viene effettuato in fondo al loop in modo che
venga copiato in strTo anche lo zero di fine stringa. È chiaro che il
vettore destinazione deve essere in grado di contenere il vettore
sorgente; nel nostro caso, la stringa C che rappresenta il nome del
font non deve superare i 32 caratteri (compreso lo zero finale).
Dopo aver inizializzato fontStruct, la procedura FontWin_OnCreate
chiama una MessageBox per informare l'utente che le inizializzazioni
sono state effettuate; questa MessageBox viene mostrata prima della
creazione della main window di FONTWIN, e quindi deve utilizzare
l'handle hwnd passato da CreateWindowEx a WndProc che a sua volta lo
passa a FontWin_OnCreate. La procedura FontWin_OnCreate termina
restituendo in EAX il valore simbolico TRUE, che generalmente viene
ignorato da Windows.

Terminata l'elaborazione del messaggio WM_CREATE, la main window di


FONTWIN (se tutto procede bene) viene creata da CreateWindowEx,
visualizzata da ShowWindow e aggiornata da UpdateWindow; in
particolare, la procedura UpdateWindow invia il messaggio WM_PAINT
che viene intercettato ed elaborato dal nostro programma.
L'elaborazione del messaggio WM_PAINT si svolge all'interno della
procedura FontWin_OnPaint; osservando il sorgente dell'applicazione
FONTWIN, si nota che questa procedura definisce le seguenti variabili
locali:

111
LOCAL paintStruct :PAINTSTRUCT ; struttura PAINTSTRUCT
LOCAL paintRect :RECT ; area di output
LOCAL paintDC :HDC ; handle del DC
LOCAL oldFont :HFONT ; handle del font
LOCAL counter :DWORD ; contatore
LOCAL angolo :DWORD ; inclinazione stringa
Queste variabili vengono dichiarate localmente in quanto sono
necessarie solamente all'interno di FontWin_OnPaint; come è stato
spiegato in precedenza, il primo passo che questa procedura deve
compiere consiste nel procurarsi un device context che verrà
utilizzato per scrivere nella client area. Il codice necessario è il
seguente:
invoke BeginPaint, hwnd, addr paintStruct
mov paintDC, eax
In possesso di paintDC, la procedura FontWin_OnPaint può stampare la
prima stringa strOut1 con il seguente codice:
invoke SetBkMode, paintDC, TRANSPARENT
MAKE_COLORREF 0, 250, 0
invoke SetTextColor, paintDC, eax
invoke TextOut, paintDC, 10, 10, offset strOut1, LEN_STROUT1
Come si può notare, viene selezionata con SetBkMode la modalità
trasparente che non altera lo sfondo dell'output; questa modalità
vale per tutto l'output prodotto da FontWin_OnPaint. Siccome non
abbiamo ancora selezionato nessun font particolare, TextOut utilizza
il font di sistema di Windows.
Il prossimo passo compiuto da FontWin_OnPaint consiste nel
selezionare un font personalizzato che verrà usato per stampare 8
volte la stringa strOut2 con 8 valori differenti per il campo
lfEscapement. È perfettamente inutile limitarsi a modificare solo
questo campo; la modifica infatti non avrà effetto finché non verrà
creato un nuovo font con CreateFontIndirect. A tale proposito,
possiamo scrivere la seguente procedura:
SetFont proc angolazione :DWORD

MOV32 fontStruct.lfEscapement, angolazione

cmp fontHandle, 0
je short setfont_continue
invoke DeleteObject, fontHandle
setfont_continue:
invoke CreateFontIndirect, offset fontStruct
mov fontHandle, eax

ret

SetFont endp
Il procedimento seguito da SetFont è molto importante; prima di tutto
viene usato il parametro angolazione per modificare il campo
lfEscapement. Subito dopo viene verificato il contenuto di
fontHandle; se fontHandle è diverso da zero, vuol dire che "punta" ad
una risorsa font che dobbiamo restituire a Windows attraverso
DeleteObject. Terminata questa fase, possiamo chiamare
CreateFontIndirect; infine, l'handle del nuovo font, restituito da
questa procedura, viene caricato in fontHandle.

112
A questo punto, all'interno di FontWin_OnPaint, possiamo scrivere il
loop che stampa 8 volte la stringa strOut2:
MAKE_COLORREF 250, 250, 0
invoke SetTextColor, paintDC, eax
mov counter, 8
mov angolo, 0
fontRotateLoop:
invoke SetFont, angolo
invoke SelectObject, paintDC, fontHandle
mov oldFont, eax
invoke TextOut, paintDC, 300, 250, offset strOut2, LEN_STROUT2
invoke SelectObject, paintDC, oldFont
add angolo, 450
dec counter
jnz fontRotateLoop
All'interno del loop vengono compiute due operazioni molto
importanti; prima di chiamare TextOut, viene selezionato il nuovo
font attraverso SelectObject. L'handle del vecchio font restituito da
SelectObject viene memorizzato in oldFont; al termine di TextOut,
viene chiamata nuovamente SelectObject per ri-selezionare il vecchio
font.
Subito dopo il loop, FontWin_OnPaint chiama DrawText per visualizzare
la stringa strOut3 al centro della client area; il codice è il
seguente:
invoke SetFont, 0
invoke SelectObject, paintDC, fontHandle
mov oldFont, eax
invoke GetClientRect, hwnd, addr paintRect
MAKE_COLORREF 150, 0, 0
invoke SetTextColor, paintDC, eax
invoke DrawText, paintDC, addr strOut3, -1, addr paintRect, DT_SINGLELINE
OR DT_CENTER OR DT_VCENTER
invoke SelectObject, paintDC, oldFont
Siccome strOut3 è una stringa C, passiamo l'argomento -1 per delegare
a DrawText il calcolo della lunghezza della stringa; l'ultima fase
svolta da FontWin_OnCreate consiste nel segnalare a Windows la fine
delle operazioni di disegno:
invoke EndPaint, hwnd, paintStruct

Ogni volta che la main window di FONTWIN viene sporcata, il SO invia


il messaggio WM_PAINT; intercettando questo messaggio abbiamo quindi
la possibilità di ripristinare costantemente il contenuto della
nostra finestra.

Non appena si prova a chiudere la main window di FONTWIN, il SO invia


il messaggio WM_CLOSE; ovviamente, nel caso di FONTWIN, chiudere la
main window significa chiudere l'intera applicazione. Il messaggio
WM_CLOSE viene intercettato da WndProc ed elaborato da
FontWin_OnClose; il codice di questa procedura è il seguente:
FontWin_OnClose proc hwnd :DWORD

invoke MessageBox, hwnd, offset msgClose, offset msgTitolo, MB_YESNO OR


MB_ICONQUESTION

ret ; eax = return value (IDYES o IDNO)

113
FontWin_OnClose endp
Come si può notare, FontWin_OnClose si limita a chiedere all'utente
la conferma della richiesta di chiusura della main window; se la
richiesta viene confermata, il messaggio WM_CLOSE viene passato a
DefWindowProc che provvede ad eseguire il ripristino dei valori
definito inviando poi il messaggio WM_DESTROY. Naturalmente, la
window procedure WndProc di FONTWIN intercetta questo messaggio e lo
passa per l'elaborazione alla procedura FontWin_OnDestroy; dal punto
di vista dei linguaggi di programmazione ad oggetti, questa procedura
rappresenta il distruttore della classe finestra FontWin. All'interno
di FontWin_OnDestroy effettuiamo quindi tutti i necessari ripristini
dei valori; il codice di questa procedura è il seguente:
FontWin_OnDestroy proc hwnd :DWORD

cmp fontHandle, 0
je short ondestroy_continue
invoke DeleteObject, fontHandle
ondestroy_continue:
invoke PostQuitMessage, NULL

ret

FontWin_OnDestroy endp
Chiamando DeleteObject con l'argomento fontHandle, permettiamo a
Windows di liberare la risorsa font associata a questo handle; in
questo modo, questa risorsa viene messa a disposizione di altre
applicazioni. Naturalmente, FontWin_OnDestroy prima di terminare,
chiama PostQuitMessage che provvede ad inviare alla nostra
applicazione il messaggio WM_QUIT; questo messaggio come sappiamo
determina l'uscita dal loop dei messaggi e la terminazione del nostro
programma.
Nota importante per il TASM.
Se si ottengono errori nell'assemblaggio di FONTWIN.ASM con il TASM,
vuol dire che probabilmente si sta utilizzando una vecchia versione
dell'include file principale win32.inc; in questo caso è necessario
scaricare la versione più aggiornata dalla sezione Downloads. Si
consiglia di verificare periodicamente la presenza di versioni più
aggiornate di win32.inc.

114
Win32 Assembly

Capitolo 7: Menu, icone, cursori, bitmap e


dialog.
Per rendere più intuitiva l'interazione tra l'utente e il computer,
le interfacce grafiche come quella di Windows mettono a disposizione
una serie di oggetti che vengono chiamati risorse; tra le risorse più
conosciute si possono citare i Menu, le Icone, i Cursori e le
immagini in formato BitMap (mappa di bit).

Attraverso i menu l'utente ha a disposizione una serie di scelte


grazie alle quali può selezionare svariate funzionalità
dell'applicazione che sta utilizzando; nel caso ad esempio di un
editor di testo, i menu vengono largamente utilizzati per permettere
all'utente di richiedere l'apertura o il salvataggio di un file, la
ricerca di una parola chiave, le operazioni di copia/incolla, la
richiesta di aiuto (help), etc.

Le icone forniscono una rappresentazione visuale dei files e delle


cartelle che si trovano nell'hard disk; grazie alle icone è possibile
distinguere i files dalle cartelle, e spesso è anche possibile
individuare il tipo di file con cui si ha a che fare (file di testo,
file eseguibile, file batch, documento di Word, foglio di calcolo di
Excell, etc).

I cursori permettono di assegnare qualsiasi forma (shape) al


puntatore del mouse; in Windows l'utente ha a disposizione numerosi
cursori predefiniti, con la possibilità di creare anche dei cursori
personalizzati. Attraverso la modifica della forma del cursore del
mouse, è possibile fornire precise informazioni all'utente; nel caso
ad esempio di un programma di elaborazione grafica (come Paint), il
cursore del mouse cambia forma a seconda dello strumento di disegno
selezionato (penna, pennello, gomma da cancellare, spray, etc). A
partire da Windows 95 vengono supportati anche i cursori animati; un
cursore animato è formato da una sequenza di immagini che possono
essere paragonate ai fotogrammi di una pellicola cinematografica.
Appena si carica un cursore animato, vengono fatti scorrere
automaticamente i vari fotogrammi che danno così l'idea di una
animazione; nel seguito del capitolo vedremo come si deve procedere
per dotare di cursori animati le proprie applicazioni.

Le bitmap sono immagini grafiche memorizzate su disco sotto forma di


mappe di bit; in Windows il formato grafico predefinito è
rappresentato proprio dalle bitmap che vengono infatti utilizzate
anche per memorizzare le icone e i cursori. Per migliorare le
prestazioni, Windows utilizza spesso bitmap prive di qualsiasi forma
115
di compressione grafica; rispetto ai formati grafici compressi come
JPG, GIF, etc, lo svantaggio di una bitmap non compressa è
rappresentato dalle notevoli richieste di spazio su disco. Le icone e
i cursori non sono altro che bitmap di forma quadrata; nel caso
generale invece una bitmap è una immagine formata da una superficie
rettangolare di pixel.

Gli aspetti relativi alla struttura interna delle risorse di Windows


vengono discussi in altri capitoli; in questo capitolo invece viene
illustrato un procedimento che permette di incorporare in modo molto
semplice svariate risorse nei propri programmi. Viene anche
illustrato un modo per creare rapidamente una semplice finestra di
dialogo (Dialog Box); le dialog box vengono largamente utilizzate per
permettere all'utente di interagire (dialogare) con una applicazione.
Per mettere in pratica i concetti esposti in questo capitolo, viene
presentato un programma di esempio chiamato RISORSE.ASM, disponibile
sia in versione MASM che TASM; il codice sorgente dell'esempio si
trova nei seguenti files zippati:

Download di tutti gli esempi del capitolo (versione MASM)


Download di tutti gli esempi del capitolo (versione TASM)

Ciascuno di questi file deve essere decompresso nella cartella di


lavoro; gli esempi presentati nella sezione Win32 Assembly
presuppongono che la cartella di lavoro sia:
c:\masm32\win32asm per il MASM e:
c:\tasm\win32asm per il TASM.
Dalla decompressione del file cap07masm.zip o cap07tasm.zip si
ottiene una cartella Risorse che verrà quindi posizionata in:
c:\masm32\win32asm\Risorse per il MASM e:
c:\tasm\win32asm\Risorse per il TASM.
All'interno della cartella Risorse sono presenti ben 15 files, la
maggior parte dei quali contengono le risorse utilizzate dal
programma di esempio.
Se non si utilizzano i percorsi specificati in precedenza per le
cartelle, è necessario apportare le opportune modifiche ai percorsi
indicati nei files RISORSE.ASM, RISORSE.RC, RISORSE.MAK (per il TASM)
e RISORSE.BAT (per il MASM).

7.1 Il Resource File.

Il metodo più semplice e più rapido per incorporare numerose risorse


nei propri programmi, consiste nel servirsi di appositi files che
vengono chiamati Resource Files (files di risorse); il Resource File
è un file rigorosamente in formato ASCII, che attraverso una sorta di
pseudo linguaggio di programmazione ci permette di dichiarare le
caratteristiche di tutte le risorse da incorporare in un programma.
Convenzionalmente per i Resource Files si utilizza l'estensione
predefinita RC; nel caso del nostro esempio il file di risorse si
chiama RISORSE.RC. Apriamo quindi il file RISORSE.RC con il nostro
editor preferito, ed esploriamone il contenuto; proprio all'inizio
del file RISORSE.RC, troviamo la seguente istruzione:
#include "..\include\resource.h"
Come si può notare, questa è chiaramente una istruzione del
116
linguaggio C; la direttiva #include del C equivale ovviamente alla
direttiva INCLUDE dell'Assembly. La caratteristica fondamentale dei
files di risorse consiste proprio nel fatto che al loro interno
dobbiamo utilizzare una sintassi presa in prestito da linguaggi come
il C e il Pascal; in questo modo si ottiene una sorta di pseudo
linguaggio attraverso il quale si possono codificare le risorse da
inserire in un programma.
Il lavoro che con questo pseudo linguaggio viene svolto all'interno
di un file di risorse, è molto simile alla dichiarazione di una serie
di macro alfanumeriche, ciascuna delle quali rappresenta una risorsa;
le vare costanti predefinite come WS_CHILD, WS_VISIBLE, etc, che si
utilizzano nel corpo di queste macro, vengono dichiarate all'interno
dell'include file principale windows.inc (o win32.inc). Il Resource
File richiede però che queste costanti vengano dichiarate con la
classica sintassi del C, per cui è necessario disporre di una copia
in versione C di windows.inc; il MASM fornisce un apposito header
file chiamato appunto resource.h, che si trova nella cartella
INCLUDE, e siccome la sintassi del C è standard, il file resource.h
può essere utilizzato senza nessuna modifica anche con TASM. Il
contenuto di questo file viene continuamente aggiornato dalla
Microsoft man mano che escono nuove versioni di Windows; è importante
quindi procurarsi la versione più aggiornata possibile di resource.h.
Si consiglia di dare un'occhiata a questo file per prendere
confidenza con le numerose costanti simboliche presenti al suo
interno.

Nel linguaggio C tutte le stringhe incluse tra /* e */ come ad


esempio:
/* codici per i menu */
rappresentano un commento che può svilupparsi anche su più linee; si
tratta quindi della stessa situazione che in Assembly si ottiene con
la direttiva COMMENT. Se il commento si sviluppa su una sola linea,
possiamo anche utilizzare la sintassi del C++ per i commenti
scrivendo:
// codici per i menu

Proseguendo nella esplorazione del file RISORSE.RC, possiamo notare


che dopo la direttiva #include incontriamo una numerosa serie di
istruzioni del tipo:
#define CM_MENU1a 101
Anche in questo caso abbiamo a che fare con istruzioni scritte in
linguaggio C; la direttiva #define del C equivale alla direttiva
MACRO dell'Assembly, e ci permette quindi di dichiarare macro
alfanumeriche distribuite su una o più linee. Nel nostro caso
dobbiamo dichiarare una serie di semplici costanti numeriche, per cui
la direttiva precedente corrisponde in Assembly a:
CM_MENU1a = 101
Lo scopo di tutte queste costanti numeriche viene chiarito più
avanti.
Dopo le direttive #define incontriamo la dichiarazione di una risorsa
che rappresenta il menu associato alla main window del nostro
programma; la dichiarazione della struttura di un menu inizia con il
nome simbolico assegnato a questa risorsa, seguito dalla parola
chiave MENU. Nel nostro caso si ha:

117
RisorseMenu MENU
In pratica, la main window del nostro programma utilizza una risorsa
menu chiamata simbolicamente RisorseMenu; al posto della stringa
RisorseMenu è possibile specificare un valore numerico. Possiamo
scrivere ad esempio:
3500 MENU
Il codice numerico deve essere compreso tra 0 e 65535; come vedremo
in seguito, l'utilizzo di un codice numerico al posto di una stringa
ci permette di risparmiare diversi byte. Per motivi di chiarezza è
sempre meglio utilizzare nomi simbolici per identificare un valore
numerico esplicito; nel caso della dichiarazione precedente possiamo
scrivere ad esempio:
#define ID_MENU 3500
Una volta che abbiamo dichiarato questa costante simbolica possiamo
scrivere:
ID_MENU MENU
Seguendo sempre la classica sintassi del C, si può notare che il
corpo della risorsa menu inizia con una parentesi graffa aperta { e
termina con una parentesi graffa chiusa }; se questi due simboli non
sono presenti sulla vostra tastiera, li potete ottenere ugualmente
ricordando che il codice ASCII di '{' è 123, e il codice ASCII di '}'
è 125. Per ottenere la parentesi graffa aperta bisogna quindi tenere
premuto il tasto [Alt] sinistro e premere in sequenza i tasti [1],
[2] e [3] sul tastierino numerico; per ottenere la parentesi graffa
chiusa bisogna invece tenere premuto il tasto [Alt] sinistro e
premere in sequenza i tasti [1], [2] e [5] sul tastierino numerico.
In alternativa, è possibile utilizzare la parola chiave BEGIN al
posto di {, e la parola chiave END al posto di }; in questo modo si
ottiene una sintassi simile a quella del Pascal.
Subito dopo l'intestazione di RisorseMenu, troviamo l'elenco dei vari
popup associati al menu; con il termine popup si indica la tipica
finestra a discesa che compare quando si clicca con il mouse sul menu
di un programma (spesso si utilizza anche la definizione di menu a
tendina). Ciascun popup associato al menu viene specificato con una
dichiarazione del tipo
POPUP "&Bitmap"
In questo modo, la stringa "Bitmap" comparirà nella menu bar (barra
del menu) del nostro programma; come si può notare, la stringa
associata ad ogni popup deve essere inserita tra doppi apici. Il
simbolo '&' posto prima della B indica che questo popup può essere
selezionato non solo con il mouse ma anche con la sequenza di tasti
[Alt][B] (si tratta sempre del tasto [Alt] sinistro); il tasto da
premere insieme a [Alt] per selezionare un popup viene chiamato hot
key (tasto caldo).
Ciascun popup è formato da una serie di menu items; con il termine
menu item si indica una delle varie opzioni (o voci) che si possono
selezionare all'interno di un popup. I vari menu items possono essere
creati con dichiarazioni del tipo:
MENUITEM "&Penna", 304
In questo modo all'interno del popup "Cursori" comparirà l'opzione
"Penna" con hot key [P]; per selezionare un hot key all'interno di un
popup non è necessario premere [Alt]. Il valore 304 specificato nella
precedente dichiarazione, rappresenta il codice numerico che
identifica il menu item "Penna"; anche in questo caso, i vari codici

118
numerici devono essere compresi tra 0 e 65535. Ogni volta che
selezioniamo un menu item, la window procedure del nostro programma
riceve da Windows un messaggio WM_COMMAND accompagnato dal codice
numerico che abbiamo assegnato allo stesso menu item; intercettando
questo messaggio possiamo quindi rispondere all'azione richiesta
dall'utente attraverso il menu.
Come è stato ampiamente spiegato nei precedenti capitoli, l'utilizzo
dei numeri espliciti nei programmi aumenta notevolmente il rischio di
commettere errori; se nel codice sorgente del nostro programma
inseriamo un numero sbagliato, non possiamo certo pretendere che l'
assembler ci segnali l'errore che abbiamo commesso. Per questo
motivo, e anche per ragioni di stile, si consiglia vivamente di
utilizzare sempre costanti simboliche per gestire dei numeri
espliciti; le numerose direttive #define presenti nel file RISORSE.RC
hanno proprio lo scopo di dichiarare i codici numerici associati ai
vari menu items del menu RisorseMenu. In questo modo possiamo
scrivere ad esempio:
MENUITEM "&Penna", CM_CURSOR4
Osservando il popup "Font", possiamo notare che tra i vari menu items
è presente anche un popup "Altri Font"; in questo modo possiamo
dichiarare dei popup innestati all'interno di altri popup. Osserviamo
inoltre che anche la sequenza dei vari menu items associati ad ogni
popup deve essere racchiusa dalla solita coppia { };
alternativamente, anche in questo caso possiamo utilizzare le parole
chiave BEGIN e END.
Una considerazione finale sui menu riguarda il fatto che come si può
notare dalle #define presenti nel file RISORSE.RC, ad ogni popup
viene assegnato un differente gruppo di codici numerici, e
all'interno di ogni popup i vari codici numerici sono consecutivi; in
questo modo è possibile apportare facilmente eventuali modifiche ad
un popup, e si possono anche ottenere notevoli vantaggi in fase di
elaborazione del messaggio WM_COMMAND.

Dopo la dichiarazione del menu RisorseMenu, nel file RISORSE.RC


incontriamo una serie di altre dichiarazioni relative alle icone, ai
cursori e alle bitmap che verranno utilizzate nel programma. Per
incorporare una qualsiasi icona, dobbiamo utilizzare una
dichiarazione del tipo:
NomeSimbolico ICON "nomefile.ico"
In pratica, questa dichiarazione è formata dal nome simbolico che
intendiamo assegnare alla risorsa, seguito dalla parola chiave ICON e
dal nome del file contenente la bitmap dell'icona (se il file si
trova in un'altra cartella, bisogna specificare il percorso
completo); nel nostro caso vogliamo incorporare un'icona memorizzata
nel file RISORSE.ICO, e vogliamo assegnare a questa risorsa il nome
simbolico RisorseIcon. Dobbiamo scrivere quindi:
RisorseIcon ICON "risorse.ico"
Come vedremo in seguito, questa risorsa verrà utilizzata come icona
personalizzata del programma RISORSE.EXE.
Le icone utilizzate da Windows sono delle bitmap quadrate formate da
16x16, 24x24, 32x32 o 48x48 pixel; un file contenente un'icona per
Windows viene memorizzato su disco con estensione predefinita ICO, e
contiene una struttura formata da un header (intestazione) seguito
dalla codifica della bitmap vera e propria. L'icona associata ad una
119
applicazione deve essere in formato 32x32, mentre l'icona piccola che
verrà visualizzata nella barra del titolo (in alto a sinistra) deve
essere in formato 16x16. L'icona associata ad una applicazione verrà
utilizzata da Windows per rappresentare graficamente l'applicazione
stessa nel File Manager; nel caso del nostro esempio vedremo che
l'icona RISORSE.ICO verrà associata nel File Manager al programma
RISORSE.EXE.
Se vogliamo creare icone personalizzate, dobbiamo procurarci un
apposito editor di icone; a tale proposito possiamo effettuare una
ricerca su Internet tenendo presente che l'editor che ci serve deve
essere in grado ovviamente di gestire il formato specifico ICO
utilizzato da Windows. Le icone sono immagini molto piccole, per cui
sarebbe inutile crearle utilizzando un numero elevato di colori; per
risparmiare spazio su disco e in memoria, è vivamente consigliabile
l'utilizzo di una palette (tavolozza) a 16 colori.

Dopo la dichiarazione per l'icona RisorseIcon, incontriamo una serie


di altre dichiarazioni che elencano i vari cursori che verranno
utilizzati dal nostro programma di esempio; come si può notare, il
procedimento da seguire è del tutto simile al caso delle icone. In
pratica, dobbiamo indicare il nome simbolico da assegnare al cursore,
seguito dalla parola chiave CURSOR e dal nome del file contenente la
risorsa; nel nostro caso possiamo notare la presenza di dichiarazioni
del tipo:
RisorseCursor1 CURSOR "jet.cur"
I cursori utilizzati da Windows sono delle bitmap quadrate formate da
32x32 pixel; un file contenente un cursore per Windows viene
memorizzato su disco con estensione predefinita CUR, e contiene una
struttura formata da un header seguito dalla codifica della bitmap
vera e propria. I files contenenti cursori animati vengono invece
memorizzati su disco con estensione predefinita ANI.
Se vogliamo creare cursori personalizzati, dobbiamo procurarci un
apposito editor di cursori; in genere, molti editor di icone sono in
grado di gestire anche il formato CUR per Windows, e gli editor più
sofisticati supportano anche i cursori animati. In fase di creazione
di un cursore personalizzato, è necessario specificare una
informazione importantissima, rappresentata dalle coordinate X e Y
del cosiddetto hot spot (punto caldo); l'hot spot di un cursore è il
punto della bitmap del cursore, rispetto al quale Windows calcolerà
istante per istante le coordinate del mouse. Nel caso
dell'applicazione RISORSE.EXE, viene utilizzato ad esempio un cursore
a forma di penna; in un caso del genere, il punto più ovvio da
utilizzare come hot spot è rappresentato naturalmente dalle
coordinate della punta della penna.

Il Resource File può essere utilizzato anche per incorporare in una


applicazione generiche immagini bitmap; a tale proposito dobbiamo
specificare come al solito il nome simbolico assegnato alla risorsa,
seguito dalla parola chiave BITMAP e dal nome del file contenente la
risorsa stessa. Nel caso del file RISORSE.RC notiamo ad esempio la
presenza di una dichiarazione del tipo:
RisorseLogo BITMAP "ramlogo.bmp"
Una bitmap generica è una immagine formata da un rettangolo di pixel;
un file contenente una immagine bitmap compatibile con Windows, viene
120
memorizzato su disco con estensione predefinita BMP, e contiene una
struttura formata da un header seguito dalla codifica della bitmap
vera e propria. Per realizzare immagini bitmap si può utilizzare un
qualsiasi programma di grafica come ad esempio Paint; al momento di
salvare l'immagine su disco, bisogna richiedere il salvataggio in
formato BMP per Windows.
Naturalmente il Resource File deve essere utilizzato per incorporare
in un programma piccole immagini bitmap; si tenga presente infatti
che tutte le risorse presenti in un Resource File vengono inserite
direttamente nel file eseguibile finale, aumentandone notevolmente le
dimensioni. Non avrebbe nessun senso quindi dichiarare in un Resource
File un'immagine bitmap formata da 1024x768 pixel, con colori a 24
bit; in un caso del genere la strada da seguire consiste nel lasciare
la bitmap in un file separato e caricarla poi in fase di esecuzione
del programma.

Il file RISORSE.RC termina con la dichiarazione di una dialog box che


verrà utilizzata come finestra di About (informazioni) del programma;
tutti gli aspetti relativi a questa dialog box vengono discussi alla
fine del capitolo.

Come è stato detto in precedenza, qualsiasi risorsa definita in un


Resource File può essere identificata non solo attraverso una
stringa, ma anche attraverso un codice numerico compreso tra 0 e
65535; consideriamo ad esempio il cursore che nel file RISORSE.RC
viene identificato con la stringa RisorseCursor2. Se vogliamo
identificare questo cursore attraverso il codice numerico 4002,
possiamo dichiarare la seguente costante numerica:
#define ID_CURSOR2 4002
A questo punto possiamo scrivere:
ID_CURSOR2 CURSOR "stella.cur"

Il file RISORSE.RC una volta completato viene passato ad un apposito


compilatore chiamato Resource Compiler (compilatore di risorse); nel
caso del MASM il compilatore di risorse si chiama RC.EXE, e come al
solito si trova nella sottocartella BIN di MASM32. Nel caso del TASM
il compilatore di risorse esiste in due versioni chiamate BRCC.EXE e
BRCC32.EXE; il compilatore BRCC.EXE funziona sia sotto Windows che
nella modalità reale del DOS, mentre il compilatore BRCC32.EXE
funziona invece solo nella modalità protetta di Win32. Sia BRCC.EXE
che BRCC32.EXE si trovano insieme a tutti gli altri strumenti di
sviluppo, nella sottocartella BIN di TASM.
Per compilare il file RISORSE.RC con il MASM bisogna impartire il
comando:
rc /v risorse.rc
Il parametro /v (verbose) fa in modo che RC.EXE visualizzi tutti i
messaggi relativi alla fase di compilazione di RISORSE.RC; in questo
modo possiamo avere maggiori informazioni nel caso in cui vengano
trovati degli errori.
Per compilare il file RISORSE.RC con il TASM bisogna impartire il
comando:
brcc32 risorse.rc
Si tenga presente che al posto dei compilatori di risorse forniti da
MASM e TASM si possono utilizzare anche quelli forniti in dotazione

121
ai vari ambienti di sviluppo per Windows.
Se la compilazione va a buon fine si ottiene un file chiamato
RISORSE.RES; il formato interno di questo file è compatibile con il
formato OBJ (object). Come si può facilmente intuire, il file
RISORSE.RES viene passato poi al linker insieme agli altri files in
formato object; il linker provvederà a collegare i vari object files
producendo quindi l'eseguibile finale.

7.2 L'include file RISORSE.INC.

Per poter utilizzare le varie risorse dichiarate in RISORSE.RC, il


modulo principale RISORSE.ASM deve conoscere tutti i nomi e le
variabili simboliche che abbiamo dichiarato nel Resource File; in
altre parole, il modulo principale RISORSE.ASM deve poter essere in
grado di interfacciarsi con il modulo RISORSE.RC. A tale proposito
possiamo notare che nel blocco dati inizializzati (_DATA) del modulo
RISORSE.ASM sono presenti le seguenti definizioni:
menuName db 'RisorseMenù, 0
iconName db 'RisorseIcon', 0

bitmap1Name db 'RisorseBmp1', 0
bitmap2Name db 'RisorseBmp2', 0
bitmap3Name db 'RisorseBmp3', 0
bitmap4Name db 'RisorseBmp4', 0

cursor1Name db 'RisorseCursor1', 0
cursor2Name db 'RisorseCursor2', 0
cursor3File db 'libro.cur', 0
cursor4File db 'penna.cur', 0
animcurFile db 'dinosau2.anì, 0

AboutDlgName db 'RisorseAboutDlg', 0
In questo modo vengono definite una serie di stringhe che contengono
i nomi con i quali vengono identificate le varie risorse dichiarate
in RISORSE.RC; è importante notare che queste stringhe devono essere
in formato C, dotate cioè di zero finale.
In relazione ai cursori possiamo notare che solo due di essi vengono
dichiarati nel file RISORSE.RC; gli altri 3 vengono invece caricati
direttamente dal disco in fase di esecuzione del programma. Per
gestire questi 3 cursori vengono definite nel blocco _DATA le 3
variabili cursor3File, cursor4File e animcurFile che devono puntare a
stringhe C contenenti il nome del file CUR o ANI; in sostanza il
programmatore ha la possibilità di scegliere se incorporare
direttamente nel programma una risorsa di tipo cursore, oppure se
lasciarla in un file separato e caricarla poi in fase di esecuzione.
Il modulo RISORSE.ASM deve conoscere anche i codici numerici
associati ai vari menu items; per evitare di dover lavorare con
valori numerici espliciti, possiamo ridichiarare in RISORSE.ASM tutte
le #define presenti in RISORSE.RC. Queste dichiarazioni possono
essere inserite nel blocco tipi e costanti simboliche del modulo
RISORSE.ASM; in alternativa, per rendere più semplici e snelli i vari
moduli, possiamo servirci di un apposito include file che nel nostro
esempio viene chiamato RISORSE.INC. Analizzando il file RISORSE.INC
possiamo notare che al suo interno sono presenti le stesse #define di
RISORSE.RC, ridichiarate naturalmente in stile Assembly; nello stesso
122
file RISORSE.INC vengono inseriti anche i vari prototipi delle
procedure utilizzate da RISORSE.ASM. Per evitare errori, si consiglia
di utilizzare un editor dotato dello strumento copia/incolla; in
questo modo si possono copiare le varie #define dal file RISORSE.RC
per incollarle poi nel file RISORSE.INC.
A questo punto dobbiamo includere tutte queste dichiarazioni nel
modulo RISORSE.ASM; a tale proposito, nella sezione inclusione
librerie del modulo RISORSE.ASM possiamo notare la presenza della
direttiva:
INCLUDE risorse.inc

7.3 Struttura generale dell'applicazione RISORSE.

L'applicazione RISORSE è dotata di una semplice main window con barra


dei menu e icona personalizzata; appena si avvia l'esecuzione, viene
mostrata la main window con colore di sfondo personalizzato e con il
classico cursore predefinito a forma di freccia. Attraverso il menu
l'utente può selezionare svariate risorse di tipo bitmap, cursore,
font e dialog; sempre attraverso il menu è possibile richiedere la
chiusura dell'applicazione.
Le bitmap selezionabili attraverso il popup "Bitmap" sono tutte da
128x128 pixel e vengono utilizzate per modificare lo sfondo della
client area della main window; non si tratta quindi del riempimento
della client area con una bitmap, ma di una vera e propria modifica
del campo hbrBackground della struttura WNDCLASSEX che definisce lo
sfondo della finestra.
Attraverso il popup "Cursori" è possibile assegnare al puntatore del
mouse 5 forme differenti una delle quali è animata; dallo stesso
popup si può tornare al cursore predefinito a forma di freccia. I
primi due cursori personalizzati sono stati incorporati nel programma
attraverso il file RISORSE.RC; gli altri tre cursori personalizzati
vengono invece caricati dal disco in fase di esecuzione.
Attraverso il popup "Font" si possono selezionare 9 font differenti
che vengono utilizzati per visualizzare una stringa; questa stringa
viene posizionata al centro della client area mediante la tecnica
mostrata nel precedente capitolo.
Attraverso il popup "Informazioni" è possibile attivare una dialog
box che mostra una serie di informazioni relative all'applicazione e
all'autore del programma; come è stato detto in precedenza, tutti i
dettagli relativi alla gestione delle dialog box verranno illustrati
alla fine del capitolo.

Per svolgere il suo lavoro l'applicazione RISORSE intercetta i 5


messaggi WM_CREATE, WM_PAINT, WM_COMMAND, WM_CLOSE e WM_DESTROY;
analizziamo quindi il lavoro che viene svolto in fase di creazione
della main window e in fase di elaborazione dei messaggi.

7.4 Installazione del menu e dell'icona.

L'installazione del menu e dell'icona personalizzata di un programma,


avviene in modo molto semplice; tutto questo lavoro infatti viene
svolto durante la fase di inizializzazione della main window. Nei
precedenti capitoli abbiamo visto che questa fase consiste nel

123
riempimento di una struttura di tipo WNDCLASSEX; a tale proposito,
supponiamo di aver definito una variabile wc di tipo WNDCLASSEX. Tra
i vari membri di questa struttura possiamo notare che uno di essi
viene chiamato:
wc.lpszMenuName
Questo membro è una DWORD che deve contenere l'identificatore della
risorsa menu associata alla window class che stiamo inizializzando;
negli esempi dei precedenti capitoli non abbiamo utilizzato nessuna
risorsa menu, per cui questo membro veniva sempre inizializzato con:
mov wc.lpszMenuName, NULL
Nell'esempio di questo capitolo abbiamo invece definito un menu
identificato simbolicamente dalla stringa RisorseMenu; per gestire
questa stringa, nel blocco _DATA di RISORSE.ASM è presente la
definizione:
menuName db 'RisorseMenù, 0
A questo punto possiamo installare il menu della window class Risorse
attraverso l'istruzione:
mov wc.lpszMenuName, offset menuName
In precedenza è stato detto che una risorsa può essere identificata
non solo attraverso una stringa, ma anche attraverso un codice
numerico; anche in questo caso l'installazione della risorsa avviene
in modo semplicissimo. Supponiamo ad esempio di voler gestire il menu
del programma attraverso un codice numerico; a tale proposito nel
file RISORSE.RC inseriamo ad esempio la dichiarazione:
#define ID_MENU 1001
Sempre nel file RISORSE.RC, l'intestazione del menu diventa:
ID_MENU MENU
Per poterci interfacciare a questa risorsa, nell'include file
RISORSE.INC inseriamo la dichiarazione:
ID_MENU = 1001
A questo punto nella fase di inizializzazione della window class
Risorse possiamo scrivere la semplicissima istruzione:
mov wc.lpszMenuName, ID_MENU
Come si può notare, questo secondo metodo è anche più semplice di
quello precedente, e presenta il vantaggio di farci risparmiare
qualche byte; è chiaro infatti che la costante ID_MENU richiede una
quantità di memoria inferiore a quella richiesta dalla stringa:
menuName db 'RisorseMenù, 0

Passiamo ora all'installazione dell'icona personalizzata che nel file


RISORSE.RC è stata dichiarata come:
RisorseIcon ICON "risorse.ico"
Nella struttura WNDCLASSEX è presente un membro chiamato:
wc.hIcon
Questo membro è una DWORD che deve contenere l'handle dell'icona da
assegnare alla finestra che stiamo inizializzando; se vogliamo
lasciare questa scelta a Windows possiamo scrivere:
mov wc.hIcon, NULL
In questo caso Windows utilizza un'icona predefinita; generalmente si
tratta di una icona a forma di finestra. Se vogliamo utilizzare una
specifica icona predefinita di Windows possiamo scrivere ad esempio:
call LoadIcon, NULL, IDI_WARNING
mov wc.hIcon, eax
In questo caso alla finestra che stiamo inizializzando viene
associata un'icona predefinita contenente un segnale di pericolo con
124
il punto esclamativo; è importante notare che nel caso di
installazione di icone predefinite, il primo parametro (hInstance) di
LoadIcon deve essere NULL.
Se invece vogliamo utilizzare l'icona personalizzata dichiarata in
RISORSE.RC, dobbiamo procedere esattamente come per il menu; prima di
tutto nel blocco _DATA di RISORSE.ASM inseriamo la definizione:
iconName db 'RisorseIcon', 0
A questo punto possiamo installare questa icona attraverso le
istruzioni:
call LoadIcon, hInstance, offset iconName
mov wc.hIcon, eax
L'argomento hInstance passato a LoadIcon è naturalmente l'handle del
programma che viene reperito come sappiamo tramite GetModuleHandle.
Se vogliamo gestire l'icona personalizzata attraverso un codice
numerico, dobbiamo seguire lo stesso procedimento indicato per il
menu; prima di tutto, nel file RISORSE.RC inseriamo ad esempio la
dichiarazione:
#define ID_PROGICON 2001
Sempre nel file RISORSE.RC, l'intestazione della risorsa icona
diventa quindi:
ID_PROGICON ICON "risorse.ico"
Per poterci interfacciare a questa risorsa, nell'include file
RISORSE.INC inseriamo la dichiarazione:
ID_PROGICON = 2001
A questo punto nella fase di inizializzazione della window class
Risorse possiamo scrivere le semplicissime istruzioni:
call LoadIcon, hInstance, ID_PROGICON
mov wc.hIcon, eax
La fase di installazione dell'icona di una applicazione, coinvolge
anche l'icona piccola da 16x16 pixel che verrà visualizzata nella
barra del titolo dell'applicazione stessa; a tale proposito notiamo
che tra i vari membri di WNDCLASSEX ne troviamo uno che viene
chiamato:
wc.hIconSm (handle to small icon)
Per installare l'icona piccola, dobbiamo seguire lo stesso identico
procedimento descritto per l'icona da 32x32 pixel; come è stato detto
in un precedente capitolo, è possibile delegare tutto questo lavoro a
Windows scrivendo l'istruzione:
mov wc.hIconSm, NULL
In questo caso Windows utilizza per l'icona piccola una versione
scalata a 16x16 pixel dell'icona da 32x32 pixel; tutto questo
discorso naturalmente vale anche quando utilizziamo una icona
personalizzata da 32x32 pixel.

Nella fase di inizializzazione della window class è anche possibile


installare un cursore personalizzato per il mouse; il procedimento da
seguire è identico a quello illustrato per le icone. Volendo ad
esempio installare il cursore identificato dalla stringa:
cursor1Name db 'RisorseCursor1', 0
possiamo sfruttare l'apposito membro:
wc.hCursor
In questo caso dobbiamo scrivere:
call LoadCursor, hInstance, offset cursor1Name
mov wc.hCursor, eax

125
Nell'esempio RISORSE.ASM viene invece installato inizialmente il
solito cursore predefinito a forma di freccia; abbiamo quindi le
classiche istruzioni:
call LoadCursor, NULL, IDC_ARROW
mov wc.hCursor, eax
Tutte le risorse di tipo cursore e bitmap che abbiamo dichiarato nel
file RISORSE.RC, vengono selezionate in fase di esecuzione attraverso
il menu della main window; come è stato detto in precedenza, la
gestione del menu avviene tramite il messaggio WM_COMMAND.

7.5 Gestione del messaggio WM_CREATE.

La window procedure della window class Risorse intercetta il


messaggio WM_CREATE per effettuare svariate inizializzazioni; a tale
proposito la window procedure chiama la procedura Risorse_OnCreate.
Le caratteristiche dei parametri ricevuti da questa procedura sono
già state descritte nei precedenti capitoli; è importante ribadire
che la procedura che elabora il messaggio WM_CREATE, può essere
considerata come il costruttore della window class che ha ricevuto il
messaggio stesso.
All'interno della procedura Risorse_OnCreate vengono caricate le
risorse bitmap e le risorse cursore utilizzate dal programma; viene
inoltre riempita una struttura di tipo LOGFONT necessaria per poter
utilizzare i font di Windows.

Per caricare una risorsa bitmap e ottenere il suo handle, possiamo


utilizzare la procedura LoadBitmap; questa procedura viene definita
nella libreria USER32.LIB e presenta il seguente prototipo:
HBITMAP LoadBitmap(HINSTANCE hInstance, LPCTSTR lpBitmapName);
Il parametro hInstance come sappiamo è l'handle dell'applicazione che
viene reperito con GetModuleHandle.
Il parametro lpBitmapName è l'indirizzo del nome simbolico (o il
codice numerico) che identifica la risorsa bitmap.
La procedura LoadBitmap termina restituendo in EAX l'handle della
bitmap (tipo di dato HBITMAP) appena caricata.
Nel caso ad esempio della bitmap identificata nel blocco _DATA con:
bitmap1Name db 'RisorseBmp1', 0
possiamo scrivere:
call LoadBitmap, hInstance, offset bitmap1Name
mov handleBitmap1, eax
Se invece nel file RISORSE.RC utilizziamo ad esempio la costante
numerica ID_BITMAP1 per identificare questa bitmap, dobbiamo
scrivere:
call LoadBitmap, hInstance, ID_BITMAP1
mov handleBitmap1, eax
Per caricare le risorse di tipo cursore utilizziamo la procedura
LoadCursor che già conosciamo; come già sappiamo, se vogliamo
caricare un cursore predefinito possiamo scrivere ad esempio:
call LoadCursor, NULL, IDC_ARROW
mov handleCursore, eax
Se invece vogliamo caricare un cursore personalizzato dichiarato in
un Resource File, dobbiamo passare come primo argomento hInstance, e
come secondo argomento l'indirizzo del nome simbolico che identifica
126
il cursore stesso; nel caso ad esempio del cursore identificato nel
blocco _DATA con:
cursor1Name db 'RisorseCursor1', 0
possiamo scrivere:
call LoadCursor, hInstance, offset cursor1Name
mov handleCursor1, eax
Come al solito, se nel file RISORSE.RC utilizziamo ad esempio la
costante numerica ID_CURSOR1 per identificare questo cursore,
dobbiamo scrivere:
call LoadCursor, hInstance, ID_CURSOR1
mov handleCursor1, eax
Se vogliamo caricare un cursore direttamente dal disco dobbiamo
utilizzare la procedura LoadCursorFromFile; questa procedura viene
definita nella libreria USER32.LIB e presenta il seguente prototipo:
HCURSOR LoadCursorFromFile(LPCTSTR lpFileName);
Il parametro lpFileName è l'indirizzo di una stringa C che contiene
il nome del file ICO o ANI.
La procedura LoadCursorFromFile termina restituendo in EAX l'handle
del cursore appena caricato.

All'interno di Risorse_OnCreate viene anche inizializzata la


struttura di tipo LOGFONT che definisce le caratteristiche del font
che vogliamo utilizzare; questo aspetto è stato già illustrato nel
precedente capitolo.

7.6 Gestione del messaggio WM_COMMAND.

Ogni volta che selezioniamo un menu item dal menu di una finestra,
Windows invia automaticamente un messaggio WM_COMMAND alla window
procedure della finestra stessa; come al solito, ulteriori
informazioni associate ad un messaggio, vengono inviate alla window
procedure attraverso i due parametri wParam e lParam.
Nel caso di WM_COMMAND la window procedure riceve tre informazioni
che vengono chiamate id, hwndCtl e codeNotify; questi tre nomi sono
stati "pescati" come al solito dall'header file windowsx.h.
Alternativamente le informazioni relative agli argomenti associati ad
un determinato messaggio possono essere ricavate anche dal solito
Win32 Programmer's Reference; nel nostro caso dobbiamo aprire il file
Win32.hlp, premere il pulsante Indice e richiedere la ricerca della
parola chiave WM_COMMAND.

L'argomento id contiene un codice numerico che identifica il mittente


del messaggio WM_COMMAND; nel caso di un menu l'argomento id contiene
il codice del menu item che è stato selezionato dall'utente e che ha
provocato quindi l'invio del messaggio WM_COMMAND. L'argomento id
viene inviato alla window procedure attraverso la WORD meno
significativa del parametro wParam; programmando in linguaggio C si
può sfruttare una macro di Windows chiamata LOWORD (Low Word = Word
Bassa), con la quale si può scrivere ad esempio:
id = LOWORD(wParam);

L'argomento codeNotify contiene un codice numerico che ci permette di


sapere se il messaggio WM_COMMAND proviene da un menu o da un
cosiddetto control; con il termine control si indicano vari
127
dispositivi di controllo di Windows come i pulsanti, le finestre di
edit, etc. Se il messaggio WM_COMMAND arriva da un control
l'argomento codeNotify vale 1; se invece il messaggio WM_COMMAND
arriva da un menu l'argomento codeNotify vale 0.
L'argomento codeNotify viene inviato alla window procedure attraverso
la WORD più significativa del parametro wParam; programmando in
linguaggio C si può sfruttare una macro di Windows chiamata HIWORD
(High Word = Word Alta), con la quale si può scrivere ad esempio:
codeNotify = HIWORD(wParam);

L'argomento hwndCtl contiene un codice numerico che rappresenta


l'handle del control che ha inviato il messaggio WM_COMMAND; questo
argomento quindi è significativo solo per i control. Se il messaggio
WM_COMMAND è stato inviato da un menu, questo argomento vale NULL;
l'argomento hwndCtl è una DWORD che viene inviata alla window
procedure attraverso il parametro lParam.

Seguendo le considerazioni esposte nei precedenti capitoli, possiamo


utilizzare una apposita procedura per la gestione del messaggio
WM_COMMAND; il prototipo C per questa procedura (ricavato dal file
windowsx.h) è il seguente:
void Cls_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify);
Nel file RISORSE.ASM questa procedura viene chiamata
Risorse_OnCommand; in base alle considerazioni appena esposte,
all'interno della procedura WndProc possiamo scrivere:
messaggio_WM_COMMAND:
cmp uMsg, WM_COMMAND ; (uMsg == WM_COMMAND) ?
jne messaggio_WM_CLOSE
mov eax, wParam ; eax = wParam
mov ecx, eax ; ecx = wParam
and eax, 0000ffffh ; eax = id = LOWORD(wParam)
shr ecx, 16 ; ecx = codeNotify = HIWORD(wParam)
call Risorse_OnCommand, hWnd, eax, lParam, ecx
jmp exitWndProc
Come si può notare, per estrarre da wParam i due argomenti id e
codeNotify utilizziamo i due registri EAX e ECX; finché è possibile è
meglio evitare l'uso di registri come EBX, ESI e EDI il cui contenuto
deve essere preservato dalle procedure di callback come WndProc.
Appena entrati in Risorse_OnCommand, possiamo iniziare ad elaborare i
codici che arrivano dalla selezione dei vari menu items; a rigore
sarebbe opportuno verificare prima di tutto il valore contenuto in
codeNotify. Come è stato detto in precedenza, se il messaggio
WM_COMMAND proviene da un control, allora l'argomento codeNotify vale
1, mentre se il messaggio WM_COMMAND arriva da un menu, allora
l'argomento codeNotify vale 0; nel nostro caso, non essendoci nessun
control possiamo dare per scontato che il messaggio WM_COMMAND sia
stato inviato in seguito alla selezione di un menu item.
Il menu associato all'applicazione RISORSE contiene ben 26 menu
items, per cui all'interno di Risorse_OnControl dobbiamo elaborare le
26 possibili scelte effettuate dall'utente; attraverso il parametro
id possiamo sapere quale menu item è stato selezionato.
In precedenza è stato detto che è molto vantaggioso assegnare ad ogni
popup un apposito gruppo di codici numerici tra loro consecutivi; in
questo modo si possono ottenere notevoli semplificazioni nella fase
di elaborazione dei vari codici, e si può rendere anche più compatta
128
la procedura Risorse_OnCommand. Per rendercene conto possiamo
osservare il seguente codice C che sfrutta appunto il fatto che i
codici di ogni popup sono numeri interi consecutivi (il simbolo && in
C rappresenta l'AND logico):
if ((id >= CM_MENU1a) && (id <= CM_MENUEXIT))
{
/* elaborazione popup "Principale" */
}
else if ((id >= CM_BITMAP1) && (id <= CM_BITMAP5))
{
/* elaborazione popup "Bitmap" */
}
else if ((id >= CM_CURSOR1) && (id <= CM_CURSOR6))
{
/* elaborazione popup "Cursori" */
}
else if ((id >= CM_FONT1) && (id <= CM_FONT9))
{
/* elaborazione popup "Font" */
}
else if ((id >= CM_HELP) && (id <= CM_ABOUT))
{
/* elaborazione popup "Informazioni" */
}
In generale bisogna dire che in alcuni casi può essere conveniente
elaborare in un colpo solo un intero popup, mentre in altri casi può
essere necessario elaborare singolarmente i vari menu items presenti
all'interno di un popup; nel seguito vengono illustrate queste due
situazioni.

Partiamo quindi con l'analisi delle elaborazioni relative al popup


"Principale"; in questo caso il parametro id può assumere uno tra i 4
codici che abbiamo chiamato simbolicamente CM_MENU1a, CM_MENU1b,
CM_MENU1c e CM_MENUEXIT.
L'elaborazione dei primi 3 menu items consiste nel mostrare una
MessageBox che visualizza il codice associato al parametro id; in
questo modo possiamo constatare che effettivamente il parametro id
contiene proprio il codice che nel file RISORSE.RC abbiamo associato
al menu item appena selezionato. In questo caso è conveniente
elaborare questi primi 3 menu items in un unico blocco di istruzioni;
così facendo possiamo risparmiare parecchio codice inutile e
dispersivo.
Per convertire il contenuto di id in una stringa, utilizziamo come al
solito la procedura wsprintf; la stringa così ottenuta viene poi
inviata alla MessageBox. È importante ribadire ancora una volta che
in una situazione di questo genere, la MessageBox è "figlia" della
main window che la sta chiamando; di conseguenza il primo argomento
passato alla MessageBox deve essere l'handle (hwnd) della main
window.
L'arrivo del codice id = CM_MENUEXIT indica che l'utente attraverso
il menu ha richiesto la chiusura dell'applicazione RISORSE.EXE; in
una situazione del genere dovremmo in teoria chiamare la procedura
Risorse_OnClose che elabora il messaggio WM_CLOSE. Questa soluzione
però è da evitare in quanto le procedure che elaborano i vari
messaggi inviati ad una finestra, dovrebbero essere chiamate solo

129
dalla relativa window procedure; la soluzione ideale consiste allora
nel forzare Windows a generare un messaggio WM_CLOSE che verrà poi
intercettato da WndProc. A tale proposito ci possiamo servire
dell'apposita procedura SendMessage che ci permette di inviare un
messaggio ad una window procedure; questa procedura viene definita
nella libreria USER32.LIB e presenta il seguente prototipo:
LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
hWnd è l'handle della finestra che ha chiamato SendMessage; in questo
modo SendMessage sa a quale window procedure deve inviare il
messaggio.
Msg è il codice numerico del messaggio da inviare; nel nostro caso si
tratta ovviamente del codice WM_CLOSE.
wParam e lParam contengono informazioni aggiuntive da inviare alla
window procedure; nel nostro caso non dobbiamo inviare nessuna
informazione aggiuntiva, per cui questi due parametri valgono
entrambi NULL.
SendMessage termina restituendo in EAX un codice che dipende dal
messaggio che abbiamo spedito.

L'istruzione:
call SendMessage, hwnd, WM_CLOSE, NULL, NULL
ha come effetto la chiamata diretta di WndProc; la window procedure
riceve quindi il messaggio WM_CLOSE e chiama Risorse_OnClose che
elabora poi questo messaggio nel modo già illustrato nei precedenti
capitoli.
La procedura SendMessage lavora in modo sincrono con il nostro
programma; questo significa che SendMessage restituisce il controllo
solo quando l'elaborazione di WM_CLOSE è terminata. In alternativa
Windows mette a disposizione anche la procedura PostMessage; questa
procedura ha gli stessi identici parametri di SendMessage, ma
presenta la caratteristica di lavorare in modo asincrono. In pratica
PostMessage invece di inviare direttamente Msg ad una window
procedure, si limita ad inserire Msg nella coda dei messaggi della
stessa window procedure; dopo aver svolto questo lavoro PostMessage
restituisce subito il controllo al caller. In generale il
programmatore in base al contesto, deve essere in grado di stabilire
se sia meglio chiamare SendMessage o PostMessage; nel nostro caso è
stata usata SendMessage in quanto l'utente ha richiesto la chiusura
dell'applicazione, e quindi prima di proseguire è necessario
attendere una eventuale conferma di questa decisione.

Passiamo ora all'elaborazione del popup "Bitmap"; in questo caso il


parametro id può assumere uno tra i 5 codici che abbiamo chiamato
simbolicamente CM_BITMAP1, CM_BITMAP2, CM_BITMAP3, CM_BITMAP4 e
CM_BITMAP5. Attraverso i primi 4 menu items l'utente sta chiedendo di
modificare lo sfondo della main window con una apposita bitmap;
attraverso il quinto menu item l'utente sta chiedendo di tornare allo
sfondo originale della finestra.
Gli handle delle bitmap selezionabili sono già stati caricati dalla
procedura Risorse_OnCreate; vediamo allora come si deve procedere per
utilizzare queste bitmap come sfondo della main window.
Nei precedenti capitoli abbiamo già conosciuto le due procedure
CreateSolidBrush (crea un pennello solido) e CreateHatchBrush (crea
un pennello retinato); questa volta utilizziamo invece una nuova
130
procedura chiamata CreatePatternBrush. Questa procedura viene
definita nella libreria GDI32.LIB e presenta il seguente prototipo:
HBRUSH CreatePatternBrush(HBITMAP hBmp);
hBmp è l'handle di una bitmap che verrà utilizzata come tratto del
pennello.
La procedura CreatePatternBrush termina restituendo in EAX l'handle
del pennello selezionato; in caso di errore si ottiene EAX=NULL.
In pratica, il pennello fornitoci da CreatePatternBrush ci permette
di disegnare con un tratto che assume la forma (pattern) della bitmap
specificata come argomento; nel caso di Windows 95 la bitmap deve
essere da 8x8 pixel (se si utilizza una bitmap più grande, Windows 95
taglia una porzione da 8x8 pixel della bitmap stessa).
Se vogliamo riempire lo sfondo della main window con la bitmap
puntata da handleBitmap1, possiamo scrivere:
call CreatePatternBrush, handleBitmap1
In questo modo otteniamo in EAX un pennello che dobbiamo passare alla
procedura SetClassLong; in fase di esecuzione di un programma, questa
procedura ci permette di modificare i vari membri della struttura
WNDCLASSEX di una finestra (compresa la window procedure). La
procedura SetClassLong viene definita nella libreria USER32.LIB e
presenta il seguente prototipo:
DWORD SetClassLong(HWND hWnd, int nIndex, LONG dwNewLong);
hWnd è l'handle della window class da modificare.
nIndex è un codice numerico che ci permette di specificare il membro
di WNDCLASSEX che vogliamo modificare.
dwNewLong è una DWORD che contiene il nuovo valore da assegnare al
membro di WNDCLASSEX che vogliamo modificare.
La procedura SetClassLong termina restituendo in EAX il vecchio
valore contenuto nel membro di WNDCLASSEX che abbiamo modificato; in
caso di errore viene restituito EAX=0.
Il problema che si presenta è dato dal fatto che nel caso di modifica
dello sfondo di una finestra, la procedura SetClassLong non produce
nessun effetto immediato; per veder comparire il nuovo sfondo
dobbiamo costringere Windows ad aggiornare sia lo sfondo sia il
contenuto della finestra. Un modo per ottenere facilmente questo
risultato consiste nel chiamare la procedura InvalidateRect; questa
procedura viene definita nella libreria USER32.LIB e presenta il
seguente prototipo:
BOOL InvalidateRect(HWND hWnd, CONST RECT *lpRect, BOOL bErase);
hWnd è l'handle della finestra da aggiornare.
lpRect è l'indirizzo di una struttura RECT che contiene la porzione
rettangolare della finestra da aggiornare; se vogliamo aggiornare
l'intera finestra dobbiamo passare lpRect=NULL.
bErase è un flag che ci permette di richiedere o meno la
cancellazione del vecchio sfondo; se bErase=TRUE viene cancellato il
vecchio sfondo.
Se la procedura InvalidateRect termina con successo restituisce un
valore non nullo in EAX; in caso contrario si ottiene EAX=0.
La chiamata:
call InvalidateRect, hwnd, NULL, TRUE
forza Windows ad inviare a WndProc i messaggi WM_ERASEBKGND e
WM_PAINT; il messaggio WM_ERASEBKGND comporta la richiesta di
aggiornamento dello sfondo di una finestra. Naturalmente il messaggio
WM_ERASEBKGND non viene intercettato da WndProc, per cui va a finire

131
a DefWindowProc e viene quindi gestito direttamente da Windows; il
messaggio WM_PAINT viene invece smistato da WndProc alla procedura
Risorse_OnPaint che provvede ad aggiornare l'output della nostra
finestra.
In base alle considerazioni appena esposte, se vogliamo riempire lo
sfondo con la bitmap puntata da handleBitmap1 possiamo scrivere:
call CreatePatternBrush, handleBitmap1
call SetClassLong, hwnd, GCL_HBRBACKGROUND, eax
call InvalidateRect, hwnd, NULL, TRUE
La costante simbolica GCL_HBRBACKGROUND ci permette di richiedere a
SetClassLong la modifica dello sfondo di una finestra; per conoscere
le altre costanti simboliche si può consultare il Win32 Programmer's
Reference.
Come si può notare, è fondamentale passare a InvalidateRect
l'argomento bErase=TRUE; in caso contrario lo sfondo della finestra
non viene aggiornato.
Quando l'utente seleziona il menu item associato al codice
id=CM_BITMAP5, ci sta chiedendo di ripristinare lo sfondo originario;
in questo caso ci basta passare a SetClassLong un pennello ottenuto
con CreateSolidBrush e dotato dello stesso colore che avevamo scelto
in fase di riempimento della struttura WNDCLASSEX.
I concetti appena esposti, possono essere applicati anche per
richiedere uno sfondo di tipo bitmap in fase di riempimento della
struttura WNDCLASSEX; possiamo scrivere ad esempio:
call LoadBitmap, hInstance, offset bitmap1Name
call CreatePatternBrush, eax
mov wc.hbrBackground, eax

Attraverso il popup "Cursori" l'utente può selezionare 6 forme


differenti per il cursore del mouse; i codici associati a questi 6
menu items vanno da CM_CURSOR1 a CM_CURSOR6. I codici da CM_CURSOR1 a
CM_CURSOR5 permettono di selezionare dei cursori personalizzati i cui
handle sono stati già caricati da Risorse_OnCreate; il codice
CM_CURSOR6 permette di tornare al cursore predefinito a forma di
freccia.
Anche per i cursori dobbiamo utilizzare la procedura SetClassLong; il
secondo argomento da passare a questa procedura è la costante
simbolica GCL_HCURSOR, mentre il terzo argomento è l'handle del nuovo
cursore. Per sostituire ad esempio il cursore corrente con il nuovo
cursore puntato da handleCursor2 possiamo scrivere:
call SetClassLong, hwnd, GCL_HCURSOR, handleCursor2
Questa chiamata provoca l'immediata visualizzazione del nuovo
cursore; il cursore animato associato al codice CM_CURSOR5 è stato
"preso in prestito" dalla cartella windows\cursors di Windows XP.
Il codice CM_CURSOR6 permette di tornare al cursore predefinito a
forma di freccia; in questo caso bisogna passare a SetClassLong
l'handle del cursore IDC_ARROW che ci viene restituito come già
sappiamo dalla chiamata:
call LoadCursor, NULL, IDC_ARROW.

Attraverso il popup "Font" è possibile selezionare 9 font differenti


che verranno utilizzati per visualizzare una stringa al centro della
client area; i codici associati a questi 9 menu items vanno da
CM_FONT1 a CM_FONT9.
132
Appena viene selezionato un nuovo font viene chiamata la procedura
SetFont che provvede a caricare nel membro lfFaceName della struttura
LOGFONT il nome del font selezionato; subito dopo SetFont carica
nella variabile globale handleFont l'handle del nuovo font
selezionato. Il procedimento è lo stesso già mostrato nel precedente
capitolo; nel nostro esempio è fondamentale che handleFont venga
definito nel blocco _DATA per poter essere inizializzato con 0. A
questo punto bisogna forzare Windows a generare un messaggio WM_PAINT
in modo che la procedura Risorse_OnPaint ridisegni la stringa con il
nuovo font appena selezionato; è necessario richiedere anche il
ridisegno dello sfondo per evitare che la nuova stringa venga
disegnata su quella vecchia producendo così un pasticcio illeggibile.
Per ottenere questo risultato possiamo sfruttare di nuovo la
procedura InvalidateRect che deve ricevere TRUE come terzo argomento
(bErase).
La chiamata di InvalidateRect produce automaticamente un messaggio
WM_PAINT che viene intercettato da WndProc e inoltrato a
Risorse_OnPaint; la procedura Risorse_OnPaint verifica se handleFont
è diverso da zero, e in caso affermativo disegna una stringa al
centro della client area attraverso la tecnica che già conosciamo.

L'ultimo popup chiamato "Informazioni" contiene due menu items; il


primo (CM_HELP) provoca la comparsa di una semplice MessageBox,
mentre il secondo (CM_ABOUT) provoca la comparsa di una dialog box
che viene descritta più avanti.

7.7 Gestione del messaggio WM_DESTROY.

Il messaggio WM_DESTROY viene gestito dalla procedura


Risorse_OnDestroy; questa procedura può essere vista come il
distruttore della window class Risorse. All'interno di
Risorse_OnDestroy dobbiamo effettuare quindi tutti i necessari
ripristini dei valori; in particolare dobbiamo restituire a Windows
tutte le risorse non più necessarie alla nostra applicazione.
Analizzando il codice di RISORSE.ASM possiamo notare che prima di
tutto vengono distrutti con DeleteObject gli handle di tutte le
bitmap che abbiamo utilizzato nel programma; come già sappiamo,
DeleteObject può essere utilizzata per distruggere diverse risorse
tra le quali si possono citare le penne, i pennelli i font e le
bitmap.
Gli handle dei cursori caricati con LoadCursor o LoadCursorFromFile,
non devono essere distrutti; questo lavoro infatti viene svolto da
Windows in fase di distruzione della window class.
Successivamente Risorse_OnDestroy distrugge con DeleteObject anche
l'handle che abbiamo utilizzato per gestire i font; l'ultimo compito
svolto da Risorse_OnDestroy consiste come al solito nel chiamare
PostQuitMessage.

7.8 La dialog box RisorseAboutDlg.

Come è stato detto in precedenza, le dialog box sono delle finestre


che permettono ad un utente di dialogare con una applicazione;
l'esempio RISORSE.ASM utilizza una semplice dialog box che può essere

133
usata per mostrare alcune informazioni relative al programma e
all'autore del programma stesso.
Gli ambienti di sviluppo più sofisticati come Microsoft Visual C++,
Borland C/C++ Compiler, Borland Delphi, etc, mettono a disposizione
appositi strumenti (tools) visuali che permettono di disegnare
letteralmente le dialog box; in questo modo è possibile creare in
modo efficiente delle dialog box estremamente complesse, che verranno
poi tradotte automaticamente nel tipico pseudo codice che si utilizza
nei files di risorse.
Se non si dispone di questi tools è ugualmente possibile progettare
"a naso" le proprie dialog box; a tale proposito ci possiamo munire
di carta e penna per tracciare uno schema della dialog box che
vogliamo creare. Una volta che il disegno è pronto, possiamo
procedere alla traduzione "manuale" della dialog box in pseudo codice
da inserire nel resource file della nostra applicazione.

Nel file RISORSE.RC vediamo un esempio pratico su come sia possibile


creare facilmente delle semplici dialog box; il codice che dichiara
una dialog box deve iniziare con una intestazione del tipo:
nome_simbolico DIALOG x, y, larghezza, altezza
Il nome_simbolico ci permette di identificare la dialog box che
stiamo creando; anche in questo caso possiamo utilizzare una stringa
oppure un codice numerico. Dopo la parola chiave DIALOG incontriamo 4
parametri che rappresentano le coordinate x, y del vertice in alto a
sinistra della finestra, la larghezza e l'altezza della finestra; le
due coordinate x, y sono espresse in pixel e vengono calcolate
rispetto al vertice in alto a sinistra dello schermo. In relazione
invece alla larghezza e all'altezza di una dialog box è necessario
fare una precisazione che assume una importanza enorme; a differenza
di quanto accade per le normali finestre, la larghezza e l'altezza di
una dialog box vengono espresse non in pixel ma in dialog units. Per
le larghezze (asse orizzontale) l'unità di misura usata è pari a 1/4
della larghezza media in pixel dei caratteri del font utilizzato; per
le altezze (asse verticale) l'unità di misura usata è pari a 1/8
dell'altezza media in pixel dei caratteri del font utilizzato. Se non
si seleziona nessun font personalizzato, le unità di misura vengono
riferite al font di sistema di Windows (font predefinito); il font di
sistema è un font a spaziatura fissa simile a quello usato dal DOS.
L'utilizzo delle dialog units si rende necessario perché in questo
modo le proporzioni delle dialog box vengono rese indipendenti
(device independent) dalla risoluzione grafica dello schermo; se non
si adottasse questo accorgimento, ogni volta che si modifica la
risoluzione grafica dello schermo verrebbero completamente alterate
le proporzioni delle dialog box.

Tornando al file RISORSE.RC possiamo notare che dopo l'intestazione


della dialog box troviamo un'altra riga che inizia con la parola
chiave STYLE; in questa riga possiamo dichiarare una serie di aspetti
stilistici della dialog box. A tale proposito possiamo utilizzare una
serie di costanti simboliche molte delle quali coincidono con quelle
utilizzate per la main window nella procedura CreateWindowEx; tutte
queste costanti devono essere combinate tra loro con l'operatore |
del C (codice ASCII 124) che equivale all'operatore OR (bit-a-bit)
dell'Assembly. La costante DS_CENTER richiede che la dialog box venga
134
centrata sullo schermo, la costante WS_CAPTION abilita la barra del
titolo, la costante WS_SYSMENU abilita il menu di sistema, la
costante WS_VISIBLE fa in modo che la dialog box diventi visibile non
appena viene attivata, etc; per maggiori dettagli su queste costanti
si può consultare il Win32 Programmer's Reference.
Dopo la riga STYLE incontriamo un'altra riga che inizia con la parola
chiave CAPTION; questa riga ci permette di specificare una stringa
(tra doppi apici) che comparirà nella barra del titolo della dialog
box.
La riga successiva inizia con la parola chiave FONT e ci permette di
specificare l'altezza e il nome (tra doppi apici) del font da
utilizzare all'interno della dialog box; se il font che abbiamo
richiesto non è disponibile, verrà utilizzato il solito font di
sistema. In base a quanto è stato detto in precedenza, l'altezza del
font selezionato influenzerà le dimensioni e l'aspetto generale della
dialog box; di conseguenza, si consiglia di scegliere dei font molto
comuni, che siano disponibili su tutti i computers.
Se vogliamo aggiungere un menu alla nostra dialog box dobbiamo
utilizzare la parola chiave MENU seguita da una stringa tra doppi
apici che identifica il menu stesso; a titolo di curiosità, nella
dichiarazione della nostra dialog box si può provare ad aggiungere la
riga:
MENU "RisorseMenu"
A questo punto inizia il corpo della dialog box; tutte le
informazioni presenti nel corpo devono essere racchiuse tra parentesi
graffe { }, oppure tra le parole chiave BEGIN END. All'interno del
corpo di una dialog box possono comparire svariati oggetti come
bitmap, icone, pulsanti, stringhe di testo, controlli di
input/output, etc; questi oggetti vengono trattati come se fossero
finestre figlie (child windows) della stessa dialog box.

Come si può notare nel file RISORSE.RC, il corpo della nostra dialog
box inizia con una serie di oggetti di tipo CTEXT; un controllo di
tipo CTEXT ci permette di inserire una stringa di testo centrato
nell'area rettangolare creata dal controllo stesso. La dichiarazione
di un controllo CTEXT assume in generale il seguente aspetto:
CTEXT "stringa di testo", id, x, y, larghezza, altezza, stile
La stringa tra doppi apici rappresenta il testo che verrà
visualizzato all'interno di un CTEXT.
id è il codice numerico che utilizziamo per identificare il controllo
CTEXT; questo codice viene inviato da Windows insieme al messaggio
WM_COMMAND generato dal controllo CTEXT. Siccome i controlli CTEXT
hanno il compito di mostrare una semplice stringa statica, il
parametro id non ha nessun significato; in questo caso bisogna
utilizzare il valore predefinito -1.
x, y, larghezza, altezza rappresentano ovviamente le caratteristiche
geometriche della finestra associata al CTEXT, e vengono espresse in
dialog units; anche x, y quindi sono espresse in dialog units e sono
riferite al vertice in alto a sinistra della dialog box. Queste
considerazioni sono valide per tutti gli oggetti presenti nel corpo
di una dialog box.
L'ultimo parametro di CTEXT definisce gli aspetti stilistici di
questo controllo; la costante WS_CHILD indica che la finestra del
CTEXT è confinata all'interno della dialog box, mentre la costante
135
WS_BORDER permette di assegnare un bordo in 3d alla finestra stessa.

Dopo i vari oggetti CTEXT incontriamo un oggetto di tipo PUSHBUTTON


che dichiara un controllo a pulsante; la dichiarazione di un
controllo PUSHBUTTON assume in generale il seguente aspetto:
PUSHBUTTON "stringa di testo", id, x, y, laghezza, altezza, stile
Il significato di questi parametri è identico al caso di CTEXT;
questa volta però il parametro id diventa importante in quanto ci
permette di sapere quando il pulsante è stato premuto. A tale
proposito si può notare che viene utilizzata la costante simbolica
IDOK che è la stessa restituita da una MessageBox quando si preme il
suo pulsante Ok; in alternativa possiamo utilizzare anche un apposito
codice numerico personalizzato.
Lo stile WS_TABSTOP indica tutti quei controlli che possiamo
selezionare in sequenza attraverso la pressione del tasto [Tab].

L'ultimo oggetto presente nella nostra dialog box viene identificato


dalla parola chiave CONTROL; la dichiarazione di un oggetto CONTROL
assume in generale il seguente aspetto:
CONTROL nome_simbolico, id, "tipo controllo", stile, x, y, larghezza,
altezza
nome_simbolico è il nome che utilizziamo per identificare il
controllo e può essere una stringa tra doppi apici o un valore
numerico.
id identifica il codice associato al controllo; se questo codice non
è significativo bisogna usare come al solito -1.
tipo_controllo è una parola chiave tra doppi apici che identifica il
tipo di controllo; nel nostro caso, dovendo definire un controllo di
tipo bitmap dobbiamo utilizzare la parola chiave STATIC.
Gli altri parametri hanno il solito significato; nel caso di CONTROL
di tipo icona o bitmap, i parametri larghezza e altezza vengono
ignorati.
Per definire un controllo di tipo bitmap bisogna inserire la costante
di stile SS_BITMAP; per definire un controllo di tipo icona bisogna
inserire la costante di stile SS_ICON.
Nel nostro caso viene definito un controllo di tipo bitmap che
utilizza il nome simbolico "RisorseLogo"; questo nome è stato
dichiarato in precedenza nel file RISORSE.RC, e identifica la bitmap
"ramlogo.bmp".

Tutte le informazioni sulla dialog box dichiarata in RISORSE.RC


vengono utilizzate nel file RISORSE.ASM per gestire la dialog box
stessa; vediamo allora in dettaglio come si sviluppa questa gestione.
Con il menu item associato al codice CM_ABOUT l'utente sta
richiedendo la visualizzazione della nostra dialog box; per
rispondere a questa richiesta possiamo utilizzare la procedura
DialogBoxParam. Questa procedura viene definita nella libreria
USER32.LIB e presenta il seguente prototipo:
int DialogBoxParam(HINSTANCE hInstance, /* istanza dell'applicazione */
LPCTSTR lpTemplateName,/* indirizzo nome simbolico dialog
box */
HWND hwndParent, /* handle della finestra madre */
DLGPROC lpDialogFunc, /* indirizzo window procedure */
LPARAM dwInitParam /* informazioni di

136
inizializzazione */
);
hInstance è l'handle dell'applicazione a cui appartiene la dialog
box; nel nostro caso la dialog box appartiene all'applicazione
RISORSE, per cui dobbiamo utilizzare il solito hInstance che avevamo
reperito con GetModuleHandle.
lpTemplateName è l'indirizzo del nome simbolico oppure il codice
numerico che identifica la dialog box; nel nostro caso utilizziamo il
nome simbolico RisorseAboutDlg.
hwndParent è l'handle della finestra "madre" che ha creato la dialog
box; nel nostro caso si tratta dell'handle della window class
Risorse.
lpDialogFunc è l'indirizzo della window procedure che elabora i
messaggi inviati da Windows alla dialog box; come accade per tutte le
finestre, anche le dialog box devono specificare quindi la loro
window procedure.
dwInitParam è una DWORD che ci permette di inviare ulteriori
informazioni per la fase di creazione della dialog box; se non esiste
nessuna informazione aggiuntiva da inviare, questo parametro deve
valere NULL.
Se DialogBoxParam termina con successo restituisce in EAX il valore
di ritorno della dialog box; in caso contrario si ottiene EAX=-1.

All'interno della procedura Risorse_OnCommand la risposta al menu


item CM_ABOUT è rappresentata dalla chiamata:
call DialogBoxParam, hInstance, offset AboutDlgName, hwnd, offset
AboutWinProc, NULL
La window procedure della nostra dialog box è rappresentata quindi
dalla procedura AboutWinProc; questa procedura ha una struttura del
tutto simile a quella di WndProc.
La procedura DialogBoxParam provvede anche a creare un loop dei
messaggi per la dialog box appena attivata; questo loop dei messaggi
viene gestito direttamente da Windows. La chiamata di DialogBoxParam
determina l'invio da parte di Windows del messaggio WM_INITDIALOG;
questo messaggio è l'equivalente di WM_CREATE e quindi viene inviato
prima che la dialog box venga visualizzata. In questo caso il
parametro lParam di AboutWinProc riceve il contenuto del parametro
dwInitParam inviato da DialogBoxParam; l'elaborazione del messaggio
WM_INITDIALOG termina in genere con la restituzione del valore TRUE
in EAX.
A questo punto la nostra dialog box viene visualizzata sullo schermo,
ed è pronta per ricevere altri messaggi; la gestione di questi
messaggi si svolge in modo del tutto analogo a quanto abbiamo visto
per WndProc, con la differenza che questa volta non dobbiamo chiamare
nessuna window procedure predefinita (come DefWindowProc).
Osserviamo in particolare che AboutWinProc intercetta il messaggio
WM_COMMAND che ci permette di interfacciarci con i vari controlli
inseriti nella dialog box; quando viene premuto ad esempio il
pulsante 'Ok' della dialog box, viene inviato il messaggio WM_COMMAND
accompagnato dall'id del pulsante premuto. Questo id si trova come al
solito nella WORD meno significativa del parametro wParam ricevuto da
AboutWinProc; si tratta del codice IDOK che abbiamo assegnato al
PUSHBUTTON nel file RISORSE.RC.
Quando l'utente preme il pulsante di chiusura della dialog box, o
137
seleziona Chiudi dal menu di sistema, viene inviato un messaggio
WM_COMMAND accompagnato da id = IDCANCEL; in risposta all'arrivo del
codice IDOK o IDCANCEL dobbiamo chiudere la dialog box. A tale
proposito ci dobbiamo servire della procedura EndDialog; questa
procedura viene definita nella libreria USER32.LIB e presenta il
seguente prototipo:
BOOL EndDialog(HWND hDlg, int nResult);
hDlg è l'handle che AboutWinProc ha ricevuto come primo argomento.
nResult è il valore di ritorno restituito dalla dialog box prima di
terminare; nel nostro caso la dialog box termina con il valore TRUE
che comunque viene ignorato.
Se EndDialog termina con successo restituisce un valore non nullo in
EAX; in caso contrario si ottiene EAX=0.

Un'ultima considerazione riguarda il fatto che la procedura


DialogBoxParam crea una dialog box di tipo modal (modale); una dialog
box modale quando viene aperta toglie il controllo alla finestra
"madre" fino a quando non viene chiusa. All'estremo opposto troviamo
invece le dialog box di tipo modeless (non modali); questo tipo di
dialog box non toglie il controllo alla finestra "madre", e questo
significa che possiamo saltare dalla finestra "madre" alla finestra
"figlia" o viceversa.

7.9 Generazione dell'eseguibile RISORSE.EXE.

Per generare l'eseguibile RISORSE.EXE con il MASM bisogna uscire al


prompt del DOS, posizionarsi nella cartella:
c:\masm32\win32asm\Risorse
e digitare:
risorse.bat
Per generare l'eseguibile RISORSE.EXE con il TASM bisogna uscire al
prompt del DOS, posizionarsi nella cartella:
c:\tasm\win32asm\Risorse
e digitare:
..\..\bin\make -B -frisorse.mak

Se analizziamo ora le dimensioni dei vari files, possiamo notare


quanto segue (caso del TASM):
Il file RISORSE.OBJ generato dall' assembler occupa su disco circa 5
Kb.
Il file RISORSE.RES generato dal compilatore di risorse occupa su
disco circa 79 Kb.
Il file RISORSE.EXE generato dal linker occupa su disco circa 88 Kb.
Tutto questo ci fa capire che le risorse incorporate in un programma
influiscono in modo drastico sulle dimensioni dell'eseguibile finale;
nel caso di RISORSE.EXE si vede che le risorse occupano quasi il 90%
dell'eseguibile.
In ogni caso, gli appena 88 Kb del nostro programma scritto in
Assembly, appaiono ridicoli al cospetto delle parecchie centinaia di
Kb dello stesso programma scritto in Java o in Visual Basic; non
parliamo poi delle prestazioni perché in questo caso il confronto
diventa veramente penoso.

138