web-dev-qa-db-fra.com

Le comportement "struct hack" est-il techniquement indéfini?

Ce que je demande, c'est l'astuce bien connue "le dernier membre d'une structure a une longueur variable". Ca fait plutot comme ca:

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 en mémoire, nous sommes capables 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 le 1 char spécifié.

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

PS: je suis conscient de l'approche C99 à ce sujet, je voudrais que les réponses collent spécifiquement à la version de l'astuce comme indiqué ci-dessus.

110
Evan Teran

Comme le dit C FAQ :

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'elle n'était pas strictement conforme à la norme C, bien qu'elle semble fonctionner sous toutes les implémentations connues. (Les compilateurs qui vérifient soigneusement les limites du tableau peuvent émettre des avertissements.)

La logique derrière le bit "strictement conforme" se trouve dans la section spécification J.2 Comportement non défini , qui inclut 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).

Le paragraphe 8 de la section 6.5.6 Les opérateurs additifs contient une autre mention selon laquelle l'accès au-delà des limites de tableau définies n'est pas défini:

Si l'opérande de pointeur et le résultat pointent tous les deux vers des éléments du même objet tableau, ou dépassent le dernier élément de l'objet tableau, l'évaluation ne doit pas produire de débordement; sinon, le comportement n'est pas défini.

52
Carl Norum

Je crois que techniquement c'est un comportement indéfini. La norme (sans doute) ne la traite pas directement, elle relève donc du "ou de l'omission de toute définition explicite du comportement". clause (§4/2 de C99, §3.16/2 de C89) qui dit que c'est un comportement indéfini.

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

Vous pouvez faire valoir que le "d'un objet tableau" 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 courtoisie de rien le définissant tout à fait.

En théorie, je peux imaginer un compilateur qui vérifie les limites du tableau et (par exemple) abandonnerait le programme lorsque/si vous tentiez d'utiliser un indice hors de portée. En fait, je ne connais pas une telle chose existante, et étant donné la popularité de ce style de code, même si un compilateur a essayé d'imposer des indices dans certaines circonstances, il est difficile d'imaginer que quiconque accepterait de le faire dans cette situation.

34
Jerry Coffin

Oui, c'est un comportement indéfini.

Le rapport de défaut de 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 de justification C99, le comité C ajoute:

La validité de cette construction a toujours été mise en doute. Dans la réponse à un rapport de défaut, le Comité a décidé qu'il s'agissait d'un comportement indéfini car le tableau p-> éléments ne contient qu'un seul élément, que l'espace existe ou non.

13
ouah

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

11
Chuck

Ce n'est pas un comportement indéfini, peu importe ce que n'importe qui, officiel ou autre, dit, car il est défini par la norme. p->s, sauf lorsqu'il est utilisé comme valeur l, est évalué comme un pointeur identique à (char *)p + offsetof(struct T, s). En particulier, il s'agit d'un pointeur char valide à l'intérieur de l'objet malloc'd, et il y a 100 (ou plus, en fonction des considérations d'alignement) adresses successives qui le suivent immédiatement qui sont également valides comme char objets à l'intérieur de l'objet alloué. Le fait que le pointeur ait été dérivé en utilisant -> au lieu d'ajouter explicitement l'offset au pointeur renvoyé par malloc, transtypé en char *, n'est pas pertinent.

Techniquement, p->s[0] est le seul élément du tableau char à l'intérieur de la structure, les quelques éléments suivants (par exemple p->s[1] à travers 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 bon vous semble, tant que vous respectez les exigences d'alignement (et char n'a pas d'exigences d'alignement).

Si vous craignez que la possibilité de chevauchement avec des octets de remplissage dans la structure puisse en quelque sorte invoquer des démons nasaux, vous pouvez éviter cela en remplaçant le 1 dans [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, sauf aucun tableau à la fin, et d'utiliser 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 structure de i<sizeof struct that_other_struct et en tant qu'objet char à une adresse suivant la fin de la structure pour i>=sizeof struct that_other_struct.

Edit: En fait, dans l'astuce ci-dessus pour obtenir la bonne taille, vous devrez peut-être également mettre une union contenant chaque type simple avant le tableau, pour vous assurer que le tableau lui-même commence par un alignement maximal plutôt que dans au milieu du rembourrage d'un autre élément. Encore une fois, je ne crois pas que tout cela soit nécessaire, mais je le propose pour le plus paranoïaque des juristes linguistiques.

Edit 2: Le chevauchement avec les octets de remplissage n'est certainement pas un problème, en raison d'une autre partie de la norme. C exige que si deux structures s'accordent dans une sous-séquence initiale de leurs éléments, les éléments initiaux communs sont accessibles via un pointeur vers 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] dans struct T, et la présence de ces éléments supplémentaires n'a pas pu affecter ou être affectée en accédant aux éléments communs de la structure plus grande à l'aide d'un pointeur vers struct T.

Oui, c'est un comportement techniquement indéfini.

Notez qu'il existe au moins trois façons d'implémenter le "struct hack":

(1) Déclarer le tableau de fin avec la taille (la manière la plus "populaire" dans le code hérité). C'est évidemment UB, car les déclarations de tableau de taille nulle sont toujours illégales en C. Même s'il compile, le langage ne garantit pas le comportement de tout code violant les contraintes.

(2) Déclarer le tableau avec une taille légale minimale - 1 (votre cas). Dans ce cas, toute tentative de prendre le pointeur sur p->s[0] et l'utiliser pour l'arithmétique des pointeurs qui va au-delà de p->s[1] est un comportement indéfini. Par exemple, une implémentation de débogage est autorisée à produire un pointeur spécial avec des informations de plage intégrées, qui seront interceptées chaque fois que vous tenterez de créer un pointeur au-delà de p->s[1].

(3) Déclarer le tableau avec une taille "très grande" comme 10000, par exemple. L'idée est que la taille déclarée est censée être plus grande que tout ce dont vous pourriez avoir besoin dans la pratique. Cette méthode est exempte d'UB en ce qui concerne la plage d'accès aux baies. Cependant, dans la pratique, bien sûr, nous allouerons toujours une plus petite quantité de mémoire (seulement autant que nécessaire). Je ne suis pas sûr de la légalité de cela, c'est-à-dire que je me demande dans quelle mesure il est légal d'allouer moins de mémoire à l'objet que la taille déclarée de l'objet (en supposant que nous n'accédions jamais aux membres "non alloués").

7
AnT

La norme est assez claire: vous ne pouvez pas accéder à des éléments à côté de la fin d'un tableau. (et passer par des pointeurs n'aide pas, car vous n'êtes pas autorisé à même incrémenter les pointeurs au-delà d'un après la fin du tableau).

Et pour "travailler en pratique". J'ai vu l'optimiseur gcc/g ++ utiliser cette partie de la norme, générant ainsi un code incorrect en rencontrant ce C. invalide.

3

Si un compilateur accepte quelque chose comme

 typedef struct {
 int len; 
 char dat []; 
};

Je pense qu'il est assez clair qu'il doit être prêt à accepter un indice sur "dat" au-delà de sa longueur. D'un autre côté, si quelqu'un code quelque chose comme:

 typedef struct {
 int any; 
 char dat [1]; 
} MY_STRUCT;

puis accède plus tard à somestruct-> dat [x]; Je ne pense pas que le compilateur soit obligé d'utiliser du code de calcul d'adresse qui fonctionnera avec de grandes valeurs de x. Je pense que si l'on voulait être vraiment en sécurité, le paradigme approprié serait plus comme:

 # définir LARGEST_DAT_SIZE 0xF000 
 typedef struct {
 int quel que soit; 
 char dat [LARGEST_DAT_SIZE]; 
} MY_STRUCT;

puis effectuez un malloc de (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desire_array_length) octets (en gardant à l'esprit que si la variable desire_array_length est plus grande que LARGEST_DAT_SIZE, les résultats peuvent être indéfinis).

Soit dit en passant, je pense que la décision d'interdire les tableaux de longueur nulle a été malheureuse (certains dialectes plus anciens comme Turbo C le prennent en charge), car un tableau de longueur nulle pourrait être considéré comme un signe que le compilateur doit générer du code qui fonctionnera avec des indices plus grands. .

1
supercat