C - Comportement de conversion entre deux pointeurs

Dec 11 2020

Mise à jour 11/12/2020: Merci @ "Some programmeur mec" pour la suggestion dans le commentaire. Mon problème sous-jacent est que notre équipe met en œuvre un moteur de stockage de type dynamique. Nous allouons plusieurs tampons char array [PAGE_SIZE] avec 16-alignés pour stocker les types dynamiques de données (il n'y a pas de structure fixe). Pour des raisons d'efficacité, nous ne pouvons pas effectuer de codage d'octets ou allouer de l'espace supplémentaire à utiliser memcpy.

Puisque l'alignement a été déterminé (c'est-à-dire 16), le reste consiste à utiliser la distribution du pointeur pour accéder aux objets du type spécifié, par exemple:

int main() {
    // simulate our 16-aligned malloc
    _Alignas(16) char buf[4096];

    // store some dynamic data:
    *((unsigned long *) buf) = 0xff07;
    *(((double *) buf) + 2) = 1.618;
}

Mais notre équipe conteste si cette opération est un comportement indéfini.


J'ai lu de nombreuses questions similaires, telles que

  • Pourquoi -Wcast-align ne prévient-il pas de la conversion de char * en int * sur x86?
  • Comment convertir un tableau de caractères en int à une position non alignée?
  • C comportement indéfini. Règle d'aliasing stricte ou alignement incorrect?
  • SEI CERT C CS EXP36-C

Mais ce sont différents de mon interprétation de la norme C, je veux savoir si c'est mon malentendu.

La principale confusion concerne la section 6.3.2.3 # 7 de C11:

Un pointeur vers un type d'objet peut être converti en un pointeur vers un type d'objet différent. Si le pointeur résultant n'est pas correctement aligné 68) pour le type référencé, le comportement n'est pas défini.

68) En général, le concept `` correctement aligné '' est transitif: si un pointeur vers le type A est correctement aligné pour un pointeur vers le type B, qui à son tour est correctement aligné pour un pointeur vers le type C, alors un pointeur vers le type A est correctement aligné pour un pointeur sur le type C.

Le pointeur résultant fait-il ici référence à l' objet pointeur ou à la valeur du pointeur ?

À mon avis, je pense que la réponse est l' objet pointeur , mais plus de réponses semblent indiquer la valeur du pointeur .


Interprétation A: objet pointeur

Mes pensées sont les suivantes: un pointeur lui-même est un objet. Selon 6.2.5 # 28 , différents pointeurs peuvent avoir des exigences de représentation et d'alignement différentes. Par conséquent, selon 6.3.2.3 # 7 , tant que deux pointeurs ont le même alignement, ils peuvent être convertis en toute sécurité sans comportement indéfini, mais il n'y a aucune garantie qu'ils puissent être déréférencés. Exprimez cette idée dans un programme:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    if (_Alignof(char *) == _Alignof(int *)) {
        // cast safely, because they have the same alignment requirement?
        int *pi = (int *) pc; 
        printf("pi: %p\n", pi);
    } else {
        printf("char * and int * don't have the same alignment.\n");
    }
}

Interprétation B: valeur du pointeur

Cependant, si la norme C11 parle de valeur de pointeur pour le type référencé plutôt que d' objet pointeur . La vérification de l'alignement du code ci-dessus n'a aucun sens. Exprimez cette idée dans un programme:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    
    /*
     * undefined behavior, because:
     * align of char is 1
     * align of int is 4
     * 
     * and we don't know whether the `value` of pc is 4-aligned.
     */
    int *pi = (int *) pc;
    printf("pi: %p\n", pi);
}

Quelle interprétation est correcte?

Réponses

6 dbush Dec 11 2020 at 01:36

L'interprétation B est correcte. La norme parle d'un pointeur vers un objet, pas de l'objet lui-même. "Resulting pointer" fait référence au résultat de la conversion, et une conversion ne produit pas de lvalue, elle fait donc référence à la valeur du pointeur après la conversion.

Prendre le code dans votre exemple, supposons qu'un intdoit être aligné sur une limite de 4 octets, à savoir l' adresse de ce doit être un multiple de 4. Si l'adresse bufest 0x1001convertissait alors cette adresse à int *est invalide parce que la valeur du pointeur est pas correctement aligné. Si l'adresse de bufest en cours de 0x1000conversion, elle int *est valide.

Mettre à jour:

Le code que vous avez ajouté résout le problème d'alignement, donc c'est bien à cet égard. Il a cependant un problème différent: il viole l'aliasing strict.

Le tableau que vous avez défini contient des objets de type char. En convertissant l'adresse en un type différent, puis en déréférençant le type de type converti, vous accédez aux objets d'un type en tant qu'objets d'un autre type. Cela n'est pas autorisé par la norme C.

Bien que le terme «aliasing strict» ne soit pas utilisé dans la norme, le concept est décrit dans les paragraphes 6 et 7 de la section 6.5:

6 Le type effectif d'un objet pour un accès à sa valeur stockée est le type déclaré de l'objet, le cas échéant. 87) Si une valeur est stockée dans un objet n'ayant pas de type déclaré via une lvalue ayant un type qui n'est pas un type de caractère, alors le type de la lvalue devient le type effectif de l'objet pour cet accès et pour les accès ultérieurs qui ne le sont pas modifier la valeur stockée. Si une valeur est copiée dans un objet n'ayant pas de type déclaré en utilisant memcpyou memmove, ou est copiée en tant que tableau de type caractère, alors le type effectif de l'objet modifié pour cet accès et pour les accès ultérieurs qui ne modifient pas la valeur est le type effectif de l'objet à partir duquel la valeur est copiée, le cas échéant. Pour tous les autres accès à un objet n'ayant pas de type déclaré, le type effectif de l'objet est simplement le type de la lvalue utilisée pour l'accès.

7 Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants: 88)

  • un type compatible avec le type effectif de l'objet,
  • une version qualifiée d'un type compatible avec le type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
  • un type d'agrégat ou d'union qui comprend l'un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou d'un syndicat contenu), ou
  • un type de caractère.

...

87) Les objets alloués n'ont pas de type déclaré.

88) Le but de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou non être aliasé.

Dans votre exemple, vous écrivez un unsigned longet un doubleau-dessus des charobjets. Aucun de ces types ne satisfait aux conditions du paragraphe 7.

En plus de cela, l'arithmétique du pointeur ici n'est pas valide:

 *(((double *) buf) + 2) = 1.618;

Comme vous traitez bufcomme un tableau de doublequand ce n'est pas le cas. À tout le moins, vous auriez besoin d'effectuer l'arithmétique nécessaire bufdirectement et de convertir le résultat à la fin.

Alors, pourquoi est-ce un problème pour un chartableau et non un tampon retourné par malloc? Parce que la mémoire renvoyée mallocn'a pas de type efficace tant que vous n'y stockez pas quelque chose, c'est ce que décrivent le paragraphe 6 et la note de bas de page 87.

Donc, d'un point de vue strict de la norme, ce que vous faites est un comportement indéfini. Mais en fonction de votre compilateur, vous pourrez peut-être désactiver l'alias strict pour que cela fonctionne. Si vous utilisez gcc, vous voudrez passer le -fno-strict-aliasingdrapeau

1 supercat Dec 11 2020 at 05:09

La norme n'exige pas que les implémentations considèrent la possibilité que le code observe jamais une valeur dans a T*qui n'est pas alignée pour le type T. En clang, par exemple, lors du ciblage de plates-formes dont les instructions de chargement / stockage "plus grandes" ne prennent pas en charge l'accès non aligné, convertir un pointeur en un type dont il ne satisfait pas l'alignement et l'utiliser ensuite memcpypeut entraîner la génération de code par le compilateur qui échouera si le pointeur n'est pas aligné, même si memcpylui-même n'imposerait aucune exigence d'alignement.

Lorsque vous ciblez un ARM Cortex-M0 ou Cortex-M3, par exemple, étant donné:

void test1(long long *dest, long long *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
    *dest = *src;
}

clang générera à la fois du code test1 et test3 qui échouerait s'il était aligné srcou destnon, mais pour test2cela, il générera du code qui est plus grand et plus lent, mais qui supportera l'alignement arbitraire des opérandes source et destination.

Pour être sûr, même en cas de bruit, l'acte de convertir un pointeur non aligné en un long long*ne provoquera généralement rien de bizarre par lui-même, mais c'est le fait qu'une telle conversion produirait UB qui exempte le compilateur de toute responsabilité de gérer le cas du pointeur non aligné dans test1.