web-dev-qa-db-fra.com

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

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)

52
David Ranieri

La tâche:

void *ptr = (char *)0x01;

Est le comportement défini par la mise en œuvre car il convertit un entier en pointeur. Ceci est détaillé dans la section 6.3.2.3 du norme C concernant les pointeurs:

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

En ce qui concerne l'arithmétique de pointeur suivante:

ptr = (char *)ptr + 1;

Cela dépend de quelques choses.

Tout d'abord, la valeur actuelle de ptr peut est une représentation de piège selon 6.3.2.3 ci-dessus. Si c'est le cas, le comportement est non défini .

Vient ensuite la question de savoir si 0x1 pointe sur un objet valide. L'ajout d'un pointeur et d'un entier n'est valide que si l'opérande du pointeur et le résultat pointent vers des éléments d'un objet tableau (un seul objet compte comme un tableau de taille 1) ou un élément situé après l'objet tableau. Ceci est détaillé dans la section 6.5.6:

7 Pour les besoins de ces opérateurs, un pointeur sur un objet qui n'est pas un élément d'un tableau se comporte de la même façon qu'un pointeur sur le premier élément d'un tableau de longueur un avec le type l'objet en tant que type d'élément

8 Lorsqu'une expression de type entier est ajoutée ou soustraite à un pointeur, le résultat a le type de l'opérande de pointeur. Si l'opérande de pointeur pointe sur un élément d'un objet tableau et que le tableau est suffisamment grand, le résultat pointe sur un élément décalé par rapport à l'élément d'origine, de sorte que la différence entre les indices des éléments de tableau résultant et original est égale à l'expression entière. En d'autres termes, si l'expression [~ # ~] p [~ # ~] pointe vers le i-ème élément d'un objet tableau, les expressions (P) + N (de manière équivalente, N + (P) ) et (P) -N (où [~ # ~] n [~ # ~] a la valeur n) point sur, respectivement, les i + n-ème et i-n-ème éléments de l'objet tableau, à condition qu'ils existent. De plus, si l'expression P pointe sur le dernier élément d'un objet tableau, l'expression (P) +1 pointe un après le dernier élément du tableau. objet, et si l'expression [~ # ~] q [~ # ~] pointe un après le dernier élément d'un objet tableau, l'expression (Q) -1 pointe vers le dernier élément de l'objet tableau. Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet de tableau, ou sur un élément situé après le dernier élément de l'objet de tableau, l'évaluation ne doit pas produire de dépassement de capacité; sinon, le comportement n'est pas défini. Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un opérateur unaire * évalué.

Sur une implémentation hébergée, la valeur 0x1 presque certainement pas pointe sur un objet valide, auquel cas l'ajout est non défini . Une implémentation intégrée pourrait toutefois supporter la définition de pointeurs sur des valeurs spécifiques. Dans ce cas, il est possible que 0x1 pointe en fait sur un objet valide. Si tel est le cas, le comportement est bien défini , sinon il est non défini .

68
dbush

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 l'implémentation, en partie du fait qu'un char 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 non dé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.

18
Bathsheba

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é, il peut ne pas pointer vers une entité du type référencé, et peut être une représentation d'interruption.

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 n'est pas défini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou toute partie de l'objet par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini.) Une telle représentation s'appelle une représentation d'interruption.

Et

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

Par conséquent, la ligne void *ptr = (char *)0x01; est déjà un comportement potentiellement 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 de machine risquait de faire planter le programme. Il s'agissait donc d'un déplacement forcé du comité des normes.

8
Davislor

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, d'intégrer des adresses codées en dur dans des pointeurs, de les incrémenter parfois, ainsi que de lire et d'écrire des données à partir de 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 est essentiellement un comportement indéfini 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. "

8
Stephen M. Webb

La norme n'exige pas que les implémentations traitent les conversions d'entier en pointeur de manière significative 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 les valeurs non spécifiées. Bien que la conversion d’un nombre entier en un pointeur soit définie par Implementation, rien n’interdirait à any implementation (peu importe ce qu’il fait réellement avec de telles conversions!) De spécifier que certains (ou même tous) des octets de la représentation ayant des valeurs non spécifiées et indiquant que certaines valeurs entières (voire toutes) peuvent se comporter comme si elles produisaient des représentations d'interruption.

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 de 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 oeuvre devrait fonctionner de cette manière. 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 générés par des conversions d'entier en pointeur se comportaient de manière étrange ne serait pas considéré comme une implémentation 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.

3
supercat