|
No tutorial sobre problema de memória vimos os dois problemas mais comuns, memory leaks e dangling pointers. Agora neste tutorial vou mostrar como criar um código que pode ser usado na detecção de memory leaks.
Existem diversas ferramentas para detecção de memory leaks (na maioria pagas) e diversas maneiras de se realizar isso, objetivo desse tutorial não é competir com estas, mas apenas demostrar um jeito simples de se detectar os leaks e ao mesmo tempo mostrar algumas funcionalidades do C++.
Estratégia de Detecção
Este método consiste em sobrecarregar os operadores new e delete do C++ e a cada invocação do new armazenar em uma lista (na verdade um std::map) a alocação, nas chamadas do delete esse registro é removido da lista. No final da execução do programa se sobrar alguma coisa na lista isso nos indica que faltou invocar o delete.
O programa no final da execução dispara a geração de um relatório sobre os leaks detectados, para que o desenvolvedor possa fazer uma análise e corrigir os erros.
Para cada alocação será armazenado o arquivo e a linha onde ela foi feita no código fonte, como também o endereço de memória que foi alocado, assim durante a execução o programa vai ter uma lista de todos os blocos de memória alocados em um certo momento, bem como o ponto onde as alocações foram feitas.
Relatório de Leaks
Para gerar um relatório poderíamos simplesmente gravar a lista de alocações em um arquivo e pronto. Mas note que não nos interessa o endereço de memória própriamente dito, o que interessa é o ponto onde a alocação foi feita, sendo assim, para gerarmos o relatorio vamos agrupar todos os itens da lista por arquivo e linha de código, assim vamos ter uma entrada apenas para cada arquivo e linha onde o leak ocorreu.
Este agrupamento é útil pois os leaks costumam ocorrer em poucos pontos específicos e de nada adianta ter uma lista enorme de endereços de memória que foram alocados em um único ponto, o interessante é apenas saber onde o leak ocorreu.
Outro dado que ajuda é o tamanho do bloco de memória e contar quantas vezes um determinado ponto do código deixou a memória vazar, assim podemos calcular quanto de memória foi perdida em cada leak.
Implementação
Decidi chamar o projeto de MemoryTracker e o primeiro arquivo deste é o IM_MemoryTracker.h (uso IM_ como prefixo aqui devido ao nome do sistema onde ele foi implementado):
#ifndef IM_MEMORY_TRACKER_H #define IM_MEMORY_TRACKER_H void IM_TrackPointer(void *ptr, const char *file, int line, size_t size); void IM_UntrackPointer(void *ptr); #endif
Estas duas funções são responsáveis por gerenciar “ponteiros”, a IM_TrackPointer simplesmente guarda o ponteiro ptr em uma lista, juntamente com file e line que indicam respectivamente o nome do arquivo e a linha onde foi feita a alocação, e size é o tamanho do bloco de memória alocado (não é necessário, mas é bom para usar nos relatórios).
A outra função, IM_UntrackPointer remove o ponteiro da lista interna.
Agora vamos partir para a implementação, não gosto muito da idéia de colocar o arquivo cpp inteiro, prefiro ir mostrando ele por partes e disponibilizar o código completo no final do tutorial.
Primeiramente vamos a estrutura básica que vai armazenar os dados sobre uma linha de código:
struct CodeInfo_s
{
std::string strFile;
int iLine;
CodeInfo_s(const char *file, int line):
strFile(file),
iLine(line)
{
//empty
}
bool operator<(const CodeInfo_s &other) const
{
if(int s = strFile.compare(other.strFile))
return s < 0;
else
return iLine < other.iLine;
}
};
Essa estrutura armazena apenas a linha e o arquivo onde possa ter ocorrido um leak. O construtor inicializa os dois atributos e também foi implementado um operador de <, que é usado no relatório que sera explicado mais a frente.
O operador de < é usado para ordenar os blocos, primeiro ele compara o nome dos arquivos, se forem diferentes usa esse resultado como valor de retorno, caso os arquivos sejam iguais, é comparado o numero das linhas.
Mas nos interessa também armazenar o tamanho do bloco alocado e mais tarde no relatório computar o total de memória perdida:
struct PointerInfo_s: public CodeInfo_s
{
size_t szSize;
size_t szCount;
PointerInfo_s(const char *file, int line, size_t size):
CodeInfo_s(file, line),
szSize(size)
{
szCount = 1;
}
bool operator<(const PointerInfo_s &info)
{
return info.szSize < szSize;
}
};
Nesta estrutura é armazenado o tamanho do bloco de memória e a quantidade de vezes que esse ponto do código deixou algo vazar. O Construtor inicializa os atributos e novamente temos o operador de <, que força a ordenação dessa estrutura pelo tamanho do bloco.
Agora que já temos uma estrutura de dados, vamos então criar o mapa onde elas vão ser armazenadas:
typedef std::map<void *, PointerInfo_s> PointerInfoMap_t; static PointerInfoMap_t mapPointerInfo_gl;
Criamos então um std::map que usa um ponteiro void como chave e armazena PointerInfo_s, usamos um typedef para facilitar referências a este tipo mais tarde.
Agora vamos a implementação do IM_TrackPointer:
void IM_TrackPointer(void *ptr, const char *file, int line, size_t size)
{
mapPointerInfo_gl.insert(std::make_pair(ptr, PointerInfo_s(file, line, size)));
}
Esta função cria um PointerInfo e insere ele no mapa de ponteiros. Note que não nos preocupamos com colisões aqui, pois não é esperado que dois ponteiros iguais sejam armazenados. Caso algum ponteiro seja repetido é porque ele já foi desalocado e se isso acontecer é esperado que ele tenha sido removido do mapa.
Vamos ver então como implementar o IM_UntrackPointer:
void IM_UntrackPointer(void *ptr)
{
mapPointerInfo_gl.erase(ptr);
}
Esta função é tão simples quanto a anterior, simplesmente removemos uma entrada do mapa pronto!
Utilizando o Detector
A forma estupida de usar este sistema é:
#include "IM_MemoryTracker.h"
int maint(int, char **)
{
int *p = new int;
IM_TrackMemory(p, __FILE__, __LINE__, sizeof(*p));
delete p;
IM_UntrackMemory(p);
return 0;
}
Basta alocar memória com new e imediatamente chamar IM_TrackMemory para que ele armazene a informação e depois chamamos o IM_UntrackMemory no delete.
Os nomes __FILE__ e __LINE__ são macros gerados pelo compilador que contem o nome do arquivo e o numero da linha do ponto onde se encontram no código.
Mas ficar inserindo e removendo isso do código é um tanto trabalhoso e deixa o código feio demais, uma maneira elegante resolver isso em C++ é sobrecarregar os operadores new e delete, vamos criar então o arquivo IM_MemoryOperators.h:
#ifndef IM_MEMORY_OPERATORS_H
#define IM_MEMORY_OPERATORS_H
#include <new>
#include <IM_MemoryTracker.h>
#include <malloc.h>
inline void *IM_NewImpl(size_t size, const char *file, int line)
{
void *p = malloc(size);
if(p)
{
IM_TrackPointer(p, file, line, size);
}
return p;
}
inline void *operator new(size_t size, const char *file, int line)
{
return IM_NewImpl(size, file, line);
}
inline void *operator new[](size_t size, const char *file, int line)
{
return IM_NewImpl(size, file, line);
}
inline void IM_DeleteImpl(void *ptr)
{
if(ptr == NULL)
return;
IM_UntrackPointer(ptr);
free(ptr);
}
inline void operator delete(void *ptr, const char *, int )
{
IM_DeleteImpl(ptr);
}
inline void operator delete(void *ptr)
{
IM_DeleteImpl(ptr);
}
inline void operator delete[](void *ptr, const char *, int )
{
IM_DeleteImpl(ptr);
}
inline void operator delete[](void *ptr)
{
IM_DeleteImpl(ptr);
}
#endif
Apesar do tamanho, o código é bem simples, comecemos pela função IM_NewImpl que é quem realmente aloca a memória usando o bom e velho malloc do C. Ela primeiro aloca memória e caso a alocação tenha sido bem sucedida adiciona o ponteiro a lista de rastreamento.
Depois temos o primeiro operador new, que recebe como parâmetro o tamanho do bloco, o nome do arquivo e a linha onde foi feita a alocação, ele simplesmente chama a função IM_NewImpl para que ela realize a alocação.
Para que o código funcione com arrays criamos também a versão do new que opera com arrays para que o detector possa ser usado com estas também.
Agora os operadores de delete, assim como no new optei por incluir uma função IM_DeleteImpl que faz o trabalho do delete, que consiste apenas em liberar a memória e remover o ponteiro da lista de rastreamento.
O operador delete tem uma certa peculiaridade, primeiro, ele não pode ser sobrecarregado como o new, você pode implementar a sua própria versão, mas ela vai ser sempre “void delete(void *)” ou “void delete[](void *)”. Mas, não sei se é uma particularidade do visual, se existe uma versão de new com parâmetros novos ele também exige que exista uma versão idêntica do operador delete para ele usar no caso de uma exception em algum construtor (caso onde o compilador invoca o delete automaticamente). Então por isso temos quatro versões deste operador.
Agora voltando ao nosso exemplo anterior:
#include "IM_MemoryOperators.h"
int maint(int, char **)
{
int *p = new(__FILE__, __LINE__) int;
delete p;
return 0;
}
O código ficou muito melhor agora, mas ainda tem alguns problemas:
- Não é muito interessante ter que ficar colocando __FILE__ e __LINE__ toda hora que vamos usar o new.
- Não pretendo usar esse detector em todos os builds, apenas na versão debug ou em uma versão de testes.
Para ajustar isso vamos modificar o arquivo IM_MemoryOperators.h que vai resultar no código a seguir:
#ifndef IM_MEMORY_OPERATORS_H
#define IM_MEMORY_OPERATORS_H
#include <new>
#ifdef IM_DEBUG
#include <IM_MemoryTracker.h>
#include <malloc.h>
inline void *IM_NewImpl(size_t size, const char *file, int line)
{
void *p = malloc(size);
if(p)
{
IM_TrackPointer(p, file, line, size);
}
return p;
}
inline void *operator new(size_t size, const char *file, int line)
{
return IM_NewImpl(size, file, line);
}
inline void *operator new[](size_t size, const char *file, int line)
{
return IM_NewImpl(size, file, line);
}
inline void IM_DeleteImpl(void *ptr)
{
if(ptr == NULL)
return;
IM_UntrackPointer(ptr);
free(ptr);
}
inline void operator delete(void *ptr, const char *, int )
{
IM_DeleteImpl(ptr);
}
inline void operator delete(void *ptr)
{
IM_DeleteImpl(ptr);
}
inline void operator delete[](void *ptr, const char *, int )
{
IM_DeleteImpl(ptr);
}
inline void operator delete[](void *ptr)
{
IM_DeleteImpl(ptr);
}
#define IM_NEW(x) new(__FILE__, __LINE__) x
#define IM_DELETE(x) delete x
#else //IM_DEBUG
#define IM_NEW(x) new x
#define IM_DELETE(x) delete x
#endif //IM_DEBUG
#endif
Este código possui duas grandes diferenças em relação a versão anterior:
- Os operadores sobrecarregados foram colocados dentro de um bloco ifdef e são compilados apenas quando IM_DEBUG é definido.
- Foram criados os macros IM_NEW e IM_DELETE, que encapsulam o uso de __FILE__ e __LINE__.
Usando essa nova versão podemos escrever:
#include "IM_MemoryOperators.h"
int maint(int, char **)
{
int *p = IM_NEW(int);
IM_DELETE(p);
return 0;
}
Dessa forma o código fica muito mais simples e não é preciso mais ficar usando os macros de linha e arquivo a todo momento. Observe que se IM_DEBUG não for definido o código de rastreamento não é utilizado.
Outro detalhe é que o macro IM_DELETE não é realmente necessário, mas eu prefiro cria-lo para que o código fique mais consistente.
Gerando o Relatório de Leaks
Para gerar o relatório vamos fazer algumas mudanças no IM_MemoryTracker.cpp. Uma forma de gerar o relatório automaticamente é registrar uma função de callback usando a função do C atexit, que armazena uma função a ser chamada no final do programa, isto vai ser feito de maneira automática na primeira chamada a IM_TrackPointer, para tal vamos criar uma variável que vai armazenar se a callback foi instalada ou não:
typedef std::map<void *, PointerInfo_s> PointerInfoMap_t; static PointerInfoMap_t mapPointerInfo_gl; static bool oHandlerInstalled = false;
Modificando IM_TrackPointer:
void IM_TrackPointer(void *ptr, const char *file, int line, size_t size)
{
if(!oHandlerInstalled)
{
atexit(createReport);
oHandlerInstalled = true;
}
mapPointerInfo_gl.insert(std::make_pair(ptr, PointerInfo_s(file, line, size)));
}
A função primeiro checa se a callback foi instalada, caso não ela é instalada e o flag ativado, depois insere o ponteiro no mapa como anteriormente.
Finalmente vamos implementar a função createReport que gera o relatório, esta função inicialmente cria um arquivo onde o relatório vai ser gravado:
static void createReport()
{
//open the file, so we force it to be cleared
std::ofstream output("IM_LeakReport.log");
if(mapPointerInfo_gl.empty())
return;
Logo após a criação do arquivo é verificado se existe algum registro no mapa de ponteiros, caso esteja vazio não a nada para se fazer. Detalhe que o arquivo é gerado independentemente de ter leak ou não, com isso é garantido que o arquivo sempre esteja limpo entre execuções.
Em seguida o sistema de relatório agrupa todos os leaks de um mesmo ponto do código em um outro mapa:
typedef std::map<CodeInfo_s, PointerInfo_s> ReportMap_t;
ReportMap_t report;
BOOST_FOREACH(PointerInfoMap_t::value_type &info, mapPointerInfo_gl)
{
ReportMap_t::iterator it = report.find(info.second);
if(it == report.end())
{
report.insert(std::make_pair(info.second, info.second));
continue;
}
++it->second.szCount;
it->second.szSize += info.second.szSize;
}
O mapa ReportMap_t utiliza CodeInfo_s como chave, tirando proveito do operador < que ordena os registros de acordo com o nome do arquivo e a linha. Dentro do loop primeiramente é verificado se determinado arquivo e linha já estão no novo mapa, caso não esteja o registro é inserido e o código vai para o próximo registro. Caso contrário o registro existente é atualizado (incrementando o contador e somando-se os tamanhos).
Após este bloco temos um mapa com todo os leaks de um arquivo e uma linha de código. Já poderíamos agora gerar um relatório, mas é conveniente ordenar os registros por tamanho, para que o programador possa trabalhar nos leaks mais graves primeiramente (muito útil quando o prazo aperta):
std::vector<PointerInfo_s> sortedReport;
BOOST_FOREACH(ReportMap_t::value_type &info, report)
{
sortedReport.push_back(info.second);
}
std::sort(sortedReport.begin(), sortedReport.end());
A técnica não é das mais eficientes, mas o objetivo aqui é escrever um código simples, então para ordenar é criado um vector e todos os registros do mapa report são inseridos neste, depois é feita uma ordenação, que utiliza o operador < definido para PointerInfo_s.
E por fim, vamos gravar os dados:
//output the report
BOOST_FOREACH(PointerInfo_s &info, sortedReport)
{
output << info.strFile << " on line " << info.iLine << ", count: " << info.szCount << " total size: " << info.szSize << std::endl;
}
E finalmente temos o relatório, para simplificar abaixo temos o código completo:
static void createReport()
{
//open the file, so we force it to be cleared
std::ofstream output("IM_LeakReport.log");
if(mapPointerInfo_gl.empty())
return;
typedef std::map<CodeInfo_s, PointerInfo_s> ReportMap_t;
ReportMap_t report;
BOOST_FOREACH(PointerInfoMap_t::value_type &info, mapPointerInfo_gl)
{
ReportMap_t::iterator it = report.find(info.second);
if(it == report.end())
{
report.insert(std::make_pair(info.second, info.second));
continue;
}
++it->second.szCount;
it->second.szSize += info.second.szSize;
}
std::vector<PointerInfo_s> sortedReport;
BOOST_FOREACH(ReportMap_t::value_type &info, report)
{
sortedReport.push_back(info.second);
}
std::sort(sortedReport.begin(), sortedReport.end());
//output the report
BOOST_FOREACH(PointerInfo_s &info, sortedReport)
{
output << info.strFile << " on line " << info.iLine << ", count: " << info.szCount <<" total size: " << info.szSize << std::endl;
}
}
Testando o Código
A maneira mais simples de testar este código é gerando alguns leaks:
#include "IM_MemoryOperators.h"
int main(int, char **)
{
for(int i = 0;i < 10; ++i)
int *p = IM_NEW(int(i));
int *k = IM_NEW(int(5));
k = IM_NEW(int(3));
IM_DELETE(k);
return 0;
}
Estou assumindo aqui que o desenvolvedor em algum ponto definiu IM_DEBUG, no caso do Visual C++, é interessante definir este abrindo o Solution Explorer, clicando com o botão direito no projeto, selecionando então Propriedades, depois C/C++ e finalmente inclua IM_DEBUG na caixa Pré – Processador.
Se tudo correu bem quando seu programa for executado ele deve gerar um arquivo com conteúdo semelhante a:
c:\develop\bcsoftware\ogreengine\src\im_test\im_main.cpp on line 38, count: 10 total size: 40
c:\develop\bcsoftware\ogreengine\src\im_test\im_main.cpp on line 40, count: 1 total size: 4
Indicando que na linha 38 aconteceram 10 leaks, que totalizaram em 40 bytes e na linha 40 um leak de 4 bytes.
Utilizando com DLLs
O código mostrado aqui não funciona de maneira adequada com dlls pelo fato de usar variáveis estáticas para armazenar os ponteiros alocados. O correto para uso com dlls é armazenar os relatórios em variáveis externas que são exportadas e criar uma dll com o detector de leaks que deve ser usada por todas as dlls e executáveis do projeto, resultando assim em apenas um relatório sendo gerado.
Se for usado da forma como esta mostrado aqui cada dll vai acabar tendo sua própria instância do relatório e se um bloco de memória for alocado em uma DLL e liberado em outra, o relatório da DLL onde ele foi alocado não vai ser notificado da liberação e vai acusar um leak falso. Este é um problema semelhante ao explicado no segundo tutorial do visual C++.
Código Fonte
Para facilitar o uso estou disponibilizando aqui um zip com o código fonte. Esta versão não é totalmente idêntica ao código mostrado aqui pois esta funciona com DLLs, basta compilar em um projeto de maneira adequada exportando os símbolos conforme o necessário (definindo IM_MEMORY_TRACKER_EXPORTS no build da DLL do MemoryTracker).










