Como depurar erros de corrupção de heap?

165

Estou depurando um aplicativo C ++ multithread (nativo) no Visual Studio 2008. Em ocasiões aparentemente aleatórias, recebo o erro "O Windows disparou um ponto de interrupção ..." com uma nota de que isso pode estar relacionado a uma corrupção no pilha. Esses erros nem sempre travam o aplicativo imediatamente, embora seja provável que travem logo após.

O grande problema com esses erros é que eles aparecem somente depois que a corrupção realmente ocorre, o que os torna muito difíceis de rastrear e depurar, especialmente em um aplicativo com vários threads.

  • Que tipo de coisa pode causar esses erros?

  • Como eu os depuro?

Dicas, ferramentas, métodos, esclarecimentos ... são bem-vindos.

Peter Mortensen
fonte

Respostas:

128

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 o Application Verifier ao pesquisar uma pergunta anterior sobre um problema de corrupção de heap .) Também usei o BoundsChecker e o Insure ++ (mencionados em outras respostas) no passado, embora tenha ficado surpreso com a quantidade de funcionalidade do Application Verifier.

Vale a pena mencionar o Electric Fence (também conhecido como "efence"), dmalloc , valgrind etc., mas a maioria deles é muito mais fácil de executar com * nix do que o Windows. O Valgrind é ridiculamente flexível: depurei um software de servidor grande com muitos problemas de pilha usando-o.

Quando tudo mais falhar, você poderá fornecer ao seu próprio operador global novas sobrecargas / delete e malloc / calloc / realloc - como fazer isso 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 recursos desejáveis ​​deve parecer familiar no dmalloc e no electricfence, e o surpreendentemente excelente livro Writing Solid Code :

  • valores de sentinela : permita um pouco mais de espaço antes e depois de cada alocação, respeitando o requisito máximo de alinhamento; preencher com números mágicos (ajuda a capturar estouros e subfluxos de buffer e o ocasional ponteiro "selvagem")
  • atribuir preenchimento : preencha novas alocações com um valor mágico diferente de 0 - o Visual C ++ já fará isso em compilações de depuração (ajuda a capturar o uso de vars não inicializados)
  • preenchimento livre : preenche a memória liberada com um valor mágico diferente de 0, projetado para disparar um segfault se for desreferenciado na maioria dos casos (ajuda a capturar ponteiros pendentes)
  • atraso na liberação : não retorne a memória liberada para a pilha por um tempo, mantenha-a cheia, mas não disponível (ajuda a capturar mais indicadores pendentes, captura a liberação dupla aproximada)
  • rastreamento : poder registrar onde uma alocação foi feita às vezes pode ser útil

Observe que em nosso sistema local de homebrew (para um destino incorporado), mantemos o rastreamento separado da maioria dos outros itens, porque a sobrecarga de tempo de execução é muito maior.


Se você estiver interessado em mais motivos para sobrecarregar essas funções / operadores de alocação, dê uma olhada na minha resposta para "Qualquer motivo para sobrecarregar o operador global novo e excluir?"; autopromoção vergonhosa à parte, lista outras técnicas que são úteis para rastrear erros de corrupção de heap, além de outras ferramentas aplicáveis.


Como eu continuo encontrando minha própria resposta aqui ao pesquisar valores de alocação / livre / cerca que a MS usa, aqui está outra resposta que abrange os valores de preenchimento do dbgheap da Microsoft .

leander
fonte
3
Uma pequena coisa que vale a pena notar no Application Verifier: você deve registrar os símbolos do Application Verifier antes dos símbolos do servidor de símbolos da Microsoft no seu caminho de busca de símbolos, se você usar isso ... Levei um pouco de pesquisa para descobrir por que! Avrf não estava encontrando os símbolos necessários.
Leander
O Application Verifier foi de grande ajuda e, combinado com algumas suposições, consegui resolver o problema! Muito obrigado e por todos os outros também, por trazer pontos úteis.
O Application Verifier precisa ser usado com o WinDbg ou deve funcionar com o depurador do Visual Studio? Eu tenho tentado usá-lo, mas ele não gera nenhum erro ou aparentemente faz alguma coisa quando depuro no VS2012.
Nathan Reed
@ Nathanathan: Eu acredito que ele também funciona com o VS - consulte msdn.microsoft.com/en-us/library/ms220944(v=vs.90).aspx - embora observe que este link é para o VS2008, não estou certeza sobre versões posteriores. A memória está um pouco confusa, mas acredito que quando tive o problema no link "pergunta anterior", executei o Application Verifier e salvei as opções, executei o programa e, quando ele travou, escolhi o VS para depurar. O AV acabou de travar / afirmar anteriormente. O comando! Avrf é específico do WinDbg, tanto quanto eu sei. Espero que outros possam fornecer mais informações!
leander 25/06
Obrigado. Na verdade, resolvi meu problema original e, afinal, não havia corrupção de pilha, mas outra coisa, o que provavelmente explica por que o App Verifier não encontrou nada. :)
Nathan Reed
35

Você pode detectar muitos problemas de corrupção de heap ativando o Page Heap para seu aplicativo. Para fazer isso, você precisa usar o gflags.exe que faz parte do Debugging Tools For Windows

Execute o Gflags.exe e, nas opções de arquivo de imagem do seu executável, marque a opção "Ativar heap de página".

Agora reinicie seu exe e conecte-o a um depurador. Com o Page Heap ativado, o aplicativo entrará no depurador sempre que ocorrer qualquer corrupção no heap.

Canopus
fonte
sim, mas depois que eu recebo essa chamada de função no meu despejo de pilha de chamada (após falha na corrupção de memória): wow64! Wow64NotifyDebugger, o que posso fazer? Eu ainda não sei o que está acontecendo de errado na minha candidatura
Guillaume07
Tentei o gflags para depurar a corrupção de heap aqui, pequena ferramenta MUITO útil, altamente recomendada. Acontece que eu estava acessando a memória liberada, que, quando instrumentada com gflags, entra imediatamente no depurador ... Prático!
Dave F
Great Tool! Acabei de encontrar um bug, que eu estava procurando há dias, porque o Windows não diz o endereço da corrupção, apenas que "algo" está errado, o que não é realmente útil.
Devolus
Um pouco atrasado para a festa, mas notei um aumento significativo no uso de memória no aplicativo que estou depurando ao ativar o Page Heap. Infelizmente, até o ponto em que o aplicativo (32 bits) fica sem memória antes que a detecção de corrupção de heap seja acionada. Alguma idéia de como lidar com esse problema?
Uceumern
13

Para realmente desacelerar as coisas e executar muitas verificações em 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 );
Dave Van Wagner
fonte
8

Que tipo de coisa pode causar esses erros?

Fazer coisas impertinentes com memória, por exemplo, escrever após o término de um buffer ou gravar em um buffer após ter sido liberado de volta para o heap.

Como eu os depuro?

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

Lembre-se de que isso reduzirá a velocidade do seu aplicativo, e poderá ser inutilizável se o seu for um aplicativo em tempo real.

Outro possível auxiliar / ferramenta de depuração pode ser o HeapAgent da MicroQuill.

ChrisW
fonte
1
Reconstruir o aplicativo com tempo de execução de depuração (sinalizador / MDd ou / MTd) seria o meu primeiro passo. Eles executam verificações adicionais no malloc e gratuitamente, e geralmente são eficazes para diminuir a localização do (s) bug (s).
Empregado Russian
HeapAgent da MicroQuill: Não há muito escrito ou ouvido falar sobre isso, mas para corrupção de heap, ele deve estar na sua lista.
Samrat Patil
1
O BoundsChecker funciona bem como um teste de fumaça, mas nem pense em executar um programa nele enquanto tenta executar esse programa na produção também. A desaceleração pode variar de 60x a 300x, dependendo de quais opções você está usando e se está ou não usando o recurso de instrumentação do compilador. Disclaimer: Eu sou um dos caras que mantém o produto para a Micro Focus.
Rick Papo
8

Uma dica rápida que recebi de Detectando acesso à memória liberada é a seguinte:

Se você deseja localizar o erro rapidamente, sem verificar todas as instruções que acessam o bloco de memória, é possível definir o ponteiro da memória para um valor inválido após 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
StackedCrooked
fonte
5

A melhor ferramenta que achei útil e sempre funcionou é a revisão de código (com bons revisores de código).

Além da revisão de código, eu tentaria primeiro o Page Heap . O Page Heap demora alguns segundos para ser configurado e, com sorte, ele pode identificar seu problema.

Se não tiver sorte com o Page Heap, baixe o Microsoft Debugging Tools for Windows e aprenda a usar o WinDbg. Desculpe, não foi possível fornecer ajuda mais específica, mas a depuração de corrupção de heap multithread é mais uma arte do que ciência. Google para "corrupção de heap do WinDbg" e você deve encontrar muitos artigos sobre o assunto.

Shing Yip
fonte
4

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

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

dreadpirateryan
fonte
3

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

Aconteceu que eu estava criando por engano a pilha com o HEAP_NO_SERIALIZE opção 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 multiencadeado [1]. Só mencionei isso porque sua postagem menciona que você tem um aplicativo com vários threads. Se você estiver usando HEAP_NO_SERIALIZE em qualquer lugar, exclua-o e isso provavelmente resolverá o seu problema.

[1] Há certas situações em que isso é legal, mas requer que você serialize as chamadas para o Heap * e normalmente não é o caso de programas com vários threads.

JaredPar
fonte
Sim: observe as opções de compilação / compilação do aplicativo e verifique se ele está sendo construído para vincular uma versão "multiencadeada" da biblioteca de tempo de execução C.
ChrisW
@ChrisW para as APIs de estilo HeapAlloc, isso é diferente. Na verdade, é um parâmetro que pode ser alterado no momento da criação da pilha, não no tempo do link.
JaredPar 18/06/09
Oh Não me ocorreu que o OP estivesse falando sobre esse heap, e não sobre o heap no CRT.
18119 ChrisW
@ Chris, a pergunta é bastante vaga, mas acabei de acertar o problema que detalhei ~ uma semana atrás, por isso está fresco em minha mente.
JaredPar 18/06/09
3

Se esses erros ocorrerem aleatoriamente, há uma alta probabilidade de você encontrar corridas de dados. Por favor, verifique: você modifica ponteiros de memória compartilhada de diferentes threads? O Intel Thread Checker pode ajudar a detectar esses problemas no programa multithread.

Vladimir Obrizan
fonte
1

Além de procurar ferramentas, considere procurar um provável culpado. Existe algum componente que você esteja usando, talvez não tenha sido 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 que foi executado em tal ambiente.

A última vez que aconteceu comigo, era um pacote nativo que havia sido usado com sucesso em trabalhos em lotes por anos. Mas foi a primeira vez nesta empresa que foi usada em um serviço da Web .NET (que é multithread). Era isso - eles mentiram sobre o código ser seguro para threads.

John Saunders
fonte
1

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

KindDragon
fonte
0

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

  • Removendo elementos de uma coleção STL enquanto itera sobre ele (acredito que há sinalizadores de depuração no Visual Studio para capturar essas coisas; eu peguei durante a revisão de código)
  • Este é mais complexo, vou dividi-lo em etapas:
    • Em um encadeamento C ++ nativo, retorne o código gerenciado
    • Em terra gerenciada, chame Control.Invokee descarte um objeto gerenciado que agrupe o objeto nativo ao qual o retorno de chamada pertence.
    • Como o objeto ainda está ativo dentro do encadeamento nativo (permanecerá bloqueado na chamada de retorno de chamada até o Control.Invokefinal). Devo esclarecer que uso boost::thread, portanto, uso uma função de membro como a função de thread.
    • Solução : use Control.BeginInvoke(minha GUI é feita com Winforms) para que o encadeamento nativo possa terminar antes que o objeto seja destruído (o objetivo do retorno de chamada é notificar com precisão que o encadeamento terminou e o objeto pode ser destruído).
dario_ramos
fonte
0

Eu tive um problema semelhante - e ele apareceu de forma aleatória. Talvez algo estivesse corrompido nos arquivos de compilação, mas acabei corrigindo-o limpando o projeto primeiro e depois reconstruindo.

Portanto, além das outras respostas dadas:

Que tipo de coisa pode causar esses erros? Algo corrompido no arquivo de compilação.

Como eu os depuro? Limpando o projeto e reconstruindo. Se estiver corrigido, esse provavelmente foi o problema.

Marty
fonte