Memory leak em linguagem C

Em 29/07/2010, em Linguagem C, por sergioprado
Neste artigo vamos anal­isar algu­mas téc­ni­cas e fer­ra­men­tas de iden­ti­fi­cação de mem­ory leak em soft­ware para sis­temas embarcados.
Com­par­tilhe!
  • Twitter
  • Facebook
  • LinkedIn
  • del.icio.us
  • Digg
  • email
  • PDF
  • Print

Depen­dendo do prob­lema que quer­e­mos resolver, pre­cisamos de uma quan­ti­dade var­iável de espaço para armazenar alguma infor­mação. Isso pode acon­te­cer, por exem­plo, quando esta­mos tra­bal­hando com pro­to­co­los de comu­ni­cação onde a quan­ti­dade de dados trafe­ga­dos é var­iável. Ou então quando tra­bal­hamos com estru­turas de dados como lis­tas lig­adas, onde ele­men­tos desta lista são adicionados/removidos dinamicamente.

A bib­lioteca C padrão fornece as funções mal­loc() para alo­car memória dinami­ca­mente e free() para desa­lo­car uma região de memória pre­vi­a­mente alocada.

1
2
void *malloc(size_t size);
void free(void *ptr);

Estas roti­nas usam uma região de memória chamada de heap. É no heap que os bytes são alo­ca­dos com mal­loc() e lib­er­a­dos com free(). O tamanho do heap é var­iável, depen­dendo de vários fatores como o tamanho da memória, capaci­dade de endereça­mento da CPU, etc. Mas clara­mente é uma região de memória finita. A cada chamada a mal­loc(), uma pequena parte desta região de memória é con­sum­ida. E se não for lib­er­ada, cor­re­mos o risco de ficar sem memória.

O uso de mal­loc() e free() de maneira descon­tro­lada pode causar dois prin­ci­pais problemas:

  1. Frag­men­tação de memória: A cada mal­loc(), um pedaço de memória é alo­cado, e fica indisponível para a apli­cação. Se o soft­ware abusar das chamadas a mal­loc(), a memória poderá ficar frag­men­tada em pequenos pedaços alo­ca­dos para a apli­cação. Depois as roti­nas de alo­cação terão difi­cul­dades de encon­trar “pedaços” de memória que aten­dem às novas chamadas à mal­loc(). A forma como ocorre esta frag­men­tação é depen­dente da bib­lioteca ou do sis­tema opera­cional respon­sável pela alo­cação de memória.
  2. Mem­ory leak: Se você alo­car regiões de memória com mal­loc(), mas depois do uso não lib­erar estas regiões com o free(), temos car­ac­ter­i­zado o mem­ory leak, um dos prin­ci­pais pesade­los do desen­volve­dor de soft­ware embar­cado. É um pesadelo porque a memo­ria começa a ser con­sum­ida aos poucos, e a apli­cação pode ficar dias sem apre­sen­tar algum prob­lema. Mas então num belo dia, quando toda a memória é con­sum­ida, coisas estra­nhas começam a acon­te­cer. A per­for­mance da apli­cação começa a se degradar, e trava­men­tos ou reboot auto­mati­cos irão invari­avel­mente acontecer.

Nosso foco neste artigo é o mem­ory leak. Vamos anal­isar então algu­mas téc­ni­cas para tratar e iden­ti­ficar este problema.

Téc­nica 0: Não usar malloc()

Usar alo­cação dinam­ica de memória em sis­temas embar­ca­dos deve ser seu ultimo recurso. Padrões como o MISRA-C, que tratei em um artigo pas­sado, proibem cat­e­gori­ca­mente seu uso. Por­tanto pensem bas­tante na solução. Depois pensem de novo. E de novo. Até ter certeza de que alo­cação dinâmica de memória é a única solução para seu problema.

Téc­nica 1: Code review

Então você usou alo­cação dinâmica de memória em seu código, e agora pre­cisa garan­tir a ausên­cia de mem­ory leak na apli­cação. E uma das primeiras téc­ni­cas a serem apli­cadas é o Code Review, ou revisão de código.

A téc­nica de revisão de código dev­e­ria ser apli­cada inde­pen­den­te­mente do uso de mal­loc(). Nós deve­mos revisar nos­sos códi­gos. E então deve­mos pedir para out­ras pes­soas revisá-los. E depois revisar o código de out­ras pes­soas. Uma boa parte dos bugs podem ser encon­tra­dos com esta técnica.

A idéia aqui é ler o código, iden­ti­ficar todas as chamadas à mal­loc(), e ver­i­ficar se existe o free() cor­re­spon­dente. Dê uma olhada na função abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int txData(char *data)
{
    unsigned char *ptr = NULL, *buf = NULL;
    int size = strlen(data);
 
    /* allocate buffer */
    if ((ptr = buf = (unsigned char*)malloc(size + 3)) == NULL)
        return(-1);
 
    /* STX */
    *ptr++ = STX;
 
    /* data */
    strncpy(ptr, data, size);
    ptr += size;
 
    /* ETX */
    *ptr++ = ETX;
 
    /* send data */
    if (txSerial(buf, size + 3) == -1)
        return(-2);
 
    /* free buffer */
    free(buf);
 
    return 0;
}

Temos aqui um exem­plo clás­sico de mem­ory leak. Esta função recebe uma string, for­mata em um buffer, e envia pela ser­ial. Como o tamanho da string é var­iável, o buffer de comu­ni­cação é alo­cado dinami­ca­mente. Veja que na linha 7 o buffer é alo­cado, e na linha 25 o buffer é lib­er­ado. Porém, quando ocorre um erro ao enviar os dados (linha 21), a função retorna sem desa­lo­car o buffer, car­ac­ter­i­zando o mem­ory leak. Este erro é muito comum. Quando usamos mal­loc() deve­mos nos cer­ti­ficar de que todos os pon­tos de retorno da função pos­suem uma chamada a free().

Esta téc­nica pode ser muito cus­tosa para apli­cações com mil­hares de lin­has de código, e isso nos leva ao uso de fer­ra­men­tas para autom­a­ti­zar esta verificação.

Téc­nica 2: Uso de ferramentas

Fer­ra­men­tas são uma parte essen­cial do processo de desen­volvi­mento, e não será difer­ente quando nosso obje­tivo é anal­isar um prob­lema de mem­ory leak. Exis­tem vários tipos de fer­ra­men­tas, algu­mas que fazem análise estática de código (anal­isa dire­ta­mente o código-fonte) e out­ras que fazem ver­i­fi­cação dinâmica (em tempo de execução).

Para tes­tar algu­mas fer­ra­men­tas, vamos com­ple­men­tar a função txData() com uma função main(), e forçar um mem­ory leak. Nosso código final ficou assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* leak.c */
#include "stdio.h"
#include "string.h"
#include "malloc.h"
 
#define STX 0x02
#define ETX 0x03
 
int txSerial(unsigned char *buffer, int size)
{
    return -1;
}
 
 
int txData(char *data)
{
    unsigned char *ptr = NULL, *buf = NULL;
    int size = strlen(data);
 
    /* allocate buffer */
    if ((ptr = buf = (unsigned char*)malloc(size + 3)) == NULL)
        return(-1);
 
    /* STX */
    *ptr++ = STX;
 
    /* data */
    strncpy(ptr, data, size);
    ptr += size;
 
    /* ETX */
    *ptr++ = ETX;
 
    /* send data */
    if (txSerial(buf, size + 3) == -1)
        return(-2);
 
    /* free buffer */
    free(buf);
 
    return 0;
}
 
void main()
{
    char *msg = "MemoryLeak";
 
    if (!txData(msg))
        printf("Mensagem enviada com sucesso!\n");
    else
        printf("Erro ao enviar mensagem!\n");
}

Primeiro vamos tes­tar com uma fer­ra­menta de analise estática de código, o SPLINT, já tratado ante­ri­or­mente no meu artigo aqui.

$ splint leak.c
Splint 3.1.2 --- 03 May 2009
....
leak.c:37:20: Owned storage ptr not released before return
  A memory leak has been detected. Only-qualified storage is not released
  before the last reference to it is lost. (Use -mustfreeonly to inhibit
  warning)
   leak.c:22:10: Storage ptr becomes owned
....

Voilá! A fer­ra­menta encon­trou facil­mente o mem­ory leak que inse­r­i­mos no código. O inter­es­sante de uma fer­ra­menta de análise estática é que, como a anal­isa é feita nos fontes da apli­cação, ela pode ser usada em qual­quer código-fonte, inde­pen­dente da arquite­tura ou do sis­tema opera­cional que esta­mos usando, desde que esteja no padrão ANSI C.

Já o uso de fer­ra­men­tas de análise dinâmica podem pegar casos especí­fi­cos que pas­sariam des­perce­bidos quando usamos fer­ra­men­tas de análise estática. Por outro lado, estas fer­ra­men­tas são depen­dentes de deter­mi­nado sis­tema opera­cional, ou então exigem que você com­pile seu código com uma bib­lioteca especí­fica. Isso porque estas fer­ra­men­tas adi­cionam uma camada extra na sua apli­cação, mon­i­torando cada chamada a mal­loc() e free(), para depois gerar um relatório que pode indicar um mem­ory leak quando a quan­ti­dade de chamadas a mal­loc() for difer­ente da quan­ti­dade de chamadas a free() por exemplo.

Uma destas fer­ra­men­tas, muito uti­lizada para detec­tar mem­ory leak em apli­cações para sis­temas Unix, é o Val­gring. Esta fer­ra­menta de debug, den­tre out­ras fun­cional­i­dades, mon­i­tora o uso de memória pela apli­cação em tempo de exe­cução. Vamos tes­tar o com­por­ta­mento desta fer­ra­menta com a nossa aplicação.

$ valgrind ./leak
==3811== Memcheck, a memory error detector
==3811== Copyright (C) 2002-2009, and GNU GPL, by Julian Seward et al.
==3811== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==3811== Command: ./leak
==3811== 
Erro ao enviar mensagem!
==3811== 
==3811== HEAP SUMMARY:
==3811==     in use at exit: 13 bytes in 1 blocks
==3811==   total heap usage: 1 allocs, 0 frees, 13 bytes allocated
==3811== 
==3811== LEAK SUMMARY:
==3811==    definitely lost: 13 bytes in 1 blocks
==3811==    indirectly lost: 0 bytes in 0 blocks
==3811==      possibly lost: 0 bytes in 0 blocks
==3811==    still reachable: 0 bytes in 0 blocks
==3811==         suppressed: 0 bytes in 0 blocks
==3811== Rerun with --leak-check=full to see details of leaked memory
==3811== 
==3811== For counts of detected and suppressed errors, rerun with: -v
==3811== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 13 from 8)

Veja que após exe­cu­tar a apli­cação ela imprime um relatório do uso de memória, indi­cando a ocor­ren­cia do mem­ory leak.

Se você quiser, dê uma olhada tam­bém na bib­lioteca dmal­loc. Ela adi­ciona uma camada adi­cional na sua apli­cação, pos­si­bil­i­tando o debug de alo­cação de memória em tempo de exe­cução. Esta bib­lioteca foi escrita em ANSI C e pode ser facil­mente por­tada para qual­quer arquitetura.

Téc­nica 3: Desen­volva sua própria biblioteca

Se nen­huma solução disponível lhe servir, sem­pre existe a pos­si­bil­i­dade de você colo­car a mão na massa e desen­volver suas próprias roti­nas de alo­cação de memória. Rein­ven­tar a roda é sem­pre mais tra­bal­hoso e cus­toso, mas se você se sen­tir aven­tureiro, pode anal­isar esta pos­si­bil­i­dade. Dê uma olhada neste artigo aqui.

Nor­mal­mente, só damos atenção a um prob­lema quando o viven­ci­amos. Sem­pre achamos que não vai acon­te­cer conosco. E quando acon­tece, temos que lidar com as con­se­quen­cias e apren­der com os erros. Mas mel­hor do que apren­der com seus próprios erros, é apren­der com os erros dos out­ros. Por­tanto, seja mais proa­t­ivo. mem­ory leak é igual a pressão alta, uma doença silen­ciosa, mas que quando apre­senta os sin­tomas, estes provavel­mente serão fatais!

Um abraço,

Ser­gio Prado

VN:F [1.9.0_1079]
Rat­ing: 7.0/10 (4 votes cast)
Mem­ory leak em lin­guagem C, 7.0 out of 10 based on 4 rat­ings
Com­par­tilhe!
  • Twitter
  • Facebook
  • LinkedIn
  • del.icio.us
  • Digg
  • email
  • PDF
  • Print

Posts rela­ciona­dos:

  1. Trata­mento de erros em Lin­guagem C
  2. Otimiza­ção de código em Lin­guagem C — Parte 2
  3. Otimiza­ção de código em Lin­guagem C — Parte 1
Tags:  

5 Respostas para “Memory leak em linguagem C”

  1. Waldemar Waldemar disse:

    Ola Ser­gio,
    Parabens pelo artigo. Muito rel­e­vante e critico em todas as apli­ca­coes.
    Ha cer­tas areas em que o desen­volve­dor deve tomar pre­cau­cao redo­brada, e segu­ra­mente esta e uma delas — extrema­mente crit­ica e del­i­cada.
    Seguindo suas ori­en­ta­coes o profis­sional tera muito mais sucesso e tran­quil­i­dade em rela­cao a qual­i­dade dos tra­bal­hos exe­cu­ta­dos.
    Parabens,
    Waldemar.

  2. […] This post was men­tioned on Twit­ter by ΔlβertΦFF FαbiαnΦN, Ser­gio Prado. Ser­gio Prado said: #blogsprado: Mem­ory leak em lin­guagem C http://bit.ly/aRkDMt […]

  3. Rimack Rimack disse:

    Parabéns pelo tópico

  4. Rafael Rafael disse:

    Valeu, deu umas dicas mesmo…

  5. Oswaldo F. Filho Oswaldo F. Filho disse:

    Ser­gio,

    Com desen­volvo soft­ware embar­cado usando como SO prin­ci­pal o Linux, na minha opinião a revisão de path ger­a­dos nos soft­wares desen­volvi­dos é fundamental .

    As fer­ra­men­tas citadas acima são bas­tante utéis também.

    Abraço!

Deixe um comentário