109 votes

Le "struct hack" est-il techniquement un comportement non défini ?

Ce que je demande, c'est le truc bien connu du "dernier membre d'une structure a une longueur variable". Cela donne quelque chose comme ça :

struct T {
    int len;
    char s[1];
};

struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");

En raison de la façon dont la structure est disposée dans la mémoire, nous sommes en mesure de superposer la structure sur un bloc plus grand que nécessaire et de traiter le dernier membre comme s'il était plus grand que la structure. 1 char spécifié.

La question est donc la suivante : Cette technique est-elle techniquement un comportement non défini ? . Je m'attendrais à ce que ce soit le cas, mais j'étais curieux de savoir ce que la norme dit à ce sujet.

PS : Je suis conscient de l'approche C99 de cette question, je voudrais que les réponses s'en tiennent spécifiquement à la version de l'astuce telle que listée ci-dessus.

51voto

Carl Norum Points 114072

Comme le C FAQ dice:

On ne sait pas si c'est légal ou portable, mais c'est plutôt populaire.

et :

... une interprétation officielle a estimé qu'il n'est pas strictement conforme à la norme C, bien qu'il semble fonctionner sous toutes les implémentations connues. (Les compilateurs qui vérifient soigneusement les limites des tableaux peuvent émettre des avertissements).

La justification de l'aspect "strictement conforme" se trouve dans la spécification, à la section J.2 Comportement indéfini qui figure dans la liste des comportements non définis :

  • Un indice de tableau est hors de portée, même si un objet est apparemment accessible avec l'indice donné (comme dans l'expression lvalue a[1][7] compte tenu de la déclaration int a[4][5] ) (6.5.6).

Paragraphe 8 de la section 6.5.6 Opérateurs additifs mentionne également que l'accès au-delà des limites définies du tableau est indéfini :

Si l'opérande pointeur et le résultat pointent tous deux sur des éléments du même objet tableau, ou sur un élément au-delà du dernier élément de l'objet tableau, l'évaluation ne doit pas produire un dépassement de capacité ; sinon, le comportement est indéfini.

34voto

Jerry Coffin Points 237758

Je crois que techniquement, c'est un comportement non défini. La norme ne l'aborde pas directement (on peut le dire), donc cela tombe sous la clause "ou par l'omission de toute définition explicite du comportement" (§4/2 de C99, §3.16/2 de C89) qui dit que c'est un comportement non défini.

L'"argument" ci-dessus dépend de la définition de l'opérateur d'inscription de tableau. Plus précisément, elle dit : "Une expression postfixe suivie d'une expression entre crochets [] est une désignation en indice d'un objet tableau." (C89, §6.3.2.1/2).

Vous pouvez argumenter que le "of an array object" est violé ici (puisque vous souscrivez en dehors de la plage définie de l'objet tableau), auquel cas le comportement est (un tout petit peu plus) explicitement indéfini, au lieu d'être simplement indéfini parce que rien ne le définit.

En théorie, je peux imaginer un compilateur qui vérifie les limites des tableaux et (par exemple) interrompt le programme si vous tentez d'utiliser un indice hors de portée. En fait, je ne sais pas si une telle chose existe, et étant donné la popularité de ce style de code, même si un compilateur essayait de faire respecter les indices dans certaines circonstances, il est difficile d'imaginer que quelqu'un accepterait qu'il le fasse dans cette situation.

12voto

ouah Points 75311

Oui, c'est un comportement indéfini.

Le rapport sur les défauts du langage C #051 donne une réponse définitive à cette question :

L'idiome, bien que courant, n'est pas strictement conforme.

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

Dans le document C99 Rationale, le comité C ajoute :

La validité de ce concept a toujours été remise en question. Dans la réponse à un rapport de défaut le Comité a décidé qu'il s'agissait d'un comportement non défini parce que le tableau p->items ne contient qu'un seul élément, indépendamment de l'existence de l'espace.

11voto

Chuck Points 138930

Cette façon particulière de procéder n'est pas explicitement définie dans une norme C, mais C99 inclut le "struct hack" comme faisant partie du langage. En C99, le dernier membre d'un struct peut être un "membre de tableau flexible", déclaré comme suit char foo[] (avec le type que vous souhaitez à la place de char ).

7voto

R.. Points 93718

Ce n'est pas un comportement indéfini sans tenir compte de ce qui se passe, officiel ou non dit, parce qu'il est défini par la norme. p->s sauf lorsqu'il est utilisé en tant que valeur l, est évalué comme un pointeur identique à (char *)p + offsetof(struct T, s) . En particulier, c'est un char à l'intérieur de l'objet malloqué, et il y a 100 (ou plus, en fonction de considérations d'alignement) adresses successives qui le suivent immédiatement et qui sont également valides en tant que char à l'intérieur de l'objet alloué. Le fait que le pointeur ait été dérivé par l'utilisation de -> au lieu d'ajouter explicitement le décalage au pointeur retourné par malloc , pour char * n'est pas pertinent.

Techniquement, p->s[0] est l'élément unique de la char à l'intérieur de la structure, les quelques éléments suivants (par ex. p->s[1] par le biais de p->s[3] ) sont probablement des octets de remplissage à l'intérieur de la structure, qui pourraient être corrompus si vous effectuez une affectation à la structure dans son ensemble, mais pas si vous accédez simplement à des membres individuels, et le reste des éléments est un espace supplémentaire dans l'objet alloué que vous êtes libre d'utiliser comme vous le souhaitez, tant que vous respectez les exigences en matière d'alignement (et char n'a pas d'exigences en matière d'alignement).

Si vous craignez que l'éventualité d'un chevauchement avec des octets de remplissage dans la structure puisse d'une manière ou d'une autre invoquer des démons nasaux, vous pouvez éviter cela en remplaçant la balise 1 sur [1] avec une valeur qui garantit qu'il n'y a pas de remplissage à la fin de la structure. Une façon simple mais inutile de le faire serait de créer une structure avec des membres identiques, mais sans tableau à la fin, et d'utiliser la fonction s[sizeof struct that_other_struct]; pour le tableau. Ensuite, p->s[i] est clairement défini comme un élément du tableau dans la struct pour i<sizeof struct that_other_struct et comme un objet char à une adresse suivant la fin du struct pour i>=sizeof struct that_other_struct .

Editar: En fait, dans l'astuce ci-dessus pour obtenir la bonne taille, vous pourriez aussi avoir besoin de mettre une union contenant chaque type simple avant le tableau, pour s'assurer que le tableau lui-même commence avec un alignement maximal plutôt qu'au milieu du padding d'un autre élément. Encore une fois, je ne crois pas que tout cela soit nécessaire, mais je le propose aux plus paranoïaques des avocats du langage.

Edit 2 : Le chevauchement avec les octets de remplissage n'est absolument pas un problème, en raison d'une autre partie de la norme. Le langage C exige que si deux structures se rejoignent dans une sous-séquence initiale de leurs éléments, les éléments initiaux communs peuvent être accédés par un pointeur sur l'un ou l'autre type. Par conséquent, si une structure identique à struct T mais avec un tableau final plus grand ont été déclarés, l'élément s[0] devrait coïncider avec l'élément s[0] sur struct T et la présence de ces éléments supplémentaires ne pouvait pas affecter ou être affectée par l'accès à des éléments communs de la structure plus grande en utilisant un pointeur à struct T .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X