
Rust : un langage trop protecteur ?
Rust a été acclamé ces dernières années comme l’un des langages les plus sécurisés pour le développement système. Sa principale innovation ? Le borrow checker (littéralement vérificateur d'emprunts), ce mécanisme de vérification à la compilation qui empêche les erreurs de mémoire comme les accès concurrents non sécurisés ou les use-after-free.
Le vérificateur d'emprunts est une fonctionnalité essentielle du langage Rust et fait partie de ce qui rend Rust Rust. Il vous aide (ou vous oblige) à gérer la propriété. Comme le dit le chapitre 4 de « The Rust Programming Language », « La propriété est la caractéristique la plus unique de Rust, et elle permet à Rust de garantir la sécurité de la mémoire sans avoir besoin d'un collecteur mémoire ».
Le vérificateur d'emprunt est un composant du compilateur Rust dont le rôle est de vérifier à la compilation que l’usage de la mémoire est sûr, sans fuite ni corruption potentielle, sans exécution du programme. Il repose sur trois concepts clés :
- Propriété (ownership) : chaque valeur a un propriétaire unique.
- Emprunts (borrowing) : on peut prêter une valeur de manière mutable ou non mutable, mais pas les deux en même temps.
- Durée de vie (lifetimes) : chaque référence a une durée de vie clairement définie et vérifiée.
Le borrow checker interdit à la compilation :
- Les accès concurrents à une même valeur mutable (race conditions évitées).
- Les références à des valeurs qui ont été libérées (dangling pointers).
- La modification de données pendant qu'elles sont empruntées de façon immuable.
- L’utilisation après déplacement (move), pour éviter la double libération.
Mais pour certains développeurs C++, ce système ressemble davantage à une « nounou » : une surprotection qui entrave la liberté de coder rapidement et efficacement. Selon eux, les développeurs expérimentés n’ont pas besoin d’un langage qui leur interdit de faire certaines choses – ils veulent juste des outils qui les aident à le faire mieux, sans être bridés.
Un faux procès fait au C++ ?
Depuis l’essor de Rust dans les milieux de la sécurité et du développement système, une idée reçue semble s’installer : le C++ serait intrinsèquement dangereux. Ses critiques mettent en avant la complexité de sa gestion manuelle de la mémoire, source supposée d’erreurs fréquentes et de vulnérabilités critiques.
Mais ce discours occulte une réalité plus nuancée : le C++ n’est pas dangereux par nature, il est exigeant. Comme le rappelle Babaei, les fuites mémoire et les erreurs d’allocation ne sont pas une fatalité si les développeurs exploitent les outils disponibles dans l’écosystème du langage.
Ainsi, plutôt que de dénigrer le C++ pour son absence de mécanismes de sécurité intégrés à la Rust, il conviendrait de promouvoir les bonnes pratiques de développement, les outils de débogage avancés et les bibliothèques modernes comme RAII, unique_ptr, shared_ptr, ou encore Valgrind.
Dans sa vidéo intitulée « Rust Devs Think We’re Hopeless; Let’s Prove Them Wrong (with C++ Memory Leaks)! », Mamadou Babaei défend avec humour et technicité la capacité des développeurs C++ à gérer efficacement la mémoire, sans recourir aux mécanismes de sécurité stricts de Rust. Il démontre comment détecter les fuites mémoire en C++ en utilisant l'outil _CrtDumpMemoryLeaks fourni par la bibliothèque d'exécution C (CRT) de Microsoft.
Et d'indiquer :
« Lorsque les développeurs Rust pensent à nous, les gens du C++, ils imaginent une lignée maudite - un traumatisme générationnel transmis de malloc à free. Pour eux, chaque ligne de C++ que nous écrivons est comme jouer à la roulette russe - sauf que les six chambres sont chargées de comportements non définis.
« Ils nous regardent comme si nous étions sans espoir. Comme si nous étions à deux doigts d'une thérapie. Mais vous savez quoi ? Nous n'avons pas besoin d'une nounou pour le compilateur. Pas de vérificateur d'emprunts. Pas de durée de vie. Pas de modèles de propriété. Pas de magie noire. Même Valgrind n'est pas nécessaire. Juste des pointeurs bruts, de la détermination brute, et un peu de santé mentale douteuse.
« Dans cette vidéo, je vais donc vous montrer comment traquer les fuites de mémoire comme si vous étiez né avec un pointeur dans une main et un débogueur dans l'autre ».
_CrtDumpMemoryLeaks : Un outil à (re)découvrir
La démonstration de Babaei repose sur un outil méconnu mais redoutablement efficace : la fonction _CrtDumpMemoryLeaks, intégrée à la CRT (C Run-Time Library) de Microsoft. Elle permet, dès la compilation en mode debug, de détecter automatiquement les blocs de mémoire non libérés avant la fin de l’exécution.
En couplant cet outil à des macros de suivi (_CRTDBG_MAP_ALLOC, new redéfini avec traçabilité), il devient possible de cartographier précisément l'origine d'une fuite mémoire. Cette démarche, bien que nécessitant une configuration spécifique, prouve que le C++ n’est pas dépourvu de garde-fous — il demande juste qu’on les active.
Démonstration : détection de fuites mémoire avec _CrtDumpMemoryLeaks
Babaei présente un exemple de programme C++ qui génère intentionnellement une fuite mémoire en allouant de la mémoire sans la libérer. Pour détecter cette fuite, il utilise les fonctions de débogage de la CRT.
Démonstration d'une fuite de mémoire en C++ simple et intentionnelle
Code C++ : | Sélectionner tout |
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | /** * Une fuite de mémoire en C++ simple et intentionnelle */ #include <atomic> #include <cstdint> #include <cstdlib> #include <ctime> #include <iostream> #include <string> #include <thread> static constexpr int32_t PASSWORD_LENGTH = 1024 * 1024; static constexpr std::chrono::milliseconds INTERVAL_MILLISECONDS{1}; std::string GeneratePassword(std::size_t length) { const std::string charset{ "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" }; const std::size_t maxIndex = charset.size(); std::string result(length, '\0'); for (std::size_t i = 0; i < length; ++i) { result[i] = charset[rand() % maxIndex]; } return result; } int32_t main() { std::srand(static_cast<uint32_t>(std::time(nullptr))); std::atomic<bool> stop{false}; std::thread inputThread([&stop]() { std::cin.get(); stop = true; }); while (!stop) { std::string* password{new std::string(GeneratePassword(PASSWORD_LENGTH))}; std::cout << "Generated password:" << "\n" << *password << "\n\n" << "Press the 'Enter' key to interrupt!" << std::endl; std::this_thread::sleep_for(INTERVAL_MILLISECONDS); } inputThread.join(); std::cout << "'Enter' key detected! Will stop!" << std::endl; return 0; } |
Une démonstration de _CrtDumpMemoryLeaks
Code C++ : | Sélectionner tout |
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | /** * Une démonstration de _CrtDumpMemoryLeaks */ #define MEMORY_LEAK_TRACKER 1 #if MEMORY_LEAK_TRACKER #define _CRTDBG_MAP_ALLOC #include <crtdbg.h> #define DEBUG_NEW new (_NORMAL_BLOCK, __FILE__, __LINE__) #define new DEBUG_NEW #endif /* MEMORY_LEAK_TRACKER */ #include <atomic> #include <cstdint> #include <cstdlib> #include <ctime> #include <iostream> #include <string> #include <thread> static constexpr int32_t PASSWORD_LENGTH = 64; static constexpr std::chrono::milliseconds INTERVAL_MILLISECONDS{1000}; std::string GeneratePassword(std::size_t length) { const std::string charset{ "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" }; const std::size_t maxIndex = charset.size(); std::string result(length, '\0'); for (std::size_t i = 0; i < length; ++i) { result[i] = charset[rand() % maxIndex]; } return result; } int32_t main() { #if MEMORY_LEAK_TRACKER int dbgFlags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); dbgFlags |= _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF; _CrtSetDbgFlag(dbgFlags); // Only track one allocation per run! // e.g. allocation number that causes leaks: //_CrtSetBreakAlloc(162); #endif /* MEMORY_LEAK_TRACKER */ std::srand(static_cast<uint32_t>(std::time(nullptr))); std::atomic<bool> stop{false}; std::thread inputThread([&stop]() { std::cin.get(); stop = true; }); while (!stop) { std::string* password{new std::string(GeneratePassword(PASSWORD_LENGTH))}; std::cout << "Generated password:" << "\n" << *password << "\n\n" << "Press the 'Enter' key to interrupt!" << std::endl; std::this_thread::sleep_for(INTERVAL_MILLISECONDS); } inputThread.join(); std::cout << "'Enter' key detected! Will stop!" << std::endl; #if MEMORY_LEAK_TRACKER _CrtDumpMemoryLeaks(); #endif /* MEMORY_LEAK_TRACKER */ return 0; } |
Explication
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | #define MEMORY_LEAK_TRACKER 1 #if MEMORY_LEAK_TRACKER #define _CRTDBG_MAP_ALLOC #include <crtdbg.h> #define DEBUG_NEW new (_NORMAL_BLOCK, __FILE__, __LINE__) #define new DEBUG_NEW #endif /* MEMORY_LEAK_TRACKER */ |
Imaginons que nous construisions des maisons en LEGO et que nous oublions parfois de nettoyer les briques après la construction. Ce code dit : « Hé, activons un traqueur de briques LEGO ! ».
Il ajoute une caméra magique (<crtdbg.h>) qui surveille l'endroit où nous laissons tomber les briques. Et lorsque nous utilisons de nouvelles briques pour construire quelque chose, il ajoute secrètement qui l'a construit et où dans le code. Ainsi, si nous oublions de nettoyer, nous saurons exactement qui a fait le désordre et où.
Explication technique :
tl;dr ; _CrtSetDbgFlag(...) permet le suivi du tas au moment de l'exécution et les rapports de fuite ; _CrtSetBreakAlloc(N) vous permet de casser sur un numéro d'allocation spécifique.
Il ajoute une caméra magique (<crtdbg.h>) qui surveille l'endroit où nous laissons tomber les briques. Et lorsque nous utilisons de nouvelles briques pour construire quelque chose, il ajoute secrètement qui l'a construit et où dans le code. Ainsi, si nous oublions de nettoyer, nous saurons exactement qui a fait le désordre et où.
Explication technique :
- _CrtSetDbgFlag(...) : Définit les drapeaux d'exécution pour activer le suivi des fuites de mémoire et le signalement automatique des fuites à la sortie du programme.
- DBG_ALLOC_MEM_DF : Indique au tas de débogage de garder une trace des allocations de mémoire.
- DBG_LEAK_CHECK_DF : Appelle automatiquement _CrtDumpMemoryLeaks() à la sortie de main().
- _CrtSetBreakAlloc(162) : Fixe un point d'arrêt à la 162e allocation (telle qu'elle est comptée en interne par le tas CRT). Lorsqu'il est atteint, Visual Studio interrompt l'exécution afin que vous puissiez inspecter l'allocation à l'origine du problème. Vous pouvez commenter cette ligne sauf si vous essayez d'isoler une fuite spécifique dans un gros programme.
tl;dr ; _CrtSetDbgFlag(...) permet le suivi du tas au moment de l'exécution et les rapports de fuite ; _CrtSetBreakAlloc(N) vous permet de casser sur un numéro d'allocation spécifique.
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 | #if MEMORY_LEAK_TRACKER _CrtDumpMemoryLeaks(); #endif /* MEMORY_LEAK_TRACKER */ return 0; } |
[QUOTE]À la toute fin, nous disons à la caméra : « Ok, montre-moi tout ce qu'on a oublié de nettoyer. »
Elle envoie un rapport disant : « Hé ! Vous avez laissé une brique à la ligne 42 ! Et une autre à la ligne 57 ! »
Explication technique :
[LIST][*]Déclenche manuellement un rapport de fuite juste avant l'arrêt du programme.[*]C'est optionnel si vous avez déjà activé _CRTDBG_LEAK_CHECK_DF - le CRT va automatiquement faire le dumping des fuites.[*]Mais l'appeler[/*]...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.