La norme C permet-elle d'affecter une valeur arbitraire à un pointeur et de l'incrémenter?




pointers language-lawyer (4)

Le comportement de ce code est-il bien défini?

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    void *ptr = (char *)0x01;
    size_t val;

    ptr = (char *)ptr + 1;
    val = (size_t)(uintptr_t)ptr;

    printf("%zu\n", val);
    return 0;
}

Je veux dire, pouvons-nous attribuer un nombre fixe à un pointeur et l'incrémenter même s'il pointe vers une adresse quelconque? (Je sais que vous ne pouvez pas le déréférencer)


La norme n'exige pas que les implémentations traitent de manière significative les conversions d'entier en pointeur pour des valeurs entières particulières, ni même pour des valeurs entières possibles autres que les constantes de pointeur nul. La seule chose garantie par de telles conversions est qu’un programme qui stocke le résultat d’une telle conversion directement dans un objet de type pointeur approprié et ne fait rien avec celui-ci, sauf examiner les octets de cet objet, verra au pire des valeurs non spécifiées. Bien que le comportement de conversion d'un entier en pointeur soit défini par Implémentation, rien n'empêcherait aucune implémentation (peu importe ce qu'il fait réellement avec de telles conversions!) De spécifier que certains (voire tous) des octets de la représentation ayant des valeurs non spécifiées et en spécifiant que certaines valeurs entières (ou même toutes) peuvent se comporter comme si elles produisaient des représentations de piège.

Les seules raisons pour lesquelles la norme dit quoi que ce soit à propos des conversions d'entier en pointeur sont les suivantes:

  1. Dans certaines mises en œuvre, le concept est significatif et certains programmes pour ces mises en œuvre le nécessitent.

  2. Les auteurs de la norme n'aimaient pas l'idée qu'une construction utilisée sur certaines implémentations représenterait une violation de contrainte sur d'autres.

  3. Il aurait été étrange que la norme décrive une construction, mais précise ensuite qu'elle comporte un comportement non défini dans tous les cas.

Personnellement, j'estime que la norme aurait dû autoriser les implémentations à traiter les conversions d'entier en pointeur comme des violations de contrainte si elles ne définissaient aucune situation dans laquelle elles seraient utiles, plutôt que d'exiger que les compilateurs acceptent le code sans signification, mais ce n'était pas le cas. la philosophie à l'époque.

Je pense qu'il serait plus simple de dire simplement que toute opération impliquant des conversions d'entier à pointeur avec autre chose que les valeurs intptr_t ou uintptr_t reçues lors de conversions pointeur à entier appelle le comportement indéfini, mais notez qu'il s'agit d'un comportement commun pour les implémentations de qualité. pour la programmation de bas niveau de traiter le comportement indéfini "de manière documentée caractéristique de l'environnement". La norme ne spécifie pas quand les implémentations doivent traiter les programmes qui appellent UB de cette manière, mais la considère plutôt comme un problème de qualité d'implémentation.

Si une implémentation spécifie que les conversions entier en pointeur fonctionnent de manière à définir le comportement de

char *p = (char*)1;
p++;

comme équivalent à "char p = (char ) 2;", alors la mise en œuvre devrait fonctionner de cette façon. D'autre part, une implémentation pourrait définir le comportement de la conversion d'entier en pointeur de telle sorte que même:

char *p = (char*)1;
char *q = p;  // Not doing any arithmetic here--just a simple assignment

libérerait des démons nasaux. Sur la plupart des plates-formes, un compilateur dans lequel l'arithmétique sur les pointeurs produits par des conversions d'entier en pointeur se comportaient de manière étrange ne serait pas considéré comme une mise en œuvre de haute qualité adaptée à la programmation de bas niveau. Un programmeur qui n'a pas l'intention de cibler un autre type d'implémentations pourrait donc s'attendre à ce que ces constructions se comportent de manière utile sur les compilateurs pour lesquels le code convient, même si la norme ne l'exige pas.


Oui, le code est bien défini comme défini par l'implémentation. Ce n'est pas indéfini. Voir ISO / IEC 9899: 2011 [6.3.2.3] / 5 et la note 67.

Le langage C a été créé à l'origine comme langage de programmation système. La programmation système nécessitait de manipuler du matériel mappé en mémoire, de fourrer des adresses codées en dur dans des pointeurs, de les incrémenter parfois, ainsi que de lire et d’écrire des données depuis et vers l’adresse résultante. À cette fin, l'attribution d'un nombre entier à un pointeur et la manipulation de ce pointeur à l'aide de l'arithmétique sont bien définies par le langage. En le définissant comme une implémentation, le langage le permet, mais toutes sortes de choses peuvent se produire: du classique stop-and-catch-fire au soulèvement d'une erreur de bus lorsque vous essayez de déréférencer une adresse étrange.

La différence entre un comportement indéfini et un comportement défini par l'implémentation réside essentiellement dans le fait qu'un comportement signifie "ne le faites pas, nous ne savons pas ce qui va arriver" et un comportement défini par l'implémentation signifie "il est correct d'aller de l'avant et de le faire, c'est à vous de le faire." vous savez ce qui va arriver. "


Non, le comportement de ce programme n'est pas défini. Une fois qu'une construction non définie est atteinte dans un programme, tout comportement futur est indéfini. Paradoxalement, tout comportement passé est également indéfini.

Le résultat de void *ptr = (char*)0x01; est défini par la mise en œuvre, en partie du fait qu'un caractère peut avoir une représentation piège.

Mais le comportement de l'arithmétique de pointeur qui s'ensuit dans l'instruction ptr = (char *)ptr + 1; est indéfini . En effet, l'arithmétique de pointeur n'est valide que dans les tableaux, dont un au-delà de la fin du tableau. Pour cela, un objet est un tableau de longueur un.


C'est un comportement indéfini.

À partir de N1570 (soulignement ajouté):

Un entier peut être converti en n'importe quel type de pointeur. Sauf indication contraire, le résultat est défini par l'implémentation, peut ne pas être correctement aligné, peut ne pas pointer vers une entité du type référencé et peut être une représentation de piège.

Si la valeur est une représentation d'interruption, la lire est un comportement indéfini:

Certaines représentations d'objet ne doivent pas nécessairement représenter une valeur du type d'objet. Si la valeur stockée d'un objet a une telle représentation et est lue par une expression lvalue qui n'a pas de type de caractère, le comportement est indéfini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l'objet par une expression lvalue qui n'a pas de type de caractère, le comportement est indéfini. ) Une telle représentation est appelée une représentation de piège.

Et

Un identifiant est une expression primaire, à condition qu'il ait été déclaré comme désignant un objet (dans ce cas, il s'agisse d'une valeur ) ou une fonction (dans ce cas, il s'agit d'un indicateur de fonction).

Par conséquent, la ligne void *ptr = (char *)0x01; est déjà potentiellement comportement indéfini, sur une implémentation où (char*)0x01 ou (void*)(char*)0x01 est une représentation de piège. Le côté gauche est une expression lvalue qui n'a pas de type de caractère et lit une représentation d'interruption.

Sur certains matériels, le chargement d’un pointeur non valide dans un registre d’ordinateur risquait de bloquer le programme. Il s’agissait donc d’un déménagement forcé du comité des normes.





pointer-arithmetic