Débogage, comportements indéfinis et programmation C sécurisée
DébutantUnité 8 sur 8 de la formation Formation : Langage C : pointeurs, mémoire et structures
Ce que tu vas apprendre
- Active les avertissements du compilateur, lis ses diagnostics et choisis des paramètres de compilation qui rendent les bogues visibles tôt, avant même l’exécution du programme.
- Activer les avertissements et lire les messages du compilateurCompile avec un ensemble strict d’options d’avertissement, lis chaque diagnostic ligne par ligne et corrige sa cause au lieu de faire disparaître le message.
- Choisir les paramètres de compilation pour le débogage, les tests et la productionUtilise des versions distinctes pour le débogage et la production, comprends ce que changent -g et les niveaux d’optimisation, et ajoute des options d’analyse statique et de durcissement là où elles conviennent.
- Activer les avertissements et lire les messages du compilateur
- Recherche les bogues méthodiquement : reproduis et isole la défaillance, puis utilise gdb ou lldb pour suspendre le programme, le parcourir pas à pas, inspecter les valeurs et trouver la véritable origine d’un plantage ou d’une valeur incorrecte.
- Déboguer méthodiquement : reproduire le problème, l’isoler et tester une hypothèseTransforme une défaillance vague en un problème précis, lis ce qu’indique le message de plantage et cerne la cause à l’aide de petites expériences contrôlées.
- Exécuter un programme, le suspendre et le parcourir pas à pas dans gdb ou lldbLance un programme sous gdb ou lldb, place des points d’arrêt, avance ligne par ligne et inspecte les variables et les tableaux pendant que le programme est suspendu.
- Diagnostiquer les plantages et les valeurs inattenduesUtilise la pile d’appels après un plantage, passe d’un cadre à l’autre et utilise les points de surveillance, les points d’arrêt conditionnels et les vidages mémoire pour trouver l’origine d’une valeur incorrecte.
- Déboguer méthodiquement : reproduire le problème, l’isoler et tester une hypothèse
- Explique ce qu’est un comportement indéfini et pourquoi il rend les programmes C imprévisibles, et reconnais ses sources les plus courantes dans les calculs, les expressions, les pointeurs et la mémoire.
- Comprendre ce qu’est un comportement indéfiniDistingue les comportements indéfinis, définis par l’implémentation et non spécifiés, et explique pourquoi un code au comportement indéfini peut sembler fonctionner puis échouer.
- Repérer les comportements indéfinis dans les calculs et les expressionsReconnais les débordements d’entiers signés, les divisions et décalages incorrects, les conversions hors plage, les effets de bord non séquencés et les lectures de valeurs non initialisées, et écris les vérifications qui les évitent.
- Repérer les comportements indéfinis liés aux pointeurs, aux tableaux et à la mémoireReconnais les accès hors limites, les chaînes incorrectes, les pointeurs nuls et pendants, les utilisations après libération et les tailles d’allocation incorrectes, et connais la méthode sûre pour chaque cas.
- Comprendre ce qu’est un comportement indéfini
- Fais vérifier au programme ses propres hypothèses pendant son exécution : exprime-les avec des assertions et compile des versions de test avec des sanitizers qui s’arrêtent sur les erreurs de mémoire et les comportements indéfinis et indiquent exactement où ils se sont produits.
- Exprimer les hypothèses à l’aide d’assertionsUtilise assert pour les conditions que seul un bogue peut rendre fausses, comprends le rôle de NDEBUG et distingue les assertions de la gestion des erreurs prévisibles.
- Repérer les erreurs de mémoire avec AddressSanitizerCompile une version de test avec AddressSanitizer, lis ses rapports d’erreur et connais ce qu’il détecte, ce qui lui échappe et son coût.
- Détecter les comportements indéfinis avec UBSan et mettre en place une procédure de vérificationUtilise UndefinedBehaviorSanitizer et découvre les autres sanitizers, puis combine avertissements, sanitizers et tests dans une procédure qui détecte tôt les erreurs.
- Exprimer les hypothèses à l’aide d’assertions
- Applique des règles de programmation sécurisée comme celles de SEI CERT C aux chaînes, aux tampons, aux données d’entrée et aux erreurs, et évalue le coût de l’absence de sécurité mémoire en C au regard de ce que ce langage offre.
- Utiliser un référentiel de programmation sécuriséeExplique l’organisation du SEI CERT C Coding Standard, lis une règle et ses exemples, et connais les principales règles qui préviennent les bogues C les plus fréquents.
- Manipuler les chaînes et les tampons en toute sécuritéConnais toujours la taille d’un tampon, lis et copie les chaînes avec des fonctions qui respectent cette taille et détecte les troncatures.
- Valider les données d’entrée et gérer les erreursConsidère toutes les données extérieures comme non fiables, convertis le texte en nombres en vérifiant toutes les erreurs et vérifie le résultat de chaque appel de bibliothèque susceptible d’échouer.
- Évaluer les compromis en matière de sécurité mémoireExplique ce qu’est la sécurité mémoire, pourquoi C ne la garantit pas, ce que cela coûte et ce que C offre en contrepartie, ainsi que la façon dont plusieurs niveaux de défense et les langages à sécurité mémoire réduisent le risque.
- Utiliser un référentiel de programmation sécurisée
Débogage en C : cours
1Faire détecter les problèmes par le compilateur
Le compilateur peut signaler de nombreux problèmes avant l’exécution d’un programme. Apprends à lire ses messages comme des indices sur ton code, puis choisis les options qui révèlent davantage de points importants. Les informations de débogage, l’optimisation, l’analyse statique et le durcissement pour la production ont des rôles différents. Bien les distinguer t’aide à créer un programme que tu peux inspecter pendant le développement et tester à nouveau sous la forme que tes utilisateurs exécuteront.
1.1 Activer les avertissements et lire les messages du compilateur
Les avertissements te donnent l’occasion de corriger un défaut avant qu’une entrée ne le déclenche. Une erreur empêche la compilation de réussir. Un avertissement permet à la compilation de continuer, mais signale un point que tu dois comprendre : du code valide mais suspect, un comportement potentiellement indéfini ou une violation des règles du langage que le compilateur accepte.
Choisir des vérifications utiles
Lance GCC ou Clang en indiquant explicitement la version du langage et un large ensemble d’avertissements :
clang -std=c17 -Wall -Wextra -Wpedantic -g program.c -o program
-Wall active de nombreuses vérifications courantes, mais pas tous les avertissements. -Wextra en ajoute d’autres, dont les comparaisons entre valeurs signées et non signées en C. -Wpedantic signale de nombreux écarts par rapport à la norme ISO choisie ; ce n’est pas un test complet de conformité. L’option /W4 de MSVC remplit un rôle similaire sous Windows.
Ajoute des vérifications adaptées aux risques présents dans ton code :
| Option | Ce qu’elle aide à révéler |
|---|---|
-Wconversion | Les conversions implicites qui peuvent changer une valeur |
-Wshadow | Une déclaration locale qui masque une autre variable |
-Wformat=2 | Des vérifications supplémentaires des chaînes de format |
-Werror | Transforme les avertissements activés en erreurs de compilation |
Par exemple, double price = 3.7; int whole = price; supprime la partie fractionnaire. -Wconversion rend cette conversion visible. Si la troncature est voulue et que la valeur est représentable, (int)price explicite ce choix. Si la partie fractionnaire compte, conserve un type à virgule flottante. Pour arrondir à l’entier le plus proche, lround de <math.h> a un rôle différent d’une conversion explicite.
Lire le premier message utile
Un diagnostic indique normalement un emplacement dans le code source, un message et l’option d’avertissement. Un accent circonflexe marque l’expression concernée ; les notes qui suivent peuvent indiquer une déclaration ou suggérer une correction. Lis tout le groupe de messages. Si une erreur de syntaxe provoque de nombreux messages ultérieurs, corrige la première erreur et recompile avant d’examiner les autres.
Supposons qu’un programme contienne ces trois lignes, avec average déclaré comme double :
if (count = 0) { /* report no data */ }
printf("%d\n", average);
int unused;
L’affectation dans la condition donne à count la valeur zéro, ce qui rend la condition fausse. Si l’intention était de faire une comparaison, utilise count == 0. Ajouter des parenthèses autour de l’affectation peut faire disparaître un avertissement tout en laissant le bogue intact.
Le format %d attend un int, mais l’argument est un double. Utilise %f ; une incompatibilité de format est un comportement indéfini, pas une demande de conversion de la valeur. Supprime la déclaration inutilisée si elle ne sert à rien. Ces trois problèmes ont des causes différentes et demandent donc chacun leur propre correction.
Suivre les indices
Prête une attention particulière aux valeurs non initialisées, aux retours manquants, aux adresses de variables locales renvoyées et à l’utilisation de sizeof sur les paramètres tableaux. Un fichier d’en-tête manquant peut laisser une fonction non déclarée ; les versions récentes de GCC et Clang rejettent cela par défaut. N’ajoute pas une conversion explicite simplement pour faire disparaître un diagnostic. Explique d’abord les types et les valeurs attendus, puis écris le code de façon à exprimer cette intention.
Un programme sans avertissement peut malgré tout avoir une condition de boucle incorrecte ou utiliser un indice invalide fourni en entrée. Certains avertissements dépendent aussi de l’optimisation et de l’analyse du flux de données, et les compilateurs détectent des cas différents. Garde les avertissements activés dans les compilations ordinaires, puis examine le comportement à l’exécution avec le débogueur et les outils de test.
Points clés
- Choisis explicitement la version de C.
-Walln’active pas tous les avertissements.- Corrige la cause d’un diagnostic avant de le supprimer.
- Une conversion de format doit correspondre au type de l’argument.
- Une compilation sans avertissement nécessite encore des tests à l’exécution.
1.2 Choisir les paramètres de compilation pour le débogage, les tests et la production
Un même code source peut être compilé pour faciliter son inspection, effectuer des tests intensifs ou produire une version de production efficace. Les paramètres de compilation déterminent les informations visibles et les vérifications supplémentaires exécutées. Donne un objectif clair à chaque version compilée.
Conserver les informations sur le code source
L’option -g ajoute les noms, les types et les correspondances entre les instructions machine et les lignes source. Elle permet à un débogueur d’afficher ton code et aide les rapports des sanitizers à indiquer les emplacements. Elle n’active pas de vérifications à l’exécution et ne désactive pas l’optimisation. Sans informations de débogage, certains noms de fonctions peuvent rester disponibles, mais les lignes source et les variables locales sont beaucoup plus difficiles à retrouver.
L’optimisation modifie la fidélité avec laquelle l’exécution suit l’ordre du code source :
| Réglage | Usage habituel | Ce que tu peux observer |
|---|---|---|
-O0 | Débogage simple | L’ordre du code source est généralement plus facile à suivre |
-Og | Débogage avec une légère optimisation | Certaines transformations subsistent |
-O2 | Version de production efficace | Opérations réordonnées et variables supprimées |
Une session de débogage sur du code optimisé peut sauter des lignes ou afficher une variable sous la forme « ». Le compilateur a préservé le comportement du code défini, mais il peut ne plus conserver toutes les valeurs intermédiaires présentes dans le code source. -O0 ne traduit pas littéralement chaque ligne C en une seule instruction machine.
Compiler selon l’objectif
Une commande simple pour le débogage est :
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 program.c -o program-debug
Une version de test ajoute des sanitizers. Une version de production utilise couramment -O2 et peut définir NDEBUG pour supprimer les assertions. La validation ordinaire des entrées et la gestion des erreurs doivent rester actives lorsque les assertions disparaissent. Teste aussi la configuration de production : un problème visible uniquement avec l’optimisation est une raison sérieuse de rechercher un comportement indéfini, notamment des lectures de valeurs non initialisées et des débordements d’entiers signés.
Conserve les options communes dans un Makefile ou une autre configuration de compilation. Des noms de sortie distincts évitent d’exécuter par accident un ancien exécutable de production pendant que tu inspectes le code source récemment modifié. Après chaque modification du code source, recompile l’exécutable que tu comptes lancer.
Ajouter l’analyse et le durcissement
L’analyse statique suit les chemins possibles dans le code source sans exécuter le programme. Essaie gcc -fanalyzer ou clang --analyze ; scan-build, clang-tidy et cppcheck offrent d’autres méthodes d’analyse. Un analyseur peut remarquer qu’une branche de gestion d’erreur libère deux fois un pointeur, même si tes tests ne passent jamais par cette branche. Examine son raisonnement : l’analyse des chemins possibles peut produire de fausses alertes.
L’analyse dynamique vérifie l’exécution réelle. Les sanitizers et Valgrind ne voient que les chemins parcourus : leur couverture dépend donc de tes entrées. Combine les deux types d’analyse.
Le durcissement en production limite les dommages causés par les défauts restants. -fstack-protector-strong place des vérifications dans certains cadres de pile pour détecter de nombreux écrasements. Sur les systèmes qui le prennent en charge, _FORTIFY_SOURCE ajoute des vérifications aux appels de bibliothèque lorsque les tailles des objets peuvent être déterminées. Il nécessite l’optimisation ; le niveau 2 est courant, tandis que le niveau 3 nécessite une prise en charge adaptée par le compilateur et la bibliothèque C. Ce sont des fonctionnalités propres aux plateformes, pas des garanties d’ISO C.
Un programme durci peut s’arrêter lorsqu’il détecte une corruption. L’opération invalide doit quand même être corrigée dans le code source ; modifier les options de production ne peut pas corriger une boucle qui dépasse les limites d’un tableau.
Points clés
- Utilise
-gpour obtenir des informations de débogage liées au code source. - Débogue avec
-O0ou-Oget teste aussi la version de production. - Garde les vérifications des entrées actives lorsque
NDEBUGsupprime les assertions. - L’analyse statique et l’analyse à l’exécution explorent des chemins et détectent des défauts différents.
- Conserve les options de compilation dans une configuration reproductible.
- Le durcissement réduit les dommages causés par les bogues restants.
2Repérer et corriger les bogues avec un débogueur
Un plantage indique où l’exécution s’est arrêtée ; l’erreur peut avoir eu lieu bien avant. Pars d’une défaillance reproductible et d’une petite entrée, puis utilise un débogueur pour inspecter le programme à des moments choisis. Les points d’arrêt et l’exécution pas à pas montrent comment les valeurs évoluent. Les cadres de pile, les points de surveillance et les points d’arrêt conditionnels aident à remonter d’une valeur incorrecte au code qui l’a créée, afin que chaque expérience réponde à une question précise.
2.1 Déboguer méthodiquement : reproduire le problème, l’isoler et tester une hypothèse
Une défaillance est le comportement que tu observes : un plantage, un total incorrect ou une sortie qui n’apparaît jamais. Un bogue est l’erreur qui l’a causée. Distinguer les deux évite de modifier la ligne du plantage avant de comprendre comment le programme y est arrivé.
Rendre la défaillance reproductible
Note la commande, les données d’entrée, les options de compilation, le résultat attendu et le résultat obtenu. Garde l’entrée qui provoque l’échec inchangée pendant tes expériences. Si un programme échoue uniquement avec un fichier, sauvegarde ce fichier avant de le modifier. La reproduction te permet de juger si une correction proposée fonctionne.
Réduis ensuite le cas tout en conservant la défaillance. Supprime les enregistrements ou le code sans rapport avec le problème et relance le programme. Supposons qu’un enregistrement parmi 1 000 déclenche à lui seul un plantage. Essayer les moitiés permet de le trouver en 10 étapes environ. Si aucune moitié ne provoque la défaillance seule, celle-ci peut nécessiter une suite d’enregistrements ou un état accumulé auparavant. Conserve cette suite et retire de plus petits morceaux autour d’elle. La division en deux n’est utile que si l’entrée conservée sollicite encore la cause du problème.
Poser une question vérifiable
Un champ vide dans un fichier réduit suggère une hypothèse : « L’analyseur utilise le champ avant de vérifier s’il existe. » Teste-la avec une petite entrée contenant un champ vide. Inspecte le pointeur renvoyé ou ajoute une vérification ciblée. Modifie une seule chose à la fois ; si tu modifies simultanément l’analyseur, la taille du tampon et l’entrée, une réussite ne t’indiquera pas quelle modification a compté.
Corrige la règle enfreinte, relance le cas réduit, puis relance l’entrée initiale et les tests existants. Conserve le cas déclencheur comme test de non-régression pour qu’une modification ultérieure ne puisse pas réintroduire silencieusement la même défaillance.
Interpréter l’arrêt
« Segmentation fault » indique généralement SIGSEGV : le système a arrêté un accès mémoire invalide. Un pointeur nul, un pointeur libéré ou une adresse hors limites en est une cause fréquente. Un accès invalide peut aussi ne pas provoquer de plantage immédiat : l’absence de message ne suffit donc pas à conclure.
« Aborted » indique généralement SIGABRT, déclenché par abort(). Une assertion qui échoue ou une vérification de bibliothèque qui détecte une corruption du tas peut en être la cause. Dans bash, les codes de retour courants associés aux signaux sont 139 pour SIGSEGV et 134 pour SIGABRT ; ce sont des conventions de l’interpréteur de commandes, pas des valeurs de retour C que ton programme doit utiliser.
Observer sans perdre la sortie
Un court message de diagnostic peut révéler la première valeur incorrecte :
fprintf(stderr, "index=%zu total=%d\n", index, total);
Les indications rendent la sortie lisible lorsque la même ligne s’exécute de nombreuses fois. stderr est utile pour les diagnostics car il n’est pas entièrement mis en tampon ; stdout redirigé vers un fichier ou un tube est normalement entièrement mis en tampon, si bien qu’un plantage peut faire perdre la sortie en attente. Supprime les affichages temporaires après la correction.
Explique à voix haute une fonction suspecte, ligne par ligne. Cette méthode du « canard en plastique » t’oblige à confronter chaque opération au comportement attendu. Si un bogue change lorsque tu ajoutes des affichages ou modifies l’optimisation, recherche un comportement indéfini avec les sanitizers. Agrandir un tableau jusqu’à ce qu’un plantage disparaisse peut masquer un indice invalide sans le corriger.
Points clés
- Sauvegarde un cas de reproduction exact avant de modifier le code.
- Réduis les données d’entrée tout en conservant la défaillance.
- Teste une hypothèse avec une seule modification ou observation contrôlée.
- Remonte du plantage à sa cause, qui peut être antérieure.
- Relance le cas initial et conserve un test de non-régression.
2.2 Exécuter un programme, le suspendre et le parcourir pas à pas dans gdb ou lldb
Un débogueur te permet de suspendre un programme en cours d’exécution et d’inspecter les valeurs qui expliquent son comportement. Utilise GDB sur de nombreux systèmes Linux ou LLDB sur macOS. Les boutons de débogage des environnements de développement intégrés reposent sur les mêmes principes : arrêter, exécuter, inspecter et avancer pas à pas.
Partir du bon exécutable
Compile avec -g -O0, puis ouvre le résultat avec gdb ./program ou lldb ./program. À l’invite, run records.txt 3 le lance avec deux arguments. Dans GDB, run < input.txt redirige l’entrée standard ; dans LLDB, utilise process launch -i input.txt. Utilise quit pour quitter.
Place un point d’arrêt avant l’exécution. break main dans GDB et b main dans LLDB arrêtent le programme au début de main. Les deux acceptent un emplacement composé d’un fichier et d’une ligne, comme b program.c:12. Les commandes complètes de LLDB comprennent breakpoint set --name main et breakpoint set --file program.c --line 12.
Un point d’arrêt suspend normalement le programme avant l’exécution de l’instruction source indiquée. Si la ligne sélectionnée affecte une valeur, affiche celle-ci après avoir exécuté la ligne pour inspecter sa nouvelle valeur. Recompile après toute modification du code source : le débogueur exécute le code machine, pas le texte ouvert dans ton éditeur.
Choisir comment avancer
| Commande | Rôle dans GDB et LLDB |
|---|---|
next ou n | Exécuter la ligne courante, généralement sans entrer dans les appels |
step ou s | Entrer dans une fonction appelée lorsque les informations source le permettent |
finish | Exécuter jusqu’au retour de la fonction sélectionnée |
continue ou c | Reprendre jusqu’à un autre arrêt ou à la fin du programme |
D’autres points d’arrêt peuvent interrompre next ou finish. Utilise next pour un appel de bibliothèque que tu n’as pas besoin d’inspecter. Si tu entres dans du code que tu ne souhaites pas examiner, finish rend le contrôle à la fonction appelante.
Inspecter une boucle à chaque passage
Considère une somme initialisée à zéro et ce tableau :
int a[5] = {4, 8, 15, 16, 23};
for (int i = 0; i <= 5; ++i) {
sum += a[i];
}
Arrête-toi dans le corps de la boucle et affiche i et sum avant l’addition. Après les cinq premières additions, les sommes successives sont 4, 12, 27, 43 et 66. À l’arrêt suivant, i == 5, mais les indices valides s’arrêtent à 4. Tu as trouvé l’itération supplémentaire sans avoir besoin de lire l’élément invalide. Remplace la condition par i < 5, recompile et relance le programme.
print expr ou p expr évalue une expression dans les deux débogueurs. Commence par des observations sûres comme p i et p sum ; une expression du débogueur qui déréférence un pointeur invalide peut elle-même tenter une lecture invalide. display i dans GDB répète l’affichage à chaque arrêt. info locals dans GDB et frame variable dans LLDB affichent les variables locales. Pour cinq éléments valides accessibles par ptr, utilise p *ptr@5 dans GDB ou parray 5 ptr dans LLDB.
Tu peux tester une hypothèse en modifiant une valeur pendant la suspension : GDB accepte set var i = 4, tandis que LLDB accepte expr i = 4. Cela modifie une exécution, pas le code source. Pour gérer les points d’arrêt, utilise info breakpoints et delete 1 dans GDB, ou breakpoint list et breakpoint delete 1 dans LLDB.
Points clés
- Compile et recompile l’exécutable avec les informations de débogage.
- Une instruction source sur laquelle le programme est arrêté ne s’est normalement pas encore exécutée.
- Utilise
next,step,finishetcontinuepour avancer de différentes façons. - Inspecte les indices et les bornes avant d’évaluer des expressions qui accèdent à la mémoire.
2.3 Diagnostiquer les plantages et les valeurs inattendues
Un plantage dans une bibliothèque commence souvent par un argument invalide provenant de ton propre code. La pile d’appels montre les appels de fonctions actifs. Utilise-la pour passer du lieu de l’arrêt à la fonction appelante dont tu peux expliquer les valeurs.
Lire les cadres en remontant depuis l’appel courant
Saisis bt dans l’un ou l’autre débogueur. Le cadre #0 correspond à l’appel actif le plus profond ; les cadres de numéro supérieur sont ses fonctions appelantes. Un cadre fournit le contexte d’exécution de la fonction, notamment les arguments et les variables locales disponibles. L’optimisation peut supprimer certaines de ces informations.
Supposons que find_name(2) ne trouve aucune correspondance et que la fonction appelante fasse ceci :
const char *name = find_name(2);
printf("%zu\n", strlen(name));
L’arrêt peut se produire dans strlen. Dans bt, trouve le premier cadre appartenant à ton code. S’il s’agit du cadre 1, sélectionne-le avec frame 1 dans GDB ou frame select 1 dans LLDB. Affiche name. Une valeur nulle explique pourquoi l’opération sur la chaîne a échoué. La fonction appelante doit gérer l’absence de nom avant d’appeler strlen ; modifier la bibliothèque ne peut pas corriger l’argument.
Utilise up et down pour te déplacer entre les fonctions appelantes et appelées. Sélectionner un cadre modifie ce que le débogueur affiche et les variables locales auxquelles une expression fait référence. Cela ne reprend pas l’exécution et n’annule pas un appel.
S’arrêter là où une valeur change
Un point d’arrêt pose la question : « Que se passe-t-il à cet endroit ? » Un point de surveillance demande : « Quelle opération change cette valeur ? » Si un nombre stocké devient incorrect bien avant un plantage, suspends le programme tant que ce nombre est dans la portée et utilise :
GDB: watch count
LLDB: watchpoint set variable count
Poursuis jusqu’à ce que le débogueur s’arrête sur un changement. Inspecte l’ancienne et la nouvelle valeur ainsi que l’emplacement source de l’instruction d’écriture. Un changement peut être légitime : compare-le donc à la règle de la structure de données. Le matériel n’offre qu’un petit nombre de points de surveillance ; quatre sont courants sur x86-64. Le stockage d’une variable locale n’a de sens que tant que sa fonction est active.
Pour une itération tardive connue, utilise un point d’arrêt conditionnel. break 20 if i == 999 dans GDB suspend le programme à la ligne 20 uniquement lorsque la condition est vraie. Son équivalent dans LLDB est breakpoint set --line 20 --condition 'i == 999'. Cela évite des centaines d’étapes manuelles.
Inspecter un plantage enregistré
Un vidage mémoire enregistre l’état d’un processus qui a planté. Là où les vidages mémoire sont activés, ulimit -c unlimited les autorise pour les programmes lancés depuis cet interpréteur de commandes. Ouvre un vidage enregistré avec gdb ./program core ou lldb ./program -c core, en utilisant l’exécutable qui l’a produit. Les règles du système déterminent si un vidage est enregistré et où il est placé ; les systèmes Linux utilisant systemd peuvent le gérer avec coredumpctl.
Exécute bt, sélectionne ton cadre et inspecte les valeurs comme lors d’un plantage en direct. Le vidage ne permet pas de reprendre l’exécution du programme. Si malloc ou free détecte une corruption, l’écriture incorrecte initiale peut être absente de cette pile. Reproduis le problème avec AddressSanitizer pour arrêter le programme plus près de cette écriture. Si une valeur locale semble arbitraire, vérifie si l’exécution a atteint son initialisation avant de mettre en cause le débogueur.
Points clés
- Utilise
btpour suivre les appels actifs. - Sélectionne un cadre dans ton code et inspecte les arguments passés à la bibliothèque.
- Les points de surveillance arrêtent le programme lors d’un changement de valeur ; les points d’arrêt conditionnels l’arrêtent dans des états choisis.
- Un vidage mémoire permet l’inspection après un plantage.
- La corruption peut être antérieure à la pile visible lors du plantage final.
3Reconnaître et éviter les comportements indéfinis
Pour certaines opérations C, le langage ne garantit aucun résultat. Elles peuvent sembler fonctionner dans une version compilée et échouer dans une autre, ce qui rend trompeuse une simple démarche par essais et erreurs. Apprends à distinguer les comportements indéfinis des choix documentés d’une plateforme et des variations autorisées. Examine ensuite les calculs, l’ordre des expressions, les bornes des tableaux et les durées de vie en mémoire, en plaçant les vérifications avant qu’une opération ne franchisse l’une de ces limites.
3.1 Comprendre ce qu’est un comportement indéfini
Un comportement indéfini signifie que la norme C n’impose aucune exigence sur ce qui se produit après une opération invalide. Elle ne promet ni exception, ni message d’erreur utile, ni valeur rebouclée prévisible. Un programme peut planter, corrompre des données, afficher une réponse plausible ou sembler fonctionner.
Distinguer trois types de comportements
| Type | Ce que le langage autorise | Exemple en C17 |
|---|---|---|
| Indéfini | Aucun comportement imposé | Débordement d’entier signé |
| Défini par l’implémentation | Choix documenté de l’implémentation | Le fait que char simple soit signé ou non |
| Non spécifié | Choix autorisé qui n’a pas à être documenté | Ordre d’évaluation des arguments indépendants d’une fonction |
Un comportement défini par l’implémentation peut être utilisé délibérément lorsque la plateforme documentée fait partie de la conception d’un programme. Pour obtenir du code portable, ne suppose pas que toutes les implémentations font le même choix. Un comportement non spécifié est également valide : f(a(), b()) peut évaluer d’abord l’un ou l’autre des appels indépendants. Écris du code qui reste correct malgré cette variation autorisée. La largeur de int et, en C17, le décalage à droite d’un entier signé négatif sont d’autres choix qui dépendent de la plateforme.
Pourquoi l’optimisation révèle des bogues
C laisse au programmeur les vérifications des bornes des tableaux et des additions d’entiers signés. Le compilateur peut optimiser le code en supposant que le programme respecte les règles du langage. Une tentative de détection de débordement peut enfreindre la règle même qu’elle cherche à vérifier :
int will_overflow(int x) {
return x + 1 < x;
}
Pour toute valeur de x pour laquelle l’addition d’entiers signés est définie, x + 1 est supérieur à x. Le compilateur peut donc remplacer le résultat par zéro. À la valeur INT_MAX, l’addition a un comportement indéfini : le compilateur ne doit donc aucun résultat rebouclé à la fonction appelante. Observer une réponse différente dans une version non optimisée ne rend pas ce test valide.
Le test sûr s’effectue avant l’opération :
if (x == INT_MAX) {
/* handle overflow */
} else {
++x;
}
Utilise <limits.h> pour connaître les limites de l’implémentation. Un maximum écrit en dur suppose une largeur de type particulière ; la macro désigne la limite réelle.
Placer les vérifications avant la première utilisation
Cet ordre est également essentiel pour les pointeurs :
if (p == NULL) {
return -1;
}
int v = *p;
Inverser ces instructions permettrait le déréférencement d’un pointeur nul avant la protection. Dès que le code déréférence p, un optimiseur peut raisonner que le chemin défini exige déjà que p soit non nul et supprimer un test ultérieur de nullité.
Modifier les options, ajouter un affichage ou déplacer une variable peut changer le symptôme visible d’un comportement indéfini. Une exécution réussie établit seulement ce que cette version compilée a fait pour cette entrée. Pour corriger le code, détermine quelle opération n’a pas de sens défini et quelle condition doit être vraie avant son exécution. Les avertissements peuvent repérer certains cas ; l’analyse statique peut explorer les chemins possibles ; ASan et UBSan vérifient de nombreuses opérations exécutées. Les règles de programmation fournissent les méthodes correspondantes dans le code source, comme vérifier un indice ou une borne arithmétique avant de l’utiliser.
Points clés
- Un comportement indéfini ne garantit ni un plantage ni un résultat stable.
- Les choix définis par l’implémentation doivent être documentés.
- Les choix non spécifiés restent conformes aux règles du langage.
- Les optimiseurs peuvent supposer qu’un débordement d’entier signé ne se produit jamais dans du code défini.
- Vérifie un pointeur ou une borne arithmétique avant l’opération risquée.
3.2 Repérer les comportements indéfinis dans les calculs et les expressions
Un calcul peut échouer avant que tu aies la possibilité d’en inspecter le résultat. Vérifie les opérandes tant que leurs valeurs sont valides et sache quel type est utilisé pour chaque opération.
Limites des types signés et non signés
Le débordement d’entier signé est indéfini. Avec un int sur 32 bits, INT_MAX vaut 2 147 483 647 ; ajouter un n’est pas une méthode portable pour obtenir un nombre négatif. INT_MIN / -1, -INT_MIN et une multiplication trop grande échouent aussi lorsque le résultat mathématique n’est pas représentable.
L’arithmétique non signée reboucle dans la plage de son type. Pour un unsigned int sur 32 bits, UINT_MAX + 1u vaut zéro et 0u - 1u vaut 4 294 967 295. Ce comportement est défini mais peut quand même fausser un calcul de taille. Une boucle décroissante non signée utilisant i >= 0 n’atteint jamais une valeur d’arrêt négative.
Pour une addition générale d’entiers signés, vérifie avant d’additionner :
if ((b > 0 && a > INT_MAX - b) ||
(b < 0 && a < INT_MIN - b)) {
/* report overflow */
} else {
result = a + b;
}
Les soustractions utilisées dans ces tests restent dans la plage autorisée car chacune est protégée par le test du signe de b. Calculer d’abord la somme puis la comparer est trop tardif. Un type plus large n’aide que s’il peut contenir tous les résultats intermédiaires : convertis un opérande avant l’opération. C23 fournit ckd_add dans <stdckdint.h> ; GCC et Clang proposent __builtin_add_overflow.
Division, décalages et conversions
Rejette zéro avant une division entière ou un calcul de reste. Rejette aussi la division du minimum d’un type signé par moins un lorsque son résultat n’est pas représentable. Les systèmes à virgule flottante IEEE 754 produisent couramment l’infini ou NaN lors d’une division par zéro, ce qui permet à des résultats numériques invalides de se propager sans interruption comparable à celle des entiers.
Pour les opérations sur les bits, utilise des opérandes non signés. Le nombre de positions du décalage doit être positif ou nul et inférieur à la largeur de l’opérande gauche après promotion. Avec des opérandes sur 32 bits, un décalage de 32 positions est indéfini. En C17, décaler vers la gauche une valeur signée négative est indéfini ; 1 << 31 est également indéfini pour un int sur 32 bits, tandis que 1u << 31 est valide pour un unsigned int sur 32 bits.
Une conversion d’une valeur à virgule flottante en entier exige que la valeur tronquée soit représentable, même si le type de destination est non signé. (unsigned)-1.0 est indéfini. La conversion d’un entier vers un type non signé ramène en revanche la valeur dans la plage de destination : (unsigned)-1 devient UINT_MAX. En C17, la conversion d’un entier hors plage vers un type signé est définie par l’implémentation. Vérifie les plages avant une conversion rétrécissante ; -Wconversion aide à révéler les changements implicites.
Rendre l’ordre d’évaluation sans conséquence
printf("%d %d\n", i, ++i); lit et modifie i sans l’ordre requis. i = i++ + 1; et a[i] = i++; contiennent aussi des opérations non séquencées. Sépare le travail en instructions distinctes, dans l’ordre voulu et explicite.
Initialise les variables automatiques avant de les lire : int sum = 0; donne un sens à sum += x;. Les variables globales et statiques sont initialisées à zéro. L’exigence de représentation en complément à deux de C23 ne définit pas le débordement d’entier signé ; le code portable a toujours besoin de vérifications.
Points clés
- Vérifie les calculs sur les entiers signés avant d’en calculer le résultat.
- Le rebouclage des entiers non signés est défini, mais peut fausser les tailles et les boucles.
- Vérifie les opérandes d’une division et les nombres de positions de décalage.
- La conversion d’une valeur à virgule flottante vers un entier non signé diffère de celle d’un entier vers un entier non signé.
- Séquence explicitement les modifications et initialise les valeurs avant leur utilisation.
- C23 laisse toujours le débordement d’entier signé indéfini.
3.3 Repérer les comportements indéfinis liés aux pointeurs, aux tableaux et à la mémoire
Une adresse plausible ne suffit pas pour qu’un pointeur soit valide. L’objet doit exister, l’accès doit rester dans ses bornes et l’opération doit être autorisée pour cet objet. Ces vérifications expliquent aussi bien les plantages immédiats que les corruptions découvertes bien plus tard.
Rester dans les limites de l’objet
Un tableau de n éléments possède des indices de zéro à n - 1. Un pointeur juste après le dernier élément peut marquer la fin d’une boucle, mais tu ne dois pas le déréférencer. i <= n parcourt un élément de trop. La règle CERT ARR30-C couvre ces erreurs de limites.
Une écriture hors limites peut atteindre de la mémoire accessible appartenant à un autre objet. Le programme peut continuer jusqu’à ce qu’un nombre, un pointeur ou une donnée de l’allocateur endommagé soit utilisé. Un plantage ultérieur dans free n’indique pas l’écriture initiale. ASan peut s’arrêter sur l’écriture lorsque ses vérifications détectent le franchissement des limites.
Former une chaîne valide avec des caractères
Une chaîne C se termine par '\0'. Son stockage doit contenir un octet de plus que son nombre de caractères. C accepte char s[5] = "hello";, mais les cinq octets contiennent des lettres, sans place pour le caractère nul de fin. La passer à strlen ou à %s peut entraîner une lecture au-delà du tableau. Prévois assez de place et assure-toi qu’un caractère nul de fin est effectivement présent. STR31-C concerne la capacité ; STR32-C concerne le passage d’une chaîne terminée par un caractère nul.
Un littéral de chaîne de caractères n’est pas modifiable. Après char *p = "hi";, p[0] = 'H'; est indéfini. Utilise char p[] = "hi"; pour obtenir une copie modifiable, ou const char *p pour rendre les tentatives de modification du littéral visibles au compilateur. C’est l’objet de STR30-C.
Respecter la durée de vie et la propriété
Vérifie NULL avant de déréférencer le résultat d’une allocation, d’une ouverture de fichier ou d’une recherche. Lorsqu’un bloc est libéré, aucun pointeur vers ce bloc ne donne plus accès à un objet vivant. Ne lis pas ce bloc, n’y écris pas et ne le libère pas à nouveau. Libère uniquement le pointeur approprié renvoyé par une allocation dynamique, jamais un tableau local ou un pointeur intérieur. free(NULL) est sans danger, mais affecter la valeur nulle à un pointeur après la libération ne corrige pas les autres alias.
Une fonction qui renvoie l’adresse d’un tableau local renvoie un pointeur pendant :
char *greeting(void) {
char text[16] = "Welcome";
return text;
}
La durée de vie du tableau prend fin au retour de la fonction. Fournis à la fonction appelante un tampon et sa capacité à remplir, ou alloue un résultat dont la propriété est transmise à la fonction appelante. Un affichage réussi immédiatement après le retour ne peut pas prolonger la durée de vie de l’ancien tableau.
Allouer les octets nécessaires à chaque élément
malloc(n) demande des octets, pas n objets du type pointé. L’expression habituelle fondée sur les éléments est n * sizeof *a. Empêche d’abord le rebouclage de ce produit :
if (n > SIZE_MAX / sizeof *a) {
/* reject the requested size */
}
Inclus <stdint.h> pour SIZE_MAX et <stdlib.h> pour les fonctions d’allocation. Une fois la taille validée, alloue la mémoire, vérifie le pointeur renvoyé et écris chaque valeur avant de la lire. malloc laisse la mémoire non initialisée. calloc(n, sizeof *a) met sa zone mémoire à zéro et les bibliothèques courantes rejettent les produits qui débordent. Une vérification de taille, une vérification du résultat de l’allocation et un indice valide résolvent des problèmes différents ; les trois comptent.
Points clés
- Ne déréférence que les pointeurs situés dans les bornes d’un objet dont la durée de vie est en cours.
- Réserve de la place pour le caractère nul de fin de chaîne et stocke-le.
- Ne modifie pas les littéraux de chaîne de caractères.
- Libérer une allocation invalide l’accès par tous ses alias.
- Renvoie une zone mémoire dont la durée de vie permet à la fonction appelante de l’utiliser.
- Vérifie la multiplication par le nombre d’éléments et le résultat de l’allocation.
4Détecter les erreurs à l’exécution avec des assertions et des sanitizers
Une version de test peut vérifier davantage que sa sortie finale. Les assertions vérifient les hypothèses que ton code s’engage à maintenir, tandis que les sanitizers vérifient les opérations, comme les accès mémoire et les calculs, au moment où elles se produisent. Apprends à lire chaque rapport, à choisir un outil adapté à l’erreur que tu soupçonnes et à exécuter des entrées qui atteignent les chemins risqués. Une procédure de vérification utile relie une erreur détectée à une correction et à un test conservé.
4.1 Exprimer les hypothèses à l’aide d’assertions
Une assertion exprime une propriété qui devrait être vraie si ton code est correct. Elle transforme une hypothèse fausse mais cachée en un arrêt proche de l’endroit où cette hypothèse compte.
Vérifier un contrat de programmation
Inclus <assert.h> et écris assert(condition);. Une condition vraie laisse l’exécution se poursuivre. Une condition fausse affiche des informations de diagnostic sur stderr et appelle abort(). Le message indique l’expression, son emplacement dans le code source et la fonction ; sa formulation exacte dépend de la plateforme.
Pour une fonction auxiliaire interne de calcul de moyenne dont la fonction appelante garantit la validité des arguments :
double average(const int *a, size_t n) {
assert(a != NULL);
assert(n > 0);
/* compute the average */
}
Un appel accidentel avec zéro élément échoue à l’endroit où la garantie est rompue. Sous un débogueur, bt montre le mécanisme de l’assertion et de l’arrêt, suivi des appels dans ton code. Sélectionne le cadre de la fonction appelante pour voir comment elle a fourni zéro.
Les contrats interviennent à plusieurs endroits :
| Type | Ce qu’il énonce | Exemple |
|---|---|---|
| Précondition | Exigence à l’entrée | Un tableau d’entrée non vide |
| Postcondition | Garantie après l’opération | Un tableau est trié |
| Invariant | Propriété maintenue entre les étapes | Un nombre stocké correspond à la structure |
Une assertion est une documentation exécutable : elle nomme la propriété et la vérifie pendant une exécution de test. Choisis des expressions qui observent l’état sans le modifier.
Garder les opérations nécessaires hors des assertions
Définir NDEBUG avant d’inclure <assert.h>, généralement avec -DNDEBUG, désactive assert. Son expression n’est pas évaluée. Cela crée un bogue grave si l’expression effectue une opération nécessaire :
assert(++count <= MAX);
L’incrémentation disparaît dans une version compilée avec NDEBUG. Écris l’action séparément :
++count;
assert(count <= MAX);
La même règle exclut l’ouverture de fichiers, l’allocation de mémoire ou la fermeture de fichiers dans une assertion. Ces opérations doivent avoir lieu dans toutes les configurations. Leurs échecs nécessitent une gestion ordinaire des erreurs.
Gérer normalement les échecs prévisibles
Un fichier d’entrée absent, un texte utilisateur invalide et un épuisement de la mémoire peuvent survenir lors d’une utilisation correcte du programme. Vérifie ces situations avec if, signale l’erreur et effectue un retour ou libère les ressources. Une fonction auxiliaire privée peut vérifier par assertion qu’une valeur validée est dans une plage donnée, mais le point d’entrée public des données doit imposer cette plage avec des vérifications qui restent dans les versions de production.
Par exemple, teste toujours fclose(f) == EOF dans le déroulement ordinaire du programme lors de la fermeture d’un fichier de sortie. L’écriture des données en tampon peut échouer pendant la fermeture ; supprimer l’appel ou sa vérification peut masquer une perte de données.
Vérifier les faits fixes à la compilation
Une assertion statique teste une expression constante pendant la compilation. En C17, <assert.h> fournit static_assert comme macro pour _Static_assert de C11 :
static_assert(sizeof(int) == 4, "requires a 4-byte int");
Une condition fausse fait échouer la compilation. Elle ne peut pas vérifier un nombre saisi par l’utilisateur qui n’est connu qu’à l’exécution. C23 fait de static_assert un mot-clé et permet d’omettre le message. Utilise cette distinction pour placer chaque vérification là où les informations nécessaires sont disponibles.
Points clés
- Les assertions vérifient les engagements internes du programme.
- Une assertion à l’exécution qui échoue indique son emplacement et interrompt le programme.
- Ne place pas d’effets de bord dans les expressions des assertions.
- Utilise la gestion ordinaire des erreurs pour les entrées invalides et les échecs liés aux ressources.
NDEBUGdésactive les assertions à l’exécution.- Les assertions statiques vérifient des expressions constantes pendant la compilation.
4.2 Repérer les erreurs de mémoire avec AddressSanitizer
AddressSanitizer, généralement appelé ASan, ajoute des vérifications aux opérations mémoire compilées. Il peut signaler un accès incorrect au moment où il se produit, avant que les données corrompues ne provoquent un plantage ailleurs.
Compiler en incluant les vérifications à l’édition de liens
Utilise GCC ou Clang pour compiler et effectuer l’édition de liens avec ASan :
clang -std=c17 -g -fsanitize=address -fno-omit-frame-pointer program.c -o program-asan
-g fournit les emplacements dans le code source. La conservation des pointeurs de cadre facilite les traces de pile. Pour un projet composé de plusieurs fichiers, instrumente les fichiers source concernés et utilise aussi l’option du sanitizer à l’étape finale d’édition de liens. MSVC fournit /fsanitize=address pour les compilations Windows prises en charge.
ASan détecte de nombreux débordements de blocs du tas, de tableaux de la pile et de tableaux globaux. Il détecte aussi les utilisations après libération, les doubles libérations, les libérations invalides et les erreurs prises en charge liées à l’utilisation après la fin d’une portée. Sous Linux, l’environnement d’exécution ASan habituel comprend la recherche de fuites à la fin du programme. Le Clang fourni par Apple sous macOS ne propose pas cette détection des fuites ; les chaînes d’outils LLVM et la prise en charge varient selon la plateforme.
Remonter d’un rapport à la cause
Considère un tableau dont l’allocation a réussi, avec des int de 4 octets :
int *a = malloc(10 * sizeof *a);
for (int i = 0; i <= 10; ++i) {
a[i] = i;
}
L’allocation contient 40 octets. Les indices de zéro à neuf utilisent ces octets. L’indice dix commence immédiatement après. ASan peut signaler « heap-buffer-overflow », « WRITE of size 4 » et un emplacement situé zéro octet après une zone de 40 octets. La trace de l’écriture indique l’affectation ; la trace de l’allocation indique le malloc qui a établi les limites du bloc.
Lis les rapports dans cet ordre :
- Identifie la catégorie d’erreur et détermine si l’accès était une lecture ou une écriture.
- Trouve le premier cadre situé dans ton code dans la trace de l’accès.
- Compare l’accès à la zone allouée et à la taille de ses éléments.
- Pour une erreur de durée de vie, trouve aussi les traces d’allocation et de libération.
Ici, la correction est i < 10. Allouer un élément supplémentaire tout en conservant la mauvaise condition d’arrêt masque l’incompatibilité entre le nombre d’éléments prévu et la boucle. ASan s’arrête par défaut à la première erreur détectée. Corrige-la, recompile et relance le programme.
Comprendre les limites de l’outil
ASan suit les accès mémoire autorisés à l’aide d’une mémoire fantôme. Des zones rouges interdites entourent les allocations et les tableaux pris en charge. Les blocs libérés passent un certain temps en quarantaine avant leur réutilisation, ce qui aide à révéler les accès à de la mémoire libérée. Une écriture au-delà d’un membre tableau jusque dans un autre membre de la même structure peut rester dans une zone valide plus grande et échapper à la détection.
ASan ne vérifie pas les lectures ordinaires de valeurs non initialisées. Memcheck de Valgrind peut trouver ces utilisations et de nombreux défauts du tas sans recompilation, mais s’exécute couramment environ 20 à 30 fois plus lentement. Le ralentissement typique d’ASan est d’environ deux fois, avec une utilisation supplémentaire de mémoire. Ne combine pas une version compilée avec ASan avec Valgrind, MemorySanitizer ou ThreadSanitizer. Exécute délibérément des entrées correspondant aux cas limites : une erreur sur un chemin non exécuté ne peut pas apparaître dans le rapport.
Points clés
- Compile et effectue l’édition de liens avec
-fsanitize=addresset les informations de débogage. - Lis ensemble la trace de l’accès et les traces d’allocation ou de libération.
- Corrige la première cause signalée, recompile et relance le programme.
- ASan ne détecte pas les lectures ordinaires de valeurs non initialisées.
- Teste des entrées correspondant aux cas limites pour atteindre les accès mémoire risqués.
4.3 Détecter les comportements indéfinis avec UBSan et mettre en place une procédure de vérification
Un outil de vérification de la mémoire ne couvre pas toutes les opérations C invalides. UndefinedBehaviorSanitizer, ou UBSan, instrumente de nombreuses opérations arithmétiques, sur les pointeurs et sur les tableaux. Associe-le à ASan pour qu’une exécution de test puisse détecter plusieurs catégories de défauts.
Rendre les erreurs de calcul visibles
Avec un int sur 32 bits, l’addition de deux valeurs de 2 000 000 000 donne un total de 4 000 000 000. Ce résultat dépasse INT_MAX. Un rapport UBSan indique le débordement d’entier signé à l’addition et nomme les opérandes. Sans l’outil, l’affichage d’une réponse négative ressemblant à une valeur rebouclée ne rendrait pas l’opération valide.
Utilise pour le total un type capable de représenter la plage acceptée, ou rejette une addition avant qu’elle ne déborde. Convertir en long long un résultat qui a déjà débordé est trop tardif ; l’opération elle-même doit utiliser un type adapté.
Une commande de test courante est :
clang -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=undefined program.c -o program-check
UBSan permet normalement de poursuivre après de nombreux signalements, si bien que le code ultérieur peut continuer. -fno-sanitize-recover=undefined fait arrêter le test avec un code de retour d’erreur lors de ces échecs. Cela aide les vérifications automatisées à détecter un bogue même lorsque la sortie finale affichée semble correcte.
Choisir l’outil adapté au défaut
| Outil | Rôle principal | Limite importante |
|---|---|---|
| UBSan | Nombreuses opérations indéfinies liées aux calculs, aux décalages, aux pointeurs nuls, à l’alignement et aux tableaux | Ne détecte pas tous les types de comportements indéfinis |
| ASan | Nombreuses erreurs de limites et de durée de vie | Ne suit pas les valeurs non initialisées ordinaires |
| MemorySanitizer | Utilisations de mémoire non initialisée | Nécessite des dépendances instrumentées pour obtenir des résultats fiables |
| ThreadSanitizer | Courses aux données | Nécessite une version compilée séparément d’ASan |
Le rebouclage des entiers non signés est défini, donc le groupe undefined ne le signale pas. Clang dispose d’une vérification distincte, -fsanitize=unsigned-integer-overflow, pour le code où un rebouclage indique un bogue. Ne conclus pas que tout résultat entier indésirable est un comportement indéfini.
MemorySanitizer est un outil Clang utilisé principalement sous Linux ; des bibliothèques non instrumentées peuvent perturber son suivi. ThreadSanitizer détecte les accès conflictuels de threads sans synchronisation, avec au moins une écriture. Les accès ne doivent pas nécessairement avoir lieu exactement au même instant. Aucun de ces outils ne peut être combiné à ASan dans une même version compilée.
Faire des vérifications une habitude
Garde les avertissements activés et résous-les. Compile avec ASan et UBSan, puis exécute le programme avec des entrées vides, des entrées à un élément, des tampons pleins, du texte invalide et des valeurs proches des bornes acceptées. Lance une analyse statique pour examiner les chemins que les tests peuvent manquer et répète automatiquement les vérifications lorsque le code change. Exécute aussi la configuration de production, dont les paramètres d’optimisation et d’assertion diffèrent.
Le fuzzing fournit de nombreuses entrées générées ou modifiées. libFuzzer et AFL++ peuvent révéler des chemins inhabituels dans un analyseur ; les sanitizers transforment une opération invalide déclenchée en un rapport utile. Sauvegarde une entrée qui révèle un défaut, réduis-la sans perdre la défaillance, corrige la cause et conserve-la comme test de non-régression. Une exécution sans erreur constitue un indice concernant les entrées exécutées, pas une garantie pour toutes les exécutions possibles.
Points clés
- Utilise UBSan pour de nombreuses erreurs de calcul et d’expression.
- Fais arrêter les tests automatisés par UBSan lorsque leur poursuite masquerait les erreurs.
- Le rebouclage des entiers non signés nécessite une vérification distincte lorsqu’il n’est pas souhaité.
- Utilise des versions compilées séparément pour MemorySanitizer ou ThreadSanitizer.
- Combine les tests aux limites, le fuzzing, l’analyse statique et les entrées de non-régression.
- La couverture des sanitizers dépend de l’exécution.
5Écrire du code C plus sûr
Un code C plus sûr commence à l’entrée des données dans le programme et se poursuit à chaque calcul de taille, copie, allocation et chemin de gestion d’erreur. Un référentiel de programmation transforme les défauts récurrents en vérifications concrètes que tu peux appliquer pendant l’écriture du code. Apprends à conserver les tailles des tampons, à valider les entrées numériques complètes et à établir clairement la propriété des ressources. Évalue ensuite ces responsabilités au regard du contrôle de la mémoire offert par C et de la protection apportée par les langages à sécurité mémoire.
5.1 Utiliser un référentiel de programmation sécurisée
Un référentiel de programmation sécurisée rassemble des règles concrètes qui préviennent les défauts récurrents. Il fournit à la personne qui relit le code des questions plus utiles que « Cela semble-t-il sûr ? ». Chaque règle relie une opération risquée à la condition ou à la conception nécessaire pour la rendre sûre.
Lire une règle pour en comprendre la raison
Le SEI CERT C Coding Standard provient du Software Engineering Institute de Carnegie Mellon University. L’édition imprimée de 2016 est accompagnée d’une version en ligne qui évolue. Il distingue les règles, dont la violation risque de causer des défauts, des recommandations qui améliorent la fiabilité et la sécurité.
Les identifiants combinent une catégorie, un nombre et -C. Les règles utilisent les nombres de 30 à 99 ; les recommandations, de 00 à 29. Par exemple, INT32-C concerne le débordement d’entier signé, tandis que DCL00-C est une recommandation sur les déclarations. Les catégories comprennent les déclarations (DCL), les expressions (EXP), les entiers (INT), la virgule flottante (FLP), les tableaux (ARR), les chaînes (STR), la mémoire (MEM), les entrées-sorties (FIO), l’environnement (ENV), les erreurs (ERR) et divers sujets (MSC).
La page d’une règle explique normalement le défaut, montre du code non conforme, présente des solutions conformes et évalue le risque. Lis les hypothèses de chaque solution. Une fonction de remplacement n’est utile que si son comportement répond effectivement à l’exigence.
Appliquer une règle au parcours des données
FIO30-C concerne les chaînes de format. Ce code traite des données extérieures comme un programme de mise en forme :
printf(user_name);
Un nom d’utilisateur contenant des conversions de format amène printf à chercher des arguments qui n’ont pas été fournis ; %n peut provoquer une écriture en mémoire. Garde le format sous le contrôle du programme :
printf("%s", user_name);
Autre possibilité : fputs(user_name, stdout) affiche le texte sans traitement de format. -Wformat=2 comprend des vérifications qui aident à trouver les formats non sûrs. La correction change la façon dont les données sont interprétées, pas simplement la longueur possible de la chaîne.
Construire une liste de vérification utile
Pour la mémoire, demande-toi si les indices restent dans les objets (ARR30-C), si le stockage des chaînes comprend le caractère nul de fin (STR31-C), si la mémoire libérée n’est jamais utilisée (MEM30-C), si les allocations dont le code est responsable sont libérées (MEM31-C), si seules les allocations valides sont libérées (MEM34-C) et si assez d’octets sont alloués (MEM35-C).
Pour les expressions, vérifie les rebouclages non voulus d’entiers non signés (INT30-C), les débordements d’entiers signés (INT32-C), les divisions invalides (INT33-C), les lectures de valeurs non initialisées (EXP33-C) et les déréférencements de pointeurs nuls (EXP34-C). Aux points d’utilisation des bibliothèques, gère les échecs signalés (ERR33-C) et les erreurs de conversion (ERR34-C). Évite system() comme le prescrit ENV33-C, car un interpréteur de commandes interprète son entrée.
Les tableaux de risque notent la gravité, la probabilité et le coût de correction de 1 à 3 ; une note de coût plus élevée signifie une correction moins coûteuse. Leur produit donne la priorité, de 1 à 27. Les niveaux regroupent les priorités en L1 (12–27), L2 (6–9) et L3 (1–4). Cela aide à ordonner les corrections sans déclarer inoffensifs les défauts de priorité inférieure.
Utilise les règles pendant la revue de code et exploite les résultats des analyseurs associés à leurs identifiants. MISRA C est un autre référentiel utilisé pour les logiciels embarqués critiques pour la sécurité. Les règles aident à prévenir les défauts, tandis que les avertissements, les tests et les sanitizers vérifient si l’implémentation les respecte effectivement. Mémoriser les identifiants ne suffit pas à détecter un caractère nul de fin manquant ou une branche de gestion d’erreur ignorée.
Points clés
- Lis ensemble le mécanisme de défaillance et la solution conforme.
- Les identifiants CERT distinguent les catégories, les règles et les recommandations.
- Ne place pas de données extérieures dans les chaînes de format.
- Applique des vérifications concrètes aux bornes, à la durée de vie, aux calculs et aux erreurs.
- Utilise les évaluations du risque pour ordonner le travail, puis vérifie le code avec des outils.
5.2 Manipuler les chaînes et les tampons en toute sécurité
Pour manipuler les chaînes en toute sécurité, il faut une source valide, une capacité de destination connue et une décision sur le texte qui ne tient pas. Une fonction limitée par la taille empêche certains écrasements, mais tu dois quand même vérifier si toute l’opération a réussi.
Transmettre la capacité avec le pointeur
sizeof mesure le type de son opérande. Pour un tableau local char buf[20], il donne 20 octets. Dans void fill(char buf[]), le paramètre est ajusté en pointeur ; sizeof buf mesure ce pointeur, pas le tableau de la fonction appelante. Passe un paramètre de capacité, par exemple avec fill(buf, sizeof buf), tant que la fonction appelante dispose encore du type tableau.
Une chaîne de cinq caractères nécessite six octets en comptant '\0'. La capacité compte chaque octet accessible en écriture ; la longueur de la chaîne exclut le caractère nul de fin. C’est pourquoi malloc(strlen(src)) alloue un octet de trop peu pour une copie.
Lire une ligne dans les limites du tampon
Utilise fgets(buf, sizeof buf, stdin) et vérifie sa valeur de retour avant d’utiliser le contenu. La fonction stocke au plus un caractère de moins que la capacité, puis un caractère nul de fin. Elle conserve le saut de ligne si celui-ci tient dans le tampon. Après une lecture réussie, buf[strcspn(buf, "\n")] = '\0'; supprime un saut de ligne stocké.
Si une ligne longue remplit le tampon, les caractères non lus restent pour l’appel suivant. Décide si tu dois rejeter l’entrée et lire le reste pour l’évacuer, poursuivre la lecture ou accepter la troncature. Supprimer un saut de ligne ne prouve pas que tu as lu la ligne entière. gets ne permet pas d’indiquer une capacité et a été retiré en C11 ; ne l’utilise jamais.
Pour un seul mot dans char name[20], %19s dans scanf limite les caractères stockés et laisse de la place pour le caractère nul de fin. Vérifie que le nombre attendu de conversions a réussi. Pour du texte numérique non fiable, préfère une ligne complète suivie d’un strtol dont le résultat est vérifié ; %d a un comportement indéfini lorsque la valeur convertie n’est pas représentable, même si tu inspectes le nombre renvoyé par scanf.
Détecter une copie raccourcie
strcpy et strcat ne connaissent pas la capacité de la destination. strncpy peut laisser le résultat sans caractère nul de fin. Avec une capacité positive, snprintf termine le texte stocké par un caractère nul et indique le nombre de caractères nécessaires à la sortie complète :
char dst[8];
int r = snprintf(dst, sizeof dst, "%s", "debugging");
if (r < 0) {
/* report encoding failure */
} else if ((size_t)r >= sizeof dst) {
/* report truncation */
}
La chaîne stockée est « debuggi » et r vaut 9. La sortie complète nécessite dix octets en comptant son caractère nul de fin. Vérifie que le résultat n’est pas négatif avant de le convertir en size_t. Pour que le résultat tienne, r doit être inférieur à la capacité.
Adapter l’opération à la zone mémoire
Utilise memmove lorsque la source et la destination se chevauchent ; memcpy avec des zones qui se chevauchent est indéfini. strlcpy est disponible sur les systèmes BSD, macOS et glibc à partir de la version 2.38, mais ne fait pas partie d’ISO C. Les fonctions de l’annexe K sont facultatives et absentes de nombreuses bibliothèques.
Ne place pas de données utilisateur dans le format lui-même. Fais correspondre chaque conversion à son argument : %d pour int, %ld pour long, %zu pour size_t, %f pour double et %s pour une chaîne terminée par un caractère nul. %p exige un void * : passe donc une conversion appropriée du pointeur. Une limite de largeur ne peut pas corriger un type d’argument incompatible.
Points clés
- Passe la capacité de destination avec le pointeur.
- Réserve un octet pour le caractère nul de fin de chaîne.
- Traite explicitement les lignes d’entrée incomplètes.
- Vérifie si
snprintfa échoué ou tronqué le résultat. - Utilise
memmovepour les plages mémoire qui se chevauchent. - Fais correspondre chaque conversion de format au type de son argument.
5.3 Valider les données d’entrée et gérer les erreurs
Des données extérieures peuvent être syntaxiquement valides et rester dangereuses pour l’opération envisagée. Valide leur longueur, leur format complet et leur plage de valeurs utile avant de les utiliser comme indice, taille d’allocation ou nombre d’itérations.
Analyser le nombre entier
atoi ne fournit aucun moyen fiable de distinguer un zéro valide d’un texte invalide, et un résultat non représentable entraîne un comportement indéfini. strtol fournit un pointeur de fin de conversion et signale les erreurs de plage. Après avoir lu une ligne complète et supprimé son saut de ligne, un analyseur décimal strict peut utiliser :
errno = 0;
char *end;
long v = strtol(text, &end, 10);
if (end == text || *end != '\0' || errno == ERANGE) {
/* reject the numeric text */
} else if (v < 0 || v > 150) {
/* reject this application's range */
} else {
int age = (int)v;
/* use age */
}
Inclus <errno.h> et <stdlib.h>. Le premier test distingue trois échecs : absence de chiffres, texte final et valeur hors de la plage de long. Le test de la plage autorisée par l’application intervient avant la conversion rétrécissante vers int. strtol accepte les espaces initiaux et un signe facultatif ; décide si la grammaire de l’application les autorise.
Pour « 42abc », la conversion peut renvoyer 42, mais end pointe vers les lettres restantes. Pour « abc », aucun chiffre n’est consommé. Pour un entier décimal trop grand, errno prend la valeur ERANGE. Un nombre non nul renvoyé ne prouve à lui seul ni que la conversion est complète ni que la valeur est dans une plage sûre.
Préserver la propriété en cas d’échec
Chaque opération susceptible d’échouer nécessite une réponse. Vérifie les résultats des allocations, de fopen et de fgets, le nombre d’affectations réussies et l’achèvement des sorties. Un retour NULL de fgets peut indiquer une fin d’entrée ou une erreur de lecture. fclose peut signaler l’échec d’une écriture en tampon. Utilise les informations d’erreur adaptées à la fonction ; de nombreux appels de bibliothèque définissent errno, que perror ou strerror peut transformer en diagnostic lisible.
Pour un redimensionnement vers une taille positive, protège le calcul de taille et l’allocation initiale :
if (new_n == 0) {
free(a);
a = NULL;
} else if (new_n > SIZE_MAX / sizeof *a) {
/* reject the size */
} else {
int *tmp = realloc(a, new_n * sizeof *a);
if (tmp == NULL) {
/* handle failure; a still owns its old block */
} else {
a = tmp;
}
}
Affecter le résultat de realloc directement au seul pointeur responsable de l’allocation fait perdre l’adresse de l’ancien bloc en cas d’échec. Traite zéro séparément : en C17, le comportement de realloc avec une taille nulle est défini par l’implémentation, tandis qu’en C23 il est indéfini. Après un redimensionnement réussi, utilise le pointeur renvoyé et abandonne les alias devenus invalides.
Prévoir un chemin de gestion d’erreur clair
Une section unique de nettoyage peut libérer les ressources acquises dans l’ordre inverse. Initialise à NULL les pointeurs responsables d’une allocation, ne ferme que les fichiers ouverts et renvoie un code d’échec après avoir signalé la cause. Un goto cleanup peut rendre ces chemins plus faciles à examiner.
Évite system() pour les tâches impliquant du texte extérieur : l’interpréteur de commandes peut interpréter ce texte comme des commandes. Préfère une opération de bibliothèque ou une interface adaptée au lancement d’un programme avec des arguments transmis séparément. Considère les arguments de ligne de commande, les valeurs d’environnement, le contenu des fichiers et les données réseau comme des entrées qui exigent autant de précautions que le texte saisi.
Points clés
- Vérifie que la conversion est complète et que le nombre est dans la plage autorisée par l’application avant de l’utiliser.
- Remets
errnoà zéro avantstrtolet inspecte son pointeur de fin de conversion. - Vérifie les échecs des fonctions de bibliothèque et préserve les ressources sur chaque chemin.
- Utilise un pointeur temporaire pour
reallocavec une taille positive. - Traite les tailles nulles et les débordements de multiplication avant le redimensionnement.
- Ne libère que les ressources effectivement acquises.
- Ne place pas de texte extérieur dans les chaînes de commandes de l’interpréteur.
5.4 Évaluer les compromis en matière de sécurité mémoire
La sécurité mémoire signifie que les accès restent dans les zones mémoire qu’un programme est autorisé à utiliser. C donne un contrôle direct de la mémoire et laisse au programmeur les vérifications des bornes et des durées de vie.
Distinguer les bornes de la durée de vie
La sécurité spatiale maintient un accès dans les bornes de son objet. Écrire l’élément d’indice dix d’un tableau de dix éléments la viole. La sécurité temporelle maintient l’accès dans la durée de vie de l’objet. Lire une allocation après free, la libérer deux fois ou utiliser l’adresse renvoyée d’une variable locale viole les règles de durée de vie.
Les conséquences peuvent aller au-delà d’un plantage. Une lecture peut exposer des données confidentielles. Une écriture peut corrompre un fichier, modifier des données de contrôle ou créer une vulnérabilité de sécurité lorsque des données extérieures contrôlent l’opération. Un programme qui continue après une corruption peut produire des résultats convaincants mais faux.
Cette responsabilité concerne aussi les équipes expérimentées. Microsoft a indiqué en 2019 que, chaque année pendant plus d’une décennie, environ 70 % des vulnérabilités auxquelles l’entreprise avait attribué un identifiant CVE concernaient la sécurité mémoire. En décembre 2023, la CISA et des agences partenaires ont appelé les éditeurs de logiciels à prévoir des transitions vers des langages à sécurité mémoire. Ce sont des observations historiques précises, pas un pourcentage mesuré pour chaque programme C.
Comprendre les raisons de choisir C
C offre un contrôle de la disposition et de l’allocation de la mémoire ainsi que de l’accès au matériel, sans imposer de ramasse-miettes. Son environnement d’exécution réduit convient aux micrologiciels soumis à de fortes contraintes. Il bénéficie d’une large prise en charge par les compilateurs, d’importants ensembles de code existant et d’interfaces que de nombreux autres langages peuvent appeler. Ces avantages expliquent son utilisation dans les noyaux de systèmes d’exploitation, les logiciels embarqués, les environnements d’exécution et les bibliothèques.
Ils ne rendent pas tous les programmes C rapides de façon prévisible : les algorithmes, les allocations, les caches et les appels système comptent toujours. L’avantage pertinent est le contrôle des coûts et des besoins de l’environnement d’exécution.
| Approche | Protection de la mémoire | Coût ou responsabilité |
|---|---|---|
| C | Bornes et durées de vie garanties par le programmeur | Raisonnement, vérifications et tests manuels |
| Rust sûr | Propriété, emprunt et vérifications des bornes | Apprentissage des règles de propriété |
| Go, Java, C#, Python | Durées de vie gérées et vérifications des bornes | Surcoûts d’exécution et de mémoire variables |
| Swift | Comptage de références et accès vérifiés | Coûts de propriété et d’exécution variables |
Les opérations non sûres et les interfaces avec C exigent une attention particulière dans les systèmes par ailleurs à sécurité mémoire. La sécurité mémoire laisse aussi à résoudre les erreurs de logique et les injections liées aux données d’entrée.
Choisir plusieurs niveaux de protection pour le risque restant
Préviens les défauts avec des règles de programmation, des avertissements et des revues de code. Détecte-les avec l’analyse statique, les tests aux limites, les sanitizers et le fuzzing. Limite les dommages restants avec des protections de pile, les vérifications _FORTIFY_SOURCE prises en charge, la randomisation de l’espace d’adressage (ASLR) et la mémoire non exécutable. Une mesure d’atténuation peut arrêter une attaque ou transformer une corruption en plantage tout en laissant le défaut dans le code source.
Les coûts guident le déploiement. ASan double généralement le temps d’exécution et augmente l’utilisation de mémoire ; Valgrind ralentit couramment l’exécution de 20 à 30 fois. Ils conviennent davantage aux tests qu’à une utilisation ordinaire en production. Le durcissement moins coûteux reste couramment actif dans les versions de production.
Pour un nouvel analyseur de fichiers provenant d’Internet, sans contrainte imposant C, un langage à sécurité mémoire réduit un risque majeur. Pour un micrologiciel C existant sur un microcontrôleur doté de 32 Ko de mémoire, la prise en charge du langage et les limites de ressources peuvent favoriser C avec des vérifications rigoureuses. Fonde ton choix sur les contraintes, l’exposition aux données d’entrée et la facilité de maintenance ; le remplacement progressif d’un analyseur risqué peut aussi être une solution pratique.
Points clés
- La sécurité spatiale concerne les bornes ; la sécurité temporelle concerne les durées de vie.
- Les défauts de mémoire peuvent exposer des données et corrompre des résultats.
- C offre un contrôle direct et une large compatibilité, mais laisse la sécurité à la charge du programmeur.
- Les langages à sécurité mémoire éliminent de nombreux défauts de mémoire, mais d’autres catégories de bogues subsistent.
- Combine prévention, détection et mesures d’atténuation en production.
- Choisis un langage en fonction des contraintes réelles de ressources et d’intégration.
Débogage en C : exemples
1Compiler un programme que tu peux inspecter
9 étapes
- 1IntroductionCommencer avant la première exécution
Un programme peut se compiler tout en contenant un défaut que le compilateur sait signaler. Ta première tâche est de demander ces retours sous forme d’avertissements et de les interpréter.
Tu vas examiner une affectation accidentelle et une incompatibilité de format, puis choisir des paramètres pour une session de débogage (une version de débogage) et une version de production. Ces choix préparent les outils utilisés ensuite à montrer des éléments utiles plutôt que des adresses inexpliquées ou des valeurs locales manquantes.
- 2ExplicationChoisir les avertissements
Commence par
-std=c17 -Wall -Wextra -Wpedantic. La première option fixe la version du langage ; les autres activent les avertissements courants, des avertissements supplémentaires et les diagnostics pour de nombreux écarts par rapport à cette norme.-Wallne signifie pas tous les avertissements. Ajoute-Wconversionpour les conversions implicites qui changent les valeurs et-Wformat=2pour des vérifications de format plus strictes.-Werrortransforme les avertissements activés en erreurs de compilation. - 3ExempleLire la condition
Le programmeur veut signaler une collection vide :
if (items = 0) { puts("No items"); }Un avertissement du compilateur indique une affectation dans une condition. Demande-toi d’abord ce que fait l’expression : elle stocke zéro dans
items, puis la condition teste ce zéro. Le corps ne s’exécute jamais. Remplace=par==pour tester le nombre existant ; ajouter des parenthèses autour de l’affectation conserverait l’opération incorrecte. - 4DéfiCorriger le type affiché
double ratio = 2.5;est affiché avecprintf("%d\n", ratio);. Que faut-il changer et pourquoi ?---reveal---
Utilise
printf("%f\n", ratio);.%dattend unint, tandis que%fattend undoubledansprintf. Un format incompatible produit un comportement indéfini. Une conversion explicite versintchangerait le nombre affiché en supprimant sa partie fractionnaire ; elle ne permettrait donc pas d’atteindre l’objectif d’afficher le rapport. - 5ConseilLire les messages dans l’ordre
Un diagnostic indique généralement l’emplacement dans le code source, le problème et l’option responsable. Lis aussi les notes associées ; elles peuvent indiquer la déclaration en conflit.
Corrige la première erreur du compilateur, puis recompile. Un point-virgule manquant peut perturber l’analyse qui suit et créer une cascade de messages. Une fois les erreurs corrigées, examine la cause de chaque avertissement. Une compilation sans avertissement nécessite toujours des tests pour les erreurs de logique et celles qui dépendent des entrées.
- 6ExplicationConserver un état utile
Pour une version de débogage simple, ajoute
-g -O0. Les informations de débogage relient l’exécution aux noms et aux lignes du code source ; une faible optimisation conserve davantage d’états intermédiaires disponibles pour l’inspection.-gne désactive pas l’optimisation. Avec-O2, le code peut toujours être débogué, mais les lignes peuvent sembler sauter et des variables peuvent être supprimées par l’optimisation.-Ogest un autre choix pour le développement, qui conserve une légère optimisation. - 7LienChoisir des vérifications adaptées à chaque tâche
L’analyse statique suit les chemins possibles du code source sans exécuter le programme. Un sanitizer vérifie les opérations sur les chemins effectivement exécutés par les tests. Utilise les deux pour étendre la couverture.
Le durcissement en production a un autre objectif : les protections de pile et les vérifications de fortification prises en charge peuvent détecter ou limiter certaines corruptions restantes. La fortification nécessite l’optimisation et une bibliothèque adaptée. Un écrasement détecté exige toujours une correction du code source.
- 8DéfiConserver la validation en production
Une version de débogage utilise des avertissements stricts et des assertions. Une version de production ajoute
-O2 -DNDEBUG. Le programmeur propose d’utiliserassert(valid_input)comme seule vérification des entrées. Que faut-il changer avant la mise en production ?---reveal---
Valide les entrées dans le déroulement ordinaire du programme et gère les échecs.
NDEBUGsupprime l’évaluation des assertions : la vérification proposée disparaît donc. Garde les avertissements activés et teste la configuration de production ainsi que la version de débogage ; des réglages différents peuvent révéler des défauts cachés. - 9RécapitulatifRelier les paramètres de compilation
Utilise les messages du compilateur pour repérer les opérations à corriger. Fais correspondre les formats aux types, examine les conversions et corrige les premières erreurs avant de suivre les messages qui en découlent.
Pour la prochaine session de débogage, compile le bon exécutable avec les informations source et un niveau d’optimisation adapté. Rends les configurations reproductibles, ajoute l’analyse et les tests avec sanitizers, et conserve les vérifications des entrées lorsque les paramètres de production suppriment les assertions.
2Retrouver l’origine d’un total incorrect et d’un plantage
12 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionTransformer un symptôme en question
- 2ExplicationConserver le cas qui échoue
- 3DéfiChoisir une expérience contrôlée
- 4ExempleSuspendre le programme dans le corps de la boucle
- 5ExempleSuivre les passages valides
- 6DéfiCorriger et vérifier le passage supplémentaire
- 7ConseilN’entrer que dans le code utile
- 8ExemplePasser à la fonction appelante
- 9ExplicationDétecter un changement au moment où il se produit
- 10DéfiExaminer un plantage dans l’allocateur
- 11LienUtiliser un état de plantage enregistré
- 12RécapitulatifChoisir la vue qui répond à la question
3Vérifier l’opération avant qu’elle ne devienne invalide
12 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionSe demander ce que C garantit
- 2ExplicationDistinguer une variation d’un défaut
- 3ExempleUn test de débordement trop tardif
- 4DéfiPlacer la vérification du pointeur
- 5ExempleProtéger une somme d’entiers signés
- 6Point cléUn rebouclage défini peut quand même fausser le programme
- 7DéfiÉlargir avant de multiplier
- 8ConseilVérifier plusieurs limites des expressions
- 9ExempleDistinguer un marqueur de fin d’un élément
- 10ExempleSuivre chaque alias après la libération
- 11DéfiCorriger une somme et sa durée de vie
- 12RécapitulatifVérifier le type, la plage, les bornes et la durée de vie
4Lire les vérifications comme des indices d’un défaut précis
11 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionVérifier plus que la sortie finale
- 2ExplicationVérifier un engagement interne par assertion
- 3DéfiConserver l’opération dans chaque version compilée
- 4ExplicationCompiler un exécutable instrumenté
- 5ExempleLire la zone indiquée dans le rapport
- 6DéfiCorriger la boucle correspondante
- 7Point cléL’absence de rapport a plusieurs explications
- 8ExempleFaire arrêter le test en cas d’erreur de calcul
- 9ExplicationChoisir les autres outils de vérification en connaissance de cause
- 10DéfiAméliorer une procédure de vérification incomplète
- 11RécapitulatifTransformer les rapports en vérifications reproductibles
5Rendre explicites les entrées, les tailles et la propriété
15 étapes
Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.
- 1IntroductionSuivre les données dans le programme
- 2ExplicationUtiliser la règle comme une question
- 3DéfiTraiter l’entrée comme une donnée
- 4ExempleTransmettre la capacité depuis la fonction appelante
- 5ExempleDistinguer un préfixe sûr d’une réussite
- 6DéfiTraiter une ligne incomplète
- 7ExempleInspecter la fin de conversion
- 8ExempleAppliquer la plage autorisée par le programme
- 9ExemplePréserver le pointeur responsable lors d’un redimensionnement
- 10DéfiPrévoir le chemin d’échec
- 11ExplicationDéfinir les limites de sécurité
- 12LienChoisir des niveaux de protection aux rôles différents
- 13DéfiChoisir un langage
- 14DéfiExaminer le parcours complet des données d’entrée
- 15RécapitulatifGarder chaque limite visible
Débogage en C : QCM
Série 1 : Indices du compilateur et choix dans le débogueur10 Questions Pratiques
Question 1 sur 10
-Wall sans avertissement convertit silencieusement un nombre de type double en int. Quelle option d’avertissement supplémentaire cible ce changement implicite de valeur ?Afficher la réponseMasquer la réponse
-Wconversion cible les conversions implicites qui peuvent modifier une valeur, notamment en supprimant une partie fractionnaire. -Wshadow concerne une déclaration masquée portant le même nom, pas un changement de type. Un avertissement doit t’amener à vérifier la valeur voulue avant d’ajouter une conversion explicite.Question 2 sur 10
double reading = 7.25; et appelle printf("%d\n", reading);. Quelle correction préserve cet objectif ?Afficher la réponseMasquer la réponse
printf attend un double pour %f : changer le format conserve donc la valeur fractionnaire et correspond à l’argument. Une conversion explicite en int pourrait satisfaire %d, mais supprimerait la partie fractionnaire que le programme doit afficher. L’incompatibilité initiale produit un comportement indéfini ; le format n’est pas une conversion numérique automatique.Question 3 sur 10
Afficher la réponseMasquer la réponse
-g ajoute les informations de débogage qui relient l’exécution aux emplacements source, aux types et aux noms. -Wextra active des diagnostics supplémentaires pendant la compilation, mais ne fournit pas ces correspondances. L’optimisation reste un choix distinct et peut toujours supprimer des valeurs même en présence d’informations de débogage.Question 4 sur 10
Afficher la réponseMasquer la réponse
Question 5 sur 10
Afficher la réponseMasquer la réponse
next exécute normalement la ligne source courante sans entrer dans ses appels de fonctions. step sert à entrer dans une fonction appelée lorsque des informations source utiles sont disponibles. Un point d’arrêt dans l’appel peut toujours interrompre next : la commande décrit donc la façon d’avancer, sans désactiver tous les arrêts.Question 6 sur 10
result = compute();. L’affichage de result montre sa valeur précédente et aucun appel n’a encore été exécuté à cet arrêt. Comment inspecter le résultat calculé ?Afficher la réponseMasquer la réponse
result contient encore sa valeur précédente. Exécuter la ligne avec next permet à l’appel et à l’affectation de se terminer avant l’inspection. Répéter un affichage sans reprendre l’exécution montre le même état et ne prouve pas que l’affectation a échoué.Question 7 sur 10
strlen et le cadre deux dans ta fonction render. Tu veux inspecter le pointeur caption de render dans GDB. Que dois-tu faire ?Afficher la réponseMasquer la réponse
frame 2 sélectionne render, où caption peut être inspecté. Rester dans le cadre zéro examine le contexte de la bibliothèque, et reprendre l’exécution est inutile lorsque l’élément recherché se trouve déjà dans le cadre d’une fonction appelante.Question 8 sur 10
Afficher la réponseMasquer la réponse
Question 9 sur 10
-O0 et -O2. Avec -O2, une variable intermédiaire est aussi indisponible dans le débogueur. Quelle recherche tient compte des deux observations ?Afficher la réponseMasquer la réponse
Question 10 sur 10
free après avoir lu de nombreux enregistrements. Agrandir un tampon de nom fait cesser le plantage visible, mais l’entrée sauvegardée amène toujours ASan à signaler une écriture au-delà de ce tampon. Quelle action traite la cause ?Afficher la réponseMasquer la réponse
Série 2 : Types, plages de valeurs et objets dont la durée de vie est en cours10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
merge(left(), right()). Les deux ordres d’évaluation sont autorisés et aucun état partagé ne crée de conflit. Comment cet ordre est-il classé en C ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
unsigned int est sur 32 bits. Quelle valeur unsigned int slots = 0u - 1u; stocke-t-il ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
unsigned int est sur 32 bits. Une entrée fournit shift = 32. Quel est le statut de 1u << shift ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
local après avoir déclaré char local[24] = "ready";. Pourquoi la lecture du résultat par la fonction appelante est-elle invalide ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
int a signé et un int b positif, quelle condition détecte un débordement de a + b sans calculer cette somme ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
int old = *p; avant if (p == NULL) return -1;. Un test avec un pointeur nul semble atteindre le retour d’erreur avec une faible optimisation, mais plante en production. Quelle correction définit un chemin valide pour un pointeur nul ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
int sur 32 bits et un long long sur 64 bits, les deux facteurs sont des int : units = 70000, rate = 40000. Quelle expression calcule correctement leur produit en long long ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
p ; alias pointe à l’intérieur de ce tampon. Un appel à realloc avec une taille positive réussit et le bloc est déplacé. La fonction affecte le résultat à p, écrit par alias, puis libère p et alias. Quelle correction de la propriété est nécessaire ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
-fsanitize=undefined. Une multiplication de valeurs size_t reboucle sans produire de rapport. Pourquoi cela peut-il se produire ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 3 : Assertions et rapports de sanitizers10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
assert(user_count > 0); comme seul moyen de rejeter les nombres invalides saisis par l’utilisateur. Les versions de production définissent NDEBUG. Que faut-il utiliser à la place de cette protection ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
-fsanitize=undefined restent silencieux, mais la valeur voisine et la taille d’allocation sont incorrectes. Quelle recherche traite les deux défauts ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
assert(++used < capacity); avant de stocker une valeur à l’indice used. Les tests réussissent, mais avec NDEBUG, elle écrase la même case à plusieurs reprises. Quelle modification du code source conserve la mise à jour voulue de l’état dans les deux versions ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
size_t, alloue un tampon dont la taille dépend de ce nombre, puis le remplit. Il ne vérifie ni si la conversion est complète ni la plage autorisée par l’application. Quel ordre de correction traite le parcours de l’entrée jusqu’à la mémoire ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 4 : Chaînes, entrées numériques et propriété des ressources10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
char line[48] à read_line(char line[]) sur un système dont les pointeurs occupent huit octets. Que mesure sizeof line dans la fonction ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
snprintf est appelé avec une capacité de destination de 11 et renvoie 11. Que peut-on en conclure ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
errno à zéro et convertit « 64kg » avec strtol. La conversion renvoie 64 et errno reste à zéro. Qu’est-ce qui doit entraîner le rejet ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
strtol, ne vérifie que errno, convertit le résultat en size_t, puis le multiplie par la taille d’un élément pour l’allocation. Quelles vérifications doivent précéder l’allocation ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
a. Après la lecture d’un nombre positif dans new_n, le code exécute a = realloc(a, new_n * sizeof *a); a[new_n - 1] = 0;. Tout nombre valide doit être traité et un échec du redimensionnement doit préserver l’ancien tableau. Quelle suite d’opérations faut-il suivre ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
fgets, sans saut de ligne stocké. L’application exige un enregistrement complet par ligne. Quel traitement évite de prendre un éventuel reste pour un nouvel enregistrement ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
strncpy(dst, src, sizeof dst), puis affiche dst avec %s. Suppose que les pointeurs occupent huit octets. Quelle nouvelle conception respecte l’exigence ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
size_t count dans un fichier et exécute immédiatement a = realloc(a, count * sizeof *a);. Les tableaux vides sont autorisés et une demande qui échoue doit préserver l’ancien tableau. Quelle nouvelle conception respecte ces exigences pour toutes les valeurs du nombre d’éléments ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Série 5 : Corriger des défauts qui interagissent10 Questions Pratiques
Enregistre cette unité pour voir chaque réponse et t'entraîner sur chaque série.
Question 1 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 2 sur 10
int i = 4;, un programmeur écrit printf("%d %d\n", i, ++i);. Quelle correction rend bien défini l’affichage voulu « ancienne valeur, puis nouvelle valeur » ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 3 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 4 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 5 sur 10
word, calcule int bytes = strlen(word);, alloue ce nombre d’octets, copie la chaîne complète, libère le nouveau bloc et renvoie son adresse. Les entrées peuvent être trop longues pour int. Quelle conception traite la taille et la durée de vie ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 6 sur 10
strtol et vérifie seulement errno != ERANGE. Il convertit ensuite explicitement le résultat en int pour un indice de tableau. Quelles vérifications manquantes peuvent encore laisser passer une entrée invalide ?Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 7 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 8 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 9 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Question 10 sur 10
Afficher la réponseMasquer la réponse
Enregistre cette unité pour voir cette réponse.
Tu bloques sur ton propre exercice ? Aide aux Devoirs
Mots-Clés
A
accès hors limites
AddressSanitizer
analyse statique
arithmétique vérifiée
ckd_add de C23 indique si une somme est représentable afin que le programme puisse gérer explicitement l’échec.assertion
assertion statique
static_assert C17 peut rejeter une plateforme où une hypothèse nécessaire sur la taille de int est fausse.avertissement du compilateur
B
bogue
C
cadre de pile
name, sélectionner son cadre après un plantage permet d’inspecter ce pointeur.capacité du tampon
caractère nul de fin de chaîne
cas minimal de reproduction
chaîne de format
printf utilise %zu pour size_t ; le texte utilisateur doit figurer dans un argument de données distinct.chemin de nettoyage
code non conforme
printf(input) permet à des données extérieures de fournir des conversions de format et enfreint FIO30-C.comportement défini par l’implémentation
char simple est signé ou non signé.comportement indéfini
comportement non spécifié
conversion implicite
double à un int supprime la partie fractionnaire lorsque la valeur tronquée est représentable.course aux données
D
débogueur
débordement d’entier signé
int sur 32 bits, ajouter un à INT_MAX ne peut pas produire une valeur suivante portable et définie.défaillance
free peut être la défaillance visible causée par une écriture bien antérieure dans l’exécution.défense en profondeur
diagnostic du compilateur
%d et désigner l’argument double qui ne lui correspond pas.données d’entrée non fiables
double libération
durcissement
durée de vie d’un objet
E
effets de bord non séquencés
i et ++i à un appel mêle une lecture et une modification sans ordre sûr.erreur de compilation
erreur de dépassement de plage
errno, une erreur de dépassement de plage de strtol lui affecte ERANGE même si une valeur limite est renvoyée.exécution pas à pas
next pour exécuter un appel sans y entrer et step lorsque tu dois entrer dans le code de la fonction.É
évaluation d’expression
count observe une valeur scalaire ; évaluer *p tente un accès par ce pointeur.F
fuzzing
H
hypothèse
I
indice de priorité du risque
informations de débogage
-g aide un débogueur à identifier la ligne source et les variables locales lors d’un arrêt ordinaire.injection de commandes
system() à partir d’un argument non vérifié permet à l’interpréteur de commandes d’interpréter la syntaxe fournie.instrumentation
invariant
L
langage à sécurité mémoire
longueur de chaîne
strlen("safe") vaut quatre, tandis que le stockage de la chaîne complète nécessite cinq octets.M
masquage de variable
count d’une boucle interne peut être modifié tandis que le count externe reste inchangé.mémoire fantôme
MemorySanitizer
O
optimisation
-O2, un débogueur peut ne pas afficher de valeur distincte pour une variable locale supprimée par le compilateur.P
pile d’appels
main.plages mémoire qui se chevauchent
memmove, car memcpy interdit le chevauchement.point d’arrêt
sum += a[i] te permet d’inspecter i sans évaluer l’accès au tableau.point d’arrêt conditionnel
i == 650 évite de parcourir manuellement les itérations précédentes de la boucle.point de surveillance
pointeur de fin de conversion
strtol indique le premier caractère non converti ou le début de l’entrée si aucun chiffre n’a été consommé.pointeur pendant
portabilité
INT_MAX évite de supposer que toutes les implémentations donnent à int la même valeur maximale.postcondition
précondition
promotion entière
int ou en unsigned int avant de nombreuses opérations arithmétiques et de décalage.propriété
realloc avec une taille positive échoue.Q
quarantaine
R
randomisation de l’espace d’adressage
rebouclage des entiers non signés
référentiel de programmation sécurisée
S
sécurité mémoire spatiale
sécurité mémoire temporelle
free viole la sécurité temporelle même si son adresse semble encore familière.SEI CERT C Coding Standard
solution conforme
%s maintient le texte fourni dans l’argument de données d’un appel à printf.T
test de non-régression
ThreadSanitizer
trace de pile
bt t’aide à trouver le premier cadre source appartenant à ton propre code.troncature
snprintf positive ou nulle et supérieure ou égale à la capacité signale que le texte complet ne tenait pas.U
UndefinedBehaviorSanitizer
utilisation après libération
V
valeur indéterminée
int sum; puis utiliser immédiatement sum += x; lit une valeur qui n’a jamais été initialisée.version de débogage
-g -O0 permet de suivre plus facilement une boucle incorrecte instruction source par instruction source.version de production
NDEBUG supprime ses assertions.vidage mémoire
Z
zone rouge
À propos de cette unité
Crée ton propre Matériel d'Étude
Transforme tes notes, diapos et documents en Guides d'Étude, Questions Pratiques, Flashcards et 6 autres outils.