零元素数组的需求是什么?


122

在Linux内核代码中,我发现了以下我无法理解的东西。

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

代码在这里:http : //lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

零元素数据数组的需求和目的是什么?


我不确定是否应该使用零长度数组struct-hack标签……
hippietrail

@hippietrail,因为通常当有人问这个结构是什么时,他们不知道它被称为“灵活数组成员”。如果他们这样做,他们很容易就能找到答案。由于他们没有,所以他们不能这样标记问题。这就是为什么我们没有这样的标签。
2013年

10
投票重新开放。我同意这不是重复的,因为其他任何一篇文章都没有解决长度为零的非标准“结构hack”和定义明确的C99功能灵活数组成员的组合。我还认为,对Linux内核中任何晦涩的代码有所了解,对于C编程社区总是有益的。主要是由于许多人给人的印象是Linux内核是某种最新的C代码,原因是未知的。实际上,这是一个充斥着非标准漏洞的可怕烂摊子,这些漏洞永远都不应被视为C规范。
隆丁

5
不是重复的-这不是我第一次没有看到有人不必要地回答问题。我也认为这个问题增加了SO知识库。
Aniket Inge 2013年

Answers:


139

这是一种具有可变大小的数据的方法,而不必调用两次mallockmalloc在这种情况下)。您可以这样使用它:

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安妮

2
@ JL2210,en.wikipedia.org
Shahbaz

36

实际上,这实际上是对GCCC90)的一种破解。

也称为struct hack

所以下一次,我会说:

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

相当于说:

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

而且我可以创建任意数量的这种struct对象。


7

这个想法是在结构的末尾允许一个可变大小的数组。大概bts_action是一些具有固定大小的标头(typesize字段)和可变大小的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));第二个相比,第二个更短,更清洁并且很容易更改。
2013年

5

该代码不是有效的C(请参阅)。出于显而易见的原因,Linux内核丝毫不关心可移植性,因此它使用了大量非标准代码。

他们正在做的是一个数组大小为0的GCC非标准扩展。一个符合标准的程序就已经编写了u8 data[];,这意味着完全相同的事情。Linux内核的作者显然喜欢使事情变得不必要的复杂和非标准,如果可以这样做的话。

在较早的C标准中,以空数组结尾的结构称为“结构hack”。其他人已经在其他答案中解释了其目的。在C90标准中,结构hack是未定义的行为,并且可能导致崩溃,主要是因为C编译器可以在结构的末尾随意添加任意数量的填充字节。这样的填充字节可能与您试图在结构末尾“入侵”的数据相冲突。

GCC早期进行了非标准的扩展,以将其从未定义的行为更改为定义良好的行为。然后,C99标准适应了这一概念,因此任何现代C程序都可以毫无风险地使用此功能。在C99 / C11中称为柔性阵列成员


3
我怀疑“ Linux内核与可移植性无关”。也许您是想移植到其他编译器?确实,它与gcc的功能交织在一起。
2013年

3
尽管如此,我认为这段特定代码不是主流代码,可能因为它的作者没有过多注意而被遗漏了。该许可证说明了有关某些德克萨斯仪器驱动程序的信息,因此内核的核心程序员不太可能会对此予以关注。我很确定内核开发人员会根据新标准或新优化不断更新旧代码。它太大了,无法确保所有内容都已更新!
2013年

1
@Shahbaz对于“显而易见的”部分,我的意思是可移植到其他操作系统,这自然没有任何意义。但是他们似乎也没有给其他编译器带来可移植性,他们使用了如此多的GCC扩展,因此Linux不太可能移植到另一个编译器。
隆丁

3
@Shahbaz对于任何贴有德州仪器(TI)的东西,TI本身在其各种TI芯片的应用笔记中都因产生有史以来最无用,糟糕透顶,天真的C代码而臭名昭著。如果代码源自TI,那么所有关于从中解释有用内容的机会都无法确定。
隆丁

4
确实Linux和gcc是密不可分的。Linux内核也很难理解(主要是因为OS仍然很复杂)。但我的观点是,由于第三方的不良编码习惯,“说Linux内核的作者显然喜欢使事情变得不必要的复杂和非标准,如果这样做可以使自己暴露出来,那是不好的” 。
2013年

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.