Rejoignez-nous le 30 avril : dévoilement de Parasoft C/C++test CT pour l'excellence en matière de tests continus et de conformité | En savoir plus

Comment gérer efficacement les pointeurs en C pour éviter les abus

Portrait de Ricardo Camacho, directeur de la conformité de la sûreté et de la sécurité
1 décembre 2023
4 min lire

Bien que les pointeurs vous permettent d’utiliser votre créativité pour résoudre un problème particulier, ils présentent plusieurs limitations qui sont parmi les plus difficiles à gérer pour les programmeurs. Lisez la suite pour savoir comment utiliser Insure++ pour identifier automatiquement les problèmes liés aux pointeurs.

Les pointeurs sont à la fois la force et le talon d’Achille de programmation en C et C++. Tandis que les pointeurs vous permettent d’être très créatif et flexible dans la manière dont vous abordez la résolution d’un problème particulier. Cependant, il est facile d’introduire involontairement des défauts dans votre code. Les problèmes avec les pointeurs sont parmi les plus difficiles rencontrés par les programmeurs C. Il y a une génération, les techniques de force brute, comme l'insertion d'instructions d'impression dans le code, constituaient souvent la meilleure stratégie pour tenter de détecter ces problèmes.

Outils pour détecter les abus de pointeurs en C

Insure++ : un outil pour les vérifications dynamiques de l'allocation de mémoire

Aujourd'hui, les outils de détection d'erreurs de mémoire comme Assurer ++ peut détecter automatiquement les problèmes liés au pointeur lors de l'exécution du code, ce qui permet d'économiser beaucoup de temps et d'éviter des maux de tête. Insure++ détecte des problèmes dans les catégories suivantes :

  • Opérations sur les pointeurs NULL
  • Opérations sur des pointeurs non initialisés
  • Opérations sur des pointeurs qui ne pointent pas vers des données valides
  • Opérations qui tentent de comparer ou de relier des pointeurs qui ne pointent pas vers le même objet de données
  • Appels de fonction via des pointeurs de fonction qui ne pointent pas vers des fonctions
  • De nombreuses autres causes d'un éventuel comportement indéfini ou d'un comportement défini par l'implémentation

Insure++ utilise un analyseur de code de pointe, ainsi que des centaines d'heuristiques, pour analyser le code de l'application, au cours duquel il signale plusieurs violations statiques possibles. Lors de l'analyse du code, il écrit un nouveau fichier de code source avec les instruments appropriés insérés dans les points problématiques, tels que le déréférencement du pointeur, la sortie de la portée, etc. Le fichier source résultant est automatiquement compilé et tous les fichiers de code objet résultants sont liés dans un nouveau programme exécutable.

Identifier l'adresse d'une variable : meilleures pratiques

Dans de nombreux cas, lorsque vous essayez de résoudre des problèmes de gestion de la mémoire et de déterminer si une variable est corrompue, vous aurez besoin de connaître l'adresse de cette variable et découvrirez probablement qu'il ne s'agit pas seulement de cette variable, mais aussi d'autres. L'utilisation d'un débogueur est souvent considérée comme une bonne pratique lorsqu'il s'agit d'identifier les problèmes dans votre code, notamment pour comprendre les valeurs des variables et les adresses mémoire.

Les débogueurs fournissent un ensemble d'outils puissants pour inspecter et manipuler l'exécution de votre programme. Cependant, il est important de noter que l’utilisation d’un débogueur n’est pas incompatible avec d’autres techniques de débogage. Les instructions d'impression et l'inspection manuelle peuvent toujours s'avérer des outils précieux, en particulier dans les situations où l'utilisation d'un débogueur peut s'avérer peu pratique.

Une approche simple que j'utilise depuis de nombreuses années consiste simplement à utiliser l'adresse de l'opérateur « & » dans une instruction printf. Prenons l'exemple de code suivant.

int myVariable = 77;
printf("Address of myVariable is: %p\n", (void *)&myVariable);

L'expression &myVariable renvoie l'adresse de myVariable. Le spécificateur de format %p est utilisé avec printf pour imprimer l'adresse au format pointeur. Le cast (void*) est utilisé pour correspondre au spécificateur de format %p. N'oubliez pas que l'adresse mémoire réelle peut varier à chaque exécution du programme.

Exemple pratique : gestion des pointeurs dans un programme « Hello World »

Vous trouverez ci-dessous le code d'un programme « Hello, World » qui utilise l'allocation dynamique de mémoire.

    
/*
 * File: hello.c
 */
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
    char *string, *string_so_far;
    int i, length;     length = 0;
    for(i=0; i<argc; i++) {
        length += strlen(argv[i])+1;
        string = malloc(length+1);
 
        /*  * Copy the string built so far. */
        if(string_so_far != (char *)0)
            strcpy(string, string_so_far);
        else *string = '\0';
        strcat(string, argv[i]);
        if(i < argc-1) strcat(string, " ");
        string_so_far = string;
    }
    printf("You entered: %s\n", string_so_far);
    return (0);
}
    

L'idée de base de ce programme est que nous gardons une trace de la taille actuelle de la chaîne dans la longueur variable. Au fur et à mesure que chaque nouvel argument est traité, nous ajoutons sa longueur à la variable de longueur et allouons un bloc de mémoire de la nouvelle taille. Notez que le code prend soin d'inclure le caractère NULL final lors du calcul de la longueur de la chaîne (ligne 14) ainsi que de l'espace entre les chaînes. Ces deux erreurs sont faciles à commettre. C'est un exercice intéressant de voir à quelle vitesse vous pouvez trouver une telle erreur avec un outil de détection d'erreurs de mémoire comme Parasoft Insure++.

Le code copie l'argument dans le tampon ou l'ajoute, selon qu'il s'agit ou non du premier passage autour de la boucle. Enfin, le pointeur string_so_far pointe vers la nouvelle chaîne plus longue.

Si vous compilez et exécutez ce programme sous Insure ++, vous verrez des erreurs de «pointeur non initialisé» signalées pour le code «strcpy (string, string_so_far)». C'est parce que la variable string_so_far n'a été définie sur rien avant le premier voyage à travers la boucle d'arguments. Dans un petit exemple de code comme celui-ci, un tel problème est évident, mais même si l'erreur est enterrée dans un tas de centaines de milliers de lignes de code et beaucoup plus subtile que l'erreur ci-dessus, Insure ++ le trouvera à chaque fois.

Rapports et informations d'Insure++

Insure++ signale tous les problèmes détectés. Les rapports Insure++ contiennent des informations détaillées telles que le type de bogue, le fichier source et le numéro de ligne, le contenu réel de la ligne de code source et les expressions à l'origine du problème, avec des rapports comprenant :

  • Le type de bug, par exemple EXPR_UNRELATED_PTRCMP
  • Le fichier source et le numéro de ligne, par exemple foo.cc:42
  • Le contenu réel de la ligne de code source, par exemple, « while (p < g) { »
  • L'expression à l'origine du problème, par exemple « p < g »
  • Informations sur tous les pointeurs et blocs de mémoire impliqués dans le bogue:
    • Valeurs du pointeur
    • Blocs de mémoire pointés (le cas échéant) et tout décalage
    • Informations sur l'allocation des blocs :
      • Trace de pile si allouée dynamiquement
      • Emplacement de déclaration de bloc (fichier source et numéro de ligne), s'il est alloué sur la pile ou globalement
      • Trace de pile de désallocation du bloc, le cas échéant
    • Trace de pile montrant comment le programme est arrivé à l'emplacement du bogue

Étapes suivantes : comment protéger vos pointeurs en C

La couverture des tests est essentielle pour assurer la sécurité des pointeurs en C et C++. L'analyse statique et l'analyse dynamique jouent un rôle important.

L'importance de la couverture des tests dans les pointeurs

Lorsqu'il s'agit de travailler avec des pointeurs en C ou C++, la couverture des tests est importante car ils introduisent des risques. Le risque peut se présenter de manière subtile et parfois très difficile à détecter, ce qui entraîne des coûts de main-d'œuvre élevés.

Il est essentiel d’utiliser un ensemble complet de méthodes de test pour couvrir une liste de problèmes et de scénarios de pointeurs. Par exemple, vous voudrez utilisez un outil comme Insure++ pour aider les développeurs à détecter les erreurs de programmation erratiques et d'accès à la mémoire, telles que la corruption du tas, les threads malveillants, les fuites de mémoire, les tableaux hors limites et les pointeurs invalides.

Analyse statique

Un moyen puissant de commencer est de en utilisant l'analyse statique ou la détection des erreurs de compilation, qui détecte les problèmes potentiels liés aux pointeurs pendant la phase de compilation. Des problèmes tels que la perte de précision du pointeur, la non-concordance dans la spécification de format ou le type d'argument, les variables inutilisées, le code mort, la division par zéro, etc.

Analyse dynamique

Tests d'analyse dynamique est une autre approche puissante et complémentaire de l’analyse statique. La détection des erreurs d'exécution permet d'identifier la mémoire de tas et de pile corrompue, ainsi que tous les types de fuites de mémoire, d'allocation de mémoire, d'erreurs ou de discordances libres et d'erreurs de tableau hors limites.

Pour garantir que vous avez testé tout le code, l'analyse de couverture de code permet d'identifier visuellement quelles sections de code ont été exécutées et lesquelles ne l'ont pas été. Laisser du code non testé dans votre application peut vous coûter des nuits blanches ou venir vous mordre plus tard.

Ajouter une analyse statique à votre boîte à outils de test de sécurité