Salve ragazzi,
visto la continua
espansioni dei Sistemi Operativi basati su Win 32 ho deciso
di scrivere qualcosa
che riguarda l'argomento ed in particolare su una delle
parti più
interessanti dei Sistema Operativi il gestore della memoria.
Il documento tratta
alcuni concetti che sono alla base per della programmazione
a basso livello ed
anche se non è corredato da esempi, penso che sia utile per
chi non ha una visione
chiara del modo protetto e del modello di memoria usato
nelle piattaforme
Win32.
- Prima parte: Breve
descrizione sui meccanismi di protezione dei processori
80386+.
- Seconda Parte:
Modello di memoria usato nelle piattaforme Win32.
Parte Prima.
L'80386 e tutti i
processori compatibili con esso, comprende una serie di
meccanismi di protezione.
Essi sono utilizzati dal sistema operativo per
ridurre l'effetto
di un bug di un programma o per limitare l'accesso ad alcune
risorse da parte
delle applicazioni utente. La protezione si basa su cinque
aspetti:
- Verifica del tipo
- Verifica del limite
- Restrizione del
dominio indirizzabile
- Restrizione dei
punti di entrata delle procedure
- Restrizione del
set d'istruzione.
Questi aspetti della
protezione sono applicati nella protezione a livello di
segmento e di pagina.
Senza entrare nel dettaglio possiamo brevemente
descrivere i cinque
apetti della protezione.
La verifica del tipo
a livello di segmento è usata dal processore per
riconoscerne i diversi
tipi. Infatti i segmenti possono essere di tipo dati o
eseguibili, ma anche
i descrittori agli stessi segmenti possono essere di tipi
diversi. Per le pagine
la verifica del tipo serve solo per comprendere se la
pagina è a
sola lettura o a lettura/scrittura.
La verifica del limite
è applicata solo nella protezione a livello di segmento,
infatti ogni segmento
definito ha una propria grandezza, quindi la protezione
del limite controlla
che non si cerchi di leggere/scrivere oltre la grandezza
massima del segmento.
La restrizione del
dominio indirizzabile è legato al concetto di livello di
privilegio. Il meccanismo
del privelegio è stato implementato dalla Intel,
dividendo in 4 livelli
da 0 a 3 (detti anche ring 0 - 3) lo stato di privilegio
attuale. Il livello
0 è quello con il privilegio più alto, di solito è
usato
dal codice di sistema,
mentre il livello 3 è quello con priorità minima ed è
destinato alle applicazioni
utente. Il processore al momento dell'esecuzione ha
un proprio livello
di privilegio (CPL, livello di privilegio corrente), ed in
base a questo livello
è abilitato all'accesso o no ad alcuni segmenti ed ha la
possibilità
o no di eseguire alcune istruzioni particolari. La restrizione del
dominio indirizzabile
a livello di segmento serve per controllare se con il CPL
attuale è
possibile accedere ad un determinato segmento. A livello di pagina
invece, la restrizione
del dominio indirizzabile è implementato assegnando a
ciascuna pagina uno
dei 2 livelli: Supervisore o Utente. Se il CPL del
processore è
3 il livello corrente è Utente; altrimenti è Supervisore.
La
differenza tra Utente
e Supervisore è nella possibilità di poter leggere o
leggere/scrivere
nelle pagine e nelle tabelle di pagine.
La restrizione dei
punti di entrata delle procedure è implementata in modo da
poter eseguire solo
le istruzioni che si trovano in segmenti con lo stesso
livello di privilegio
di quello corrente (CPL). Cioè il controllo viene
effettuato in ogni
jump far e call far, il livello di privilegio del segmento
in cui si deve saltare
(chaimato DPL) deve essere uguale al CPL. Naturalmente
ci sono delle eccezioni
è possibile infatti eseguire anche codice in segmenti
di livello di privilegio
più basso rispetto a quello corrente, ma questo
avviene solo in particolari
segmenti. Ed infine è anche possibile (com'era
logico attendersi)
trasferire il controllo a livelli di privilegio numericamete
inferiori (cioè
a segmenti con privilegio più alto di quello corrente); questo
trasferimento è
possibile solo tramite particolari descrittori chiamati porte
ed il processo di
passaggio fra livelli di privilegio diversi viene chiamata
callgate (porte di
chiamata... brutta traduz.. ma copiata dai manuali Intel).
Infine la restrizione
del set d'istruzioni: ci sono alcune istruzioni che
possono essere eseguite
solo se il CPL è 0, queste comprendono tutte le
operazioni sui registri
di controllo, di debug, di test, ed in più qualche
altra istruzione
come HLT, LGDT, LIDT etc.
Ok non sarò
stato il massimo della chiarezza ma purtroppo è difficile spiegare
il tutto senza entrare
nel dettaglio, comunque anche se non avete compreso
tutto, l'importante
è che avete capito i concetti fondamentali della
protezione.
Parte Seconda.
Iniziamo ora un'affascinante
discussione sulla gestione della memoria in
ambiente Win32.
Win32 a ring 3 usa
il modello di memoria flat (o piatta in italiano), mentre a
ring 0 esso usa il
normale metodo di indirizzamento selettore/spiazzamento, con
il paging attivato.
Prima di continuare voglio ricordare il protected-mode
addressing model
dei processori 80386+.
Nel modo protetto
ogni segmento è definito dal programmatore, egli, infatti,
può scegliere
alcuni attributi come l'indirizzo base del segmento, la
grandezza, livello
di privilegio ed altri parametri. La struttura che definisce
questi attributi
è chiamata descrittore. I descrittori sono situati in due
tabelle la LDT (tabella
dei descrittori locale) e la GDT (tabella dei
descrittori globale).
C'è una sola GDT e più LDT definite a tempo di
esecuzione. Il puntatore
alla LDT si trova nel registro LDTR mentre quello alla
GDT si trova nel
registro GDTR. Si può selezionare un descrittore caricando un
selettore in un registro
di segmento. Un selettore è formato da 16 bit, che
indicano: La tabella
scelta (LDT o GDT), un indice in questa tabella ed il
livello di privilegio
del richiedente (RPL). Quindi si può indirizzare una
locazione tramite
la coppia selettore/spiazzamento (selector/offset); l'offset
può essere
a 32 bit o a 16, 32 per segmenti definiti a 32 bit e 16 per segmenti
definiti a 16 bits.
Quando viene modificato
un registro di segmento con un nuovo selettore, l'80x86
legge le informazioni
del descrittore (selezionato) e crea un indirizzo lineare
(Linear Address)
prima di accedere alla memoria. L'indirizzo lineare è creato
leggendo il campo
"segment base" del descrittore ed aggiungendoci lo
spiazzamento.
LDTR or GDTR---->|TABELLA
DEI DESCRITTORI|
|
|
SELETTORE ---->
| entry.Segment_Base_field + SPIAZZAMENTO = INDIRIZZO LINEARE
Ora se il meccanismo
della paginazione è disabilitato, l'indirizzo lineare è
uguale all'indirizzo
fisico.
Se invece la paginazione
è abilitata l'indirizzo è diverso da quello fisico. In
questo caso, l'indirizzo
lineare è visto dall'80x86 in questo modo:
Indirizzo Lineare
= DIR:PAGE:OFFSET
Indirizzo Lineare
a 32 bit:
Bits 0..11
= OFFSET
Bits 12..21
= PAGE
Bits 22..31
= DIR
Il meccanismo della
paginazione è implementato dall'80x86 in questo modo:
- una pagina è
generalmente di 4k (sui 486+ può essere maggiore)
- ci sono due livelli
di tabelle di pagine, nelle quali ogni elemento specifica
l'indirizzo del page
frame, la protezione e così via.
Nel registro CR3
(detto anche PDBR Page Directory Base Register) c'è un
puntatore alla tabella
di pagine di 1° livello (la directory table). L'elemento
della directory table
punta ad un page table (la tabella di 2° livello).
L'indirizzo fisico
sarà allora creato in questo modo:
Indirizzo Lineare = DIR:PAGE:OFFSET
CR3 ------>| PAGE
DIRECTORY | (o directory table)
|
|
DIR -----> | entry
---------| -> |PAGE TABLE|
|
|
PAGE------>| entry.frame_address_field
+ OFFSET = PHYSICAL ADDRESS
Nota: questo schema
è valido solo se le pagine sono di 4k e l'extending address
è disabilitato.
Il modo di indirizzamento
FLAT è un semplice modello usato per bypassare la
segmentazione. Il
modo di indirizzamento FLAT è creato assegnando ai segmenti
dati e codice (CS
e DS..ed anche ES di solito) un indirizzo base uguale a 0. In
questo modo lo spiazzamento
altro non è che l'indirizzo lineare, infatti il
campo segment base
(che è uguale a 0) viene sommato allo spiazzamento ma
SPIAZZAMENTO + 0
= INDIRIZZO LINEARE! Ci sono diverse implementazione del modo
di indirizzamento
FLAT con il meccanismo della paginazione abilitata,
disabilitata o con
qualche altra lieve differenza rispetto a quanto spiegato.
Nota, non è
possibile disabilitare la segmentazione solo la paginazione può
essere abilitata
o disabilitata, il modello FLAT non disabilita la paginazione
la "nasconde" solamente!
Per maggiori informazioni
su questi argomenti leggete i manuali Intel.
Ed ora il modello
di memoria utilizzato dalle piattaforme win32 per i chip
Intel. Nota: per
le versioni di Win NT per processori diversi da quelli Intel e
da quelli compatibili
80386, la discussione seguente potrebbe non essere
valida!
A ring 3 win32 usa
il modello FLAT, 3 segmenti principali sono creati uno per
il codice e due per
i dati (sono CS,DS e ES), entrambi con il campo indirizzo
base impostato a
0 e limite a 4 GB. Win32 usa la paginazione per questo
l'indirizzo lineare
è diverso da quello fisico (ogni pagina è di 4kb). Per la
natura stessa del
modello FLAT con la paginazione abilitata a ring 3 i processi
possono vedere solo
gli indirizzi mappati nel loro address space e nient'altro.
Non ci sono win32
API (o almeno io spero) che permettono di allocare un
descrittore nella
GDT o nella LDT, ma se sappiano dove un descrittore in una
GDT o LDT punta,
noi possiamo caricarlo in un registro di segmento e possiamo
quindi indirizzare
anche questa regione di memoria con i relativi attributi.
Naturalmente sappiamo
che la stessa regione è comunque indirizzabile anche
utilizzando il registro
DS di default in quando è l'indirizzo lineare quello
che realmente ci
indica una regione di memoria e non i segmenti!. Come
risultato del modello
FLAT implementato a ring 3 è possibile scrivere in un
sezione di codice
(bisogna però usare WritememoryProcess o altri trucchetti).
Questo è possibile
solo perché il segmento dati e di codice hanno lo stesso
indirizzo base e
conseguentemente lo stesso spazio di indirizzi, ed essendo
possibile scrivere
nel segmento dati... ;).
Nota: l'80x86 non
permette di scrivere in un segmento eseguibile!.
Facciamo un esempio
per rendere tutto più semplice, supponiamo di avere il
seguente frammento
di codice:
.data
data_code db
40 dup(?)
.code
mov eax,34 ;fake number!
@1:
mov [data_code], eax ;accessing data through DS
....
;è possibile apportare ulteriori modifiche sull'array data_code
:-)
.....
;ora supponiamo di aver copiato del "codice" nell'array data_code
;per eseguirlo basterà:
@2:
jmp offset data_code ;accessing data through CS..uh executing
;)
; karino no?!!
L'esempio è
abbastanza semplice, ed in realtà le uniche istruzioni di rilievo
sono le @1 e @2.
Si capisce subito che nell'istruzione @1, in realtà è
sottointeso l'uso
del registro DS come segmento da utilizzare perciò
l'indirizzo data_code
sarà visto dal processore come un indirizzo che fà parte
di un segmento dati
(DS:data_code), ed è quindi possibile scriverci sù.
Analogamente nell'istruzione
@2 è implicito l'uso del registro CS e quindi
l'indirizzo data_code
è visto come parte di un segmento eseguibile
(CS:data_code) ed
è perciò possibile eseguirlo. Da questo se ne deduce che
in
win32 ogni pagina
in memoria può essere eseguita, per questo motivo i flags
PAGE_EXECUTE (usati
in VirtualAlloc e VirtualProtect) negli ambienti win32
progettati per processori
Intel praticamente non servono ma esistono solo per
compatibilità
con gli ambienti progettati per altri processori. Premesso quindi
che possiamo allocare
un qualsiasi blocco di memoria ed eseguirci del codice,
un ulteriore aspetto
da tener in considerazione riguarda invece il codice che
si automodifica.
Ho già detto che possiamo scrivere anche nelle pagine che
contengono codice,
ma questo può essere fatto solo ad una condizione, e cioè
che queste pagine
non siano protette da scrittura (se ci troviamo a ring 0
nemmeno questo aspetto
ci interesserà più e quindi potremo fare ciò che
vogliamo! Viva la
libertà!). Per ottenere questa informazione basta chiamare
un'apposita Api,
VirtualQuery, che ci darà alcune informazioni sulla pagina tra
cui il tipo protezione
(Lettura o Lettura/scrittura). Per modificare invece il
tipo di protezione
basta solo usare VirtualProtect ed il gioco è fatto! Si
potrà ora
scrivere nella pagina scelta.
Win32, logicamente,
non può usare solo il modello FLAT; infatti, a ring 0 si
possono utilizzare
i selettori (o meglio è solo a ring 0 che normalmente si
usano!). In win 9x
ogni VM (Virtual Machine) ha una propria LDT e la usa per
accedere alla memoria
mentre le applicazioni utente usano praticamente la
stessa LDT. In Nt
invece ogni processo ha la sua LDT. Proprio su questa
differenza si basa
un metodo per riconoscere Nt da win9x, infatti i selettori
delle appz (a ring
3) win9x usano la LDT mentre quelle Nt usano la GDT. Nella
LDT, ci sono segmenti
a 32 o a 16 bit, questo è dovuto al fatto che Win9x ed Nt
possono eseguire
codice a 16 bit. In realtà i segmenti non servono a molto per
gestire le applicazioni
win 32 anche da ring 0, essi sono invece importanti
nelle applicazioni
a 16 bits. Un'ultima nota sui selettori: la LDT e la GDT in
win 9x non sono protetti
quindi si può benissimo scriverci sù ;), la cosa
(logicamente) non
è vera per Nt (e per tutti i sistemi operativi creati con una
certa logica in mente!).
Naturalmente, anche
a ring 0 la paginazione è abilitata, perciò creare
l'indirizzo fisico
è un pò più complesso. A ring 0 è possibile
allocare una
pagina, e generalmente
è la pagina l'unità di allocazione base. Infatti alcuni
servizi VMM richiedono
un numero di pagina come argomento, questo altro non è
che la combinazione
di DIR:TABLE dell'indirizzo lineare. Per esempio se abbiamo
un indirizzo lineare
possiamo trovare il numero di pagina corispondente,
semplicemente con
l'istruzione: shr linear_address,12 (la DIR:TABLE è shiftata
a destra nella posizione
dello spiazzamento).
Il meccanismo della
paginazione implementato nel 386+ è uno strumento molto
comodo per la gestione
della memoria virtuale e quindi windows lo sfrutta a
pieno. Infatti un
indirizzo lineare, come ho già detto, mi indentifica una
pagina e uno spiazzamento
nella pagina, ma nessuno ci assicura che la pagina
sia allocata o che
sia in memoria. Se si prova ad accedere ad una pagina non
presente si verifica
un'eccezione. Il gestore dell'interruzione (che in questo
caso è la
parte del kernel di winzoz che implementa la memoria virtuale)
controlla se la pagina
sia presente nel file di swap ed in questo caso rialloca
la pagina in memoria
e fa in modo di rieseguire l'istruzione che ha causato
l'eccezione. Nel
caso, invece, in cui la pagina non sia presente nel file di
swap, windows visualizza
la magica finestra "Errore di pagina non valida" (o
qualcosa di simile
non mi ricordo bene!). Capirete quindi come sia facile
implementare la gestione
della memoria virtuale da parte di winzoz (grazie
Intel almeno per
questo!).
Un'altra caratteristica
importante della paginazione è che elimina in parte il
problema della deframmentazione
della memoria. Infatti una serie di indirizzi
lineari contigui
(che indirizzano più di una pagina) non è detto che facciano
riferimento ad aree
di memoria fisicamente contigue. Ciò permette alle
applicazioni di allocare
blocchi di memoria molto grandi e trattarli come se
fossero costituiti
da uno spazio fisico contiguo (anche se ciò la maggior parte
delle volte non è
vero!). Bisogna comunque sapere che se la paginazione risolve
il problema della
frammentazione della memoria fisica, il problema più generico
della "frammentazione"
in generale rimane. Infatti si deve stare attenti alla
frammentazione degli
indirizzi lineari. Cioè può succedere che gli indirizzi
lineari contigui
utilizzabili nello spazio di indirizzamento di un processo
finiscano. Per ovviare
a questo problema Win32 mette a disposizione una serie
di flags da utilizzare
nelle Api per l'allocazione della memoria. In pratica
una volta allocato
il blocco ti viene restituito un handle e non un indirizzo
lineare. Prima di
accedere al blocco lo devi "bloccare" (cioè chiami l'api
GlobalLock che ti
retituisce l'indirizzo lineare) quindi a fine accesso lo devi
"sbloccare" (api
GlobalUnlock), facendo quindi uso di questi handle invece che
di indirizzi lineari,
lo spazio degli indirizzi lineari verrà automaticamente
deframmentato da
windows; per maggiori informazioni vedere l'Api GlobalAlloc
con il flag GHND.
Ritornado a noi, cerchiamo
ora di capire cosa si intende per address space
(spazio di indirizzamento)
di un processo. Sicuramente avrete sentito parlare
di Address Space!
In effetti è una delle caratteristiche di win 32 ed
sostanzialmente consiste
nell'assegnare ad ogni processo un proprio spazio di
indirizzamento. Cioè
l'indirizzo 400000 di un processo A punterà ad un'area di
memoria fisica diversa
dall'indirizzo 400000 di un processo B. Questo permette
ad ogni processo
di accedere solo alle regioni di memoria private ed a quelle
che il sistema operativo
decide di "concedergli". Bene ora spieghiamo come il
S.O. implementa questa
caratteristica :P
Sappiamo che la memoria
è divisa in pagine, che queste pagine sono specificate
dalle tabelle di
pagine e che la posizione in memoria di queste tabelle è
specificata nel registro
CR3. Sicuramente già sapete che quando c'è un context
switch (il passaggio
da un processo ad un'altro) il processore legge le
informazioni per
avviare il nuovo processo dal TSS (Task State Segment) dove
oltre alle normali
informazioni sui registri ci sono i campi che specificano la
nuova LDT (che andrà
in LDTR) e la nuova tabella di pagine di 1° livello (che
andrà in CR3),
quindi potenzialmente al passaggio tra un processo ed un'altro
si avrà un
cambio delle tabelle di pagine e della LDT. In win32 le LDT cambiano
solo tra le diverse
Virtual Machine (in Nt anche tra i diversi processi),
invece le tabelle
di pagine cambiano sempre. Bisogna ora porci una domanda:in
un context switch
si possono cambiare le tabelle di pagine in modo da
indirizzare memoria
fisica completamente differente da quella del processo
precedente? Beh,
in teoria sì in pratica no, infatti ci sono alcune aree di
memoria che devono
essere comunque comuni a tutti i processi, come ad esempio
la GDT che per definizione
è comune a tutti i processi. Ed infatti anche in
win32 alcune parti
di memoria sono condivise tra processi, in particolare il
Kernel è condiviso
tra tutti i processi. Ogni processo avrà quindi una serie di
indirizzi lineari
a disposizione per se stesso (indirizzi che punteranno ad
altre aree fisiche
in altri processi) ed in più avrà a disposizione anche
un'altra serie di
indirizzi lineari, come quelli riferiti al Kernel, che
saranno uguali per
qualunque processo. Questo significa che ci sono delle
tabelle di pagine
di secondo livello che verranno usate in ogni processo
(ricordate? esse
sono indirizzate tramite la tabella di primo livello), mentre
altre invece saranno
uniche per ogni processo.
In particolare in
win 9x è documentata la diversificazione degli indirizzi
lineari:
00000000H -
003fffffH usato dalle VM a livello DOS
00400000H -
7fffffffH l'area privata di un processo, corrisponde all'address
space privato
80000000H -
0bfffffffH usata per codice e dati condivisi, appz 16 bits DPMI
data etc.
0c0000000H - 0ffbfffffH
usata per codice e dati per le VM
E' evidente quindi
che la massima memoria privata allocabile dal processo è
circa 2 GB. Inoltre
l'esistenza dell'area shared (80000000H - 0bfffffffH) viene
utilizzata da windows
per la condivisione di aree di memoria tra i vari
processi. In particolare
i memory mapped file vengono creati proprio in
quest'area e ciò
ha come conseguenza il fatto che l'indirizzo lineare
restituito dall'Api
MapVieOfFile è lo stesso per tutti i processi ;).
In NT il discorso
è un po' diverso. Prima della service pack 3 in NT 4.0 le
applicazioni utente
(user mode) avevano a disposizione gli indirizzi lineari da
0 a 2 GB mentre i
programmi di sistema (kernel mode) avevano a disposizione gli
indirizzi da 2 a
4 GB. Dall'introduzione del service pack 3 i programmi di
sistema hanno a disposizione
gli indirizzi lineari da 3 a 4 GB, con una
conseguente diminuzione
dello spazio di indirizzamento di un GB per i programmi
in kernel mode, ed
un'aumento di un GB invece per le applicazioni utente. WinNt
non ha aree shared,
quindi per condividere blocchi di memoria tra più processi
lavora in maniera
diversa da win9x, in particolare egli tiene traccia delle
pagine che devono
essere condivise e le mappa nello spazio di indirizzamento di
tutti processi che
le utilizzano. In altre parole i blocchi di memoria pubblici
saranno mappati per
ogni processo che ne fà richiesta ed avranno in genere
indirizzi lineare
diversi.
Un'ulteriore aspetto
sulla gestione della memoria di win 32 è il copy-on-write.
Quando due o più
processi condividono un'area di memoria ed uno di questi la
modifica, il copy-on-write
entra in gioco creando una copia privata del blocco
al processo che lo
ha modificato. Tutte le modifiche quindi influenzeranno da
ora in poi la copia
privata e solo la copia non modificata sarà condivisa con
gli altri processi.
Questo è in generale come funziona in copy-on-write in
molti sistemi operativi,
vediamolo ora in particolare come è stato implementato
in Nt e win 9x.
In Nt, la cosa funziona
così:
le pagine dati scrivibili
sono inizializzate dal sistema operativo come a sola
lettura. Quando un
processo proverà a scriverci ci sarà un page faults (si
verifica quando si
prova a scrivere ad pagina a sola lettura da ring 3) ed il
S.O. creerà
quindi un copia privata della pagina per il processo, la mapperà
nell'address space
del processo stesso (con l'attributo di lettura/scrittura) e
quindi rieseguirà
l'istruzione che ha provocato il page fault. In questo modo
se una nuova istanza
del processo viene eseguita essa condividerà solo le
pagine che hanno
l'attributo a sola lettura (quelle che non sono state
modificate) con l'istanza
precedente. Se poi quest'ultima istanza proverà a
scrivere su queste
pagine, scatterà di nuovo il copy-on-write. Questo è un
meccanismo molto
importante per il S.O. e gli permette infatti di condividere
dati tra più
processi senza eccessivi problemi e nello stesso tempo di copiare
solo le pagine effettivamente
modificate dai processi stessi ottimizzando così
l'uso della memoria
e la velocità di esecuzione.
In win 9x il copy-on-write
non è implementato ma è in certo senso emulato
tramite l'Api WriteProcessMemory.
Cioè se si prova a scrivere una pagina
tramite WriteProcessMemory
scatterà il copy-on-write come già descritto (copia
privata della pagina,
rimapping etc..). Sfortunatamente WriteProcessMemory non
permette di scrivere
nell'area shared di win 9x e quindi non è utilizzabile il
copy-on-write di
dati in quell'area (memory mapped files, shared dlls etc).
Un'altro metodo di
emulare il copy-on-write in win 9x consiste nell'utilizzare
i memory mapped file
creandoli con il parametro PAGE_WRITECOPY, con conseguente
meccanismo del copy-on-write
nel caso che un processo provi a scrivere
nell'area di memoria
del m.m.f. stesso.
Abbiamo ora finito
la discussione generale su come è gestita la memoria negli
ambienti win 32 progettati
per processori Intel (gran parte del discorso è
comunque applicabile
anche alla versione di Nt per processori Alpha), tra breve
presenterò
anche alcuni esempi di programmi a ring 0 (per win 9x la maggior
parte... e forse
qualcuno per Nt) che usano le funzioni base per la
manipolazione delle
pagine (almeno spero!). Spero che la discussione vi possa
essere stata utile
:D
!!!! Fine !!!!
Soliti saluti:
Saluto tutti i membri
del gruppo RingZerO e tutti gli amici di #crack-it ed in
particolare (per
l'occasione di questo tut):
Kill3xx: per avermi
dato alcuni suggerimenti per il tut e per averlo riletto
dopo ogni modifica...
dovrebbero darti il premio nobel per la pazienza :-)
NeuRal_NoiSE: per
averlo letto e per averne dato un giudizio positivo anche se
a domanda specifica
se l'è cavata con un "domani lo leggo meglio" :P
D4eMoN: per aver avuto
la pazienza di leggerlo..ma non ho avuto un suo giudizio
:D...cmq il tuo cz
di crackme lo potevi fà un pò + umano ...no???!!! :P
Insanity: detto anche
l'uomo con il saluto + veloce del west..:-D per aver
pubblicato il documento
fidandosi di ciò che ho scritto ;-)
|