【问题标题】:Does access through pointer change strict aliasing semantics?通过指针访问会改变严格的别名语义吗?
【发布时间】:2018-08-14 16:34:47
【问题描述】:

使用这些定义:

struct My_Header { uintptr_t bits; }

struct Foo_Type { struct My_Header header; int x; }
struct Foo_Type *foo = ...;

struct Bar_Type { struct My_Header header; float x; }
struct Bar_Type *bar = ...;

说这段C代码对吗(“case one”):

foo->header.bits = 1020;

...实际上与此代码语义不同(“案例二”):

struct My_Header *alias = &foo->header;
alias->bits = 1020;

我的理解是它们应该是不同的:

  • 案例一认为分配无法影响 Bar_Type 中的标头。它仅被视为能够影响其他 Foo_Type 实例中的标头。

  • 情况二,通过强制通过通用别名指针进行访问,将导致优化器意识到所有可能包含struct My_Header 的类型的所有赌注都关闭。它将与通过任何指针类型的访问同步。 (例如,如果您有一个 Foo_Type 指向实际上是一个 Bar_Type,它可以通过标头访问并可靠地找出它所具有的 - 假设这是标头位可以告诉您的内容。)

这依赖于优化器没有变得“智能”并将案例二变成案例一。

【问题讨论】:

  • 嗯。我在这里看不到任何别名...
  • @EugeneSh。混叠是隐含的。通过 foo 写入标题,通过 bar 读取它们,看不到更新。
  • 指向结构的成员并不违反任何规则;您仍在通过指向My_Header 的指针访问My_Header,因此您并没有真正为任何东西添加别名。
  • 据我从C11 6.5.2.3p6 了解到,如果Bar_TypeFoo_Type 不能通过联合成员访问进行访问,编译器的行为就好像Bar_TypeFoo_Type 没有即使它们实际上重叠,也会在内存中重叠。
  • @HostileFork:编译器识别从另一种类型的左值明显派生的指针可能用于访问后一种类型的对象的情况的能力是实施质量问题。基本原理中没有任何内容表明他们认为编译器编写者会以规则为借口故意忽略这种推导可见的情况。从标准的角度来看,使用结构指针的代码和直接访问成员的代码具有等效(未定义)的行为,但质量编译器应该同时处理两者。

标签: c language-lawyer strict-aliasing


【解决方案1】:

代码bar->header.bits = 1020;struct My_Header *alias = &bar->header; alias->bits = 1020; 完全相同。

严格的别名规则是根据通过左值访问对象定义的:

6.5p7 对象应访问其存储的值 仅由具有以下之一的左值表达式 类型:

唯一重要的是左值的类型,以及左值指定的对象的有效类型。不是你是否将左值推导的一些中间阶段存储在指针变量中。


注意:自发布以下文本以来,该问题已被编辑。以下文字适用于malloc分配空间的原始问题,而不是截至8月23日的当前问题。

关于代码是否正确。您的代码相当于 N2013 rev 1571 中的 Q80 effective_type_9.c,这是对现有 C 实现的调查,着眼于起草改进的严格别名规则。

Q80。将结构写入 malloc 区域后,是否可以通过指向不同结构类型的指针访问其成员,该结构类型在相同偏移处具有相同叶成员类型?

绊脚石是代码(*bar).header.bits = 1020;是否只设置了int位的有效类型;或整个*bar。因此,读取(*foo).header.bits 是否读取int,还是读取整个*foo

只读取int 不会是严格的别名违规(可以将int 读取为int);但是将Bar_Struct 读取为Foo_Struct 将是违规行为。

本文的作者认为 write 为整个 *bar 设置有效类型,尽管他们没有为此提供理由,而且我在 C 标准中没有看到任何文本支持该立场。

在我看来,目前对于您的代码是否正确没有明确的答案。

【讨论】:

  • “你会想”,但我在 2016 年有一个真实案例,将代码从案例一更改为案例二会导致之前没有同步的同步。这个问题的起源是我调整了一些东西以通过 char* 使用字节访问,因此(我认为)使这个问题无关紧要......但是......当时我通过说出它起作用的原因来证明修改是合理的是我认为通过指针访问使它工作......因为我无法弄清楚为什么否则你可以通过指针保证突变会起作用。正是这个问题让我停了下来。
  • @HostileFork 似乎一些编译器供应商也认为Q.80的答案是“否”(并且您的两种代码形式都是UB)
  • @HostileFork:许多技巧会导致迟钝的编译器在一段时间内错过“优化”,直到作者“修复”它们,但这是使用像 icc 这样支持的更高质量编译器的唯一可靠方法低级构造。
  • @M.M:据我所知,“有效类型”规则从未准确描述所有极端情况下的真实编译器行为。重要的是编译器想要重新排序访问以与另一个访问合并,或者将其从循环或函数中提升出来,并且需要知道它是否会与其他任何内容冲突,这又取决于编译器是否查看两个兴趣点会发现任何冲突的证据。
  • @HostileFork 我确实回答了原始表格,最好不要在发布答案后对问题进行重大编辑,所以我同意恢复
【解决方案2】:

您有两个包含My_Header 的结构这一事实是一个红鲱鱼,会使您的思维复杂化,而不会带来任何新的东西。您的问题可以在没有任何结构的情况下陈述和澄清(My_Header ofcourse 除外)。

foo->header.bits = 1020;

编译器清楚地知道要修改哪个对象。

struct My_Header *alias = &foo->header;
alias->bits = 1020;

这里同样如此:通过非常基本的分析,编译器可以准确地知道alias->bits = 1020; 修改了哪个对象。

有趣的部分来了:

void foo(struct My_Header* p)
{
   p->bits = 1020;
}

在此函数中,指针p 可以为My_header 类型的任何对象(或子对象)起别名。如果您有 N 个包含 My_header 成员的结构,或者您没有成员,这并不重要。 My_Header 类型的任何对象都可能在此函数中被修改。

例如

// global:
struct My_header* global_p;

void foo(struct My_Header* p)
{
   p->bits = 1020;
   global_p->bits = 15;

   return p->bits;
   // the compiler can't just return 1020 here because it doesn't know
   // if `p` and `global_p` both alias the same object or not.
}

为了让您相信 Foo_TypeBar_Type 是红鲱鱼,不要紧看这个例子,它的分析与前面的案例相同,既不涉及 Foo_Type 也不涉及 Bar_type

// global:
struct My_header* gloabl_p;

void foo(struct Foo_Type* foo)
{
   foo->header.bits = 1020;
   global_p->bits = 15;

   return foo->header.bits;
   // the compiler can't just return 1020 here because it doesn't know
   // if `foo.header` and `global_p` both alias the same object or not.
}

【讨论】:

  • 好吧,问题不在于我传递指向 My_Header 的指针,而是传递指向 Foo_Type 的指针(从 malloc 的区域中提取),这实际上可能是通过标头初始化的东西Bar_Type 中的指针。我希望在读取 foo 标头时可以保证看到这些位的写入。我的直觉是优化器不会尊重通过指针的访问,因为在简化的示例中是不同的,你说缺乏尊重是由于分析了它知道什么和不知道什么......指针规则放在一边.
  • @HostileFork 我......并没有真正关注你。你可以用你的真实用例发布一个问题。很难理解您的代码到底是什么以及您要避免什么问题。
  • C 中解引用指针的要求是指向有效对象,否则为 UB。这正是为了避免优化这些访问的问题(嗯,原因之一)。
  • @bolov 我试图用我认为显而易见的东西来更新它,但显然不是。希望更清楚。
  • 我认为该模式是标准的作者预期某些编译器可能会优化的模式,并且不打算禁止它(请注意,没有毯子 允许使用聚合成员类型的任意左值来访问聚合。从另一个派生的左值通常不被视为“别名”后者,除非后者在前者的活动生命周期内使用, 并且 N1570 6.5p7 的脚注表明规则旨在指示编译器何时必须允许别名。在你的最后一个例子中......
【解决方案3】:

按照 N1570 p5.6p7 的编写方式,只有在使用字符类型的左值或调用 memcpy 等库函数执行访问时,才会定义访问结构或联合的单个成员的代码行为。即使结构或联合具有T 类型的成员,标准(故意恕我直言)避免使用看似不相关的T 类型的左值访问聚合存储的那部分权限。目前,gcc 和 clang 似乎授予使用成员类型的左值访问结构而非联合的全面权限,但 N1570 p5.6p7 不需要这样做。它将相同的规则应用于两种聚合及其成员。由于标准不授予使用不相关的成员类型左值访问结构的全面权限,并且授予此类权限会损害有用的优化,因此不能保证 gcc 和 clang 将继续使用不相关的成员类型左值的行为。

不幸的是,正如使用联合可以证明的那样,gcc 和 clang 在识别不同类型的左值之间的关系方面非常差,即使一个左值明显是从另一个左值派生的。给定类似的东西:

struct s1 {short x; short y[3]; long z; };
struct s2 {short x; char y[6]; };
union U { struct s1 v1; struct s2 v2; } unionArr[100];
int i;

标准中没有任何内容可以区分以下成对函数的“别名”行为:

int test1(int i)
{
  return unionArr[i].v1.x;
}
int test2a(int j)
{
  unionArr[j].v2.x = 1;
}

int test2a(int i)
{
  struct s1 *p = &unionArr[i].v1;
  return p->x;
}
int test2b(int j)
{
  struct s2 *p = &unionArr[j].v2;
  p->x = 1;
}

它们都使用int 类型的左值来访问与struct s1struct s2union Uunion U[100] 类型的对象关联的存储,即使int 未列为访问其中任何一个的允许类型。

虽然即使是第一种形式也会调用 UB 似乎很荒谬,但如果人们认识到对超出标准中明确列出的访问模式的支持作为实施质量问题的访问模式,那应该不是问题。根据公布的基本原理,标准的作者认为编译器编写者会尝试生成高质量的实现,因此没有必要禁止“符合”的实现质量低到无用。一个实现可能是“符合”的,但无法处理test1a()test2b(),如果他们将访问union U 的成员v2.x,但只是在某种意义上,一个实现可能是“符合”的除了一些特定的人为和无用的程序之外,无法正确处理任何东西。

不幸的是,尽管我认为标准的作者可能已经预料到高质量的实现将能够处理像 test2a()/test2b()test1a()/test1b() 这样的代码,gcc 和 clang 都不支持他们可靠地模式(*)。别名规则的既定目的是避免在没有证据的情况下强制编译器允许使用别名,并且别名的可能性是“可疑的”[可疑]。我没有看到任何证据表明他们打算让质量编译器无法识别采用unionArr[i].v1 地址并使用它的代码很可能访问与使用unionArr[i] 的其他代码相同的存储(当然是, 明显与unionArr[i].v2) 相关联。然而,gcc 和 clang 的作者似乎认为无需考虑这些事情就可以实现高质量的实现。

(*) 给定例如

int test(int i, int j)
{
  if (test2a(i))
    test2b(j);
  return test2a(i);
}

gcc 和 clang 都不会识别如果 i==j,test2b(j) 将访问与 test2a(i) 相同的存储,即使两者都将访问同一数组的相同元素。

【讨论】:

  • 我认为您提供了一个您确定的示例(并已与 gcc 和 clang 进行了检查),这似乎没有那么争议——但仍然不起作用。因此,我所描述的类似,可能在 gcc 或 clang 中也不能被视为理所当然......?
  • @HostileFork:指示的test2 函数在大多数 上下文中可以正常工作,但它并不可靠。如果它被调用两次,gcc 和 clang 都将优化从 unionArr[i] 重新推导 p,因此不会识别它可能与获取地址的代码交互987654348@ 并写信给它。
  • @HostileFork:添加了 test1b() 和 test2b(),它们应该类似于 test1atest2a。在 i==j 的情况下,gcc 和 clang 都会被序列 test2a(i); test2b(j); test2a(i); 绊倒。
  • 联合绕过严格的别名,所以这与问题无关
  • @M.M:标准不承认直接使用聚合成员与获取地址并使用它之间的区别,其别名规则也不区分结构和联合。相反,支持几乎任何类型的成员访问都是实施质量问题。碰巧的是,当前版本的 gcc 和 clang 支持使用指向 struct 成员的指针比使用 struct-member-access 左值更灵活的语义,但使用指向联合成员的指针的语义不如 union-member-access 左值灵活。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 2021-08-18
  • 2023-02-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多