Un memory leak non è memoria sparita. È memoria che è ancora tua, ancora riservata, e che non hai più modo di restituire, perché l'ultimo puntatore che la raggiungeva non c'è più. Niente va in crash. Il programma continua a girare, un po' più pesante ogni volta, finché prima o poi qualcosa fallisce da qualche parte che non c'entra nulla.
Questa pagina spiega come nascono i leak, la disciplina che ne previene la maggior parte e i due strumenti che trovano il resto.
Che aspetto ha un leak
Ogni iterazione sovrascrive block con un puntatore nuovo. Il blocco precedente è ancora allocato; nessuna variabile ne conserva l'indirizzo; non potrà mai essere liberato. Tre iterazioni perdono dodici kilobyte. Un server che lo fa una volta per richiesta li perde per sempre, al ritmo con cui arrivano le richieste.
La correzione è una riga, free(block); alla fine del corpo, ma la vera abilità è capire dove va messa.
Come nascono i leak
1. Il puntatore perso
Qualsiasi assegnazione a un puntatore che contiene ancora l'unico riferimento a un blocco vivo lo fa perdere.
char *name = malloc(32);
name = malloc(64); /* i primi 32 byte ora sono irraggiungibili */
Il ciclo qui sopra è lo stesso bug travestito da ciclo. Lo è anche riassegnare un campo di una struct, e lo è la scorciatoia con realloc vista in calloc e realloc:
p = realloc(p, n); /* se fallisce: p diventa NULL e il vecchio blocco resta orfano */
2. L'uscita anticipata
Ogni percorso di uscita da una funzione deve rilasciare ciò che la funzione ha già preso. Quello che viene dimenticato è sempre un percorso di errore.
Il percorso normale è corretto e quello di errore perde memoria, ed è per questo che il bug sopravvive ai test: durante lo sviluppo il ramo di errore non viene quasi mai eseguito. La soluzione è un'unica sezione di pulizia a cui salta ogni percorso:
È l'unico uso di goto che i programmatori C esperti raccomandano attivamente. Funziona perché ogni puntatore parte da NULL e free(NULL) non fa nulla, quindi un unico blocco di uscita è corretto indipendentemente da quanto sia andata avanti la funzione.
3. La proprietà poco chiara
I leak più subdoli non sono affatto errori di codice: sono due funzioni in disaccordo su chi dovesse occuparsene.
char *build_message(void); /* chi chiama deve liberarla? */
void store(char *text); /* store ne prende la proprieta'? */
Se build_message restituisce memoria allocata e store la copia, deve liberarla il chiamante. Se store tiene il puntatore, il chiamante non deve farlo. Niente nel codice dice quale delle due, quindi una delle due ipotesi viene fatta due volte, e ottieni un leak oppure una doppia free.
Il rimedio è una convenzione, dichiarata in un commento accanto a ogni funzione che alloca:
/* Restituisce una stringa appena allocata; il chiamante deve liberarla. */
char *build_message(void);
/* Prende la proprieta' di 'text'; verra' liberato da store_free(). */
void store(char *text);
Scrivi la regola accanto alla funzione, non in un documento di progetto. È l'abitudine più preziosa in assoluto nella gestione della memoria in C.
La disciplina della proprietà
Quattro regole coprono quasi tutto:
- Ogni allocazione ha esattamente un proprietario: un pezzo di codice responsabile di liberarla.
- Abbina a ogni funzione che alloca una che rilascia.
vec_init/vec_free,config_load/config_free. La simmetria rende visibile una chiamata mancante. - Libera nello stesso livello che ha allocato, a meno che il commento della funzione non trasferisca esplicitamente la proprietà.
- Imposta un puntatore a
NULLdopo averlo liberato, così un uso accidentale successivo va in crash nel punto del guasto invece di corrompere l'heap in silenzio.
Trovare i leak: valgrind
Su Linux, valgrind non richiede di ricompilare, anche se i simboli di debug rendono leggibile il rapporto:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
Per il ciclo con il leak all'inizio di questa pagina, il rapporto termina più o meno così:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
Leggilo dal fondo. "definitely lost" significa che all'uscita non esisteva alcun puntatore al blocco: un leak vero. Lo stack trace indica la riga della malloc che l'ha creato, non la riga in cui è stato perso, e di solito basta per trovare la free mancante.
Compaiono altre due categorie:
- indirectly lost: blocchi raggiungibili solo tramite un blocco a sua volta perso, come gli elementi di una lista collegata persa. Correggi quello "definitely lost" e questi spariscono.
- still reachable: allocati all'uscita ma con un puntatore ancora vivo, tipicamente una cache globale. Non è un leak nel senso pericoloso, ma conviene liberarli perché il rapporto resti vuoto.
Valgrind intercetta anche letture di memoria non inizializzata e scritture oltre la fine di un blocco, ed è spesso così che scopri il bug dietro al leak.
Trovare i leak: AddressSanitizer
AddressSanitizer è integrato in GCC e Clang, è molto più veloce di valgrind e funziona dove valgrind non arriva (compreso il macOS attuale):
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
Il rapporto sui leak viene stampato automaticamente all'uscita:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan trasforma anche gli use-after-free e gli overflow dei buffer sull'heap in interruzioni immediate e chiaramente etichettate, invece che in misteriose corruzioni successive. Compila le esecuzioni dei test con ASan attivo e le release con ASan disattivo, perché costa memoria e velocità.
Se sulla tua piattaforma il rilevamento dei leak non scatta, imposta ASAN_OPTIONS=detect_leaks=1 nell'ambiente prima dell'esecuzione.
Correggere passo dopo passo un programma che perde memoria
Ecco un piccolo programma con tre leak distinti:
Valgrind segnala tre voci "definitely lost" con tre numeri di riga diversi. Corrette una alla volta:
La correzione 1 è il commento sulla proprietà messo in pratica: shout alloca, main libera. La correzione 2 elimina del tutto la doppia allocazione invece di liberare la prima: il codice più semplice è anche quello corretto. La correzione 3 aggiunge la free mancante sull'uscita anticipata; con più allocazioni in gioco, l'unica etichetta cleanup: vista prima scala meglio che ripetere le free.
Abitudini che prevengono i leak
- Scrivi la
freesubito dopo aver scritto lamalloc, poi riempi il codice in mezzo. - Dai a ogni funzione che alloca una funzione corrispondente che libera.
- Dichiara la proprietà in un commento su ogni funzione che restituisce o riceve un puntatore da lei allocato.
- Usa un unico blocco di uscita
cleanup:nelle funzioni che gestiscono più allocazioni. - Esegui abitualmente i test con
-fsanitize=address, non solo quando qualcosa sembra non andare. - Considera "definitely lost: 0 bytes" parte di un'esecuzione dei test superata.
Domande frequenti
Cos'è un memory leak in C?
Memoria allocata con malloc che non puoi più liberare, perché nel programma niente vi punta più. Il blocco resta riservato per tutta la vita del processo. Non è un crash: il programma continua a funzionare, solo che usa più memoria a ogni passaggio finché alla fine la esaurisce.
Come si trovano i memory leak in C?
Esegui il programma con valgrind: valgrind --leak-check=full ./program. Segnala ogni blocco ancora allocato all'uscita, con lo stack trace della malloc che l'ha creato. Su macOS o dove valgrind non è disponibile, compila con -fsanitize=address e lo stesso rapporto compare all'uscita.
Cosa causa i memory leak in C?
Tre schemi li coprono quasi tutti: sovrascrivere l'unico puntatore a un blocco (compreso p = realloc(p, n) quando fallisce), uscire in anticipo da una funzione che ha già allocato, e una proprietà poco chiara, con due funzioni che danno ciascuna per scontato che sia l'altra a liberare, così non lo fa nessuna.
I memory leak contano se tanto il programma termina?
Per un programma che viene eseguito una volta e termina, il sistema operativo recupera tutto, quindi l'impatto pratico è nullo. Contano per tutto ciò che resta in esecuzione a lungo, come un server, il ciclo di un gioco o un demone, dove un leak per richiesta cresce senza limiti. Libera la memoria comunque in modo coerente: un rapporto pieno di leak innocui nasconde quelli che contano.