Segmenti e loro inizializzazione in Masm 6.1, Direttive estese e semplificate, calling/naming convention...un po'di tutto

Data

By "Lonely Wolf"

 

14/04/2004

UIC's Home Page

Published by Quequero

Se son matto pazienza. Preferisco la mia follia alla saggezza degli altri - Van Gogh

Complimenti Lone, quando ti metti le fai per bene le cose!!

Zia, cosa è pagano? Tutto ciò che non piace ai preti Morgana... - Dalle Nebbie di Avalon

....

E-mail: [email protected]
Su azzurra, #crack-it #cryptorev #pmode

....

Difficoltà

( )NewBies(X)Intermedio( )Avanzato( )Master

 

Oggi niente cracking o reversing...oggi architetturing, segmenti & co. :D che male non fa :P


Direttive estese e semplificate, segmenti e loro inizializzazione in Masm 6.1, calling/naming convention...un po'di tutto

Written by "Lonely Wolf"

Introduzione

Anni fa a scuola, iniziando il linguaggio assembler dopo la parte introduttiva, il prof (cmq mitiko!) distribuì a tutta la classe una fotocopia con lo "scheletro" di un programma assembler scritto con le direttive estese del masm, avete presente, stack segment para stack, ASSUME... qualche riga sotto il codice opportuno per l'inizializzazione dei segmenti, quella che era la parte "standard"(?) come la chiamò lui ed esordì dicendoci che dovevamo fidarci e prenderla per buona(!) per non entrare in dettagli e particolari che evidentemente non conosceva/non sapeva/non voleva spiegare. Boh, che dovevamo fare? L'abbiamo presa per buona e siamo andati avanti, ma personalmente mi scocciava avere un pezzo di codice che non sapevo bene che facesse e che non potevo spiegare..cmq vabbe', alla fine qualcosa ci ho capito. Sono passati diversi anni da allora e io mi son diplomato. Un mesetto fa, neanche, un mio amico (ciao Luchino!:D) che frequenta lo stesso corso di informatica mi chiama perchè ha bisogno che gli rispieghi alcune cose di assembler che non ha capito bene, gli faccio qualche domanda per farmi un'idea e cosa viene fuori? Che il loro prof (un altro a questo giro) incappando sullo stesso punto ha liquidato l'ennesima classe con il solito "prendete per buono questo pezzo di codice...ora non lo stiamo a spiegare...non scendiamo nei dettagli..." Mi sono detto: Noooo! Siamo ancora a questi livelli???
Non solo, l'altro giorno ho visto di peggio: il libro di testo di sistemi adottato quest'anno dalla classe terza del mio amico Luca. Scritto da una donna...arriva al punto in cui spiega l'uso delle direttive estese e semplificate. Le semplificate al solito, .MODEL ecc...mentre per quella tipa le direttive estese sono:

DATA SEGMENT
 ...
DATA ENDS
STACK SEGMENT
 ..
STACK ENDS

CODE SEGMENT
...

CODE ENDS
Scandaloso! A questo punto faceva solo le direttive semplificate e faceva più figura...la scuola superiore...puah! Per questo motivo nasce quello che state leggendo, in ITALIANO (per la gioia di quei disgraziati che non sanno/vogliono leggere in inglese... :P), assemblato con pezzi vari rimediati in qua e la da internet (per cui non mi rientra di darvi una bibliografia precisa), anzi, se scrivo castronerie sono ben lieto che me lo facciate presente o anche se ritenete opportuno aggiungere qualcosa. Spero di essere chiaro e di fare un buon lavoro, e soprattutto che capiate (anzi, che capiamo, mi ci metto anche io visto che per arrivare in fondo a questo tutorial ho dovuto sudare e farmi spiegare diverse cosette) e che magari possiate fare una bella figura col prof, dandogli una bella "sverniciatina" ;) ihihih
Ma Masm6.1?? ma è obsoleto! potrebbe dire qualcuno. Beh, si, non è esattamente quello che si può definire l'ultimo grido, ma tuttavia ritengo che ne valga davvero la pena approfondire e (in teoria) tutto questo non dovrebbe essere inutile. Non voglio reinventare la ruota, per cui non troverete l'elenco delle istruzioni e come dovreste usarle, in rete ce ne sono anche troppi, ma semplicemente affrontare l'argomento segmentazione e inizializzazione (lo scopo per cui è nato, anche se redigendo il tute mi sono reso conto che per spiegare certe cose ne andavano spiegate delle altre, alcune delle quali al momento della stesura - notturna, e sono alla quarta notte di fila con questa, o è la quinta? - io stesso non sapevo o mi son reso conto di non aver capito poi così bene, per cui ho dovuto farmele spiegare da qualche anima pia in chan ;) e per questo le dimensioni del tute sono lievitate. Inoltre, nel tute verranno presi in considerazione, quanto meno accennate, molti di quei paroloni o di quelle sigle oscure che potete trovare facilmente in giro da queste parti, quali...boh TLB, TSS, LDT, GDT...anche se una trattazione completa esula dagli scopi di questo tute. Ci ho passato le feste sopra e sicuramente ci dovrò tornare sopra per capire meglio varie cosine, ma ne è valsa la pena: ragazzi, basta con i bignami e la riviste su carta patinata, studiamo i manuali intel. Un si sbaglia ;D

Tools usati

Volontà - Non so dove si possa scaricare, provate su emule o idc, tanto ormai si trova praticamente di tutto :D Beh, il set di istruzioni del vostro processore :P

URL o FTP del programma

Sta sulla scheda madre :)

Notizie sul programma

Ragazzi, oggi si va giù pesi, rilassatevi, fumate, prendete una boccata d'aria prima...  

Essay

La segmentazione
-----------------

Eh, hai detto niente, e mo da dove comincio?
Dal manuale Intel (Basic Architecture): "Con la segmentazione, un registro di segmento a 16 bit contiene un puntatore a un segmento di memoria fino a 64Kb. Usando 4 registri di segmento alla volta i processori 8086/8088 sono capaci di indirizzare 256Kb senza switchare tra i segmenti.......Con il modello della memoria segmentata un programma vede la memoria come un gruppo di spazi di indirizzi indipendenti chiamati appunto Segmenti. Usando questo modello codice, dati, stack sono tipicamente contenuti in segmenti diversi. Per indirizzare un byte in un segmento, un programma deve fornire un Logical Address(Indirizzo Logico) composto da un segmento selettore e un offset. Un indirizzo logico è spesso riferito come Far Pointer. Il segmento selettore identifica il segmento che deve essere acceduto e l'offset identifica un byte nello spazio di indirizzi di tale segmento...
Internamente tutti i segmenti che vengono definiti per un sistema sono mappati nel Linear Address Space. Per accedere ad una locazione di memoria il processore traduce (in maniera trasparente) gli indirizzi logici in indirizzi lineari. La prima ragione per usare la segmentazione è per accrescere l'affidabilità dei programmi e dei sistemi. Ad esempio, mettendo lo stack in un segmento separato si evita che questo cresca nel codice o nei dati e sovrascriva i dati relativi.
Con il modello flat (che vedremo cmq dopo) o segmentato, il linear address space viene mappato nello spazio di indirizzi fisico del processore sia direttamente che mediante paging. Quando si usa il mapping diretto (paging disabilitato) ogni indirizzo lineare ha una corrispondenza 1:1 con un indirizzo fisico (e quindi questi indirizzi lineari vengono mandati sul bus indirizzi del processore senza dover essere tradotti). Quando si usa il meccanismo di paging (anche esso trasparente) della IA-32 lo spazio di indirizzi lineare viene diviso in pagine, mappate nella memoria virtuale.


8086 impiega una architettura "segmentata", nella quale ogni indirizzo è rappresentato come

segmento:offset

dove segmento consiste sempre di un valore a 16 bit come offset. In real mode, il valore del segmento è un indirizzo fisico che ha una relazione matematica col il valore offset. Insieme, segmento e offset creano un'indirizzo fisico di 20 bit. La manipolazione del segmento e dell'offset che avviene direttamente nella programmazione in real mode viene chiamata "aritmetica dei segmenti". E'implicito che i programmi che utilizzano questa tecnica non siano compatibili in un contesto protected mode, ovviamente...(non ridere ntos :D) Ma come fa il processore a combinare un segmento di 16 bit e un offset sempre di 16 bit per formare un indirizzo di 20 bit? Ecco come/cosa fa:

1. A scuola ci dicevano "si mette uno 0 in fondo all'indirizzo..." si è vero, il risultato è quello ma un momento ;)
Il processore shifta l'indirizzo del segmento di 4 bit nulli, producendo così un indirizzo di 20 bit. Questa operazione ha lo stesso effetto della moltiplicazione dell'indirizzo del segmento per 16. (un po'più esaustiva come spiegazione, non trovate? :P)
2. Il processore aggiunge questo indirizzo di segmento di 20 bit all'offset di 16 (che NON viene shiftato)
3. Il processore utilizza così l'indirizzo risultante di 20 bit chiamato indirizzo fisico, per accedere alla corrente locazione nello spazio di indirizzi di 1 megabyte (si 1 Mb. 2^20 boys)

Es.


      5 3 C 2   valore registro di segmento

    5 3 C 2 0   valore registro shifato di 1
  +
      1 0 7 A   l'offset di 16 bit
--------------
    5 4 C 9 A   indirizzo fisico di 20 bit
Nota:
Un'indirizzo fisico di 20 bit può essere specificato con 4096 segmento:offset diversi! Infatti gli indirizzi 0000:F800, 0F00:0800 e 0F80:0000 si riferiscono tutti allo stesso indirizzo fisico 0F800.

Questi concetti sono molto importanti, perchè riguardano molti aspetti della programmazione in asm, specialmente per gli indirizzi e i puntatori. Originariamente l'architettura segmentata fu progettata per abilitare un processore a 16 bit per accedere a uno spazio di indirizzi più largo di 64K. Perchè 64k? Beh, dovreste saperlo e mi scuso con quelli di voi che queste cose oramai le mangiano a colazione (ehi, mica vi ho detto di leggerlo per forza sto tute :P hheheh). Basta fare 2 conti, anzi 1, 64K è la grandezza massima che può avere un segmento visto che è il numero più grande che si può rappresentare su *16* bit. Anche i successivi 80386 e 80486 aderivano a questo limite quando giravano in real mode, mentre in protected mode usavano i loro bei registri a 32bit e potevano indirizzare memoria fino a 4 Gb.

Ancora sulla segmentazione e paginazione dai manuali intel: La segmentazione provvede un meccanismo per isolare codice individuale, dati e stack in modo che più programmi (task) possano essere eseguiti sullo stesso processore senza interferire l'uno con l'altro. La paginazione fornisce un meccansimo per implementare una demand-paged convenzionale, sistema di memoria virtuale dove sezioni dell'ambiente di esecuzione di un programma sono mappate nella memoria fisica quando necessario (Quando segmentazione e paginazione vengono combinate insieme, i segmenti possono essere mappati in pagine in molti modi)
Anche la paginazione può essere utilizzata per fornire isolamento tra task multipli (ogni segmento viene diviso in pagine solitamente di 4Kb che vengono memorizzate nella memoria fisica o eventualmente su disco). Le informazioni che il processore usa per mappare indirizzi lineari nei relativi indirizzi fisici e per generare eventuali Page-Fault vengono mantenute in "Page Directory" e un set di "Page Tables" in memoria, per tenere traccia delle pagine. In questo modo, quando un programma tenta di accedere ad una locazione nello Linear Address Space il processore usa la Page Directory e la Page Tables per *Tradurre* l'indirizzo lineare nel corrispondente indirizzo fisico. Se la pagina richiesta non dovesse trovarsi nella memoria fisica il processore interromperebbe l'esecuzione del programma (generando una eccezione "Page-Fault, #PF"). L'Exception Handler per il Page-Fault la caricherebbe dal disco e il ritorno dall'Exception Handler farebbe riprendere l'esecuzione del programma.
Per minimizzare i cicli di bus richiesti per la traduzione degli indirizzi, le Page Directory accedute più frequentemente e le Page Tables entries sono cachate nel processore in dispositivi chiamati Translation Lookaside Buffers (TLBs). La famiglia P6 e processori Pentium hanno TLBs separate per cachare dati e istruzioni. L'istruzione CPUID può essere usata per determinare la grandezza del TLB del proprio processore. Le TLBs soddisfano molte richieste di lettura della Page Directory corrente e Page Tables senza richiedere un ciclo di bus, ciclo che occorrerebbe solo in caso di "cache miss" (ovvero quando non è presente nel buffer, ergo la pagina non è stata acceduta da un pezzo). Mentre esiste un modo per abilitare/disabilitare il paging (il bit 31 del registro di controllo CR0..Disponibile su tutti i processore IA-32 a partire dal 80386) non c'è alcun modo per disabilitare la segmentazione.....
La segmentazione provvede un meccanismo per dividere lo spazio di memoria indirizzabile del processore (Linear Address Space) in spazi di indirizzi più piccoli protetti, chiamati appunto SEGMENTI. I segmenti possono essere usati per mantenere codice, dati e stack per un programma oppure per le strutture dati del sistema (come LDT o TSS). Se più di un programma è in esecuzione a ognuno di questi viene assegnato un suo set di segmenti. Il processore gestisce anche i limiti di questi segmenti e assicura che un programma non possa interferire con l'esecuzione di un altro scrivendo in un altro segmento. Il meccanismo di segmentazione permette anche di tipizzare i segmenti in modo che alcune operazioni su tali segmenti, possano essere ristrette o meno. Tutti i segmenti in un sistema sono contenuti nello Linear Address Space del processore. Per localizzare un byte in un determinato segmento, un indirizzo logico deve essere fornito. (come detto prima) un indirizzo logico è formato da segmentoSelettore:offset.
Il selettore è un identificatore unico del segmento. Provvede anche un offset in una descriptor table (ad esempio GDT) che a sua volta punta a una struttura dati chiamata Segment Descriptor. Ogni segmento ha un proprio segment descriptor che specifica la grandezza del segmento, i diritti di accesso e il livello di privilegio per il segmento (Ring 0-1-2-3), il tipo di segmento e la locazione del primo byte del segmento nello Linear Address Space (chiamato Base Address). L'offset viene poi aggiunto a questo indirizzo base per localizzare un byte all'interno del segmento. Base Address + Offset = Linear Address Space nello Linear Address Space del processore. (Un Physical Address - Indirizzo Fisico viene definito come il range di indirizzi che il processore può generare sul suo bus indirizzi).


In breve: Real Mode e Protected Mode
------------------------------------
In real mode può essere eseguito un solo processo alla volta (niente multithreading quindi, ndnt) e gli indirizzi corrispondono sempre alla Reale locazione in memoria (indirizzi fisici volevo dire). Sempre dal Manuale Intel (spero di tradurre a modino ;P): Protected Mode usa i contenuti di un registro di segmento come selettori o puntatori a una tabella di descrittori. I descrittori provvedono un indirizzo a 24 bit, fino a 16Mb di memoria fisica, supporto per la gestione della memoria virtuale su un segmento basato su swapping e vari meccanismi di protezione. I meccanismi di protezione includono: Controllo dei limiti di un segmento, opzioni sui segmenti di read-only o execution-only, e fino a 4 livelli di privilegio (Anche se in pratica se ne usano solo 2, ndnt) per proteggere per proteggere il codice del sistema operativo (gli osannati Ring) da interferenze. In aggiunta, hardware task switching e LDT (Local Descriptor Table) permettono al sistema operativo di proteggere le appplicazioni degli utenti l'una dalle altre. ...Per garantire la compatibilità, si può sempre usare la Real Mode, ma solo attivando la modalità Virtual 8086.
Quando si opera in protected mode tutti gli accessi alla memoria passano attraverso la GDT o la LDT. Queste tabelle contengono delle entries chiamate Segment Descriptor. Un segment descriptor fornisce l'indirizzo di base di un segmento e diritti di accesso, tipo e informazioni di uso. Ognuno di questi segment descriptor possiede un Segment Selector associato. Il segment selector fornisce un indice nella GDT o LDT, un flag global/local (che determina appunto se il segment descriptor punta alla GDT o alla LDT) e informazioni sui diritti di accesso. Per accedere a un byte in un segmento, entrambi i segment selector e un offset devono essere forniti. Il segment selector fornisce l'accesso al segment descriptor per il segmento (nella GDT o LDT). Dal segment selector, il processore ottiene l'indirizzo base del segmento nel Linear Address Space. L'offset poi provvede la locazione del byte relativa al base address. Questo meccanismo può essere usato per accedere a qualsiasi segmento codice, dati, o stack valido nella LDT o GDT, in base all'accessibilità del segmento data dal CPL (Current Privilege Level) nel quale il processore sta operando (Il CPL è definito come livello di protezione del codice segmento in esecuzione). Il linear address della base della GDT è contenuto nel registro GDTR mentre quello della LDT nel registro LDTR (registri per i quali abbiamo istruzioni dedicate).
In protected mode gli indirizzi non corrispondono direttamente alla memoria fisica. il processore alloca e gestisce la memoria dinamicamente. Quindi se il codice e i dati di un prog occupano meno di 64K risiedono nello stesso segmento per cui basta solo l'offset localizzare una variabile o un'istruzione. In caso contrario, se ad esempio il segmento data occupa 2 o più segmenti il programma dovrà specificare sia segmento che l'offset per localizzare una determinata variabile. (Questo problema non sussiste invece in uno spazio di indirizzi "flat" della modalità protetta a 32 bit) Con l'"avvento" dei processori in modalità protetta, l'architettura segmentata ebbe un altro scopo. I segmenti potevano separare blocchi differenti di codice o dati e proteggerli da interazioni indesiderate (e inoltre supportavano il modello di memoria flat). Ehi, do per scontato che un'idea di quali siano e a cosa servono più o meno i registri dell'8086 ce l'abbiate. (per chi volesse approfondire ci sarebbe, oltre ai man intel :P il tute di albe su pmode sulla protected mode, ad esempio).

Sull'organizzazione dei segmenti
--------------------------------
Allora, nella famigghia dei processori basati su 8086 il termine segmento ha 2 significati:

- Un blocco di memoria di una discreta grandezza chiamato "segmento fisico" (ripeto, il numero di byte in un segmento di memoria fisico è 64K per processori a 16 bit, meh, non ve lo dico più)
- Un blocco di memoria di grandezza variabile chiamato "segmento logico" occupato dal codice o dai dati di un programma.

Segmenti di memoria fisica
--------------------------
Come spiegato prima, un segmento fisico può iniziare solo a una locazione di memoria il cui indirizzo sia divisibile per 16, compreso l'indirizzo 0. Intel chiama queste locazioni "Paragrafi" per cui si può riconoscere facilmente la locazione di un paragrafo dato che il suo indirizzo termina sempre con 0 (1000h o 2EA70h). (se ti stai chiedendo perchè, ti ricordo il gioino di prima, hai presente lo shift...)

Segmenti logici
---------------
I segmenti logici contengono I 3 componenti di un programma: codice, dati e stack. MASM si preoccupa di organizzare le 3 parti per noi così che occupano segmenti fisici di memoria. I registri di segmento CS, DS e SS contengono gli indirizzi dei segmenti fisici dove risiedono i segmenti logici Possiamo definire i nostri cari segmenti in 2 modi: con le "direttive di segmento semplificate" o con le "direttive estese", che possiamo usare anche insieme nello stesso programma. Dato che le hanno chiamate "semplificate", queste direttive nascondono molti dei dettagli delle definizioni dei segmenti e si occupano di generare il codice opportuno per specificare gli attributi dei segmenti e ordinarli (si, è possibile anche definire un ordine). Le direttive complete, "full", richiedono una sintassi più complicata (che sfiga eh? ;D) ma forniscono più controllo su come l'assemblatore genera i segmenti.

Prima di andare oltre: near e far
---------------------------------
Ma in questo contesto che significa parlare di dati vicini, lontani? a/da cosa? Gli indirizzi che hanno un nome di segmento implicito o registri di segmento associati con loro vengono chiamati near address. Gli indirizzi che hanno un esplicito segmento associato con loro vengono chiamati far address. L'assemblatore gestisce il codice near o far automaticamente mentre bisogna specificarli come gestire i dati near o far. Il modello di segmento microsoft mette tutti i dati near e lo stack in un gruppo chiamato DGROUP (Andreageddon non lo sapeeevaaaa, Andreageddon non lo sapeeevaaa :P). Il codice near viene messo in un segmento chiamato _TEXT. Il codice o i dati far di un modulo vengono messi in un segmento separato. L'assemblatore non è in grado di determinare gli indirizzi per alcuni componenti del programma e questi vengono detti rilocabili. L'assemblatore genera un record e il linker provvede l'indirizzo una volta che ha ottenuto l'indirizzo di tutti i segmenti.
Ad esempio, dati che si trovano nello stesso segmento (diciamo DS, default) vengono 'raggiunti' indicando solo l'offset per cui si dice che sono near, se invece dovessimo avere un programma con una vagonata di dati che occupano anche un altro segmento (diciamo ES), dovremo specificare in qualche modo anche il segmento in cui si trovano determinati dati per cui si dice che sono far.

Codice near
-----------
I trasferimenti di controllo all'interno di codice near non richiedono cambiamenti ai registri di segmento (perchè sono sempre nello stesso, sono "vicini", "in zona" :D) e il processore gestisce automaticamente i cambiamenti dell'offset nel registro IP quando il flusso del programma viene modificato da istruzioni come JMP, CALL e RET.

call nearproc ;<- cambia l'offset del codice

Questa chiamata cambia ovviamente il registro IP in modo che questo punti a un nuovo indirizzo ma lascia inalterato il segmento (ricordate segmento:offset?). Quando la procedura ritorna, il processore resetta IP all'offset della successiva istruzione alla call

Codice far
----------
In questo caso il processore gestisce i cambiamenti ai registri di segmento.

call farproc ; <-cambia segmento e offset

muove automaticamente in CS e IP il segmento e l'offset della procedura farproc ove essa risiede (capito il senso di vicino e lontano?). Quando poi la call termina e la procedura ritorna il processore setta CS al valore originale e IP punta all'istuzione successiva alla call.

Dati near
---------
Un programma accede ai dati near direttamente perchè un registro di segmento mantiene già il segmento corretto per il dato. Spesso il termine near è usato in riferimento ai dati nel gruppo DGROUP. Dopo la prima inizializzazione di DS e SS, questi registri puntano normalmente a DGROUP. Se durante l'esecuzione del programma vengono modificati, perdendo così il riferimento, questi registri vanno ripristinati al loro valore prima di poter riferirsi a qualsiasi elemento nel DGROUP (causando potenzialmente errori psichedelici :P) Il processore assume che tutti i riferimenti di memoria siano relativi al segmento nel registro DS, con l'eccezione dei riferimenti che usano BP o SP. Il processore associa questi registri con SS. (comportamento che si può comunque "bypassare" o per meglio dire si può fare "override"..però ve lo spiego un'altra volta) Il seguente esempio dimostra come il processore acceda sia ai segmenti DS che SS a seconda di dove l'operatore puntatore contenga BP o SP. La distinzione perde significato quando DS e SS sono uguali.

near WORD 0
...
mov ax,nearvar ; <- legge da  DS:[nearvar]
mov di,[bx] ; <- legge da DS:[BX]
mov [di],cx ; <- scrive in DS:[DI]
mov [bp+6], ax ; <- scrive in SS:[BP+6]
mov bx,[BP] ; <-legge da SS:[BP] 
Dati far
--------
Per leggere o modificare un dato far, un registro di segmento deve puntare al segmento che contiene il dato. Questo richiede 2 passi. Primo, caricare il segmeno (normalmente ES o DS) con il valore corretto, e poi (opzionalmente) settare una direttiva ASSUME (che vedremo dopo nei dettagli) al segmento dell'indirizzo. Un paio di esempi, che è meglio.
Un metodo comune per accedere ai dati far è inizializzare ES:
;primo metodo

mov ax, SEG farvar
mov es, ax ; <- carica in ES il segmento dell'indirizzo far
mov ax, es:farvar ; <- fornisce un esplicito segment override sull'indirizzamento
; mette in ax il dato contenuto (segmento:offset) nel segmento ES all'offset di farvar quindi

(Restrizione Intel, non si può passare il SEG direttamente a un registro di segmento)

;secondo metodo

mov ax, SEG farvar
mov es, ax
ASSUME ES:SEG farvar ; <- dice all'assemblatore che da ora ES contiene l'indirizzo del segmento farvar
mov ax,farvar


Se un'istruzione necessita di sostituire un segmento, il codice risultante sarà un po'più grande visto che il passaggio deve essere codificato nel prog, cmq il codice risultante potrebbe ancora esser più piccolo del codice generato per fare caricamenti multipli del segmento default. Se un programma usa ES per accedere ai dati far non ha bisogno di ripristinare ES quando ha finito (a meno che il prog non usi il modello FLAT). Cmq alcuni compilatori richiedono di ripristinare ES. Per accedere ai dati far, settare prima DS al segmento far e poi ripristinare il valore originale. Altro esempio:

push ds ; <- salva il segmento dati
mov ax, SEG fararray
mov ds, ax
ASSUME ds: SEG fararray
mov ax, fararray[0]
...
pop ds ; <- ripristina il segmento
ASSUME ds: @data ; <-e l'assunzione default

(beh ASSUME è solo una macro, a livello di asm diventa la stessa cosa che il primo codice, ndnt)

-------------------------------------------
Uso delle definizioni estese dei segmenti
-------------------------------------------
"Ehm...ragazzi, prendetela per bona sta parte, dovete fidarvi..." ihihiihihi E'un po'dura questa parte, ma come si fa a usare le direttive semplificate...se non si sa COSA semplificano? :> Un segmento definito inizia con la direttiva SEGMENT e termina con la direttiva ENDS (END Segment immagino :P):

Agevoliamo un esempio prima di passare alla sintassi:


STACK SEGMENT PARA STACK 'STACK'
  DB  200h DUP (?)
STACK ENDS

DATA SEGMENT WORD 'DATA'
  msg DB 'Lonely Wolf',13,10,'$'

DATA ENDS

CODE SEGMENT WORD 'CODE'
     ASSUME cs:CODE, ds:DATA

Start:
 mov ax,DATA
 mov ds,ax          ;  <- carica in DS il segmento dati. 
 mov dx,OFFSET msg ;  <- DS:DX punta così al messaggio  (perchè DS è il segmento dati, msg è un dato...per cui ;D)
 mov ah,9
 int 21h
 mov ah,4Ch
 int 21h

CODE ENDS
 END Start
(devo confessarvi che questo esempio però l'ho preso dal librozzo sul Tasm, a memoria non me lo ricordavo, e dando un'occhiata sta faccenda non la spiega così dettagliatamente come il manuale del masm. Giulia sei sicura che vuoi che ti passi questo libro? Dai passa al Masm non fare la talebana ;P lol)

name SEGMENT [align] [READONLY] [combine] [use] ['class'] dichiarazioni name ENDS

name ovviamente definisce il nome del segmento. Il linker può anche combinare segmenti con nomi identici da moduli differenti se però il valore di combine non è PRIVATE. Le seguenti opzioni per la direttiva SEGMENT danno al linker e all'assemblatore istruzioni su come settare e combinare i segmenti: (ulteriormente commentati tra poco)

align          Definisce il limite di memoria sul quale inizia un nuovo segmento
READONLY       Dice all'assemblatore di riportare un errore se qualsiasi istruzione cerca di modificare qualcosa

               nel segmento READONLY
combine        Determina come il linker dovrà combinare i segmenti da moduli diversi quando crea l'eseguibile.
use            Determina la grandezza di un segmento. USE16 indica che gli offset nei segmenti sono su 16bit,
               mentre USE32...(questo funge solo 386/486)
class          Provvede un nome di classe per il segmento. Il linker automaticamente raggruppa i segmenti
               della stessa classe in memoria

Ovviamente non si possono cambiare le proprietà di un segmento una volta definite

Allineamento dei Segmenti (align)
---------------------------------
Questo tipo opzionale nella direttiva SEGMENT definisce il range di indirizzi di memoria dal quale un indirizzo di inizio per il segmento verrà selezionato e può essere uno dei seguenti:


BYTE             L'indirizzo del prossimo byte disponibile
WORD             L'indirizzo della prossima parola disponibile
DWORD            L'indirizzo della prossima DWORD disponibile
PARA             L'indirizzo del prossimo paragrafo disponibile (16 byte per paragrafo). DEFAULT
PAGE             L'indirizzo della prossima pagina disponibile (256 byte per pagina)


Il linker usa le informazioni di allineamento per determinare l'indirizzo di partenza relativo per ogni segmento e il sistema operativo calcola l'indirizzo di partenza corrente quando il programma viene caricato.

Rendere i segmenti di sola lettura (READONLY)
---------------------------------------------
Questo attributo (come dice il nome :P) è utile quando devi creare un segmento codice in sola lettura per protected mode ad esempio e per citare alla lettera il manuale "It protects against illegal self-modifying code". L'assemblatore genererà quindi un errore quando si cerca di scrivere su uno di codesti segmenti.



Combinazione di segmenti (combine)
----------------------------------
Questo tipo opzionale determina come il linker deve comportarsi quando trova in moduli diversi segmenti aventi lo stesso nome. combine controllo il comportamento del linker non dell'assemblatore.


PRIVATE    NON combina i segmenti anche se hanno lo stesso nome. DEFAULT
PUBLIC     Concatena tutti i segmenti aventi lo stesso nome per formare un unico e contiguo segmento
STACK      Concatena tutti i segmenti aventi lo stesso nome e fa si che il sistema operativo setti SS:00 sul fondo
           e SS:SP che punta alla cima al segmento risultante. L'inizializzazione dei dati è inaffidabile come vediamo dopo
COMMON     Sovrappone i segmenti. La lunghezza dell'area risultante è la lunghezza del più grande dei segmenti combinati.
           L'inizializzazione dei dati è inaffidabile come vediamo dopo
MEMORY     Usato come sinonimo di PUBLIC (mmmahhh)
AT address Prende address come locazione del segmento. Un segmeno AT non può contenere alcun codice o dati inizializzatoma
           è utile per definire strutture o variabili che corrispondono a specificare locazioni di memoria far come uno
           screen buffer. Non si usa in protected mode.


Non mettete dati inizializzati nei segmenti STACK e COMMON. Con questi tipi di combine, il linker sovrascrive i dati inizializzati di ogni modulo all'inizio del segmento. L'ultimo modulo contenente dati inizializzati scrive sugli altri degli altri moduli. Normalmente si deve provvedere almeno un segmento stack (che abbia il tipo combine STACK) in un programma. Se nessun segmento stack viene dichiarato, LINK visualizza un messaggio di errore che puoi ignorare se hai una ragione specifica per non dichiarare un segmento stack, ad esempio perchè si potrebbe non volere un segmento stack separato in un file.com

Ordinamento dei segmenti (class)
--------------------------------
Il tipo opzionale class aiuta a controllare l'ordinamento dei segmenti. 2 segmenti con lo stesso nome non sono combinati se la loro classe è diversa. Il linker dispone i segmenti così che tutti i segmenti identificati con un dato tipo class siano consecutivi nel file exe. Comunque, all'interno di una classe il linker dispone i segmenti nell'ordine incontrato. le direttive .ALPHA (sta per alphabetic).SEQ (default. dispone i segmenti nell'ordine in cui gli diciamo) .DOSSEG (ordine dei segmenti convenzionale per MS-DOS, ovvero 1.Segmento codice 2.Segmento Dati in questo ordine: a) Segmenti che non sono nella classe BSS (che se non lo sapete, dopo vi dico cosa è, però solo se fate i bravi :P ihih) o STACK b) Segmenti della classe BSS c) Segmenti della classe STACK) determinano questo ordine nel file OBJ. Il metodo più comune è di specificare un tipo class per piazzare tutti i segmenti codice per primi nel file exe. Di solito questo ordinamento può essere ignorato. Comunque potrebbe essere importante se si volesse che un segmento appaia all'inizio o alla fine di un programma. Come appena detto, per i file com, il segmento codice deve apparire primo nel file eseguibile visto che l'esecuzione inizia all'indirizzo 100h.
Oh, ci siete sempre? :D
Ci siamo quasi, ora facciamo ASSUME, poi un po'di direttive semplificate (eheh io anche se il prof non le aveva mai spiegate usavo le direttive semplificate, nei compiti in classe era tattico: 1.le usavo solo io 2.avevo più tempo a disposizione ;D)

La direttiva ASSUME per i registri di segmento
----------------------------------------------
Molte delle istruzioni assembler ASSUMONO, prendono per bono come si dice qui :p che ci sia un certo segmento di default. Ad esempio, JMP ASSUME che il segmento associato sia con il CS, PUSH e POP ASSUMONO il registro CS e le istruzioni MOV ASSUMONO che il segmento associato sia il DS. Come è logico pensare, Quando l'assemblatore ha bisogna di referenziare un oggetto deve sapere QUALE segmento CONTIENE l'indirizzo. Chi gli da questa informazione? La direttiva ASSUME. Un po' di sintassi anche qui:


ASSUME segregister:seglocation [, segregister:seglocation]
ASSUME dataregister:qualifiedtype [, dataregister:qualifiedtype]
ASSUME register:ERROR [,register:ERROR]
ASSUME [register:] NOTHING [, register:NOTHING]
ASSUME register:FLAT [, register:FLAT]


seglocation deve essere il nome del segmento o di un gruppo associato con il segregister. Le altre istruzioni che assumono un registro di default per referenziare le label o le variabili automaticamente assume che se il segmento di default è segregister, la label o la variabile è nella seglocation. Segregister può essere CS,DS,ES,SS (FS e GS per 386/486) mentre seglocation può essere:

-Il nome del segmento definito nel nostro sorgente con la direttiva SEGMENT
-Il nome di un gruppo definito con la direttiva GROUP
-La parola chiave NOTHING, ERROR o FLAT
-Una espressione SEG
(l'operatore SEG ritorna l'indirizzo del segmento di una locazione di memoria:
mov ax,SEG farvar ; <- Load Segment Address
mov es,ax
)


Una piccola nota che ho trovato su FS: il registro FS contiene in ogni momento un selettore valido, ed è stato documentato da Pietrek in [Pietrek 95.01]. Questo selettore in FS punta a un TIB (Thread Information Block) che contiene svariate info sugli elementi inerenti thread. I contenuti di un TIB sono usati da molte Win32 syscall.

La parola chiave NOTHING cancella l'assunzione corrente, o meglio, ASSUME NOTHING cancella ogni assunzione sui registri fatta in una precedente ASSUME qualcosa.. Di solito una singola dichiarazione ASSUME definisce tutti i 4 registri di segmento all'inizio del file sorgente, anche se comunque si può usare una direttiva ASSUME in qualsiasi punto del programma. L'uso della direttiva ASSUME per cambiare una assunzione su un segmento è spesso equivalente a cambiare l'assunzione con l'operatore di segment override : che eventualmente, se questo tute vi è piaciuto possiamo vedere la prossima volta, per i masm evangelist :D heheh Comunque, un programma deve caricare esplicitamente un registro di segmento con un certo indirizzo di segmento prima di poter accedere ai dati all'interno di tale segmento (ehbbeh). ASSUME dice semplicemente all'assemblatore di assumere che tale registro è stato correttamente inizializzato.

NOTA:
Masm 6.1 assegna AUTOMATICAMENTE al registro CS l'indirizzo del segmento codice corrente del programma, così che non c'è bisogno di includere nel nostro programma ASSUME CS: _MYCODE

NOTA2:
Questa direttiva ASSUME riguarda solo le assunzioni fatte in fase di assemblaggio, non in runtime.

Definizione di gruppi di segmento
---------------------------------
Un gruppo è una collezione di segmenti che non superano i 64K in modalità 16bit. Un programma indirizza un codice o i vari elementi dei dati nel gruppo relativo all'inizio del gruppo. Un gruppo ti permette di sviluppare segmenti logici separati per tipi di dati diversi a poi combinarli in un unico segmento (gruppo) per tutti i dati. L'uso di un gruppo evita di dover continuamente ricaricare registri di segmento per accedere a diversi segmenti e di conseguenza il programma userà poche istruzioni e sarà anche un pochino più veloce.
L'esempio più comune di un gruppo è quello speciale per i dati near chiamato DGROUP. Nel modello dei segmenti Micro$oft, molti segmenti (_DATA, _BSS, CONST, STACK) sono combinati in questo DGROUP.
Siccome non lo sapevo e mi sono documentato, per la cronaca (su su, alzate la mano, quanti di voi lo sapevano? :P heheh) BSS (Block Started by Symbol deriva da un vecchio operatore asm)è quella regione di memoria che contiene le variabili globali (non inizializzate) e statiche mentre come sapete bene lo heap viene usato per l'allocazione dinamica della memoria.
Figoooo!!, guardate questo schemettino che ho trovato girottolando sulla memoria (nelle SPARC ma la zuppa è quella):


        | . . . |
        +-------+ -.			int x = 1;	/* x placed in data */
        | text  |   \			int y;	/* y in placed bss  */

        |-------|    | program's	int main(){
        | data  |    | layout in	   int z;	/* z alloc. on stack */
        |-------|    | memory		   ...
        | bss   |    |	 	   	y = 2;	/* code put in text  */
        |-------|    |		  	 ...
        | heap  |    |		  	 return 0; /* retval in reg %o0 */
        |---|---|    |			}
        | . V . |    |

        | . . . |    |			a dynamically allocated variable will
        | . ^ . |    |		  	placed in the heap
        |---|---|    |
        | stack |   /			a local variable declared with the
        +-------+ -'			  keyword "static" in C will be placed
        | . . . |
I linguaggi di alto livello M$ (qua sta scritto così) piazzano tutti i segmenti dati near in questo DGROUP. La direttiva .MODEL definisce automaticamente il DGROUP e il registro DS normalmente punta all'inizio del gruppo fornendo un accesso relativamente veloce a tutti i dati nel DGROUP.

name GROUP segment [, segment]

dove name è ovviamente la label scelta del gruppo e può riferirsi anche a un gruppo precedentemente definito. Questa funzione ci lascia aggiungere segmenti al gruppo uno alla volta. Ad esempio, se MYGROUP era stato precedentemente definito per includere ASEG e BSEG allora

MYGROUP GROUP CSEG

aggiunge semplicemente CSEG e gli altri due NON vengono rimossi. Ogni segment può essere qualsiasi nome di segmento valido con un'unica restrizione: un segmento non può appartenere a più di un gruppo. Questa direttiva non riguarda l'ordine con il quale i segmenti di un gruppo vengono caricati.

--------------------------------
Uso delle direttive semplificate
--------------------------------
Che sudata eh? Ci siete fin qui? Come ci ispira il nome, con queste direttive ci dovremmo risparmiare l'ingrato compito di definire nei dettagli segmenti, ordinamenti e amenità simili...vediamo, vediamo. Le direttive semplificate sono

.MODEL
.cODE
   
.CONST
.DATA
.DATA?
.FARDATA
.FARDATA?
.CODE
.STACK
.STARTUP
.EXIT


L'avevo dato per scontato, cmq per la cronaca, un programma Masm consiste in moduli fatti di segmenti. Ogni programma scritto solo in Masm ha un modulo principale (main) dove inizia l'esecuzione del programma. Questo main module può contenere codice, dati, o segmenti stack definiti con queste benedette direttive semplificate. Qualsiasi modulo aggiuntivo dovrà contenere solo il segmento codice e data. Comunque, attenzione, ogni modulo che usa queste direttive semplificate dovrà sempre cominciare con la direttiva .MODEL. Vediamo un esempio che mostra la struttura di un modulo principale che usa queste direttive. Usa il processore default (8086) e la distanza di stack default (NEARSTACK). Come si diceva prima, eventuali moduli linkati useranno solo le direttive .MODEL .CODE .DATA e .END per terminare, ovviamente. (lo so, avrò detto ovviamente 10000 volte, è colpa di Ntoskrnl, a forza di andare a giro con lui l'ho appreso per osmosi :D rotfl)


.MODEL small, c

.STACK ;  <- Usa per default 1Kb di stack, altrimenti si specifica quanto ci serve es. .STACK 2048 ovvero 2Kb

.DATA  ;  <- Inizio data segment

 <- ;inserire qui le varie dichiarazioni, variabili ecc..

.CODE  ;  <- Inizio del segmento codice
.STARTUP  ;  <- Genera codice di avvio (visto ganzo lo fa lui?)

; <- le istruzioni del vostro programma 

.EXIT  ;  <- Genera codice di uscita
.END  ;  <- Deve stare alla fine di ogni modulo 


Le direttive .DATA e .CODE non hanno bisogno di una dichiarazione separata per indicare il loro termine

Definizione degli attributi base di .MODEL
------------------------------------------
questa direttiva è importantissima poichè riguarda l'intero modulo: il modello della memoria, default calling e naming conventions, sistema operativo e tipo di stack (per ulteriori info guarda sotto "Calling e naming convention: queste sconosciute").
Quindi, in cima in cima al nostro programma ci sarà


.MODEL memorymodel [, modeloptions]
memorymodel è obbligatorio e gli eventuali modeloptions devono essere separati da virgola (o eventualmente passati da riga di comando con ml). Vediamo nei dettagli le varie opzioni

Memory model     TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE, FLAT.
                 Determina la grandezza del codice e dei data pointers.

Language         C, BASIC, FORTRAN, PASCAL, SYSCALL, STDCALL.
                 Stabilisce le calling e naming conventions per le procedure e i simboli pubblici (approfondiamo dopo se
                 sopravvivo a questo tute :P)

Stack distance   NEARSTACK, FARSTACK.
                 Specificando NEARSTACK il segmento stack viene raggruppato in un singolo segmento fisico (DGROUP)
                 con i dati e si assume che SS sia uguale a DS. Specificando FARSTACK il segmento non viene
                 raggruppato in DGROUP ed evidentemente SS non sarà uguale a DS.


Si possono usare delle 'combo' come le chiamo io certe cose :D ovvero


.MODEL small

.MODEL large, c, farstack

Definizione dei modelli di memoria
----------------------------------
Ma guarda un po'che gatta da pelare che mi sono procurato ;)
Vai col tabellozzo, prego la regia...
(sul manuale del masm è + dettagliata sta tabella, io ho potato alcuni campi)

Mem.Model        Default Code     Default Data
==============================================
Tiny              Near             Near
Small             Near		   Near
Medium            Far              Near
Compact           Near             Far
Large             Far              Far
Huge              Far              Far

Flat              Near             Near

==============================================
E' cosa
buona e giusta scegliere il modello di memoria più piccolo che contiene i nostri dati e codice.

In breve

Small, Medium, Compact, Large ed Huge
Il modello di memoria Small
  supporta un segmento dati e un segmento codice. Il modello Large supporta segmenti codice e dati multipli, mentre Medium supporta segmenti codice multipli ma segmento dati singolo e Compact segmento dati multiplo e segmento codice singolo. E cmq sia, possiamo fare l'override del default, ad esempio possiamo mettere elementi "larghi/lontani"(far) nel modello Small

Tiny
Funziona solo sotto MS-DOS, piazza il segmento codice e dati in un singolo segmento e cmq la grandezza totale del programma non può occupare più di 64Kb. Di default codice e dati statici sono near e non si può fare l'override di questa cosa anche se è possibile allocare dati far dinamicamente al run time usando servizi MS-DOS per allocazione. Tiny produce file .COM e invia al linker l'argomento /TINY

Flat
Dal Manuale Intel: Usando questo modello, un programma vede la memoria come un singolo e continuo spazio di indirizzi chiamato linear address space. Codice, dati e stack sono tutti contenuti in questo spazio. Il linear address space (ragazzi, IMO ci sono termini che devono restare in inglese, se no si storpia troppo) è indirizzabile al byte (da 0 a 232-1 ovvero FFFFFFFFh). Un indirizzo per qualsiasi byte in questo spazio di indirizzi si chiama Linear Address. Questo modello di memoria NON segmentato è disponibile su sistemi operativi a 32bit, diciamo, simile al modello Tiny per il fatto che tutto il codice e i dati stanno in un unico segmento di 32 bit. Questo spazio di indirizzi (fisico) non segmentato può essere mappato come memoria read-only, read-write e memory mapped I/O. Sempre da Manuale Intel: L'architettura IA-32 supporta anche un estensione dello spazio di indirizzi fisico a 236 (64 Gb) con un indirizzo fisico massimo pari a FFFFFFFFF...(Per ulteriori informazioni sapete dove cercare ;P)
(sarebbe il PAE a cui ti riferisci - lo so nt, Physical Address Extension ma per oggi non ne parliamo che se no non ne esco più -, per completezza ti ho trovato un piccolo riferimento all'argomento: http://www.microsoft.com/whdc/hwdev/platform/server/pae/default.mspx, ndnt) Per usare il modello Flat prima di usare la direttiva .MODEL bisogna esplicitare .386 o .486 (indovinate un po', indica la modalità del processore che intendiamo usare nel nostro programma, è il set di direttive per selezionare il processore e il coprocessore). Il sistema operativo automaticamente inizializza i registri di segmento al momento del caricamento. CS, DS, ES ed SS occupano tutti il supergruppo FLAT. Tutti i registri di segmento contengono descrittori che mappano l'intero spazio di indirizzi logico che le nostre applicazioni vedono. NOTA: Intel specifica che si possono implementare 2 tipi diversi di modello flat: Base, Protetto e MultiSegmento...

ATTENZIONE!
Guardate un po' cosa
ho trovato riguardo il modello flat...
"...The first one is the "flat model" dogma: "Win32 uses the flat model, and this This is simply not true. model precludes the use of segmentation..."
L'autore di questa Win32Faq, ho cercato la sua firma nel txt ma non so chi sia, dice che non è affatto vero che nel modello FLAT non si possano usare i registri di segmento, e quasi sarcasticamente dice che There is no such thing in the Intel CPU as a "flat model bit", è soltanto una convenzione. L'uso di questo modello non eredita alcuna limitazione-CPU per un programatore che occasionalmente volesse usare i registri di segmento. Strano, in un file hlp del masm 8 leggo:

"...Differing from earlier 16 bit code that used combined segment and offset addressing with a 64k segment limit, FLAT memory model works only in offsets and has a range of 4 gigabytes. This makes assembler easier to write and the code is generally a lot faster. All segment registers are automatically set to the same value with this memory model and this means that segment / offset addressing must NOT be used in 32 bit programs that run in 32 bit Windows.

???
Prosegue " the best evidence is that Microsoft themselves use segmentation in the Win32 world: in any Win32 thread, the FS register always contains a special descriptor, that doesn't follow the flat model rules, and is used to access the TID (Thread Information Block, see [Pietrek 95.01]). The lack of access to segmentation from Ring 3 code only comes from an OS design decision...The second explanation Microsoft commonly gives about the lack of access to segmentation from Ring 3 code is the need for portability, and the lack of hardware mechanisms to implement segmentation on non-Intel platforms. As we already mentioned, this looks to us as a very moot point, as " e fa un elenco di motivazioni per confutare l'affermazione di M$ e le sue proposte per implementare LDT a quanto ho capito. Boh, non so se tutto questo sia roba vecchia, cmq in caso vi giro questo file.

Specificare un processore e un coprocessore
-------------------------------------------
Masm supporta un set di direttive per selezionare processori e coprocessori. Naturalmente, una volta che si sceglie un processore dobbiamo usare solo il set di istruzioni per quel processore, è evidente. Il default è 8086, per cui se va bene così non importa esplicitarlo in alcun modo. La selezione avviene specificando in cima al codice del programma una delle direttive .186 .286 .386 .486 .586 Le direttive .286P .386P .486P abilitano quelle istruzioni disponibili solo a un livello più alto di priviliegio in aggiunta al normale set di istruzioni per il processore dato. Per selezionare un coprocessore matematico usa le direttive .8087 (default) .287 .387 e .NO87 (quest'ultima disattiva ogni istruzione legata al coprocessore. Attenzione: dal processore 486 registri e istruzioni del coprocessore sono built-in,per cui in tal caso non serve specificare.

Scegliere le convenzioni del linguaggio
----------------------------------------
Questa facoltà facilita la compatibilità con i linguaggi di alto livello (qualora dovesse servire) determinando la codifica interna per i nomi dei simboli pubblici ed esterni, il codice generato dalla procedura di inizializzazione e il cleanup, l'ordine con il quale gli argomenti devono essere passati a una INVOKE (non a caso nel primo tutorial della saga anche iczelion accenna a questa cosina). La programmer's guide del Masm ci dice che PASCAL, BASIC e FORTRAN sono uguali. C e SYSCALL hanno la stessa calling convention ma differenti naming convention. Boh, vediamo :)

Calling e naming Convention...queste sconosciute
-------------------------------------------------
Le calling convention, come dice il nome, definiscono come un linguaggio implementi una chiamata a una funzione e come la funzione debba ritornare al chiamante. Le naming convention specificano come e se il compilatore o l'assemblatore alterano il nome di un identificatore o il modo in cui questo debba essere salvato prima di piazzarlo in file object.
Quando viene chiamata una funzione, gli argomenti gli sono tipicamente passati ed eventualmente viene ritornato un valore. Quindi una calling convention stabilisce COME gli argomenti sono passati e i valori ritornati alle funzioni, specifica anche come i nomi delle funzioni vengono "decorati".

Inoltre osservando del codice (con un disassemblatore o un debugger) e tenendo d'occhio alcuni dettagli possiamo riconoscere quale convenzione è stata usata e riconoscere i codici di prologo relativi ad esempio.
Diventa molto importante quando ad esempio vogliamo linkare dei moduli C/C++ con del codice asm (pensateci un attimo, se un modulo dovesse chiamare una funzione di un altro modulo linkato in questo modo, e fossero stati prodotti da compilatori diversi con modi diversi di passarsi parametri, è evidente che ci sarebbero dei problemucci) (sì, guarda che i problemi nascono anche per molto meno metti che una dll scritta con vc++ abbia funzioni cdecl e io le chiamo dal mio prog vc++ come stdcall, non c'è bisogno di andare a cercare l'asm, ndnt). Indipentemente dalla calling convention scelta, succedono le seguenti cose:

1. Tutti gli argomenti sono estesi a 4 bytes (su win32) e messi nelle appropriate locazioni di memoria.
    Di solito queste locazioni sono lo stack ma potrebbero essere anche registri, dipende dalla convention scelta.
      
2. L'esecuzione del programma salta all'indirizzo della funzione chiamata.

3. All'interno della funzione i registri ESI, EDI, EBX, and EBP vengono salvati sullo stack (non ci avete
    mai fatto caso, quando ad esempio steppate con softice ed entrate in una call di qualche
    programma, all'inizio c'è tutta la serie di push e seghe varie per preparare lo stackframe?). La parte di codice che
    si preoccupa di fare questo viene chiamato "prologo e di solito viene generato dal
    compilatore.  (a meno che non spefichi in vc++ che la funzione sia naked per esempio, ndnt)  

4. Viene eseguito il codice opportuno della funzione e il valore di ritorno viene messo nel registro EAX.

5. I registri ESI, EDI, EBX, and EBP sono ripristinati. Il pezzo di codice che fa questo viene 
    chiamato "epilogo" ecome il prologo,nella maggioranza dei casi viene generato dal compilatore.

6. Gli argomenti vengono rimossi dallo stack. Questa operazione viene chiamata "stack cleanup"
    e potrebbe essere performato sia all'interno della funzione chiamata (callee) che dal chiamante (caller)
    a seconda della calling convention usata.
Uso di codice automatico per Prologo ed epilogo
-----------------------------------------------
L'assemblatore genera automaticamente il codice di prologo quando incontra la prima istruzione o label dopo la direttiva PROC (che deve essere dichiarata prima di una INVOKE) e genera il codice di epilogo quando trova ret o iret. Usando questo codice generato si risparmia tempo e diminuisce il numero di righe di codice che devono essere scritte. La generazione del codice di prologo o di epilogo dipende:

- Variabili locali definite
- Argomenti passati alla procedura
- Processore corrente selezionato (solo epilogo)
- La calling convention corrente
- Le opzioni passate in prologuearg della direttiva PROC - I registri che devono essere salvati

Il codice standard per il prologo e l'epilogo, come vedremo, gestisce i parametri e lo spazio per le variabili locali. Se una procedura non dovesse avere alcun parametro e variabile locale verrebbero omessi, a meno che FORCEFRAME non venga specificato in prologuearg. Il codice di prologo consiste in 3 passi:

- Puntare BP alla cima dello stack (preparazione dello stackframe)
- Settare lo spazio sullo stack per le variabili locali
- Salvare i registri che le procedure/funzioni devono preservare

Il codice di epilogo cancella questi 3 passi in ordine inverso.

Codice di prologo ed epilogo user-defined
---------------------------------------------
Ganzo, ragazzi sentite che roba, lo sapevate? (non su rieducational channel :D) bada te quante cose sto scoprendo a questo giro...
Se per qualche motivo (ho quasi paura a indagare :P) volessimo usare un set di istruzioni diverso per il codice di prologo e di epilogo nelle procedure, è possibile scrivere macros che vengono eseguite al posto del prologo/epilogo standard. Queste macros user-defined rispondono correttamente se è stato specificato FORCEFRAME.
Per scrivere un proprio codice di prologo/epilogo la direttiva OPTION deve apparire nel programma. Essa disabilita automaticamente la generazione del codice prologo/epilogo. Quando si specifica
OPTION PROLOGUE:nomemacro
OPTION EPILOGUE:nomemacro

l'assemblatore chiama la macro specificata e si aspetta che sia nella forma:


nomemacro MACRO nomeproc, \
                    flag, \
               parmbytes, \
              localbytes, \
               -reglist-, \
              userparams


Per ulteriori dettagli Masm Programmer's Guide pag.204. Si trova anche in pdf da scaricare...anzi, se proprio non avete niente di meglio da fare è una lettura che vi consiglio ;)

Dicevamo, ah si, Le principali calling convention sono: __cdecl, __stdcall, __fastcall, thiscall (Ci sarebbero anche altre convenzioni più vecchie come PASCAL (mammon_ ci spiega che veniva usata in Win16 e pusha i parametri da sx verso dx e lo stack veniva ripulito dal callee, al contrario della convenzione C), BASIC, FORTRAN (che per l'assemblatore sono sinonimi ma mette i nomi in uppercase) e SYSCALL - che lascia i nomi inalterati)

Leggi articolo originale
Vediamo una tabella riassuntiva presa dal sito M$ (si riferisce a Visual C++ ma per i nostri scopi dovrebbe essere più che sufficiente):

Keyword Stack cleanup Passaggio dei Parametri
__cdecl Caller Pusha i parametri sullo stack in ordine inverso (da dx a sx)
__stdcall Callee Pusha i parametri sullo stack in ordine inverso (da dx a sx)
__fastcall Callee Parametri passati nei registri, poi pushati sullo stack
thiscall Callee Parametri pushati sullo stack; il puntatore this salvato in ECX
 

__cdecl

Oltre a quanto riportato in tabella, __cdecl è la convenzione default per i programmi scritti in C/C++. Se un nostro programma usa un'altra convenzione ma per qualche motivo una nostra funzione deve usare per forza __cdecl possiamo usare la seguente sintassi (questo discorso vale anche per le altre..mah veramente dipende soprattutto dalle impostazioni del compilatore, ndnt) :

int __cdecl sumExample (int a, int b);


...
int __cdecl sumExample (int a, int b)
{
  return a+b;
}
...

int c = sumExample (2, 3);


Vediamo una chiamata con questa convenzione:

; pusha gli argomenti da dx a sx
push        3 
push        2

;  <- chiama la funzione definita sopra
call        _sumExample ; <-  il nome della funzione viene decorata con il prefisso underscore _ 

; <- cleanup the stack aggiungendo la grandezza degli argomenti al registro ESP 
add         esp,8

;  <- copia il valore di ritorno da EAX in una variabile locale (int c) 
mov         dword ptr [c],eax

Questa invece è la funziona chiamata:

;  <- prologo
  push        ebp
  mov         ebp,esp
  sub         esp,0C0h ; <- Qui completa la preparazione dello stackframe (chi? il compilatore!)
                        AndreaGeddon> se lo fa in vc in debug mode

                        contemplando i param passati e spazio per var locali
  push        ebx
  push        esi
  push        edi
  lea         edi,[ebp-0C0h]
  mov         ecx,30h
  mov         eax,0CCCCCCCCh ;  <- Ancora 'robaccia' messa dal compilatore per assicurarsi
                               che la funzione ritorni correttamente
  rep stos    dword ptr [edi]

;  <-	return a + b;
  mov         eax,dword ptr [a]
  add         eax,dword ptr [b]

; <- epilogo
  pop         edi
  pop         esi
  pop         ebx 
  mov         esp,ebp
  pop         ebp
  ret
(la cdecl è usata al 100% se il numero di argomenti passato è imprecisato, questo è importante, ndnt)

__stdcall

Questa è la convenzione usata di solito per chiamare funzioni Win32 API. Infatti WINAPI non è altro che

#define WINAPI __stdcall

Prendiamo sempre in considerazione la funzione di esempio di prima ma con lo specificatore __stdcall ovviamente. Con questa convenzione i nomi delle funzioni sono decorate mettendo davanti il solito underscore e appendendo '@' e il numero di byte che rappresentano lo spazio richiesto sullo stack (disassemblando un programma o cmq dandoci un'occhiata dentro con qualche tool non avete mai visto niente del genere? Io si, con PE Explorer ad esempio e infatti mi chiedevo cosa fosse quella roba...mumble mumble).


; <- al solito pusha gli argumenti da dx a sx
  push        3
  push        2

  call        _sumExample@8 <- Per la cronaca, lo spazio richiesto è 8 perchè i
                               parametri pushati sullo stack sono 2 DWORD, 4byte+4byte

  mov         dword ptr [c],eax


E questo è invece il codice della funzione relativa:


; <- prologo (identico alla __cdecl)

; <-	return a + b; 
  mov         eax,dword ptr [a]
  add         eax,dword ptr [b]

; <-epilogo (lo stesso codice di __cdecl)
...
; <- cleanup the stack e ritorno
  ret         8


Poichè lo stack è ripulito dalla funzione chiamata, __stdcall crea eseguibili più piccoli della __cdecl nella quale il codice di cleanup deve essere generato per ogni chiamata di funzione. D'altro canto, funzioni con numero variabili di argomenti devono usare __cdecl visto che solo il caller conosce il numero di argomenti passati a ogni call perciò solo il caller può fare il cleanup, ripeto, solo lui sa di QUANTO ripulirlo, per dirlo alla io rozzo :P

__fastcall

Questa convenzione implica che gli argomenti devono essere piazzati nei registri invece che sullo stack laddove possibile. Poichè si lavora sui registri è implicito che si vada più veloce (fast) :)
Prendiamo la solita funzione con lo specificatore __fastcall.

1. I primi 2 argomenti della funzione che richiedono 32 bit o meno vengono piazzati nei registri ECX ed EDX.
    I restanti sono pushati sullo stack da dx a sx.

2. Gli argomenti vengono poppati dallo stack dalla funzione chiamata

3. Il nome della funzione viene decorato anteponendo @ e appendendo un'altra @ e il numero di bytes
   (decimale) di spazio richiesto per gli argomenti.


Ah, questa non la sapevo, leggo che M$ si riserva il diritto di cambiare i registri per il passaggio degli argomenti (NO COMMENT... boh ;P)

; <- piazza gli argomenti in EDX e ECX
  mov         edx,3
  mov         ecx,2

  call        @fastcallSum@8

  mov         dword ptr [c],eax


E ora il codice della funzione:

; <-  prologo
  push        ebp
  mov         ebp,esp
  sub         esp,0D8h
  push        ebx
  push        esi
  push        edi
  push        ecx
  lea         edi,[ebp-0D8h]
  mov         ecx,36h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]
  pop         ecx
  mov         dword ptr [ebp-14h],edx
  mov         dword ptr [ebp-8],ecx

; <-  return a + b;

  mov         eax,dword ptr [a]
  add         eax,dword ptr [b]
;<-  epilogo 
  pop         edi
  pop         esi
  pop         ebx
  mov         esp,ebp
  pop         ebp  

  ret




thiscall

Thiscall è la calling convention default per le funzioni membro di classe C++ (eccetto per quelle con numero variabile di argomenti (per le quali si ritorna alla cdecl, ndnt)).

1. Gli argomenti sono passati da dx a sx e messi nello stack. this è piazzato in ECX.


2. Lo Stack cleanup viene fatto dalla funzione chiamata.

Vediamo un esempio:



struct CSum
{
	int sum ( int a, int b) {return a+b;}
};


Il codice asm relativo è:

  push        3
  push        2
  lea         ecx,[sumObj] 

  call        ?sum@CSum@@QAEHHH@Z	; CSum::sum
  mov         dword ptr [s4],eax


Mentre il codice della call sarà:


push        ebp
  mov         ebp,esp
  sub         esp,0CCh
  push        ebx
  push        esi
  push        edi
  push        ecx
  lea         edi,[ebp-0CCh]
  mov         ecx,33h
  mov         eax,0CCCCCCCCh
  rep stos    dword ptr [edi]

  pop         ecx
  mov         dword ptr [ebp-8],ecx
  mov         eax,dword ptr [a]
  add         eax,dword ptr [b]
  pop         edi
  pop         esi
  pop         ebx  

  mov         esp,ebp
  pop         ebp
  ret         8
Uhhh che carini questi disegnini della M$, perdonatemi non voglio essere prolisso però credo non faccia male aggiungere anche questo esempio


void    calltype MyFunc( char c, short s, int i, double f );
.
.
void    MyFunc( char c, short s, int i, double f )
    {

    .
    .
    }
.
.
MyFunc ('x', 12, 8192, 2.7183);


Vi posterei il link della M$, ma quelli ogni tanto potano le pagine...boh cmq http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_core_results_of_calling_example.asp Ma siii, dai di ascii art ;)

Allora, per questo esempio con la convenzione __cdecl il nome della funzione diventa _MyFunc e


-stack-     -locazione-
2.7183      ESP+0x14
            ESP+0x10 <- Non è che è vuoto, ma essendo un double occupa 2 dword per la sua rappresentazione interna
8192        ESP+0x0C
12          ESP+0x08
x           ESP+0x04 <- ma ESP+4 non dovrebbe essere Address of Caller? M$ dogma
ret.addr.   ESP               
registri

non usato   ECX
non usato   EDX
__stdcall and thiscall, il nome della funzione decorato diventa _MyFunc@20

-stack-     -locazione-
2.7183      ESP+0x14
            ESP+0x10
8192        ESP+0x0C
12          ESP+0x08
x           ESP+0x04
ret.addr.   ESP

registri

This(solo thiscall) ECX
non usato           EDX

__fastcall il nome della funzione decorata diventa @MyFunc@20


-stack-     -locazione-
2.7183      ESP+0x0C

            ESP+0x08
8192        ESP+0x04
ret.addr.   ESP

registri


x 	    ECX
12 	    EDX
Carino questo, guardate quest'altro pezzo di codice che ho trovato (in un angolo remoto del mio hd):

This C code with its interspersed assembly code demonstrates the various calling conventions.


// The strings passed to each function.
static char * g_szStdCall   = "__stdcall"   ;
static char * g_szCdeclCall = "__cdecl"     ;
static char * g_szFastCall  = "__fastcall"  ;
static char * g_szNakedCall = "__naked"     ;

// The extern "C" turns off all C++ name decoration.

extern "C"
{
// The __cdecl function.
void CDeclFunction ( char *        szString ,
                     unsigned long ulLong   ,
                     char          chChar    ) ;
// The __stdcall function.
void __stdcall StdCallFunction ( char *        szString ,
                                 unsigned long ulLong   ,
                                 char          chChar    ) ;
// The __fastcall function.
void __fastcall FastCallFunction ( char *        szString ,
                                   unsigned long ulLong   ,
                                   char          chChar    ) ;
// The naked function.  The declspec goes on the definition, not the
//  declaration.
int NakedCallFunction ( char *        szString  ,
                        unsigned long ulLong    ,
                        char          chChar     ) ;
}
void main ( void )

{
 00401000 55                   push        ebp
 00401001 8B EC                mov         ebp,esp
 00401003 53                   push        ebx
 00401004 56                   push        esi
 00401005 57                   push        edi
    // Call each function to generate the code.
    CDeclFunction ( g_szCdeclCall , 1 , 'a' ) ;
 00401008 6A 61                push        61h
 0040100A 6A 01                push        1

 0040100C A1 14 30 40 00       mov         eax,[00403014]
 00401011 50                   push        eax
 00401012 E8 45 00 00 00       call        0040105C
 00401017 83 C4 0C             add         esp,0Ch

    StdCallFunction ( g_szStdCall , 2 , 'b' ) ;
 0040101C 6A 62                push        62h
 0040101E 6A 02                push        2
 00401020 8B 0D 10 30 40 00    mov         ecx,dword ptr ds:[00403010h]
 00401026 51                   push        ecx
 00401027 E8 3D 00 00 00       call        00401069
    FastCallFunction ( g_szFastCall , 3 , 'c' ) ;
 0040102E 6A 63                push        63h
 00401030 BA 03 00 00 00       mov         edx,3
 00401035 8B 0D 18 30 40 00    mov         ecx,dword ptr ds:[00403018h]
 0040103B E8 38 00 00 00       call        00401078

    NakedCallFunction ( g_szNakedCall , 4 , 'd' ) ;
 00401042 6A 64                push        64h
 00401044 6A 04                push        4

 00401046 8B 15 1C 30 40 00    mov         edx,dword ptr ds:[0040301Ch]

 0040104C 52                   push        edx
 0040104D E8 40 00 00 00       call        00401092
 00401052 83 C4 0C             add         esp,0Ch
}
 00401057 5F                   pop         edi

 00401058 5E                   pop         esi
 00401059 5B                   pop         ebx
 0040105A 5D                   pop         ebp
 0040105B C3                   ret
void CDeclFunction ( char *        szString ,
                     unsigned long ulLong   ,
                     char          chChar    )
{
 0040105C 55                   push        ebp
 0040105D 8B EC                mov         ebp,esp
 0040105F 53                   push        ebx
 00401060 56                   push        esi
 00401061 57                   push        edi
    __asm NOP __asm NOP // NOPs stand for the function body here
 00401062 90                   nop

 00401063 90                   nop
}
 00401064 5F                   pop         edi
 00401065 5E                   pop         esi
 00401066 5B                   pop         ebx
 00401067 5D                   pop         ebp
 00401068 C3                   ret
void __stdcall StdCallFunction ( char *        szString ,
                                 unsigned long ulLong   ,
                                 char          chChar    )
{
 00401069 55                   push        ebp

 0040106A 8B EC                mov         ebp,esp
 0040106C 53                   push        ebx
 0040106D 56                   push        esi
 0040106E 57                   push        edi
    __asm NOP __asm NOP
 0040106F 90                   nop
 00401070 90                   nop

} 00401071 5F                   pop         edi
 00401072 5E                   pop         esi
 00401073 5B                   pop         ebx

 00401074 5D                   pop         ebp
 00401075 C2 0C 00             ret         0Ch
void __fastcall FastCallFunction ( char *        szString ,
                                   unsigned long ulLong   ,

                                   char          chChar    )
{
 00401078 55                   push        ebp
 00401079 8B EC                mov         ebp,esp
 0040107B 83 EC 08             sub         esp,8
 0040107E 53                   push        ebx
 0040107F 56                   push        esi
 00401080 57                   push        edi
 00401081 89 55 F8             mov         dword ptr [ebp-8],edx
 00401084 89 4D FC             mov         dword ptr [ebp-4],ecx
    __asm NOP __asm NOP
 00401087 90                   nop
 00401088 90                   nop
}
 00401089 5F                   pop         edi
 0040108A 5E                   pop         esi
 0040108B 5B                   pop         ebx
 0040108C 8B E5                mov         esp,ebp
 0040108E 5D                   pop         ebp
 0040108F C2 04 00             ret         4
 78:
__declspec(naked) int NakedCallFunction ( char *        szString  ,
                                          unsigned long ulLong    ,
                                          char          chChar     )

{
    __asm NOP __asm NOP
 00401092 90                   nop
 00401093 90                   nop
    // Naked functions must EXPLICITLY do a return.
    __asm RET
 00401094 C3                   ret
__declspec ... facciamoci un'idea di cosa si tratti, visto che è stata rammentata (..INPUT, INPUT, INPUT...:D). Non è che sia una calling convention, cmq MSDN mi dice che "L'edizione a 32 bit di VC++ usa uses __declspec(dllimport) and __declspec(dllexport) per sostituire __export nella precedente versione a 16 bit. (...se devi esportare una funzione dice che è da esportare e la mette nella Export table, se la importa la mette nella IT, è fondamentale eccome, ndnt). Quindi si usa anche per importare variabili usati in una dll o funzioni (ma anche per l'export). Il formato PE è progettato per minimizzare il numero di pagine che devono essere "toccate" per risolvere imports. Per fare questo sappiamo che tutti gli import addresses per qualsiasi programma stanno nella IAT (Import Address Table). Una nota su Thunk. Da un thread apparso su RCE intitolato guardacaso: What is a thunk?

...Wow!!! Now all the loader has to do, it simply put the value in memory from your computer or my computer in one single
place,instead of the 60 places. So, the exe on my computer would look like:

---Begin Exe----
0001:00000001 XXX
0001:00000003 XXX
0001:00000005 XXX
0001:00000007 jmp 0001:00000090
0001:00000009 XXX

0001:00000011 XXX
0001:00000013 jmp 0001:00000090
0001:00000015 XXX
0001:00000017 XXX
0001:00000019 XXX
0001:00000021 jmp 0001:00000090
:

:
0001:00000090 Call 700999F (in memory, which is MessageBoxA)
----End Exe-----

Simple to understand, is it not? Here, all jumps are called, well JUMPS. But the call at 0001:00000090?
Its called as THUNKS. Get it? Simple is it not?
 
(Per ulteriori approfondimenti vi rimando al...MESSAGGIO PROMOZIONALE...Tutorial di Ntoskrnl sui PE


Settare la distanza dello Stack
-------------------------------
Come abbiamo detto poco fa, la parola chiave NEARSTACK (conveniente per la maggioranza dei programmi) piazza il segmento Stack nel DGROUP con il segmento data. La direttiva .STARTUP poi genera il codice per aggiustare SS:SP così che SS abbia lo stesso indirizzo del DS. Se omettiamo .STARTUP dovremo farla a manina questa cosa. In questo caso si potrà usare quindi DS per accedere agli elementi nello stack (inclusi di conseguenza parametri e variabili locali) a SS per accedere ai dati near. FARSTACK in pratica fa creare un segmento stack a se' stante . Barbanera consiglia...ehm, Masm consiglia di usare FARSTACK per programmi residenti in memoria e DLL. Il simbolo predefinito @stack ci dice se la locazione dello stack è il DGROUP (near) o STACK.

Creazione di uno Stack
---------------------
Cmq, lo stack (pila - LIFO dai su che lo sapete) è un'area di memoria usata per il pushing e popping dei registri e per memorizzare l'indirizzo di ritorno quando viene chiamata una subroutine, per le variabili locali e il passaggio dei parametri. Come detto sopra, se uno volesse uno stack più grande del default di 1Kb, lo specifica direttamente con .STACK size (Es .STACK 2048). Se volete approfondire, abbiamo ad esempio itassembly.cjb.net o bigspider.cjb.net, abbiamo sempre L'architettura x86, struttura generale del solito Andreageddon, oppure vi consiglio di leggervi un vecchio(?) articolo apparso su Assembly Programming Journal Vol 1 n.4 - Stack Frames and High-Level Calls niente meno che di mammon_ Ho quasi finito, pazienza, su non piangete :)

Creazione di un segmento Dati
------------------------------
I programmi possono contenere sia dati far che near. E'buona norma che i dati usati più di frequente si trovino nell'area near, dove l'accesso è anche più veloce. Quest'area può diventare affollata, cmq, perchè in un sistema operativo a 16bit l'ammontare totale di tutti i dati near in tutti i moduli non può eccedere 64Kb

Le direttive .DATA .DATA? .FARDATA .FARDATA? creano segmenti dati e si può accedere ai vari segmenti all'interno del DGROUP senza dover ricaricare i registri di segmento e queste direttive possono impedire che le istruzioni appaiano nel segmento dati assumendo CS come ERROR (poi si spiega anche questo e cmq con le direttive estese si usa ASSUME CS:ERROR).

Segmenti dati near
------------------
La direttiva .DATA crea un segmento dati near (max 64Kb). Quando si usa .MODEL l'assemblatore definisce automaticamente sto benedetto DGROUP per i nostri segmenti dati che sono normalmente acceduti attraverso DS o SS. (la direttiva .CONST comunque viene usata per definire costanti come stringhe e numeri floating point che devono essere immagazzinati in memoria).

Segmenti dati far
-----------------
I modelli di memoria large e huge usano per default dati far. Quando si usa .FARDATA o .FARDATA? con i modelli di memoria small e medium, l'assemblatore crea il segmento dati far FAR_DATA e FAR_BSS e possiamo accedere a tali variabili come

mov ax, SEG farvar
mov ds, ax


Creazione di Segmenti codice
----------------------------
si potrebbe avere un programma con moduli chiamati da altri moduli e avere entrambi segmenti codice near e far.

Segmento codice near
--------------------
Spesso il modello di memoria small è la miglior scelta per i programmi che non sono linkati a moduli scritti in altri linguaggi, specialmente se non hai davvero bisogno di avere più di 64Kb per il codice. Usando la direttiva .CODE diciamo all'assemblatore di iniziare un segmento codice per ospitare le nostre istruzioni.

Segmento codice far
-------------------
Quando hai bisogno di più di 64 Kb per il codice si deve usare uno tra i modelli di memoria medium, large o huge per creare segmenti far. Nel modello di memoria larger l'assemblatore crea segmenti codice differenti per ogni modulo. Se usi invece segmenti codice multipli in un modello di memoria small, compact o tiny il linker ne fa uno solo. Per ogni segmento codice far l'assemblatore chiama ogni segmento MODNAME_TEXT dove MODNAME è il nome del modulo, mentre con un segmento codice near lo chiama _TEXT.

Vediamo un esempio di codice far:



.CODE FIRST
...
...

.CODE SECOND
...
...
(in questo caso verrebbero creati i segmenti FIRST_TEXT e SECOND_TEXT). Ove il processore esegua una call far o un jump, carica CS con il nuovo indirizzo di segmento. Nota: L'assemblatore assume sempre che CS contenga l'indirizzo del segmento codice corrente o del gruppo.

Inizio e Fine del codice con le direttive .STARTUP e .EXIT
----------------------------------------------------------
Il modo più facile per iniziare e finire un programma MS-DOS è usando queste direttive nel modulo principale che generano il codice appropriato alla distanza dello stack specificata con .MODEL e cmq non si applicano al modello flat.
Utilizzo:

.CODE
.STARTUP


..


.EXIT
END
Se non si usa .STARTUP bisogna dare l'indirizzo di partenza come argomento per la direttiva END, ovvero

.CODE

start:

...

END start

Con il default NEARSTACK, .STARTUP fa puntare DS a DGROUP e setta SS:SP relativamente al DGROUP generando il codice seguente:

@Startup:
mov dx, DGROUP
mov ds, dx
mov bx, ss
sub bx, dx
shl bx, 1 ; If .286 or higher, this is
shl bx, 1 ; shortened to shl bx, 4
shl bx, 1
shl bx, 1
cli ; Not necessary in .286 or higher
mov ss, dx
add sp, bx
sti ; Not necessary in .286 or higher

Starting and Ending Code with .STARTUP and .EXIT
(C) 1992-1996 Microsoft Corporation. All rights reserved.
Macro Assembler 6.1 (16-bit) - MSDN Archive Edition Page 40
.
.
END @Startup
Un programma MS-DOS con l'attributo FARSTACK non ha bisogno di aggiustare SS:SP, così .STARTUP fa solo


@Startup:

mov dx, DGROUP
mov ds, dx
.
.
.
END @Startup
Quando il programma termina si può ritornare un codice di uscita al sistema operativo e può tornare utile per quelle applicazioni che fanno un controllo su tale valore di ritorno e di solito un codice 0 significa tutto ok. La direttiva .EXIT accetta un codice di uscita da 1 byte come argomento opzionale:

.EXIT 1 ;

.EXIT ; genera il seguente codice che ritorna il controllo a MS-DOS terminando il programma.
        ; Il valore di ritorno può essere una costante, un riferimento di memoria e va in AL

mov al, value
mov ah, 04Ch
int 21h


Inizializzazione dei registri di segmento
-----------------------------------------
Dai dai ci siamo arrivati!
Prima di utilizzare gli indirizzi segmentati nel programma, dobbiamo inizializzare i registri di segmento. Questo processo di inizializzazione dipende dai registri usati e dall'aver scelte le direttive semplificate o meno in quanto le direttive semplificate gestiscono vari aspetti al posto nostro come abbiamo appena visto. Come sappiamo, la famiglia di processori 8086 usa un sistema di registri di segmento di default per semplificare l'accesso al codice e ai dati. Normalmente, i registri di segmento DS, SS e CS vengono inizializzati all'inizio di un programma. Ecco i passi da seguire per inizializzare i segmenti:

1. Dire all'assemblatore quale segmento è associato con un determinato registro. DEVE saperlo in fase di assemblamento

2. Dire al processore quale segmento è associato con un determinato registro scrivendo il codice necessario per caricare il valore corretto del segmento nel registro di segmento nel processore.

Parliamo con l'assemblatore sui valori dei segmenti...
--------------------------------------------------------
Questo compito lo facciamo con la direttiva ASSUME (con le direttive semplificate l'assembler genera automaticamente le assunzioni appropriate e .STARTUP setta DS = SS, a meno che non si specifica FARSTACK, permettendo di accedere ai dati sia con SS che con DS). Ecco un esempio di assume con le direttive estese:

ASSUME cs:_TEXT, ds:DGROUP, SS:DGROUP

è anche possibile avere segmenti diversi per i dati e il codice

ASSUME cs:MYCODE, ds:MYDATA, ss:MYSTACK, es:OTHER

Usando la direttiva .CODE l'assemblatore assume che CS sia il segmento corrente.

...e pure due chiacchiere col processore sempre sui valori dei segmenti
----------------------------------------------------------------------
L'ultimo passo per inizializzare i segmenti è informare il processore al run time. Come i valori dei segmenti sono inizializzati dipende dal sistema operativo e dall'uso o meno delle direttive semplificate.

Specificare un indirizzo di partenza
------------------------------------
L'indirizzo di partenza di un programma determina dove l'esecuzione di un programma inizia (applausi :D). Dopo il sistema operativo carica il programma, semplicemente saltando al suo indirizzo di partenza dando quindi al processore il controllo sul programma. Naturalmente, dietro le quinte è lo scheduler che si occupa di gestire i processi che devono essere "runnati", poi si potrebbe discutere del meccanismo di task switching ben documentato da intel, e cmq come ormai sapete multithread non vuol dire che i processi vengono eseguiti "contemporaneamente", è solo un'impressione, il realtà i processi vengono sempre eseguiti UNO alla volta...ma questa è un' altra storia. Se volete approfondire le meraviglie e le profondità dei sistemi operativi vi consiglio IL librone di Silberschatz, 6 ed. Tanembaum 3 ed non mi piace, si lui è un grande ma fa troppo lo sborone con il suo minix, era già meglio la seconda edizione). Il vero indirizzo di partenza è noto solo al loader (e a qualsiasi gonzo con un PE Editor, un Process viewer, un disasm o un hex editor, cmq..., ndnt). Una nota sul codice rilocabile: (ehm questo discorso vale per le dll o cmq i moduli che vengono caricati all'interno dello spazio di un processo, siccome potrebbe esserci già un modulo ad un certo addr il modulo viene rilocato ovvero messo da un'altra parte da quella prevista e questo è possibile solo se il modulo contiene nel sul PE una relocation table (se non ce l'ha dà errore) che dice al loader dove aggiornare gli indirizzi assoluti in base ad un valore delta calcolato dal loader, questo discorso lo spiego bene nel tut sul PE, ndnt). Questo offset dipende dal tipo di programma.
Programmi con estensione .exe contengono un header (.exe, .ocx. .dll. .sys ecc si chiama PE Header, ndnt) dal quale il loader legge un RVA e lo combina con l'image base che ha deciso per l'exe.
I .com invece non hanno questo header, così per convenzione il loader salta al primo byte del programma. In entrambi i casi la direttiva .STARTUP identifica dove inizia l'esecuzione. per un .exe   immediatamente prima l'istruzione dove si vuole che l'esecuzione cominci. In un .com prima della prima istruzione assembly nel codice sorgente. Facciamo un esempio di come dire a un programma dove iniziare l'esecuzione (es. file .com e quindi modello tiny):


_TEXT SEGMENT WORD PUBLIC 'CODE'
	ORG 100h


start:
...

_TEXT ENDS
       END start
ORG è obbligatoria nel modello di memoria tiny senza la direttiva .STARTUP. Piazza la prima istruzione all'offset 100h nel segmento codice per lasciare spazio ai 256 byte (100h) dell'area dati chiamata PSP (Program Segment Prefix, creato da DOS per tutti i programmi e contiene svariate informazioni per l'esecuzione dello stesso. Dove sta? Vediamo un esempio (Sempre da AsmJournal), dando uno sguardo veloce alla struttura di un vecchio file .com:


FFFFh +--------------------+ <- SP
	 |		      |
	 |	 Stack	      |
	 |		      |
	 +--------------------+
	 |		      |
	 | Uninitialized Data |
	 |		      |

	 +--------------------+
	 |		      |
	 |   COM File Image   |
	 |		      |
    100h +--------------------+ <- IP
	 |		      |
	 |	  PSP	      |
	 |		      |
      0h +--------------------+ <- CS, DS, ES, SS
OK, ma quali informazioni contiene?

[ PSP - Program Segment Prefix ]

   Offset	Size		Description
   ------	----		-----------
   0h		Word		INT 20h instruction
   2h		Word		Segment address of top of the current program's
				allocated memory
   4h		Byte		Reserved
   5h		Byte		Far call to DOS function dispatcher (INT 21h)
   6h		Word		Available bytes in the segment for .COM files
   8h		Word		Reserved
   Ah		Dword		INT 22h termination address
   Eh		Dword		INT 23h Ctrl-Break handler address
   12h		Dword		DOS 1.1+ INT 24h critical error handler address
   16h		Byte		Segment of parent PSP
   18h	     20 Bytes		DOS 2+ Job File Table (one byte per file handle
				FFh = available/closed)
   2Ch		Word		DOS 2+ segment address of  process' environment
				block
   2Eh		Dword		DOS 2+ process' SS:SP  on entry to last INT 21h
				function call
   32h		Word		DOS 3+ number of entries in JFT
   34h		Dword		DOS 3+ pointer to JFT
   38h		Dword		DOS 3+ pointer to previous PSP
   3Ch	     20 Bytes		Reserved
   50h	      3 Bytes		DOS 2+ INT 21h/RETF instructions
   53h	      9 Bytes		Unused
   5Ch	     16 Bytes		Default unopened File Control Block 1 (FCB1)
   6Ch	     16 Bytes		Default unopened File Control Block 2 (FCB2)


   7Ch	      4 Bytes		Unused
   80h		Byte		Command line length in bytes

   81h	    127 Bytes		Command line (ends with a Carriage Return 0Dh)
Cmq ci fermiamo qui, una trattazione completa esula dallo scopo di questo tutorial, se no davvero non ne esco più:D) ). Il sistema operativo si prende cura di inizializzare il PSP così che devi solo essere sicuro che quest'area esista.

Inizializzare ds
----------------
Per l'ennesima volta, DS viene automaticamente inizializzato al corretto valore (DGROUP) se si usa la direttiva .STARTUP. Se non si usa .STARTUP con MS-DOS bisogna fare

mov ax, DGROUP
mov ds, ax
Questo esempio carica DGROUP ma si può usare qualsiasi segmento o gruppo.

Inizializzare SS e SP
---------------------
Se si usa la direttiva semplificata .STACK o se si definisce un segmento che ha il tipo combine STACK con le direttive estese SS e SP vengono inizializzate automaticamente (praticamente un ricatto :P iihihhhi).
Per un file .exe l'indirizzo dello stack viene codificato nell'header dell'eseguibile e risolto in fase di caricamento. Per un file .com il loader setta SS = CS e inizializza SP = 0FFFEh

Let's talk about Masm 6.1x
-------------------------
* Supporto completo Win32. Un'applicazione scritta con masm è una vera applicazione win32 console e supporta nomi lunghi per i src, file oggetto...

* Supporta sia di Intel OMF ("old") che COFF - Common Object File Format - (Win32)

* Controllo dei parametri in fase di assemblamento. Generazione di nome decorati che permettono un controllo in fase di linking del numero di parametri passati ad una funzione (ad esempio nella definizione di PROC e PROTO) e questo può essere usato dalle win32 import libraries per evitare crash in caso venissero passati parametri errati

In caso di errore masm genera il postfix "@" sbagliato e il link fallisce con un riferimento indefinito. E questo è sempre meglio che ritrovarsi ad avere a che fare con errori a run time impredicibili.

* Supporto TYPEDEF

* Supporto della direttiva INCLUDELIB per semplificare il processo di linking e permettere l'invocazione automatica delle librerie importate dal linker

* INVOKE - Masm provvede questa macro per gestire molte dei dettagli importanti per le chiamate di procedure, come il pushing dei parametri nel rispetto della calling convention.
* La vecchia definizione dei dati (DB, DW, DD) è stata estesa con tipi nuovi e più precisi (i vecchi sono cmq sempre validi):
BYTE, SBYTE, WORD, SWORD, DWORD, SDWORD, FWORD, QWORD, TBYTE, REAL4, REAL8, REAL10.

Nota su INVOKE:
Esiste una piccola limitazione sull'uso di questa direttiva (in ML), quella che all'epoca venne definita "The infamous 512 bytes buffer":
Il suo input buffer (parsing) aka "logical line" è solo di 512 byte. Questo è documentato pure nel Prog.Guide Chapter 1/ Language Components of MASM/ Statements", page 22. Diciamo che dobbiamo chiamare una funzione. Ma quando una INVOKE di Win32 viene fatta con una lunga lista di parametri, si potrebbe voler scrivere (e commentare) in un modo simile: so at this point, we have to call it a feature. But when coding a Win32 INVOKE with a long parm list, you might want to code (and document your code) such as this:


 INVOKE CreateProcess,
          OFFSET lpApplicationName,    ;=> to EXE name
          OFFSET lpCommandLine,        ;=> to command line string
          OFFSET lpProcessAttributes,  ;=> to process sec attribs
          OFFSET lpThreadAttributes,   ;=> to thread sec attribs
          bInheritHandles,             ;handle inheritance flag
          dwCreationFlags,             ;creation flags
          OFFSET lpEnvironment,        ;=> to new env block
          OFFSET lpCurrentDirectory,   ;=> to current dir name
          OFFSET lpStartupInfo,        ;=> to STARTUPINFO
          OFFSET lpProcessInformation  ;=> to PROCESS_INFORMATION
Bene, scordatelo. I commenti contano nei 512 byte, così che la "logical line" (che non usa TAB ma spazi) non entra nel buffer di 512 byte. Masm lo segnerà come errore. Per cui spesso ci si potrebbe trovare a rimuovere commenti e spazi, packare molti parametri per linea. Non si supera l'ostacolo con il carattere di continuazione \ o altri barbatrucchi (mamma miiiaaa che figurone che farete in classe... :P iihhihi). Beh, non so, non mi sono ancora documentato, tuttavia ha senso pensare che se siamo arrivati a Masm8...l'avranno sistemata sta cosa...boh
-----------------------------------------------------------------------------------------------------------------------------------
Credo di aver finito e di essere sopravvisuto, anche se con i vestiti ridotti a brandelli, le vesciche e la disidratazione :D rotfl Spero di non aver trascritto boiate e soprattutto che possa essere utile a tutti coloro che si avvicinano al mondo incantato della programmazione in assembler, come lo è stato per me...credo :P.
Fatemi sapere!
Alla prossima (ehi, però non vi abituate adesso, massa di viziati! :P ahahah)

Follemente vostro

Lonely Wolf

PS.
BIBLIOGRAFIA

A parte i link che trovate nel tute, mi sono massiciamente basato sul libro MAsm Programmer's guide (che si rimedia con google), il sito msdn.microsoft.com per le info sulle calling convention, e testi vari da cui ho estrapolati piccoli pezzi, come Memory in SPARC SPARC Assembly Language Mark Smotherman Clemson University da cui ho estrapolato il pezzettino su BSS, poi vabbeh, ho chiesto qua e la in chan per farmi spiegare alcune cosette (grazie).

Note finali

Il mio secondo tutorial! e il primo che scrivo utilizzando il mio UICFORM (o quasi) :P Allora, saluto Ntoskrnl (ovviamente :P grazie davvero per le tue note e per aver corretto qualche mio strafalcione), albe, drb4tch, Quequero, andreageddon, evilcry, ironspark, kratorius...e anche i satelliti di marte :D ketamine, N0body88, halt Ah, ciao Urk! (ehi fatti sentire;D)

Disclaimer

Sono contento di averlo fatto, ho imparato molto. Come sono andato?