En primer lugar, me doy cuenta de que esta no es una pregunta de estilo de preguntas y respuestas perfectas con una respuesta absoluta, pero no se me ocurre ninguna redacción para que funcione mejor. No creo que haya una solución absoluta para esto y esta es una de las razones por las que lo publico aquí en lugar de Stack Overflow.
Durante el último mes he estado reescribiendo un código de servidor bastante antiguo (mmorpg) para que sea más moderno y más fácil de extender / modificar. Comencé con la parte de la red e implementé una biblioteca de terceros (libevent) para manejar cosas por mí. Con todos los cambios de código y refactorización, introduje daños en la memoria en algún lugar y he estado luchando para descubrir dónde sucede.
Parece que no puedo reproducirlo de manera confiable en mi entorno de desarrollo / prueba, incluso cuando implemento bots primitivos para simular alguna carga ya no me cuelgo (arreglé un problema liberador que causó algunas cosas)
Lo he intentado hasta ahora:
Valorando muchísimo: no hay escrituras inválidas hasta que la cosa se cuelga (lo que puede llevar más de 1 día en producción ... o solo una hora), lo que realmente me desconcierta, seguramente en algún momento accederá a la memoria no válida y no sobrescribirá las cosas. ¿oportunidad? (¿Hay alguna manera de "extender" el rango de direcciones?)
Herramientas de análisis de código, a saber, la cobertura y cppcheck. Si bien señalaron algunos ... casos desagradables y extremos en el código, no había nada serio.
Grabando el proceso hasta que se bloquea con gdb (a través de undodb) y luego trabajando hacia atrás. Esto / suena / debería ser factible, pero termino bloqueando gdb usando la función de autocompletar o termino en alguna estructura liberadora interna donde me pierdo ya que hay demasiadas ramas posibles (una corrupción causa otra y así en). Supongo que sería bueno si pudiera ver a qué pertenece originalmente un puntero / dónde se asignó, eso eliminaría la mayoría de los problemas de ramificación. Sin embargo, no puedo ejecutar valgrind con undodb, y el registro gdb normal es inusualmente lento (si eso incluso funciona en combinación con valgrind).
¡Revisión de código! Solo (a fondo) y haciendo que algunos amigos revisen mi código, aunque dudo que sea lo suficientemente completo. Estaba pensando en tal vez contratar a un desarrollador para que haga una revisión / depuración de código conmigo, pero no puedo permitirme poner demasiado dinero y no sabría dónde buscar a alguien que estaría dispuesto a trabajar por poco. no tiene dinero si no encuentra el problema o alguien calificado.
También debo tener en cuenta: por lo general obtengo retrocesos constantes. Hay algunos lugares donde ocurre el bloqueo, principalmente relacionado con la clase de socket que se corrompe de alguna manera. Ya sea un puntero no válido que apunta a algo que no es un zócalo o la clase del zócalo se sobrescribe (¿parcialmente?) Con galimatías. Aunque sospecho que está fallando allí, ya que esa es una de las partes más utilizadas, por lo que es la primera memoria dañada que se usa.
En general, este problema me ha mantenido ocupado durante casi 2 meses (de vez en cuando, más como un proyecto de pasatiempo) y realmente me frustra hasta el punto en que me vuelvo gruñón IRL y pienso en renunciar. Simplemente no puedo pensar en qué más debo hacer para encontrar el problema.
¿Hay alguna técnica útil que me haya perdido? ¿Cómo lidias con eso? (No puede ser tan común ya que no hay mucha información sobre esto ... ¿o estoy realmente ciego?)
Editar:
Algunas especificaciones en caso de que importe:
Usando c ++ (11) a través de gcc 4.7 (versión provista por debian wheezy)
El código base es de alrededor de 150 mil líneas.
Editar en respuesta a la publicación david.pfx: (perdón por la lenta respuesta)
¿Mantiene registros cuidadosos de accidentes para buscar patrones?
Sí, todavía tengo vertederos de los últimos accidentes por ahí
¿Son los pocos lugares realmente similares? ¿En qué manera?
Bueno, en la versión más reciente (parecen cambiar cada vez que agrego / elimino código o cambio estructuras relacionadas) siempre quedaría atrapado en un método de temporizador de elemento. Básicamente, un elemento tiene un tiempo específico después del cual caduca y envía información actualizada al cliente. El puntero de socket inválido estaría en la clase de jugador (todavía válido hasta donde puedo decir), principalmente relacionado con eso. También estoy experimentando un montón de bloqueos en la fase de limpieza, después del apagado normal, donde está destruyendo todas las clases estáticas que no se han destruido explícitamente ( __run_exit_handlers
en la traza inversa). Principalmente involucrando std::map
a una clase, suponiendo que eso es solo lo primero que surge.
¿Cómo se ven los datos corruptos? Ceros? Ascii? ¿Patrones?
Todavía no he encontrado ningún patrón, me parece algo aleatorio. Es difícil saberlo porque no sé dónde comenzó la corrupción.
¿Está relacionado con el montón?
Está completamente relacionado con el montón (habilité el protector de pila de gcc y eso no captó nada).
¿La corrupción ocurre después de un
free()
?
Vas a tener que elaborar un poco sobre eso. ¿Te refieres a tener punteros de objetos ya liberados por ahí? Estoy configurando cada referencia a nulo una vez que el objeto se destruye, así que a menos que me haya perdido algo en alguna parte, no. Eso debería aparecer en valgrind, aunque no fue así.
¿Hay algo distintivo sobre el tráfico de red (tamaño del búfer, ciclo de recuperación)?
El tráfico de red consta de datos sin procesar. Por lo tanto, las matrices de caracteres, (u) intX_t o estructuras empaquetadas (para eliminar el relleno) para cosas más complejas, cada paquete tiene un encabezado que consiste en una identificación y el tamaño del paquete en sí que se valida con el tamaño esperado. Son alrededor de 10-60bytes con el tamaño más grande (paquete de "arranque" interno, disparado una vez al inicio) que tiene un tamaño de unos pocos Mb.
Montones y montones de afirmaciones de producción. Choque temprano y previsiblemente antes de que el daño se propague.
Una vez tuve un bloqueo relacionado con la std::map
corrupción, cada entidad tiene un mapa de su "vista", cada entidad que puede verlo y viceversa está en eso. Agregué un búfer de 200 bytes en el frente y después, lo llené con 0x33 y lo revisé antes de cada acceso. La corrupción simplemente desapareció mágicamente, debo haber movido algo que la corrompió de otra manera.
Registro estratégico, para que sepa con precisión lo que estaba sucediendo justo antes. Agregue al registro a medida que se acerca a una respuesta.
Funciona ... hasta cierto punto.
En la desesperación, ¿puede guardar el estado y reiniciar automáticamente? Puedo pensar en algunas piezas de software de producción que hacen eso.
De alguna manera hago eso. El software consiste en un proceso principal de "caché" y algunos otros procesos de trabajo que acceden al caché para obtener y guardar cosas. Entonces, por accidente, no pierdo mucho progreso, todavía desconecta a todos los usuarios y así sucesivamente, definitivamente no es una solución.
Concurrencia: roscado, condiciones de carrera, etc.
Hay un hilo de mysql para hacer consultas "asíncronas", sin embargo, todo está intacto y solo comparte información con la clase de base de datos a través de funciones con todos los bloqueos.
Interrupciones
Hay un temporizador de interrupción para evitar que se bloquee que simplemente aborta si no completó un ciclo durante 30 segundos, aunque ese código debería ser seguro:
if (!tics) {
abort();
} else
tics = 0;
tics es el volatile int tics = 0;
que aumenta cada vez que se completa un ciclo. Código antiguo también.
eventos / devoluciones de llamada / excepciones: estado corrupto o la pila de forma impredecible
Se están utilizando muchas devoluciones de llamada (E / S de red asíncrona, temporizadores), pero no deberían hacer nada malo.
Datos inusuales: datos de entrada / temporización / estado inusuales
He tenido algunos casos extremos relacionados con eso. La desconexión de un socket mientras los paquetes aún se están procesando dio como resultado el acceso a un nullptr y tal, pero hasta ahora han sido fáciles de detectar, ya que cada referencia se limpia inmediatamente después de decirle a la clase que ya está hecha. (La destrucción en sí se maneja mediante un ciclo que elimina todos los objetos destruidos en cada ciclo)
Dependencia de un proceso externo asincrónico.
¿Cuidado para elaborar? Este es el caso, el proceso de caché mencionado anteriormente. Lo único que podría imaginar fuera de mi cabeza sería que no terminara lo suficientemente rápido y usara datos basura, pero ese no es el caso ya que también usa la red. Mismo modelo de paquete.
/analyze
) de Microsoft y los guardias Malloc y Scribble de Apple también. También debe utilizar tantos compiladores como sea posible utilizando tantos estándares como sea posible porque las advertencias del compilador son un diagnóstico y mejoran con el tiempo. No hay bala de plata, y una talla no sirve para todos. Cuantas más herramientas y compiladores use, más completa será la cobertura porque cada herramienta tiene sus fortalezas y debilidades.