Detección de “memory leaks” en C++

Siguiendo con mi renovado interés por C++ llevaba varios días dándole vueltas a cuál podría ser un tema interesante para publicar en esta categoría del blog de Minabta. Recordé que hace poco conversando con un compañero y comentando lo bien que estaba la última edición del libro de Bjarne Stroustrup le comenté que C++ dispone, dentro de la librería STL, de un interfaz para implementar un “garbage collector” y que desde mi época de programador de centralitas de commutación de circuitos punto a punto, el lenguaje C++ había evolucionado muchísimo y ya, con los unique_prt, y los shared_ptr, no había casi necesidad de tener que trabajar directamente con punteros (en una gran parte de los desarrollos más habituales). Y que las pérdidas de memoria, o “memory leaks” eran cosas del pasado.

Aún así, esta discusión me sirvió para decidir que iba a “abrir” una temática nueva y publicaría una serie de “posts” sobre C++ y el uso de la memoria. Para no intentar (inútilmente) hacer sobra al gran Bjarne y demás “gurús” del C++ que ya han publicado toneladas de “posts” sobre punteros, malloc, free, etc. me decidí a empezar por algo más sencillo: analizar qué hay disponible (y gratuito) para detección de “memory leaks” en C++.

Así, que dicho y hecho. Vamos al tema.

Introducción a la gestión de memoria en C/C++ y sus problemas asociados

Vaya por delante que este tema, correctamente explicado tomaría casi la extensión de un libro técnico. Intentaré condensarlo al máximo.

Todos sabemos que en C/C++ disponemos de dos tipos de memoria: la pila de llamadas (del inglés “call stack“) y la memoria de libre uso (o montículo, del inglés “heap“). La pila es gestionada por el compilador y se utiliza cuando declaramos e inicializamos cualquier variable en un ámbito (“scope“) determinado. También cuando realizamos llamadas una función. De ahí que lo que es almacenado en la pila por el compilador, es el propio compilador el que se encarga de liberarlo (con éxito la inmensa mayoría de las veces).

Ahora, cuando el desarrollador quiere almacenar algo de manera explícita y controlar su ámbito (o su periodo de vida) lo tiene que almacenar en el “heap“. Esto en lenguaje C/C++ se realizaba mediante punteros (o arrays sin inicializar) y usando los operadores new y delete (dijimos que nos centraríamos en C++). A continuación tenemos un ejemplo de cómo se crea correctamente un puntero a un objeto y cómo se debería de liberar cuando ya no es necesario.

using namespace std;
 
int main(void)
{
 
   // ... en alguna parte de nuestro código almacenamos un objeto con el operador "new"
   std::string* myStrPtr = new std::string{"A really huge huge string pointed by a pointer"};
 
   // ... en alguna parte (mas tarde) de nuestro código liberamos la memoria ocupada anteriormente
   delete myStrPtr;
 
   // ...
   return 0;
}

Como vemos,en general a cada llamada al operador new le corresponde una llamada al operador delete sobre el mismo puntero. Si esto no es así, la memoria reservada por new nunca sería liberada y tendríamos lo que se denomina como una “pérdida de memoria” (del inglés “memory leak“). Si nuestra aplicación tuviera muchos de estos fallos podríamos acabar consumiendo toda la memoria disponible del sistema operativo y eso conduciría a que la aplicación acabaría “explotando” o generando los volcados de memoria conocidos también como “core dump” en algunos SO’s.

Evidentemente existen otras muchas otras acciones inadecuadas cuando manejamos posiciones de memoria que pueden conducir a efectos indeseados en nuestras aplicaciones (uso de punteros sin inicializar, acceso a posiciones de memoria no inicializadas o fuera de rango). Casi todas estos fallos potenciales deben de ser tenidos en cuenta a la hora de programar. Afortunadamente contamos con cierta ayuda como veremos en el siguiente punto.

Herramientas o librerías de detección de “memory leaks” para C/C++

Como solo Dios y el Papa en la religión católica son infalibles, el resto de los mortales necesitamos ayuda con nuestro código fuente. Por eso dentro del panorama tecnológico existen varias herramientas y/o librerías que nos pueden ayudar a detectar estos problemas en nuestros programas.

Por acotar un poco la búsqueda, decidí restringir mis opciones a aquellas soluciones que no supusieran un desembolso monetario. Lo que otros llaman (incorrectamente) “Open Source” vamos. Así que después de varias sesiones de investigación encontré las siguientes opciones:

Si queréis un análisis exhaustivo de cada una de ellas (y de alguna que otra más aunque no del todo específica para C++) os recomiendo que leáis el siguiente post Review: 5 memory debuggers for Linux coding en Computer World de hace unos años.

Después de probar Valgrind y Mtrace me decidí por el primero puesto que es casi un estándar de facto en la comunidad de desarrolladores de C++ y además no implica modificar el código fuente como nos impone el uso de Mtrace por ejemplo.

Ahora que tenemos clara la herramienta, vamos a jugar un poco con ella para ver cómo se comporta. Vamos a ver algunos errores típicos y cómo detectarlos y corregirlos.

Uso de Memcheck de Valgrind

Empezaremos con algo simple: Nos olvidamos de liberar un puntero a un objeto. Usemos el siguiente código fuente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream> 
 
using namespace std;
 
class MyClass 
{
    private:
        string _className;
        char* _resource;
 
    public:
    MyClass () : _className {"default_classname"}
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with default constructor..." << endl;
    };
 
    MyClass (const string& classname) : _className (classname)
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with parametrized constructor (" << classname << ")..." << endl;
    };
 
    ~MyClass ()
    {
        free(_resource);
        cout << "Destructing MyClass with name '" << _className << "'" << endl;
    };
 
    void doStuff () 
    {
        cout << "Doing some stuff with class '" << _className << "'" << endl;
    }
};
 
int main (void) 
{
 
    MyClass* p = new MyClass("Created with a pointer");
 
    cout << "Doing stuff..." << endl; p->doStuff();
 
    cout << "Exiting from main..." << endl;
    return 0;
}

Ahora, compilamos el archivo acordándonos de incluir la información de depuración e invocamos la ejecución de nuestro programa a través de Valgrind mediante esta secuencia de comandos:

$> g++ -g -o main.exe main.cpp
 
$> valgrind --leak-check=full ./main.exe

Esto nos generará la siguiente salida:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
==633== Memcheck, a memory error detector
==633== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==633== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==633== Command: ./main.exe
==633==
==633== error calling PR_SET_PTRACER, vgdb might block
Constructing MyClass instance with parametrized constructor (Created with a pointer)...
Doing stuff...
Doing some stuff with class 'Created with a pointer'
Exiting from main...
==633==
==633== HEAP SUMMARY:
==633== in use at exit: 1,048,639 bytes in 3 blocks
==633== total heap usage: 6 allocs, 3 frees, 1,125,462 bytes allocated
==633==
==633== 1,048,639 (40 direct, 1,048,599 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3==633== at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==633== by 0x108E34: main (main.cpp:39)==633==
==633== LEAK SUMMARY:
==633== definitely lost: 40 bytes in 1 blocks
==633== indirectly lost: 1,048,599 bytes in 2 blocks
==633== possibly lost: 0 bytes in 0 blocks
==633== still reachable: 0 bytes in 0 blocks
==633== suppressed: 0 bytes in 0 blocks
==633==
==633== For counts of detected and suppressed errors, rerun with: -v
==633== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Si nos fijamos en la línea 28 de la salida se nos informa de que tenemos un error, y más arriba en las líneas 16 a 18 se nos indica que tenemos un problema de pérdida de bloques de memoria en la línea main.cpp:39 lo cual es totalmente cierto puesto que no liberamos el puntero p que reservamos con un operador new

Vamos a intentar arreglar nuestro “leak” (ahora veréis porque digo lo de “intentar“). Añadimos una llamada para liberar el puntero a nuestro objeto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
 
using namespace std;
 
class MyClass 
{
    private:
        string _className;
        char* _resource;
 
    public:
    MyClass () : _className {"default_classname"}
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with default constructor..." << endl;
    };
 
    MyClass (const string& classname) : _className (classname)
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with parametrized constructor (" << classname << ")..." << endl;
    };
 
    ~MyClass ()
    {
        free(_resource); // Esto está bien (¿?)
        cout << "Destructing MyClass with name '" << _className << "'" << endl;
    };
 
    void doStuff () 
    {
        cout << "Doing some stuff with class '" << _className << "'" << endl;
    }
};
 
int main (void) 
{
 
    MyClass* p = new MyClass("Created with a pointer");
 
    cout << "Doing stuff..." << endl; p->doStuff();
 
    free(p); // Liberamos el puntero (¿?)
    cout << "Exiting from main..." << endl;}

Al volver a ejecutar la comprobación como anteriormente vemos en la salida que no hemos avanzado mucho en la solución.

==53== Memcheck, a memory error detector
==53== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==53== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==53== Command: ./main.exe
==53==
==53== error calling PR_SET_PTRACER, vgdb might block
Constructing MyClass instance with parametrized constructor (Created with a pointer)...
Doing stuff...
Doing some stuff with class 'Created with a pointer'
==53== Mismatched free() / delete / delete []==53==    at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==53==    by 0x108EEE: main (main.cpp:44)==53==  Address 0x5b7dce0 is 0 bytes inside a block of size 40 alloc'd==53==    at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==53==    by 0x108E84: main (main.cpp:39)==53==
Exiting from main...
==53==
==53== HEAP SUMMARY:
==53==     in use at exit: 1,048,599 bytes in 2 blocks
==53==   total heap usage: 6 allocs, 4 frees, 1,125,462 bytes allocated
==53==
==53== 23 bytes in 1 blocks are definitely lost in loss record 1 of 2
==53==    at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==53==    by 0x4F6326C: void std::__cxx11::basic_string&lt;char, std::char_traits, std::allocator &gt;::_M_construct&lt;char*&gt;(char*, char*, std::forward_iterator_tag) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==53==    by 0x108FFD: MyClass::MyClass(std::__cxx11::basic_string&lt;char, std::char_traits, std::allocator &gt; const&amp;) (main.cpp:18)
==53==    by 0x108E92: main (main.cpp:39)
==53==
==53== 1,048,576 bytes in 1 blocks are definitely lost in loss record 2 of 2==53==    at 0x4C3089F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==53==    by 0x109007: MyClass::MyClass(std::__cxx11::basic_string&lt;char, std::char_traits, std::allocator &gt; const&amp;) (main.cpp:20)==53==    by 0x108E92: main (main.cpp:39)==53==
==53== LEAK SUMMARY:
==53==    definitely lost: 1,048,599 bytes in 2 blocks
==53==    indirectly lost: 0 bytes in 0 blocks
==53==      possibly lost: 0 bytes in 0 blocks
==53==    still reachable: 0 bytes in 0 blocks
==53==         suppressed: 0 bytes in 0 blocks
==53==
==53== For counts of detected and suppressed errors, rerun with: -v
==53== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)

Ahora tenemos dos errores más que en nuestra primera comprobación. Si nos fijamos en la línea 10 de la salida, se nos informa de que tenemos una incongruencia entre los tipos de llamada de reserva de memoria y los tipos de llamadas de liberación de memoria. En C/C++ tenemos las siguientes combinaciones consideradas como correctas, aunque ya veis que el programa funciona “correctamente” haciendo mezclas incorrectas:

  • malloc(), calloc(), realloc(), valloc() o memalign deben ser liberadas con free()
  • new debe ser liberado con delete
  • new[] debe ser liberado con delete[]

Con esto en mente, vamos a seguir corrigiendo nuestros “leaks“:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
 
using namespace std;
 
class MyClass 
{
    private:
        string _className;
        char* _resource;
 
    public:
    MyClass () : _className {"default_classname"}
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with default constructor..." << endl;
    };
 
    MyClass (const string& classname) : _className (classname)
    {
        _resource = new char[1024*1024];
        cout << "Constructing MyClass instance with parametrized constructor (" << classname << ")..." << endl;
    };
 
    ~MyClass ()
    {
        delete[] _resource;
        cout << "Destructing MyClass with name '" << _className << "'" << endl;
    };
 
    void doStuff () 
    {
        cout << "Doing some stuff with class '" << _className << "'" << endl;
    }
};
 
int main (void) 
{
 
    MyClass* p = new MyClass("Created with a pointer");
 
    cout << "Doing stuff..." << endl; p->doStuff();
 
    delete p; // Ahora si liberamos correctamente la memoria
    cout << "Exiting from main..." << endl;
}

Probamos ahora a realizar la comprobación de nuevo:

==59== Memcheck, a memory error detector
==59== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==59== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==59== Command: ./main.exe
==59==
==59== error calling PR_SET_PTRACER, vgdb might block
Constructing MyClass instance with parametrized constructor (Created with a pointer)...
Doing stuff...
Doing some stuff with class 'Created with a pointer'
Destructing MyClass with name 'Created with a pointer'
Exiting from main...
==59==
==59== HEAP SUMMARY:
==59==     in use at exit: 0 bytes in 0 blocks
==59==   total heap usage: 6 allocs, 6 frees, 1,125,462 bytes allocated
==59==
==59== All heap blocks were freed -- no leaks are possible
==59==
==59== For counts of detected and suppressed errors, rerun with: -v
==59== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Como vemos, ahora tenemos un programa perfecto que no pierde memoria, y que además no tiene ninguna operación de reserva/liberación de memoria que pueda dar problemas en algunos entornos.

Conclusión

Hemos visto que C++ nos da toda la potencia de un lenguaje de bajo nivel como C pero a cambio de tener que preocuparnos de algunas cuestiones como la asignación y liberación de memoria. Simplemente añadiendo a nuestros pipelines de compilación y pruebas una comprobación de este estilo podremos garantizar que no estamos liberando software con defectos difíciles de detectar. Herramientas como Valgrind (o cualquiera parecida) nos ayudan con un coste de integración mínimo en nuestros procesos de desarrollo.
También, C++ junto con la librería STL nos proporciona un conjunto de utilidades, como pueden ser por ejemplo los unique_prt o shared_ptr que nos ayudan a despreocuparnos de estos posibles fallos cuando estamos desarrollando. Pero eso es un tema que trataré en un próximo “post.
….

Deja un comentario

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.