Il 'baco' della Malloc


Home Page | Commenti | Articoli | Faq | Documenti | Ricerca | Archivio | Storie dalla Sala Macchine | Contribuire | Imposta lingua:en it | Login/Register


Trovo il seguente messaggio su un NG dedicato alla programmazione in C/C++:

...il programma funzionava perfettamente finche' non ho aggiunto l'istruzione
int i=0;
A questo punto continua a segnalarmi un errore di "access violation", ma non capisco da che puo' dipendere, se rimuovo la dichiarazione sembra tornare tutto a posto...

Chiunque abbia un minimo di esperienza in C/C++ avra' gia' identificato il problema: un bel "puntatore pazzo". Il guaio sta' nel trovare dove e' il maledetto puntatore che sta' sputtanando la memoria del sistema.

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.

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

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

L'uso di DLL o altre librerie condivise 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.

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

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.


I commenti sono aggiunti quando e soprattutto se ho il tempo di guardarli e dopo aver eliminato le cagate, spam, tentativi di phishing et similia. Quindi non trattenete il respiro.

4 messaggi  this document does not accept new posts

Gianluca (TV)

Gianluca (TV) Di Gianluca (TV) postato il 27/05/2008 16: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...


Massimo

Massimo Di Massimo postato il 15/12/2008 14:35

Finora avevo sentito parlare solo di Lint e di Splinter (che sono gratis)... Bounds Checker non lo conoscevo.

Non conoscevo nemmeno il bug delle malloc(), anche se il post immaginario sull'assegnazione che sputtana tutto mi dà una terrificante sensazione di deja-vu :-]


alex

alessiodp-AT-hotmail.it Di alex postato il 27/06/2009 16:39

1. non parlerei di Bug

2. E' chiaramente scritto nella documentazione ANSI C - CPP che non c'e' nessuna verifica automatica dei puntatori...e questo vale per tutti i puntatori compreso quello restituito da malloc();

3. Se un programmatore in C non sa gestire i puntatori puo' usare un linguaggio che ne preveda un utilizzo piu' semplice...

4. dei memory agent segnalati nessuno e' in produzione in questo momento ed ad ogni modo avevano qualche utilita' sui vecchi OS microsoft...

5. la routine di stack proposta oltre che banale é inutile...

-- alex


anonymous

-AT- alex Di anonymous postato il 28/06/2009 11:20

>

> 5. la routine di stack proposta oltre che banale é inutile...

[Non sono un programmatore]

Inutile nel senso che non funziona o nel senso che non ha un uso pratico?

-- anonymous


Precedente Successivo

Davide Bianchi, lavora come Unix/Linux System Administrator presso una societa' di Hosting in Olanda.

Volete contribuire? Leggete come!.
 
 

Il presente sito e' frutto del sudore della mia fronte (e delle mie dita), se siete interessati a ripubblicare uno degli articoli, documenti o qualunque altra cosa presente in questo sito per cortesia datemene comunicazione (o all'autore dell'articolo se non sono io), cosi' il giorno che faccio delle aggiunte potro' avvisarvi e magari mandarvi il testo aggiornato.


Questo sito era composto con VIM, ora e' composto con VIM ed il famosissimo CMS FdT.

Questo sito non e' ottimizzato per la visione con nessun browser particolare, ne' richiede l'uso di font particolari o risoluzioni speciali. Siete liberi di vederlo come vi pare e piace, o come disse qualcuno: "Finalmente uno dei POCHI siti che ancora funzionano con IE5 dentro Windows 3.1".

Web Interoperability Pleadge Support This Project Powered By Gigan