Quand les octets de pad sont-ils copiés – affectation de structure, transmission par valeur, autre?

Lors du débogage d’un problème, le problème suivant est apparu. (Veuillez ignorer les erreurs de code mineures; le code est juste pour illustration.)

La structure suivante est définie:

typedef struct box_t { uint32_t x; uint16_t y; } box_t; 

Les instances de cette structure sont passées par valeur de fonction à fonction (évidemment simplifié):

 void fun_a(box_t b) { ... use b ... } void fun_b(box_t bb) { // pass bb by value int err = funa(bb); } void fun_c(void) { box_t real_b; box_t some_b[10]; ... ... use real_b and some_b[] ... ... funb(real_b); funb(some_b[3]); ... box_t copy_b = some_b[5]; ... } 

Dans certains cas, deux instances de box_t sont comparées comme suit:

  memcmp(bm, bn, sizeof(box_t)); 

Dans plusieurs appels nesteds, les octets de l’argument box_t ont été vidés en utilisant quelque chose comme ceci:

 char *p = (char*) &a_box_t_arg; for (i=0; i < sizeof(box_t); i++) { printf(" %02X", *p & 0xFF); p++; } printf("\n"); 

Le sizeof (box_t) est 8; il y a 2 octets de pad (découverts après le uint16_t). Le vidage a montré que les champs de la structure étaient égaux, mais les octets de pad ne l’étaient pas; cela a provoqué l’échec de memcmp (sans surprise).

La partie intéressante a été de découvrir d’où venaient les valeurs de pad «corrompues». Après un suivi en arrière, il a été découvert que certaines instances de box_t avaient été déclarées en tant que variables locales et avaient été initialisées comme suit:

 box_t b; bx = 1; by = 2; 

Ce qui précède ne semble (semble pas) initialiser les octets du pad, qui semblent contenir des “ordures” (tout ce qui était dans la stack, espace alloué pour b). Dans la plupart des cas, l’initialisation a été effectuée à l’aide de memset(b, 0, sizeof(box_t)) .

La question est de savoir si initialiser une instance de box_t avec (1) une affectation de structure ou (2) en passant par valeur fera toujours l’équivalent d’un memcpy de sizeof (box_t). Est-il toujours vrai que seuls les 6 octets des «champs réels» sont copiés (et les octets du pad ne le sont pas).

D’après le débogage, il apparaît que l’équivalent memcpy sizeof (box_t) est toujours effectué. Y at-il quelque chose (par exemple, dans la norme) qui spécifie réellement cela? Il serait utile de savoir sur quoi on peut compter en ce qui concerne le traitement des octets du pad pendant le débogage.

Merci! (Utilisation de GCC 4.4.3 sur Ubuntu LTS 10.4 64 bits)

Pour les points bonus:

 void f(void) { box_t ba; box_t bb; box_t bc; 

Les 3 instances sont allouées à 16 octets tandis que sizeof () indique 8. Pourquoi cet espace supplémentaire?

La valeur des octets de remplissage n’est pas spécifiée (C99 / C11 6.2.6.1 §6):

Lorsqu’une valeur est stockée dans un object de type structure ou union, y compris dans un object membre, les octets de la représentation de l’object correspondant aux octets de remplissage prennent des valeurs non spécifiées.

Voir aussi la note 42/51 (C99: TC3, projet de C1x):

Ainsi, par exemple, l’affectation de structure n’a pas besoin de copier de bits de remplissage.

Le compilateur est libre de copier ou de ne pas copier le remplissage à sa guise. Sur x86 [1], je suppose que 2 octets de remplissage de fin seront copiés, mais pas 4 octets (ce qui peut se produire même sur du matériel 32 bits, car les structures peuvent nécessiter un alignement de 8 octets, par exemple pour permettre la lecture atomique de fichiers). double valeurs).

[1] Aucune mesure réelle n’a été effectuée.


Pour développer la réponse:

La norme ne donne aucune garantie en ce qui concerne les octets de remplissage. Toutefois, si vous initialisez un object avec une durée de stockage statique, il est fort probable que vous obtiendrez un remplissage avec remise à zéro. Mais si vous utilisez cet object pour en initialiser un autre via une affectation, tous les paris sont à nouveau désactivés (et je m’attendrais à ce que les derniers octets de remplissage – à nouveau, aucune mesure effectuée – soient particulièrement bons pour être omis de la copie).

Utiliser memset() et memcpy() – même lors de l’atsortingbution de membres individuels, car cela peut également invalider le remplissage – est un moyen de garantir les valeurs d’octets de remplissage sur les implémentations raisonnables. Cependant, en principe, le compilateur est libre de modifier les valeurs de remplissage “derrière votre dos” à tout moment (ce qui peut être lié à la mise en cache de membres dans des registres – à nouveau deviné), ce que vous pouvez probablement éviter en utilisant le stockage volatile .

La seule solution de rechange raisonnablement portable à laquelle je puisse penser est de spécifier explicitement la disposition de la mémoire en introduisant des membres factices de taille appropriée tout en vérifiant avec des moyens spécifiques au compilateur qu’aucun remplissage supplémentaire n’est introduit ( __atsortingbute__ ((packed)) , -Wpadded pour gcc).

C11 vous permettra de définir les membres anonymes de la structure et du syndicat:

 typedef union box_t { unsigned char allBytes[theSizeOfIt]; struct { uint32_t x; uint16_t y; }; } box_t; 

Cette union se comporterait presque comme avant, vous pouvez accéder à .x etc., mais l’initialisation et l’affectation par défaut seraient modifiées. Si vous vous assurez toujours que vos variables sont correctement initialisées, procédez comme suit:

 box_t real_b = { 0 }; 

ou comme ça

 box_t real_a = { .allBytes = {0}, .x = 1, .y = 2 }; 

Tous les octets de remplissage doivent être correctement initialisés à 0 . Cela n’aiderait pas si vos types entiers auraient des bits de remplissage , mais au moins les types uintXX_t que vous avez choisis ne les auront pas par définition.

gcc et ses adeptes l’implémentent déjà comme extension même s’ils ne sont pas encore complètement C11.

Edit: Dans P99, il existe une macro pour le faire de manière cohérente:

 #define P99_DEFINE_UNION(NAME, ...) \ union NAME { \ uint8_t p00_allbytes[sizeof(union { __VA_ARGS__ })]; \ __VA_ARGS__ \ } 

C’est la taille du tableau qui est déterminée en déclarant une union “non étiquetée” juste pour sa taille.

Comme Christoph l’a dit, il n’y a aucune garantie concernant le rembourrage. Votre meilleur pari est de ne pas utiliser memcmp pour comparer deux structures. Cela fonctionne au mauvais niveau d’abstraction. memcmp fonctionne par octet lors de la représentation, alors que vous devez comparer les valeurs des membres.

Mieux vaut utiliser une fonction de comparaison séparée qui prend deux structures et compare chaque membre séparément. Quelque chose comme ça:

 int box_isequal (box_t bm, box_t bn) { return (bm.x == bn.x) && (bm.y == bn.y); } 

Pour votre bonus, les trois objects sont des objects séparés, ils ne font pas partie du même tableau et l’arithmétique de pointeur entre eux n’est pas autorisée. En tant que variables locales de fonction, elles sont généralement allouées sur la stack et, comme elles sont séparées, le compilateur peut les aligner de la manière la plus appropriée, par exemple pour les performances.