Depurando corrupção de memória


23

Primeiro, percebo que essa não é uma pergunta perfeita no estilo de perguntas e respostas, com uma resposta absoluta, mas não consigo pensar em nenhuma redação para fazê-la funcionar melhor. Eu não acho que exista uma solução absoluta para isso e essa é uma das razões pelas quais eu estou postando aqui em vez de Stack Overflow.

No mês passado, eu reescrevi um código do servidor bastante antigo (mmorpg) para ser mais moderno e mais fácil de estender / mod. Comecei com a parte da rede e implementei uma biblioteca de terceiros (libevent) para lidar com coisas para mim. Com todas as mudanças de re-fatoração e código, introduzi corrupção de memória em algum lugar e tenho lutado para descobrir onde isso acontece.

Parece que não consigo reproduzi-lo de forma confiável no meu ambiente de desenvolvimento / teste, mesmo ao implementar bots primitivos para simular alguma carga, não recebo mais falhas (corrigi um problema de conteúdo livre que causava algumas coisas)

Eu tentei até agora:

Valgrinding o inferno fora disso - Nenhuma gravação inválida até que a coisa trava (o que pode levar mais de 1 dia na produção .. ou apenas uma hora) o que está realmente me deixando desconcertado, certamente em algum momento ele acessaria a memória inválida e não substituiria as coisas por chance? (Existe uma maneira de "espalhar" o intervalo de endereços?)

Ferramentas de análise de código, nomeadamente cobertura e cppcheck. Enquanto eles apontaram alguns casos desagradáveis ​​e contundentes no código, não havia nada sério.

Gravar o processo até ele travar com o gdb (via undodb) e depois trabalhar o meu caminho para trás. Isso / parece / deve ser factível, mas eu acabo travando o gdb usando o recurso de preenchimento automático ou acabo em alguma estrutura de eventos internos em que me perco, pois há muitas ramificações possíveis (uma corrupção causa outra e assim por diante) em). Eu acho que seria bom se eu pudesse ver ao que um ponteiro pertence originalmente / onde foi alocado, isso eliminaria a maioria dos problemas de ramificação. Porém, eu não posso executar o valgrind com o undodb, e o registro gdb normal é inusitavelmente lento (se isso funcionar em combinação com o valgrind).

Revisão de código! Sozinho (completamente) e alguns amigos examinando meu código, embora eu duvide que tenha sido completo o suficiente. Eu estava pensando em contratar um desenvolvedor para fazer uma revisão / depuração de código comigo, mas não posso me dar muito dinheiro e não saberia onde procurar alguém que esteja disposto a trabalhar por pouco. sem dinheiro, se ele não encontrar o problema ou alguém qualificado.

Devo também observar: eu costumo receber retrocessos consistentes. Existem alguns lugares onde a falha ocorre, principalmente relacionada à classe de soquete que está sendo corrompida de alguma forma. Seja um ponteiro inválido apontando para algo que não seja um soquete ou a própria classe de soquete seja sobrescrita (parcialmente?) Com rabiscos. Embora eu suspeite que esteja travando mais lá, já que é uma das partes mais usadas, por isso é a primeira memória corrompida que é usada.

No geral, essa questão me deixou ocupada por quase dois meses (mais ou menos, mais um projeto de hobby) e está realmente me frustrando a ponto de me tornar um IRL mal-humorado e pensar em desistir. Só não consigo pensar no que mais devo fazer para encontrar o problema.

Perdi algumas técnicas úteis? Como você lida com isso? (Não pode ser tão comum, pois não há muita informação sobre isso .. ou eu sou realmente cego?)

Editar:

Algumas especificações, caso isso importe:

Usando c ++ (11) via gcc 4.7 (versão fornecida pelo debian wheezy)

A base de código tem cerca de 150 mil linhas

Edite em resposta à david.pfx post: (desculpe pela resposta lenta)

Você está mantendo registros cuidadosos de falhas, para procurar padrões?

Sim, ainda tenho despejos dos acidentes recentes por aí

Os poucos lugares são realmente parecidos? De que maneira?

Bem, na versão mais recente (eles parecem mudar sempre que adiciono / removo código ou altera estruturas relacionadas), ele sempre fica preso em um método de timer de item. Basicamente, um item tem um tempo específico após o qual expira e envia informações atualizadas ao cliente. O ponteiro de soquete inválido estaria na classe Player (ainda válida até onde eu sei), principalmente relacionada a isso. Também estou enfrentando muitas falhas na fase de limpeza, após o desligamento normal, onde está destruindo todas as classes estáticas que não foram explicitamente destruídas ( __run_exit_handlersno backtrace). Principalmente envolvendo std::mapuma classe, supondo que essa seja apenas a primeira coisa que surge.

Como são os dados corrompidos? Zeros? Ascii? Padrões?

Ainda não encontrei nenhum padrão, me parece um pouco aleatório. É difícil dizer, já que não sei onde a corrupção começou.

É relacionado à pilha?

É totalmente relacionado à pilha (habilitei o protetor de pilha do gcc e isso não capturou nada).

A corrupção acontece depois de um free()?

Você terá que elaborar um pouco sobre isso. Você quer dizer ter ponteiros de objetos já liberados por aí? Estou definindo como nula todas as referências quando o objeto é destruído, então, a menos que eu tenha perdido algo em algum lugar, não. Isso deve aparecer em valgrind, mas não foi.

Existe algo distinto no tráfego da rede (tamanho do buffer, ciclo de recuperação)?

O tráfego de rede consiste em dados brutos. Portanto, matrizes char, (u) intX_t ou estruturas compactadas (para remover preenchimento) para coisas mais complexas, cada pacote possui um cabeçalho que consiste em um ID e o próprio tamanho do pacote que é validado com relação ao tamanho esperado. Eles têm entre 10 e 60 bytes de tamanho, com o maior (pacote interno de 'inicialização', disparado uma vez na inicialização), com um tamanho de alguns Mb.

Muitas e muitas afirmações de produção. Falha cedo e previsivelmente antes que o dano se propague.

Uma vez tive um acidente relacionado à std::mapcorrupção, cada entidade tem um mapa de sua "visualização", cada entidade que pode vê-lo e vice-versa está nesse. Adicionei um buffer de 200 bytes na frente e depois, preenchi com 0x33 e verifiquei antes de cada acesso. A corrupção simplesmente desapareceu magicamente, devo ter movido algo ao redor que a tornou corrompida outra coisa.

Registro estratégico, para que você saiba com precisão o que estava acontecendo pouco antes. Adicione ao registro à medida que você se aproxima de uma resposta.

Funciona .. a uma extensão.

Desesperado, você pode salvar o estado e reiniciar automaticamente? Eu posso pensar em algumas peças de software de produção que fazem isso.

Eu meio que faço isso. O software consiste em um processo principal de "cache" e em outros processos de trabalho, todos acessando o cache para obter e salvar coisas. Portanto, por acidente, não perco muito progresso, ele ainda desconecta todos os usuários e assim por diante, definitivamente não é uma solução.

Simultaneidade: rosqueamento, condições da corrida, etc.

Existe um encadeamento mysql para fazer consultas "assíncronas", porém tudo intocado e compartilha apenas informações com a classe do banco de dados através de funções com todos os bloqueios.

Interrompe

Existe um cronômetro de interrupção para impedir que ele seja bloqueado e que seja interrompido se não for concluído um ciclo por 30 segundos, mas esse código deve ser seguro:

if (!tics) {
    abort();
} else
    tics = 0;

Os tiques volatile int tics = 0;aumentam cada vez que um ciclo é concluído. Código antigo também.

eventos / retornos de chamada / exceções: estado de corrupção ou pilha imprevisível

Muitos retornos de chamada estão sendo usados ​​(E / S de rede assíncrona, timers), mas não devem fazer nada de errado.

Dados incomuns: dados de entrada / cronometragem / estado incomuns

Eu tive alguns casos extremos relacionados a isso. Desconectar um soquete enquanto os pacotes ainda estão sendo processados ​​resultou no acesso a um nullptr e tal, mas esses foram fáceis de detectar até agora, pois todas as referências são limpas logo após dizer à classe em si que está pronto. (A destruição em si é tratada por um loop que exclui todos os objetos destruídos a cada ciclo)

Dependência de um processo externo assíncrono.

Cuidado ao elaborar? É esse o caso, o processo de cache mencionado acima. A única coisa que eu poderia imaginar em cima da minha cabeça seria não terminar rápido o suficiente e usar dados de lixo, mas esse não é o caso, já que também está usando rede. Mesmo modelo de pacote.


7
Infelizmente, isso é comum em aplicativos C ++ não triviais. Se você estiver usando o controle de origem, testar vários conjuntos de alterações para restringir qual alteração de código causou o problema pode ajudar, mas talvez não seja viável nesse caso.
Telastyn

Sim, realmente não é viável no meu caso. Eu basicamente passei do trabalho para o total e completamente quebrado por 2 meses e depois para o estágio de depuração, onde eu tenho um código que funciona. O sistema antigo realmente não me permitiu implementar um código de rede flexível, sem quebrar tudo.
Robin

2
Nesse ponto, talvez seja necessário tentar isolar cada parte. Pegue cada classe / subconjunto da solução, faça uma zombaria para que ela funcione e teste o inferno até encontrar a seção que falhou.
Ampt

comece comentando partes dos códigos até que você não tenha mais a falha.
precisa saber é

1
Além de Valgrind, Coverity e cppcheck, você deve adicionar Asan e UBsan ao seu regime de teste. Se o seu código for corss-platofrm, adicione o Enterprise Analysis ( /analyze) da Microsoft e os guardas Malloc e Scribble da Apple também. Você também deve usar o maior número possível de compiladores, usando o maior número possível de padrões, pois os avisos do compilador são um diagnóstico e melhoram com o tempo. Não há bala de prata e um tamanho não serve para todos. Quanto mais ferramentas e compiladores você usar, mais completa será a cobertura, pois cada ferramenta possui seus pontos fortes e fracos.

Respostas:


21

É um problema desafiador, mas suspeito que há muito mais pistas para encontrar nas falhas que você já viu.

  • Você está mantendo registros cuidadosos de falhas, para procurar padrões?
  • Os poucos lugares são realmente parecidos? De que maneira?
  • Como são os dados corrompidos? Zeros? Ascii? Padrões?
  • Existe algum multi-threading envolvido? Poderia ser uma condição de corrida?
  • É relacionado à pilha? A corrupção acontece depois de um free ()?
  • É relacionado à pilha? A pilha é corrompida?
  • Uma referência pendente é uma possibilidade? Um valor de dados que mudou misteriosamente?
  • Existe algo distinto no tráfego da rede (tamanho do buffer, ciclo de recuperação)?

Coisas que usamos em situações semelhantes.

  • Muitas e muitas afirmações de produção. Falha cedo e previsivelmente antes que o dano se propague.
  • Muitos e muitos guardas. Itens de dados extras antes e depois de variáveis ​​locais, objetos e mallocs () configurados para um valor e depois verificados com frequência.
  • Registro estratégico, para que você saiba com precisão o que estava acontecendo pouco antes. Adicione ao registro à medida que você se aproxima de uma resposta.

Desesperado, você pode salvar o estado e reiniciar automaticamente? Eu posso pensar em algumas peças de software de produção que fazem isso.

Sinta-se à vontade para adicionar detalhes, se pudermos ajudar.


Posso apenas acrescentar que erros seriamente indeterminados como este não são tão comuns assim, e não há muitas coisas que (normalmente) possam causá-los. Eles incluem:

  • Simultaneidade: rosqueamento, condições da corrida, etc.
  • Interrupções / eventos / retornos de chamada / exceções: estado corrompido ou a pilha imprevisivelmente
  • Dados incomuns: dados de entrada / hora / estado não usuais
  • Dependência de um processo externo assíncrono.

Essas são as partes do código para focar.


+1 Todas as boas sugestões, especialmente as afirmações, guardas e registro.
precisa saber é o seguinte

Editei mais algumas informações na minha pergunta como resposta à sua resposta. Na verdade, isso me fez pensar nas falhas ao desligar, que eu ainda não tinha visto extensivamente, então vou continuar com isso por enquanto, acho.
Robin

5

Use uma versão de depuração do malloc / free. Embrulhe-os e escreva seus próprios, se necessário. Muita diversão!

A versão que eu uso adiciona bytes de guarda antes e depois de cada alocação, e mantém uma lista "alocada", na qual são verificados gratuitamente os blocos liberados. Isso captura a maioria da saturação de buffer e vários erros "livres" não autorizados.

Uma das fontes mais insidiosas de corrupção continua a usar um pedaço depois que ele foi libertado. O Free deve preencher a memória liberada com um padrão conhecido (tradicionalmente, 0xDEADBEEF). Ajuda se as estruturas alocadas incluírem um elemento "número mágico" e incluir liberalmente verificações do número mágico apropriado antes de usar uma estrutura.


1
O Valgrind deve pegar o dobro de liberações / uso de dados gratuitos, não deveria?
Robin

Escrever esse tipo de sobrecarga para novo / excluir me ajudou a localizar vários problemas de corrupção de memória. Especialmente os bytes de proteção que são verificados na exclusão e causam um ponto de interrupção acionado pelo programa que automaticamente me coloca no depurador.
Emily L.

3

Parafraseando o que você diz na sua pergunta, não é possível dar uma resposta definitiva. O melhor que podemos fazer é fazer sugestões sobre o que procurar e ferramentas e técnicas.

Algumas sugestões parecerão ingênuas, outras podem parecer mais aplicáveis, mas, com sorte, uma desencadeia um pensamento que você pode seguir. Devo dizer que a resposta de david.pfx tem bons conselhos e sugestões.

Dos sintomas

  • para mim, parece uma saturação de buffer.

  • um problema relacionado está usando dados de soquete não validados como um subscrito ou chave, etc.

  • é possível que você esteja usando uma variável global em algum lugar, ou tenha uma global e local com o mesmo nome, ou de alguma forma os dados de um jogador interfiram em outro?

Como em muitos bugs, você provavelmente está fazendo uma suposição inválida em algum lugar. Ou possivelmente mais de um. Vários erros de interação são difíceis de detectar.

  • Toda variável tem uma descrição? E você pode definir uma asserção de validade?
    Caso contrário, varra o código para ver se cada variável parece estar sendo usada corretamente. Adicione essa afirmação onde quer que faça sentido.

  • A sugestão de adicionar afirmação de lotes é boa: o primeiro lugar para colocá-los é em todos os pontos de entrada de funções. Valide os argumentos e qualquer estado global relevante.

  • Uso muitos logs para depurar códigos de longa duração / assíncronos / em tempo real.
    Novamente, insira uma gravação de log em todas as chamadas de função.
    Se os arquivos de log ficarem muito grandes, as funções de log poderão quebrar / alternar arquivos / etc.
    É mais útil se as mensagens de log recuarem com a profundidade da chamada da função.
    O arquivo de log pode mostrar como um erro se propaga. Útil quando um pedaço de código faz algo não muito certo que age como uma bomba de ação atrasada.

Muitas pessoas têm seu próprio código de registro em casa. Eu tenho um antigo sistema de log de macro C em algum lugar, e talvez uma versão C ++ ...


3

Tudo o que foi dito nas outras respostas é muito relevante. Uma coisa importante mencionada parcialmente pela ddyer é que a embalagem do malloc / free tem benefícios. Ele menciona algumas, mas eu gostaria de adicionar uma ferramenta de depuração muito importante: você pode registrar todos os malloc / free em um arquivo externo, juntamente com algumas linhas de pilha de chamadas (ou a pilha de chamadas completa, se você se importa). Se você for cuidadoso, poderá facilmente fazer isso muito rápido e usá-lo na produção, se for necessário.

Pelo que você descreve, meu palpite pessoal é que você pode estar mantendo uma referência a um ponteiro em algum lugar para liberar memória e pode acabar liberando um ponteiro que não pertence mais a você ou que está escrevendo nele. Se você puder inferir um intervalo de tamanho para monitorar com a técnica acima, poderá reduzir consideravelmente o log. Caso contrário, depois de descobrir qual memória foi corrompida, você poderá descobrir o padrão malloc / free que a levou a partir dos logs com bastante facilidade.

Uma observação importante é que, como você mencionou, alterar o layout da memória pode ocultar o problema. Portanto, é muito importante que o seu log não faça alocações (se você puder!) Ou o mínimo possível. Isso ajudará a reprodutibilidade se estiver relacionado à memória. Também ajudará se for o mais rápido possível se o problema estiver relacionado a vários segmentos.

Também é importante capturar alocações de bibliotecas de terceiros para que você possa registrá-las adequadamente. Você nunca sabe de onde ela pode vir.

Como última alternativa, você também pode criar um alocador personalizado onde aloca pelo menos 2 páginas para cada alocação e mapeá-las quando liberar (alinhe a alocação ao limite de uma página, aloque uma página antes e marque-a como não acessível ou alinhe o aloque no final de uma página e aloque uma página depois e a marca não está acessível). Certifique-se de não reutilizar esses endereços de memória virtual para novas alocações por pelo menos algum tempo. Isso implica que você precisará gerenciar sua memória virtual (reserve e use-a como quiser). Observe que isso prejudicará seu desempenho e poderá acabar usando quantidades significativas de memória virtual, dependendo de quantas alocações você o alimenta. Para atenuar isso, ajudará se você puder executar em 64 bits e / ou reduzir o intervalo de alocações que precisam disso (com base no tamanho). O Valgrind pode muito bem já fazer isso, mas pode ser muito lento para você entender o problema. Fazer isso apenas para alguns tamanhos ou objetos (se você souber qual deles, poderá usar o alocador especial apenas para esses objetos) garantirá que o desempenho seja minimamente impactado.


0

Tente definir um ponto de observação no endereço de memória em que ele trava. O GDB será interrompido na instrução que causou a memória inválida. Então, com o rastreamento posterior, você pode ver seu código que está causando a corrupção. Isso pode não ser a fonte da corrupção, mas a repetição do ponto de controle em cada corrupção pode levar à origem do problema.

A propósito, como a pergunta está marcada como C ++, considere o uso de ponteiros compartilhados que cuidam da propriedade mantendo uma contagem de referência e exclua a memória com segurança após o ponteiro sair do escopo. Mas use-os com cuidado, pois podem causar impasse em um uso raro de dependência circular.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.