Pergunta

Estou trabalhando em um multithread Aplicativo C++ que está corrompendo o heap.As ferramentas habituais para localizar esta corrupção parecem ser inaplicáveis.Compilações antigas (18 meses) do código-fonte exibem o mesmo comportamento da versão mais recente, então isso já existe há muito tempo e simplesmente não foi percebido;por outro lado, os deltas de origem não podem ser usados ​​para identificar quando o bug foi introduzido - existem bastante de alterações de código no repositório.

O prompt para o comportamento de travamento é gerar taxa de transferência neste sistema - transferência de dados por soquete que é transferida para uma representação interna.Eu tenho um conjunto de dados de teste que periodicamente causará uma exceção no aplicativo (vários lugares, várias causas - incluindo falha na alocação de heap, assim:corrupção de pilha).

O comportamento parece estar relacionado à potência da CPU ou à largura de banda da memória;quanto mais de cada um a máquina tiver, mais fácil será travar.Desativar um núcleo hyper-threading ou dual-core reduz a taxa de corrupção (mas não elimina).Isso sugere um problema relacionado ao tempo.

Agora aqui está o problema:
Quando é executado em um ambiente de depuração leve (digamos Visual Studio 98 / AKA MSVC6) a corrupção do heap é razoavelmente fácil de reproduzir - dez ou quinze minutos se passam antes que algo falhe terrivelmente e exceções, como um alloc; ao executar em um ambiente de depuração sofisticado (Rational Purify, VS2008/MSVC9 ou até mesmo o Microsoft Application Verifier), o sistema fica limitado pela velocidade da memória e não trava (limitado pela memória:CPU não está ficando acima 50%, a luz do disco não está acesa, o programa está indo o mais rápido que pode, consumindo caixa 1.3G de 2G de RAM).Então, Tenho a opção de poder reproduzir o problema (mas não identificar a causa) ou identificar a causa ou um problema que não consigo reproduzir.

Meus melhores palpites atuais sobre o próximo passo são:

  1. Obtenha uma caixa incrivelmente suja (para substituir a caixa de desenvolvimento atual:2 GB de RAM em um E6550 Core2 Duo);isso tornará possível reproduzir a falha que causa mau comportamento ao executar em um ambiente de depuração poderoso;ou
  2. Operadores de reescrita new e delete usar VirtualAlloc e VirtualProtect para marcar a memória como somente leitura assim que terminar.Corra sob MSVC6 e faça com que o sistema operacional pegue o bandido que está gravando na memória liberada.Sim, isso é um sinal de desespero:quem diabos reescreve new e delete?!Eu me pergunto se isso o tornará tão lento quanto em Purify et al.

E não:O envio com instrumentação Purify integrada não é uma opção.

Um colega acabou de passar e perguntou "Stack Overflow?Estamos tendo estouros de pilha agora?!?"

E agora, a pergunta: Como localizo o corrompidor de heap?


Atualizar:equilíbrio new[] e delete[] parece ter percorrido um longo caminho para resolver o problema.Em vez de 15 minutos, o aplicativo agora dura cerca de duas horas antes de travar.Ainda não.Alguma sugestão adicional?A corrupção do heap persiste.

Atualizar:uma versão construída no Visual Studio 2008 parece dramaticamente melhor;a suspeita actual recai sobre o STL implementação que acompanha VS98.


  1. Reproduza o problema. Dr Watson produzirá um despejo que pode ser útil em análises posteriores.

Vou anotar isso, mas estou preocupado que o Dr. Watson só tropece depois do fato, e não quando a pilha estiver sendo pisoteada.

Outra tentativa pode estar usando WinDebug como uma ferramenta de depuração bastante poderosa e ao mesmo tempo leve.

Tenho isso acontecendo no momento, novamente:não ajuda muito até que algo dê errado.Quero pegar o vândalo em flagrante.

Talvez essas ferramentas permitam pelo menos restringir o problema a determinado componente.

Não tenho muita esperança, mas tempos de desespero exigem...

E você tem certeza de que todos os componentes do projeto possuem configurações corretas da biblioteca de tempo de execução (C/C++ tab, categoria Geração de código nas configurações do projeto VS 6.0)?

Não, não estou, e amanhã passarei algumas horas examinando o espaço de trabalho (58 projetos nele) e verificando se todos estão compilando e vinculando com os sinalizadores apropriados.


Atualizar:Isso levou 30 segundos.Selecione todos os projetos no Settings caixa de diálogo, desmarque até encontrar o(s) projeto(s) que não possuem as configurações corretas (todos eles tinham as configurações corretas).

Foi útil?

Solução

Minha primeira escolha seria uma ferramenta de heap dedicada, como pageheap.exe.

Reescrever new e delete pode ser útil, mas isso não captura as alocações confirmadas pelo código de nível inferior.Se é isso que você quer, é melhor desviar o low-level alloc APIs usando Microsoft Detours.

Também verificações de sanidade, como:verifique se suas bibliotecas de tempo de execução correspondem (release vs.depuração, multithread vs.thread único, dll vs.lib estática), procure por exclusões incorretas (por exemplo, delete onde delete [] deveria ter sido usado), certifique-se de não misturar e combinar suas alocações.

Tente também desligar seletivamente os threads e ver quando/se o problema desaparece.

Qual é a aparência da pilha de chamadas etc. no momento da primeira exceção?

Outras dicas

Tenho os mesmos problemas no meu trabalho (também usamos VC6 às vezes).E não há solução fácil para isso.Tenho apenas algumas dicas:

  • Tente com crash dumps automáticos na máquina de produção (veja Dumper de Processo).Minha experiência diz que o Dr.Watson é imperfeito para despejo.
  • Deletar tudo pegar(...) do seu código.Freqüentemente, eles escondem sérias exceções de memória.
  • Verificar Depuração avançada do Windows - há muitas dicas excelentes para problemas como o seu.Eu recomendo isso de todo o coração.
  • Se você usar STL tentar STLPort e compilações verificadas.Iterador inválido é um inferno.

Boa sorte.Problemas como o seu levam meses para serem resolvidos.Esteja pronto para isso...

Execute o aplicativo original com ADplus -crash -pn appnename.exeQuando o problema de memória aparecer, você terá um grande despejo.

Você pode analisar o dump para descobrir qual local de memória foi corrompido.Se você tiver sorte, a memória de substituição é uma string única, você pode descobrir de onde ela veio.Se você não tiver sorte, precisará se aprofundar win32 amontoe e descubra quais eram as características originais da memória.(heap -x pode ajudar)

Depois de saber o que estava errado, você pode restringir o uso do appverifier com configurações especiais de heap.ou sejavocê pode especificar o que DLL você monitora ou qual tamanho de alocação monitorar.

Esperamos que isso acelere o monitoramento o suficiente para detectar o culpado.

Na minha experiência, nunca precisei do modo de verificação de heap completo, mas passei muito tempo analisando os despejos de memória e navegando nas fontes.

P.S:Você pode usar DebugDiag para analisar os despejos.Ele pode apontar o DLL possuir o heap corrompido e fornecer outros detalhes úteis.

Tivemos muita sorte escrevendo nossas próprias funções malloc e free.Na produção, eles apenas chamam o padrão malloc e free, mas no debug, eles podem fazer o que você quiser.Também temos uma classe base simples que não faz nada além de substituir os operadores new e delete para usar essas funções, então qualquer classe que você escrever pode simplesmente herdar dessa classe.Se você tiver uma tonelada de código, pode ser um grande trabalho substituir chamadas para malloc e free para o novo malloc e free (não se esqueça de realloc!), mas no longo prazo é muito útil.

No livro de Steve Maguire Escrevendo código sólido (altamente recomendado), existem exemplos de coisas de depuração que você pode fazer nessas rotinas, como:

  • Acompanhe as alocações para encontrar vazamentos
  • Aloque mais memória do que o necessário e coloque marcadores no início e no final da memória – durante a rotina livre, você pode garantir que esses marcadores ainda estejam lá
  • memset a memória com um marcador na alocação (para encontrar o uso da memória não inicializada) e na liberação (para encontrar o uso da memória liberada)

Outra boa ideia é nunca usar coisas como strcpy, strcat, ou sprintf - sempre use strncpy, strncat, e snprintf.Também escrevemos nossas próprias versões, para garantir que não descartaríamos o fim de um buffer, e elas também trouxeram muitos problemas.

Você deve atacar esse problema com análise estática e em tempo de execução.

Para análise estática, considere compilar com PREfast (cl.exe /analyze).Ele detecta incompatibilidade delete e delete[], estouros de buffer e uma série de outros problemas.Esteja preparado, porém, para percorrer muitos kilobytes de aviso L6, especialmente se o seu projeto ainda tiver L4 não consertado.

PREfast está disponível com Visual Studio Team System e, aparentemente, como parte do SDK do Windows.

A aparente aleatoriedade da corrupção da memória parece muito com um problema de sincronização de threads - um bug é reproduzido dependendo da velocidade da máquina.Se os objetos (pedaços de memória) forem compartilhados entre threads e as primitivas de sincronização (seção crítica, mutex, semáforo, outros) não forem por classe (por objeto, por classe), então é possível chegar a uma situação onde a classe (pedaço de memória) é excluída/liberada durante o uso ou usada após excluída/liberada.

Como teste para isso, você poderia adicionar primitivas de sincronização a cada classe e método.Isso tornará seu código mais lento porque muitos objetos terão que esperar uns pelos outros, mas se isso eliminar a corrupção do heap, seu problema de corrupção do heap se tornará um problema de otimização de código.

Isso ocorre em condições de pouca memória?Se assim for, pode ser que o novo esteja retornando NULL em vez de lançar std::bad_alloc.Mais velho VC++ os compiladores não implementaram isso corretamente.Há um artigo sobre Falhas de alocação de memória herdada batendo STL aplicativos criados com VC6.

Você tentou compilações antigas, mas há um motivo pelo qual você não pode voltar no histórico do repositório e ver exatamente quando o bug foi introduzido?

Caso contrário, eu sugeriria adicionar algum tipo de registro simples para ajudar a rastrear o problema, embora eu não saiba o que especificamente você deseja registrar.

Se você puder descobrir o que exatamente PODE causar esse problema, por meio do Google e da documentação das exceções que está obtendo, talvez isso forneça mais informações sobre o que procurar no código.

Minha primeira ação seria a seguinte:

  1. Construa os binários na versão "Release", mas criando um arquivo de informações de depuração (você encontrará essa possibilidade nas configurações do projeto).
  2. Use o Dr Watson como um depurador padrão (DrWtsn32 -I) em uma máquina na qual você deseja reproduzir o problema.
  3. Reproduza o problema.O Dr. Watson produzirá um despejo que poderá ser útil em análises posteriores.

Outra tentativa pode ser usar o WinDebug como uma ferramenta de depuração que é bastante poderosa e ao mesmo tempo leve.

Talvez essas ferramentas permitam pelo menos restringir o problema a determinado componente.

E você tem certeza de que todos os componentes do projeto possuem configurações corretas da biblioteca de tempo de execução (guia C/C++, categoria Geração de código nas configurações do projeto VS 6.0)?

Portanto, pelas informações limitadas que você possui, isso pode ser uma combinação de uma ou mais coisas:

  • Uso incorreto de heap, ou seja, liberações duplas, leitura após liberação, gravação após liberação, configuração do sinalizador HEAP_NO_SERIALIZE com alocações e liberações de vários threads no mesmo heap
  • Fora da memória
  • Código incorreto (ou seja, buffer overflows, buffer underflows, etc.)
  • Problemas de “tempo”

Se forem os dois primeiros, mas não o último, você já deve ter percebido isso com o pageheap.exe.

O que provavelmente significa que é devido à forma como o código está acessando a memória compartilhada.Infelizmente, rastrear isso será bastante doloroso.O acesso não sincronizado à memória compartilhada geralmente se manifesta como estranhos problemas de “tempo”.Coisas como não usar a semântica de aquisição/liberação para sincronizar o acesso à memória compartilhada com um sinalizador, não usar bloqueios adequadamente, etc.

No mínimo, seria útil poder acompanhar de alguma forma as alocações, como foi sugerido anteriormente.Pelo menos você pode ver o que realmente aconteceu até a corrupção do heap e tentar diagnosticar a partir disso.

Além disso, se você pode redirecionar facilmente as alocações para vários heaps, você pode tentar fazer isso para ver se isso resolve o problema ou resulta em um comportamento de bugs mais reproduzível.

Quando você estava testando com o VS2008, você executou o HeapVerifier com Conserve Memory definido como Sim?Isso pode reduzir o impacto no desempenho do alocador de heap.(Além disso, você precisa executar Debug-> Start with Application Verifier, mas você já deve saber disso.)

Você também pode tentar depurar com Windbg e vários usos do comando !heap.

MSN

Se você optar por reescrever novo/excluir, fiz isso e tenho um código-fonte simples em:

http://gandolf.homelinux.org/~smhanov/blog/?id=10

Isso detecta vazamentos de memória e também insere dados de proteção antes e depois do bloco de memória para capturar corrupção de heap.Você pode simplesmente integrar-se a ele colocando #include "debug.h" no topo de cada arquivo CPP e definindo DEBUG e DEBUG_MEM.

A sugestão de Graeme de custom malloc/free é uma boa ideia.Veja se você consegue caracterizar algum padrão sobre a corrupção para lhe dar uma ideia da alavancagem.

Por exemplo, se estiver sempre em um bloco do mesmo tamanho (digamos 64 bytes), altere seu par malloc/free para sempre alocar pedaços de 64 bytes em sua própria página.Ao liberar um pedaço de 64 bytes, defina os bits de proteção de memória nessa página para evitar leituras e wites (usando VirtualQuery).Então, qualquer pessoa que tentar acessar essa memória gerará uma exceção em vez de corromper o heap.

Isso pressupõe que o número de pedaços pendentes de 64 bytes seja apenas moderado ou que você tenha muita memória para gravar na caixa!

O pouco tempo que tive para resolver um problema semelhante.Se o problema persistir, sugiro que você faça o seguinte:Monitore todas as chamadas para new/delete e malloc/calloc/realloc/free.Faço uma única DLL exportando uma função para registrar todas as chamadas.Esta função recebe parâmetro para identificar seu código fonte, ponteiro para área alocada e tipo de chamada salvando esta informação em uma tabela.Todos os pares alocados/liberados são eliminados.No final ou depois você precisa fazer uma chamada para outra função para criar um relatório para os dados restantes.Com isso você pode identificar chamadas erradas (novas/gratuitas ou malloc/delete) ou ausentes.Se houver algum caso de buffer sobrescrito em seu código as informações salvas podem estar erradas mas cada teste pode detectar/descobrir/incluir uma solução de falha identificada.Muitas execuções para ajudar a identificar os erros.Boa sorte.

Você acha que isso é uma condição de corrida?Vários threads estão compartilhando um heap?Você pode fornecer a cada thread um heap privado com HeapCreate, para que eles possam ser executados rapidamente com HEAP_NO_SERIALIZE.Caso contrário, um heap deverá ser thread-safe, se você estiver usando a versão multithread das bibliotecas do sistema.

Algumas sugestões.Você mencionou os muitos avisos em W4 - eu sugeriria dedicar um tempo para corrigir seu código para compilar de forma limpa no nível de aviso 4 - isso ajudará muito a evitar bugs sutis e difíceis de encontrar.

Segundo - para a opção /analyze - ela realmente gera muitos avisos.Para usar essa opção em meu próprio projeto, o que fiz foi criar um novo arquivo de cabeçalho que usava #pragma warning para desligar todos os avisos adicionais gerados por/analyze.Mais adiante no arquivo, ativo apenas os avisos que me interessam.Em seguida, use a opção do compilador /FI para forçar esse arquivo de cabeçalho a ser incluído primeiro em todas as suas unidades de compilação.Isso deve permitir que você use a opção /analyze enquanto controla a saída

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top