Valgrind: ¿una herramienta olvidada que permanece en la sombra o una herramienta de depuración seria?

Last updated

Hace un par de meses, echamos un vistazo a los «sanitizers» de C++, unas pequeñas herramientas muy útiles que nos ayudan a mantener nuestro código limpio y libre de errores de memoria.

Hoy daremos un paso atrás en el tiempo y hablaremos de una herramienta antigua, pero que sigue siendo muy potente, llamada Valgrind. Lleva ya bastante tiempo prestando servicio a los desarrolladores, ya que detecta una amplia variedad de errores, como lecturas de memoria no inicializada, desbordamientos de búfer del montón, fugas de memoria, interbloqueos, etc.

Tras el lanzamiento por parte de Clang de su conjunto de sanitizadores, Valgrind pasó a un segundo plano, pero, como verás en esta entrada del blog, hay algunos casos en los que Valgrind realmente deja a los sanitizadores por los suelos. 

Una bestia bajo el capó

Una de las mayores ventajas que tiene Valgrind frente a los sanitizadores es que no requiere que el software esté instrumentado antes de someterlo a comprobación. Esto significa que se puede utilizar para depurar cualquier tipo de software «tal cual», sin necesidad de tener acceso a su código fuente.

Esto no sería posible sin la «bestia» que se esconde bajo el capó de Valgrind. En resumen, una vez inicializado Valgrind, este toma el control de tu programa y lo ejecuta en una CPU simulada proporcionada por el núcleo de Valgrind. A continuación, añade su propio código de instrumentación en función del tipo de errores que quieras detectar. 

A diferencia de los sanitizadores, Valgrind utiliza la instrumentación binaria dinámica (DPI) y la compilación «just-in-time» (JIT) para integrar el código de nuestro software con el código de instrumentación, es decir, para interceptar las llamadas a las funciones de asignación con el fin de almacenar información adicional. 

Here’s how it works. Each allocated block maps to a shadow block where the call stack at the time of the malloc function call is stored. Once the free function is called, Valgrind tries to find the shadow block corresponding to the address passed to free. If the block is not found, Valgrind emits an error message. Otherwise, the block is added to the queue and gets marked as inaccessible. This way, it is possible to detect invalid access to freed memory. However, please note that blocks can be removed from the queue once the system runs out of free space.

Flexibilidad que ofrece una arquitectura por capas

Como se muestra en la siguiente figura, Valgrind se compone de dos capas: el núcleo de Valgrind y el complemento de la herramienta, que puede ser cualquiera de las herramientas del conjunto de herramientas de Valgrind, entre las que se incluyen: 

  • Memcheck: realiza un seguimiento de las asignaciones de memoria y detecta fugas de memoria
  • Helgrind: detecta problemas relacionados con el multihilo (por ejemplo, interbloqueos, conflictos de acceso a datos, etc.)
  • Cachegrind: actúa como perfilador de caché y de predicción de ramificaciones
  • Massif: analiza el uso de la memoria del montón

Ten en cuenta que Valgrind es de código abierto y que puedes crear tu propia herramienta si lo deseas. 

Estas dos capas tienen cada una su propia función. La parte central carga el software que se va a someter a prueba en el proceso y desensambla su código. Una vez hecho esto, los fragmentos de código se pasan al complemento de la herramienta, que añade la instrumentación al código y, finalmente, lo vuelve a ensamblar. 

Una forma más sencilla de depurar

Como mencionamos en nuestra entrada anterior del blog, los sanitizadores de C++ requieren que se recompile el código. Esto puede resultar un inconveniente cuando se quiere probar el código tanto con AddressSanitizer como con MemorySanitizer. Dado que ambos no pueden funcionar a la vez, tendrías que realizar varias ejecuciones de prueba para detectar lecturas de memoria no inicializada y otros errores relacionados con las direcciones. Por otro lado, Valgrind puede ejecutar prácticamente cualquier software tal cual. Lo único que necesita es que sea compatible con todas las instrucciones que utiliza nuestro software.

Imagina que quieres depurar una biblioteca cuyo código fuente no está disponible. El uso de «sanitizers» no daría ningún resultado porque, a diferencia de Valgrind, funcionan a nivel del compilador. Con Valgrind, puedes gestionar estos casos de forma predeterminada, pero ten en cuenta que es posible que recibas mensajes de error que no te digan gran cosa, ya que no tienes control sobre ese código. No obstante, tienes la posibilidad de filtrar estos mensajes incluyéndolos en un archivo de supresiones que se lee al iniciar Valgrind. 

Además de ofrecer un proceso de depuración más ágil, Valgrind es una excelente alternativa en aquellas plataformas en las que no se admiten los sanitizadores. Apple Clang, por ejemplo, no incluye LeakSanitizer, por lo que Valgrind es la mejor alternativa, a menos que estés dispuesto a cambiar de compilador. 

El rendimiento como principal inconveniente

Echemos un vistazo a una versión ligeramente modificada del ejemplo de nuestra primera entrada sobre los sanitizadores de C++: 

char const * src{ "Hello world!" };
auto const dst{ std::make_unique< char[] >( std::strlen( src ) ) };

for ( auto i{ 0ul }; i < 1000000; ++i )
{
std::strcpy( dst.get(), src );
}

std::puts( dst.get() );

A for loop has been added to emphasize the performance difference between AddressSanitizer and Valgrind. In both cases, the code is compiled using the following command (running ASan also requires us to add -fsanitize=address):

clang++ example.cpp -g -o example.out

Un rápido vistazo al gráfico que aparece a continuación revela una enorme diferencia de rendimiento entre Valgrind y ASan, y esta es la razón por la que muchos desarrolladores pueden mostrarse reacios a utilizar la herramienta desde el principio.

Comparación lado a lado 

Para comprender mejor qué errores son capaces de detectar Valgrind y los sanitizadores, echemos un vistazo a la siguiente tabla: 

* MSan detecta
lecturas de memoria no inicializada** UBSan detecta comportamientos indefinidos

As you can see, Valgrind won’t help you catch overflows in stack and global variables. This is because it only has access to the heap allocations performed by malloc function. Also, don’t count on it detecting any undefined behavior in your code — though you’ll get warned about trying to access memory that resulted from undefined behavior.  

Al mismo tiempo, ASan tampoco es tan perfecto. No detecta lecturas de memoria no inicializada ni comportamientos indefinidos, mientras que MSan y UBSan sí lo hacen. 

Valgrind o sanitizadores: ¿qué deberíamos usar?

La pregunta sigue siendo, pues: ¿deberíamos utilizar Valgrind o son los sanitizadores una alternativa mejor? No hay una respuesta correcta a esta pregunta, ya que estas dos herramientas funcionan de maneras muy diferentes. Lo ideal sería utilizar ambas, dependiendo de tu entorno y de los errores que quieras detectar.

Aunque los sanitizadores actuales suponen una carga mucho menor para la CPU y ofrecen una gama más amplia de errores detectados, siguen teniendo sus desventajas. Funcionan a nivel del compilador, lo que significa que se necesita el código fuente. Además, requieren recompilar el código cada vez que se ejecuta una prueba, tardan más en integrarse y carecen de compatibilidad con determinadas plataformas.  

Si tienes entre manos un proyecto grande y no quieres preocuparte por recompilar tu código, quizá tenga más sentido optar por Valgrind. Nos ha resultado especialmente útil para depurar bibliotecas de código cerrado y detectar con mayor facilidad problemas como los errores de «use-after-free». Aunque, dado que los sanitizadores no dejan de mejorar, solemos preferirlos a Valgrind.

Al fin y al cabo, independientemente de las herramientas que utilices, lo más importante es que tu código sea lo más seguro posible. Teniendo esto en cuenta, no te pierdas las próximas entradas interesantes sobre la depuración en C++.

Descubre nuestras soluciones

Explorar nuestras soluciones está a un clic de distancia. Prueba nuestros productos o habla con nosotros con uno de nuestros expertos para profundizar en lo que ofrecemos.

Informe
Análisis del aumento del fraude de identidad impulsado por la inteligencia artificial

La IA no solo ha acelerado el fraude, sino que lo ha convertido en un sistema. Hemos analizado millones de interacciones relacionadas con la identidad para trazar un mapa de cómo están evolucionando los ataques a la identidad en las distintas regiones, según los tipos de ataque y los niveles de sofisticación, y qué deben replantearse las organizaciones para mantenerse al día.

Ver los datos