Qual è la necessità di un array con zero elementi?


122

Nel codice del kernel Linux ho trovato la seguente cosa che non riesco a capire.

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

Il codice è qui: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Qual è la necessità e lo scopo di una matrice di dati con zero elementi?


Non sono sicuro se dovrebbe esserci un tag zero-length-array o struct-hack ...
hippietrail

@hippietrail, perché spesso quando qualcuno chiede cosa sia questa struttura, non sa che è indicato come "membro di array flessibile". Se lo avessero fatto, avrebbero potuto facilmente trovare la loro risposta. Dal momento che non lo fanno, non possono contrassegnare la domanda come tale. Ecco perché non abbiamo un tag del genere.
Shahbaz

10
Vota per riaprire. Sono d'accordo che questo non fosse un duplicato, perché nessuno degli altri post affronta la combinazione di uno "struct hack" non standard con lunghezza zero e il membro dell'array flessibile con funzionalità C99 ben definito. Penso anche che sia sempre un vantaggio per la comunità di programmatori C fare luce su qualsiasi codice oscuro dal kernel Linux. Soprattutto perché molte persone hanno l'impressione che il kernel Linux sia una sorta di codice C all'avanguardia, per ragioni sconosciute. Mentre in realtà è un terribile pasticcio inondato di exploit non standard che non dovrebbero mai essere considerati come un canone C.
Lundin

5
Non è un duplicato: non è la prima volta che vedo qualcuno chiudere una domanda inutilmente. Inoltre penso che questa domanda si aggiunge alla base di SO Knowledge.
Aniket Inge

Risposte:


139

Questo è un modo per avere dimensioni variabili dei dati, senza dover chiamare malloc( kmallocin questo caso) due volte. Lo useresti in questo modo:

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

Questo non era standard ed era considerato un hack (come ha detto Aniket), ma è stato standardizzato in C99 . Il formato standard per ora è:

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

Nota che non menzioni alcuna dimensione per il datacampo. Notare anche che questa variabile speciale può arrivare solo alla fine della struttura.


In C99, questo argomento è spiegato in 6.7.2.1.16 (enfasi mia):

In un caso speciale, l'ultimo elemento di una struttura con più di un membro denominato può avere un tipo di matrice incompleto; questo è chiamato un membro di array flessibile. Nella maggior parte delle situazioni, il membro della matrice flessibile viene ignorato. In particolare, la dimensione della struttura è come se il membro della matrice flessibile fosse omesso, tranne per il fatto che potrebbe avere un riempimento finale maggiore di quanto l'omissione implicherebbe. Tuttavia, quando a. (o ->) ha un operando di sinistra che è (un puntatore a) una struttura con un membro di array flessibile e l'operando di destra nomina quel membro, si comporta come se quel membro fosse sostituito con l'array più lungo (con lo stesso tipo di elemento ) che non renderebbe la struttura più grande dell'oggetto a cui si accede; l'offset della matrice rimarrà quella del membro della matrice flessibile, anche se questo sarebbe diverso da quello della matrice sostitutiva. Se questo array non avesse elementi,

O in altre parole, se hai:

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

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

Puoi accedere var->datacon indici in [0, extra). Nota che sizeof(struct something)darà solo la dimensione che tiene conto delle altre variabili, cioè dà datauna dimensione di 0.


Può essere interessante anche notare come lo standard fornisca effettivamente esempi di mallocing di un tale costrutto (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]));

Un'altra nota interessante dello standard nella stessa posizione è (enfasi mia):

supponendo che la chiamata a malloc abbia successo, l'oggetto puntato da p si comporta, per la maggior parte degli scopi, come se p fosse stato dichiarato come:

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

(ci sono circostanze in cui questa equivalenza viene interrotta; in particolare, gli offset del membro d potrebbero non essere gli stessi ).


Per essere chiari, il codice originale nella domanda non è ancora standard in C99 (né C11) e sarebbe comunque considerato un hack. La standardizzazione C99 deve omettere il limite dell'array.
MM

Cosa [0, extra)?
SS Anne


36

Questo è un trucco in realtà, per GCC ( C90 ) in effetti.

È anche chiamato struct hack .

Quindi la prossima volta direi:

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

Sarà equivalente a dire:

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

E posso creare un numero qualsiasi di tali oggetti struct.


7

L'idea è di consentire un array di dimensioni variabili alla fine della struttura. Presumibilmente, bts_actionè un pacchetto di dati con un'intestazione di dimensione fissa (i campi typee size) e un datamembro di dimensione variabile . Dichiarandolo come un array di lunghezza 0, può essere indicizzato come qualsiasi altro array. Dovresti quindi allocare una bts_actionstruttura, ad esempio una datadimensione di 1024 byte , in questo modo:

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

Vedi anche: http://c2.com/cgi/wiki?StructHack


2
@Aniket: non sono del tutto sicuro da dove venga questa idea.
sheu

in C ++ sì, in C, non necessario.
amc

2
@sheu, nasce dal fatto che il tuo stile di scrittura mallocti fa ripetere più volte e se mai il tipo di actionmodifiche, devi aggiustarlo più volte. Confronta i seguenti due per te stesso e saprai: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));Il secondo è più breve, più pulito e chiaramente più facile da cambiare.
Shahbaz

5

Il codice non è valido C ( vedi questo ). Il kernel Linux, per ovvie ragioni, non si preoccupa minimamente della portabilità, quindi utilizza molto codice non standard.

Quello che stanno facendo è un'estensione GCC non standard con dimensione dell'array 0. Un programma conforme allo standard avrebbe scritto u8 data[];e avrebbe significato la stessa cosa. Gli autori del kernel Linux apparentemente amano rendere le cose inutilmente complicate e non standard, se si rivela un'opzione per farlo.

Nei vecchi standard C, terminare una struttura con un array vuoto era noto come "the struct hack". Altri ne hanno già spiegato lo scopo in altre risposte. Lo struct hack, nello standard C90, era un comportamento indefinito e poteva causare crash, principalmente perché un compilatore C è libero di aggiungere un numero qualsiasi di byte di riempimento alla fine della struttura. Tali byte di riempimento possono entrare in conflitto con i dati che hai tentato di "hackerare" alla fine della struttura.

GCC all'inizio ha fatto un'estensione non standard per cambiare questo comportamento da non definito a ben definito. Lo standard C99 ha quindi adattato questo concetto e qualsiasi programma C moderno può quindi utilizzare questa funzione senza rischi. È noto come membro di array flessibile in C99 / C11.


3
Dubito che "il kernel Linux non sia interessato alla portabilità". Forse intendevi la portabilità ad altri compilatori? È vero che è abbastanza intrecciato con le funzionalità di gcc.
Shahbaz

3
Tuttavia, penso che questo particolare pezzo di codice non sia un codice mainstream ed è probabilmente escluso perché il suo autore non ha prestato molta attenzione ad esso. La licenza dice che riguarda alcuni driver di Texas Instruments, quindi è improbabile che i programmatori principali del kernel prestino attenzione. Sono abbastanza sicuro che gli sviluppatori del kernel aggiornino costantemente il vecchio codice secondo nuovi standard o nuove ottimizzazioni. È semplicemente troppo grande per assicurarsi che tutto sia aggiornato!
Shahbaz

1
@ Shahbaz Con la parte "ovvia" intendevo la portabilità ad altri sistemi operativi, che naturalmente non avrebbe alcun senso. Ma a loro non sembra importare nemmeno la portabilità ad altri compilatori, hanno usato così tante estensioni GCC che Linux probabilmente non sarà mai portato su un altro compilatore.
Lundin

3
@Shahbaz Per quanto riguarda il caso di qualsiasi cosa etichettata Texas Instruments, TI stessa è nota per aver prodotto il codice C più inutile, schifoso e ingenuo mai visto, nelle loro note app per vari chip TI. Se il codice proviene da TI, tutte le scommesse riguardanti la possibilità di interpretare qualcosa di utile da esso sono disattivate.
Lundin

4
È vero che linux e gcc sono inseparabili. Anche il kernel Linux è abbastanza difficile da capire (soprattutto perché un sistema operativo è comunque complicato). Quello che volevo dire, però, era che non è carino dire "Gli autori del kernel Linux apparentemente amano rendere le cose inutilmente complicate e non standard, se un'opzione per farlo si rivela" a causa di una cattiva pratica di codifica di terze parti .
Shahbaz

1

Un altro utilizzo dell'array di lunghezza zero è come etichetta con nome all'interno di una struttura per assistere il controllo dell'offset della struttura in fase di compilazione.

Supponiamo di avere alcune definizioni di strutture di grandi dimensioni (che si estendono su più linee di cache) per assicurarti che siano allineate al limite della linea di cache sia all'inizio che al centro, dove attraversa il confine.

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

Nel codice puoi dichiararli usando estensioni GCC come:

__attribute__((aligned(CACHE_LINE_BYTES)))

Ma vuoi comunque assicurarti che venga applicato in runtime.

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

Ciò funzionerebbe per una singola struttura, ma sarebbe difficile coprire molte strutture, ciascuna con un nome membro diverso da allineare. Molto probabilmente otterrai codice come di seguito in cui devi trovare i nomi del primo membro di ogni struttura:

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

Invece di andare in questo modo, puoi dichiarare un array di lunghezza zero nella struttura che funge da etichetta con un nome coerente ma non occupa spazio.

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

Quindi il codice di asserzione di runtime sarebbe molto più facile da mantenere:

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

Idea interessante. Solo una nota che gli array di lunghezza 0 non sono consentiti dallo standard, quindi questa è una cosa specifica del compilatore. Inoltre, potrebbe essere una buona idea citare la definizione di gcc del comportamento degli array di lunghezza 0 in una definizione di struttura, per lo meno per mostrare se potrebbe introdurre il riempimento prima o dopo la dichiarazione.
Shahbaz
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.