2
votes

Est-ce que lancer un pointeur vers intptr_t, faire de l'arithmétique dessus, puis restituer, est-il un comportement défini?

Disons que je veux déplacer un pointeur void * de 4 octets. Sont les équivalents suivants:

UNE:

void* new_address(void* in_ptr) {
    char* tmp = (char*)in_ptr;
    char* new_address = tmp + 4;
    return (void*)new_address;
}

B:

void* new_address(void* in_ptr) {
    intptr_t tmp = (intptr_t)in_ptr;
    intptr_t new_address = tmp + 4;
    return (void*)new_address;
}

Les deux comportements sont-ils définis? Une convention plus populaire / acceptée? Une autre raison d'utiliser l'un sur l'autre ?.
Considérons uniquement les systèmes 64 bits. Si intptr_t n'est pas disponible, nous pouvons utiliser int64_t à la place.

Le contexte est un allocateur de mémoire personnalisé qui doit déplacer le pointeur avant d'allouer un nouveau bloc de mémoire à une adresse spécifique (à des fins d'alignement). Nous ne savons pas encore sur quel objet le pointeur résultant va pointer, mais nous savons que nous devons le déplacer vers un emplacement spécifique qui, dans les exemples ci-dessus, fait 4 octets.


16 commentaires

Je préfère le second car il ne nécessite pas de moulages. Les moulages sont surutilisés et souvent mauvais. En termes de comportement parfaitement défini, je pense que vous êtes sur de la glace mince. Votre pointeur d'origine, une fois déballé du vide *, peut ne plus s'aligner correctement par exemple.


Que s'est-il passé lorsque vous les avez testés?


mevets, supposons que nous prenions soin de pointer vers des objets réels en mémoire. Martin James, les deux fonctionnent (compilés avec gcc) mais ce n'est pas parce que cela fonctionne maintenant sur un système spécifique avec un compilateur spécifique qu'il est à l'épreuve du temps.


Je ne suis pas sûr de la pertinence de mentionner les systèmes 64 bits ou int64_t . Supposons simplement que intptr_t est disponible.


Le codé en dur 4 est à l'épreuve du temps? Si la taille est connue, pourquoi ne pas travailler avec le type spécifique et utiliser l'arithmétique habituelle du pointeur?


new_address pointe-t- new_address dans le même objet que pnt ? Are both defined behavior? Allez-vous accéder au pointeur ou faites-vous simplement de l'arithmétique dessus? Demandez-vous si l'accès au pointeur résultant est défini, ou simplement si la conversion est définie? Est- in_ptr que in_ptr est un pointeur valide?


@WeatherVane parce que nous ne savons pas encore sur quel type spécifique va pointer. J'ai ajouté un peu de contexte à la question. 4 est codé en dur pour éviter une logique qui n'est pas pertinente pour la question.


Le standard C n'exige pas que la conversion de void * en intptr_t et intptr_t soit une conversion raisonnable, octet par octet, monotone, donc toute attente sur le résultat suivant l'ajout serait vos propres attentes, non sanctionnées par le standard C .


Mais puisque vous avez tagué gcc , vous pourriez probablement supposer que la conversion se comporte au moins de manière raisonnable pour GCC.


Concernant GCC: Le comportement défini par l'implémentation documenté pour les tableaux et les pointeurs dit: "Lors de la conversion d'un pointeur à un entier et inversement, le pointeur résultant doit référencer le même objet que le pointeur d'origine, sinon le comportement n'est pas défini. utiliser l'arithmétique entière pour éviter le comportement indéfini de l'arithmétique des pointeurs comme proscrit dans C99 et C11 6.5.6 / 8. ".


@IanAbbott cela semble répondre à une partie de ma question (faire de l'arithmétique sur intptr et renvoyer est UB). Et le faire via char *? L'allocateur de mémoire est un cas, un autre auquel je peux penser est de lire un flux d'octets et de les interpréter comme divers objets. Disons que nous avons un float, un int et une structure stockés les uns après les autres, puis-je faire de l'arithmétique sur char *, puis transtyper en void * puis en objet le temps de lire les objets? Est-ce un comportement défini?


Je pense que les règles qui régissent le type effectif d'un objet permettent de traiter les objets comme des tableaux de type caractère, et vice versa tant que les objets sont correctement alignés. Si l'objet est incorporé dans un flux d'octets dans un objet alloué, il n'a aucun type effectif autre qu'un type de caractère jusqu'à ce qu'il soit accédé par une expression lvalue qui n'a pas de type de caractère, auquel point il a le même type effectif comme expression lvalue (c'est-à-dire du même type que le type de pointeur déférencé utilisé pour y accéder). C11 6.5 / 6-7 a les détails sanglants.


C'est un peu inutile de discuter sans savoir où ces pointeurs pointent au départ, avant de les transformer en pointeurs vides.


@Lundin ils pointent dans un grand espace vide de mémoire allouée (le résultat de malloc) et sont utilisés pour pointer vers l'endroit où les objets de différents types vont être stockés.


Ok alors vous n'aurez aucun souci d'alignement et d'aliasing de pointeur etc. Ce qui signifie que les deux versions fonctionneront très bien sur n'importe quel compilateur connu.


@IanAbbott: Les règles sur les types effectifs et les alias n'ont jamais été écrites de manière à décrire sans ambiguïté tous les cas de coin, car il n'y a jamais eu de consensus sur la façon dont ces cas de coin devraient être traités . Si la Norme avait explicitement déclaré que le soutien (ou son absence) pour de nombreux cas secondaires est un problème de qualité de la mise en œuvre sur lequel il renonce à sa compétence, cela aurait attiré l'attention sur ce que les implémentations de qualité à diverses fins devraient soutenir, plutôt que de prétendre que toutes les constructions pour lesquelles le support n'est pas obligatoire sont "cassées".


3 Réponses :


2
votes

Michael Kerrisk dit à la page 1415 que,

Les standards C font une exception à la règle selon laquelle les pointeurs de types différents n'ont pas besoin d'avoir la même représentation: les pointeurs des types char * et void * doivent avoir la même représentation interne.

Toutes les garanties de la norme C (7.18.1.4) sont que vous pouvez convertir les valeurs void* en intptr_t (ou uintptr_t ) et uintptr_t et obtenir une valeur égale pour le pointeur.

La nuance est ici que nous ne pouvons pas appliquer d'opérations mathématiques (y compris == ) si void* est utilisé.


5 commentaires

Je ne pense pas que la norme C offre une telle garantie. Nomme-t-il un chapitre exact?


@Lundin oui monsieur, il en parle à différentes parties du livre. Cependant, la partie est extraite de l'annexe C. Vous pouvez rechercher certains mots du texte dans le livre pour les localiser.


@Lundin, que diriez-vous de C17, paragraphe 6.2.5 / 28: "un pointeur vers void doit avoir les mêmes exigences de représentation et d'alignement qu'un pointeur vers un type de caractère." Une note de bas de page ajoute que "les mêmes exigences de représentation et d'alignement sont censées impliquer l'interchangeabilité en tant qu'arguments aux fonctions, valeurs de retour de fonctions et membres d'unions." Un langage similaire apparaît dans les versions précédentes de la norme.


@snr, les pointeurs vers void peuvent être comparés pour une (in) égalité. En fait, cela est très courant sous la forme de if (p == NULL) , bien que cela ne se limite pas aux comparaisons avec des constantes de pointeur nulles. Pour autant que je sache, les pointeurs vers void peuvent également être des opérandes d'opérateurs relationnels, soumis à la même sémantique que les pointeurs vers d'autres types. Ce que vous ne pouvez pas faire avec les pointeurs vers void (ou vers tout autre type incomplet) est pointeur + addition d'entier ou pointeur - pointeur de différence.


@JohnBollinger Merci, c'est réglé. Je ne me souvenais pas l'avoir lu dans la norme avant.



2
votes

Est-ce que le cast d'un pointeur vers le comportement [...] défini par intptr_t?

La conversion d'un pointeur en n'importe quel type entier est définie et le résultat est défini par l'implémentation, sauf lorsque le résultat ne peut pas être représenté en type entier, alors son comportement n'est pas défini. Voir C11 6.3.2.3p6 . Mais intptr_t doit pouvoir représenter void* - le comportement est défini.

, faire de l'arithmétique dessus puis relancer, définir le comportement?

Tout entier peut être converti en n'importe quel type de pointeur. Le pointeur résultant est défini par l'implémentation - il n'y a aucune garantie que l'ajout de 4 à intptr_t incrémentera la valeur du pointeur de 4 . Voir C11 6.3.2.3p5 .

Les deux comportements sont-ils définis?

Oui, mais le résultat est la mise en œuvre définie.

Une convention plus populaire / acceptée?

Subjective: Je dis que l'utilisation de uintptr_t est plus populaire que intptr_t . La conversion d'un pointeur en uintptr_t ou en char* pour faire de l'arithmétique se produit dans certains codes, je ne peux pas dire ce qui est le plus populaire.

Une autre raison d'utiliser l'un sur l'autre ?.

Pas vraiment, mais je pense aller avec char* .

Quand il s'agit d' accéder réellement aux données derrière le pointeur résultant, cela dépend. Si le pointeur résultant pointe dans le même objet, tout va bien (rappelez-vous que la conversion est définie par l'implémentation). Si le pointeur résultant ne pointe pas vers le même objet, je pense que la meilleure interprétation serait de lire c2263 Clarifying Pointer Provenance v4 2.2.3Q5 et je pense que c'est: la norme C11 actuelle ne spécifie pas clairement cela, ce qui rendrait le comportement non défini.

Parce que vous avez balisé gcc , les deux extraits de code doivent être compilés en code équivalent - je crois que sur toutes les architectures, les pointeurs sont convertis 1: 1 en (u)intptr_t sur gcc. Gcc docs implémentation comportement défini 4.7 tableaux et pointeurs états casting from pointer to integer and back again, the resulting pointer must reference the same object as the original pointer, otherwise the behavior is undefined - vous êtes donc en sécurité tant que le pointeur résultant pointe au même objet.

Le contexte est un allocateur de mémoire personnalisé

Voir les implémentations des macros container_of et offsetof . Ne codez pas en dur + 4 dans votre code, et si vous le faites, ne memcpy pas des exigences d'alignement pour accéder aux pointeurs résultants - n'oubliez pas d'utiliser memcpy pour copier en toute sécurité le contexte ou gérer correctement l'alignement. Ne réinventez pas la roue - en cas de doute, voyez d'autres implémentations comme glibc malloc.c ou newlib malloc.c - elles calculent toutes les deux sur char* dans la macro mem2chunk , mais font également des calculs sur les entiers uintptr_t .


1 commentaires

Cependant, intptr_t est facultatif . il n'est pas garanti qu'une implémentation fournisse un type entier qui satisfait aux exigences pour ce type, et sinon, intptr_t ne sera pas défini.



2
votes

Aucun 'programme strictement conforme n'utilise A. L'utilisation du résultat peut être un comportement intptr_t car il n'est pas nécessaire que l'addition contre intptr_t soit reflétée dans une valeur de pointeur si cet intptr_ est reconverti en pointeur. C'est à la fois un comportement non spécifié et défini par l'implémentation.

Si le type optionnel intptr_t est défini, tout ce que vous avez la garantie est que vous pouvez convertir void * en intptr_t , puis reconvertir cette valeur en void * et les deux valeurs se compareront égales (==).

La manière strictly conforming d'effectuer l'arithmétique du pointeur est int_ptr est garanti de fonctionner si et seulement si le pointeur int_ptr est valide et pour le plus grand objet englobant, il y a 3 octets ou plus dans cet objet au-delà de cette valeur. C'est 3 parce qu'il est valide de pointer (mais pas de déréférencer) l'adresse qui est (logiquement) un octet au-delà de la fin d'un objet.

Object inclut un objet déclaré (y compris un tableau) ou un bloc de mémoire tel que renvoyé par malloc() .

Toute bonne pratique consiste à préférer écrire des programmes «strictement conformes» lorsque cela est possible. Toute bonne pratique consiste donc à préférer B à A.

Selon la norme, l'utilisation du pointeur (comme pointeur) peut entraîner un comportement indéfini car il peut être (défini par l'implémentation) une représentation d'interruption.

Un programme strictement conforme est défini comme suit: «Un programme strictement conforme ne doit utiliser que les caractéristiques du langage et de la bibliothèque spécifiés dans la présente Norme internationale 3) Il ne doit pas produire de sortie dépendant d'un comportement non spécifié, non défini ou défini par l'implémentation, et doit ne dépassez aucune limite minimale de mise en œuvre.

Il y a un certain désaccord quant à savoir si le code proposé pour A n'est pas spécifié ou si l'implémentation est définie. La norme dit les deux parce que le comportement défini par l'implémentation est une sous-catégorie de non spécifié. Cependant, parce que l'implémentation peut le documenter comme une représentation d'interruption à l'aide de la valeur, cela peut entraîner un comportement indéfini.

Mais j'espère que cela est balayé par le fait que les «programmes strictement conformes» ne dépendent pas d'un comportement non spécifié, non défini ou défini par l'implémentation. Donc, la bonne pratique ici est certainement B.

Envisagez un environnement sécurisé qui crypte les valeurs de pointeur pour confondre délibérément le déréférencement de valeurs de pointeur arbitraires. En principe, il pourrait fournir intptr_t et être conforme.

Bien que je maintienne toujours que si A ne fonctionne pas, alors intptr_t étant un type optionnel, il serait préférable de ne pas le fournir. Le fait qu'il soit défini n'est pas spécifié et dépend de la mise en œuvre. C'est parce qu'aucun 'programme strictement conforme' ne l'utilise et il n'a d'autre utilité pratique que de manipuler un pointeur comme type arithmétique d'une manière non prise en charge par l'arithmétique de pointeur sur un type de pointeur compatible char * . L'extrait de A appartient à cette catégorie. Pour stocker un void * déclarez un void * ou char [sizeof (void *)] ou malloc() ou similaire. Pour superposer un void * sur un type arithmétique, déclarez une union et bénéficiez que l' union sera alignée pour un void * . Mais selon la spécification, il n'est pas spécifié, défini par l'implémentation, aucun «programme strictement conforme» ne peut s'appuyer dessus et peut entraîner un comportement indéfini.

Une très longue façon de dire la réponse, ici, est B.


18 commentaires

Je pense qu'il manque un mot dans votre réponse et j'ai du mal à le comprendre. "B est garanti de fonctionner si et seulement si le plus grand objet englobant ...?". Le pointeur dans mon cas indique une mémoire vide et sera utilisé pour y stocker divers objets. Est-ce défini si je fais cela avec char *? (tant que je m'occupe de l'alignement)


@PiotrLopusiewicz J'ai lié le libellé. Un bloc de mémoire renvoyé par malloc() est également un objet. L'arithmétique du pointeur est bien définie (dans cette contrainte). Notez que malloc() renvoie une valeur alignée pour stocker n'importe quelle variable. En C11, nous avons aligned_alloc() . À moins que vous n'allouiez un objet mixte, vous pouvez généralement éviter de convertir en char* et d'ajouter un nombre d'octets. Je suppose que le 4 que vous ajoutez est sizeof() quelque chose comme int . Le truc char* +4 est rarement nécessaire, mais il est bien défini. et fait ce que vous voulez.


Ce n'est pas nécessairement un comportement indéfini, mais un comportement défini implicitement. 6.3.2.3/5. Vous n'obtenez UB qu'en cas de mauvais alignement ou de formats de pointeurs invalides. Sur la plupart des systèmes du monde réel, le code publié fonctionnera très bien.


@Lundin C'est certainement un comportement indéfini par la définition C du comportement indéfini "comportement, lors de l'utilisation d'une construction de programme non portable ou erronée ou de données erronées, pour lequel la présente Norme internationale n'impose aucune exigence". La norme n'impose aucune exigence concernant l'arithmétique intptr_t en termes de conversion de la résultante en void * . Je mentionne «dans la norme C» parce que oui, sur la plupart des plates-formes du monde réel, l'arithmétique intptr_t équivaut à une arithmétique de pointeur. Il n'y a certainement aucun intérêt dans ce cas. char* pointer est la voie à suivre pour le but du PO.


The standard imposes no requirement regarding intptr_t arithmetic in terms of casting resultant back to void * Elle fait: C11 6.3.2.3p5 : An integer may be converted to any pointer type . The standard imposes no requirement regarding intptr_t arithmetic in terms of casting resultant back to void * mais elle le fait - elle impose que le résultat soit défini par l'implémentation, l'implémentation doit donc implémenter et documenter un comportement.


@Persixty, le comportement que la norme spécifie comme étant défini par l'implémentation ne répond pas à cette définition. La norme impose des exigences à un tel comportement: qu'il soit conforme à la documentation (requise) de l'implémentation C dans laquelle il est exercé.


Fair-play. Un comportement non portable non spécifié et le déréférencement de la valeur peuvent entraîner un comportement non défini. C'est une belle distinction mais techniquement correcte. Le meilleur type de correct.


La norme comporte trois termes formels et bien définis: comportement non défini, comportement non spécifié et comportement défini par l'implémentation. Voir Qu'est-ce qu'un comportement non défini et comment fonctionne-t-il? . Il n'y a rien appelé "comportement non portable non spécifié" dans C.


Mais quel choix est fait est la mise en œuvre définie. Et il n'y a aucun moyen utile de coder pour eux. Par des termes C qui sont non spécifiés «comportement non spécifié où chaque implémentation documente comment le choix est fait». Notez le choix du mot. Avoir une liste d'options fixe n'est toujours pas spécifié - non spécifié. Mais il n'y a rien de mal avec unspeicifed, L'ordre de contigunité des valeurs de retour de malloc n'est pas spécifié. Mais on s'en fiche. Quelle phrase voulez-vous pour certains qui, faute de précision, ne peuvent pas être utilisés de manière portable?


@Lundin. Voir aussi la note de bas de page «Les fonctions de mappage pour convertir un pointeur en entier ou un entier en pointeur sont destinées à être cohérentes avec la structure d'adressage de l'environnement d'exécution.». J'ai du mal à penser à quelque chose de moins défini et de moins portable. Je vois le point fin que la valeur peut être placée dans un pointeur. Mais son utilisation est (bien entendu) indéfinie.


@Lundin N'oubliez pas qu'il n'est pas spécifié (par les termes de la norme) si `intptr_t 'est déclaré parce que c'est optionnel et que l'implémentation` `fait un choix' 'et donc son utilisation n'est pas portable (pour les environnements qui font le choix de l'omettre). Il n'est pas spécifié et n'est pas portable.


Dans cet exemple spécifique. Comportement non défini = si la conversion de l'adresse modifiée en un pointeur entraîne un désalignement ou une représentation d'interruption de pointeur, un crash ou similaire. Comportement non spécifié = le compilateur a un comportement déterministe quant à la façon dont il traite les adresses sur ce système donné, mais il n'est documenté nulle part. Définition de l'implémentation = le compilateur a un comportement déterministe pour les adresses sur ce système et les exigences d'alignement, d'adressage, etc. du cœur du processeur sont bien connues et documentées. Ainsi: il s'agit d'un comportement défini par l'implémentation sur tous les ordinateurs du monde réel.


@Lundin "comportement non spécifié où chaque implémentation documente comment le choix est fait". Le comportement défini par l'implémentation est une sous-catégorie de comportement non spécifié. Si vous avez raison, j'ai raison, appelez-le sans précision. Je suis d'accord avec le «monde réel» et la note «Les fonctions de mappage pour convertir un pointeur en entier ou un entier en pointeur sont destinées à être cohérentes avec la structure d'adressage de l'environnement d'exécution» implique que A est destiné à fonctionner. Mais ce n'est pas portable.


@Lundin Je dirais que la spécification devrait être plus difficile et dire que si intptr_t est fourni, elle doit refléter la structure d'adressage (dans le sens où A équivaut à l'arithmétique du pointeur). C'est parce que si ce n'est pas le cas, je n'en connais aucune utilité pratique. Mais dans l'état actuel des choses, il n'est pas spécifié et le faire est (en principe) non portable.


@Lundin J'ai formulé autour du débat parce que A n'est pas «strictement conforme», B est et montre toujours être préféré (dans ce cas).


@Persixty: En effet, je ne vois pas grand-chose à servir en permettant aux implémentations de définir les types uintptr_t et intptr_t sans avoir rien garanti sur leur sémantique au-delà du fait qu'un pointeur à allers-retours est égal au pointeur d'origine. La norme permet la possibilité qu'un pointeur juste après la fin d'un objet se compare égal à un pointeur vers un autre objet qui le suit immédiatement, même si chaque objet ne peut être accessible (sans UB) qu'en utilisant son propre pointeur. Ainsi, une implémentation conforme mais capricieuse pourrait décider ...


... que si &y est égal à (&x)+1 , ce int *p = (int*)((uintptr_t)&y) mettrait p à (&x)+1 si le code en aval essaie d'accéder à *p [provoquant ainsi tel accès pour invoquer UB] ou &y si le code en aval tente d'accéder à p[-1] [invoquant ainsi UB dans ce cas]. Bien sûr, de tels problèmes pourraient être résolus si la norme indiquait clairement que dans les cas où des parties de la norme et la documentation d'une mise en œuvre décriraient un comportement, mais d'autres parties disent que ce n'est pas nécessaire, la question de savoir s'il faut se comporter de manière utile est une qualité de mise en œuvre. problème hors de la compétence de la norme.


D'accord. Je pense toujours que la qualité de la norme pourrait être améliorée en la rendant explicitement mise en œuvre définie si si uintptr_t uip=(void*)p alors v=uip+n sera converti en un pointeur avec la valeur ps=(char*)p+n si ps cette valeur est un pointeur valide. Et de plus pour quelles valeurs de a , si ps est un pointeur valide que si v%a == 0 ps doit avoir l'alignement de a . C'est clair (ici et ailleurs) que c'est ce que les développeurs veulent et attendent même de uintptr_t . Remarque: les valeurs valides de a peuvent différer de celles valides pour aligned_alloc() .