【问题标题】:Is it legal to access struct members via offset pointers from other struct members?通过来自其他结构成员的偏移指针访问结构成员是否合法?
【发布时间】:2019-01-15 04:49:09
【问题描述】:

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

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. 结构布局需要一致,所以&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!",但这种行为是由标准保证的,还是未定义或实现定义的行为?

【问题讨论】:

  • @M.M 第二部分的原因是联合代码的假定有效性似乎暗示第一个样本也应该有效。不过,我想将另一个仅询问联合代码有效性的问题分开可能是有意义的。
  • @AJMansfield 标准中没有这样的含义;工会有特殊规定
  • foo.arr[1] = 1; 是 UB,没有规定下一个成员是 foo.arr[1]
  • @chux 可以用assert 解决异议;或者注意写入填充字节是合法的
  • @M.M Legal 写入填充。嗯。我怀疑将任何位模式写入填充是可以的。有时这是隐藏检查位的地方。也许是个好问题。

标签: c pointers struct language-lawyer c11


【解决方案1】:

简介:该领域的标准不足,关于该主题已有数十年的争论历史和严格的别名,没有令人信服的解决方案或修复建议。

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


首先:人们普遍认为,您的第一个代码示例中的代码是未定义的行为,因为通过直接指针算术访问数组边界之外。

规则是 C11 6.5.6/8 。它说来自指针的索引必须保留在“数组对象”中(或结束后的一个)。它没有说 哪个 数组对象,但普遍认为在int *p = &foo.a; 的情况下,“数组对象”是foo.a,而不是foo.a 是一个更大的对象子对象。

相关链接: one, two.


其次:人们普遍认为您的两个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。

【讨论】:

  • 很好的答案,我想补充一点,C 委员会最近成立了一个“内存模型工作组”来讨论这类问题,并为 C2x 提出一个更具决定性的模型。
  • @JensGustedt 酷,期待看到他们想出什么:)
  • 认为第一个例子正确的原因是因为foo.arr保证与&foo.a相同,并且foo.arr + 1指向与&foo.b相同的位置,然后替换foo.arr&foo.a,如&foo.a + 1,也应保证指向与&foo.b 相同的位置;并且由于内部结构与数组的联合不应该改变结构的内存布局,所以这个表达式在没有它的情况下也应该是合法的。我将编辑我的问题,使推理链更加明确。
  • 直到 C99,几乎普遍认为,用于各种目的的编译器需要支持超出标准对所有编译器强制要求的类型使用模式,并且对此类模式的支持被认为是实施质量问题。在专门用于或配置用于高端数字运算的编译器中适用的限制将使编译器无法处理低级内存管理代码。缺乏识别不同类型实现的意愿,任何试图解决单一规则集的尝试......
  • ...因为几乎可以保证一切都会破坏大量代码,同时不必要地损害许多优化。标准的一部分允许编译器以任意方式处理其行为将在其他地方定义的动作这一事实并不意味着质量编译器旨在用于任何特定目的的任何判断应该 这样做。
【解决方案2】:

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多