灵活的数组成员会导致未定义的行为吗?

时间:2017-06-25 11:01:29

标签: c language-lawyer undefined-behavior flexible-array-member

  1. 通过在结构类型中使用灵活的数组成员(FAM),我们是否将程序暴露给未定义行为的可能性?

  2. 程序是否可以使用FAM并且仍然是严格遵守的程序?

  3. 灵活数组成员的偏移量是否需要位于结构的末尾?

  4. 这些问题同时适用于C99 (TC3)C11 (TC1)

    #include <stdio.h>
    #include <stdlib.h>
    #include <stddef.h>
    
    int main(void) {
        struct s {
            size_t len;
            char pad;
            int array[];
        };
    
        struct s *s = malloc(sizeof *s + sizeof *s->array);
    
        printf("sizeof *s: %zu\n", sizeof *s);
        printf("offsetof(struct s, array): %zu\n", offsetof(struct s, array));
    
        s->array[0] = 0;
        s->len = 1;
    
        printf("%d\n", s->array[0]);
    
        free(s);
        return 0;
    }
    

    输出:

    sizeof *s: 16
    offsetof(struct s, array): 12
    0
    

2 个答案:

答案 0 :(得分:26)

简答

  1. 是。使用FAM的常见约定使我们的程序暴露于未定义行为的可能性。话虽如此,我并未意识到任何现有的符合规定的实施方案都会行为不端。

  2. 可能,但不太可能。即使我们实际上没有达到未定义的行为,我们仍然可能无法严格遵守。

  3. 否。 FAM的偏移量不需要位于结构的末尾,它可以覆盖任何尾随填充字节。

  4. 答案适用于C99 (TC3)C11 (TC1)

    长答案

    首先在C99(TC0)(1999年12月)中引入FAM,并且它们的原始规范要求FAM的偏移位于结构的末尾。原始规范定义明确,因此不会导致未定义的行为,或者是严格一致性的问题。

    C99 (TC0) §6.7.2.1 p16(1999年12月)

      

    [本文档为官方标准,受版权保护,不可免费提供]

    问题在于常见的C99实现(例如GCC)没有遵循标准的要求,并允许FAM覆盖任何尾随填充字节。他们的方法被认为更有效,并且因为他们遵循标准的要求 - 会导致向后兼容性,委员会选择更改规范,从C99 TC2(2004年11月)起不再需要标准FAM的偏移量在结构的末尾。

    C99 (TC2) §6.7.2.1 p16(2004年11月)

      

    [...]结构的大小就好像省略了柔性阵列成员一样,除了它可能有更多的尾随填充而不是遗漏意味着。

    新规范删除了需要FAM偏移量位于结构末尾的语句,并且它引入了一个非常不幸的结果,因为标准赋予实现不保留任何填充字节值的自由在一致状态下的结构或工会内部。更具体地说:

    C99 (TC3) §6.2.6.1 p6

      

    当值存储在结构或联合类型的对象中时,包括在成员对象中,对应于任何填充字节的对象表示的字节采用未指定的值。

    这意味着如果我们的任何FAM元素对应(或覆盖)任何尾随填充字节,则在存储到结构的成员时(它们​​)可以采用未指定的值。我们甚至不需要思考这是否适用于存储在FAM本身的值,即使严格的解释只适用于FAM以外的其他成员,也是有害的。

    #include <stdio.h>
    #include <stdlib.h>
    #include <stddef.h>
    
    int main(void) {
        struct s {
            size_t len;
            char pad;
            int array[];
        };
    
        struct s *s = malloc(sizeof *s + sizeof *s->array);
    
        if (sizeof *s > offsetof(struct s, array)) {
            s->array[0] = 123;
            s->len = 1; /* any padding bytes take unspecified values */
    
            printf("%d\n", s->array[0]); /* indeterminate value */
        }
    
        free(s);
        return 0;
    }
    

    一旦我们存储到结构的成员,填充字节采用未指定的字节,因此任何关于对应于任何尾随填充字节的FAM元素的值的假设现在都是假的。这意味着任何假设都会导致我们严格遵守规定。

    未定义的行为

    尽管填充字节的值是&#34;未指定的值&#34;,但是对于受它们影响的类型却无法说明,因为基于未指定值的对象表示可以生成陷阱表示。因此,描述这两种可能性的唯一标准术语是“不确定的价值”。如果FAM的类型碰巧有陷阱表示,那么访问它不仅仅是未指定值的问题,而是未定义的行为。

    但等等,还有更多。如果我们同意描述这种价值的唯一标准术语是&#34;不确定的值&#34;,那么即使FAM的类型没有陷阱表示,我们也达到了未定义的行为,因为C标准委员会的官方解释是将不确定的值传递给标准库函数是不确定的行为。

答案 1 :(得分:7)

如果一个人允许严格遵守的程序在其工作的情况下使用实施定义的行为&#34;所有合法的行为(尽管几乎任何类型的有用输出都依赖于实现定义的细节,例如执行字符集),如果程序不关心是否可以在严格一致的程序中使用灵活的数组成员柔性阵列构件的偏移量与结构的长度一致。

数组不被视为内部有任何填充,因此任何因FAM而添加的填充都将位于其之前。如果在结构内部或外部有足够的空间来容纳FAM中的成员,则这些成员是FAM的一部分。例如,给定:

struct { long long x; char y; short z[]; } foo;

&#34; foo&#34;的大小由于对齐,可能会在z的开头之外填充,但任何此类填充都可用作z的一部分。写y可能会干扰z之前的填充,但不应该干扰z本身的任何部分。

相关问题