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

Flexible array members can lead to undefined behavior?

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

  2. 一个程序是否可以使用 FAM 并且仍然是一个严格符合的程序?

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

这些问题适用于 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

简答

  1. 是的。 使用 FAM 的常见约定使我们的程序有可能出现未定义的行为。话虽如此,我并不知道任何现有的符合规范的实现会出现异常行为。

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

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

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


长答案

FAM 最初是在 C99 (TC0)(1999 年 12 月)中引入的,其原始规范要求 FAM 的偏移位于结构的末尾。原始规范定义明确,因此不会导致未定义的行为,也不会成为严格符合性的问题。

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

[This document is the official standard, it is copyrighted and not freely available]

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

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

[...] the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply.

新规范删除了要求 FAM 的偏移量位于结构末尾的语句,它引入了一个非常不幸的后果,因为标准允许实现不保留任何值的自由以一致的状态在结构或联合中填充字节。更具体地说:

C99 (TC3) §6.2.6.1 p6

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

这意味着如果我们的任何 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 元素的值所做的任何假设现在都是错误的。这意味着任何假设都会导致我们无法严格遵守。

未定义的行为

虽然填充字节的值是"unspecified values",但受它们影响的类型却不能这样说,因为基于未指定值的对象表示可以生成陷阱表示。因此,描述这两种可能性的唯一标准术语是 "indeterminate value"。如果 FAM 的类型恰好有陷阱表示,那么访问它不仅仅是一个未指定的值的问题,而是未定义的行为。

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

如果允许严格遵守的程序在 "work" 具有所有合法行为的情况下使用实现定义的行为(尽管几乎任何类型的有用输出都取决于实现定义的行为诸如执行字符集之类的细节),只要程序不关心灵活数组成员的偏移量是否与结构的长度一致,就应该可以在严格符合的程序中使用灵活的数组成员。

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

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

由于对齐,"foo" 的大小可能会超出 z 的开头,但任何此类填充都可用作 z 的一部分。写入 y 可能会干扰 z 之前的填充,但不应干扰 z 本身的任何部分。

这是一个涉及棘手话题的长篇回答。

TL;DR

我不同意 by Dror K

关键问题是误解了 C99 和 C11 标准中的 §6.2.1 ¶6 的含义,并将其不适当地应用于简单的整数赋值,例如:

fam_ptr->nonfam_member = 23;

此赋值不允许更改fam_ptr指向的结构中的任何填充字节。 因此,基于这可以改变结构中的填充字节的假设的分析是错误的。

背景

原则上,我对 C99 标准并不太担心,而且 其更正;它们不是当前标准。 然而,灵活数组成员规范的演变是 内容丰富。

C99 标准 — ISO/IEC 9899:1999 — 有 3 个技术更正:

  • TC1 于 2001-09-01 发布(7 页),
  • TC2 于 2004-11-15 发布(15 页),
  • TC3 发布于 2007-11-15(10 页)。

例如,TC3 指出 gets() 已过时并且 已弃用,导致它从 C11 标准中删除。

C11 标准 — ISO/IEC 9899:2011 — 有一项技术 更正,但这只是意外地设置了两个宏的值 以 201ymmL 的形式保留 — 所需的值 __STDC_VERSION____STDC_LIB_EXT1__ 已更正为值 201112L。 (您可以看到 TC1 — 正式的“ISO/IEC 9899:2011/Cor.1:2012(en) 信息技术 — 编程语言 — C TECHNICAL 更正 1" — 在 https://www.iso.org/obp/ui/#iso:std:iso-iec:9899:ed-3:v1:cor:1:v1:en。 我还没弄清楚你是怎么下载它的,但它很简单 这真的无关紧要。

关于灵活数组成员的 C99 标准

ISO/IEC 9899:1999(TC2 之前)§6.7.2.1 ¶16:

As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. With two exceptions, the flexible array member is ignored. First, the size of the structure shall be equal to the offset of the last element of an otherwise identical structure that replaces the flexible array member with an array of unspecified length.106) Second, when a . (or ->) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it.

126) The length is unspecified to allow for the fact that implementations may give array members different alignments according to their lengths.

(此脚注在重写中被删除。) 最初的 C99 标准包括一个例子:

¶17 EXAMPLE Assuming that all array members are aligned the same, after the declarations:

struct s { int n; double d[]; };
struct ss { int n; double d[1]; };

the three expressions:

sizeof (struct s)
offsetof(struct s, d)
offsetof(struct ss, d)

have the same value. The structure struct s has a flexible array member d.

¶18 If sizeof (double) is 8, then after the following code is executed:

struct s *s1;
struct s *s2;
s1 = malloc(sizeof (struct s) + 64);
s2 = malloc(sizeof (struct s) + 46);

and assuming that the calls to malloc succeed, the objects pointed to by s1 and s2 behave as if the identifiers had been declared as:

struct { int n; double d[8]; } *s1;
struct { int n; double d[5]; } *s2;

¶19 Following the further successful assignments:

s1 = malloc(sizeof (struct s) + 10);
s2 = malloc(sizeof (struct s) + 6);

they then behave as if the declarations were:

struct { int n; double d[1]; } *s1, *s2;

and:

double *dp;
dp = &(s1->d[0]); // valid
*dp = 42;         // valid
dp = &(s2->d[0]); // valid
*dp = 42;         // undefined behavior

¶20 The assignment:

*s1 = *s2;

only copies the member n and not any of the array elements. Similarly:

struct s t1 = { 0 };          // valid
struct s t2 = { 2 };          // valid
struct ss tt = { 1, { 4.2 }}; // valid
struct s t3 = { 1, { 4.2 }};  // invalid: there is nothing for the 4.2 to initialize
t1.n = 4;                     // valid
t1.d[0] = 4.2;                // undefined behavior

此示例的某些内容 material 在 C11 中已删除。 TC2 中没有注意到(也不需要注意到)更改,因为 例子不规范。 但是C11中重写的material在学习的时候很有参考价值

N983 论文识别灵活数组成员的问题

N983 来自 WG14 Pre-Santa Cruz-2002 邮寄 我相信,这是缺陷报告的初始陈述。 它指出一些 C 编译器(引用三个)设法将 FAM 在结构末尾的填充之前。 最终的缺陷报告是 DR 282.

据我了解,这份报告导致了 TC2 的变化,不过 我没有追踪过程中的所有步骤。 看来 DR 不再单独提供了。

TC2 在规范 material.

中使用了 C11 标准中的措辞

关于灵活数组成员的 C11 标准

那么,C11 标准对灵活数组成员有何规定?

§6.7.2.1 Structure and union specifiers

¶3 A structure or union shall not contain a member with incomplete or function type (hence, a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.

这将 FAM 牢牢定位在结构的末尾——“最后一个 member' 根据定义在结构的末尾,这是 确认人:

¶15 Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared.

¶17 There may be unnamed padding at the end of a structure or union.

¶18 As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. In most situations, the flexible array member is ignored. In particular, the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply. However, when a . (or ->) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it.

本段包含 ISO/IEC 的 ¶20 中的更改 9899:1999/Cor.2:2004(E) — C99 的 TC2;

包含a的结构主要部分末尾的数据 灵活的数组成员是可以出现的常规尾随填充 任何结构类型。 这样的填充不能被合法访问,但可以传递给 库函数等通过指向结构的指针而不会产生 未定义的行为。

C11 标准包含三个示例,但第一个和第三个是 与匿名结构和联合相关,而不是 灵活的数组成员。 请记住,示例不是 'normative',而是说明性的。

¶20 EXAMPLE 2 After the declaration:

struct s { int n; double d[]; };

the structure struct s has a flexible array member d. A typical way to use this is:

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

and assuming that the call to malloc succeeds, the object pointed to by p behaves, for most purposes, as if p had been declared as:

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

(there are circumstances in which this equivalence is broken; in particular, the offsets of member d might not be the same).

¶21 Following the above declaration:

struct s t1 = { 0 };          // valid
struct s t2 = { 1, { 4.2 }};  // invalid
t1.n = 4;                     // valid
t1.d[0] = 4.2;                // might be undefined behavior

The initialization of t2 is invalid (and violates a constraint) because struct s is treated as if it did not contain member d. The assignment to t1.d[0] is probably undefined behavior, but it is possible that

sizeof (struct s) >= offsetof(struct s, d) + sizeof (double)

in which case the assignment would be legitimate. Nevertheless, it cannot appear in strictly conforming code.

¶22 After the further declaration:

struct ss { int n; };

the expressions:

sizeof (struct s) >= sizeof (struct ss)
sizeof (struct s) >= offsetof(struct s, d)

are always equal to 1.

¶23 If sizeof (double) is 8, then after the following code is executed:

struct s *s1;
struct s *s2;
s1 = malloc(sizeof (struct s) + 64);
s2 = malloc(sizeof (struct s) + 46);

and assuming that the calls to malloc succeed, the objects pointed to by s1 and s2 behave, for most purposes, as if the identifiers had been declared as:

struct { int n; double d[8]; } *s1;
struct { int n; double d[5]; } *s2;

¶24 Following the further successful assignments:

s1 = malloc(sizeof (struct s) + 10);
s2 = malloc(sizeof (struct s) + 6);

they then behave as if the declarations were:

struct { int n; double d[1]; } *s1, *s2;

and:

double *dp;
dp = &(s1->d[0]); // valid
*dp = 42;         // valid
dp = &(s2->d[0]); // valid
*dp = 42;         // undefined behavior

¶25 The assignment:

*s1 = *s2;

only copies the member n; if any of the array elements are within the first sizeof (struct s) bytes of the structure, they might be copied or simply overwritten with indeterminate values.

请注意,这在 C99 和 C11 之间发生了变化。

标准的另一部分描述了这种复制行为:

§6.2.6 Representation of types §6.2.6.1 General

¶6 When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.51) The value of a structure or union object is never a trap representation, even though the value of a member of the structure or union object may be a trap representation.

51) Thus, for example, structure assignment need not copy any padding bits.

说明有问题的 FAM 结构

C chat room中, 我wrote一些 这是释义的信息:

考虑:

struct fam1 { double d; char c; char fam[]; };

假设 double 需要 8 字节对齐(或 4 字节;这无关紧要 太多了,但我会坚持使用 8),那么 struct non_fam1a { double d; char c; }; 将在 c 之后有 7 个填充字节,大小为 16。 此外,struct non_fam1b { double d; char c; char nonfam[4]; };nonfam 数组后有 3 个字节填充,大小为 16.

建议struct fam1fam的开头可以偏移 9,即使 sizeof(struct fam1) 是 16。 这样 c 之后的字节就不会被填充(必须)。

因此,对于足够小的 FAM,结构体加上 FAM 的大小可能仍然 小于 struct fam.

的大小

原型分配为:

struct fam1 *fam = malloc(sizeof(struct fam1) + array_size * sizeof(char));

当 FAM 的类型为 char 时(如 struct fam1)。 当 fam 的偏移量小于 sizeof(struct fam1).

Dror K.尖头 输出:

There are macros out there for calculating the 'precise' required storage based on FAM offsets that are less than the size of the structure. Such as this one: https://gustedt.wordpress.com/2011/03/14/flexible-array-member/

解决问题

问题问:

  1. By using flexible array members (FAMs) within structure types, are we exposing our programs to the possibility of undefined behavior?
  2. Is it possible for a program to use FAMs and still be a strictly conforming program?
  3. Is the offset of the flexible array member required to be at the end of the struct?

The questions apply to both C99 (TC3) and C11 (TC1).

我相信如果你编码正确,答案是"No"、"Yes"、"No and Yes, depending …"。

问题一

我假设问题 1 的意图是“你的程序必须 如果您使用任何 FAM,不可避免地会遇到未定义的行为 任何地方?” 说明我认为显而易见的事情:有很多方法可以暴露一个 对未定义行为进行编程(其中一些方法涉及 具有灵活数组成员的结构)。

我不认为简单地使用 FAM 意味着程序 自动具有(调用、暴露于)未定义的行为。

问题二

第 4 节 一致性 定义:

¶5 A strictly conforming program shall use only those features of the language and library specified in this International Standard.3) It shall not produce output dependent on any unspecified, undefined, or implementation-defined behavior, and shall not exceed any minimum implementation limit.

3) A strictly conforming program can use conditional features (see 6.10.8.3) provided the use is guarded by an appropriate conditional inclusion preprocessing directive using the related macro. …

¶7 A conforming program is one that is acceptable to a conforming implementation.5).

5) Strictly conforming programs are intended to be maximally portable among conforming implementations. Conforming programs may depend upon nonportable features of a conforming implementation.

我认为标准 C 没有任何功能,如果用于 标准打算的方式,使程序不严格 符合。 如果有的话,它们与语言环境相关的行为有关。 FAM 代码的行为本质上不依赖于语言环境。

我不认为使用 FAM 本身就意味着该程序 不严格符合。

问题三

我认为问题 3 在以下方面存在歧义:

  • 3A:灵活数组成员的偏移量是否要求等于 包含灵活数组成员的结构的大小?
  • 3B:灵活数组成员的偏移量是否要求更大 比结构中任何先前成员的偏移量?

3A 的答案是 "No"(参见上面引用的 ¶25 处的 C11 示例)。

3B 的答案是 "Yes"(见证 §6.7.2.1 ¶15,上面引用)。

反对 Dror 的回答

我需要引用C标准和Dror的回答。我将使用 [DK] 来表示 从 Dror 的回答开始引用,未标记的引用来自 C 标准。

截至 2017-07-01 18:00-08:00, 通过 Dror K 说:

[DK]

  1. Yes. Common conventions of using FAMs expose our programs to the possibility of undefined behavior. Having said that, I'm unaware of any existing conforming implementation that would misbehave.

我不相信仅仅使用 FAM 就意味着该程序 自动具有未定义的行为。

[DK]

  1. Possible, but unlikely. Even if we don't actually reach undefined behavior, we are still likely to fail strict conformance.

我不相信使用 FAM 会自动呈现程序 不严格符合。

[DK]

  1. No. The offset of the FAM is not required to be at the end of the struct, it may overlay any trailing padding bytes.

这是对我的解释3A的回答,我同意。

长答案包含对上述简短答案的解释。

[DK]

The problem was that common C99 implementations, such as GCC, didn't follow the requirement of the standard, and allowed the FAM to overlay any trailing padding bytes. Their approach was considered to be more efficient, and since for them to follow the requirement of the standard- would result with breaking backwards compatibility, the committee chose to change the specification, and as of C99 TC2 (Nov 2004) the standard no longer required the offset of the FAM to be at the end of the struct.

我同意这个分析。

[DK]

The new specification removed the statement that required the offset of the FAM to be at the end of the struct, and it introduced a very unfortunate consequence, because the standard gives the implementation the liberty not to keep the values of any padding bytes within structures or unions in a consistent state.

我同意新规范删除了 FAM 的要求 存储在大于或等于大小的偏移量处 结构。

我不同意填充字节有问题。

标准明确指出结构的结构赋值 包含 FAM 会有效地忽略 FAM(§6.7.2.1 ¶18)。 它必须复制非 FAM 成员。 明确指出根本不需要复制填充字节 (§6.2.6.1 ¶6 和脚注 51)。 并且示例 2 明确指出(非规范性 §6.7.2.1 ¶25) 如果 FAM 与结构定义的 space 重叠,则数据 来自与结构末端重叠的 FAM 部分 可能会也可能不会被复制。

[DK]

This means that if any of our FAM elements correspond to (or overlay) any trailing padding bytes, upon storing to a member of the struct- they (may) take unspecified values. We don't even need to ponder whether this applies to a value stored to the FAM itself, even the strict interpretation that this only applies to members other than the FAM, is damaging enough.

我不认为这是个问题。 您可以使用复制包含 FAM 的结构的任何期望 结构赋值并复制 FAM 数组本质上是 有缺陷 — 副本在逻辑上未复制 FAM 数据。 依赖范围内的 FAM 数据的任何程序 结构损坏;那是(有缺陷的)程序的 属性,而不是 标准。

[DK]

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

当然,理想情况下,代码会将命名成员 pad 设置为 确定值,但这实际上不会导致问题 因为它从未被访问过。

我非常不同意 s->array[0]printf() 不确定;它的值为 123.

之前的标准引用是(在C99和C99中都是一样的§6.2.6.1 ¶6 和 C11,尽管脚注编号在 C99 中为 42,在 C11 中为 51):

When a value is stored in an object of structure or union type, including in a member object, the bytes of the object representation that correspond to any padding bytes take unspecified values.

请注意,s->len 不是对结构或对象的赋值 工会类型;它是对 size_t 类型对象的赋值。 我认为这可能是造成混淆的主要原因。

如果代码包含:

struct s *t = malloc(sizeof(*t) + sizeof(t->array[0]));
*t = *s;
printf("t->array[0] = %d\n", t->array[0]);

那么打印出来的值确实是不确定的。 但是,那是因为不能保证复制带有 FAM 的结构 复制 FAM。 更接近正确的代码是(当然,假设您添加 #include <string.h>):

struct s *t = malloc(sizeof(*t) + sizeof(t->array[0]));
*t = *s;
memmmove(t->array, s->array, sizeof(t->array[0]));
printf("t->array[0] = %d\n", t->array[0]);

现在打印的值是确定的(它是 123)。 请注意 if (sizeof *s > offsetof(struct s, array)) 上的条件 我的分析是material

因为其余的长答案(主要是 heading 'undefined behavior') 是基于对 分配时结构的填充字节发生变化的可能性 对于结构的整数成员,其余的讨论不 需要进一步分析。

[DK]

Once we store to a member of the struct, the padding bytes take unspecified bytes, and therefore any assumption made about the values of the FAM elements that correspond to any trailing padding bytes, is now false. Which means that any assumption leads to us failing strict conformance.

这是基于错误的前提;结论是错误的。