Was braucht ein Array mit null Elementen?

122

Im Linux-Kernel-Code habe ich Folgendes gefunden, was ich nicht verstehen kann.

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

Der Code ist hier: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Was ist die Notwendigkeit und der Zweck eines Datenarrays mit null Elementen?

Jeegar Patel
quelle
Ich bin mir nicht sicher, ob es entweder ein Zero-Length-
Arrays-
@hippietrail, weil jemand oft, wenn er fragt, was diese Struktur ist, nicht weiß, dass sie als "flexibles Array-Mitglied" bezeichnet wird. Wenn sie das getan hätten, hätten sie leicht ihre Antwort finden können. Da dies nicht der Fall ist, können sie die Frage nicht als solche kennzeichnen. Deshalb haben wir kein solches Tag.
Shahbaz
10
Stimmen Sie ab, um wieder zu öffnen. Ich bin damit einverstanden, dass dies kein Duplikat war, da keiner der anderen Beiträge die Kombination eines nicht standardmäßigen "Struktur-Hacks" mit einer Länge von Null und dem gut definierten flexiblen Array-Mitglied mit C99-Funktion behandelt. Ich denke auch, dass es für die C-Programmier-Community immer von Vorteil ist, obskuren Code aus dem Linux-Kernel zu beleuchten. Hauptsächlich, weil viele Leute den Eindruck haben, dass der Linux-Kernel aus unbekannten Gründen eine Art C-Code auf dem neuesten Stand der Technik ist. Während es in Wirklichkeit ein schreckliches Durcheinander ist, das mit nicht standardmäßigen Exploits überflutet ist, die niemals als C-Kanon angesehen werden sollten.
Lundin
5
Kein Duplikat - ist nicht das erste Mal, dass ich jemanden sehe, der eine Frage unnötig schließt. Ich denke auch, dass diese Frage die SO Knowledge Base erweitert.
Aniket Inge

Antworten:

139

Dies ist eine Möglichkeit, variable Datengrößen zu erhalten, ohne malloc( kmallocin diesem Fall) zweimal aufrufen zu müssen . Sie würden es so verwenden:

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

Dies war früher kein Standard und wurde als Hack angesehen (wie Aniket sagte), aber es wurde in C99 standardisiert . Das Standardformat dafür ist jetzt:

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

Beachten Sie, dass Sie keine Größe für das dataFeld angeben. Beachten Sie auch, dass diese spezielle Variable nur am Ende der Struktur stehen kann.


In C99 wird diese Angelegenheit in 6.7.2.1.16 (Schwerpunkt Mine) erläutert:

Als Sonderfall kann das letzte Element einer Struktur mit mehr als einem benannten Element einen unvollständigen Array-Typ haben. Dies wird als flexibles Array-Mitglied bezeichnet. In den meisten Situationen wird das flexible Array-Mitglied ignoriert. Insbesondere ist die Größe der Struktur so, als ob das flexible Array-Element weggelassen worden wäre, mit der Ausnahme, dass es möglicherweise mehr nachlaufende Polsterung aufweist, als die Auslassung implizieren würde. Wenn jedoch a. Der Operator (oder ->) hat einen linken Operanden, der (ein Zeiger auf) eine Struktur mit einem flexiblen Array-Element ist, und der rechte Operand benennt dieses Element. Es verhält sich so, als ob dieses Element durch das längste Array (mit demselben Elementtyp) ersetzt würde ) das würde die Struktur nicht größer machen als das Objekt, auf das zugegriffen wird; Der Versatz des Arrays bleibt der des flexiblen Array-Elements, auch wenn dies von dem des Ersatz-Arrays abweichen würde. Wenn dieses Array keine Elemente hätte,

Oder mit anderen Worten, wenn Sie haben:

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

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

Sie können var->datamit Indizes in zugreifen [0, extra). Beachten Sie, dass sizeof(struct something)nur die Größe für die anderen Variablen angegeben wird, dh dataeine Größe von 0.


Es kann auch interessant sein festzustellen, wie der Standard tatsächlich Beispiele für ein mallocsolches Konstrukt enthält (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]));

Ein weiterer interessanter Hinweis des Standards an derselben Stelle ist (Hervorhebung von mir):

Unter der Annahme, dass der Aufruf von malloc erfolgreich ist, verhält sich das Objekt, auf das p zeigt, für die meisten Zwecke so, als ob p deklariert worden wäre als:

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

(Es gibt Umstände, unter denen diese Äquivalenz gebrochen ist; insbesondere sind die Offsets von Mitglied d möglicherweise nicht dieselben ).

Shahbaz
quelle
Um klar zu sein, ist der ursprüngliche Code in der Frage in C99 (oder C11) immer noch nicht Standard und würde immer noch als Hack betrachtet. Bei der C99-Standardisierung muss das gebundene Array weggelassen werden.
MM
Was ist [0, extra)?
SS Anne
36

Dies ist tatsächlich ein Hack für GCC ( C90 ).

Es wird auch als Struktur-Hack bezeichnet .

Also würde ich das nächste Mal sagen:

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

Es wird gleichbedeutend sein mit zu sagen:

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

Und ich kann beliebig viele solcher Strukturobjekte erstellen.

Aniket Inge
quelle
7

Die Idee ist, ein Array mit variabler Größe am Ende der Struktur zuzulassen. Vermutlich bts_actionhandelt es sich um ein Datenpaket mit einem Header fester Größe (die Felder und typeund size) und einem Element variabler Größe data. Durch Deklarieren als Array mit 0 Länge kann es wie jedes andere Array indiziert werden. Sie würden dann eine bts_actionStruktur mit einer dataGröße von beispielsweise 1024 Byte wie folgt zuweisen :

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

Siehe auch: http://c2.com/cgi/wiki?StructHack

sheu
quelle
2
@Aniket: Ich bin mir nicht ganz sicher, woher diese Idee kommt.
Sheu
in C ++ ja, in C nicht benötigt.
Amc
2
@sheu, es kommt von der Tatsache, dass Ihr Schreibstil mallocSie dazu bringt, sich mehrmals zu wiederholen, und wenn jemals die Art der actionÄnderungen, müssen Sie es mehrmals korrigieren . Vergleichen Sie die folgenden beiden für sich selbst und Sie werden wissen: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));Die zweite ist kürzer, sauberer und deutlich einfacher zu ändern.
Shahbaz
5

Der Code ist ungültig C ( siehe hier ). Der Linux-Kernel befasst sich aus offensichtlichen Gründen nicht im geringsten mit der Portabilität, daher verwendet er viel nicht standardmäßigen Code.

Was sie tun, ist eine nicht standardmäßige GCC-Erweiterung mit Arraygröße 0. Ein standardkonformes Programm hätte geschrieben u8 data[];und es hätte genau dasselbe bedeutet. Die Autoren des Linux-Kernels lieben es anscheinend, Dinge unnötig kompliziert und nicht standardisiert zu machen, wenn sich eine Option dazu ergibt.

In älteren C-Standards wurde das Beenden einer Struktur mit einem leeren Array als "Struktur-Hack" bezeichnet. Andere haben ihren Zweck bereits in anderen Antworten erläutert. Der Struktur-Hack im C90-Standard war ein undefiniertes Verhalten und konnte zu Abstürzen führen, hauptsächlich da ein C-Compiler am Ende der Struktur eine beliebige Anzahl von Füllbytes hinzufügen kann. Solche Auffüllbytes können mit den Daten kollidieren, die Sie am Ende der Struktur "hacken" wollten.

GCC hat frühzeitig eine nicht standardmäßige Erweiterung vorgenommen, um dies von undefiniertem zu genau definiertem Verhalten zu ändern. Der C99-Standard hat dieses Konzept dann angepasst, und jedes moderne C-Programm kann diese Funktion daher ohne Risiko nutzen. Es ist in C99 / C11 als flexibles Array-Mitglied bekannt .

Lundin
quelle
3
Ich bezweifle, dass "der Linux-Kernel sich nicht mit Portabilität befasst". Vielleicht meinten Sie Portabilität zu anderen Compilern? Es ist wahr, dass es ziemlich mit den Merkmalen von gcc verwoben ist.
Shahbaz
3
Trotzdem denke ich, dass dieser spezielle Code kein Mainstream-Code ist und wahrscheinlich weggelassen wird, weil sein Autor ihm nicht viel Aufmerksamkeit geschenkt hat. Die Lizenz besagt, dass es sich um einige Treiber für Texas Instruments handelt, daher ist es unwahrscheinlich, dass die Kernprogrammierer des Kernels darauf geachtet haben. Ich bin mir ziemlich sicher, dass die Kernel-Entwickler alten Code ständig nach neuen Standards oder neuen Optimierungen aktualisieren. Es ist einfach zu groß, um sicherzustellen, dass alles aktualisiert wird!
Shahbaz
1
@ Shahbaz Mit dem "offensichtlichen" Teil meinte ich die Portabilität auf andere Betriebssysteme, was natürlich keinen Sinn ergeben würde. Aber sie scheinen sich auch nicht um die Portabilität für andere Compiler zu kümmern, sie haben so viele GCC-Erweiterungen verwendet, dass Linux wahrscheinlich nie auf einen anderen Compiler portiert wird.
Lundin
3
@ Shahbaz Was alles betrifft, was als Texas Instruments bezeichnet wird, ist TI selbst dafür berüchtigt, den nutzlosesten, beschissensten, naivsten C-Code zu produzieren, der jemals in seinen App-Notizen für verschiedene TI-Chips gesehen wurde. Wenn der Code von TI stammt, sind alle Wetten bezüglich der Möglichkeit, etwas Nützliches daraus zu interpretieren, ungültig.
Lundin
4
Es ist wahr, dass Linux und GCC untrennbar miteinander verbunden sind. Der Linux-Kernel ist auch ziemlich schwer zu verstehen (hauptsächlich, weil ein Betriebssystem sowieso kompliziert ist). Mein Punkt war jedoch, dass es nicht schön ist zu sagen, "Die Autoren des Linux-Kernels lieben es anscheinend, Dinge unnötig kompliziert und nicht standardisiert zu machen, wenn sich eine Option dazu ergibt", aufgrund einer schlechten Codierungspraxis von Drittanbietern .
Shahbaz
1

Eine andere Verwendung des Arrays mit der Länge Null ist die Verwendung einer benannten Bezeichnung innerhalb einer Struktur, um die Überprüfung des Kompilierungszeit-Strukturversatzes zu unterstützen.

Angenommen, Sie haben einige große Strukturdefinitionen (die mehrere Cache-Zeilen umfassen), die Sie sicherstellen möchten, dass sie sowohl am Anfang als auch in der Mitte, in der sie die Grenze überschreiten, an der Cache-Zeilengrenze ausgerichtet sind.

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

Im Code können Sie sie mit GCC-Erweiterungen wie folgt deklarieren:

__attribute__((aligned(CACHE_LINE_BYTES)))

Sie möchten jedoch weiterhin sicherstellen, dass dies zur Laufzeit erzwungen wird.

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

Dies würde für eine einzelne Struktur funktionieren, aber es wäre schwierig, viele Strukturen abzudecken, von denen jede einen anderen Mitgliedsnamen hat, der ausgerichtet werden muss. Sie würden höchstwahrscheinlich den folgenden Code erhalten, in dem Sie die Namen des ersten Mitglieds jeder Struktur finden müssen:

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);

Anstatt diesen Weg zu gehen, können Sie ein Array mit der Länge Null in der Struktur deklarieren, das als benannte Bezeichnung mit einem konsistenten Namen fungiert, aber keinen Speicherplatz belegt.

#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
    ....
};

Dann wäre der Laufzeit-Assertionscode viel einfacher zu pflegen:

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
quelle
Interessante Idee. Nur ein Hinweis, dass Arrays mit einer Länge von 0 vom Standard nicht zugelassen sind, daher ist dies eine compilerspezifische Sache. Es könnte auch eine gute Idee sein, die Definition von gcc für das Verhalten von Arrays mit 0 Längen in einer Strukturdefinition zu zitieren, um zumindest zu zeigen, ob vor oder nach der Deklaration ein Auffüllen eingeführt werden kann.
Shahbaz