Webinaire en vedette : Dévoilement de Parasoft C/C++test CT pour l'excellence en matière de tests continus et de conformité | Voir le séminaire

Détection de la corruption de mémoire en C et C ++

Portrait de Ricardo Camacho, directeur de la conformité de la sûreté et de la sécurité
22 novembre 2023
7 min lire

Découvrez cette brève explication des raisons pour lesquelles la corruption de la mémoire en C et C++ est si difficile à détecter par l'analyse du code et les instructions d'utilisation d'un outil de détection des défauts de mémoire qui vous éviteront de longues heures de sessions de débogage.

Les programmeurs continuent d'utiliser les langages de programmation C et C++ car ils peuvent facilement interagir avec la mémoire, travailler en étroite collaboration avec le matériel et offrir la puissance, les performances et l'efficacité nécessaires au développement embarqué. Cependant, ces langages sont sujets à des problèmes de mémoire subtils tels que des fuites de mémoire, un débordement de mémoire tampon, un débordement numérique, etc.

Il est malheureusement trop courant que de telles erreurs restent cachées lors des tests normaux. Les logiciels présentant des problèmes subtils tels qu'une corruption de la mémoire peuvent fonctionner parfaitement sur une machine mais planter sur une autre ou bien fonctionner pendant un certain temps pour ensuite planter de manière inattendue lorsque le système est resté opérationnel pendant un certain nombre de jours.

Ces types de corruption de mémoire, ainsi que d’autres erreurs courantes telles que des problèmes de manipulation de chaînes, une initialisation incorrecte et des erreurs de pointeur, entraînent des plantages en production. Avec l'augmentation du nombre de logiciels embarqués aujourd'hui dans les avions, les voitures, les appareils médicaux et le marché croissant de l'IoT, les conséquences des logiciels buggés sont devenues plus que des clients mécontents. Ils peuvent mettre la vie en danger.

Qu’est-ce que la corruption de la mémoire ?

Les erreurs de corruption de mémoire sont désagréables, surtout si elles sont bien dissimulées. Lorsqu’ils se manifestent, ils peuvent être trompeusement difficiles à reproduire et à retrouver. À titre d'exemple de ce qui peut arriver, considérons le programme présenté ci-dessous.

Ce programme concatène les arguments donnés sur la ligne de commande et imprime la chaîne résultante:

/*
 * File: hello.c
 */
#include <string.h>
#include <string.h>
int main(argc, argv)
    int argc;
    char *argv[];
{
    int i;
    char str[16];
    str[0] = '\0';
    for(i=0; i<; argc; i++) {
        strcat(str, argv[i]);
        if(i < (argc-1)) strcat(str, “ “);
    }
    printf("You entered: %s\n", str);
    return (0);
}

Si vous compilez et exécutez ce programme avec votre compilateur normal, vous ne verrez probablement rien d'intéressant. Par exemple:

c:\source> cc -o hello hello.c 
    c:\source> cc -o hello  
    You entered: hello
    c:\source> hello world
    You entered: hello world
    c:\source> hello cruel world
    You entered: hello cruel world

Si telle était l'étendue de vos procédures de test, vous concluriez probablement que ce programme fonctionne correctement, malgré le fait qu'il a un bogue de corruption de mémoire très grave, il ne s'est tout simplement pas manifesté en produisant une sortie incorrecte. Ceci est courant avec les problèmes de mémoire - ils peuvent souvent ne pas être détectés car ils peuvent ne pas affecter directement la sortie et ne seront donc pas détectés par des tests unitaires normaux ou des tests fonctionnels.

Ce type d'erreur semble assez simple quand il s'agit d'un petit programme d'exemple où il ne sera pas négligé, mais lorsqu'il est enterré dans un code compliqué avec des centaines de milliers de lignes et beaucoup d'allocation dynamique, il peut facilement éviter la détection jusqu'à la publication.

Types de corruption de mémoire en C et C++

La corruption de la mémoire est un problème critique en programmation, en particulier en C et C++, car ces langages offrent un accès direct à la gestion de la mémoire, augmentant ainsi le risque d'erreurs. Comme nous l'avons souligné ci-dessus, la corruption de la mémoire se produit lorsqu'un programme accède ou modifie la mémoire de manière involontaire, entraînant une corruption des données, des plantages ou des failles de sécurité.

Comprendre les différents types de corruption de la mémoire est crucial pour détecter les problèmes de mémoire et ouvre également la voie à leur atténuation. Les sections suivantes fournissent un aperçu de quatre types courants de corruption de mémoire.

1. Débordements de tampon

Les débordements de tampon se produisent lorsqu'un programme tente d'écrire plus de données dans un tampon qu'il n'a été conçu pour en contenir. Cela peut entraîner l'écrasement des données dans des emplacements de mémoire adjacents, corrompant potentiellement d'autres données ou provoquant le blocage du programme. Les dépassements de tampon sont une source courante de failles de sécurité, car ils peuvent être exploités pour exécuter du code arbitraire.
Les débordements de tampon peuvent se produire de plusieurs manières. Par exemple, si un programme copie une chaîne dans un tampon sans vérifier la longueur de la chaîne, il peut écraser la mémoire au-delà de la fin du tampon. De plus, si un programme utilise un index de tableau hors limites, il peut accéder à une mémoire qui ne lui appartient pas et corrompre les données.

Pour éviter les débordements de tampon, les programmeurs doivent toujours vérifier la taille du tampon de destination avant d'y copier des données. Ils doivent également utiliser des pratiques de programmation sûres, telles que l'utilisation des fonctions strcpy_s et strncpy_s en C ou de la classe std::string en C++, qui permet de vérifier les limites. Les normes de codage telles que MISRA C/C++ identifient l'utilisation de fonctions système non sécurisées et proposent des alternatives pour remédier à ces vulnérabilités identifiées.

2. Utilisation après libération

Des erreurs d'utilisation après libération surviennent lorsqu'un programme tente d'accéder ou de modifier une mémoire déjà libérée. Cela peut se produire si un pointeur vers la mémoire libérée n'est pas correctement invalidé, ou si le pointeur est passé à une autre partie du programme qui ne sait pas que la mémoire n'est plus valide. Ce type d'erreur de corruption de mémoire peut entraîner un comportement imprévisible, car la mémoire libérée peut être réaffectée à un objectif différent. Les causes courantes incluent l'échec de la définition des pointeurs sur NULL après avoir libéré la mémoire associée et l'utilisation incorrecte d'un pointeur après qu'il a été transmis à une fonction qui libère la mémoire associée.

Les erreurs d'utilisation après libération peuvent être difficiles à détecter et à déboguer, car le programme peut sembler fonctionner correctement jusqu'à ce que la mémoire libérée soit réutilisée. Cela peut conduire à un comportement imprévisible, tel que des plantages ou une corruption des données.

Les développeurs peuvent atténuer ce type de corruption de la mémoire en libérant de la mémoire lorsqu'elle n'est plus nécessaire. Ils doivent également utiliser des techniques appropriées de gestion des pointeurs, telles que la définition des pointeurs sur NULL après avoir libéré la mémoire vers laquelle ils pointent.

3. Double-gratuit

Les erreurs de double libération, également appelées erreurs de double suppression, se produisent lorsqu'un programme tente de libérer deux fois le même bloc de mémoire. Cela peut se produire si le programme gère plusieurs pointeurs vers le même bloc mémoire et le libère plusieurs fois, ou s'il passe plusieurs fois le même pointeur vers la fonction libre.

Les erreurs de double-libération sont de graves problèmes de corruption de mémoire qui peuvent entraîner un comportement imprévisible du programme, des pannes et des failles de sécurité si elles ne sont pas résolues. Pour éviter les erreurs de double libération, les programmeurs doivent conserver des enregistrements appropriés des blocs de mémoire alloués et s'assurer qu'ils ne sont libérés qu'une seule fois. Avant de libérer un bloc mémoire, ils doivent valider le pointeur pour vérifier s'il a déjà été libéré, empêchant ainsi les tentatives de libération double de la même mémoire.

4. Fuites de mémoire

Les fuites de mémoire se produisent lorsqu'un programme alloue de la mémoire mais ne parvient pas à la libérer ou à la libérer lorsqu'elle n'est plus nécessaire. Ce type de corruption de mémoire peut entraîner un épuisement progressif de la mémoire disponible, entraînant des problèmes de performances. Oublier de libérer la mémoire allouée dynamiquement et perdre toutes les références à la mémoire allouée sans la libérer sont des causes courantes de fuites de mémoire.

Comme les autres types de problèmes de corruption de mémoire, ils peuvent également être difficiles à détecter et à déboguer, car le programme peut sembler fonctionner correctement pendant un certain temps avant de manquer de mémoire.
Pour éviter les fuites de mémoire, les programmeurs doivent toujours libérer explicitement la mémoire lorsqu'elle n'est plus nécessaire. Ils doivent également utiliser un débogueur de mémoire automatisé pour C et C++, comme Parasoft Insure ++.

Causes et conséquences courantes de la corruption dans la mémoire C++

La corruption de la mémoire en C++ peut résulter de diverses causes, allant d'erreurs de programmation à des pratiques dangereuses, et ses conséquences peuvent avoir un impact négatif sur le comportement, la sécurité et l'intégrité des données du programme. Pensez à allouer de la mémoire au démarrage de l'application et à gérer ce bloc de mémoire avec des fonctions nouvelles et gratuites surchargées pour contrôler et remédier aux problèmes de corruption de mémoire.

Les sections suivantes donnent un aperçu de certaines causes et conséquences courantes de la corruption de la mémoire en C++.

1. Comportement indéfini

Un comportement non défini est l'une des causes notables de corruption de la mémoire en C et C++. Cela se produit lorsque le programme exécute du code non conforme aux spécifications du langage. Dans le contexte de la mémoire, l'accès à la mémoire non initialisée, la lecture/écriture au-delà des limites du tableau et le déréférencement de pointeurs nuls ou suspendus peuvent tous conduire à un comportement indéfini. Les conséquences d'un comportement indéfini sont imprévisibles, ce qui en fait une préoccupation majeure pour les développeurs.

2. Vulnérabilités de sécurité

La corruption de la mémoire constitue une menace sérieuse pour la sécurité des programmes C et C++. L'exploitation des vulnérabilités de la mémoire est une technique courante utilisée par les attaquants pour compromettre les systèmes. Les dépassements de tampon, l'utilisation après libération et d'autres problèmes liés à la mémoire peuvent être exploités pour exécuter du code arbitraire, injecter des charges utiles malveillantes ou manipuler le comportement du programme. Comprendre et atténuer ces vulnérabilités est essentiel pour développer des logiciels sécurisés.

3. Crashes et instabilité du programme

La corruption de la mémoire se manifeste souvent par des plantages et une instabilité des programmes. Lorsque la mémoire corrompue est accédée ou manipulée, cela peut entraîner un comportement inattendu, provoquant le blocage du programme. Identifier la cause première de ces plantages peut être difficile, nécessitant un débogage et une analyse approfondis. Des pratiques et des outils appropriés de gestion de la mémoire peuvent aider à prévenir ces problèmes et à améliorer la stabilité du programme.

4. Corruption des données

La corruption de la mémoire peut entraîner la corruption de structures de données et de variables critiques au sein d'un programme. Cela peut entraîner des calculs incorrects, une perte de données ou un comportement involontaire. Par exemple, les dépassements de tampon peuvent écraser des structures de contrôle importantes, entraînant une corruption des données. La prévention de la corruption des données implique une gestion minutieuse de la mémoire, une vérification appropriée des limites et le respect de pratiques de codage sécurisées.

Comment détecter les erreurs de mémoire ?

La meilleure façon d'aborder la recherche de défauts de mémoire complexes est d'utiliser un outil de détection d’erreurs de mémoire ou « débogueur d’exécution ». C'est facile à utiliser. Remplacez simplement le nom de votre compilateur (cc) par « assurer » comme ci-dessous.

cc -o hello hello.c

devient

insure -o hello hello.c

Ensuite, exécutez le programme. Si vous disposez d'un makefile bien formaté, vous pouvez utiliser Parasoft Insure++ en définissant la commande de votre compilateur pour assurer :

make CC=insure hello

Une fois que vous avez compilé avec le débogueur d'exécution, vous pouvez exécuter la commande:

hello cruel world

Cela générera les erreurs indiquées ci-dessous car la chaîne en cours de concaténation devient plus longue que les 16 caractères alloués dans la déclaration à la ligne 11 :

[hello.c:14] **WRITE_OVERFLOW**
&gt;&gt;         strcat(str, argv[i]);
  Writing overflows memory: &lt;argument 1&gt;
          bbbbbbbbbbbbbbbbbbbbbbbbbb
          |           16           | 2 |
          wwwwwwwwwwwwwwwwwwwwwwwwwwwwww
   Writing  (w) : 0xbfffeed0 thru 0xbfffeee1 (18 bytes)
   To block (b) : 0xbfffeed0 thru 0xbfffeedf (16 bytes)
                 str, declared at hello.c, 11
  Stack trace where the error occurred:
                          strcat()  (interface)
                            main()  hello.c, 14
**Memory corrupted.  Program may crash!!**
[hello.c:17] **READ_OVERFLOW**
&gt;&gt;     printf("You entered: %s\n", str);
  String is not null terminated within range: str
  Reading   : 0xbfffeed0
  From block: 0xbfffeed0 thru 0xbfffeedf (16 bytes)
             str, declared at hello.c, 11
  Stack trace where the error occurred:
                            main()  hello.c, 17
You entered: hello cruel world    

Vous avez probablement remarqué quelque chose d'intéressant dans le résultat, à savoir qu'il y a deux erreurs résultant de ce problème :

  1. Le débordement d'écriture lorsque vous essayez de mettre trop d'octets dans le tampon de chaîne.
  2. Un débordement de lecture lorsque vous lisez à partir du tampon de chaîne.

Comme vous pouvez le constater, l'erreur peut se manifester de différentes manières et à différents endroits, alors imaginez ce qui peut se produire dans un programme réel. Il est presque évident que tous les programmes C et C++ fonctionnels présentent des fuites de mémoire et d'autres erreurs de mémoire.

Si vous voulez trouver ces erreurs sans passer des semaines à courir après des problèmes obscurs, jetez un œil à Parasoft Insure ++. Il peut trouver tous les problèmes liés à l'écrasement de la mémoire ou à la lecture au-delà des limites légales d'un objet, qu'il soit alloué de manière statique, c'est-à-dire une variable globale, localement sur la pile, dynamiquement avec malloc ou new, ou même en tant que mémoire partagée. bloc. Il peut même détecter des situations dans lesquelles un pointeur passe d'un bloc de mémoire à un autre et commence à y écraser la mémoire, même si les blocs de mémoire sont adjacents. La détection des erreurs d'exécution avec Insure++ renforcera votre application et vous évitera les sessions de débogage qui durent toute la nuit.

Découvrez le débogueur de mémoire ultime pour C et C++.

« MISRA », « MISRA C » et le logo triangulaire sont des marques déposées de The MISRA Consortium Limited. ©The MISRA Consortium Limited, 2021. Tous droits réservés.