Il 'baco' della Malloc |
| A cura di Davide Bianchi |
L'utilizzo di malloc() e free() in modo non del tutto ortodosso puo'
provocare bug e malfunzionamenti molto complicati da trovare. Vediamo
come e perche' questo si verifica.
|
| free() - malloc() = crash |
Trovo il seguente messaggio su un NG dedicato alla programmazione in
C/C++:
...il programma funzionava perfettamente finche' non ho aggiunto
l'istruzione Nel 90% dei casi, un simile comportamento e' dovuto al "bug della malloc()", cioe' all'utilizzo di free() su un puntatore che non e' stato ottenuto con malloc(). Per capire come un simile casino si puo' verificare occorre capire come funziona "sotto il coperchio" il meccanismo di gestione della memoria.
|
|||||||||||
| La malloc "sotto il coperchio" |
In ambiente Windows 32, quando un programma viene avviato il sistema
fornisce come minimo 3 "blocchi" di memoria: un blocco per i dati,
un blocco per il codice ed un blocco per lo stack, il sistema predispone
poi un'area di memoria "virtuale" assegnata al programma stesso che
potra' quindi accedere ad una vasta zona di memoria a lui
riservata, senza preoccuparsi di conflitti con altri programmi. Benche' la memoria virtuale riservata al programma sia molto grande (gigabyte), se cerchiamo di utilizzarne una parte direttamente il sistema si incavolera' moltissimo, in generale otterremo un bell'errore di "accesso violation" (dipende dal compilatore e dal sistema Win 95/98/NT/2000...). Questo perche', benche' il nostro programma possa teoricamente accedere a gigabyte di memoria, prima di farlo deve informarne in qualche modo il sistema stesso, che provvedera' ad allocare realmente la memoria. Il processo quindi e' quello di chiamare una delle funzioni di gestione della memoria malloc() se stiamo lavorando in C, oppure usare new se stiamo lavorando in C++, le due cose non sono poi tanto differenti in quanto (di solito) new e' costruita sopra una malloc(). La malloc() solitamente ottiene un blocco di memoria contigua disponibile e passa il puntatore ottenuto al programma chiamante, il quale fara' quello che vuole con il blocco di memoria e poi utilizzera' la corrispondente funzione free() per "restituire" il blocco di memoria al sistema. Come viene gestita la memoria dalla malloc? in genere la malloc, internamente, si gestisce un elenco di "blocchi" di memoria disponibili, quando richiediamo un pezzo di memoria la malloc controlla quale e' il blocco sufficientemente grande a contenere il pezzo da noi richiesto, quindi "marca" il blocco come utilizzato nella sua lista interna e ritorna il puntatore al blocco stesso. Se la malloc non ha nessun blocco sufficientemente grande, richiedera' al sistema un nuovo blocco di memoria che poi gestira' per cavoli suoi. Questo comportamento (incidentalmente) provoca tutta una serie di problemi: supponiamo di avere un programma del tipo: char *p = (char*) malloc(1024); // faccio qualche cosa con p... free( p );Come diavolo fa free() a sapere quanto e' grosso il blocco di memoria che deve "restituire" all'elenco di blocchi disponibili? Questo significa che, da qualche parte, malloc()/free() devono tenersi la dimensione del blocco di memoria per poterlo gestire. In generale le varie implementazioni utilizzano i primi 4 byte del blocco stesso per memorizzare questa informazione, quindi quando usiamo malloc(32) in realta' il blocco che otteniamo e' 36 byte, in cui i primi 4 contengono la dimensione del blocco, ma malloc() ci ritorna il puntatore al 5o byte del blocco, cosi' noi possiamo usare 32 byte senza preoccuparci di niente... Questo comporta un ulteriore problema: malloc() deve fornire anche un indirizzo di memoria correttamente allineato, altrimenti l'accesso ad un long di 8 byte produrrebbe un ottimo "invalid hardware address", oppure (nel caso di processori Intel) il programma risulterebbe mostruosamente lento...
|
|||||||||||
| Un (quasi) invisibile bug... |
Ok, adesso sappiamo tutto della malloc() e della free(), vediamo il
codice seguente, che implementa un semplicissimo stack
autoincrementante:
#define INCR_STACK 32
int *push( int *stack, int value )
{
int *nuovoStack;
int nuovaDim;
/* verifico se lo stack e' abbastanza grande*/
if( stack[0] <= stack[1] )
{
/* devo ingrandire lo stack */
nuovaDim = stack[0] + INCR_STACK;
nuovoStack = (int*) malloc( sizeof(int) * nuovaDim );
/* copio il vecchio stack nel nuovo */
memcpy( nuovoStack, stack, sizeof(int) * stack[0] );
nuovoStack[0] = newSize;
/* elimino il vecchio stack */
/***** OCCHIO!!! ******/
free( stack );
stack = nuovoStack;
}
/* inserisco l'elemento nello stack */
stack[ stack[1] + 1 ] = value;
stack[1]++;
return stack;
}
void faiqualchecosa()
{
int value;
int stack[4] = {4,2};
int i;
value=1024;
for( i=2; i<4; ++i )
{
push( stack, i );
value++;
}
}
I primi due elementi dello stack sono usati per memorizzare la
dimensione (numero di elementi) dello stack ed il "puntatore"
all'elemento corrente dello stack stesso.La banalita' del programma e' sconvolgente, e proprio questo e' il problema. Per chiunque non fosse riuscito ancora a capire quale e' il problema ecco la soluzione, il programmatore che ha scritto faiqualchecosa() fondamentalmente ha pensato "non mi sembra il caso di allocare dinamicamente qualche cosa quando so gia' di quanti elementi avro' bisogno"...tutto giusto in teoria, in pratica pero' la dichiarazione int stack[4] = (4,2);, crea un array dichiarato di 4 elementi, ma solo due sono usati, quindi l'array stesso e' solo di due elementi. Il risultato di questo codice e' che la free() tenta di disallocare uno spazio di memoria che non e' stato allocato precedentemente da malloc(), questo fa si' che un blocco di memoria che non è gestito da malloc sia segnalato come "libero", questo potrebbe influenzare il blocco di memoria direttamente prima del nostro "stack", cioe' lo spazio occupato da value, che a questo punto dovrebbe valere piu' di 1024... Cosa succede esattamente a questo punto dipende molto dal compilatore e dal sistema sui cui il programma e' usato. Nel caso peggiore non succede nulla, perche' il blocco di memoria che viene "sputtanato" non contiene nulla di cruciale per l'esecuzione del programma, questo significa che il nostro programma contiene un potenziale bug che e' li' pronto a demolire la memoria e noi non ce ne siamo accorti, nel caso migliore il programma si inchioda immediatamente alla prima esecuzione segnalando un qualsivoglia errore in un punto completamente diverso del codice, chiaro sintomo che abbiamo un puntatore pazzo a spasso per la memoria del computer...
|
|||||||||||
| E non dimentichiamoci le DLL... |
L'uso delle DLL porta un ulteriore livello di complessita' al nostro
problema. Una DLL puo' essere fatta con un qualsiasi linguaggio che sia in grado di creare il file binario del formato giusto (VB, Delphi, Pascal, C, C++), e (di solito), puo' essere usata con un compilatore qualsiasi che sia in grado di creare le giuste chiamate al sistema per mappare la DLL nello spazio di indirizzamento del programma e richiamarne le funzioni. Questo significa che se usate malloc() o free() nella DLL e nel programma, puo' darsi che stiate usando due diverse malloc() e free(). Ora, siccome ogni coppia di malloc()/free() utilizza un suo proprio elenco di blocchi di memoria disponibili per gestire la cosa, se non siete piu' che attenti potete usare la free() sbagliata... cioe' passare un blocco di memoria ad una free() dopo che era stato allocato con un'altra malloc(). Il risultato forse e' peggiore del precedente esempio, perche' prima se non altro avevamo un codice in cui c'era una free() ma nessuna malloc(), ma adesso... magari abbiamo il giusto numero di malloc()/free(). Piu' DLL usate e piu' non le avete scritte voi, piu' aumentano le possibilita' di avere diverse copie di malloc()/free() nel vostro spazio di indirizzi del programma, questo significa che aumentano i problemi. Una (imperfetta) soluzione e' quella di forzare le DLL ed il vostro programma ad usare il runtime del linguaggio come DLL condivisa invece che includere tutto il runtime nel programma stesso, in questo modo la DLL ed il programma condivideranno lo stesso runtime e vi sara' una sola copia di malloc()/free() in memoria, questo ovviamente se la DLL ed il programma li avete fatti voi.
|
|||||||||||
| Evitare il problema |
Come "regola del dito", evitare sempre di fare malloc()/free() in
codice che deve essere messo in una DLL, se proprio dovete farlo,
togliete la malloc()/free() dalla funzione e create una nuova funzione
il cui nome e la cui documentazione specifichino chiaramente che
"la funzione alloca la memoria/disalloca la memoria", e che tali
funzioni devono essere chiamate in coppia quando la DLL viene usata. Cosi' facendo obbligherete l'utente/programmatore a sapere che sta' allocando/disallocando la memoria e gli darete anche una traccia da seguire quando/se il suo programma esplodera' misteriosamente per un bug di questo tipo. Oppure obbligate l'utente a passarvi un blocco di memoria gia' allocato, cioe' scaricate sull'utente il compito di gestirsi la memoria all'esterno della vostra DLL. Un altro sistema e' quello di usare HeapAlloc() per allocare la memoria direttamente dall'heap del programma e HeapFree() per rilasciarla, queste sono API di Windows, quindi sara' Windows a gestirsi il lavoro ed eviteremo i problemi dovuti a molteplici copie di malloc()/free().
|
|||||||||||
| Usare programmi di controllo |
Quando trovo un messaggio come quello menzionato precedentemente di
solito la mia risposta e' qualche cosa del tipo "hai provato un tool
di verifica del codice tipo BoundsChecker ?". Purtroppo, generalmente,
l'autore del messaggio non e' molto disponibile verso questo tipo di
suggerimenti, si limita a ribadire che "se tolgo quella stupida
assegnazione tutto funziona!". Purtroppo, bisogna impararlo sulla propria pelle (nel modo peggiore cioe'), che e' sempre possibile avere un bug nascosto nella gestione della memoria, che e' li' che aspetta che noi aggiungiamo quella "stupidissima assegnazione" cioe' quel tanto che basta a spostare l'immagine della memoria in maniera da "sputtanare" parti importanti del codice invece di memoria inutilizzata. Un tool di controllo consente, nella maggioranza dei casi, di individuare a priori questo tipo di problemi, se non altro consente di "mettere il dito" nel punto giusto del problema, individuando quando la memoria viene sputtanata e da chi. Questo tipo di tool in genere non sono gratuiti, ve ne sono alcuni free, ma la loro utilita' e' molto inferiore a quella dei tool a pagamento. Vi assicuro pero' che questo tipo di tool risparmiano parecchie notti insonni ed in generale valgono la spesa. Questo e' un elenco (senza pretese di completezza) di alcuni tool che sono in grado di rilevare problemi nell'allocazione/gestione della memoria in maniera completamente automatica.
|
|
Comments Max length of comments: 1000 chars. |
1 commento Gianluca (TV) dice il 27/05/2008 15:49: Interessante questo articolo. Complimenti anche per i racconti, alcuni sono davvero esilaranti. Anche se non è bello da dire... grazie di patire per noi tutto questo :-D ah...nel codice sopra riga 21 nuovoStack[0] = newSize; credo che intendessi scrivere nuovoStack[0] = nuovaDim; o sbaglio? il problema di tradurre il codice dall'inglese... Add a comment (max 1000 chars)
|
| Author |
Davide Bianchi,
works as Unix/Linux administrator for a "network security" company of Haarlem. Contacts: mail: davide AT onlyforfun.net , ICQ: 268751033, Jabber: davideyeahsure AT gmail.com Skype: davideyahsure |
| Contribuire | Volete contribuire? Leggete come! |
| Copyright | This site is made by me with blood, sweat and gunpowder, if you want to republish or redistribute any part of it, please drop me (or the author of the article if is not me) a mail. |
This site isn't optimized for vision with any specific browser, nor
it requires special fonts or resolution.
You're free to see it as you wish.
Last Update: 04/12/2008