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_handlers
no backtrace). Principalmente envolvendo std::map
uma 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::map
corrupçã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.
/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.