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).