2012-04-09 10 views
11

Estoy manteniendo una aplicación heredada escrita en C++. Se cuelga de vez en cuando y Valgrind me dice que es una eliminación doble de algún objeto.¿Cómo depurar dobles eliminaciones en C++?

¿Cuáles son las mejores formas de encontrar el error que está causando una eliminación doble en una aplicación que no entiende completamente y que es demasiado grande para ser reescrita?

¡Comparta sus mejores consejos y trucos!

+8

No creo que haya un enfoque único para todos. En última instancia, debe rastrear cómo llegó a ser que un puntero que tenía 'delete' llamado en él todavía flotaba en uso después del hecho, lo que se reduce a rastrear la lógica de la aplicación, desafortunadamente. Puede ayudar establecer explícitamente el puntero a 'NULL' inmediatamente después de' delete', porque eso detectará otros errores de acceso a través de seg-faults. –

+1

@Oli: Eso solo funciona si la dirección del objeto se almacena en una sola variable. Aunque lo más probable es que sea cierto en el momento en que se elimina el objeto, en los escenarios de uso después de liberación probablemente no lo sea. –

+0

@BenVoigt: De acuerdo. –

Respuesta

4

Aquí hay algunos indicios de que en general me han ayudado en esa situación:

  1. vuelta a su nivel de registro a la máxima depuración, si está utilizando un registrador. Busca cosas sospechosas en la salida. Si su aplicación no registra las asignaciones de punteros y elimina el objeto/clase bajo sospecha, es hora de insertar algunas declaraciones cout << "class Foo constructed, ptr= " << this << endl; en su código (y las correspondientes delete/impresiones de destructor).
  2. Ejecute valgrind con --db-attach = yes. Lo encontré muy útil, aunque un poco tedioso. Valgrind le mostrará un seguimiento de la pila cada vez que detecte un error de memoria importante o un evento y luego le preguntará si desea depurarlo. Es posible que te encuentres presionando varias veces repetidas veces si tu aplicación es grande, pero sigue buscando la línea de código donde el objeto en cuestión se elimina primero (y luego).
  3. Simplemente recorra el código. Busque la construcción/eliminación del objeto en cuestión. Lamentablemente, a veces termina siendo una biblioteca de terceros :-(.
  4. Actualización: Acabo de descubrir esto recientemente: Aparentemente gcc 4.8 y posterior (si puede usar GCC en su sistema) tiene algunos nuevos incorporados características para la detección de errores de memoria, el "address sanitizer". también disponible en el LLVM compiler system.
+2

como una adición al punto 3. También puede buscar operaciones de asignación 'Thing * thisThing = otherThing' si está asignando el puntero del objeto a otra cosa (incluso temporalmente), y eliminar cualquiera de ellos, entonces el objeto es eliminado de la memoria, y cualquier intento de acceder, modificar o eliminar el otro causará problemas. – gardian06

+0

Sí, excelente punto, gardian06! –

2

Sí. Lo que dijo @OliCharlesworth. No hay una manera segura de probar un puntero para ver si apunta a la memoria asignada, ya que realmente es solo la ubicación de la memoria.

El mayor problema que su pregunta implica es la falta de reproducibilidad. Siguiendo con esto en mente, estás atascado con el cambio de constructos simples de 'eliminar' al delete foo;foo = NULL;.

Incluso en ese caso, el mejor de los casos es que "parece que ocurre menos" hasta que realmente lo ha sellado.

También pregunto por qué evidencia Valgrind sugiere que es un problema de eliminación doble. Podría ser una pista mejor persistiendo allí.

Es uno de los problemas realmente más desagradables.

+1

Suponiendo que 'foo' no es solo una copia del puntero. –

+0

Blergh. Sí. También hay eso. He estado envolviendo las cosas en la semántica de la construcción de bloques de concreto durante tanto tiempo que me olvidé de los problemas simples. –

0

que probablemente está actualizando desde una versión que trata de borrar de forma diferente a continuación, la nueva versión.

probablemente lo que la versión anterior hizo fue cuando se llamó al delete, realizó un control estático para if (X != NULL){ delete X; X = NULL;} y el n en la nueva versión solo hace la acción delete.

es posible que tenga que revisar las asignaciones de punteros y hacer un seguimiento de las referencias de los nombres de objeto desde la construcción hasta la eliminación.

2

Esto puede o no funcionar para usted.

Hace mucho tiempo estaba trabajando en el programa de 1M + líneas que tenía 15 años en ese momento.Frente al mismo problema exacto: eliminación doble con gran conjunto de datos. Con tales datos, cualquier "memoria perfiladora" fuera de la caja sería un no ir.

Las cosas que eran de mi lado:

  1. Era muy reproducibles - tuvimos lenguaje de macros y funcionando mismo guión de la misma manera reproduce cada vez
  2. algún momento de la historia del proyecto alguien decidió que "#define malloc my_malloc" y "#define free my_free" tuvieron algún uso. Estos no hicieron mucho más que llamar a malloc incorporado() y libre() pero el proyecto ya se compiló y funcionó de esta manera.

Ahora el truco/idea:

my_malloc(int size) 
{ 
    static int allocation_num = 0; // it was single threaded 

    void* p = builtin_malloc(size+16); 

    *(int*)p = ++allocation_num; 
    *((char*)p+sizeof(int)) = 0; // not freed 

    return (char*)p+16; // check for NULL in order here 
} 

my_free(void* p) 
{ 
    if (*((char*)p+sizeof(int))) 
    { 
     // this is double free, check allocation_number 
     // then rerun app with this in my_alloc 
     // if (alloc_num == XXX) debug_break(); 
    } 

    *((char*)p+sizeof(int)) = 1; // freed 

    //built_in_free((char*)p-16); // do not do this until problem is figured out 
} 

Con la nueva/Eliminar podría ser más complicado, pero aún con LD_PRELOAD que podría ser capaz de sustituir malloc/libre sin ni siquiera volver a compilar la aplicación.

0

Lo he encontrado útil: backtrace() on linux. (Tiene que compilar con -dinámica.) Esto le permite averiguar de dónde viene ese doble gratis colocando un bloque try/catch alrededor de todas las operaciones de memoria (nuevo/eliminar) y luego en el bloque catch, imprima su rastro de pila.

De esta forma puede reducir los sospechosos mucho más rápido que ejecutar valgrind.

envolví traza en un poco de clase a mano para que yo sólo puedo decir:

try { 
    ... 
} catch (...) { 
    StackTrace trace; 
    std::cerr << "Double free!!!\n" << trace << std::endl; 
    throw; 
} 
0

En Windows, suponiendo que la aplicación está construida con MSVC++, puede tomar ventaja de las amplias heap debugging herramientas integradas en la depuración versión de la biblioteca estándar.

También en Windows, puede usar Application Verifier. Si recuerdo bien, tiene un modo de las fuerzas que cada asignación en una página separada con páginas de guardia protegidas en el medio. Es muy efectivo para encontrar desbordamientos de búfer, pero sospecho que también sería útil para una situación de doble liberación.

Otra cosa que podría hacer (en cualquier plataforma) habría que hacer una copia de las fuentes que se transforman (tal vez con macros) para que cada instancia de:

delete foo; 

se reemplaza con:

{ delete foo; foo = nullptr; } 

(Las llaves ayudan en muchos casos, aunque no son perfectas.) Eso convertirá muchas instancias de doble libre en una referencia de puntero nulo, por lo que es mucho más fácil de detectar. No atrapa todo; es posible que tenga una copia de un puntero obsoleto, pero puede ayudar a eliminar muchos de los escenarios comunes de uso después de eliminar.