34
  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
4

3 回答 3

26

简短的回答

  1. 是的。使用 FAM 的常见约定使我们的程序暴露于未定义行为的可能性。话虽如此,我不知道任何现有的符合标准的实现会行为不端。

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

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

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


长答案

FAM 最初是在 C99 (TC0)(1999 年 12 月)中引入的,它们的原始规范要求 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 元素的值的假设现在都是错误的。这意味着任何假设都会导致我们无法严格遵守。

未定义的行为

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

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

于 2017-06-25T11:01:29.430 回答
19

这是一个很长的答案,广泛涉及一个棘手的话题。

TL;博士

我不同意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 技术更正 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:

作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为 灵活数组成员。除了两个例外,灵活数组成员被忽略。首先,结构的大小应等于其他相同结构的最后一个元素的偏移量,该结构用未指定长度的数组替换灵活数组成员。106) 其次,当一个.(或->) 运算符的左操作数是(指向)具有灵活数组成员的结构,右操作数命名该成员,它的行为就好像该成员被替换为最长的数组(具有相同的元素类型),不会使结构大于被访问的对象;数组的偏移量应保持灵活数组成员的偏移量,即使这与替换数组的偏移量不同。如果这个数组没有元素,它的行为就好像它有一个元素,但如果尝试访问该元素或生成一个越过它的指针,则行为是不确定的。

126)未指定长度以允许实现可以根据数组成员的长度为数组成员提供不同的对齐方式。

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

¶17 示例假设所有数组成员的对齐方式相同,在声明之后:

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

三种表达方式:

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

具有相同的价值。结构 struct s 有一个灵活的数组成员 d。

¶18 如果 sizeof (double) 为 8,则在执行以下代码后:

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

并假设对 malloc 的调用成功,s1 和 s2 指向的对象的行为就好像标识符已声明为:

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

¶19 在进一步成功分配之后:

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

然后它们的行为就好像声明是:

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

和:

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

¶20 作业:

*s1 = *s2;

只复制成员 n 而不是任何数组元素。相似地:

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

在 C11 中删除了一些示例材料。TC2 中没有注意到(也不需要指出)更改,因为这些示例不是规范性的。但是 C11 中重写的材料在研究时提供了丰富的信息。

N983 论文确定了灵活阵列成员的问题

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

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

TC2 在规范材料中使用了 C11 标准中的措辞。

灵活阵列成员的 C11 标准

那么,C11 标准对灵活数组成员有什么看法呢?

§6.7.2.1 结构和联合说明符

¶3 结构或联合不应包含不完整或函数类型的成员(因此,结构不应包含自身的实例,但可能包含指向自身实例的指针),除了结构的最后一个成员具有多个命名成员可能具有不完整的数组类型;这样的结构(以及任何可能递归地包含此类结构的成员的联合)不应是结构的成员或数组的元素。

这将 FAM 牢牢地定位在结构的末尾——根据定义,“最后一个成员”位于结构的末尾,这可以通过以下方式确认:

¶15 在结构对象中,非位域成员和位域所在的单元的地址按照声明的顺序增加。

¶17 在结构或联合的末尾可能有未命名的填充。

¶18 作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为 灵活数组成员。在大多数情况下,灵活数组成员被忽略。特别是,结构的大小就像省略了柔性数组成员一样,只是它可能具有比省略所暗示的更多的尾随填充。然而,当一个.(或->) 运算符的左操作数是(指向)具有灵活数组成员的结构,右操作数命名该成员,它的行为就好像该成员被替换为最长的数组(具有相同的元素类型),不会使结构大于被访问的对象;数组的偏移量应保持灵活数组成员的偏移量,即使这与替换数组的偏移量不同。如果这个数组没有元素,它的行为就好像它有一个元素,但如果尝试访问该元素或生成一个越过它的指针,则行为是不确定的。

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

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

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

¶20 示例 2 声明后:

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

该结构struct s具有灵活的数组成员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可能不一样)。

¶21 在上述声明之后:

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

的初始化t2无效(并且违反了约束),因为struct s它被视为不包含 member d。赋值t1.d[0]可能是未定义的行为,但有可能

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

在这种情况下,分配将是合法的。然而,它不能出现在严格符合的代码中。

¶22 在进一步声明之后:

struct ss { int n; };

表达式:

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

总是等于 1。

¶23 如果sizeof (double)是 8,那么在执行以下代码之后:

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

并假设调用malloc成功,在大多数情况下,指向s1s2行为的对象就好像标识符已声明为:

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

¶24 在进一步成功的分配之后:

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

然后它们的行为就好像声明是:

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

和:

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

¶25 作业:

*s1 = *s2;

只复制会员n;如果任何数组元素在sizeof (struct s)结构的第一个字节内,它们可能会被复制或简单地用不确定的值覆盖。

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

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

§6.2.6 类型的表示 §6.2.6.1 概述

¶6当值存储在结构或联合类型的对象中时,包括在成员对象中,对应于任何填充字节的对象表示的字节采用未指定的值。51)结构或联合对象的值永远不是陷阱表示,即使结构或联合对象的成员的值可能是陷阱表示。

51)因此,例如,结构分配不需要复制任何填充位。

说明有问题的 FAM 结构

C聊天室,我写了一些信息,这是一个释义:

考虑:

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

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

建议是famin的开头struct fam1可以在偏移量 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. 指出

有一些宏可以根据小于结构大小的 FAM 偏移量计算“精确”所需的存储空间。比如这个:https ://gustedt.wordpress.com/2011/03/14/flexible-array-member/

解决问题

问题问:

  1. 通过在结构类型中使用灵活的数组成员(FAM),我们是否将我们的程序暴露于未定义行为的可能性?
  2. 一个程序是否有可能使用 FAM 并且仍然是一个严格遵守的程序?
  3. 灵活数组成员的偏移量是否需要位于结构的末尾?

这些问题适用于 C99 (TC3) 和 C11 (TC1)。

我相信,如果你编码正确,答案是“否”、“是”、“否和是,取决于……”。

问题 1

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

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

问题2

第 4 节一致性定义:

¶5严格遵守的程序应仅使用本国际标准中指定的语言和库的那些特性。3) 它不应产生依赖于任何未指定、未定义或实现定义的行为的输出,并且不应超过任何最小实现限制。

3)一个严格符合的程序可以使用条件特性(见 6.10.8.3),只要使用相关宏的适当的条件包含预处理指令保护使用。…</p>

¶7符合标准的程序是符合标准的实现可接受的程序。5)

5)严格符合的程序旨在最大限度地在符合的实现中移植。符合标准的程序可能取决于符合标准的实现的不可移植特性。

我不认为标准 C 有任何特性,如果按照标准的意图使用,会使程序不严格符合。如果有的话,它们与依赖于语言环境的行为有关。FAM 代码的行为本质上并不依赖于语言环境。

我不认为使用 FAM 本质上意味着程序不严格符合。

问题 3

我认为问题3在以下之间模棱两可:

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

3A 的答案是“否”(参见上文引用的 ¶25 中的 C11 示例)。

3B 的答案是“是”(见上文引用的 §6.7.2.1 ¶15)。

不同意 Dror 的回答

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

截至 2017-07-01 18:00 -08:00,Dror K 的简短回答 说:

[DK]

  1. 是的。使用 FAM 的常见约定使我们的程序暴露于未定义行为的可能性。话虽如此,我不知道任何现有的符合标准的实现会行为不端。

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

[DK]

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

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

[DK]

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

这是我的解释 3A 的答案,我同意这一点。

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

[DK]

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

我同意这个分析。

[DK]

新规范删除了要求 FAM 的偏移量位于结构末尾的语句,并且引入了一个非常不幸的结果,因为该标准允许实现不将任何填充字节的值保留在结构中或工会处于一致的状态。

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

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

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

[DK]

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

我不认为这是一个问题。任何期望您可以使用结构赋值来复制包含 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]in 的 值printf()是不确定的;它的值为123

先前的标准引用是(它在 C99 和 C11 中是相同的 §6.2.6.1 ¶6,尽管脚注编号在 C99 中是 42,在 C11 中是 51):

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

请注意,这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)) 我的分析无关紧要。

由于长答案的其余部分(主要是标题“未定义的行为”标识的部分)是基于一个错误的推断,即当分配给结构的整数成员时结构的填充字节可能发生变化,其余的讨论无需进一步分析。

[DK]

一旦我们存储到 struct 的成员,填充字节将采用未指定的字节,因此对与任何尾随填充字节对应的 FAM 元素的值所做的任何假设现在都是错误的。这意味着任何假设都会导致我们无法严格遵守。

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

于 2017-07-02T12:11:24.417 回答
7

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

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

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

z由于对齐,“foo”的大小可能会超出z. 书写y可能会干扰 之前的填充z,但不应干扰其z自身的任何部分。

于 2017-06-26T19:30:38.607 回答