【问题标题】:What's the need of array with zero elements?零元素的数组需要什么?
【发布时间】:2013-01-16 14:16:18
【问题描述】:

在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

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

【问题讨论】:

  • 我不确定是否应该有zero-length-arraysstruct-hack 标签...
  • @hippietrail,因为经常有人问这个结构是什么时,他们不知道它被称为“灵活数组成员”。如果他们这样做了,他们很容易找到答案。因为他们不这样做,所以他们不能这样标记问题。这就是为什么我们没有这样的标签。
  • 投票重新开放。我同意这不是重复的,因为其他帖子都没有解决长度为零的非标准“struct hack”和定义明确的 C99 功能灵活数组成员的组合。我还认为 C 编程社区对 Linux 内核中任何晦涩的代码有所了解总是有益的。主要是因为许多人认为 Linux 内核是某种最先进的 C 代码,原因不明。虽然实际上它是一个可怕的混乱,充斥着不应该被视为某些 C 规范的非标准漏洞。
  • 不是重复的 - 我不是第一次看到有人不必要地关闭问题。我也认为这个问题增加了 SO 知识库。

标签: c structure flexible-array-member


【解决方案1】:

这是一种具有可变数据大小的方法,无需调用malloc(在本例中为kmalloc)两次。你可以这样使用它:

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

这曾经不是标准的,被认为是一种 hack(正如 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);

您可以使用[0, extra) 中的索引访问var->data。请注意,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 的偏移量可能不一样)。

【讨论】:

【解决方案2】:

这实际上是一个 hack,实际上是针对 GCC (C90)。

也称为struct hack

所以下一次,我会说:

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

这就相当于说:

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

我可以创建任意数量的此类结构对象。

【讨论】:

    【解决方案3】:

    这个想法是在结构的末尾允许一个可变大小的数组。据推测,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

    【讨论】:

    • @Aniket:我不完全确定那个的想法是从哪里来的。
    • 在 C++ 中是的,在 C 中,不需要。
    • @sheu,这是因为你的写作风格malloc 会让你重复多次,如果action 的类型发生变化,你必须多次修复它。自己比较一下以下两个,你就会知道:struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing)); vs. struct some_thing *variable = malloc(10 * sizeof(*variable)); 第二个更短、更干净,而且显然更容易更改。
    【解决方案4】:

    代码无效 C (see this)。很明显,Linux 内核根本不关心可移植性,因此它使用了大量非标准代码。

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

    在较早的 C 标准中,以空数组结束结构被称为“结构黑客”。其他人已经在其他答案中解释了它的目的。在 C90 标准中,struct hack 是未定义的行为,可能导致崩溃,主要是因为 C 编译器可以在结构末尾添加任意数量的填充字节。这样的填充字节可能会与您尝试在结构末尾“破解”的数据发生冲突。

    GCC 很早就做了一个非标准的扩展来将它从未定义的行为改变为明确定义的行为。 C99 标准随后采用了这个概念,因此任何现代 C 程序都可以毫无风险地使用这个特性。在C99/C11中称为灵活数组成员

    【讨论】:

    • 我怀疑“linux内核不关心可移植性”。也许您的意思是对其他编译器的可移植性?的确,它与 gcc 的特性十分纠缠。
    • 尽管如此,我认为这段特殊的代码不是主流代码,可能因为它的作者没有太多关注而被遗漏了。许可证说它是关于一些德州仪器驱动程序的,所以内核的核心程序员不太可能注意到它。我很确定内核开发人员会根据新标准或新优化不断更新旧代码。它太大了,无法确保所有内容都已更新!
    • @Shahbaz 对于“显而易见”的部分,我的意思是可移植到其他操作系统,这自然没有任何意义。但他们似乎也不在乎其他编译器的可移植性,他们使用了太多的 GCC 扩展,以至于 Linux 不太可能被移植到其他编译器。
    • @Shahbaz 至于任何标有德州仪器的案例,TI 本身因在其针对各种 TI 芯片的应用说明中生成了有史以来最无用、最蹩脚、最幼稚的 C 代码而臭名昭著。如果代码来自 TI,那么所有关于解释其中有用信息的可能性的赌注都将失败。
    • 确实linux和gcc密不可分。 Linux 内核也很难理解(主要是因为操作系统很复杂)。不过,我的观点是,由于第三方糟糕的编码实践,说“Linux 内核的作者显然喜欢让事情变得不必要的复杂和非标准,如果这样做的选项暴露了自己”并不好。 .
    【解决方案5】:

    零长度数组的另一种用法是作为结构内的命名标签,以帮助编译时结构偏移检查。

    假设您有一些大型结构定义(跨越多个缓存行),您希望确保它们在开始和中间跨越边界时都与缓存行边界对齐。

    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 长度数组行为的定义可能是一个好主意,至少可以显示它是否可以在声明之前或之后引入填充。
    猜你喜欢
    • 2019-01-14
    • 1970-01-01
    • 2012-03-16
    • 2011-11-10
    • 2022-06-13
    • 2018-09-10
    • 1970-01-01
    • 1970-01-01
    • 2014-03-30
    相关资源
    最近更新 更多