Apa kebutuhan array dengan elemen nol?

122

Dalam kode kernel Linux saya menemukan hal berikut yang tidak dapat saya mengerti.

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

Kode di sini: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Apa kebutuhan dan tujuan dari sebuah array data dengan elemen nol?

Jeegar Patel
sumber
Saya tidak yakin apakah harus ada tag zero-length-arrays atau struct-hack ...
hippietrail
@hippietrail, karena seringkali ketika seseorang bertanya apa struct ini, mereka tidak tahu bahwa itu disebut sebagai "anggota array fleksibel". Jika ya, mereka dapat dengan mudah menemukan jawabannya. Karena tidak, mereka tidak dapat menandai pertanyaan seperti itu. Itulah mengapa kami tidak memiliki tag seperti itu.
Shahbaz
10
Beri suara untuk membuka kembali. Saya setuju bahwa ini bukan duplikat, karena tidak ada posting lain yang membahas kombinasi "struct hack" non-standar dengan panjang nol dan anggota array fleksibel fitur C99 yang terdefinisi dengan baik. Saya juga berpikir akan selalu bermanfaat bagi komunitas pemrograman C untuk menjelaskan kode yang tidak jelas dari kernel Linux. Terutama karena banyak orang memiliki kesan bahwa kernel Linux adalah semacam kode C seni, untuk alasan yang tidak diketahui. Sementara pada kenyataannya itu adalah kekacauan mengerikan yang dibanjiri eksploitasi non-standar yang tidak pernah dianggap sebagai beberapa kanon C.
Lundin
5
Bukan duplikat - ini bukan pertama kalinya saya melihat seseorang menutup pertanyaan tanpa perlu. Juga saya pikir pertanyaan ini menambah basis Pengetahuan SO.
Aniket Inge

Jawaban:

139

Ini adalah cara untuk memiliki ukuran data variabel, tanpa harus memanggil malloc( kmallocdalam kasus ini) dua kali. Anda akan menggunakannya seperti ini:

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

Ini dulunya tidak standar dan dianggap hack (seperti kata Aniket), tetapi distandarisasi di C99 . Format standarnya sekarang adalah:

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

Perhatikan bahwa Anda tidak menyebutkan ukuran apa pun untuk data bidang tersebut. Perhatikan juga bahwa variabel khusus ini hanya dapat muncul di akhir struct.


Dalam C99, hal ini dijelaskan dalam 6.7.2.1.16 (penekanan saya):

Sebagai kasus khusus, elemen terakhir dari sebuah struktur dengan lebih dari satu anggota bernama mungkin memiliki tipe array yang tidak lengkap; ini disebut anggota array yang fleksibel. Dalam kebanyakan situasi, anggota array fleksibel diabaikan. Secara khusus, ukuran struktur adalah seolah-olah anggota larik fleksibel dihilangkan kecuali bahwa ia mungkin memiliki lebih banyak bantalan tambahan daripada yang disiratkan oleh kelalaian. Namun, bila a. Operator (atau ->) memiliki operan kiri yaitu (penunjuk ke) struktur dengan anggota array yang fleksibel dan operan kanan memberi nama anggota itu, berperilaku seolah-olah anggota itu diganti dengan array terpanjang (dengan tipe elemen yang sama ) yang tidak akan membuat struktur lebih besar dari objek yang diakses; offset dari larik akan tetap menjadi anggota larik fleksibel, meskipun ini akan berbeda dari yang ada pada larik pengganti. Jika array ini tidak memiliki elemen,

Atau dengan kata lain, jika Anda memiliki:

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

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

Anda dapat mengakses var->datadengan indeks di [0, extra). Perhatikan bahwa sizeof(struct something)hanya akan memberikan akuntansi ukuran untuk variabel lain, yaitu memberikan dataukuran 0.


Mungkin menarik juga untuk mencatat bagaimana standar sebenarnya memberikan contoh mallockonstruksi seperti itu (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]));

Catatan menarik lainnya menurut standar di lokasi yang sama adalah (penekanan saya):

dengan asumsi bahwa panggilan ke malloc berhasil, objek yang ditunjukkan oleh p berperilaku, untuk sebagian besar tujuan, seolah-olah p telah dideklarasikan sebagai:

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

(ada keadaan di mana kesetaraan ini rusak; khususnya, offset anggota d mungkin tidak sama ).

Shahbaz
sumber
Untuk lebih jelasnya, kode asli dalam pertanyaan tersebut masih belum standar di C99 (maupun C11), dan masih akan dianggap sebagai peretasan. Standardisasi C99 harus menghilangkan ikatan larik.
MM
Apa [0, extra)?
SS Anne
36

Ini sebenarnya adalah retasan, untuk GCC ( C90 ) sebenarnya.

Ini juga disebut hack struct .

Jadi lain kali, saya akan mengatakan:

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

Ini akan sama dengan mengatakan:

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

Dan saya dapat membuat sejumlah objek struct semacam itu.

Aniket Inge
sumber
7

Idenya adalah untuk memungkinkan array berukuran variabel di akhir struct. Agaknya, bts_actionadalah beberapa paket data dengan header berukuran tetap ( bidang typedan size), dan dataanggota berukuran variabel . Dengan mendeklarasikannya sebagai larik dengan panjang 0, ia dapat diindeks sama seperti larik lainnya. Anda kemudian akan mengalokasikan bts_actionstruct, katakanlah dataukuran 1024-byte , seperti ini:

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

Lihat juga: http://c2.com/cgi/wiki?StructHack

Sheu
sumber
2
@ Aniket: Saya tidak sepenuhnya yakin dari mana ide itu muncul .
sheu
di C ++ ya, di C, tidak diperlukan.
amc
2
@sheu, ini berasal dari fakta bahwa gaya penulisan mallocAnda membuat Anda berulang kali dan jika ada jenis actionperubahan, Anda harus memperbaikinya beberapa kali. Bandingkan dua hal berikut ini untuk Anda sendiri dan Anda akan tahu: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));Yang kedua lebih pendek, lebih bersih dan jelas lebih mudah diubah.
Shahbaz
5

Kode tersebut tidak valid C ( lihat ini ). Kernel Linux, karena alasan yang jelas, sama sekali tidak peduli dengan portabilitas, jadi ia menggunakan banyak kode non-standar.

Apa yang mereka lakukan adalah ekstensi non-standar GCC dengan ukuran larik 0. Program yang memenuhi standar akan menulis u8 data[];dan artinya akan sama. Penulis kernel Linux tampaknya suka membuat hal-hal yang tidak perlu menjadi rumit dan tidak standar, jika opsi untuk melakukannya muncul dengan sendirinya.

Dalam standar C yang lebih lama, mengakhiri struct dengan array kosong dikenal sebagai "the struct hack". Orang lain telah menjelaskan tujuannya dalam jawaban lain. Peretasan struct, dalam standar C90, adalah perilaku yang tidak ditentukan dan dapat menyebabkan crash, terutama karena kompiler C bebas untuk menambahkan sejumlah byte padding di akhir struct. Byte padding tersebut dapat bertabrakan dengan data yang Anda coba "retas" di akhir struct.

GCC sejak awal membuat ekstensi non-standar untuk mengubahnya dari perilaku tidak terdefinisi menjadi perilaku terdefinisi dengan baik. Standar C99 kemudian mengadaptasi konsep ini dan program C modern apa pun dapat menggunakan fitur ini tanpa risiko. Ini dikenal sebagai anggota larik fleksibel di C99 / C11.

Lundin
sumber
3
Saya ragu bahwa "kernel linux tidak peduli dengan portabilitas". Mungkin maksud Anda portabilitas untuk kompiler lain? Memang benar itu cukup terjalin dengan fitur gcc.
Shahbaz
3
Namun demikian, menurut saya bagian kode khusus ini bukanlah kode utama dan mungkin ditinggalkan karena pembuatnya tidak terlalu memperhatikannya. Lisensi mengatakan ini tentang beberapa driver instrumen texas, jadi tidak mungkin programmer inti dari kernel memperhatikannya. Saya cukup yakin pengembang kernel terus memperbarui kode lama sesuai dengan standar baru atau pengoptimalan baru. Itu terlalu besar untuk memastikan semuanya diperbarui!
Shahbaz
1
@Shahbaz Dengan bagian yang "jelas", yang saya maksud adalah portabilitas ke sistem operasi lain, yang secara alami tidak masuk akal. Tetapi mereka juga tidak peduli tentang portabilitas ke kompiler lain, mereka telah menggunakan begitu banyak ekstensi GCC sehingga Linux kemungkinan besar tidak akan pernah di-porting ke kompiler lain.
Lundin
3
@Shahbaz Mengenai kasus apa pun yang berlabel Texas Instruments, TI sendiri terkenal karena menghasilkan kode C yang paling tidak berguna, jelek, dan naif yang pernah ada, dalam catatan aplikasi mereka untuk berbagai chip TI. Jika kode tersebut berasal dari TI, maka semua taruhan mengenai peluang menafsirkan sesuatu yang berguna darinya dibatalkan.
Lundin
4
Memang benar bahwa linux dan gcc tidak dapat dipisahkan. Kernel Linux juga cukup sulit untuk dipahami (kebanyakan karena OS itu rumit). Maksud saya, adalah tidak baik untuk mengatakan "Penulis kernel Linux tampaknya suka membuat hal-hal yang tidak perlu rumit dan tidak standar, jika opsi untuk melakukannya mengungkapkan dirinya" karena praktik pengkodean yang buruk pihak ketiga-ish .
Shahbaz
1

Penggunaan lain dari array panjang nol adalah sebagai label bernama di dalam struct untuk membantu waktu kompilasi pemeriksaan offset struct.

Misalkan Anda memiliki beberapa definisi struct besar (mencakup beberapa baris cache) yang ingin Anda pastikan mereka sejajar dengan batas baris cache baik di awal dan di tengah di mana ia melintasi batas.

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

Dalam kode, Anda dapat mendeklarasikannya menggunakan ekstensi GCC seperti:

__attribute__((aligned(CACHE_LINE_BYTES)))

Namun Anda tetap ingin memastikan ini diterapkan dalam runtime.

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

Ini akan berfungsi untuk satu struct, tetapi akan sulit untuk mencakup banyak struct, masing-masing memiliki nama anggota yang berbeda untuk disejajarkan. Anda kemungkinan besar akan mendapatkan kode seperti di bawah ini di mana Anda harus menemukan nama anggota pertama dari setiap struct:

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

Alih-alih menggunakan cara ini, Anda dapat mendeklarasikan array dengan panjang nol di struct yang bertindak sebagai label bernama dengan nama yang konsisten tetapi tidak menggunakan spasi.

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

Maka kode pernyataan waktu proses akan jauh lebih mudah dikelola:

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
sumber
Ide yang menarik. Sebagai catatan, array dengan panjang 0 tidak diperbolehkan oleh standar, jadi ini adalah hal khusus kompilator. Selain itu, mungkin merupakan ide yang baik untuk mengutip definisi gcc tentang perilaku array dengan panjang 0 dalam definisi struct, paling tidak untuk menunjukkan apakah ia dapat memperkenalkan padding sebelum atau sesudah deklarasi.
Shahbaz