Resposta ao Desafio da Semana #1 [Memory Leak - C/C++]


 

Por: Roberto A. Farah

Um cliente contactou você porque uma aplicação C aparenta estar consumindo memória sem nunca liberá-la. Seu objetivo é de certificar que o sintoma é esse mesmo, e, em seguida, identificar o problema causando esse sintoma, propor uma solução (solução prática que exija o mínimo de alterações na aplicação) e descrever as possíveis estratégias para se isolar esse problema.

Nota: Imagine que você tenha que descrever um modo de se identificar o problema de memória que seja válido para essa simples aplicação e para uma grande aplicação com milhares de linhas de código. Esse é o ponto mais importante desse desafio.

SINTOMA

Definitivamente temos um memory leak ocorrendo.

PROBLEMA

A memória dinamicamente alocada nunca é liberada pela aplicação.

A aplicação usa malloc() para alocar memória e não usa free() para liberá-la.

SOLUÇÃO

Onde há:

// Libera memoria

pbyAllocated = NULL;

Colocar:

// Libera memoria

free(pbyAllocated);

pbyAllocated = NULL;

ESTRATÉGIA

Muitas são as abordagens possíveis para se isolar o problema, digo, para se comprovar se a aplicação tem um handle ou memory leak. Eis algumas delas:

a) Revise o código. Sim, embora óbvio, para uma aplicação pequena como essa é talvez a melhor maneira de se identificar onde está o problema numa aplicação pequena. Obviamente não recomendado numa aplicação muito grande ou com muitos módulos.

b) Use o Performance Monitor. Para isso rode a aplicação e aguarde antes de pressionar uma tecla para começar. Em seguida selecione o objeto Process e a instancia da aplicação MemoryLeak.exe.

 

Em counters acima, selecione:

Private Bytes, Virtual Byes e Handle Count.

Essa estratégia é ótima para se identificar potenciais vazamentos de handle ou memória.

Em seguida, pressione uma tecla na janela da aplicação e observe. Você deveria obter algo como:

  

Note que Handle Count, que é o contador de handles usados pela aplicação, está constante, o que sugere que não há um handle leak. Se o valor estivesse aumentando, juntamente com os outros dois contadores, nos indicaria que há um handle leak. Isso ocorre, por exemplo quando você usa chamadas de API que retornam um handle e, ao final, não fecha o handle.

Observe que Private Bytes aumenta continuamente. Private Bytes, refere-se ao número de bytes alocados pela aplicação e de uso exclusivo da aplicação.

Virtual Bytes é o tamanho da memória virtual (virtual address space) que o processo está usando. O default é 2 GB.

Portanto, os contadores mostram que a aplicação não libera memória enquanto executando e que durante a execução o consumo de memória aumenta gradativamente.

Para se isolar problemas de memória em aplicações .NET é necessário se usar os contadores de memória do .NET além desses.

Também é possível se usar outros contadores para uma análise mais apurada da memória, mas, regra geral, os contadores acima serão suficientes na grande maioria dos casos. A próxima vez que você desconfiar de algum memory leak, use essa abordagem, será útil! É ótimo quando você não tem acesso ao código fonte ou quando você precisa primeiro identificar qual a aplicação causando handle/memory leak, para, depois verificar o código!

c) Outra estratégia possível seria utilizar ferramentas como UMDH (https://support.microsoft.com/kb/268343/en-us ), Gflags que vem no Debugging Tools For Windows (https://www.microsoft.com/whdc/devtools/debugging/default.mspx ), PageHeap (https://support.microsoft.com/kb/286470/en-us ) ou DebugDiag em modo de Memory e Handle Leak ( https://www.debugdiag.com )

Essas ferramentas alteram algumas chaves de registry para poder mapear alocações de memória. São úteis em casos de Heap Corruption (corrupção de memória de heap) ou Memory Leak (vazamentos de memória).

Eis os resultados de se usar DebugDiag para se isolar esse problema:

 

Após carregar a aplicação MemoryLeak.exe:

 

 

 

 

 

 

Isso iniciaria o DebugDiag para analisar por 15 minutos a aplicação.

O DebugDiag vai gerar um dump também, portanto, será possível se ter muita riqueza de detalhes sobre o handle/memory leak após a análise.

O mesmo pode ser feito com GFLAGS e UMDH. E esse artigo explica muito bem: https://support.microsoft.com/kb/268343/en-us

Essas ferramentas registrarão todas as chamadas de alocação de heap do processo, incluindo a pilha de chamada para cada alocação, número de alocações, etc...

Para usar o UMDH é necessário se instalar o Debugging Tools for Windows, cujo link está mais acima.

UMDH vai gerar arquivos texto com a pilha de alocação, portanto, será possível ver o ponto onde a memória está sendo alocada. Ou seja, além de saber se há um leak ou não você poderá saber qual a rotina causando o leak!

Para poupar espaço omiti as saídas.

d) Finalmente, a abordagem que considero a melhor para os desenvolvedores, pois tem o benefício de ser proativa para se achar memory leaks, ou seja, ao se testar a versão Debug da aplicação, sempre que houver um Leak ele será mostrado no Visual C++, também apontando o local que está sendo responsável por vazar memória. Portanto, se o desenvolvimento é efetuado usando essa abordagem os leaks serão detectados nos testes efetuados em desenvolvimento!

Essa abordagem usa a versão Debug do C runtime, o #include <crtdbg.h>, _CrtSetDbgFlag() e _CrtDumpMemoryLeaks().

O artigo de referência é:

https://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsdebug/html/vxconEnablingMemoryLeakDetection.asp

Eis a saída esperada ao executar a versão Debug no Visual C++:

Detected memory leaks!

Dumping objects ->

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {150} normal block at 0x025075E8, 490000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {149} normal block at 0x02490CF8, 480000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {148} normal block at 0x02402408, 470000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {147} normal block at 0x02390B18, 460000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {146} normal block at 0x022FD228, 450000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {145} normal block at 0x02290938, 440000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {144} normal block at 0x021F8048, 430000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {143} normal block at 0x02190758, 420000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {142} normal block at 0x020F3E68, 410000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {141} normal block at 0x02090578, 400000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {140} normal block at 0x01FEEC88, 390000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {139} normal block at 0x01F90398, 380000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {138} normal block at 0x01EE9AA8, 370000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {137} normal block at 0x01E901B8, 360000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {136} normal block at 0x01DE58C8, 350000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {135} normal block at 0x01D90FD8, 340000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {134} normal block at 0x01D2D6E8, 330000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {133} normal block at 0x01CDDDF8, 320000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {132} normal block at 0x01C90508, 310000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {131} normal block at 0x01C1EC18, 300000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {130} normal block at 0x01BD6328, 290000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {129} normal block at 0x01B90A38, 280000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {128} normal block at 0x01B4C148, 270000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {127} normal block at 0x01B0B858, 260000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {126} normal block at 0x01ACCF68, 250000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {125} normal block at 0x01A90678, 240000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {124} normal block at 0x01A2ED88, 230000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {123} normal block at 0x019F7498, 220000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {122} normal block at 0x019C2BA8, 210000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {121} normal block at 0x019902B8, 200000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {120} normal block at 0x019379C8, 190000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {119} normal block at 0x0190A0D8, 180000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {118} normal block at 0x018DF7E8, 170000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {117} normal block at 0x018B6EF8, 160000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {116} normal block at 0x01890608, 150000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {115} normal block at 0x01855D18, 140000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {114} normal block at 0x01834428, 130000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {113} normal block at 0x01815B38, 120000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {112} normal block at 0x017F9248, 110000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {111} normal block at 0x017DF958, 100000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {110} normal block at 0x017C8068, 90000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {109} normal block at 0x017B3778, 80000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {108} normal block at 0x017A0E88, 70000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {107} normal block at 0x01790598, 60000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {106} normal block at 0x01772CA8, 50000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {105} normal block at 0x017673B8, 40000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {104} normal block at 0x0175EAC8, 30000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {103} normal block at 0x017581D8, 20000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {102} normal block at 0x017548E8, 10000 bytes long.

 Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

c:\development\my tools\blog articles\article #5\memoryleak\memoryleak\memoryleak.cpp(44) : {101} normal block at 0x01752FF8, 0 bytes long.

 Data: <> Pü_

Object dump complete.

E agora o código fonte alterado para revelar memory leaks em versão Debug. Em vermelho as alterações:

// MemoryLeak.cpp : Defines the entry point for the console application.

//

#define _CRTDBG_MAP_ALLOC // Antes dos includes!

#include "stdafx.h"

#include <windows.h>

#include <stdlib.h>

#include <string.h>

#include <stdio.h>

#include <conio.h>

#include <process.h>

#include <crtdbg.h>

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// SINTOMAS: Vazamento de memoria. (memory leak)

//

// OBJETIVO: Isolar o problema causando o memory leak e corrigir a aplicacao.

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

int _tmain(int argc, _TCHAR* argv[])

{

       

        _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF |

                                _CRTDBG_CHECK_ALWAYS_DF |

                                _CRTDBG_DELAY_FREE_MEM_DF |

                                _CRTDBG_LEAK_CHECK_DF);

    _tprintf(_T("Memory Leak Demo\n"));

        _tprintf(_T("Pressione alguma tecla para iniciar...\n\n"));

        _getch();

    for(int i = 0; i < 50; i++)

    {

                BYTE* pbyAllocated = (BYTE*) malloc (sizeof(BYTE) * (10000 * i));

                if(pbyAllocated)

                {

                        _tprintf(_T("Memoria alocada com sucesso!\n"));

                }

                else

                {

                        _tprintf(_T("Alocacao de memoria falhou...\n"));

                }

                // Libera memoria

                pbyAllocated = NULL;

                Sleep(1000);

    }

        _tprintf(_T("Pressione alguma tecla para finalizar...\n"));

        _CrtDumpMemoryLeaks();

        _getch();

        return 0;

}

Nota: Para C++ basta declarar a macro abaixo para substituir o operador new em versão Debug:

#ifdef _DEBUG

#define DEBUG_CLIENTBLOCK new( _CLIENT_BLOCK, __FILE__, __LINE__)

#else

#define DEBUG_CLIENTBLOCK

#endif // _DEBUG

CONCLUSÃO

Usando o Performance Monitor você pode facilmente identificar potenciais memory ou handle leaks. Uso a palavra potencial aqui porque um incremento constante de memória pode não ser exatamente um memory leak se a memória é liberada em determinado momento ( obviamente não o momento que a aplicação cai por consequência do leak J ). O Performance Monitor é usado para identificar sintomas de alta CPU também, entre outros que mostrarei em novos artigos.

Entretanto, após comprovar se há um leak você terá que usar outra abordagem para saber de onde vêm o memory leak. Seja usando o DebugDiag, instrumentando o código, etc...

Para se identificar as chamadas alocando memória pode-se usar ferramentas que através do Registry mapeiam alocações dinâmicas (usamos muito aqui) como o DebugDiag em modo de Handle/Memory Leak por exemplo ou instrumentar o código fonte com as macros e funções do DCRT – Debug C Runtime. Minha recomendação é de se instrumentar o código quando você sabe qual é a aplicação ocasionando memory leak e tem acesso ao código fonte, do contrário, usar o DebugDiag e outras ferramentas é mais trabalhoso mas útil para se saber de onde vêm as alocações de memória que deverão ser filtradas para se saber quais estão alocando memória e nunca liberando, se você não tem o código fonte.

Frequentemente combinamos as abordagens em situações reais. Por exemplo, num primeiro instante usamos o Performance Monitor (ou outra ferramenta) para identificar qual processo é o suspeito de vazamento de handle ou memória, para, em seguida, usarmos outra abordagem para se identificar de onde, no código fonte, vem o leak.

Para mais informações sobre o uso das rotinas do DCRT e das ferramentas de troubleshooting mencionadas acima consultem:

https://support.microsoft.com

https://msdn.microsoft.com

Agora o próximo problema...