Valgrind : Un outil négligé dans l’ombre ou un outil de débogage sérieux ?
Il y a quelques mois, nous avons jeté un coup d’œil sur les assainisseurs C++ – de petits outils pratiques qui nous aident à garder notre code propre et exempt de bogues de mémoire.
Aujourd’hui, nous allons faire un retour dans le passé et parler d’un outil plus ancien, mais toujours très puissant, appelé Valgrind. Il est au service des développeurs depuis un certain temps déjà, car il détecte un large éventail d’erreurs telles que les lectures de mémoire non initialisée, les dépassements de tampon du tas, les fuites de mémoire, les blocages, etc.
Après que Clang a publié sa collection d’assainisseurs, Valgrind a été relégué au second plan, mais comme vous le verrez dans ce billet de blog, il y a des cas où Valgrind fait tomber les assainisseurs dans un chapeau à coque.
Une bête sous le capot
L’un des principaux avantages de Valgrind par rapport aux assainisseurs est qu’il n’a pas besoin d’instrumenter le logiciel avant de le vérifier. Cela signifie qu’il peut être utilisé pour déboguer n’importe quel type de logiciel “dès sa sortie de la boîte” sans avoir accès à son code source.
Cela ne serait pas possible sans la bête qui se trouve sous le capot de Valgrind. En bref, une fois que Valgrind est initialisé, il prend le contrôle de votre programme et l’exécute sur un processeur simulé fourni par le noyau de Valgrind. Il ajoute ensuite son propre code d’instrumentation en fonction du type d’erreurs que vous cherchez à détecter.
Contrairement aux assainisseurs, Valgrind utilise l’instrumentation binaire dynamique (DPI) et la compilation juste à temps (JIT) pour intégrer le code de notre logiciel au code d’instrumentation, c’est-à-dire pour intercepter les appels de fonctions d’allocation afin de stocker des informations supplémentaires.
Voici comment cela fonctionne. Chaque bloc alloué correspond à un bloc d’ombre où est stockée la pile d’appels au moment de l’appel de la fonction malloc. Une fois la fonction free appelée, Valgrind essaie de trouver le bloc d’ombre correspondant à l’adresse passée à free. Si le bloc n’est pas trouvé, Valgrind émet un message d’erreur. Dans le cas contraire, le bloc est ajouté à la file d’attente et marqué comme inaccessible. De cette manière, il est possible de détecter les accès non valides à la mémoire libérée. Cependant, veuillez noter que les blocs peuvent être retirés de la file d’attente une fois que le système n’a plus d’espace libre.
Flexibilité offerte par une architecture en couches
Comme le montre la figure ci-dessous, Valgrind est composé de deux couches : Le noyau de Valgrind et le plugin de l’outil qui peut être n’importe quel outil de la suite d’outils Valgrind, y compris :
- Memcheck – suit les allocations de mémoire et signale les fuites de mémoire
- Helgrind – détecte les problèmes liés au multithreading (par exemple, les blocages, les courses de données, etc.)
- Cachegrind – agit comme un profileur de cache et de prédiction de branche
- Massif – analyse l’utilisation de la mémoire du tas
Gardez à l’esprit que Valgrind est un logiciel libre et que vous pouvez écrire votre propre outil si vous le souhaitez.
Ces deux couches ont chacune leur propre rôle. La partie centrale charge le logiciel testé dans le processus et désassemble son code. Une fois cette opération effectuée, les fragments de code sont transmis au plugin de l’outil qui ajoute l’instrumentation au code et, enfin, le réassemble.
Un moyen plus simple de déboguer
Comme nous l’avons mentionné dans notre précédent article de blog, les assainisseurs C++ exigent que votre code soit recompilé. Cela peut être gênant lorsque vous souhaitez tester votre code avec AddressSanitizer et MemorySanitizer. Comme les deux ne peuvent pas fonctionner ensemble, vous devez effectuer plusieurs tests pour détecter les lectures de mémoire non initialisées et d’autres bogues liés à l’adresse. D’un autre côté, Valgrind peut exécuter pratiquement n’importe quel logiciel tel quel. La seule chose dont il a besoin est la prise en charge de toutes les instructions que notre logiciel utilise.
Supposons que vous souhaitiez déboguer une bibliothèque dont le code source est inaccessible. L’utilisation de sanitizers ne donnerait aucun résultat car, contrairement à Valgrind, ils travaillent au niveau du compilateur. Avec Valgrind, vous pouvez traiter ces cas par défaut, mais gardez à l’esprit que vous risquez de vous retrouver avec des messages d’erreur qui ne signifient pas grand-chose pour vous, car vous n’avez aucun contrôle sur ce code. Vous êtes cependant libre de filtrer ces messages en les écrivant dans un fichier de suppression qui est lu au démarrage de Valgrind.
En plus d’un processus de débogage plus rationalisé, Valgrind est un excellent substitut sur les plateformes où les sanitizers ne sont pas supportés. Apple Clang, par exemple, n’est pas livré avec LeakSanitizer, ce qui fait de Valgrind votre meilleure alternative à moins que vous ne soyez prêt à changer de compilateur.
La performance est le principal inconvénient
Examinons une version légèrement modifiée de l’exemple exemple de notre premier article sur les assainisseurs 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() );
Une adresse for loop a été ajoutée pour souligner la différence de performance entre AddressSanitizer et Valgrind. Dans les deux cas, le code est compilé à l’aide de la commande suivante (l’exécution de ASan nécessite également l’ajout de -fsanitize=address) :
clang++ example.cpp -g -o example.out
Un rapide coup d’œil au tableau ci-dessous révèle un énorme écart de performance entre Valgrind et ASan, et c’est la raison pour laquelle de nombreux développeurs peuvent être réticents à utiliser l’outil en premier lieu.
Comparaison côte à côte
Afin de mieux comprendre quels bugs Valgrind et les assainisseurs sont capables de détecter, examinons le tableau suivant :
* MSan détecte les lectures de mémoire non initialisées
** UBSan détecte les comportements non définis
Comme vous pouvez le constater, Valgrind ne vous aidera pas à détecter les débordements dans la pile et les variables globales. En effet, il n’a accès qu’aux allocations du tas effectuées par la fonction malloc. Ne comptez pas non plus sur Valgrind pour détecter un comportement indéfini dans votre code – bien que vous soyez averti si vous essayez d’accéder à la mémoire résultant d’un comportement indéfini.
En même temps, ASan n’est pas non plus parfait. Il ne détecte pas les lectures de mémoire non initialisées ni les comportements indéfinis, contrairement à MSan et UBSan.
Valgrind ou désinfectants : que faut-il utiliser ?
La question qui se pose alors est la suivante : devons-nous utiliser Valgrind ou les assainisseurs constituent-ils une meilleure solution de remplacement ? Il n’y a pas de réponse correcte à cette question car ces deux outils fonctionnent de manière très différente. Idéalement, vous devriez utiliser les deux en fonction de votre environnement et des erreurs que vous souhaitez détecter.
Même si les assainisseurs d’aujourd’hui imposent un surcoût de CPU beaucoup plus faible et offrent un éventail plus large d’erreurs détectées, ils ont toujours leurs inconvénients. Ils fonctionnent au niveau du compilateur, ce qui signifie que vous avez besoin du code source. Ils vous obligent également à recompiler votre code à chaque test, prennent plus de temps à intégrer et ne sont pas compatibles avec certaines plates-formes.
Si vous avez un projet de grande envergure et que vous ne voulez pas vous préoccuper de la recompilation de votre code, il peut être plus judicieux d’opter pour Valgrind. Nous l’avons trouvé particulièrement utile pour déboguer des bibliothèques à code source fermé et détecter plus facilement des bogues de type “use-after-free”. Cependant, comme les assainisseurs sont de plus en plus performants, nous avons tendance à les préférer à Valgrind.
En fin de compte, quels que soient les outils que vous utilisez, la chose la plus importante est de rendre votre code aussi sûr que possible. En gardant cela à l’esprit, restez à l’écoute pour d’autres articles intéressants sur le débogage en C++.