Ce que je demande, c'est le truc bien connu "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 pouvons superposer la structure sur un bloc plus grand que nécessaire et traiter le dernier membre comme s'il était plus grand que celui 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 dit à ce sujet.
PS: Je suis conscient de l'approche C99 à ce sujet, j'aimerais que les réponses s'en tiennent spécifiquement à la version de l'astuce répertoriée ci-dessus.
c
undefined-behavior
c89
Evan Teran
la source
la source
Réponses:
Comme le dit la FAQ C :
et:
La justification du bit 'strictement conforme' est dans la spécification, section J.2 Comportement non défini, qui inclut dans la liste des comportements non définis:
Le paragraphe 8 de la section 6.5.6 Les opérateurs additifs a une autre mention que l'accès au-delà des limites de tableau définies n'est pas défini:
la source
p->s
n'est jamais utilisé comme tableau. Il est passé àstrcpy
, auquel cas il se désintègre en un plainechar *
, ce qui arrive à pointer vers un objet qui peut légalement être interprété commechar [100];
à l'intérieur de l'objet alloué.malloc
, lorsque vous avez simplement converti levoid *
vers un pointeur vers [une structure contenant] un tableau. Il est toujours valide d'accéder à n'importe quelle partie de l'objet alloué en utilisant un pointeur verschar
(ou de préférenceunsigned char
).malloc
. Recherchez "objet" dans la norme avant de lancer bs.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 par 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 d'indice de tableau. Plus précisément, il dit: "Une expression avec 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 affirmer que le "d'un objet tableau" est violé ici (puisque vous indiquez 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 de simplement indéfini grâce à rien de tout à fait le définir.
En théorie, je peux imaginer un compilateur qui vérifie les limites du tableau et (par exemple) abandonne le programme lorsque / si vous essayez d'utiliser un indice hors de portée. En fait, je ne sais pas qu'une telle chose existe, et étant donné la popularité de ce style de code, même si un compilateur essayait d'appliquer des indices dans certaines circonstances, il est difficile d'imaginer que quiconque accepterait de le faire dans cette situation.
la source
arr[x] = y;
pourrait être réécrit commearr[0] = y;
; pour un tableau de taille 2,arr[i] = 4;
pourrait être réécrit commei ? arr[1] = 4 : arr[0] = 4;
Bien que je n'ai jamais vu un compilateur effectuer de telles optimisations, sur certains systèmes embarqués, ils pourraient être très productifs. Sur un PIC18x, en utilisant des types de données 8 bits, le code de la première instruction serait de seize octets, le deuxième, deux ou quatre et le troisième, huit ou douze. Pas une mauvaise optimisation si légale.a[2] == a + 2
), ce n'est pas le cas. Si j'ai raison, toutes les normes C définissent l'accès au tableau comme une arithmatique de pointeur.Oui, c'est un comportement indéfini.
Le rapport sur les défauts de langage C # 051 donne une réponse définitive à cette question:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
Dans le document de justification C99, le Comité C ajoute:
la source
malloc
) est valide dans l'addition, alors comment le pointeur identique peut-il, obtenu via une autre route, être invalide dans l'ajout? Même s'ils veulent prétendre que c'est UB, cela n'a pas de sens, car il n'y a aucun moyen informatique pour une implémentation de faire la distinction entre l'utilisation bien définie et l'utilisation supposée non définie.*foo
contient un tableau à un seul élémentboz
, l'expressionfoo->boz[biz()*391]=9;
pourrait être simplifiée comme suit:)biz(),foo->boz[0]=9;
. Malheureusement, le rejet des tableaux à zéro élément par les compilateurs signifie que beaucoup de code utilise des tableaux à un seul élément à la place, et serait brisé par cette optimisation.Cette manière particulière de le faire n'est explicitement définie dans aucun standard C, mais C99 inclut le "struct hack" dans le cadre du langage. En C99, le dernier membre d'une structure peut être un "membre de tableau flexible", déclaré comme
char foo[]
(avec le type de votre choix à la place dechar
).la source
Ce n'est pas un comportement indéfini , indépendamment de ce que quiconque, officiel ou non , dit, car il est défini par la norme.
p->s
, sauf lorsqu'il est utilisé comme lvalue, donne un pointeur identique à(char *)p + offsetof(struct T, s)
. En particulier, il s'agit d'unchar
pointeur valide à l'intérieur de l'objet malloc'd, et il y a 100 adresses successives (ou plus, en fonction des considérations d'alignement) qui le suivent immédiatement, qui sont également valides commechar
objets à l'intérieur de l'objet alloué. Le fait que le pointeur ait été dérivé en utilisant->
au lieu d'ajouter explicitement le décalage au pointeur renvoyé parmalloc
, cast verschar *
, n'est pas pertinent.Techniquement,
p->s[0]
est le seul élément duchar
tableau à l'intérieur de la structure, les quelques éléments suivants (par exemple àp->s[1]
traversp->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 à l'individu les membres et le reste des éléments constituent un espace supplémentaire dans l'objet alloué que vous êtes libre d'utiliser comme vous le souhaitez, tant que vous respectez les exigences d'alignement (et que vous n'avezchar
aucune exigence d'alignement).Si vous craignez que la possibilité de 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
1
in[1]
par une valeur qui garantit qu'il n'y a pas de remplissage à la fin de la structure. Un moyen simple mais inutile de faire cela serait de créer une structure avec des membres identiques sauf aucun tableau à la fin, et de l'utilisers[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 pouri<sizeof struct that_other_struct
et comme un objet char à une adresse suivant la fin de la structure pouri>=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 qu'au milieu du remplissage 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. En conséquence, si une structure identique à
struct T
mais avec un tableau final plus grand était déclarée, l'éléments[0]
devrait coïncider avec l'éléments[0]
dansstruct T
, et la présence de ces éléments supplémentaires ne pourrait pas affecter ou être affectée par l'accès aux éléments communs de la structure plus grande en utilisant un pointeur versstruct T
.la source
malloc
lequel on accède en tant que tableau ou s'il s'agit d'une structure plus grande accessible via un pointeur vers une structure plus petite dont les éléments sont un sous-ensemble initial des éléments de la structure plus grande, entre autres cas.malloc
n'alloue pas une plage de mémoire accessible avec l'arithmétique du pointeur, à quoi cela servirait-il? Et si la normep->s[1]
est définie comme du sucre syntaxique pour l'arithmétique des pointeurs, cette réponse réaffirme simplement quemalloc
c'est utile. Que reste-t-il à discuter? :)1
. C'est précisément aussi simple que cela.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
supposer que laif
branche est entrée. Ceci est UB (et non garanti pour l'initialisationn
) selon 6.5.6 p8 (dernière phrase), comme je l'ai lu. Connexes: 6.5.9 p6 avec note de bas de page 109. (Les références sont à C11 n1570.) [...]Oui, il s'agit d'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 0 (la manière la plus "populaire" dans le code hérité). C'est évidemment UB, puisque les déclarations de tableau de taille zéro sont toujours illégales en C. Même s'il compile, le langage ne donne aucune garantie sur 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
p->s[0]
et de l'utiliser pour l'arithmétique du pointeur qui va au-delà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 incorporées, qui intercepteront chaque fois que vous tenterez de créer un pointeur au-delà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 réelle. Cette méthode est exempte d'UB en ce qui concerne la plage d'accès au tableau. 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 pour l'objet que la taille déclarée de l'objet (en supposant que nous n'accédions jamais aux membres "non alloués").
la source
s[1]
n'est pas un comportement indéfini. C'est la même chose que*(s+1)
, qui est identique à*((char *)p + offsetof(struct T, s) + 1)
, qui est un pointeur valide vers unchar
dans l'objet alloué.foo[]
sucre syntaxique pour*foo
), alors tout accès au-delà de la plus petite de sa taille déclarée et de sa taille allouée est UB, quelle que soit la façon dont l'arithmétique du pointeur a été effectuée.foo[]
dans une structure n'est pas un sucre syntaxique pour*foo
; c'est un membre de tableau flexible C99. Pour le reste, voir ma réponse et mes commentaires sur d'autres réponses.unsigned char [sizeof object]
tableau superposé imaginaire . Je maintiens mon affirmation selon laquelle le membre de tableau flexible "hack" pour le pré-C99 a un comportement bien défini.La norme indique clairement que vous ne pouvez pas accéder aux éléments situés à la fin d'un tableau. (et passer par des pointeurs n'aide pas, car vous n'êtes même pas autorisé à incrémenter les pointeurs au-delà d'un point après la fin du tableau).
Et pour "travailler dans la pratique". J'ai vu l'optimiseur gcc / g ++ utiliser cette partie de la norme générant ainsi un code erroné lors de la rencontre de ce C.
la source
Si un compilateur accepte quelque chose comme
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:
puis accède plus tard à somestruct-> dat [x]; Je ne pense pas que le compilateur soit obligé d'utiliser un code de calcul d'adresse qui fonctionnera avec de grandes valeurs de x. Je pense que si l'on voulait être vraiment sûr, le paradigme approprié serait plutôt:
puis effectuez un malloc de (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desire_array_length) octets (en gardant à l'esprit que si la longueur de la table est plus grande que LARGEST_DAT_SIZE, les résultats peuvent être indéfinis).
Incidemment, je pense que la décision d'interdire les tableaux de longueur nulle était malheureuse (certains dialectes plus anciens comme Turbo C le supportent) 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 index plus grands .
la source