c++ windows multithreading debugging memory

c++ - Heap corrupción bajo Win32; como ubicar



windows multithreading (15)

Estoy trabajando en una aplicación C ++ multiproceso que está corrompiendo el montón. Las herramientas habituales para localizar esta corrupción parecen ser inaplicables. Las compilaciones antiguas (18 meses) del código fuente exhiben el mismo comportamiento que la versión más reciente, por lo que esto ha existido durante mucho tiempo y simplemente no se notó; En el lado negativo, los deltas de origen no se pueden usar para identificar cuándo se introdujo el error: hay muchos cambios de código en el repositorio.

El mensaje para el comportamiento de bloqueo es generar rendimiento en este sistema: transferencia de datos de socket que se transforma en una representación interna. Tengo un conjunto de datos de prueba que periódicamente harán que la aplicación haga una excepción (varios lugares, varias causas, incluida la falla de asignación de montón, por lo tanto: corrupción de montón).

El comportamiento parece estar relacionado con la potencia de la CPU o el ancho de banda de la memoria; Cuanto más de cada uno tiene la máquina, más fácil es chocar. Deshabilitar un núcleo de hiperprocesamiento o un núcleo de doble núcleo reduce la tasa de corrupción (pero no elimina). Esto sugiere un problema relacionado con el tiempo.

Ahora aquí está el problema:
Cuando se ejecuta en un entorno de depuración ligero (por ejemplo, Visual Studio 98 / AKA MSVC6 ), la corrupción del montón es razonablemente fácil de reproducir: transcurren diez o quince minutos antes de que algo falle de manera horrenda y excepciones, como una alloc; cuando se ejecuta en un entorno de depuración sofisticado (Rational Purify, VS2008/MSVC9 o incluso Microsoft Application Verifier), el sistema se limita a la velocidad de la memoria y no se bloquea (Vinculado a la memoria: la CPU no supera el 50% , la luz del disco no está encendida , el programa funciona tan rápido como puede, la caja consume 1.3G de 2G de RAM). Entonces, tengo que elegir entre poder reproducir el problema (pero no identificar la causa) o poder identificar la causa o un problema que no puedo reproducir.

Mis mejores conjeturas actuales sobre dónde seguir es:

  1. Obtenga una caja increíblemente desagradable (para reemplazar la caja de desarrollo actual: 2 Gb de RAM en un E6550 Core2 Duo ); esto hará posible reprobar el bloqueo que causa el mal comportamiento cuando se ejecuta bajo un poderoso entorno de depuración; o
  2. Vuelva a escribir operadores new y delete para usar VirtualAlloc y VirtualProtect para marcar la memoria como de solo lectura tan pronto como termine. Ejecute bajo MSVC6 y MSVC6 que el sistema operativo atrape al chico malo que está escribiendo para liberar memoria. Sí, esto es un signo de desesperación: ¿quién demonios reescribe new y delete ? Me pregunto si esto lo hará tan lento como en Purify et al.

Y no: el envío con instrumentación Purify incorporada no es una opción.

Un colega simplemente pasó y preguntó "¿Desbordamiento de pila? ¿Estamos recibiendo desbordamientos de pila ahora?!?"

Y ahora, la pregunta: ¿cómo ubico el corruptor de montón?

Actualización: equilibrar new[] y delete[] parece haber recorrido un largo camino hacia la solución del problema. En lugar de 15 minutos, la aplicación ahora dura aproximadamente dos horas antes de fallar. Aún no está allí. ¿Alguna sugerencia adicional? La corrupción del montón persiste.

Actualización: una versión de lanzamiento bajo Visual Studio 2008 parece dramáticamente mejor; La sospecha actual se basa en la implementación de STL que se incluye con VS98 .

  1. Reproduce el problema. Dr Watson producirá un volcado que podría ser útil en futuros análisis.

Tomaré nota de eso, pero me preocupa que el Dr. Watson solo se tropezará después del hecho, no cuando se pisotee el montón.

Otro intento podría ser usar WinDebug como una herramienta de depuración que es bastante poderosa y al mismo tiempo ligera.

Lo tengo en marcha en este momento, de nuevo: no mucha ayuda hasta que algo sale mal. Quiero atrapar al vándalo en el acto.

Tal vez estas herramientas le permitirán al menos limitar el problema a ciertos componentes.

No tengo muchas esperanzas, pero los tiempos desesperados requieren ...

¿Y está seguro de que todos los componentes del proyecto tienen la configuración correcta de la biblioteca de tiempo de ejecución ( C/C++ tab , categoría Generación de código en la configuración del proyecto VS 6.0)?

No, no lo estoy, y mañana pasaré un par de horas revisando el espacio de trabajo (58 proyectos en él) y comprobando que todos están compilando y vinculando con las banderas apropiadas.

Actualización: esto tomó 30 segundos. Seleccione todos los proyectos en el cuadro de diálogo Settings , anule la selección hasta que encuentre los proyectos que no tienen la configuración correcta (todos tenían la configuración correcta).


¿Crees que esta es una condición de carrera? ¿Múltiples hilos comparten un montón? ¿Puede dar a cada subproceso un montón privado con HeapCreate? Entonces pueden ejecutarse rápidamente con HEAP_NO_SERIALIZE. De lo contrario, un montón debería ser seguro para subprocesos, si está utilizando la versión multiproceso de las bibliotecas del sistema.


¿Está esto en condiciones de poca memoria? Si es así, es posible que lo nuevo devuelva NULL lugar de arrojar std :: bad_alloc. Los compiladores de VC++ no implementaron esto correctamente. Hay un artículo sobre fallas de asignación de memoria heredadas que STL aplicaciones STL creadas con VC6 .


Debe atacar este problema con tiempo de ejecución y análisis estático.

Para el análisis estático, considere compilar con PREfast ( cl.exe /analyze ). Detecta delete y delete[] no coincidentes delete[] , desbordamientos de búfer y una serie de otros problemas. Sin embargo, prepárese para atravesar muchos kilobytes de advertencia de L6, especialmente si su proyecto aún no tiene L4 fijo.

PREfast está disponible con Visual Studio Team System y, apparently , como parte del SDK de Windows.


Ejecute la aplicación original con ADplus -crash -pn appnename.exe Cuando ADplus -crash -pn appnename.exe el problema de memoria, obtendrá un volcado grande y agradable.

Puede analizar el volcado para determinar qué ubicación de memoria estaba dañada. Si tiene suerte, la memoria de sobrescritura es una cadena única que puede averiguar de dónde vino. Si no tiene suerte, tendrá que cavar en el montón win32 y averiguar cuáles fueron las características de la memoria original. (Heap -x podría ayudar)

Después de saber lo que estuvo mal, puede reducir el uso del appverifier con configuraciones especiales de almacenamiento dinámico. es decir, puede especificar qué DLL supervisa o qué tamaño de asignación supervisar.

Esperemos que esto acelere el monitoreo lo suficiente como para atrapar al culpable.

En mi experiencia, nunca necesité el modo de verificador dinámico completo, pero pasé mucho tiempo analizando los volcados de memoria y examinando las fuentes.

PD: puede usar DebugDiag para analizar los volcados. Puede señalar la DLL posee el montón dañado y darle otros detalles útiles.


El poco tiempo que tuve para resolver un problema similar. Si el problema persiste, le sugiero que haga esto: Supervise todas las llamadas a new / delete y malloc / calloc / realloc / free. Realizo una sola DLL exportando una función para registrar todas las llamadas. Esta función recibe el parámetro para identificar la fuente de su código, el puntero al área asignada y el tipo de llamada guardando esta información en una tabla. Se elimina todo el par asignado / liberado. Al final o después de que necesite, realice una llamada a otra función para crear un informe para los datos que quedan. Con esto puede identificar llamadas incorrectas (nuevas / gratuitas o malloc / eliminar) o perdidas. Si se sobrescribe el búfer en su código, la información guardada puede ser incorrecta, pero cada prueba puede detectar / descubrir / incluir una solución de falla identificada. Muchas ejecuciones para ayudar a identificar los errores. Buena suerte.


Entonces, de la información limitada que tiene, esto puede ser una combinación de una o más cosas:

  • Uso incorrecto del almacenamiento dinámico, es decir, liberaciones dobles, lectura libre, escritura libre, configurando el indicador HEAP_NO_SERIALIZE con asignaciones y liberaciones de varios subprocesos en el mismo montón
  • Sin memoria
  • Código incorrecto (es decir, desbordamientos del búfer, desbordamientos del búfer, etc.)
  • Problemas de "sincronización"

Si se trata de los dos primeros pero no el último, ya debería haberlo detectado con pageheap.exe.

Lo que probablemente significa que se debe a cómo el código está accediendo a la memoria compartida. Desafortunadamente, rastrear eso será bastante doloroso. El acceso no sincronizado a la memoria compartida a menudo se manifiesta como problemas extraños de "sincronización". Cosas como no usar la semántica de adquirir / liberar para sincronizar el acceso a la memoria compartida con un indicador, no usar bloqueos de manera adecuada, etc.

Como mínimo, ayudaría poder rastrear las asignaciones de alguna manera, como se sugirió anteriormente. Al menos, puede ver lo que realmente sucedió hasta la corrupción del montón e intentar diagnosticar a partir de eso.

Además, si puede redirigir fácilmente las asignaciones a varios montones, puede intentarlo para ver si eso soluciona el problema o da como resultado un comportamiento de errores más reproducible.

Cuando estaba probando con VS2008, ¿ejecutó con HeapVerifier con Conserve Memory configurado en Yes? Eso podría reducir el impacto en el rendimiento del asignador de almacenamiento dinámico. (Además, debe ejecutarlo Debug-> Start with Application Verifier, pero es posible que ya lo sepa).

También puede intentar depurar con Windbg y varios usos del comando! Heap.

MSN


Hemos tenido bastante buena suerte al escribir nuestras propias funciones malloc y gratuitas. En producción, simplemente llaman al malloc estándar y gratis, pero en la depuración, pueden hacer lo que quieras. También tenemos una clase base simple que no hace nada más que anular los operadores nuevos y eliminar para usar estas funciones, entonces cualquier clase que escriba simplemente puede heredar de esa clase. Si tiene una tonelada de código, puede ser un gran trabajo reemplazar las llamadas a malloc y gratis a las nuevas malloc y gratis (¡no se olvide de realloc!), Pero a la larga es muy útil.

En el libro de Steve Maguire Writing Solid Code (muy recomendado), hay ejemplos de cosas de depuración que puede hacer en estas rutinas, como:

  • Lleve un registro de las asignaciones para encontrar fugas
  • Asigne más memoria de la necesaria y coloque marcadores al principio y al final de la memoria; durante la rutina gratuita, puede asegurarse de que estos marcadores sigan allí
  • memset la memoria con un marcador en la asignación (para encontrar el uso de memoria no inicializada) y en libre (para encontrar el uso de memoria libre)

Otra buena idea es nunca usar cosas como strcpy , strcat o sprintf ; siempre use strncpy , strncat y snprintf . También hemos escrito nuestras propias versiones de estos, para asegurarnos de no descartar el final de un búfer, y estos también han detectado muchos problemas.


Intentó compilaciones antiguas, pero ¿hay alguna razón por la que no pueda seguir más atrás en el historial del repositorio y ver exactamente cuándo se introdujo el error?

De lo contrario, sugeriría agregar un registro simple de algún tipo para ayudar a rastrear el problema, aunque no sé qué es lo que específicamente desea registrar.

Si puede averiguar exactamente qué PUEDE causar este problema, a través de Google y la documentación de las excepciones que está obteniendo, tal vez eso le dará una idea más clara sobre qué buscar en el código.


La aparente aleatoriedad de la corrupción de la memoria se parece mucho a un problema de sincronización de subprocesos: se reproduce un error dependiendo de la velocidad de la máquina. Si los objetos (fragmentos de memoria) se comparten entre subprocesos y las primitivas de sincronización (sección crítica, mutex, semáforo, otros) no son por clase (por objeto, por clase), entonces es posible llegar a una situación donde la clase (fragmento de memoria) se elimina / libera mientras está en uso, o se usa después de eliminar / liberar.

Como prueba para eso, puede agregar primitivas de sincronización a cada clase y método. Esto hará que su código sea más lento porque muchos objetos tendrán que esperar el uno al otro, pero si esto elimina la corrupción del montón, su problema de corrupción del montón se convertirá en uno de optimización de código.


La sugerencia de Graeme de malloc personalizado / gratis es una buena idea. Vea si puede caracterizar algún patrón sobre la corrupción para que pueda manejarlo.

Por ejemplo, si siempre está en un bloque del mismo tamaño (digamos 64 bytes), cambie su par malloc / free para asignar siempre fragmentos de 64 bytes en su propia página. Cuando libere un fragmento de 64 bytes, configure los bits de protección de memoria en esa página para evitar lecturas y wites (usando VirtualQuery). Entonces, cualquiera que intente acceder a esta memoria generará una excepción en lugar de corromper el montón.

¡Esto supone que el número de fragmentos de 64 bytes pendientes es solo moderado o que tiene mucha memoria para grabar en la caja!


Mi primera acción sería la siguiente:

  1. Cree los binarios en la versión "Release" pero creando un archivo de información de depuración (encontrará esta posibilidad en la configuración del proyecto).
  2. Use el Dr. Watson como depurador de defualt (DrWtsn32 -I) en una máquina en la que desea reproducir el problema.
  3. Repita el problema. El Dr. Watson producirá un volcado que podría ser útil en futuros análisis.

Otro intento podría ser usar WinDebug como una herramienta de depuración que es bastante poderosa y al mismo tiempo ligera.

Tal vez estas herramientas le permitirán al menos limitar el problema a ciertos componentes.

¿Y está seguro de que todos los componentes del proyecto tienen la configuración correcta de la biblioteca de tiempo de ejecución (pestaña C / C ++, categoría Generación de código en la configuración del proyecto VS 6.0)?


Mi primera opción sería una herramienta de pageheap.exe dinámico dedicada, como pageheap.exe .

Reescribir nuevo y eliminar puede ser útil, pero eso no capta las asignaciones comprometidas por el código de nivel inferior. Si esto es lo que desea, es mejor low-level alloc API utilizando Microsoft Detours.

También comprobaciones de sanidad como: verificar la coincidencia de las bibliotecas en tiempo de ejecución (versión frente a depuración, multiproceso frente a subproceso único, dll frente a lib estática), buscar eliminaciones incorrectas (p. Ej., Eliminar donde eliminar [] debería haber sido usado), asegúrese de que no está mezclando y haciendo coincidir sus alocs.

También intente apagar selectivamente los hilos y ver cuándo / si el problema desaparece.

¿Cómo se ve la pila de llamadas, etc. en el momento de la primera excepción?


Si elige reescribir nuevo / eliminar, he hecho esto y tengo un código fuente simple en:

Esto detecta pérdidas de memoria y también inserta datos de protección antes y después del bloqueo de memoria para capturar la corrupción del montón. Puede integrarse con él colocando #include "debug.h" en la parte superior de cada archivo CPP y definiendo DEBUG y DEBUG_MEM.


Tengo los mismos problemas en mi trabajo (también usamos VC6 veces). Y no hay una solución fácil para ello. Solo tengo algunas pistas:

  • Pruebe con los volcados automáticos en la máquina de producción (consulte Proceso Dumper ). Mi experiencia dice que el Dr. Watson no es perfecto para tirar.
  • Elimine todas las capturas (...) de su código. A menudo ocultan serias excepciones de memoria.
  • Verifique la depuración avanzada de Windows : hay muchos consejos excelentes para problemas como el suyo. Recomiendo esto con todo mi corazón.
  • Si usa STL pruebe STLPort y compilaciones verificadas. El iterador inválido es el infierno.

Buena suerte. Problemas como los suyos nos toman meses para resolver. Prepárate para esto ...


Un par de sugerencias Menciona las abundantes advertencias en W4: sugeriría tomarse el tiempo para arreglar su código para compilarlo limpiamente en el nivel de advertencia 4; esto ayudará en gran medida a evitar errores sutiles difíciles de encontrar.

En segundo lugar, para el modificador / análisis, de hecho genera abundantes advertencias. Para usar este modificador en mi propio proyecto, lo que hice fue crear un nuevo archivo de encabezado que utilizaba la advertencia #pragma para desactivar todas las advertencias adicionales generadas por / analyse. Luego, más abajo en el archivo, solo enciendo las advertencias que me interesan. Luego, use el modificador / FI compilador para forzar que este archivo de encabezado se incluya primero en todas sus unidades de compilación. Esto debería permitirle usar el modificador / analyse mientras controla la salida