Pourquoi MISRA C indique-t-il qu’une copie de pointeurs peut provoquer une exception de mémoire?

La directive 4.12 de MISRA C 2012 est “L’allocation de mémoire dynamic ne doit pas être utilisée”.

À titre d’exemple, le document fournit cet exemple de code:

char *p = (char *) malloc(10); char *q; free(p); q = p; /* Undefined behaviour - value of p is indeterminate */ 

Et le document dit que:

Bien que la valeur stockée dans le pointeur rest inchangée à la suite de l’appel de libération, il est possible que, sur certaines cibles, la mémoire sur laquelle elle pointe n’existe plus et que le fait de copier ce pointeur puisse provoquer une exception de mémoire .

Je suis ok avec presque toute la phrase mais la fin. Comme p et q sont tous deux alloués sur la stack, comment la copie des pointeurs peut-elle provoquer une exception de mémoire?

Selon la norme, copier le pointeur q = p; , est un comportement indéfini.

Lecture J.2 États de comportement non définis:

La valeur d’un pointeur sur un object dont la durée de vie est terminée est utilisée (6.2.4).

En allant à ce chapitre, nous voyons que:

6.2.4 Durée de stockage des objects

La durée de vie d’un object correspond à la partie de l’exécution du programme pendant laquelle il est garanti que la mémoire lui est réservée. Un object existe, a une adresse constante, 33) et conserve sa dernière valeur stockée pendant toute sa durée de vie.34) Si un object est référencé en dehors de sa durée de vie, le comportement est indéfini. La valeur d’un pointeur devient indéterminée lorsque l’object vers lequel il pointe (ou juste après) atteint la fin de sa vie.

Quel est indéterminé:

3.19.2 valeur indéterminée : soit une valeur non spécifiée, soit une représentation de piège

Une fois que vous avez libéré un object via le pointeur, tous les pointeurs vers cette mémoire deviennent indéterminés. (Même) la lecture de mémoire indéterminée est un comportement indéfini (UB). Ce qui suit est UB:

 char *p = malloc(5); free(p); if(p == NULL) // UB: even just reading value of p as here, is UB { } 

Tout d’abord, un peu d’histoire …

Lorsque l’ISO / CEI JTC1 / SC22 / WG14 a commencé à formaliser le langage C (pour produire ce qui est maintenant l’ISO / CEI 9899: 2011), ils ont rencontré un problème.

De nombreux vendeurs de compilateurs ont interprété les choses de différentes manières.

Très tôt, ils ont pris la décision de ne casser aucune fonctionnalité existante. Ainsi, là où les implémentations du compilateur étaient divergentes, la norme offre unspecified comportements undefined unspecified et undefined .

MISRA C tente de piéger les pièges que ces comportements vont déclencher. Voilà pour la théorie …

Passons maintenant au détail de cette question:

Étant donné que le but de free () est de libérer la mémoire dynamic dans le tas, il y avait trois implémentations possibles, qui étaient toutes “dans la nature”:

  1. réinitialiser le pointeur sur NULL
  2. laisser le pointeur tel quel
  3. détruire le pointeur

La norme ne pouvant imposer aucune de ces conditions, le comportement n’est donc pas undefined : votre implémentation peut suivre un chemin, mais un compilateur différent peut faire autre chose … vous ne pouvez pas en déduire, et il est dangereux de s’appuyer sur une méthode.

Personnellement, je préférerais que la norme soit spécifique et nécessite free () pour définir le pointeur sur NULL, mais ce n’est que mon opinion.

Donc, le TL; DR; la réponse est malheureusement: parce que c’est!

Bien que p et q soient tous deux des variables de pointeur sur la stack, l’adresse mémoire renvoyée par malloc() n’est pas sur la stack.

Une fois qu’une zone mémoire qui a été mallocée avec succès est libérée, il est alors impossible de dire qui peut utiliser la zone mémoire ou la disposition de la zone mémoire.

Ainsi, une fois que free() est utilisé pour libérer une zone de mémoire précédemment obtenue à l’aide de malloc() tentative d’utilisation de la zone de mémoire constitue un type d’action non défini. Vous pourriez avoir de la chance et ça va marcher. Vous pourriez être malchanceux et ce ne sera pas. Une fois que vous avez free() une zone mémoire, vous ne la possédez plus, mais quelque chose d’autre.

Le problème ici semble être de savoir quel code machine est impliqué dans la copie d’une valeur d’un emplacement de mémoire à un autre. Rappelez-vous que MISRA vise le développement de logiciels embarqués, la question est donc toujours de savoir quel type de processeurs géniaux existe pour faire quelque chose de spécial avec une copie.

Les normes MISRA concernent la robustesse, la fiabilité et l’élimination des risques de défaillance logicielle. Ils sont assez difficiles.

La valeur de p ne peut pas être utilisée en tant que telle après que la mémoire sur laquelle elle pointe a été libérée. Plus généralement, la valeur d’un pointeur non initialisé a le même statut: même s’il suffit de le lire pour le copier, il invoque un comportement indéfini.

La raison de cette ressortingction surprenante est la possibilité de représentations de pièges. Libérer la mémoire indiquée par p peut faire que sa valeur devienne une représentation de piège.

Je me souviens d’une de ces cibles, au début des années 90, qui se comportait de cette manière. Ce n’est pas une cible intégrée à l’époque et une utilisation répandue à ce moment-là: Windows 2.x. Il utilisait l’architecture Intel en mode protégé 16 bits, où les pointeurs avaient une largeur de 32 bits, avec un sélecteur 16 bits et un décalage de 16 bits. Pour accéder à la mémoire, les pointeurs ont été chargés dans une paire de registres (un registre de segment et un registre d’adresse) avec une instruction spécifique:

  LES BX,[BP+4] ; load pointer into ES:BX 

Le chargement de la partie sélecteur de la valeur du pointeur dans un registre de segment avait pour effet de valider la valeur du sélecteur: si le sélecteur ne pointait pas sur un segment mémoire valide, une exception serait déclenchée.

Compilation de la déclaration innocente q = p; pourrait être compilé de différentes façons:

  MOV AX,[BP+4] ; loading via DX:AX registers: no side effects MOV DX,[BP+6] MOV [BP-6],AX MOV [BP-4],DX 

ou

  LES BX,[BP+4] ; loading via ES:BX registers: side effects MOV [BP-6],BX MOV [BP-4],ES 

La deuxième option a 2 avantages:

  • Le code est plus compact, 1 moins d’instructions

  • La valeur du pointeur est chargée dans des registres pouvant être utilisés directement pour déréférencer la mémoire, ce qui peut entraîner moins d’instructions générées pour les instructions suivantes.

La libération de la mémoire peut dé-cartographier le segment et rendre le sélecteur non valide. La valeur devient une valeur d’interruption et son chargement dans ES:BX déclenche une exception, également appelée interruption sur certaines architectures.

Tous les compilateurs n’utilisent pas l’instruction LES pour copier uniquement les valeurs de pointeur car elle est plus lente, mais certains le font lorsqu’il est demandé de générer un code compact, un choix courant, car la mémoire est plutôt chère et rare.

La norme C permet cela et décrit une forme de comportement non défini le code où:

La valeur d’un pointeur sur un object dont la durée de vie est terminée est utilisée (6.2.4).

parce que cette valeur est devenue indéterminée telle que définie de la manière suivante:

3.19.2 valeur indéterminée: soit une valeur non spécifiée, soit une représentation de piège

Notez cependant que vous pouvez toujours manipuler la valeur en créant un alias via un type de caractère:

 /* dumping the value of the free'd pointer */ unsigned char *pc = (unsigned char*)&p; size_t i; for (i = 0; i < sizeof(p); i++) printf("%02X", pc[i]); /* no problem here */ /* copying the value of the free'd pointer */ memcpy(&q, &p, sizeof(p)); /* no problem either */ 

Le code qui examine un pointeur après l’avoir libéré est problématique même si le pointeur n’est jamais déréférencé:

  1. Les auteurs de la norme C ne souhaitaient pas interférer avec les implémentations du langage sur les plates-formes où les pointeurs contiennent des informations sur les blocs de mémoire environnants et qui pourraient valider ces points chaque fois que quelque chose est fait avec eux, qu’ils soient déréférencés ou non. Si de telles plates-formes existent, le code qui utilise des pointeurs en violation de la norme peut ne pas fonctionner avec elles.

  2. Certains compilateurs partent du principe qu’un programme ne recevra jamais une combinaison d’intrants pouvant appeler l’UB, de sorte que toute combinaison d’intrants produisant une UB devrait être présumée impossible. En conséquence, même des formes de UB qui n’auraient aucun effet préjudiciable sur la plate-forme cible si un compilateur les ignorait tout simplement peuvent avoir des effets secondaires arbitraires et illimités.

IMHO, il n’y a aucune raison pour que les opérateurs d’égalité, relationnels ou de différence de pointeur sur les pointeurs libérés aient un effet négatif sur un système moderne, mais parce qu’il est à la mode pour les compilateurs d’appliquer des “optimisations” folles, des constructions utiles qui devraient être utilisables sur les plateformes ordinaires sont devenues dangereuses.

Le libellé médiocre de l’exemple de code vous rejette.

Il dit “la valeur de p est indéterminée”, mais ce n’est pas la valeur de p qui est indéterminée, car p a toujours la même valeur (l’adresse d’un bloc de mémoire qui a été libérée).

L’appel de free (p) ne change pas p – p n’est modifié que lorsque vous quittez la scope dans laquelle p est défini.

Au lieu de cela, c’est la valeur de ce que p indique qui est indéterminée , puisque le bloc de mémoire a été libéré et qu’il peut également être non mappé par le système d’exploitation. L’access via p ou via un pointeur avec alias (q) peut provoquer une violation d’access.

Un concept important à intérioriser est la signification de comportement “indéterminé” ou “indéfini”. C’est exactement ça: inconnu et inconnaissable. Nous disions souvent aux étudiants: “Il est parfaitement légitime que votre ordinateur se transforme en une goutte informe, ou que le disque s’envole vers Mars”. En lisant la documentation originale incluse, je n’ai vu aucun endroit où il était dit de ne pas utiliser malloc. Cela indique simplement qu’un programme erroné échouera. En fait, le fait que le programme prenne une exception de mémoire est une bonne chose, car il vous indique immédiatement que votre programme est défectueux. Pourquoi le document suggère que cela pourrait être une mauvaise chose m’échappe. Ce qui est mal, c’est que sur la plupart des architectures, il ne faudra PAS d’exception de mémoire. Continuer à utiliser ce pointeur produira des valeurs erronées, rendra potentiellement le tas inutilisable et, si ce même bloc de stockage est alloué pour un usage différent, corrompre les données valides de cet usage ou interpréter ses valeurs comme les vôtres. En bout de ligne: n’utilisez pas de pointeurs “périmés”! Autrement dit, écrire un code défectueux signifie que cela ne fonctionnera pas.

De plus, le fait d’assigner p à q n’est PAS décidément «indéfini». Les bits stockés dans la variable p, qui n’ont pas de sens, sont facilement et correctement copiés dans q. Tout cela signifie maintenant que toute valeur accessible par p peut désormais aussi être consultée par q, et puisque p est un non-sens non défini, q est maintenant un non-sens non défini. Donc, utiliser l’un ou l’autre pour lire ou écrire produira des résultats “non définis”. Si vous avez la chance de pouvoir utiliser une architecture susceptible de provoquer une erreur de mémoire, vous pourrez facilement détecter l’utilisation incorrecte. Sinon, utiliser l’un ou l’autre pointeur signifie que votre programme est défectueux. Prévoyez de passer beaucoup d’heures à le trouver.