Detectando fugas de memoria en Visual C++

A veces nos encontramos con que nuestro programa “chorrea” memoria, y hasta hace poco tiempo no había nada tan terrible para un programador como encontrar dónde se producía el problema, máxime cuando se trataba de aplicaciones con múltiples hilos.

Hubo una época en la que incluso había herramientas de terceros y analizadores de código para encontrar este tipo de problemas. Pero recientemente (creo que a partir de la versión 2005 de Visual C++), Microsoft añadió una serie de utilidades a la compilación Debug para detectar estos errores de forma casi inmediata.

Visual C++ 2005 creó la técnica, y Visual C++ 2008 la perfeccionó. Y por supuesto Visual C++ la mantiene.

Veamos un ejemplo. ¿Qué tiene de malo este código fuente tan sencillo?

class Elemento
{
    public:
    char A,B,C,D;

};

int _tmain(int argc, _TCHAR* argv[])
{
    Elemento *e=new Elemento();

    return 0;
}

Es evidente más allá de toda duda que no hemos liberado e. Este es un caso trivial, porque cuando salgamos del programa el espacio de direcciones se limpiará y no pasará nada. Pero imaginaros esa asignación en un bucle dentro de un programa destinado a estar horas y horas en ejecución.

Llega un momento en que ocupa toda la memoria disponible y revienta. Y lo peor es que el error que nos dé será tan críptico y tan extraño que, si no miramos el programa corriendo en el administrador de tareas no nos daremos cuenta. Y a veces ni así si el crecimiento del uso de la memoria es lento.

Además, en cada máquina el error será diferente, e incluso en la misma máquina dará diferentes errores dependiendo del uso del programa y del propio sistema (hablamos de excepciones extrañas con mensajes extraños e incluso mostrando texto corrupto).

Imaginaros ahora que os dan un programa de cien mil líneas que haya pasado por cinco o seis programadores, algunos de ellos malos y otros peores, que el programa tenga fugas de memoria y que te lo asignen a ti para solucionarlos.

No, no vale pegarse un tiro ni prenderle fuego al ordenador o a la empresa.

***

Es muchísimo más fácil que todo eso. Si nuestro programa está hecho en MFC tan sólo tendremos que recompilarlo con Visual Studio 2008 o siguiente, ejecutarlo un rato desde el IDE y cerrarlo.

Si no usa MFC y sólo usa Win32 u otras librerías, debemos añadir varias cosas al mismo. Abajo el nuevo código.

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

class Elemento
{
    public:
    char A,B,C,D;

};

int _tmain(int argc, _TCHAR* argv[])
{
    Elemento *e=new Elemento();

    _CrtDumpMemoryLeaks();
    return 0;
}

Hemos incluido unos ficheros cabecera y en el punto de salida hemos hecho una llamada a la función _CrtDumpMemoryLeaks(). La salida de la ventana Output (from Debug) tras una ejecución es la siguiente:

[… texto omitido …]
‘fugas_memoria.exe’: Loaded ‘C:WindowsSysWOW64msvcr100d.dll’, Symbols loaded.
Detected memory leaks!
Dumping objects –>
{107} normal block at 0x00695E30, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
The program ‘[716] fugas_memoria.exe: Native’ has exited with code 0 (0x0).

Nos fijamos en algunas líneas que nos dicen que ha habido fugas de memoria en un bloque de 4 bytes (que se corresponden a los cuatro char que hemos indicado en la clase). Mola, ¿no?

Bueno, ya sabemos que tenemos una fuga de memoria, pero no sabemos quién la está generando. Vamos a averiguarlo.

Lo primero es fijarnos en la línea

{107} normal block at 0x00695E30, 4 bytes long

y en el número que hay entre corchetes: 107. Ese es el número de bloque de memoria que no está siendo liberado. Una vez que sabemos qué bloque es, debemos relanzar la aplicación pero esta vez en lugar de ejecutarla con F5 o con Debug -> Run, ejecutamos un solo paso para que se nos quede en el punto de entrada inicial pulsando F11 o Debug -> Step Into. También podemos poner un punto de interrupción.

Una vez hecho esto, abrimos una ventana de Watch (Inspección) y añadimos la variable

{,,msvcr100d.dll}_crtBreakAlloc

a inspeccionar. La parte “msvcr100d.dll” dependerá de la versión del compilador que estemos usando. En general será “msvcr71d.dll” para 2003, “msvcr80d.dll” para 2005 y “msvcr80d.dll” para 2008 , aparte de la expuesta para 2010.

Ahora ponemos 107 como valor, ejecutamos el programa y… ¡Nos saltará una excepción! Le damos a “break” y se nos abre un fichero de código fuente de las tripas del runtime de C/C++. Para ir a donde nos interesa, abrimos la ventana de “Call Stack” y buscamos nuestro código fuente entre la pila de llamadas.

¡Y allí estará el new o el malloc() que nunca se libera! ¡Hemos encontrado la fuga!

***

A simple vista parece algo lioso, pero os puedo asegurar que cada vez que explico esto en alguna de las empresas por las que voy se notan los suspiros de alivio de todo el departamento de desarrollo y vuelven las sonrisas.

Si nuestra aplicación está hecha en MFC todavía es más fácil porque la mitad de cosas ya vienen predefinidas (de ahí esa extraña definición de “DEBUG_NEW” que hay por los ficheros de código fuente).

Las apuestas pueden subir cuando el error es aleatorio, pero siempre será más fácil así que sin ayuda. Además, existen trucos como poner periódicamente llamadas al volcado de las fugas, aparte de que hay más opciones para la detección, algunas de ellas no documentadas porque vienen de la propia experiencia personal.

Todo esto que he explicado aquí está contado en varios lugares de la MSDN, y hay más opciones y variación, de hecho una búsqueda de “memory leak” arroja bastantes resultados.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *