C++
Outils et techniques de débogage et de prévention du débogage C ++
Recherche…
Introduction
Les développeurs C ++ consacrent beaucoup de temps au débogage. Ce sujet est destiné à faciliter cette tâche et à inspirer des techniques. Ne vous attendez pas à une liste exhaustive des problèmes et solutions résolus par les outils ou un manuel sur les outils mentionnés.
Remarques
Ce sujet n'est pas encore complet, des exemples sur les techniques / outils suivants seraient utiles:
- Mentionnez plus d'outils d'analyse statique
- Outils d'instrumentation binaire (comme UBSan, TSan, MSan, ESan ...)
- Durcissement (CFI ...)
- Fuzzing
Mon programme C ++ se termine par segfault - valgrind
Ayons un programme de base défaillant:
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p3 << std::endl;
}
}
int main() {
fail();
}
Construisez-le (add -g pour inclure les informations de débogage):
g++ -g -o main main.cpp
Courir:
$ ./main
Segmentation fault (core dumped)
$
Déboguons-le avec valgrind:
$ valgrind ./main
==8515== Memcheck, a memory error detector
==8515== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==8515== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==8515== Command: ./main
==8515==
==8515== Conditional jump or move depends on uninitialised value(s)
==8515== at 0x400813: fail() (main.cpp:7)
==8515== by 0x40083F: main (main.cpp:13)
==8515==
==8515== Invalid read of size 4
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== Address 0x0 is not stack'd, malloc'd or (recently) free'd
==8515==
==8515==
==8515== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==8515== Access not within mapped region at address 0x0
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== If you believe this happened as a result of a stack
==8515== overflow in your program's main thread (unlikely but
==8515== possible), you can try to increase the size of the
==8515== main thread stack using the --main-stacksize= flag.
==8515== The main thread stack size used in this run was 8388608.
==8515==
==8515== HEAP SUMMARY:
==8515== in use at exit: 72,704 bytes in 1 blocks
==8515== total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8515==
==8515== LEAK SUMMARY:
==8515== definitely lost: 0 bytes in 0 blocks
==8515== indirectly lost: 0 bytes in 0 blocks
==8515== possibly lost: 0 bytes in 0 blocks
==8515== still reachable: 72,704 bytes in 1 blocks
==8515== suppressed: 0 bytes in 0 blocks
==8515== Rerun with --leak-check=full to see details of leaked memory
==8515==
==8515== For counts of detected and suppressed errors, rerun with: -v
==8515== Use --track-origins=yes to see where uninitialised values come from
==8515== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
$
Tout d'abord, nous nous concentrons sur ce bloc:
==8515== Invalid read of size 4
==8515== at 0x400819: fail() (main.cpp:8)
==8515== by 0x40083F: main (main.cpp:13)
==8515== Address 0x0 is not stack'd, malloc'd or (recently) free'd
La première ligne nous indique que segfault est provoqué par la lecture de 4 octets. Les deuxième et troisième lignes sont des piles d'appels. Cela signifie que la lecture invalide est effectuée à la fonction fail()
, ligne 8 de main.cpp, appelée par main, ligne 13 de main.cpp.
En regardant la ligne 8 de main.cpp, nous voyons
std::cout << *p3 << std::endl;
Mais on vérifie d'abord le pointeur, alors qu'est-ce qui ne va pas? Permet de vérifier l'autre bloc:
==8515== Conditional jump or move depends on uninitialised value(s)
==8515== at 0x400813: fail() (main.cpp:7)
==8515== by 0x40083F: main (main.cpp:13)
Il nous dit qu'il y a une variable unitialisée à la ligne 7 et nous la lisons:
if (p3) {
Ce qui nous indique la ligne où nous vérifions p3 au lieu de p2. Mais comment est-il possible que p3 ne soit pas initialisé? Nous l'initialisons par:
int *p3 = p1;
Valgrind nous conseille de réexécuter avec --track-origins=yes
, faisons-le:
valgrind --track-origins=yes ./main
L'argument pour valgrind est juste après valgrind. Si nous le mettons après notre programme, il serait transmis à notre programme.
La sortie est presque la même, il n'y a qu'une seule différence:
==8517== Conditional jump or move depends on uninitialised value(s)
==8517== at 0x400813: fail() (main.cpp:7)
==8517== by 0x40083F: main (main.cpp:13)
==8517== Uninitialised value was created by a stack allocation
==8517== at 0x4007F6: fail() (main.cpp:3)
Ce qui nous indique que la valeur non initialisée que nous avons utilisée à la ligne 7 a été créée à la ligne 3:
int *p1;
qui nous guide vers notre pointeur non initialisé.
Analyse Segfault avec GDB
Utilisons le même code que ci-dessus pour cet exemple.
#include <iostream>
void fail() {
int *p1;
int *p2(NULL);
int *p3 = p1;
if (p3) {
std::cout << *p2 << std::endl;
}
}
int main() {
fail();
}
Commençons par le compiler
g++ -g -o main main.cpp
Permet de l'exécuter avec gdb
gdb ./main
Maintenant, nous serons dans le shell gdb. Tapez run.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/opencog/code-snippets/stackoverflow/a.out
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400850 in fail () at debugging_with_gdb.cc:11
11 std::cout << *p2 << std::endl;
Nous voyons que la faute de segmentation se produit à la ligne 11. La seule variable utilisée à cette ligne est le pointeur p2. Permet d'examiner son impression de saisie de contenu.
(gdb) print p2
$1 = (int *) 0x0
Nous voyons maintenant que p2 a été initialisé à 0x0, ce qui signifie NULL. Sur cette ligne, nous savons que nous essayons de déréférencer un pointeur NULL. Nous allons donc le réparer.
Code propre
Le débogage commence par la compréhension du code que vous essayez de déboguer.
Mauvais code:
int main() {
int value;
std::vector<int> vectorToSort;
vectorToSort.push_back(42); vectorToSort.push_back(13);
for (int i = 52; i; i = i - 1)
{
vectorToSort.push_back(i *2);
}
/// Optimized for sorting small vectors
if (vectorToSort.size() == 1);
else
{
if (vectorToSort.size() <= 2)
std::sort(vectorToSort.begin(), std::end(vectorToSort));
}
for (value : vectorToSort) std::cout << value << ' ';
return 0; }
Meilleur code:
std::vector<int> createSemiRandomData() {
std::vector<int> data;
data.push_back(42);
data.push_back(13);
for (int i = 52; i; --i)
vectorToSort.push_back(i *2);
return data;
}
/// Optimized for sorting small vectors
void sortVector(std::vector &v) {
if (vectorToSort.size() == 1)
return;
if (vectorToSort.size() > 2)
return;
std::sort(vectorToSort.begin(), vectorToSort.end());
}
void printVector(const std::vector<int> &v) {
for (auto i : v)
std::cout << i << ' ';
}
int main() {
auto vectorToSort = createSemiRandomData();
sortVector(std::ref(vectorToSort));
printVector(vectorToSort);
return 0;
}
Quels que soient les styles de codage que vous préférez et que vous utilisez, le fait d’avoir un style de codage (et de mise en forme) cohérent vous aidera à comprendre le code.
En regardant le code ci-dessus, on peut identifier quelques améliorations pour améliorer la lisibilité et le débogage:
L'utilisation de fonctions séparées pour des actions séparées
L'utilisation de fonctions séparées vous permet d'ignorer certaines fonctions du débogueur si les détails ne vous intéressent pas. Dans ce cas précis, la création ou l’impression des données pourrait ne pas vous intéresser et vous ne souhaiterez intervenir que dans le tri.
Un autre avantage est que vous devez lire moins de code (et le mémoriser) tout en parcourant le code. Il ne vous reste plus qu'à lire 3 lignes de code dans main()
pour le comprendre, au lieu de l'intégralité de la fonction.
Le troisième avantage est que vous avez simplement moins de code à regarder, ce qui aide un œil averti à repérer ce bug en quelques secondes.
Utiliser des mises en forme / constructions cohérentes
L'utilisation de mises en forme et de constructions cohérentes permet de supprimer l'encombrement du code, ce qui facilite la concentration sur le code plutôt que sur le texte. Beaucoup de discussions ont été nourries sur le bon style de mise en forme. Indépendamment de ce style, avoir un style unique et cohérent dans le code améliorera la familiarité et facilitera la mise au point du code.
Comme le code de formatage prend beaucoup de temps, il est recommandé d'utiliser un outil dédié à cette fin. La plupart des IDE ont au moins une sorte de support pour cela et peuvent rendre le formatage plus cohérent que les humains.
Vous remarquerez peut-être que le style ne se limite pas aux espaces et aux nouvelles lignes car nous ne mélangeons plus les fonctions de style libre et les fonctions de membre pour obtenir le début / la fin du conteneur. ( v.begin()
vs std::end(v)
).
Faites attention aux parties importantes de votre code.
Quel que soit le style que vous choisissez de choisir, le code ci-dessus contient quelques marqueurs qui pourraient vous donner une idée de ce qui pourrait être important:
- Un commentaire affirmant
optimized
, cela indique des techniques de fantaisie - Certains retours précoces dans
sortVector()
indiquent que nous faisons quelque chose de spécial - Le
std::ref()
indique que quelque chose se passe avec lesortVector()
Conclusion
Avoir du code propre vous aidera à comprendre le code et réduira le temps nécessaire pour le déboguer. Dans le deuxième exemple, un réviseur de code pourrait même détecter le bogue à première vue, tandis que le bogue pourrait être caché dans les détails du premier. (PS: Le bogue est en comparaison avec 2
)
Analyse statique
L'analyse statique est la technique par laquelle on vérifie le code à la recherche de motifs liés à des bogues connus. L'utilisation de cette technique prend moins de temps qu'une revue de code, mais ses vérifications ne sont limitées qu'à celles programmées dans l'outil.
Les vérifications peuvent inclure le point-virgule incorrect derrière l'instruction if (var);
( if (var);
) jusqu'à des algorithmes de graphe avancés qui déterminent si une variable n'est pas initialisée.
Avertissements du compilateur
Activer l'analyse statique est facile, la version la plus simpliste est déjà intégrée dans votre compilateur:
Si vous activez ces options, vous remarquerez que chaque compilateur trouvera des bogues que les autres ne détectent pas et que vous obtiendrez des erreurs sur des techniques qui pourraient être valides ou valides dans un contexte spécifique. while (staticAtomicBool);
pourrait être acceptable même si while (localBool);
n'est pas.
Donc, contrairement à la révision de code, vous vous battez contre un outil qui comprend votre code, vous indique beaucoup de bogues utiles et est parfois en désaccord avec vous. Dans ce dernier cas, vous devrez peut-être supprimer l'avertissement localement.
Comme les options ci-dessus activent tous les avertissements, elles peuvent activer les avertissements que vous ne souhaitez pas. (Pourquoi votre code devrait-il être compatible C ++ 98?) Si oui, vous pouvez simplement désactiver cet avertissement spécifique:
-
clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
g++ -Wall -Weverything -Werror -Wno-errortoaccept ...
-
cl.exe /W4 /WX /wd<no of warning>...
Lorsque les avertissements du compilateur vous aident pendant le développement, ils ralentissent un peu la compilation. C'est pourquoi vous ne souhaitez peut-être pas toujours les activer par défaut. Soit vous les exécutez par défaut, soit vous permettez une intégration continue avec les contrôles les plus coûteux (ou tous).
Outils externes
Si vous décidez d'avoir une intégration continue, l'utilisation d'autres outils n'est pas un tel effort. Un outil comme clang-tidy a une liste de vérifications qui couvre un large éventail de questions, quelques exemples:
- Bugs réels
- Prévention du tranchage
- Affirme avec des effets secondaires
- Contrôles de lisibilité
- Indication trompeuse
- Vérifier le nom de l'identificateur
- Contrôles de modernisation
- Utilisez make_unique ()
- Utilisez nullptr
- Contrôles de performance
- Trouver des copies inutiles
- Trouver des appels d'algorithme inefficaces
La liste pourrait ne pas être aussi grande, car Clang a déjà beaucoup d'avertissements sur le compilateur, mais cela vous rapprochera encore plus d'une base de code de haute qualité.
Autres outils
D'autres outils ayant un but similaire existent, comme:
- l'analyseur statique de studio visuel comme outil externe
- clazy , un plugin de compilateur Clang pour vérifier le code Qt
Conclusion
De nombreux outils d'analyse statique existent pour C ++, tous deux intégrés au compilateur en tant qu'outils externes. Les essayer ne prend pas beaucoup de temps pour les configurations faciles et ils trouveront des bogues que vous pourriez manquer dans la révision du code.
Safe-stack (corruptions de piles)
Les corruptions de piles sont des bogues ennuyeux à regarder. Comme la pile est corrompue, le débogueur ne peut souvent pas vous donner une bonne trace de votre emplacement et de la manière dont vous l'avez obtenu.
C'est là que la pile de sécurité entre en jeu. Au lieu d'utiliser une seule pile pour vos threads, il utilisera deux: Une pile sécurisée et une pile dangereuse. La pile sécurisée fonctionne exactement comme avant, sauf que certaines pièces sont déplacées vers la pile dangereuse.
Quelles parties de la pile sont déplacées?
Chaque partie susceptible de corrompre la pile sera retirée de la pile sécurisée. Dès qu'une variable de la pile est passée par référence ou que l'on prend l'adresse de cette variable, le compilateur décidera de l'allouer sur la deuxième pile au lieu de la sûre.
Par conséquent, toute opération effectuée avec ces pointeurs, toute modification apportée à la mémoire (basée sur ces pointeurs / références) ne peut affecter que la mémoire de la deuxième pile. Comme on n'obtient jamais un pointeur proche de la pile sécurisée, la pile ne peut pas corrompre la pile et le débogueur peut toujours lire toutes les fonctions de la pile pour donner une belle trace.
A quoi sert-il réellement?
La pile sécurisée n’a pas été inventée pour vous donner une meilleure expérience de débogage, cependant, c’est un effet secondaire intéressant pour les bugs méchants. Son objectif initial est de faire partie du projet d'intégrité du pointeur de code (CPI) , dans lequel il tente d'empêcher de remplacer les adresses de retour pour empêcher l'injection de code. En d'autres termes, ils essaient d'empêcher l'exécution d'un code de piratage.
Pour cette raison, la fonctionnalité a été activée sur chrome et a été signalée comme ayant une surcharge de processeur inférieure à 1%.
Comment l'activer?
Pour le moment, l'option n'est disponible que dans le compilateur Clang , où l'on peut passer -fsanitize=safe-stack
au compilateur. Une proposition a été faite pour implémenter la même fonctionnalité dans GCC.
Conclusion
Les corruptions de pile peuvent devenir plus faciles à déboguer lorsque la pile sécurisée est activée. En raison de la surcharge de performances, vous pouvez même activer par défaut dans votre configuration de construction.