通过来自其他结构成员的偏移指针访问struct成员是否合法?

问题描述 投票:6回答:2

在这两个示例中,通过偏移指向其他成员的指针来访问结构的成员是否会导致未定义/未指定/实现定义的行为?

struct {
  int a;
  int b;
} foo1 = {0, 0};

(&foo1.a)[1] = 1;
printf("%d", foo1.b);


struct {
  int arr[1];
  int b;
} foo2 = {{0}, 0};

foo2.arr[1] = 1;
printf("%d", foo2.b);

C11§6.7.2.1的第14段似乎表明这应该是实施定义的:

结构或联合对象的每个非位字段成员以适合其类型的实现定义方式对齐。

然后继续说:

结构对象中可能存在未命名的填充,但不是在其开头。

但是,像下面这样的代码似乎很常见:

union {
  int arr[2];
  struct {
    int a;
    int b;
  };
} foo3 = {{0, 0}};

foo3.arr[1] = 1;
printf("%d", foo3.b);

(&foo3.a)[1] = 2; // appears to be illegal despite foo3.arr == &foo3.a
printf("%d", foo3.b);

该标准似乎保证foo3.arr&foo3.a相同,并且没有理由将其称为一种方式是合法的而另一种方式不合理,但同样地说,添加外部联合与数组应该没有意义突然让(&foo3.a)[1]合法化。

因此,我思考第一个例子的理由也必须合法:

  1. foo3.arr保证与&foo.a相同
  2. foo3.arr + 1&foo3.b指向相同的记忆位置
  3. 因此,&foo3.a + 1&foo3.b必须指向相同的内存位置(从1和2)
  4. struct layouts必须是一致的,所以&foo1.a&foo1.b应该与&foo3.a&foo3.b完全相同
  5. 因此,&foo1.a + 1&foo1.b必须指向相同的内存位置(从3和4)

我遇到过一些外部消息来源,表明foo3.arr[1](&foo3.a)[1]的例子都是非法的,但我无法在标准中找到具体说法。即使它们都是非法的,也可以使用灵活的数组指针构建相同的场景,据我所知,它具有标准定义的行为。

union {
  struct {
    int x;
    int arr[];
  };
  struct {
    int y;
    int a;
    int b;
  };
} foo4;

原始应用程序正在考虑是否严格按照标准定义从一个结构域到另一个结构域的缓冲区溢出:

struct {
  char buffer[8];
  char overflow[8];
} buf;
strcpy(buf.buffer, "Hello world!");
println(buf.overflow);

我希望这几乎可以在任何真实的编译器上输出"rld!",但这种行为是由标准保证的,还是未定义或实现定义的行为?

c pointers struct language-lawyer c11
2个回答
10
投票

简介:这个领域的标准是不够的,关于这个主题的争论有几十年的历史,而且严格的别名没有令人信服的解决方案或建议。

这个答案反映了我的观点,而不是任何强加标准。


首先:通常认为第一个代码示例中的代码是未定义的行为,因为通过直接指针算法访问数组的边界。

规则是C11 6.5.6 / 8。它说从指针索引必须保持在“数组对象”(或一个结束)之内。它没有说明哪个数组对象,但一般认为在int *p = &foo.a;的情况下,“数组对象”是foo.a,而不是foo.a是子对象的任何更大的对象。

相关链接:onetwo


其次:人们普遍认为你的两个union例子都是正确的。标准明确规定,任何工会成员都可以阅读;并且无论相关内存位置的内容是什么,都被解释为正在读取的联合成员的类型。


你建议union正确意味着第一个代码也应该是正确的,但事实并非如此。问题不在于指定读取的内存位置;问题在于我们如何到达指定内存位置的表达式。

即使我们知道&foo.a + 1&foo.b是相同的内存地址,但是通过第二个访问int是有效的,并且无效通过第一个访问int

通常同意您可以通过以不违反6.5.6 / 8规则的其他方式计算其地址来访问int,例如:

((int *)((char *)&foo + offsetof(foo, b))[0]

要么

((int *)((uintptr_t)&foo.a + sizeof(int)))[0]

相关链接:onetwo


关于((int *)&foo)[1]是否有效,一般不会达成一致。有人说它与你的第一个代码基本相同,因为标准说“指向对象的指针,适当转换,指向元素的第一个对象”。其他人说它与我上面的(char *)示例基本相同,因为它遵循指针转换的规范。一些人甚至声称它是严格的别名违规,因为它将结构别名为数组。

也许相关的是N2090 - Pointer provenance proposal。这并不直接解决这个问题,也没有提议废除6.5.6 / 8。


3
投票

根据C11草案N1570 6.5p7,尝试使用除字符类型左值,结构或联合类型或包含结构或联合类型之外的任何内容来访问结构或联合对象的存储值,即使行为也会调用UB否则将由标准的其他部分完整描述。本节不包含允许使用非字符成员类型(或任何非字符数字类型)的左值来提供访问结构或联合的存储值的规定。

然而,根据公布的理由文件,该标准的作者认识到,在标准没有强制要求的情况下,不同的实施提供了不同的行为保证,并将这种“流行的扩展”视为一种好的和有用的东西。他们认为,市场应该比委员会更好地回答关于何时以及如何支持这种延期的问题。虽然标准允许一个迟钝的编译器忽略someStruct.array[i]可能影响someStruct的存储值的可能性似乎很奇怪,但是标准的作者认识到任何编写者不是故意钝的编译器都会支持这样的构造,无论是标准授权与否,以及任何试图强制设计编译器的任何有用行为都是徒劳的。

因此,编译器对基本上与结构或联合有关的任何事物的支持水平都是一个实施质量问题。专注于与各种程序兼容的编译器编写者将支持各种构造。那些专注于最大化代码性能的代码只需要那些语言完全没用的构造,将支持更窄的集合。但是,该标准缺乏对此类问题的指导。

PS - 配置为与MSVC样式的volatile语义兼容的编译器将该限定符解释为指示对指针的访问可能具有与已经获取地址且未受到保护的对象交互的副作用restrict,无论是否有任何其他理由期待这种可能性。当以“不寻常”的方式访问存储时使用这样的限定符可能使人类读者更明显的是,代码同时做了“怪异”的事情,因此它将确保与使用这种语义的任何编译器兼容,即使此类编译器不会识别该访问模式。不幸的是,一些编译器编写者拒绝支持除优化级别0以外的任何其他语义,除非程序要求使用非标准语法。

© www.soinside.com 2019 - 2024. All rights reserved.