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

时间:2018-08-08 02:44:12

标签: c pointers struct language-lawyer c11

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

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字段到另一个struct字段的缓冲区溢出:

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

我希望它会在几乎任何现实世界的编译器上输出"rld!",但是该行为是否由标准保证?或者是未定义或实现定义的行为? < / p>

2 个答案:

答案 0 :(得分:10)

简介:该标准在该领域还不够完善,对此主题的争论已有数十年历史,并且在没有令人信服的解决方案或修正建议的情况下进行严格的混叠。

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


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

规则是C11 6.5.6 / 8。它说从一个指针开始的索引必须保留在“数组对象”内(或末尾)。它没有说出哪个数组对象,但通常认为在int *p = &foo.a;的情况下,“数组对象”是foo.a,而不是其中的任何较大对象{ {1}}是一个子对象。

相关链接: onetwo


第二:通常,您的两个foo.a示例都是正确的。该标准明确规定,工会的任何成员都可以阅读;以及相关内存位置的任何内容均被解释为正在读取的联合成员的类型。


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

即使我们知道union&foo.a + 1是相同的内存地址,也可以有效地通过第二秒访问&foo.b,而不能通过第二次访问int首先。

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

int

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

相关链接:onetwo


对于((int *)((uintptr_t)&foo.a + sizeof(int)))[0] 是否有效,不是普遍一致。有人说它与您的第一个代码基本相同,因为该标准说“指向适当转换后的对象的指针,指向该元素的第一个对象”。其他人则说它与我上面的((int *)&foo)[1]示例基本相同,因为它遵循指针转换的规范。甚至有人声称这是严格的别名冲突,因为它将结构别名为数组。

可能与N2090 - Pointer provenance proposal相关。这不能直接解决该问题,也不会建议废除6.5.6 / 8。

答案 1 :(得分:3)

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

但是,根据已发布的Rationale文档,该标准的作者认识到,在标准不施加任何要求的情况下,不同的实现方式提供了不同的行为保证,并且将这种“受欢迎的扩展”视为一件好事和有用的事情。他们认为,与何时由委员会相比,由市场来更好地回答何时以及如何支持这种扩展的问题。尽管该标准允许钝的编译器忽略someStruct.array[i]可能影响someStruct的存储值的可能性似乎很奇怪,但该标准的作者认识到,其作者不是故意编写的。无论标准是否规定,钝化都将支持这种构造,并且从钝化设计的编译器中强制实施任何有用行为的任何尝试都是徒劳的。

因此,编译器对实质上与结构或联合有关的所有内容的支持水平是实现质量的问题。致力于与多种程序兼容的编译器作者将支持多种构造。那些只专注于最大化只需要那些语言完全没有用的结构的代码的性能的人,将支持更狭窄的集合。但是,该标准缺乏有关此类问题的指导。

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