Como depurar erros de corrupção de heap?

Eu estou depurando um aplicativo C ++ (nativo) multi-threaded sob Visual Studio 2008. Em ocasiões aparentemente aleatórias, recebo um erro “O Windows acionou um ponto de interrupção …” com uma observação que isso pode ser devido a uma corrupção no heap. Esses erros nem sempre travam o aplicativo imediatamente, embora seja provável que ele falhe logo após.

O grande problema com esses erros é que eles aparecem apenas após a ocorrência da corrupção, o que os torna muito difíceis de rastrear e depurar, especialmente em um aplicativo multi-threaded.

  • Que tipo de coisas podem causar esses erros?

  • Como faço para depurá-los?

Dicas, ferramentas, methods, esclarecimentos … são bem-vindos.

O Application Verifier, combinado com o Debugging Tools for Windows, é uma configuração incrível. Você pode obter ambos como parte do Windows Driver Kit ou do Windows SDK mais leve . (Descobri sobre o Application Verifier ao pesquisar uma questão anterior sobre um problema de corrupção de heap .) Eu usei BoundsChecker e Insure ++ (mencionado em outras respostas) no passado também, embora tenha ficado surpreso com a funcionalidade do Application Verifier.

Electric Fence (aka “efence”), dmalloc , valgrind , e assim por diante são todos dignos de nota, mas a maioria deles é muito mais fácil de rodar com o Windows * do que com o Windows. Valgrind é ridiculamente flexível: depurei grandes softwares de servidor com muitos problemas de heap usando-os.

Quando tudo mais falhar, você pode fornecer seu próprio operador global new / delete e sobrecargas malloc / calloc / realloc – como fazer isso irá variar um pouco dependendo do compilador e da plataforma – e isso será um pouco de investimento – mas pode valer a pena a longo prazo. A lista de resources desejável deve parecer familiar a partir de dmalloc e electricfence, e o surpreendentemente excelente livro Writing Solid Code :

  • valores de sentinela : permite um pouco mais de espaço antes e depois de cada alocação, respeitando o requisito de alinhamento máximo; preencha com números mágicos (ajuda a capturar overflows e underflows de buffer e o ponteiro “wild” ocasional)
  • preenchimento de alocação: preencha novas alocações com um valor não-0 mágico – O Visual C ++ já fará isso para você em compilações de Depuração (ajuda a detectar o uso de variantes não inicializadas)
  • Preenchimento gratuito : preencha a memory liberada com um valor não-0 mágico, projetado para acionar um segfault se for desreferenciado na maioria dos casos (ajuda a detectar pointers pendentes)
  • delayed free : não retorne a memory liberada para o heap por um tempo, mantenha-o livre, mas não disponível (ajuda a capturar mais pointers pendentes, captura liberações duplas próximas)
  • rastreamento : ser capaz de registrar onde uma alocação foi feita às vezes pode ser útil

Note que em nosso sistema homebrew local (para um alvo embutido) nós mantemos o rastreamento separado da maioria das outras coisas, porque a sobrecarga de tempo de execução é muito maior.


Se você estiver interessado em mais razões para sobrecarregar essas funções de alocação / operadores, dê uma olhada na minha resposta a “Qualquer motivo para sobrecarregar o operador global novo e excluir?” ; Sem auto-promoção descarada, lista outras técnicas úteis no rastreamento de erros de corrupção de heap, bem como outras ferramentas aplicáveis.

Você pode detectar muitos problemas de corrupção de heap ativando o heap de página para seu aplicativo. Para fazer isso você precisa usar gflags.exe que vem como parte de ferramentas de debugging para Windows

Execute o Gflags.exe e nas opções de arquivo de imagem para o seu executável, marque a opção “Ativar Heap de Página”.

Agora reinicie o seu exe e append a um depurador. Com o heap de página ativado, o aplicativo entrará no depurador sempre que ocorrer algum dano de heap.

Um artigo muito relevante é Depurar a corrupção de heap com o Application Verifier e o Debugdiag .

Para realmente desacelerar as coisas e executar um monte de verificação de tempo de execução, tente adicionar o seguinte na parte superior do seu main() ou equivalente no Microsoft Visual Studio C ++

 _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF ); 

Que tipo de coisas podem causar esses erros?

Fazer coisas maliciosas com memory, por exemplo, escrever após o final de um buffer, ou gravar em um buffer depois de ser liberado de volta para o heap.

Como faço para depurá-los?

Use um instrumento que adiciona verificação de limites automatizada ao seu executável: ou seja, valgrind no Unix, ou uma ferramenta como o BoundsChecker (a Wikipedia sugere também Purify e Insure ++) no Windows.

Tenha em atenção que estes irão abrandar a sua aplicação, pelo que poderão ficar inutilizáveis ​​se a sua for uma aplicação em tempo real.

Outro possível auxílio / ferramenta de debugging pode ser o HeapAgent do MicroQuill.

Uma dica rápida, que eu recebi de Detectar access à memory liberada, é esta:

Se você deseja localizar o erro rapidamente, sem verificar cada instrução que acessa o bloco de memory, você pode definir o ponteiro de memory para um valor inválido depois de liberar o bloco:

 #ifdef _DEBUG // detect the access to freed memory #undef free #define free(p) _free_dbg(p, _NORMAL_BLOCK); *(int*)&p = 0x666; #endif 

A melhor ferramenta que achei útil e trabalhei todas as vezes é a revisão de código (com bons revisores de código).

Além da revisão de código, eu primeiro tentei o Page Heap . Página Heap leva alguns segundos para configurar e com sorte pode apontar o seu problema.

Se não tiver sorte com o Page Heap, baixe o Debugging Tools for Windows da Microsoft e aprenda a usar o WinDbg. Desculpe não poderia lhe dar uma ajuda mais específica, mas depurar a corrupção de heap multi-threaded é mais uma arte do que uma ciência. Google para “corrupção do heap WinDbg” e você deve encontrar muitos artigos sobre o assunto.

Você também pode querer verificar se está vinculando à biblioteca de tempo de execução C dinâmica ou estática. Se os arquivos DLL estiverem vinculados à biblioteca de tempo de execução C estática, os arquivos DLL terão heaps separados.

Portanto, se você criar um object em uma DLL e tentar liberá-lo em outra DLL, receberá a mesma mensagem que está vendo acima. Esse problema é referenciado em outra pergunta de estouro de pilha, liberando memory alocada em uma DLL diferente .

Que tipo de funções de alocação você está usando? Recentemente, acertei um erro semelhante usando as funções de alocação de estilo Heap *.

Descobri que eu estava criando, por engano, o heap com a opção HEAP_NO_SERIALIZE . Isso essencialmente faz com que as funções Heap sejam executadas sem segurança de thread. É uma melhoria de desempenho se usada corretamente, mas nunca deve ser usada se você estiver usando o HeapAlloc em um programa multi-thread [1]. Eu só mencionei isso porque o seu post menciona que você tem um aplicativo multi-threaded. Se você estiver usando HEAP_NO_SERIALIZE em qualquer lugar, exclua-o e isso provavelmente resolverá seu problema.

[1] Há certas situações em que isso é legal, mas requer que você serialize chamadas para o heap * e geralmente não é o caso de programas multi-threaded.

Se esses erros ocorrerem aleatoriamente, há grande probabilidade de você ter encontrado raças de dados. Por favor, verifique: você modifica pointers de memory compartilhada de diferentes threads? O Intel Thread Checker pode ajudar a detectar esses problemas no programa multithread.

Além de procurar ferramentas, considere procurar um provável culpado. Existe algum componente que você esteja usando, talvez não escrito por você, que pode não ter sido projetado e testado para ser executado em um ambiente multithread? Ou simplesmente um que você não conhece correu em tal ambiente.

A última vez que aconteceu comigo, foi um pacote nativo que tinha sido usado com sucesso de trabalhos em lote por anos. Mas foi a primeira vez que esta empresa foi usada em um serviço da Web .NET (que é multithreaded). Era isso – eles mentiram sobre o código ser thread-safe.

Você pode usar macros VC CRT Heap-Check para _CrtSetDbgFlag : _CRTDBG_CHECK_ALWAYS_DF ou _CRTDBG_CHECK_EVERY_16_DF .. _CRTDBG_CHECK_EVERY_1024_DF .

Eu gostaria de adicionar minha experiência. Nos últimos dias, resolvi uma instância desse erro em meu aplicativo. No meu caso particular, os erros no código foram:

  • Removendo elementos de uma coleção STL enquanto iterar sobre ela (acredito que existam sinalizadores de debugging no Visual Studio para capturar essas coisas; eu a peguei durante a revisão de código)
  • Esse é mais complexo, vou dividir em etapas:
    • A partir de um encadeamento nativo em C ++, chame de volta para o código gerenciado
    • Em terrenos gerenciados, chame Control.Invoke e descarte um object gerenciado que encapsula o object nativo ao qual o retorno de chamada pertence.
    • Como o object ainda está ativo dentro do thread nativo (ele permanecerá bloqueado na chamada de retorno de chamada até que Control.Invoke encerrado). Devo esclarecer que eu uso boost::thread , então eu uso uma function de membro como a function de thread.
    • Solução : Use Control.BeginInvoke (minha GUI é feita com WinForms) para que o thread nativo possa terminar antes que o object seja destruído (o propósito do callback é precisamente notificar que o thread terminou e o object pode ser destruído).

Eu tive um problema semelhante – e ele apareceu de forma bastante aleatória. Talvez algo estivesse corrompido nos arquivos de construção, mas acabei consertando isso limpando o projeto primeiro e depois reconstruindo.

Então, além das outras respostas dadas:

Que tipo de coisas podem causar esses erros? Algo corrompido no arquivo de compilation.

Como faço para depurá-los? Limpar o projeto e reconstruir. Se for consertado, esse provavelmente foi o problema.