Win32 Assembly

Capitolo 3: Struttura di un programma Assembly per Win32

Sin dalle prime versioni il SO Windows e' stato scritto in C standard (ANSI C); le parti piu' 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 modalita' 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 modalita' protetta delle CPU 80386 e superiori esiste la segmentazione della memoria tipica della modalita' reale a 16 bit; la differenza sta nel fatto che mentre in modalita' reale a 16 bit i segmenti sono da 64 Kb ciascuno, in modalita' protetta a 32 bit ciascun segmento puo' arrivare sino a 4 Gb.
La struttura di un programma Assembly per Win32 e' molto simile alla struttura di un programma Assembly per DOS con modello di memoria SMALL; in base a quanto e' stato detto nella sezione Assembly Base, un programma che gira in modalita' reale con modello di memoria SMALL e' 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 puo' 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 cio' 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 e' 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 piu efficiente e veloce dei dati e dello stack del programma.
La stessa situazione si verifica nel caso di un'applicazione per Win32 che e' 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 pero' stiamo lavorando in modalita' 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 dovra' essere dotato di appositi segmenti di programma destinati a contenere il codice, i dati e lo stack; come e' 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 linguagi 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 piu' 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 e' stato gia' detto, si tratta delle direttive minime necessarie per generare un programma Assembly per Win32.

La direttiva .386,
indica all'assembler che vogliamo utilizzare il set di istruzioni a 32 bit delle CPU 80386 e superiori; come sappiamo Win32 lavora in modalita' protetta a 32 bit, per cui richiede come minimo una CPU di classe 80386. Come e' stato spiegato nella sezione Assembly Base, e' 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 modalita' 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 e' FLAT. In base a quanto e' stato detto in precedenza, il modello FLAT puo' essere paragonato ad una sorta di modello SMALL per la modalita' protetta a 32 bit; in sostanza, il nostro programma e' 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 piu' importante da considerare e' 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 modalita' reale 8086 il modello FLAT rappresenta la fine di un incubo. In modalita' 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, e' 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 piu' di 64 Kb di codice e/o piu' 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 piu' grande di 64 Kb; anche in questo caso siamo costretti a richiedere al SO due o piu' blocchi di memoria.
Per superare questo problema bisogna ricorrere alla modalita' 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. Il problema che si presenta e' dato dal fatto che in ambiente DOS, se vogliamo scrivere programmi che girano in modalita' 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 modalita' 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 cio' che e' 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 e' 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 piu' 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 e' 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 e' piu' veloce nella pulizia dello stack. La convenzione STDCALL seguita da Win32 e' 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 e' 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 e' 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 e' necessario distinguere tra lettere maiuscole e lettere minuscole; questo ci permette ad esempio di definire due variabili chiamate var1 e VAR1 che verranno considerate distinte dall'assembler (si tratta comunque di un pessimo stile di programmazione). Ricordiamoci che Windows e' stato scritto prevalentemente in C, e cioe' con un linguaggio che distingue tra lettere maiuscole e lettere minuscole (case-sensitive); in sostanza questo significa che in C il nome var1 e' diverso dal nome VAR1. Il Pascal invece e' 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 puo' 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 gia' sappiamo questo e' 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. E' 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 e' 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 piu' 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 e' associato un include file che contiene l'interfaccia della libreria stessa. Una libreria contiene il codice delle varie procedure, e cioe' le definizioni delle procedure; il corrispondente include file contiene i prototipi di queste procedure, e cioe' le dichiarazioni delle procedure, piu' 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.
Figura 2 - Segmento dati inizializzati

_DATA    SEGMENT  DWORD PUBLIC USE32 'DATA'

_DATA    ENDS
In questo blocco dobbiamo inserire tutti i dati inizializzati del nostro programma, cioe' 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 e' 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 'DATA'. L'attributo di allineamento e' DWORD, e questo significa che il segmento _DATA deve partire da un'indirizzo di memoria multiplo di 4 byte; come sappiamo, questo e' 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 velocita' 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 gia' 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 cio' che si vede in Figura 2 con la direttiva semplificata .DATA.

3.3 Il segmento dati non inizializzati

Il segmento dati inizializzati precedentemente descritto, puo' anche contenere la definizione di dati privi di valore iniziale; un esempio pratico puo' essere rappresentato dalla definizione:
varDword1 dd ?.
Inserendo i dati non inizializzati nel segmento dati inizializzati, otteniamo naturalmente un programma perfettamente funzionante; se pero' vogliamo aiutare il SO ad ottimizzare al massimo l'utilizzo della memoria, allora e' 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.
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'. E' importante che in questo blocco non vengano inseriti dati inizializzati; in tal caso il programma funziona ugualmente bene, ma costringe il SO ad utilizzare piu' memoria di quella necessaria.
Se preferiamo servirci delle direttive semplificate per i segmenti, possiamo sostituire tutto cio' 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 piu' 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 e' 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 e' 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 puo' 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.
Figura 5 - Definition File

NAME           Win16App
DESCRIPTION    'Applicazione per Win16'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
Il definition file e' un file in formato ASCII che reca l'estensione DEF; convenzionalmente, il nome del definition file e' 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 verra' 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 verra' assegnato all'eseguibile generato dal linker; nel nostro caso, l'eseguibile verra' 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 verra' generato dal linker; nel nostro caso il tipo WINDOWS indica che Win16App.exe sara' in formato eseguibile per Windows.
Il campo STUB indica un programma che verra' 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:
This program requires 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 verra' automaticamente caricato in memoria al momento dell'avvio del nostro programma, l'attributo MOVEABLE indica che all'occorrenza Windows potra' 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 potra' scaricare blocchi di codice sull'Hard Disk per fronteggiare situazioni di scarsita' 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 piu' istanze dello stesso programma, ogni istanza avra' la sua copia privata del segmento dati.
Il campo HEAPSIZE indica la dimensione iniziale in byte che verra' assegnata al Local Heap della nostra applicazione; nel caso di Figura 5, viene assegnata una dimensione iniziale di 1024 byte che se necessario verra' modificata da Windows.
Il campo STACKSIZE indica la dimensione in byte da assegnare allo stack del nostro programma; in Win16 la dimensione minima raccomandata e' di 8192 byte (8 Kb), mentre in Win32 e' di circa 10 Kb.

Gia' 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 e' 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 e' superfluo in quanto in Win32 ogni applicazione ha il suo spazio di indirizzamento privato. In generale quando si sviluppano applicazioni per Win32 e' anche possibile omettere il definition file; in questo caso i vari linker utilizzano una serie di impostazioni predefinite che spesso sono molto piu' 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 disponibilita' 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 e' 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 semplicita' dello scheletro del segmento di codice di un'applicazione Assembly per Win32.
Figura 6 - Segmento di codice

_TEXT    SEGMENT DWORD PUBLIC USE32 'CODE'

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 e' che il segmento di codice deve chiamarsi obbligatoriamente _TEXT; l'attrubuto di classe inoltre deve essere obbligatoriamente 'CODE'. 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, 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 e' 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 e' 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 virtu' del fatto che Windows e' scritto in C, anche i manuali di riferimento sull'API di Windows utilizzano la sintassi del linguaggio C; chi conosce bene questo linguaggio si trovera' 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 e' 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 piu' chiara l'istruzione; ricordiamoci che in modalita' 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 e' in grado di determinare la loro dimensione in bit. Se l'operando e' 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 operandi a 32 bit, in modo che ESP rimanga sempre allineato alla DWORD; e' 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 e' 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 piu' 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 puo' essere analizzato attraverso il Listing File.

Un'aspetto importantissimo da analizzare e' legato al fatto che anche all'interno del segmento di codice del nostro programma possiamo definire delle variabili; come al solito, e' 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 puo' 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:

; -------------- 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 e' 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 e' dato dal fatto che ci troviamo in modalita' 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), e' 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 modalita' 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 e' ovviamente un segmento eseguibile), e' 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 e' 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 puo' 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 e' quello di permettere una gestione piu' 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 piu' assoluta di modificare il contenuto di questi registri (come ad esempio, tentare di caricare DGROUP in DS); d'altra parte abbiamo anche visto che in modalita' protetta i registri di segmento svolgono un ruolo completamente diverso da quello svolto in modalita' 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, puo' 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
A lungo andare, l'uso delle istruzioni PUSH per il passaggio degli argomenti alle procedure puo' 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, e' 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 e' 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 puo' 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 puo' essere riscritta come:
IF TASM
   call     ExitProcess, 0
ELSE
   invoke   ExitProcess, 0
ENDIF
Se la costante TASM ha un valore diverso da zero, allora verra' eseguita la direttiva avanzata CALL; se invece la costante TASM vale zero, allora verra' 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 diventa piuttosto impegnativo perche' il programmatore e' costretto praticamente a scrivere due programmi in uno; se si ha la necessita' di assemblare con TASM un programma scritto per il MASM contenente quindi parecchie direttive INVOKE, si puo' tentare di utilizzare la seguente macro:
invoke equ call.
E' chiaro pero' che se nella lista degli argomenti passati tramite INVOKE e' 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 e' 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 e' possibile utilizzare le direttive avanzate di MASM e TASM.
Avendo a disposizione le ultime versioni di MASM e TASM non c'e' nessuna ragione per utilizzare il template di Figura 8; come gia' 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 puo' 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 e' 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 e' 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
Come si puo' notare, il template di Figura 9 risulta molto piu' semplice e compatto del template di Figura 8; sono inoltre scomparse tutte le fastidiose direttive ASSUME e anche il gruppo DGROUP. Molti programmatori Assembly preferiscono utilizzare la versione di Figura 8 del template, in quanto la sintassi classica per i segmenti di programma contribuisce ad evidenziare maggiormente i vari blocchi che formano il programma stesso; in ogni caso, indipendentemente dai gusti personali e' necessario ribadire che con le versioni piu' recenti di MASM e TASM, la versione di Figura 9 del template deve essere preferita a quella di Figura 8.

3.9 Programmazione in modalita' 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 modalita' protetta a 32 bit delle CPU 80386 e superiori; e' necessario quindi riassumere brevemente le principali differenze che esistono tra la modalita' protetta a 32 bit e la modalita' 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 pero' e' un numero a 32 bit; con un offset a 32 bit possiamo spostarci teoricamente da 00000000h a FFFFFFFFh. Quest'offset e' riferito al base address, cioe' 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 puo' ricoprire solo il ruolo di registro base. In modalita' protetta a 32 bit possiamo quindi scrivere istruzioni del tipo:
mov dx, [eax + edi + 120500]
E' 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, e' importante tenere ESP e EBP sempre allineati alla DWORD; a tale proposito e' 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, e' 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 piu' 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 modalita' 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 responsabilita' di ripulire lo stack.
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 modalita' protetta bisogna evitare nella maniera piu' assoluta di chiamare porzioni di codice scritte espressamente per la modalita' 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 modalita' reale, e la loro chiamata quindi manda in crash un programma che gira in modalita' 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 gia' 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
   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 argometi di tipo puntatore a stringa, e cioe' 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, cioe' 4 byte piu' 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 e' 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 puo' notare, si tratta dello stesso procedimento gia' 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 gia' conosciamo; l'unica differenza e' 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 puo' essere o diretta intrasegmento (salto ad una etichetta NEAR), o indiretta intrasegmento (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 puo' notare, gli argomenti vengono inseriti nello stack partire dall'ultimo; il valore immediato restituito dall'operatore OFFSET e' naturalmente l'indirizzo 32 bit del relativo operando. Appena copiaStringa restituisce il controllo al caller, il valore di ritorno della procedura e' 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 e' 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 e' 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 puo' notare, tutta la gestione dello stack frame, compresa la pulizia dello stack, viene delegata all'assembler; le modalita' che 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 e' 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 e' di tipo diretto intrasegmento (in sostanza, copiaStringa e' 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.