Depuración de daños en la memoria


23

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_handlersen la traza inversa). Principalmente involucrando std::mapa 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::mapcorrupció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.


77
Lamentablemente, esto es muy común en aplicaciones C ++ no triviales. Si está utilizando el control de código fuente, probar varios conjuntos de cambios para reducir qué cambio de código causó el problema puede ayudar, pero quizás no sea factible en este caso.
Telastyn

Sí, realmente no es factible en mi caso. Básicamente pasé de trabajar a estar completamente roto por 2 meses y luego a la etapa de depuración donde tengo un código que funciona. El viejo sistema realmente no me permitió implementar mi nuevo código de red un poco flexible sin romper todo.
Robin

2
En este punto, puede que tenga que intentar aislar cada parte. Tome cada clase / subconjunto de la solución, haga una burla a su alrededor para que pueda funcionar, y pruebe todo hasta que encuentre la sección que falla.
Ampt

comience comentando porciones de códigos hasta que ya no tenga el bloqueo.
cpp81

1
Además de Valgrind, Coverity y cppcheck, debe agregar Asan y UBsan a su régimen de prueba. Si su código es corss-platofrm, entonces agregue Enterprise Analysis ( /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.

Respuestas:


21

Es un problema desafiante, pero sospecho que hay muchas más pistas que se pueden encontrar en los accidentes que ya has visto.

  • ¿Mantiene registros cuidadosos de accidentes para buscar patrones?
  • ¿Son los pocos lugares realmente similares? ¿En qué manera?
  • ¿Cómo se ven los datos corruptos? Ceros? Ascii? ¿Patrones?
  • ¿Hay algún subproceso múltiple involucrado? ¿Podría ser una condición de carrera?
  • ¿Está relacionado con el montón? ¿La corrupción ocurre después de un free ()?
  • ¿Está relacionado con la pila? ¿La pila se corrompe?
  • ¿Es posible una referencia colgante? ¿Un valor de datos que cambió misteriosamente?
  • ¿Hay algo distintivo sobre el tráfico de red (tamaño del búfer, ciclo de recuperación)?

Cosas que hemos usado en situaciones similares.

  • Montones y montones de afirmaciones de producción. Choque temprano y previsiblemente antes de que el daño se propague.
  • Montones y montones de guardias. Elementos de datos adicionales antes y después de variables locales, objetos y mallocs () establecidos en un valor y luego verificados con frecuencia.
  • 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.

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.

Siéntase libre de agregar detalles si podemos ayudarlo.


¿Puedo agregar que los errores seriamente indeterminados como este no son tan comunes, y no hay muchas cosas que puedan (generalmente) causarlos? Incluyen:

  • Concurrencia: roscado, condiciones de carrera, etc.
  • Interrupciones / eventos / devoluciones de llamada / excepciones: estado corrupto o la pila de forma impredecible
  • Datos inusuales: datos de entrada / tiempo / estado inusuales
  • Dependencia de un proceso externo asincrónico.

Estas son las partes del código para enfocarse.


+1 Todas las buenas sugerencias, especialmente las afirmaciones, los guardias y el registro.
andy256

Edité más información en mi pregunta como respuesta a su respuesta. Eso realmente me hizo pensar en los bloqueos al cerrar que aún no había visto extensamente, así que supongo que por ahora voy a continuar.
Robin

5

Use una versión de depuración de malloc / free. Envuélvalas y escriba las suyas si es necesario. ¡Mucha diversión!

La versión que uso agrega bytes de protección antes y después de cada asignación, y mantiene una lista "asignada" contra la cual se comprueban los fragmentos libres. Esto detecta la mayoría de los errores de desbordamiento del búfer y múltiples "libres".

Una de las fuentes más insidiosas de corrupción es seguir usando una parte después de haber sido liberada. Free debería llenar la memoria liberada con un patrón conocido (tradicionalmente, 0xDEADBEEF) Ayuda si las estructuras asignadas incluyen un elemento de "número mágico", e incluyen generosamente cheques para el número mágico apropiado antes de usar una estructura.


1
Sin embargo, Valgrind debería obtener el doble de liberaciones / uso de datos gratuitos, ¿no?
Robin

Escribir este tipo de sobrecargas para new / delete me ha ayudado a localizar numerosos problemas de corrupción de memoria. Especialmente los bytes de protección que se verifican al eliminar y provocan un punto de interrupción activado por el programa que automáticamente me deja en el depurador.
Emily L.

3

Parafraseando lo que dices en tu pregunta, no es posible darte una respuesta definitiva. Lo mejor que podemos hacer es hacer sugerencias de cosas a buscar y herramientas y técnicas.

Algunas sugerencias parecerán ingenuas, otras pueden parecer más aplicables, pero esperamos que uno active un pensamiento que puede seguir. Debo decir que la respuesta de david.pfx tiene buenos consejos y sugerencias.

De los síntomas

  • para mí suena como un desbordamiento de búfer.

  • Un problema relacionado es el uso de datos de socket no validados como subíndice o clave, etc.

  • ¿Es posible que esté utilizando una variable global en algún lugar, o que tenga una global y local con el mismo nombre, o de alguna manera los datos de un jugador interfieran con otro?

Al igual que con muchos errores, probablemente esté haciendo una suposición inválida en alguna parte. O posiblemente más de uno. Múltiples errores de interacción son difíciles de detectar.

  • ¿Cada variable tiene una descripción? ¿Y puedes definir una afirmación de validez?
    Si no los agrega, escanee el código para ver que cada variable parece usarse correctamente. Agregue esa afirmación donde tenga sentido.

  • La sugerencia de agregar muchas afirmaciones es buena: el primer lugar para colocarlas es en cada punto de entrada de función. Valide los argumentos y cualquier estado global relevante.

  • Utilizo muchos registros para depurar códigos de larga duración / asíncronos / en tiempo real.
    Nuevamente, inserte una escritura de registro en cada llamada de función.
    Si los archivos de registro se hacen demasiado grandes, las funciones de registro pueden envolver / cambiar archivos / etc.
    Es más útil si los mensajes de registro sangran con la profundidad de la llamada a la función.
    El archivo de registro puede mostrar cómo se propaga un error. Útil cuando un código hace algo que no es del todo correcto que actúa como una bomba de acción retardada.

Muchas personas tienen su propio código de registro de cosecha propia. Tengo un viejo sistema de registro de macros C en alguna parte, y tal vez una versión C ++ ...


3

Todo lo que se dijo en las otras respuestas es muy relevante. Una cosa importante mencionada parcialmente por ddyer es que envolver malloc / free tiene beneficios. Menciona algunos, pero me gustaría agregarle una herramienta de depuración muy importante: puede registrar cada malloc / free en un archivo externo junto con algunas líneas de pila de llamadas (o la pila de llamadas completa si le importa). Si tienes cuidado, puedes hacer esto bastante rápido y usarlo en producción si es necesario.

Por lo que describe, mi suposición personal es que podría estar manteniendo una referencia a un puntero en algún lugar para liberar memoria y podría terminar liberando un puntero que ya no le pertenece o que le está escribiendo. Si puede inferir un rango de tamaño para monitorear con la técnica anterior, debería poder reducir considerablemente el registro. De lo contrario, una vez que encuentre qué memoria se ha dañado, puede descubrir el patrón malloc / free que lo llevó fácilmente desde los registros.

Una nota importante es que, como mencionas, cambiar el diseño de la memoria puede ocultar el problema. Por lo tanto, es muy importante que su registro no realice asignaciones (si puede) o la menor cantidad posible. Esto ayudará a la reproducibilidad si está relacionado con la memoria. También ayudará si es lo más rápido posible si el problema está relacionado con subprocesos múltiples.

También es importante que atrape las asignaciones de bibliotecas de terceros para que también pueda registrarlas correctamente. Nunca se sabe de dónde podría venir.

Como última alternativa, también puede hacer un asignador personalizado donde asigne al menos 2 páginas para cada asignación y desasignarlas cuando esté libre (alinee la asignación a un límite de página, asigne una página antes y márquela como no accesible o alinee el asignar al final de una página y asignar una página después y marcar como no accesible). Asegúrese de no reutilizar esas direcciones de memoria virtual para nuevas asignaciones durante al menos algún tiempo. Esto implica que deberá administrar su memoria virtual usted mismo (resérvela y úsela como desee). Tenga en cuenta que esto degradará su rendimiento y podría terminar usando cantidades significativas de memoria virtual dependiendo de cuántas asignaciones lo alimente. Para mitigar esto, será útil si puede ejecutar en 64 bits y / o reducir el rango de asignaciones que lo necesitan (según el tamaño). Valgrind podría muy bien hacer esto, pero puede ser demasiado lento para que puedas entender el problema. Hacer esto solo para unos pocos tamaños u objetos (si sabe cuál, puede usar el asignador especial solo para esos objetos) garantizará que el rendimiento se vea mínimamente afectado.


0

Intente configurar un punto de observación en la dirección de memoria en la que se bloquea. GDB se romperá en la instrucción que causó la memoria no válida. Luego, con la traza inversa, puede ver su código que está causando la corrupción. Puede que esta no sea la fuente de corrupción, pero repetir el punto de observación en cada corrupción puede conducir a la fuente del problema.

Por cierto, dado que la pregunta está etiquetada como C ++, considere usar punteros compartidos que se encarguen de la propiedad manteniendo un recuento de referencia y elimine la memoria de forma segura después de que el puntero se salga del alcance. Pero úselos con precaución ya que pueden causar un punto muerto en un uso poco frecuente de dependencia circular.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.