灵活的数组成员可能导致未定义的行为?

  1. 通过在结构types中使用灵活数组成员(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)


长的答案

FAMs首次引入C99(TC0)(1999年12月),它们的原始规范要求FAM的偏移位于结构的末尾。 原来的规范是明确的,因此不会导致不确定的行为,或者是严格遵守的问题。

C99 (TC0) §6.7.2.1 p16 (Dec 1999)

[这个文件是官方的标准,它是受版权保护而不是免费的]

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

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

结构的大小就像是柔性arrays成员被省略,除了它可能有更多的尾部填充比遗漏暗示的更多。

新的规范删除了要求FAM的偏移位于结尾的语句,并且引入了一个非常不幸的后果,因为标准赋予了实现自由,不将任何填充字节的值保留在结构内或工会一致的状态。 进一步来说:

C99 (TC3) §6.2.6.1 p6

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

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

未定义的行为

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

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

这是一个很长的回答广泛处理一个棘手的话题。

TL; DR

我不同意Dror K的分析 。

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

 fam_ptr->nonfam_member = 23; 

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

背景

原则上,我并不担心C99标准及其更正; 他们不是目前的标准。 然而,灵活arrays成员规范的演变是信息化的。

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有一个技术勘误,但是它只是简单地将两个macros的值设置为__STDC_VERSION__ ,将__STDC_VERSION____STDC_LIB_EXT1__所需的值修改为值201112L 。 (您可以在https://www.iso.org/obp/ui上正式查看TC1“ISO / IEC 9899:2011 / Cor.1:2012(en)信息技术 – 编程语言-C技术更正1” /#iso:std:iso-iec:9899:ed-3:v1:cor:1:v1:en 。我还没有弄清楚你是如何得到它的下载的,但它非常简单,这很重要。

灵活arrays成员上的C99标准

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

作为特殊情况,具有多个名称成员的结构的最后一个元素可能具有不完整的数组types; 这被称为一个灵活的数组成员 。 除了两个例外,可变数组成员被忽略。 首先,结构的大小应该等于另一个相同结构的最后一个元素的偏移量,该结构用一个未指定长度的数组来replace柔性数组成员。 第二,当一个. (或-> )运算符的左操作数是(指向)一个具有灵活数组成员的结构,右操作数指定该成员,它的行为就好像该成员被replace为最长的数组(具有相同的元素types)不会使结构大于被访问的对象; 数组的偏移量应保持为可变数组成员的偏移量,即使这与replace数组的偏移量不同。 如果这个数组没有元素,就像它有一个元素一样,但是行为是不确定的,如果试图访问那个元素或者生成一个指向它的指针。

126)未指定长度以允许实现可以根据其长度给予arrays成员不同的alignment。

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

¶17示例假设所有数组成员在声明之后都是相同的:

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

三个expression式:

 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); 

然后他们performance得好像这些声明是:

 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文件确定了灵活arrays成员的一个问题

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

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

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

灵活arrays成员上的C11标准

那么,C11标准对于灵活的arrays成员有什么要说的呢?

§6.7.2.1结构和联合说明符

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

这将FAM牢牢定位在结构的末端 – “最后一个成员”在结构的末端被定义,这由以下证实:

¶15在一个结构体对象中,非位域成员和位域所在的单元的地址增加了它们被声明的顺序。

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

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

本段包含ISO / IEC 9899:1999 / Cor.2:2004(E)¶20的变化 – C99的TC2;

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

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被视为不包含成员d 。 对t1.d[0]的赋值可能是未定义的行为,但可能是这样的

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

在这种情况下,这项任务是合法的。 不过,它不能出现在严格符合的代码中。

¶22进一步宣布后:

 struct ss { int n; }; 

expression式:

 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指向的对象在大多数情况下performance得好像标识符被声明为:

 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); 

然后他们performance得好像这些声明是:

 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)一个sizeof (struct s)字节内,则它们可能被复制或者简单地用不确定的值覆盖。

请注意,这在C99和C11之间改变了。

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

§6.2.6表示types§6.2.6.1概述

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

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

说明有问题的FAM结构

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

考虑:

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

假设double需要8个字节的alignment(或4个字节;没关系太多,但我会坚持8),然后struct non_fam1a { double d; char c; }; struct non_fam1a { double d; char c; };c之后将有7个填充字节,并且大小为16个。此外, struct non_fam1b { double d; char c; char nonfam[4]; }; struct non_fam1b { double d; char c; char nonfam[4]; };nonfam数组之后将有3个字节的填充,并且大小为16。

这个build议是,尽pipesizeof(struct fam1)是16,那么struct fam1fam的开始struct fam1可以是9,所以c之后的字节不是padding(必然)。

因此,对于一个足够小的FAM,struct FAM的大小仍然可能小于struct fam大小。

原型分配是:

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

当FAM是chartypes时(如在struct fam1 )。 当fam的偏移小于sizeof(struct fam1)时,这是一个(总的) sizeof(struct fam1)

Dror K. 指出 :

在那里有macros来计算基于小于结构尺寸的FAM偏移量的“精确”所需的存储。 如这一个: https : //gustedt.wordpress.com/2011/03/14/flexible-array-member/

解决这个问题

问题是:

  1. 通过在结构types中使用灵活的数组成员(FAM),我们是否将我们的程序暴露给未定义行为的可能性?
  2. 一个程序是否可以使用FAM,而且仍然是一个严格符合的程序?
  3. 灵活数组成员的偏移量是否需要在结构的末尾?

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

我相信,如果你的代码正确,答案是“否”,“是”,“不,是,取决于…”。

问题1

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

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

问题2

第§4节一致性定义:

¶5 严格符合的程序只能使用本标准规定的语言和库的特征。 3)根据任何未指定的,未定义的或实现定义的行为,不得产生输出,且不得超过任何最小实现限制。

3)严格符合规定的程序可以使用条件特征(见6.10.8.3),前提是使用相关macros指令的适当条件包含预处理指令保护使用。 …

¶7 一致性程序是一个可以接受的一致性实现。 5)

5)严格符合的程序旨在最大限度地兼容执行。 符合的程序可能取决于符合实现的不可移植特征。

我不认为标准C有什么特点,如果以标准的方式使用,使得程序不严格符合。 如果有这样的话,它们与地区依赖的行为有关。 FAM代码的行为并不固有地依赖于语言环境。

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

问题3

我认为问题3在以下两点之间是不明确的:

  • 3A:柔性arrays构件的偏移量是否需要与包含柔性arrays构件的结构的尺寸相等?
  • 3B:柔性arrays构件的偏移量是否需要大于结构中任何先前构件的偏移量?

对3A的答案是“否”(见上面引用的¶25中的C11例子)。

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

Dror的答案异议

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

截至2017年7月1日18:00 – 08:00,Dror K的简短回答说:

[DK]

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

我不相信简单地使用FAM意味着程序自动具有未定义的行为。

[DK]

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

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

[DK]

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

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

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

[DK]

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

我同意这个分析。

[DK]

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

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

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

该标准明确指出,包含FAM的结构的结构赋值有效地忽略了FAM(§6.7.2.1¶18)。 它必须复制非FAM成员。 明确指出填充字节根本不需要被复制(第6.2.6.1节和第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设置为一个确定的值,但是这并不会导致实际的问题,因为它永远不会被访问。

我强烈地不同意printf()s->array[0]的值是不确定的; 它的值是123

先前的标准报价是(在C99和C11中都是相同的§6.2.6.1¶6,尽pipe脚注编号是C99中的42和C11中的51):

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

请注意, s->len不是对结构或联合types的对象的赋值; 它是一个size_ttypes的对象的赋值。 我想这可能是这里混乱的主要来源。

如果代码包括:

 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]

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

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

如果允许严格符合规范的程序在与所有合法行为“起作用”的情况下使用实现定义的行为(尽pipe几乎任何有用的输出都将取决于实现定义的细节,如执行字符集),只要程序不关心柔性arrays成员的偏移是否与结构的长度一致,就可以在严格一致的程序中使用柔性arrays成员。

数组在内部不被视为有任何填充,所以任何由于FAM而被添加的填充将在其之前。 如果在结构内或结构外有足够的空间容纳FAM中的成员,那么这些成员就是FAM的一部分。 例如,给出:

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

由于alignment的原因,“foo”的大小可能会超出z的起始位置,但是任何这样的填充都可以用作z一部分。 写入y可能会打扰z之前的填充,但不应该打扰z本身的任何部分。