Heap corrupción bajo Win32; cómo localizar?

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

La solicitud de locking del comportamiento consiste en generar un rendimiento en este sistema: transferencia de datos del socket que se envía a una representación interna. Tengo un conjunto de datos de prueba que causarán periódicamente que la aplicación haga una excepción (varios lugares, varias causas, incluido el error de asignación de heap, por lo tanto: corrupción del 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 estrellarse. Deshabilitar un núcleo de hiper-threading o un núcleo de doble núcleo reduce la tasa de corrupción (pero no la elimina). Esto sugiere un problema relacionado con el tiempo.

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

Mis mejores conjeturas actuales sobre dónde ir a continuación son:

  1. Obtenga una caja increíblemente grunty (para reemplazar la caja de desarrollo actual: 2Gb de RAM en un E6550 Core2 Duo ); esto hará posible reproducir el error que causa el mal comportamiento cuando se ejecuta bajo un poderoso entorno de depuración; o
  2. Reescriba los operadores new y delete para usar VirtualAlloc y VirtualProtect para marcar la memoria como de solo lectura tan pronto como haya terminado. Ejecutar bajo MSVC6 y hacer que el sistema operativo atrape al tipo malo que está escribiendo en la memoria liberada. Sí, esto es un signo de desesperación: ¿quién diablos reescribe new y delete ? Me pregunto si esto va a ser tan lento como en Purify et al.

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

Un colega acaba de pasar y preguntó “Stack Overflow? ¿Estamos recibiendo desbordamientos de stack ahora?!?”

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


Actualización: equilibrar new[] y delete[] parece haber avanzado mucho para resolver el problema. En lugar de 15 minutos, la aplicación ahora dura unas dos horas antes de estrellarse. Todavía no está allí. ¿Alguna otra sugerencia? La corrupción del montón persiste.

Actualización: una comstackción de lanzamiento en Visual Studio 2008 parece dramáticamente mejor; la sospecha actual se basa en la implementación de STL que se envía con VS98 .


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

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

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

Tengo que ir en este momento, de nuevo: no hay mucha ayuda hasta que algo sale mal. Quiero atrapar el vándalo en el acto.

Quizás estas herramientas le permitan al menos reducir el problema a cierto componente.

No tengo mucha esperanza, pero los tiempos desesperados requieren …

¿Y está seguro de que todos los componentes del proyecto tienen la configuración correcta de la biblioteca en 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, y pasaré un par de horas mañana revisando el espacio de trabajo (58 proyectos) y verificando que todos comstackn y enlazan con las banderas correspondientes.


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

Mi primera opción sería una herramienta de montón dedicada como pageheap.exe .

Volver a escribir nuevo y eliminar podría ser útil, pero eso no atrapa los allocs cometidos por el código de nivel inferior. Si esto es lo que quiere, mejor low-level alloc API utilizando Microsoft Detours.

También comprobaciones de cordura, tales como: verifique que coincidan las bibliotecas en tiempo de ejecución (release vs. debug, multi-threaded vs. single-thread, dll vs. lib estáticas), busque malas eliminaciones (por ejemplo, eliminar donde debería haber sido delete []) utilizado), asegúrese de no mezclar y hacer coincidir sus allocs.

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

¿Cómo es la stack de llamadas, etc. en el momento de la primera excepción?

Tengo los mismos problemas en mi trabajo (también utilizamos VC6 veces). Y no hay una solución fácil para eso. Solo tengo algunos consejos:

  • Intente con volcados automáticos en la máquina de producción (consulte Descargador de procesos ). Mi experiencia dice que el Dr. Watson no es perfecto para tirar basura.
  • Elimine todas las capturas (…) de su código. A menudo esconden excepciones de memoria graves.
  • Verifique la depuración avanzada de Windows : hay muchos consejos excelentes para problemas como el suyo. Lo recomiendo con todo mi corazón.
  • Si usa STL pruebe STLPort y verifique las comstackciones. El iterador inválido es el infierno.

Buena suerte. Problemas como el tuyo tardan meses en resolverse. Prepárate para esto …

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

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

Después de saber qué fue lo que se ha estropeado, puede restringir el uso de la aplicación con configuraciones especiales de almacenamiento dynamic. 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 de montón completo, pero pasé mucho tiempo analizando el / los volcado (s) de locking y las fonts de exploración.

PD: Puedes usar DebugDiag para analizar los vertederos. Puede señalar el DLL posee el montón corrupto y darle otros detalles útiles.

Tuvimos mucha suerte escribiendo nuestras propias funciones gratuitas y malloc. En producción, simplemente llaman al estándar Malloc y gratuito, pero en depuración, pueden hacer lo que quieran. También tenemos una clase base simple que no hace 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 tienes un montón de código, puede ser un gran trabajo reemplazar las llamadas a malloc y gratis al nuevo malloc y gratuito (¡no olvides el realloc!), Pero a la larga es muy útil.

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

  • Mantenga 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 libre, puede asegurarse de que estos marcadores estén todavía allí.
  • memset la memoria con un marcador en la asignación (para encontrar el uso de la memoria no inicializada) y en libre (para encontrar el uso de la 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 que no cancelamos el buffer, y estos también han atrapado muchos problemas.

Debería atacar este problema tanto con el tiempo de ejecución como con el análisis estático.

Para el análisis estático, considere comstackr con PREfast ( cl.exe /analyze ). Detecta la delete y delete[] , los desbordamientos del búfer y una serie de otros problemas. Sin embargo, prepárate para recorrer muchos kilobytes de advertencia L6, especialmente si tu proyecto aún no tiene L4 corregido.

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

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 (trozos de memoria) se comparten entre subprocesos y la sincronización (sección crítica, mutex, semáforo, otro) las primitivas no están en base a clase (por objeto, por clase), entonces es posible llegar a una situación donde la clase (fragmento de memoria) se elimina / libera durante el uso, o se utiliza 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 del código.

¿Esto es en condiciones de poca memoria? Si es así, puede ser que lo nuevo sea devolver NULL lugar de arrojar std :: bad_alloc. Los comstackdores anteriores de VC++ no implementaron esto correctamente. Hay un artículo sobre fallas en la asignación de memoria heredada que bloquea aplicaciones STL creadas con VC6 .

Probaste comstackciones antiguas, pero ¿hay alguna razón por la que no puedas ir más atrás en el historial del repository 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 estoy perdiendo específicamente lo que podría querer 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 le dará más información sobre qué buscar en el código.

Mi primera acción sería la siguiente:

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

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

Quizás estas herramientas le permitan al menos reducir el problema a cierto componente.

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

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

  • Uso incorrecto del montón, es decir, doble libre, lectura después de libre, escritura después de libre, estableciendo el indicador HEAP_NO_SERIALIZE con allocs y libre de múltiples hilos en el mismo montón
  • Sin memoria
  • Código incorrecto (es decir, desbordamientos de búfer, subdesbordamientos de búfer, etc.)
  • Problemas de “sincronización”

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

Lo que probablemente significa que se debe a la forma en que 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 “temporización”. Cosas como no usar la semántica de adquisición / liberación para sincronizar el acceso a la memoria compartida con una bandera, no usar lockings de manera apropiada, etc.

Por lo menos, sería útil poder seguir las asignaciones de alguna manera, como se sugirió anteriormente. Al menos entonces puedes 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 intentar eso para ver si eso soluciona el problema o si produce un comportamiento de errores más reproducible.

Cuando estaba probando con VS2008, ¿se ejecutó con HeapVerifier con Conserve Memory establecido en Sí? Eso podría reducir el impacto en el rendimiento del asignador de montón. (Además, debe ejecutarlo Depurar-> Comenzar con el Verificador de aplicación, pero es posible que ya lo sepa).

También puede probar la depuración con Windbg y varios usos del comando! Heap.

MSN

Si eliges reescribir new / delete, he hecho esto y tengo un código fuente simple en:

http://gandolf.homelinux.org/~smhanov/blog/?id=10

Esto capta pérdidas de memoria y también inserta datos de guardia antes y después del bloque de memoria para capturar la corrupción del montón. Simplemente puede integrarse con él poniendo #include “debug.h” en la parte superior de cada archivo CPP y definiendo DEBUG y DEBUG_MEM.

La sugerencia de Graeme de personalizar malloc / free es una buena idea. Vea si puede caracterizar algún patrón sobre la corrupción para darle un manejo para aprovechar.

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

¡Esto supone que la cantidad de fragmentos sobresalientes de 64 bytes es solo moderada o tienes mucha memoria para quemar en la caja!

El poco tiempo que tuve para resolver un problema similar. Si el problema persiste, le sugiero que haga esto: monitoree todas las llamadas a new / delete y malloc / calloc / realloc / free. Hago DLL solo exportando una función para registrar todas las llamadas. Esta función recibe un parámetro para identificar la fuente del código, el puntero al área asignada y el tipo de llamada que guarda esta información en una tabla. Se elimina todo el par asignado / liberado. Al final o después de que lo necesite, realice una llamada a otra función para crear un informe de los datos de la izquierda. Con esto puedes identificar las llamadas incorrectas (nuevas / gratis o malloc / eliminar) o faltantes. Si tiene un caso de búfer sobreescrito 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 carreras para ayudar a identificar los errores. Buena suerte.

¿Crees que esta es una condición de carrera? ¿Hay varios hilos que comparten un montón? ¿Puede dar a cada hilo un montón privado con HeapCreate, y luego pueden ejecutarlo rápidamente con HEAP_NO_SERIALIZE? De lo contrario, un montón debe ser seguro para subprocesos, si está utilizando la versión de subprocesos múltiples de las bibliotecas del sistema.

Un par de sugerencias Mencionas las abundantes advertencias en W4, te sugiero que te tomes el tiempo de arreglar tu código para comstackr limpio en el nivel de advertencia 4, esto evitará sutiles errores difíciles de encontrar.

En segundo lugar, para el interruptor / analizar, sí genera muchas advertencias. Para utilizar este modificador en mi propio proyecto, lo que hice fue crear un nuevo archivo de encabezado que utilizara la advertencia #pragma para desactivar todas las advertencias adicionales generadas por / analizar. Luego, más abajo en el archivo, enciendo solo esas advertencias que me importan. A continuación, utilice el modificador del comstackdor / FI para forzar que este archivo de encabezado se incluya primero en todas sus unidades de comstackción. Esto debería permitirle usar el interruptor / analyze mientras controla la salida