Memory leak em linguagem C

- por Sergio Prado

Categorias: Linguagem C Tags: , , ,

Dependendo do problema que queremos resolver, precisamos de uma quantidade variável de espaço para armazenar alguma informação. Isso pode acontecer, por exemplo, quando estamos trabalhando com protocolos de comunicação onde a quantidade de dados trafegados é variável. Ou então quando trabalhamos com estruturas de dados como listas ligadas, onde elementos desta lista são adicionados/removidos dinamicamente.

A biblioteca C padrão fornece as funções malloc() para alocar memória dinamicamente e free() para desalocar uma região de memória previamente alocada.

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

Estas rotinas usam uma região de memória chamada de heap. É no heap que os bytes são alocados com malloc() e liberados com free(). O tamanho do heap é variável, dependendo de vários fatores como o tamanho da memória, capacidade de endereçamento da CPU, etc. Mas claramente é uma região de memória finita. A cada chamada a malloc(), uma pequena parte desta região de memória é consumida. E se não for liberada, corremos o risco de ficar sem memória.

O uso de malloc() e free() de maneira descontrolada pode causar dois principais problemas:

  1. Fragmentação de memória: A cada malloc(), um pedaço de memória é alocado, e fica indisponível para a aplicação. Se o software abusar das chamadas a malloc(), a memória poderá ficar fragmentada em pequenos pedaços alocados para a aplicação. Depois as rotinas de alocação terão dificuldades de encontrar “pedaços” de memória que atendem às novas chamadas à malloc(). A forma como ocorre esta fragmentação é dependente da biblioteca ou do sistema operacional responsável pela alocação de memória.
  2. Memory leak: Se você alocar regiões de memória com malloc(), mas depois do uso não liberar estas regiões com o free(), temos caracterizado o memory leak, um dos principais pesadelos do desenvolvedor de software embarcado. É um pesadelo porque a memoria começa a ser consumida aos poucos, e a aplicação pode ficar dias sem apresentar algum problema. Mas então num belo dia, quando toda a memória é consumida, coisas estranhas começam a acontecer. A performance da aplicação começa a se degradar, e travamentos ou reboot automaticos irão invariavelmente acontecer.

Nosso foco neste artigo é o memory leak. Vamos analisar então algumas técnicas para tratar e identificar este problema.

Técnica 0: Não usar malloc()

Usar alocação dinamica de memória em sistemas embarcados deve ser seu ultimo recurso. Padrões como o MISRA-C, que tratei em um artigo passado, proibem categoricamente seu uso. Portanto pensem bastante na solução. Depois pensem de novo. E de novo. Até ter certeza de que alocação dinâmica de memória é a única solução para seu problema.

Técnica 1: Code review

Então você usou alocação dinâmica de memória em seu código, e agora precisa garantir a ausência de memory leak na aplicação. E uma das primeiras técnicas a serem aplicadas é o Code Review, ou revisão de código.

A técnica de revisão de código deveria ser aplicada independentemente do uso de malloc(). Nós devemos revisar nossos códigos. E então devemos pedir para outras pessoas revisá-los. E depois revisar o código de outras pessoas. Uma boa parte dos bugs podem ser encontrados com esta técnica.

A idéia aqui é ler o código, identificar todas as chamadas à malloc(), e verificar se existe o free() correspondente. 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 exemplo clássico de memory leak. Esta função recebe uma string, formata em um buffer, e envia pela serial. Como o tamanho da string é variável, o buffer de comunicação é alocado dinamicamente. Veja que na linha 7 o buffer é alocado, e na linha 25 o buffer é liberado. Porém, quando ocorre um erro ao enviar os dados (linha 21), a função retorna sem desalocar o buffer, caracterizando o memory leak. Este erro é muito comum. Quando usamos malloc() devemos nos certificar de que todos os pontos de retorno da função possuem uma chamada a free().

Esta técnica pode ser muito custosa para aplicações com milhares de linhas de código, e isso nos leva ao uso de ferramentas para automatizar esta verificação.

Técnica 2: Uso de ferramentas

Ferramentas são uma parte essencial do processo de desenvolvimento, e não será diferente quando nosso objetivo é analisar um problema de memory leak. Existem vários tipos de ferramentas, algumas que fazem análise estática de código (analisa diretamente o código-fonte) e outras que fazem verificação dinâmica (em tempo de execução).

Para testar algumas ferramentas, vamos complementar a função txData() com uma função main(), e forçar um memory 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 testar com uma ferramenta de analise estática de código, o SPLINT, já tratado anteriormente 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 ferramenta encontrou facilmente o memory leak que inserimos no código. O interessante de uma ferramenta de análise estática é que, como a analisa é feita nos fontes da aplicação, ela pode ser usada em qualquer código-fonte, independente da arquitetura ou do sistema operacional que estamos usando, desde que esteja no padrão ANSI C.

Já o uso de ferramentas de análise dinâmica podem pegar casos específicos que passariam despercebidos quando usamos ferramentas de análise estática. Por outro lado, estas ferramentas são dependentes de determinado sistema operacional, ou então exigem que você compile seu código com uma biblioteca específica. Isso porque estas ferramentas adicionam uma camada extra na sua aplicação, monitorando cada chamada a malloc() e free(), para depois gerar um relatório que pode indicar um memory leak quando a quantidade de chamadas a malloc() for diferente da quantidade de chamadas a free() por exemplo.

Uma destas ferramentas, muito utilizada para detectar memory leak em aplicações para sistemas Unix, é o Valgring. Esta ferramenta de debug, dentre outras funcionalidades, monitora o uso de memória pela aplicação em tempo de execução. Vamos testar o comportamento desta ferramenta 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 executar a aplicação ela imprime um relatório do uso de memória, indicando a ocorrencia do memory leak.

Se você quiser, dê uma olhada também na biblioteca dmalloc. Ela adiciona uma camada adicional na sua aplicação, possibilitando o debug de alocação de memória em tempo de execução. Esta biblioteca foi escrita em ANSI C e pode ser facilmente portada para qualquer arquitetura.

Técnica 3: Desenvolva sua própria biblioteca

Se nenhuma solução disponível lhe servir, sempre existe a possibilidade de você colocar a mão na massa e desenvolver suas próprias rotinas de alocação de memória. Reinventar a roda é sempre mais trabalhoso e custoso, mas se você se sentir aventureiro, pode analisar esta possibilidade.

Normalmente, só damos atenção a um problema quando o vivenciamos. Sempre achamos que não vai acontecer conosco. E quando acontece, temos que lidar com as consequencias e aprender com os erros. Mas melhor do que aprender com seus próprios erros, é aprender com os erros dos outros. Portanto, seja mais proativo. memory leak é igual a pressão alta, uma doença silenciosa, mas que quando apresenta os sintomas, estes provavelmente serão fatais!

Um abraço,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.