Débogage, comportements indéfinis et programmation C sécurisée

Apprends à repérer et à prévenir les bogues qui font planter les programmes C, produisent des résultats erronés ou les rendent vulnérables. Active les avertissements du compilateur et comprends leur signification, retrouve pas à pas l’origine des plantages et des valeurs incorrectes dans gdb ou lldb, et reconnais les comportements indéfinis dans les calculs, les pointeurs et la mémoire. Vérifie tes hypothèses avec des assertions, détecte les erreurs de mémoire et les comportements indéfinis avec AddressSanitizer et UndefinedBehaviorSanitizer, et écris du code plus sûr en suivant des règles de programmation sécurisée pour les chaînes, les données d’entrée et les erreurs. Enfin, évalue le coût de l’absence de sécurité mémoire en C au regard du contrôle que ce langage te donne.

DébutantUnité 8 sur 8 de la formation Formation : Langage C : pointeurs, mémoire et structures

Ce que tu vas apprendre

  1. 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 compilateur
      Compile 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 production
      Utilise 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.
  2. 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èse
      Transforme 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 lldb
      Lance 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 inattendues
      Utilise 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.
  3. 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éfini
      Distingue 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 expressions
      Reconnais 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émoire
      Reconnais 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.
  4. 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’assertions
      Utilise 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 AddressSanitizer
      Compile 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érification
      Utilise UndefinedBehaviorSanitizer et découvre les autres sanitizers, puis combine avertissements, sanitizers et tests dans une procédure qui détecte tôt les erreurs.
  5. 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ée
      Explique 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 erreurs
      Considè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émoire
      Explique 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.

Débogage en C : cours

5 chapitres

1

Faire 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 :

OptionCe qu’elle aide à révéler
-WconversionLes conversions implicites qui peuvent changer une valeur
-WshadowUne déclaration locale qui masque une autre variable
-Wformat=2Des vérifications supplémentaires des chaînes de format
-WerrorTransforme 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.
  • -Wall n’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églageUsage habituelCe que tu peux observer
-O0Débogage simpleL’ordre du code source est généralement plus facile à suivre
-OgDébogage avec une légère optimisationCertaines transformations subsistent
-O2Version de production efficaceOpé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 -g pour obtenir des informations de débogage liées au code source.
  • Débogue avec -O0 ou -Og et teste aussi la version de production.
  • Garde les vérifications des entrées actives lorsque NDEBUG supprime 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.
2

Repé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
CommandeRôle dans GDB et LLDB
next ou nExécuter la ligne courante, généralement sans entrer dans les appels
step ou sEntrer dans une fonction appelée lorsque les informations source le permettent
finishExécuter jusqu’au retour de la fonction sélectionnée
continue ou cReprendre 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, finish et continue pour 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 bt pour 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.
3

Reconnaî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
TypeCe que le langage autoriseExemple en C17
IndéfiniAucun comportement imposéDébordement d’entier signé
Défini par l’implémentationChoix documenté de l’implémentationLe 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.
4

Dé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 :

TypeCe qu’il énonceExemple
PréconditionExigence à l’entréeUn tableau d’entrée non vide
PostconditionGarantie après l’opérationUn tableau est trié
InvariantPropriété maintenue entre les étapesUn 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.
  • NDEBUG dé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 :

  1. Identifie la catégorie d’erreur et détermine si l’accès était une lecture ou une écriture.
  2. Trouve le premier cadre situé dans ton code dans la trace de l’accès.
  3. Compare l’accès à la zone allouée et à la taille de ses éléments.
  4. 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=address et 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
OutilRôle principalLimite importante
UBSanNombreuses opérations indéfinies liées aux calculs, aux décalages, aux pointeurs nuls, à l’alignement et aux tableauxNe détecte pas tous les types de comportements indéfinis
ASanNombreuses erreurs de limites et de durée de vieNe suit pas les valeurs non initialisées ordinaires
MemorySanitizerUtilisations de mémoire non initialiséeNécessite des dépendances instrumentées pour obtenir des résultats fiables
ThreadSanitizerCourses aux donnéesNé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 snprintf a échoué ou tronqué le résultat.
  • Utilise memmove pour 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 avant strtol et 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 realloc avec 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.

ApprocheProtection de la mémoireCoût ou responsabilité
CBornes et durées de vie garanties par le programmeurRaisonnement, vérifications et tests manuels
Rust sûrPropriété, emprunt et vérifications des bornesApprentissage des règles de propriété
Go, Java, C#, PythonDurées de vie gérées et vérifications des bornesSurcoûts d’exécution et de mémoire variables
SwiftComptage de références et accès vérifiésCoû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

5 Leçons Interactives

1

Compiler un programme que tu peux inspecter

9 étapes
Lis les retours du compilateur, corrige le code qu’ils désignent et choisis des paramètres de compilation distincts pour le débogage et la production.
  1. 1
    IntroductionCommencer 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.

  2. 2
    ExplicationChoisir 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.

    -Wall ne signifie pas tous les avertissements. Ajoute -Wconversion pour les conversions implicites qui changent les valeurs et -Wformat=2 pour des vérifications de format plus strictes. -Werror transforme les avertissements activés en erreurs de compilation.

  3. 3
    ExempleLire 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.

  4. 4
    DéfiCorriger le type affiché

    double ratio = 2.5; est affiché avec printf("%d\n", ratio);. Que faut-il changer et pourquoi ?

    ---reveal---

    Utilise printf("%f\n", ratio);. %d attend un int, tandis que %f attend un double dans printf. Un format incompatible produit un comportement indéfini. Une conversion explicite vers int changerait le nombre affiché en supprimant sa partie fractionnaire ; elle ne permettrait donc pas d’atteindre l’objectif d’afficher le rapport.

  5. 5
    ConseilLire 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.

  6. 6
    ExplicationConserver 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.

    -g ne 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. -Og est un autre choix pour le développement, qui conserve une légère optimisation.

  7. 7
    LienChoisir 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.

  8. 8
    Dé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’utiliser assert(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. NDEBUG supprime 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.

  9. 9
    Ré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.

2

Retrouver l’origine d’un total incorrect et d’un plantage

12 étapes
Passe de la reproduction à l’exécution pas à pas, aux traces de pile et aux arrêts ciblés qui révèlent l’origine d’une valeur incorrecte.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionTransformer un symptôme en question
  2. 2
    ExplicationConserver le cas qui échoue
  3. 3
    DéfiChoisir une expérience contrôlée
  4. 4
    ExempleSuspendre le programme dans le corps de la boucle
  5. 5
    ExempleSuivre les passages valides
  6. 6
    DéfiCorriger et vérifier le passage supplémentaire
  7. 7
    ConseilN’entrer que dans le code utile
  8. 8
    ExemplePasser à la fonction appelante
  9. 9
    ExplicationDétecter un changement au moment où il se produit
  10. 10
    DéfiExaminer un plantage dans l’allocateur
  11. 11
    LienUtiliser un état de plantage enregistré
  12. 12
    RécapitulatifChoisir la vue qui répond à la question
3

Vérifier l’opération avant qu’elle ne devienne invalide

12 étapes
Raisonne sur l’optimisation, les limites des calculs et les durées de vie des objets sans te fier à une sortie accidentelle.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionSe demander ce que C garantit
  2. 2
    ExplicationDistinguer une variation d’un défaut
  3. 3
    ExempleUn test de débordement trop tardif
  4. 4
    DéfiPlacer la vérification du pointeur
  5. 5
    ExempleProtéger une somme d’entiers signés
  6. 6
    Point cléUn rebouclage défini peut quand même fausser le programme
  7. 7
    DéfiÉlargir avant de multiplier
  8. 8
    ConseilVérifier plusieurs limites des expressions
  9. 9
    ExempleDistinguer un marqueur de fin d’un élément
  10. 10
    ExempleSuivre chaque alias après la libération
  11. 11
    DéfiCorriger une somme et sa durée de vie
  12. 12
    RécapitulatifVérifier le type, la plage, les bornes et la durée de vie
4

Lire les vérifications comme des indices d’un défaut précis

11 étapes
Choisis des vérifications par assertion, ASan et UBSan, puis relie chaque rapport à une correction durable.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionVérifier plus que la sortie finale
  2. 2
    ExplicationVérifier un engagement interne par assertion
  3. 3
    DéfiConserver l’opération dans chaque version compilée
  4. 4
    ExplicationCompiler un exécutable instrumenté
  5. 5
    ExempleLire la zone indiquée dans le rapport
  6. 6
    DéfiCorriger la boucle correspondante
  7. 7
    Point cléL’absence de rapport a plusieurs explications
  8. 8
    ExempleFaire arrêter le test en cas d’erreur de calcul
  9. 9
    ExplicationChoisir les autres outils de vérification en connaissance de cause
  10. 10
    DéfiAméliorer une procédure de vérification incomplète
  11. 11
    RécapitulatifTransformer les rapports en vérifications reproductibles
5

Rendre explicites les entrées, les tailles et la propriété

15 étapes
Applique des règles de programmation à un court parcours de traitement des entrées, puis évalue les coûts de sécurité liés au choix de C.

Enregistre cette unité pour suivre chaque Leçon Interactive pas à pas.

  1. 1
    IntroductionSuivre les données dans le programme
  2. 2
    ExplicationUtiliser la règle comme une question
  3. 3
    DéfiTraiter l’entrée comme une donnée
  4. 4
    ExempleTransmettre la capacité depuis la fonction appelante
  5. 5
    ExempleDistinguer un préfixe sûr d’une réussite
  6. 6
    DéfiTraiter une ligne incomplète
  7. 7
    ExempleInspecter la fin de conversion
  8. 8
    ExempleAppliquer la plage autorisée par le programme
  9. 9
    ExemplePréserver le pointeur responsable lors d’un redimensionnement
  10. 10
    DéfiPrévoir le chemin d’échec
  11. 11
    ExplicationDéfinir les limites de sécurité
  12. 12
    LienChoisir des niveaux de protection aux rôles différents
  13. 13
    DéfiChoisir un langage
  14. 14
    DéfiExaminer le parcours complet des données d’entrée
  15. 15
    RécapitulatifGarder chaque limite visible

Débogage en C : QCM

50 Questions Pratiques en 5 séries

Série 1 : Indices du compilateur et choix dans le débogueur10 Questions Pratiques

Question 1 sur 10

Une compilation avec -Wall sans avertissement convertit silencieusement un nombre de type double en int. Quelle option d’avertissement supplémentaire cible ce changement implicite de valeur ?
  1. -Wshadow
  2. -Wformat=2
  3. -Wconversion
  4. -Wpedantic
Afficher la réponseMasquer la réponse
Réponse : C
-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

Un programme doit afficher une mesure comportant une partie fractionnaire. Il déclare double reading = 7.25; et appelle printf("%d\n", reading);. Quelle correction préserve cet objectif ?
  1. Utiliser %f avec reading.
  2. Convertir reading en int.
  3. Utiliser %ld avec reading.
  4. Convertir reading en long.
Afficher la réponseMasquer la réponse
Réponse : A
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

Un débogueur affiche les adresses des fonctions, mais ne peut pas afficher les lignes source ni les noms des variables locales. Quelle option du compilateur fournit les informations source manquantes ?
  1. -Wextra
  2. -DNDEBUG
  3. -Werror
  4. -g
Afficher la réponseMasquer la réponse
Réponse : D
-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

Une entrée de 240 enregistrements provoque un plantage, mais chaque moitié réussit lorsqu’elle est exécutée seule. Quelle est la stratégie de réduction la plus utile pour la suite ?
  1. Ne garder que le dernier enregistrement et le tester seul.
  2. Retirer de plus petits morceaux en conservant la suite qui échoue.
  3. Doubler chaque tampon et abandonner l’entrée initiale.
  4. Relancer une moitié qui réussit jusqu’au retour du plantage.
Afficher la réponseMasquer la réponse
Réponse : B
La défaillance peut dépendre de plusieurs enregistrements ensemble ou d’un état accumulé : conserve donc une suite qui échoue tout en retirant de plus petits morceaux. Choisir un seul enregistrement parce qu’il est le dernier suppose une indépendance que la réussite des deux moitiés ne démontre pas. Chaque réduction doit être vérifiée par rapport à la défaillance réelle.

Question 5 sur 10

L’exécution est suspendue sur un appel que tu veux traiter comme une seule étape du code source. Quelle commande du débogueur exécute normalement cet appel sans y entrer ?
  1. step
  2. next
  3. print
  4. backtrace
Afficher la réponseMasquer la réponse
Réponse : B
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

Un point d’arrêt se trouve sur 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é ?
  1. Saisir à nouveau print result sans reprendre l’exécution.
  2. Supprimer le point d’arrêt et modifier l’initialiseur.
  3. Passer à la fonction appelante sans exécuter l’appel.
  4. Utiliser next, puis inspecter la nouvelle valeur de result.
Afficher la réponseMasquer la réponse
Réponse : D
L’arrêt précède l’affectation, donc 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

Une trace de pile montre le cadre zéro dans strlen et le cadre deux dans ta fonction render. Tu veux inspecter le pointeur caption de render dans GDB. Que dois-tu faire ?
  1. Exécuter frame 2, puis p caption.
  2. Exécuter frame 0, puis p caption.
  3. Exécuter next, puis p caption.
  4. Exécuter continue, puis p caption.
Afficher la réponseMasquer la réponse
Réponse : A
La sélection d’un cadre rend disponible le contexte local de la fonction choisie sans exécuter davantage de code. 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

Un nombre devient incorrect entre deux observations connues, l’une correcte et l’autre incorrecte, mais l’emplacement de l’écriture est inconnu. Quelle fonctionnalité du débogueur permet le mieux de localiser cette écriture ?
  1. Un point d’arrêt au dernier appel de printf.
  2. Une trace de pile après la fin normale.
  3. Un point de surveillance sur la variable contenant le nombre.
  4. Un affichage du code source autour de sa déclaration.
Afficher la réponseMasquer la réponse
Réponse : C
Un point de surveillance arrête le programme lorsque la zone mémoire surveillée change, révélant l’opération responsable ainsi que les anciennes et nouvelles valeurs. Un point d’arrêt au dernier affichage ne montre que l’état déjà incorrect. Compare chaque écriture sur laquelle le programme s’arrête à la règle prévue pour le nombre, car certaines modifications de cette variable peuvent être légitimes.

Question 9 sur 10

Un programme sans avertissement produit des totaux différents avec -O0 et -O2. Avec -O2, une variable intermédiaire est aussi indisponible dans le débogueur. Quelle recherche tient compte des deux observations ?
  1. Supprimer définitivement l’optimisation et accepter le total de débogage initial.
  2. Conserver -O2 et considérer la variable absente comme une corruption.
  3. Désactiver les symboles de débogage et comparer les adresses affichées.
  4. Rechercher les comportements indéfinis avec des sanitizers et utiliser -g -O0 pour avancer pas à pas.
Afficher la réponseMasquer la réponse
Réponse : D
L’optimisation peut légitimement supprimer des valeurs intermédiaires, tandis que des totaux différents donnent une raison de rechercher des opérations indéfinies dans le code. Une version compilée avec des sanitizers aide à trouver ces opérations et une version de débogage peu optimisée facilite l’inspection. Accepter le seul résultat de débogage laisse inexpliquée la différence avec la version de production.

Question 10 sur 10

Un analyseur plante dans 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 ?
  1. Garder le tampon agrandi et désactiver ASan pour ce test.
  2. Retrouver et corriger la borne de l’écriture, puis conserver l’entrée.
  3. Remplacer l’appel à l’allocateur par une assertion et garder le tampon agrandi.
  4. Intercepter le plantage de l’allocateur et poursuivre avec les enregistrements.
Afficher la réponseMasquer la réponse
Réponse : B
Le rapport ASan indique une écriture invalide qui subsiste malgré le changement de symptôme. Corrige l’indice ou la borne de copie, relance l’entrée initiale et conserve-la comme test de non-régression. Agrandir la zone mémoire voisine peut masquer la corruption lors d’une vérification ultérieure de l’allocateur sans rendre l’accès initial valide.
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

Deux appels indépendants sont passés comme arguments dans 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 ?
  1. Un comportement indéfini.
  2. Un comportement non spécifié.
  3. Un comportement défini par l’implémentation.
  4. Un ordre imposé de gauche à droite.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Suppose qu’un unsigned int est sur 32 bits. Quelle valeur unsigned int slots = 0u - 1u; stocke-t-il ?
  1. Zéro, car les valeurs non signées ne peuvent pas être négatives.
  2. Moins un, pour conserver le résultat mathématique.
  3. Aucune valeur définie, car la soustraction déborde.
  4. 4 294 967 295, par rebouclage non signé défini.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Suppose qu’un unsigned int est sur 32 bits. Une entrée fournit shift = 32. Quel est le statut de 1u << shift ?
  1. Indéfini : le nombre de positions atteint la largeur du type.
  2. Zéro défini : tous les bits à un dépassent la largeur du résultat.
  3. Un défini : le nombre de positions reboucle modulo la largeur.
  4. Défini par l’implémentation : les décalages non signés varient.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un tableau comporte sept éléments. Quel est le plus grand indice valide d’un élément ?
  1. 7
  2. 8
  3. 6
  4. 5
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Une fonction renvoie local après avoir déclaré char local[24] = "ready";. Pourquoi la lecture du résultat par la fonction appelante est-elle invalide ?
  1. Le tableau a trop de place inutilisée pour une chaîne.
  2. Le retour transforme le tableau en littéral de chaîne.
  3. La durée de vie du tableau local a pris fin au retour de la fonction.
  4. La fonction appelante doit appliquer sizeof avant toute lecture de chaîne.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Pour un int a signé et un int b positif, quelle condition détecte un débordement de a + b sans calculer cette somme ?
  1. a > INT_MAX - b
  2. a + b > INT_MAX
  3. a > INT_MAX + b
  4. a - b > INT_MAX
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Une fonction auxiliaire exécute 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 ?
  1. Garder l’ordre initial et déclarer volatile la variable locale chargée.
  2. Garder l’ordre initial et compiler sans -Werror.
  3. Afficher p entre la lecture et la vérification.
  4. Déplacer le test de nullité avant le premier déréférencement.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Sur une plateforme avec un 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 ?
  1. (long long)(units * rate)
  2. (long long)units * rate
  3. (int)((long long)units * rate)
  4. (long long)(int)(units * rate)
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Une fonction C détient un tampon du tas par l’intermédiaire de 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 ?
  1. Abandonner l’ancien alias, utiliser l’allocation renvoyée et la libérer une fois.
  2. Garder l’ancien alias, agrandir le nouveau bloc et libérer les deux pointeurs.
  3. Vérifier que l’alias est non nul, écrire par son intermédiaire et le libérer seul.
  4. Utiliser l’allocation renvoyée, puis libérer l’alias avant le pointeur responsable.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Une version de vérification utilise uniquement -fsanitize=undefined. Une multiplication de valeurs size_t reboucle sans produire de rapport. Pourquoi cela peut-il se produire ?
  1. La vérification ne s’exécute que lorsque NDEBUG est défini.
  2. Le rebouclage est masqué parce que size_t est un pointeur.
  3. Le rebouclage non signé est défini et exclu de ce groupe.
  4. UBSan ne vérifie que les écritures dans les tableaux de la pile et du tas.
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

Une fonction auxiliaire privée de liste garantit que son nombre stocké est toujours égal au nombre de nœuds après chaque mise à jour. De quel type de condition de programmation s’agit-il ?
  1. Un invariant.
  2. Un format d’entrée.
  3. Un résultat d’allocation.
  4. Une option de compilation.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Une application utilise 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 ?
  1. Une assertion statique sur la même variable d’exécution.
  2. Une assertion supplémentaire après la réussite de l’allocation.
  3. Une conversion explicite de user_count vers un type non signé.
  4. Une vérification ordinaire des bornes avec un chemin d’erreur.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

ASan signale une écriture de quatre octets « 0 bytes after 28-byte region ». Suppose que chaque élément du tableau occupe quatre octets. Quel indice dont l’accès a été tenté commence à cette adresse ?
  1. L’indice 6.
  2. L’indice 7.
  3. L’indice 8.
  4. L’indice 28.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un test doit repérer les utilisations ordinaires de valeurs non initialisées. Pourquoi une version compilée avec ASan seul est-elle insuffisante ?
  1. ASan ne suit ces utilisations que dans le tas.
  2. ASan ne suit ces utilisations que dans les variables globales.
  3. ASan ne suit pas ces utilisations de valeurs.
  4. ASan a besoin du mode de reprise pour les suivre.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un test UBSan automatisé consigne un débordement signé, mais continue et renvoie un succès. Quelle option fait arrêter l’exécution lorsqu’un comportement indéfini est détecté ?
  1. -fno-omit-frame-pointer : conserver les cadres de pile.
  2. -Wconversion : signaler les changements implicites de valeur.
  3. -fno-sanitize-recover=undefined
  4. -D_FORTIFY_SOURCE=2 : vérifier les copies prises en charge.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Un test écrit au-delà du membre tableau d’une structure jusque dans le membre suivant et provoque aussi le rebouclage d’un produit non signé calculant un nombre d’octets. ASan et -fsanitize=undefined restent silencieux, mais la valeur voisine et la taille d’allocation sont incorrectes. Quelle recherche traite les deux défauts ?
  1. Inspecter les bornes du membre et vérifier le produit non signé avant utilisation.
  2. Accepter les bornes du membre et inspecter seulement les calculs signés du total.
  3. Agrandir la structure et accepter la taille d’allocation rebouclée comme vérifiée.
  4. Garder l’indice du membre et transformer chaque nombre stocké en assertion.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Une équipe veut examiner à la fois les débordements du tas et les utilisations de valeurs non initialisées. Sa version ASan actuelle est liée à des bibliothèques tierces compilées normalement. Quelle organisation des vérifications répond aux exigences des deux outils ?
  1. Combiner les instrumentations ASan et MSan en gardant toutes les bibliothèques tierces inchangées.
  2. Utiliser UBSan à la place des deux et garder les bibliothèques inchangées.
  3. Exécuter l’exécutable ASan sous Valgrind pour chaque test.
  4. Conserver ASan et compiler MSan séparément avec des dépendances compatibles.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Tu disposes d’un vidage mémoire provenant d’un plantage qui ne se reproduit plus. Que peux-tu faire avec l’exécutable correspondant ?
  1. Reprendre après le plantage et modifier l’entrée.
  2. Inspecter les cadres et les variables enregistrées disponibles.
  3. Réexécuter les écritures antérieures depuis la trace de pile enregistrée.
  4. Recompiler le vidage en un exécutable corrigé.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Une fonction auxiliaire exécute 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 ?
  1. Incrémenter séparément, puis vérifier la borne interne par assertion.
  2. Garder l’incrémentation dans assert et utiliser -O0.
  3. Remplacer l’assertion par static_assert sans autre changement.
  4. Rendre used non signé et conserver l’assertion.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Le fuzzing découvre un plantage lorsqu’un analyseur convertit « -3 », le convertit explicitement en 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 ?
  1. Augmenter l’allocation, convertir d’abord et accepter toute conversion non nulle.
  2. Vérifier le remplissage après l’exécution, puis déterminer si la conversion a réussi.
  3. Valider la conversion et le nombre, vérifier le produit de taille, puis l’allocation.
  4. Rendre le nombre de remplissage non signé et garder la conversion et l’allocation.
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

Un programme doit afficher une chaîne terminée par un caractère nul fournie dans un fichier, sans interpréter les conversions de format qu’elle contient. Quel appel permet de le faire ?
  1. printf(label);
  2. printf("%s", label);
  3. printf("%d", label);
  4. printf("%p", label);
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

Une fonction appelante passe char line[48] à read_line(char line[]) sur un système dont les pointeurs occupent huit octets. Que mesure sizeof line dans la fonction ?
  1. Le tableau de 48 octets de la fonction appelante.
  2. La longueur actuelle de la chaîne.
  3. La taille du tableau plus un caractère nul de fin.
  4. Le paramètre pointeur de huit octets.
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 ?
  1. La sortie complète ne tenait pas.
  2. Exactement onze caractères ont été stockés.
  3. La sortie tenait avec son caractère nul de fin.
  4. Une erreur d’encodage a empêché la sortie.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Un analyseur décimal strict remet 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 ?
  1. La valeur numérique renvoyée est différente de zéro.
  2. L’argument de base indique la base décimale.
  3. Le pointeur de fin indique un texte restant.
  4. Le résultat tient dans la plage de long.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Un programme accepte des nombres d’éléments décimaux de 1 à 900. Il analyse « -4rest » avec 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 ?
  1. Convertir le préfixe, multiplier, puis rejeter un résultat nul et un échec d’allocation.
  2. Exiger une conversion complète et une plage acceptée, puis vérifier le produit non signé.
  3. Accepter le préfixe si errno vaut zéro, puis imposer seulement la capacité du tampon.
  4. Retirer le signe moins, accepter le préfixe, puis utiliser une assertion pour la taille.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Un tableau n’est détenu que par l’intermédiaire de 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 ?
  1. Vérifier le produit, redimensionner par un pointeur temporaire et n’écrire qu’après réussite.
  2. Calculer d’abord le produit, redimensionner directement dans a et vérifier l’ancienne taille.
  3. Redimensionner par un pointeur temporaire, ignorer une valeur nulle et écrire par le a initial.
  4. Vérifier seulement la nullité, remplacer a et valider le produit après l’écriture.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Un tampon de ligne de 12 octets reçoit 11 caractères de 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 ?
  1. Supprimer le dernier caractère stocké, puis accepter le préfixe comme ligne complète.
  2. Analyser le préfixe et laisser l’appel suivant créer un enregistrement.
  3. Ajouter un caractère nul de fin et déclarer la ligne complète.
  4. Déterminer ce qu’il reste à lire et appliquer la politique de traitement des lignes.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Une fonction reçoit un pointeur de destination et une capacité de 7. Elle doit préserver une entrée entière de neuf caractères ou signaler un échec. Elle utilise strncpy(dst, src, sizeof dst), puis affiche dst avec %s. Suppose que les pointeurs occupent huit octets. Quelle nouvelle conception respecte l’exigence ?
  1. Garder sizeof dst, ajouter un caractère nul après huit octets et afficher le préfixe.
  2. Copier six caractères, ajouter un caractère nul et signaler la réussite pour le préfixe.
  3. Utiliser la capacité fournie, vérifier le résultat d’une copie limitée et rejeter la troncature.
  4. Utiliser l’entrée comme format de printf pour éviter toute vérification de taille.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Un programme est désormais compilé en C23. Il lit un 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 ?
  1. Appeler realloc pour toute taille, affecter le résultat au pointeur responsable, puis vérifier le débordement du produit.
  2. Traiter zéro séparément, vérifier le produit, puis utiliser un pointeur temporaire.
  3. Convertir le produit en int signé, puis appeler realloc.
  4. Utiliser assert(new_n > 0), puis remplacer directement le pointeur responsable.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Un nouvel analyseur de fichiers traite des données provenant d’Internet et n’a aucune contrainte d’intégration, de matériel ou de ressources imposant C. Quel choix de conception réduit le plus directement les risques liés à la gestion manuelle des bornes et des durées de vie ?
  1. Utiliser un langage à sécurité mémoire pour l’analyseur.
  2. Utiliser C avec seulement un tampon par défaut plus grand.
  3. Utiliser C et désactiver toutes les allocations dynamiques.
  4. Utiliser C avec seulement la randomisation des adresses.
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

Un pointeur est utilisé après la libération de son objet. Quel aspect de la sécurité mémoire est violé ?
  1. La sécurité des chaînes de format.
  2. La sécurité spatiale.
  3. La sécurité numérique.
  4. La sécurité temporelle.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 2 sur 10

En C17, pour 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 » ?
  1. Sauvegarder i dans old, incrémenter i, puis afficher old et i.
  2. Inverser les deux arguments et garder l’incrémentation dans l’appel.
  3. Ajouter des parenthèses autour de ++i et garder les deux arguments.
  4. Convertir les deux arguments en unsigned dans le même appel.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 3 sur 10

Une évaluation du risque CERT attribue une gravité de 3, une probabilité de 2 et une note de correction de 2. Quels sont la priorité et le niveau obtenus ?
  1. Priorité 7, niveau L2.
  2. Priorité 9, niveau L2.
  3. Priorité 12, niveau L1.
  4. Priorité 18, niveau L1.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 4 sur 10

Du texte est décalé vers la gauche dans un même tableau et les plages d’octets source et destination se chevauchent. Quelle fonction définit la copie dans cette situation ?
  1. memcpy
  2. memmove
  3. strncpy
  4. snprintf
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 5 sur 10

Une fonction est responsable de 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 ?
  1. Garder le nombre en int, ajouter un octet et conserver la libération avant le retour.
  2. Utiliser size_t, mais allouer le nombre d’octets de la longueur et transférer le bloc obtenu.
  3. Garder une longueur size_t vérifiée, ajouter le caractère nul de fin et transférer une allocation valide vérifiée.
  4. Garder le nombre en int et l’allocation, mais supprimer seulement la libération prématurée.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 6 sur 10

Un analyseur supprime le saut de ligne d’une ligne complète, appelle 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 ?
  1. La présence d’un séparateur décimal et d’un chiffre avant celui-ci.
  2. Seulement l’égalité du résultat avec LONG_MAX.
  3. Seulement l’utilisation de la base décimale pour la conversion.
  4. L’absence de chiffres, le texte final et la plage d’indices valides.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 7 sur 10

Une boucle dont le comportement dépend d’une taille est testée sous ASan et UBSan uniquement avec un élément. Le test réussit. En production, elle échoue avec un tampon plein et les assertions désactivées. Quel plan de vérification traite ces différences ?
  1. Tester les entrées aux limites dans les versions de vérification et de production.
  2. Répéter le test existant à un élément avec des affichages de diagnostic supplémentaires.
  3. Garder les assertions activées et ignorer le cas du tampon plein.
  4. Exécuter une analyse statique et abandonner tous les tests dynamiques.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 8 sur 10

Un analyseur C existant active la protection de pile et l’ASLR. Une entrée issue du fuzzing déclenche malgré tout une écriture au-delà d’un bloc du tas signalée par ASan. Quelle réponse combine correctement prévention et atténuation ?
  1. Retirer le sanitizer car le durcissement définit l’écriture.
  2. Corriger la borne dans le tas et conserver les protections en production.
  3. Désactiver l’ASLR pour rendre l’écriture invalide valide.
  4. Agrandir le bloc du tas jusqu’à retarder le plantage.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 9 sur 10

Après un plantage dans une fonction de manipulation de chaînes, le cadre zéro ne contient aucune variable locale utile. Le cadre trois dans ton code passe un pointeur renvoyé par une recherche. Ce pointeur est nul et les entrées absentes sont des données valides. Quelles correction et vérification faut-il effectuer ?
  1. Ajouter une assertion à l’exécution avant l’appel de bibliothèque, puis garder la fonction appelante.
  2. Remplacer la recherche par un tableau alloué plus grand.
  3. Utiliser continue jusqu’à ce que le pointeur devienne non nul.
  4. Gérer l’absence de résultat avant l’appel, puis tester cette entrée.
Afficher la réponseMasquer la réponse

Enregistre cette unité pour voir cette réponse.

Question 10 sur 10

Une fonction ouvre un fichier de sortie et alloue deux tampons. La seconde allocation échoue. Son nettoyage ferme le fichier et effectue un retour, mais ne libère jamais le premier tampon. Quelle correction préserve à la fois la propriété et le signalement de l’erreur ?
  1. Signaler l’échec et fermer le fichier, mais garder le premier tampon sans fonction appelante responsable.
  2. Libérer le premier tampon et fermer le fichier, mais renvoyer un succès pour le travail incomplet.
  3. Libérer le tampon acquis, vérifier la fermeture et signaler l’échec de l’opération.
  4. Libérer le second résultat nul et fermer le fichier, sans libérer la première allocation.
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

78 Mots-Clés, chacun avec sa définition

A

accès hors limites
Un accès hors limites lit ou écrit en dehors des éléments valides d’un objet, même si l’adresse correspond à une zone mémoire mappée.
L’indice six est hors d’un tableau de six éléments, car son dernier élément valide est à l’indice cinq.
AddressSanitizer
AddressSanitizer est une instrumentation fondée sur le compilateur qui détecte à l’exécution de nombreuses opérations invalides liées aux bornes, aux durées de vie et aux libérations.
Un rapport peut indiquer l’écriture incorrecte et l’allocation dont cette écriture a dépassé les bornes.
analyse statique
L’analyse statique examine le code source et les chemins d’exécution possibles sans exécuter le programme sur des données de test.
Un analyseur peut trouver un chemin menant à une double libération qu’aucun test de non-régression actuel n’atteint.
arithmétique vérifiée
L’arithmétique vérifiée détecte un résultat mathématique non représentable sans obliger le programme à effectuer un débordement indéfini.
ckd_add de C23 indique si une somme est représentable afin que le programme puisse gérer explicitement l’échec.
assertion
Une assertion est une vérification exécutable d’une hypothèse de programmation qui signale un échec et interrompt le programme lorsqu’elle est activée et fausse.
Une fonction auxiliaire interne de calcul de moyenne peut vérifier par assertion que la fonction appelante ayant validé les données fournit un nombre non nul.
assertion statique
Une assertion statique vérifie une expression constante pendant la compilation et fait échouer celle-ci lorsque la condition est fausse.
Un static_assert C17 peut rejeter une plateforme où une hypothèse nécessaire sur la taille de int est fausse.
avertissement du compilateur
Un avertissement du compilateur signale du code suspect ou un problème détecté tout en permettant normalement à la compilation de continuer.
Une affectation utilisée comme condition peut être compilée avec succès tout en recevant un avertissement.

B

bogue
Un bogue est un défaut du code qui peut provoquer un comportement incorrect lorsqu’un chemin ou une entrée particulière l’atteint.
Une condition de boucle peut être incorrecte même lors des exécutions où l’accès invalide qu’elle provoque ne produit aucun plantage visible.

C

cadre de pile
Un cadre de pile est le contexte d’exécution d’un appel de fonction actif, avec ses arguments et variables locales disponibles et ses informations de retour.
Lorsqu’une fonction appelante passe un pointeur name, sélectionner son cadre après un plantage permet d’inspecter ce pointeur.
capacité du tampon
La capacité du tampon est la quantité totale de mémoire accessible en écriture, y compris la place nécessaire au caractère nul de fin d’une chaîne.
Un tableau de dix octets peut stocker au plus neuf caractères d’un octet sous forme de chaîne C terminée par un caractère nul.
caractère nul de fin de chaîne
Un caractère nul de fin de chaîne est le caractère de valeur zéro qui marque la fin d’une chaîne C et nécessite un espace de stockage en plus de ses caractères visibles.
La chaîne « ready » nécessite six octets pour que les fonctions de manipulation des chaînes trouvent un caractère nul de fin après ses cinq lettres.
cas minimal de reproduction
Un cas minimal de reproduction est une petite entrée et un cas de programme qui déclenchent encore la défaillance étudiée.
En retirant les enregistrements sans rapport avec le problème dans l’entrée d’un analyseur qui plante, il reste un champ vide qui déclenche toujours le plantage de manière constante.
chaîne de format
Une chaîne de format indique à une fonction d’entrée-sortie formatée comment interpréter les arguments ; ses conversions doivent donc correspondre à leurs types réels.
Un format printf utilise %zu pour size_t ; le texte utilisateur doit figurer dans un argument de données distinct.
chemin de nettoyage
Un chemin de nettoyage libère les ressources effectivement acquises avant le retour d’une fonction, y compris après une opération partiellement accomplie.
Une section commune de nettoyage peut libérer un tampon et fermer un fichier après l’échec d’une allocation ultérieure.
code non conforme
Un code non conforme est un exemple qui enfreint une règle de programmation particulière dans les conditions décrites par cette règle.
Utiliser 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
Un comportement défini par l’implémentation autorise un choix que chaque implémentation C doit documenter afin que les programmes puissent en tenir compte.
Une implémentation C17 documente si son type char simple est signé ou non signé.
comportement indéfini
Un comportement indéfini est un comportement pour lequel la norme C n’impose aucune exigence après une opération invalide.
Un débordement signé peut sembler reboucler dans une version compilée et se comporter différemment après optimisation.
comportement non spécifié
Un comportement non spécifié autorise un choix parmi des résultats valides sans exiger que l’implémentation documente son choix.
Des appels indépendants utilisés comme arguments peuvent être évalués dans l’un ou l’autre ordre sans invalider l’appel.
conversion implicite
Une conversion implicite change le type d’une valeur sans conversion explicite écrite et peut perdre des informations ou modifier la valeur.
Affecter un double à un int supprime la partie fractionnaire lorsque la valeur tronquée est représentable.
course aux données
Une course aux données se produit lorsque des threads effectuent des accès conflictuels sans la synchronisation requise et qu’au moins l’un des accès est une écriture.
Deux mises à jour non synchronisées d’un état partagé n’ont pas besoin de se produire au même instant pour constituer une course.

D

débogueur
Un débogueur contrôle l’exécution d’un programme pour te permettre de le suspendre, d’inspecter son état et d’avancer dans les opérations choisies.
GDB et LLDB peuvent arrêter le programme avant le corps d’une boucle pour que tu puisses comparer l’indice courant à la borne.
débordement d’entier signé
Un débordement d’entier signé se produit lorsqu’un résultat arithmétique n’est pas représentable dans son type signé de destination et entraîne un comportement indéfini.
Avec un int sur 32 bits, ajouter un à INT_MAX ne peut pas produire une valeur suivante portable et définie.
défaillance
Une défaillance est un écart observé par rapport au comportement attendu, comme un plantage, un résultat incorrect ou une sortie absente.
Un plantage dans free peut être la défaillance visible causée par une écriture bien antérieure dans l’exécution.
défense en profondeur
La défense en profondeur combine prévention, détection et réduction des dommages afin que plusieurs protections traitent les défauts restants.
Les avertissements et la revue de code préviennent les bogues, les sanitizers les détectent pendant les tests et le durcissement limite certains dommages.
diagnostic du compilateur
Un diagnostic du compilateur est un message qui indique un emplacement dans le code source et un problème suspecté ou avéré repéré pendant la compilation.
Un diagnostic de format peut pointer vers %d et désigner l’argument double qui ne lui correspond pas.
données d’entrée non fiables
Les données d’entrée non fiables sont des données extérieures au contrôle du programme dont le format, la longueur et la plage de valeurs doivent être vérifiés avant une utilisation sensible.
Un nombre fourni en ligne de commande doit être validé avant de contrôler l’allocation d’un tableau ou la borne d’une boucle.
double libération
Une double libération tente de libérer la même allocation plusieurs fois sans qu’une nouvelle allocation valide ait établi la propriété.
Une branche de nettoyage qui libère un bloc déjà libéré peut déclencher un rapport de l’allocateur ou d’ASan.
durcissement
Le durcissement ajoute des protections destinées à détecter certaines corruptions ou à limiter les dommages causés par des défauts qui subsistent dans un programme distribué.
Une vérification de protection de pile peut arrêter une fonction après avoir détecté l’écrasement de son canari.
durée de vie d’un objet
La durée de vie d’un objet est la période d’exécution pendant laquelle un objet existe et sa zone mémoire peut être utilisée selon les règles de C.
Un tableau automatique local existe pendant l’appel de sa fonction, tandis qu’une allocation dure jusqu’à sa libération.

E

effets de bord non séquencés
Les effets de bord non séquencés sont des modifications et des utilisations conflictuelles d’une même valeur scalaire sans l’ordre que C exige pour une évaluation définie.
Passer à la fois i et ++i à un appel mêle une lecture et une modification sans ordre sûr.
erreur de compilation
Une erreur de compilation est un diagnostic qui empêche la compilation de réussir tant que le problème signalé n’est pas résolu.
Corriger un point-virgule manquant peut supprimer plusieurs messages ultérieurs causés par la même erreur de syntaxe.
erreur de dépassement de plage
Une erreur de dépassement de plage se produit lorsque la valeur mathématique d’une conversion se trouve hors de la plage représentable par le type du résultat.
Après la remise à zéro de 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
L’exécution pas à pas fait avancer l’exécution d’une étape choisie dans le code source et permet l’inspection entre les instructions ou les appels de fonctions.
Utilise next pour exécuter un appel sans y entrer et step lorsque tu dois entrer dans le code de la fonction.

É

évaluation d’expression
Dans un débogueur, l’évaluation d’expression calcule une expression à partir de l’état courant du cadre sélectionné pendant que l’exécution est suspendue.
Afficher count observe une valeur scalaire ; évaluer *p tente un accès par ce pointeur.

F

fuzzing
Le fuzzing teste un programme avec de nombreuses entrées générées ou modifiées automatiquement pour découvrir des cas d’échec manqués par les tests prévus.
Un enregistrement modifié peut atteindre une branche d’analyseur où un sanitizer détecte un accès invalide.

H

hypothèse
Une hypothèse de débogage est une explication proposée pour une défaillance, qui permet de faire une prédiction vérifiable par une observation ou une modification.
Soupçonner un champ absent suggère d’inspecter son pointeur juste avant le traitement de la chaîne.

I

indice de priorité du risque
Un indice de priorité du risque CERT est le produit des notes de gravité, de probabilité et de correction, utilisé pour aider à ordonner l’application des règles ou les corrections.
Une note de correction plus élevée augmente la priorité, car la correction correspondante est moins coûteuse à effectuer.
informations de débogage
Les informations de débogage décrivent les noms, les types et les emplacements du code source dans un exécutable afin que les outils d’inspection puissent relier les instructions au code.
L’option -g aide un débogueur à identifier la ligne source et les variables locales lors d’un arrêt ordinaire.
injection de commandes
Une injection de commandes se produit lorsqu’un texte extérieur modifie la structure d’une commande de l’interpréteur et provoque des commandes ou des opérations non voulues.
Construire une chaîne pour system() à partir d’un argument non vérifié permet à l’interpréteur de commandes d’interpréter la syntaxe fournie.
instrumentation
L’instrumentation ajoute du code de vérification ou d’observation à un programme afin que des outils puissent inspecter ses opérations pendant l’exécution.
Une version ASan nécessite des fichiers objet instrumentés et l’environnement d’exécution du sanitizer à l’édition de liens finale.
invariant
Un invariant est une propriété maintenue au fil des étapes répétées ou des états valides d’une boucle ou d’une structure de données.
Le nombre stocké par une liste doit correspondre à son nombre de nœuds après chaque insertion terminée.

L

langage à sécurité mémoire
Un langage à sécurité mémoire impose des règles de bornes et de durée de vie pour ses opérations sûres, ce qui réduit le besoin de vérifications manuelles de sécurité mémoire.
Rust sûr combine des règles de propriété et des vérifications des bornes ; l’appel de code C non sûr exige toujours des précautions.
longueur de chaîne
La longueur de chaîne est le nombre de caractères avant le premier caractère nul de fin, sans compter ce caractère lui-même.
strlen("safe") vaut quatre, tandis que le stockage de la chaîne complète nécessite cinq octets.

M

masquage de variable
Le masquage de variable se produit lorsqu’une déclaration dans une portée interne masque une autre déclaration portant le même nom.
Le count d’une boucle interne peut être modifié tandis que le count externe reste inchangé.
mémoire fantôme
La mémoire fantôme est une zone de stockage auxiliaire qu’un outil de vérification utilise pour enregistrer des informations sur les octets de mémoire de l’application.
ASan consulte les informations de la mémoire fantôme pour vérifier si un accès instrumenté touche une zone mémoire autorisée.
MemorySanitizer
MemorySanitizer est l’outil de Clang qui suit à l’exécution les utilisations de valeurs non initialisées et nécessite une instrumentation compatible des dépendances.
Il cible une catégorie d’erreurs liées aux valeurs non initialisées que la vérification ordinaire d’ASan ne suit pas.

O

optimisation
L’optimisation transforme un programme pour améliorer son exécution ou son stockage tout en préservant le comportement exigé du code défini.
Avec -O2, un débogueur peut ne pas afficher de valeur distincte pour une variable locale supprimée par le compilateur.

P

pile d’appels
La pile d’appels est la chaîne des appels de fonctions actifs, de la fonction courante vers les fonctions qui l’ont appelée.
Un appel de bibliothèque de manipulation de chaînes au cadre zéro peut avoir reçu son pointeur invalide de main.
plages mémoire qui se chevauchent
Des plages mémoire qui se chevauchent partagent au moins un octet ; une copie entre elles nécessite donc un comportement qui préserve les données source initiales.
Décaler du texte dans un même tableau peut nécessiter memmove, car memcpy interdit le chevauchement.
point d’arrêt
Un point d’arrêt est un emplacement d’exécution choisi où un débogueur arrête le programme, normalement avant l’exécution de l’instruction indiquée.
Un point d’arrêt avant sum += a[i] te permet d’inspecter i sans évaluer l’accès au tableau.
point d’arrêt conditionnel
Un point d’arrêt conditionnel arrête le programme à un emplacement choisi uniquement lorsque sa condition est vraie dans l’état courant du programme.
Une condition comme i == 650 évite de parcourir manuellement les itérations précédentes de la boucle.
point de surveillance
Un point de surveillance arrête l’exécution lorsqu’un emplacement mémoire surveillé change, ce qui aide à identifier l’instruction qui a écrit sa valeur.
Surveiller le nombre stocké par une structure peut révéler une affectation inattendue bien avant un plantage ultérieur.
pointeur de fin de conversion
Un pointeur de fin de conversion fourni par strtol indique le premier caractère non converti ou le début de l’entrée si aucun chiffre n’a été consommé.
Pour « 73x », le pointeur de fin indique le « x » final qu’un analyseur numérique strict rejette.
pointeur pendant
Un pointeur pendant désigne une zone mémoire dont la durée de vie de l’objet est terminée : il ne peut donc pas donner un accès valide à cet ancien objet.
Renvoyer l’adresse d’un tableau local laisse à la fonction appelante un pointeur vers une zone mémoire dont la durée de vie est terminée.
portabilité
La portabilité est la capacité d’un code à se comporter comme prévu sur différentes implémentations sans dépendre d’hypothèses non garanties par les plateformes.
Utiliser INT_MAX évite de supposer que toutes les implémentations donnent à int la même valeur maximale.
postcondition
Une postcondition est une propriété qu’une opération s’engage à établir lorsqu’elle se termine avec succès.
Après le tri d’un tableau, une postcondition peut indiquer que chaque élément est inférieur ou égal au suivant.
précondition
Une précondition est une exigence qui doit être satisfaite au début d’une fonction ou d’une opération, établie par la fonction appelante ou par une validation antérieure.
Une fonction auxiliaire privée peut exiger un pointeur non nul avant d’examiner le premier élément d’un tableau.
promotion entière
Une promotion entière convertit un type entier plus petit en int ou en unsigned int avant de nombreuses opérations arithmétiques et de décalage.
La validité du nombre de positions de décalage dépend de la largeur de l’opérande gauche après promotion, pas seulement de son écriture initiale.
propriété
La propriété est la responsabilité, dans un programme, de gérer la durée de vie d’une ressource, notamment de la libérer et de conserver un moyen valide d’y accéder.
Un résultat temporaire préserve le pointeur responsable de l’allocation initiale lorsqu’un appel à realloc avec une taille positive échoue.

Q

quarantaine
Une quarantaine ASan retarde la réutilisation des blocs libérés, ce qui augmente la probabilité que des accès par d’anciens pointeurs atteignent une mémoire encore marquée comme invalide.
Un pointeur pendant peut être détecté avant que le bloc libéré soit réutilisé pour une autre allocation.

R

randomisation de l’espace d’adressage
La randomisation de l’espace d’adressage (ASLR) fait varier les emplacements mémoire d’une exécution à l’autre afin de rendre plus difficiles certaines attaques exploitant des défauts de mémoire restants.
L’ASLR peut compliquer l’exploitation d’un accès incorrect tout en laissant sans correction l’écriture hors limites initiale.
rebouclage des entiers non signés
Le rebouclage des entiers non signés ramène un résultat arithmétique dans la plage du type non signé ; il est défini, mais peut enfreindre les hypothèses de taille d’un programme.
Un produit donnant un nombre d’octets qui déborde peut demander une allocation bien plus petite que celle nécessaire au tableau prévu.
référentiel de programmation sécurisée
Un référentiel de programmation sécurisée est un ensemble d’exigences et de recommandations applicables pour éviter des défauts et des vulnérabilités connus.
Une liste de vérification pour la revue peut relier chaque contrôle de taille d’allocation au défaut qu’il vise à prévenir.

S

sécurité mémoire spatiale
La sécurité mémoire spatiale exige que chaque accès mémoire reste dans les bornes de l’objet auquel il est autorisé à accéder.
Une vérification des bornes avant une écriture dans un tableau empêche un indice de sélectionner un élément hors de ce tableau.
sécurité mémoire temporelle
La sécurité mémoire temporelle n’autorise l’accès à un objet que pendant sa durée de vie et lorsque cet accès est valide.
Un alias devenu invalide après free viole la sécurité temporelle même si son adresse semble encore familière.
SEI CERT C Coding Standard
Le SEI CERT C Coding Standard est le recueil de règles C, de recommandations, d’exemples et d’évaluations du risque du SEI de Carnegie Mellon.
Sa règle MEM35-C aide les personnes qui relisent le code à vérifier si une allocation contient assez d’octets pour ses objets.
solution conforme
Une solution conforme est une conception ou un exemple de code qui satisfait l’exigence d’une règle dans les conditions indiquées pour cette solution.
Utiliser un format fixe %s maintient le texte fourni dans l’argument de données d’un appel à printf.

T

test de non-régression
Un test de non-régression vérifie un comportement qui a déjà échoué afin de détecter le retour de ce défaut après des modifications ultérieures.
Une entrée sauvegardée contenant un enregistrement vide qui faisait auparavant planter un analyseur est exécutée à nouveau après chaque modification de l’analyseur pour détecter le retour du plantage.
ThreadSanitizer
ThreadSanitizer est un outil de vérification à l’exécution qui repère les accès conflictuels et non synchronisés de threads à la mémoire partagée.
Il nécessite une version compilée séparément d’ASan pour examiner les courses aux données potentielles.
trace de pile
Une trace de pile est un rapport du débogueur sur les cadres d’appels actifs, généralement numérotés à partir de zéro pour le cadre le plus profond.
Après un plantage, bt t’aide à trouver le premier cadre source appartenant à ton propre code.
troncature
La troncature supprime la partie du texte qui ne tient pas dans une destination, même lorsque le préfixe stocké est correctement terminé par un caractère nul.
Une valeur de retour de snprintf positive ou nulle et supérieure ou égale à la capacité signale que le texte complet ne tenait pas.

U

UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer insère des vérifications de nombreuses opérations indéfinies pendant l’exécution, notamment les débordements signés et les décalages invalides.
Un rapport UBSan peut indiquer une addition qui déborde même lorsque le programme affiche un total plausible.
utilisation après libération
Une utilisation après libération accède à une allocation après sa libération, en dehors de la durée de vie de l’objet initial.
Libérer le pointeur responsable met aussi fin à l’accès valide par un alias qui contient encore son ancienne adresse.

V

valeur indéterminée
Une valeur indéterminée est l’état d’un objet dont la valeur utilisable n’a pas été établie, comme pour de nombreuses variables automatiques non initialisées.
Déclarer int sum; puis utiliser immédiatement sum += x; lit une valeur qui n’a jamais été initialisée.
version de débogage
Une version de débogage est un exécutable configuré pour l’inspection, généralement avec des informations source et peu d’optimisation.
Une version compilée avec -g -O0 permet de suivre plus facilement une boucle incorrecte instruction source par instruction source.
version de production
Une version de production est un exécutable configuré pour une utilisation ordinaire, généralement avec l’optimisation et le durcissement pris en charge activés.
Une version de production doit conserver les vérifications des entrées même lorsque NDEBUG supprime ses assertions.
vidage mémoire
Un vidage mémoire est un instantané enregistré d’un processus au moment d’un plantage, qu’un débogueur peut inspecter avec l’exécutable correspondant.
Un vidage enregistré permet d’afficher une trace de pile et d’inspecter les valeurs locales après la fin du processus en échec.

Z

zone rouge
Une zone rouge est une zone mémoire interdite placée autour d’un objet pris en charge afin qu’un outil de vérification puisse détecter les accès qui franchissent sa limite.
Une écriture supplémentaire immédiatement après un bloc du tas peut entrer dans sa zone rouge et déclencher ASan.

À propos de cette unité

Par StudeliSignaler une erreur

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.

Commencer