Vazamento de Memória

por Fabio A. Mazzarino

Já trabalhei em várias equipes, algumas tinham um cuidado extremo com a gestão da alocação de memória, outras não se preocupavam tanto assim. Independente da atitude da equipe é importante saber como evitar vazamentos, e quais as consequências.

Para quem não sabe o que é vazamento de memória: é a manutenção de memória alocada dinamicamente após seu uso de forma que não possa ser recuperada, utilizada ou desalocada. O mesmo pode-se dizer de outros recursos. O programa funciona perfeitamente, porém em algum momento a memória acaba, e aí começam os problemas.

Quando o vazamento ocorre de maneira única e isolada, o problema é pequeno. Mas quando o vazamento ocorre dentro de loops, e principalmente em sistemas que necessitam manter-se funcionando continuamente por períodos de tempo, passam a ser significativos, causando problemas de falta de memória, crashes e indisponibilidades.

Começando Simples e Inofensivo

A seguir um exemplo bem simples de vazamento:

int main(int argc, char *argv[]) {
    if (argc < 1) 
        exit(1);
    char *name = malloc(strlen(argv[1]) + 5);
    strcpy(name, argv[1]);
    if (!strcmp(name, "tmp") {
        printf("%s\n", name);
        exit(1);
    }
    strcat(name, ".tmp");
    printf("%s\n", name);
    free(name);
}

O vazamento nesse código não é significativo, porque ocorre somente uma vez, e logo o programa é finalizado, fazendo com que o sistema desaloque toda a memória alocada. Mas vamos ver um exemplo mais destrutivo.

E Assim Começam os Problemas

#include <Serial.h>
void setup() {
    Serial.begin(9600);
}
void loop() {
    byte *buffer = NULL;
    unsigned total = Serial.available()
    if (!total)
        return;

    buffer = malloc(total + 5);
    Serial.readBytes(buffer, total);
    buffer[total] = '\x0';
    if (!strcmp(buffer, "tmp"))
        return;
    strcat(buffer, ".tmp");
    Serial.write(buffer);
}

Acima um trecho de código para Arduino. A função setup é executada um única vez, a função loop é executada repetidamente pelo Arduino. Note que uma falha simples vai vazar 16 bytes várias vezes por segundo, em um sistema com a memória bem restrita isso vai causar um grande problema muito rapidamente. Afetando diretamente a disponibilidade e estabilidade do sistema.

Este é o grande problema do vazamento de memória. Em programas experimentais, em ambiente de desenvolvimento, e até mesmo de testes, o vazamento de memória não se exibe. O problema vai aparecer em produção, de forma difícil de reproduzir, depois de muito tempo de execução, da pior forma possível.

Processamento batch de um volume grande de dados é um dos principais afetados pelo vazamento de memória. O consumo de memória vai aumentando proporcionalmente com o volume de dados, até não ser mais possível processar o volume necessário. Em serviços ou daemons o tempo médio entre falhas vai reduzindo conforme o volume de dados processado.

É Mais Comum que Se Pensa

A maioria dos vazamentos de memória ocorre durante a utilização dos comandos de rompimento do fluxo de execução, como por exemplo return, break, continue e goto. Como nos dois casos anteriores.

É comum um desenvolvedor fazer uma trecho de código que não vaze memória, e mais tarde um outro desenvolvedor faz um tratamento de erro com um return, ou um break, e acaba por criar um vazamento de memória. Por isso a importância de ferramentas de análise estática de código, um erro desse é facilmente detectado por essas ferramentas.

Funções que Alocam Memória

Outra forma muito comum de vazamento de memória são funções que alocam memória sem avisar apropriadamente. O trecho a seguir é equivalente a um código que já encontrei profissionalmente.

int buildEntityName(Entity *entity, char* &name) {
    name = (char*)malloc(64);
    *name = '\x0';
    strcat(name, entity->number);
    strcat(name, entity->instance);
    strcat(name, entity->version);
    strcat(name, entity->revision);
    return 1;
}
int sendEntityName(Entity *entity, System *to) {
    char *entityname = (char*)malloc(64);
    buildEntityName(entity, name);
    to->send(name);
    free(entityname);
    return 1;
}

Apesar do vazamento ser um só existem duas situações de risco. A primeira é o código da função buildEntityName: preferencialmente uma função não deve alocar memória, salvo quando for essencial, seu nome deixe claro que há uma alocação. Se ainda assim for necessário alocar memória, o mais apropriado é documentar devidamente.

Solução #1 – não alocar memória

int buildEntityName(Entity *entity, char *name, unsigned maxsize) {
    if (maxsize < 64)
        return 0;
    strcat(name, entity.number);
    strcat(name, entity.instance);
    strcat(name, entity.version);
    strcat(name, entity.revision);
    return 1;
}

Solução #2 – documentar alocação

/* ATENCAO - FUNCAO QUE ALOCA NOVA ENTIDADE - DESALOCAR UTILIZANDO deleteEntity */
Entity* newEntity() {
    return malloc(sizeof(Entity))
}
void deleteEntity(Entity *entity) {
    free(entity);
}

Outro problema no código é a alocação desnecessária. Sempre que possível prefira alocar arrays, ao invés de utilizar alocação dinâmica com malloc ou new:

int sendEntityName(Entity *entity, System *to) {
    char entityname[64] = "";
    buildEntityName(entity, entityname, 64);
    to->send(entityname);
}

Atenção à Documentação

Um outro problema que encontrei em outra equipe é a falta de atenção à documentação. No documento das funções dizia claramente que a mensagem da estrutura de erro necessitava ser desalocada utilizando delete[] em caso de falha. Mas a estrutura de erro era sempre utilizada da seguinte forma.

Error err;
err = exec();
if (err.code) {
    std::cout << "Nao foi possivel executar exec(): " << err.msg << std::endl;
    std::cout << "Tentando execFailsafe()" << std::endl;

    err = execFailsafe();
    if (err.code) {
        std::cout << "Nao foi possivel executar execFailsafe(): " << err.msg << std::endl;
        std::cout << "Abortando" << std::endl;
        return false;
    }
}

Note que a cada erro existe um vazamento de memória, equivalente ao tamanho da mensagem de erro. Uma solução bem simples é seguir a recomendação da documentação:

Error err;
err = exec();
if (err.code) {
    std::cout << "Nao foi possivel executar exec1(): " << err.msg << std::endl;
    std::cout << "Executando execFailsafe()" << std::endl;
    delete[] err.msg;

    erro = execFailsafe();
    if (err.code) {
        std::cout << "Nao foi possivel executar execFailsafe(): " << err.msg << std::endl;
        std::cout << "Abortando" << std::endl;
        delete[] err.msg;
        return false;
    }
}

Como Evitar

São duas principais ferramentas para evitar vazamento de memória. A primeira é a análise estática de código, aquela executada antes do código ser executada, preferencialmente utilizando ferramentas com lint ou cppcheck. Ou mesmo programação em pares, ou revisão de código (peer review).

A segunda é a análise dinâmica de alocação, através da utilização de macros ou funções para alocação de memória que mapeiam em tempo de execução cada alocação e desalocação, garantindo ao final da execução se houve ou não vazamento, e qual alocação gerou o vazamento.