อาร์เรย์ที่มีองค์ประกอบศูนย์ต้องการอะไร


122

ในโค้ดเคอร์เนลของลินุกซ์ฉันพบสิ่งต่อไปนี้ซึ่งฉันไม่เข้าใจ

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

รหัสอยู่ที่นี่: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

ความต้องการและวัตถุประสงค์ของอาร์เรย์ข้อมูลที่มีองค์ประกอบเป็นศูนย์คืออะไร?


ฉันไม่แน่ใจว่าควรมีแท็กอาร์เรย์ที่มีความยาวเป็นศูนย์หรือแท็กแฮ็ก ...
hippietrail

@hippietrail เพราะบ่อยครั้งเมื่อมีคนถามว่าโครงสร้างนี้คืออะไรพวกเขาไม่รู้ว่ามันเรียกว่า "สมาชิกอาร์เรย์แบบยืดหยุ่น" หากเป็นเช่นนั้นพวกเขาจะพบคำตอบได้อย่างง่ายดาย เนื่องจากไม่มีจึงไม่สามารถแท็กคำถามเช่นนี้ได้ นั่นคือเหตุผลที่เราไม่มีแท็กดังกล่าว
Shahbaz

10
โหวตเพื่อเปิดอีกครั้ง ฉันยอมรับว่านี่ไม่ใช่รายการที่ซ้ำกันเนื่องจากไม่มีโพสต์อื่นใดที่กล่าวถึงการรวมกันของ "การแฮ็กโครงสร้าง" ที่ไม่ได้มาตรฐานที่มีความยาวเป็นศูนย์และคุณสมบัติ C99 ที่กำหนดไว้อย่างดีมีสมาชิกอาร์เรย์ที่ยืดหยุ่นได้ ฉันคิดว่ามันเป็นประโยชน์สำหรับชุมชนการเขียนโปรแกรม C เสมอในการให้ความกระจ่างเกี่ยวกับโค้ดที่คลุมเครือจากเคอร์เนลลินุกซ์ เนื่องจากหลายคนมีความรู้สึกว่าเคอร์เนล Linux เป็นรหัส C ที่ทันสมัยโดยไม่ทราบสาเหตุ ในขณะที่ในความเป็นจริงมันเป็นความยุ่งเหยิงที่ท่วมท้นไปด้วยการหาประโยชน์ที่ไม่ได้มาตรฐานซึ่งไม่ควรถือว่าเป็น C Canon บางตัว
Lundin

5
ไม่ใช่ครั้งแรกที่ฉันเห็นคนปิดคำถามโดยไม่จำเป็น นอกจากนี้ฉันคิดว่าคำถามนี้ช่วยเพิ่มฐานความรู้ SO
Aniket Inge

คำตอบ:


139

นี่เป็นวิธีที่จะมีขนาดของข้อมูลที่แปรผันโดยไม่ต้องเรียกmalloc( kmallocในกรณีนี้) สองครั้ง คุณจะใช้มันดังนี้:

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

นี้เคยเป็นไม่ได้มาตรฐานและได้รับการพิจารณาสับ (ตามที่กล่าว Aniket) แต่มันก็เป็นมาตรฐานใน C99 รูปแบบมาตรฐานสำหรับตอนนี้คือ:

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

โปรดทราบว่าคุณไม่ได้ระบุขนาดใด ๆ สำหรับdataฟิลด์ โปรดทราบว่าตัวแปรพิเศษนี้สามารถอยู่ที่ส่วนท้ายของโครงสร้างเท่านั้น


ใน C99 เรื่องนี้อธิบายไว้ใน 6.7.2.1.16 (เน้นของฉัน):

ในกรณีพิเศษองค์ประกอบสุดท้ายของโครงสร้างที่มีสมาชิกที่มีชื่อมากกว่าหนึ่งคนอาจมีประเภทอาร์เรย์ที่ไม่สมบูรณ์ สิ่งนี้เรียกว่าสมาชิกอาร์เรย์ที่ยืดหยุ่น. ในสถานการณ์ส่วนใหญ่สมาชิกอาร์เรย์แบบยืดหยุ่นจะถูกละเว้น โดยเฉพาะอย่างยิ่งขนาดของโครงสร้างจะเหมือนกับว่าสมาชิกอาร์เรย์แบบยืดหยุ่นถูกละไว้ยกเว้นว่าอาจมีช่องว่างต่อท้ายมากกว่าที่การละเว้นจะบ่งบอกได้ อย่างไรก็ตามเมื่อก. (หรือ ->) ตัวดำเนินการมีตัวถูกดำเนินการด้านซ้ายซึ่งเป็น (ตัวชี้ไปที่) โครงสร้างที่มีสมาชิกอาร์เรย์แบบยืดหยุ่นและชื่อตัวถูกดำเนินการด้านขวานั้นจะทำงานราวกับว่าสมาชิกนั้นถูกแทนที่ด้วยอาร์เรย์ที่ยาวที่สุด (ด้วยประเภทองค์ประกอบเดียวกัน ) ที่จะไม่ทำให้โครงสร้างมีขนาดใหญ่กว่าวัตถุที่เข้าถึง ค่าชดเชยของอาร์เรย์จะยังคงเป็นของสมาชิกอาร์เรย์ที่ยืดหยุ่นแม้ว่าจะแตกต่างจากอาร์เรย์ทดแทนก็ตาม ถ้าอาร์เรย์นี้ไม่มีองค์ประกอบ

หรือกล่าวอีกนัยหนึ่งถ้าคุณมี:

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

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

คุณสามารถเข้าถึงvar->dataด้วยดัชนีในรูปแบบ[0, extra). โปรดทราบว่าsizeof(struct something)จะให้การบัญชีขนาดสำหรับตัวแปรอื่น ๆ เท่านั้นเช่นให้dataขนาด 0


อาจเป็นเรื่องที่น่าสนใจที่จะสังเกตว่ามาตรฐานให้ตัวอย่างของโครงสร้างmallocดังกล่าวอย่างไร (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]));

ข้อสังเกตที่น่าสนใจอีกประการหนึ่งตามมาตรฐานในตำแหน่งเดียวกันคือ (เน้นของฉัน):

สมมติว่าการโทรไปยัง malloc สำเร็จวัตถุที่ชี้โดย p จะทำงานตามวัตถุประสงค์ส่วนใหญ่ราวกับว่า p ได้รับการประกาศเป็น:

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

(มีบางกรณีที่การเทียบเท่านี้หักโดยเฉพาะอย่างยิ่งการชดเชยของสมาชิก d อาจไม่เหมือนกัน )


เพื่อความชัดเจนรหัสเดิมในคำถามยังไม่เป็นมาตรฐานใน C99 (หรือ C11) และยังถือว่าเป็นการแฮ็ก มาตรฐาน C99 ต้องละเว้นอาร์เรย์ที่ผูกไว้
MM

คืออะไร[0, extra)?
SS Anne


36

นี่คือการแฮ็กสำหรับGCC ( C90 ) ในความเป็นจริง

เรียกอีกอย่างว่าแฮ็คโครงสร้าง

ครั้งต่อไปฉันจะพูดว่า:

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

จะเทียบเท่ากับการพูดว่า:

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

และฉันสามารถสร้างวัตถุโครงสร้างจำนวนเท่าใดก็ได้


7

แนวคิดคือการอนุญาตให้มีอาร์เรย์ขนาดตัวแปรที่ส่วนท้ายของโครงสร้าง สันนิษฐานว่าbts_actionเป็นแพ็กเก็ตข้อมูลบางส่วนที่มีส่วนหัวขนาดคงที่ ( typeและsizeฟิลด์) และdataสมาชิกขนาดตัวแปร การประกาศเป็นอาร์เรย์ความยาว 0 ทำให้สามารถสร้างดัชนีได้เช่นเดียวกับอาร์เรย์อื่น ๆ จากนั้นคุณจะจัดสรรโครงสร้างที่มีbts_actionขนาด 1024 ไบต์dataดังนี้:

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

ดูเพิ่มเติมที่: http://c2.com/cgi/wiki?StructHack


2
@Aniket: ฉันไม่แน่ใจว่าแนวคิดนั้นมาจากไหน
sheu

ใน C ++ ใช่ใน C ไม่จำเป็น
amc

2
@sheu มันมาจากการที่สไตล์การเขียนของmallocคุณทำให้คุณต้องทำซ้ำหลาย ๆ ครั้งและหากมีการactionเปลี่ยนแปลงประเภทนี้คุณต้องแก้ไขหลาย ๆ ครั้ง เปรียบเทียบสองข้อต่อไปนี้ด้วยตัวคุณเองแล้วคุณจะรู้ว่าstruct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));เทียบกับอันstruct some_thing *variable = malloc(10 * sizeof(*variable));ที่สองสั้นกว่าสะอาดกว่าและเปลี่ยนง่ายกว่าอย่างชัดเจน
Shahbaz

5

รหัสไม่ถูกต้อง C ( ดูสิ่งนี้ ) เคอร์เนลลินุกซ์นั้นมีเหตุผลที่ชัดเจนไม่เกี่ยวข้องกับการพกพาเลยแม้แต่น้อยดังนั้นจึงใช้โค้ดที่ไม่ได้มาตรฐานจำนวนมาก

สิ่งที่พวกเขากำลังทำคือส่วนขยายที่ไม่ได้มาตรฐาน GCC ที่มีขนาดอาร์เรย์ 0 โปรแกรมที่เป็นไปตามมาตรฐานจะเขียนขึ้นu8 data[];และมันจะมีความหมายเหมือนกัน เห็นได้ชัดว่าผู้เขียนเคอร์เนลลินุกซ์ชอบที่จะทำให้สิ่งต่างๆซับซ้อนและไม่ได้มาตรฐานโดยไม่จำเป็นหากตัวเลือกในการทำเช่นนั้นเปิดเผยตัวเอง

ในมาตรฐาน C รุ่นเก่าการสิ้นสุดโครงสร้างด้วยอาร์เรย์ว่างเรียกว่า "การแฮ็กโครงสร้าง" คนอื่น ๆ ได้อธิบายจุดประสงค์แล้วในคำตอบอื่น ๆ การแฮ็กโครงสร้างในมาตรฐาน C90 เป็นพฤติกรรมที่ไม่ได้กำหนดไว้และอาจทำให้เกิดปัญหาได้เนื่องจากคอมไพเลอร์ C มีอิสระที่จะเพิ่มไบต์ช่องว่างภายในจำนวนเท่าใดก็ได้ที่ส่วนท้ายของโครงสร้าง ไบต์ช่องว่างภายในดังกล่าวอาจชนกับข้อมูลที่คุณพยายาม "แฮ็ก" ในตอนท้ายของโครงสร้าง

GCC ในช่วงต้นได้สร้างส่วนขยายที่ไม่ได้มาตรฐานเพื่อเปลี่ยนจากพฤติกรรมที่ไม่ได้กำหนดเป็นพฤติกรรมที่กำหนดไว้อย่างชัดเจน จากนั้นมาตรฐาน C99 จึงปรับแนวคิดนี้และโปรแกรม C สมัยใหม่ใด ๆ จึงสามารถใช้คุณลักษณะนี้ได้โดยไม่มีความเสี่ยง เรียกว่าสมาชิกอาร์เรย์แบบยืดหยุ่นใน C99 / C11


3
ฉันสงสัยว่า "เคอร์เนล linux ไม่เกี่ยวข้องกับการพกพา" บางทีคุณอาจหมายถึงการพกพาไปยังคอมไพเลอร์อื่น ๆ เป็นความจริงที่ว่ามันค่อนข้างเกี่ยวพันกับคุณสมบัติของ gcc
Shahbaz

3
อย่างไรก็ตามฉันคิดว่าโค้ดชิ้นนี้ไม่ใช่โค้ดกระแสหลักและอาจถูกทิ้งเนื่องจากผู้เขียนไม่ได้ให้ความสนใจกับมันมากนัก ใบอนุญาตกล่าวเกี่ยวกับไดรเวอร์เครื่องมือของเท็กซัสดังนั้นจึงไม่น่าเป็นไปได้ที่โปรแกรมเมอร์หลักของเคอร์เนลจะให้ความสนใจกับมัน ฉันค่อนข้างมั่นใจว่านักพัฒนาเคอร์เนลกำลังอัปเดตโค้ดเก่าอยู่ตลอดเวลาตามมาตรฐานใหม่หรือการเพิ่มประสิทธิภาพใหม่ มันใหญ่เกินไปที่จะทำให้ทุกอย่างได้รับการอัปเดต!
Shahbaz

1
@Shahbaz ด้วยส่วนที่ "ชัดเจน" ฉันหมายถึงการพกพาไปยังระบบปฏิบัติการอื่น ๆ ซึ่งโดยธรรมชาติแล้วจะไม่สมเหตุสมผลเลย แต่ดูเหมือนว่าพวกเขาจะไม่ให้ความสำคัญกับความสามารถในการพกพาไปยังคอมไพเลอร์อื่น ๆ เช่นกันพวกเขาใช้ส่วนขยาย GCC จำนวนมากที่ Linux ไม่น่าจะถูกย้ายไปยังคอมไพเลอร์อื่น
Lundin

3
@ Shahbaz สำหรับกรณีของสิ่งที่มีชื่อว่า Texas Instruments TI เองก็มีชื่อเสียงในการผลิตโค้ด C ที่ไร้ประโยชน์ไร้เดียงสาและไร้เดียงสาที่สุดเท่าที่เคยเห็นมาในบันทึกย่อของแอพสำหรับชิป TI ต่างๆ หากรหัสมาจาก TI การเดิมพันทั้งหมดเกี่ยวกับโอกาสในการตีความสิ่งที่เป็นประโยชน์จากรหัสนั้นจะถูกปิด
Lundin

4
เป็นเรื่องจริงที่ linux และ gcc แยกกันไม่ออก เคอร์เนลลินุกซ์ยังค่อนข้างเข้าใจยาก (ส่วนใหญ่เป็นเพราะระบบปฏิบัติการมีความซับซ้อนอยู่แล้ว) แม้ว่าประเด็นของฉันก็คือมันไม่ดีที่จะพูดว่า "ดูเหมือนว่าผู้เขียนเคอร์เนล Linux ชอบที่จะทำสิ่งต่าง ๆ ที่ซับซ้อนและไม่ได้มาตรฐานโดยไม่จำเป็นหากตัวเลือกในการทำเช่นนั้นเผยให้เห็นตัวเอง" เนื่องจากวิธีการเขียนโค้ดของบุคคลที่สามไม่ดี .
Shahbaz

1

การใช้อาร์เรย์ที่มีความยาวเป็นศูนย์อีกอย่างหนึ่งคือการตั้งชื่อป้ายกำกับภายในโครงสร้างเพื่อช่วยในการคอมไพล์การตรวจสอบการชดเชยโครงสร้างเวลา

สมมติว่าคุณมีคำจำกัดความของโครงสร้างขนาดใหญ่ (ครอบคลุมบรรทัดแคชหลายบรรทัด) ที่คุณต้องการตรวจสอบให้แน่ใจว่ามีการจัดแนวให้ตรงกับขอบเขตบรรทัดแคชทั้งในจุดเริ่มต้นและตรงกลางโดยที่มันข้ามขอบเขต

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

ในโค้ดคุณสามารถประกาศได้โดยใช้ส่วนขยาย GCC เช่น:

__attribute__((aligned(CACHE_LINE_BYTES)))

แต่คุณยังคงต้องการให้แน่ใจว่าสิ่งนี้ถูกบังคับใช้ในรันไทม์

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

สิ่งนี้ใช้ได้กับโครงสร้างเดียว แต่มันยากที่จะครอบคลุมหลายโครงสร้างแต่ละส่วนมีชื่อสมาชิกที่แตกต่างกันเพื่อจัดแนว คุณมักจะได้รับรหัสด้านล่างซึ่งคุณต้องหาชื่อของสมาชิกตัวแรกของแต่ละโครงสร้าง:

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

แทนที่จะทำแบบนี้คุณสามารถประกาศอาร์เรย์ที่มีความยาวเป็นศูนย์ในโครงสร้างที่ทำหน้าที่เป็นป้ายชื่อที่มีชื่อที่สอดคล้องกัน แต่ไม่กินเนื้อที่ใด ๆ

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

จากนั้นรหัสยืนยันรันไทม์จะดูแลรักษาง่ายกว่ามาก:

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

ความคิดที่น่าสนใจ โปรดทราบว่ามาตรฐานไม่อนุญาตให้ใช้อาร์เรย์ความยาว 0 ดังนั้นนี่จึงเป็นสิ่งเฉพาะของคอมไพเลอร์ นอกจากนี้คุณควรอ้างคำจำกัดความของ gcc เกี่ยวกับพฤติกรรมของอาร์เรย์ที่มีความยาว 0 ในคำจำกัดความของโครงสร้างอย่างน้อยที่สุดเพื่อแสดงว่าสามารถแนะนำช่องว่างก่อนหรือหลังการประกาศได้หรือไม่
Shahbaz
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.