Quel est le besoin d'un tableau avec zéro élément?

122

Dans le code du noyau Linux, j'ai trouvé la chose suivante que je ne peux pas comprendre.

 struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

Le code est ici: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Quel est le besoin et le but d'un tableau de données sans élément?

Jeegar Patel
la source
Je ne sais pas s'il devrait y avoir une balise zero-length-arrays ou struct-hack ...
hippietrail
@hippietrail, car souvent, lorsque quelqu'un demande ce qu'est cette structure, il ne sait pas qu'elle est appelée "membre de tableau flexible". S'ils l'avaient fait, ils auraient pu facilement trouver leur réponse. Puisqu'ils ne le font pas, ils ne peuvent pas marquer la question en tant que telle. C'est pourquoi nous n'avons pas une telle balise.
Shahbaz
10
Votez pour rouvrir. Je conviens que ce n'était pas un doublon, car aucun des autres messages n'aborde la combinaison d'un "struct hack" non standard de longueur nulle et du membre de tableau flexible bien défini C99. Je pense aussi qu'il est toujours avantageux pour la communauté de programmation C de faire la lumière sur tout code obscur du noyau Linux. Principalement parce que beaucoup de gens ont l'impression que le noyau Linux est une sorte de code C de pointe, pour des raisons inconnues. Alors qu'en réalité, c'est un terrible gâchis inondé d'exploits non standard qui ne devraient jamais être considérés comme un canon C.
Lundin
5
Pas un double - ce n'est pas la première fois que je vois quelqu'un fermer une question inutilement. Je pense également que cette question ajoute à la base de connaissances SO.
Aniket Inge

Réponses:

139

C'est un moyen d'avoir des tailles de données variables, sans avoir à appeler malloc( kmallocdans ce cas) deux fois. Vous l'utiliseriez comme ceci:

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

Cela n'était pas standard et était considéré comme un hack (comme l'a dit Aniket), mais il a été standardisé en C99 . Le format standard pour cela est maintenant:

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

Notez que vous ne mentionnez aucune taille pour le datachamp. Notez également que cette variable spéciale ne peut venir qu'à la fin de la structure.


Dans C99, cette question est expliquée au 6.7.2.1.16 (c'est moi qui souligne):

Dans un cas particulier, le dernier élément d'une structure avec plus d'un membre nommé peut avoir un type de tableau incomplet; c'est ce qu'on appelle un membre de tableau flexible. Dans la plupart des situations, le membre du tableau flexible est ignoré. En particulier, la taille de la structure est comme si l'élément de réseau flexible était omis, sauf qu'il peut avoir plus de remplissage de fin que l'omission impliquerait. Cependant, quand un. (ou ->) a un opérande gauche qui est (un pointeur vers) une structure avec un membre de tableau flexible et l'opérande droit nomme ce membre, il se comporte comme si ce membre était remplacé par le tableau le plus long (avec le même type d'élément ) qui ne rendrait pas la structure plus grande que l'objet auquel on accède; le décalage du tableau doit rester celui du membre du tableau flexible, même s'il diffère de celui du tableau de remplacement. Si ce tableau ne contenait aucun élément,

Ou en d'autres termes, si vous avez:

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

Vous pouvez y accéder var->dataavec des index au format [0, extra). Notez que sizeof(struct something)ne donnera la taille que pour les autres variables, c'est-à-dire donne dataune taille de 0.


Il peut être intéressant de noter également comment la norme donne en fait des exemples d' mallocune telle construction (6.7.2.1.17):

struct s { int n; double d[]; };

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

Une autre note intéressante de la norme au même endroit est (c'est moi qui souligne):

en supposant que l'appel à malloc réussit, l'objet pointé par p se comporte, dans la plupart des cas, comme si p avait été déclaré comme:

struct { int n; double d[m]; } *p;

(il existe des circonstances dans lesquelles cette équivalence est rompue; en particulier, les décalages du membre d peuvent ne pas être les mêmes ).

Shahbaz
la source
Pour être clair, le code original de la question n'est toujours pas standard dans C99 (ni C11), et serait toujours considéré comme un hack. La normalisation C99 doit omettre le tableau lié.
MM
Quoi [0, extra)?
SS Anne
36

C'est un hack en fait, pour GCC ( C90 ) en fait.

C'est aussi appelé un struct hack .

Alors la prochaine fois, je dirais:

struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

Ce sera équivalent à dire:

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

Et je peux créer n'importe quel nombre de ces objets struct.

Aniket Inge
la source
7

L'idée est de permettre un tableau de taille variable à la fin de la structure. Il y a probablement bts_actionun paquet de données avec un en-tête de taille fixe (les champs typeet size) et un datamembre de taille variable . En le déclarant comme un tableau de longueur 0, il peut être indexé comme n'importe quel autre tableau. Vous alloueriez ensuite une bts_actionstructure, disons de datataille 1024 octets , comme ceci:

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

Voir aussi: http://c2.com/cgi/wiki?StructHack

sheu
la source
2
@Aniket: Je ne sais pas vraiment d'où vient cette idée.
sheu
en C ++ oui, en C, pas nécessaire.
amc
2
@sheu, cela vient du fait que votre style d'écriture mallocvous fait vous répéter plusieurs fois et si jamais le type de actionchangements, vous devez le corriger plusieurs fois. Comparez les deux suivants pour vous-même et vous saurez: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs struct some_thing *variable = malloc(10 * sizeof(*variable));Le second est plus court, plus propre et clairement plus facile à changer.
Shahbaz
5

Le code n'est pas valide C ( voir ceci ). Le noyau Linux n'est, pour des raisons évidentes, pas du tout concerné par la portabilité, il utilise donc beaucoup de code non standard.

Ce qu'ils font, c'est une extension non standard de GCC avec une taille de tableau 0. Un programme conforme au standard aurait écrit u8 data[];et cela aurait signifié exactement la même chose. Les auteurs du noyau Linux aiment apparemment rendre les choses inutilement compliquées et non standard, si une option pour le faire se révèle.

Dans les anciens standards C, terminer une structure par un tableau vide était connu sous le nom de "struct hack". D'autres ont déjà expliqué son objectif dans d'autres réponses. Le piratage de la structure, dans le standard C90, était un comportement non défini et pouvait provoquer des plantages, principalement du fait qu'un compilateur C est libre d'ajouter n'importe quel nombre d'octets de remplissage à la fin de la structure. Ces octets de remplissage peuvent entrer en collision avec les données que vous avez essayé de "pirater" à la fin de la structure.

GCC a dès le début fait une extension non standard pour changer ce comportement indéfini en comportement bien défini. La norme C99 a ensuite adapté ce concept et tout programme C moderne peut donc utiliser cette fonctionnalité sans risque. Il est connu sous le nom de membre de matrice flexible dans C99 / C11.

Lundin
la source
3
Je doute que "le noyau Linux ne soit pas concerné par la portabilité". Peut-être parliez-vous de la portabilité vers d'autres compilateurs? Il est vrai qu'il est assez lié aux fonctionnalités de gcc.
Shahbaz
3
Néanmoins, je pense que ce morceau de code particulier n'est pas un code traditionnel et qu'il est probablement laissé de côté parce que son auteur n'y a pas prêté beaucoup d'attention. La licence dit qu'il s'agit de certains pilotes d'instruments texas, il est donc peu probable que les programmeurs principaux du noyau y aient prêté attention. Je suis sûr que les développeurs du noyau mettent constamment à jour l'ancien code selon de nouvelles normes ou de nouvelles optimisations. C'est juste trop grand pour s'assurer que tout est mis à jour!
Shahbaz
1
@Shahbaz Avec la partie "évidente", je parlais de la portabilité vers d'autres systèmes d'exploitation, ce qui n'aurait naturellement aucun sens. Mais ils ne semblent pas non plus se soucier de la portabilité vers d'autres compilateurs, ils ont utilisé tellement d'extensions GCC que Linux ne sera probablement jamais porté sur un autre compilateur.
Lundin
3
@Shahbaz En ce qui concerne tout ce qui est étiqueté Texas Instruments, TI eux-mêmes sont connus pour produire le code C le plus inutile, merdique et naïf jamais vu, dans leurs notes d'application pour diverses puces TI. Si le code provient de TI, tous les paris concernant la chance d'interpréter quelque chose d'utile à partir de celui-ci sont désactivés.
Lundin
4
Il est vrai que linux et gcc sont inséparables. Le noyau Linux est également assez difficile à comprendre (surtout parce qu'un système d'exploitation est compliqué de toute façon). Mon point cependant, c'est que ce n'est pas agréable de dire "Les auteurs du noyau Linux aiment apparemment rendre les choses inutilement compliquées et non standard, si une option pour le faire se révèle" en raison d'une mauvaise pratique de codage tierce .
Shahbaz
1

Une autre utilisation du tableau de longueur nulle est une étiquette nommée à l'intérieur d'une structure pour aider à la vérification du décalage de la structure au moment de la compilation.

Supposons que vous ayez des définitions de structure volumineuses (couvrant plusieurs lignes de cache) que vous souhaitez vous assurer qu'elles sont alignées sur la limite de la ligne de cache à la fois au début et au milieu, là où elle traverse la limite.

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

Dans le code, vous pouvez les déclarer à l'aide d'extensions GCC comme:

__attribute__((aligned(CACHE_LINE_BYTES)))

Mais vous voulez toujours vous assurer que cela est appliqué au moment de l'exécution.

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

Cela fonctionnerait pour une seule structure, mais il serait difficile de couvrir plusieurs structures, chacune ayant un nom de membre différent à aligner. Vous obtiendrez probablement du code comme ci-dessous où vous devez trouver les noms du premier membre de chaque structure:

assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

Au lieu d'aller de cette façon, vous pouvez déclarer un tableau de longueur nulle dans la structure agissant comme une étiquette nommée avec un nom cohérent mais ne consomme aucun espace.

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

Ensuite, le code d'assertion d'exécution serait beaucoup plus facile à maintenir:

assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);
Wei Shen
la source
Idée intéressante. Notez simplement que les tableaux de longueur 0 ne sont pas autorisés par la norme, c'est donc une chose spécifique au compilateur. Aussi, il peut être judicieux de citer la définition de gcc du comportement des tableaux de longueur 0 dans une définition de struct, à tout le moins pour montrer si elle pourrait introduire un remplissage avant ou après la déclaration.
Shahbaz