通过指针访问是否更改严格的别名语义?

时间:2018-08-14 16:34:47

标签: c language-lawyer strict-aliasing

具有以下定义:

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代码(“案例一” )是否正确:

foo->header.bits = 1020;

...实际上与该代码明显不同(“情况二” ):

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

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

  • 第一种情况认为分配不能影响Bar_Type中的头。仅被视为可以影响其他Foo_Type实例中的标头。

  • 第二种情况,通过强制通过通用别名指针进行访问,将使优化器认识到,对于可能包含struct My_Header的任何类型,所有赌注都不可用。它将与通过任何指针类型的访问同步。 (例如,如果您有一个Foo_Type指向实际上是一个Bar_Type的对象,则它可以通过标头进行访问并可靠地找到它所拥有的内容-假设标头位可以告诉你。)

这依赖于优化器不会变得“智能”,并使案例二回到案例一。

3 个答案:

答案 0 :(得分:3)

代码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将会是违反的。

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

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

答案 1 :(得分:2)

您有两个包含My_Header的结构,这是一个红色鲱鱼,使您的思考变得复杂,而且没有任何新内容。无需任何结构(当然My_Header除外)就可以陈述和阐明您的问题。

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是红色鲱鱼,没关系,请看此示例,该示例的分析与之前的案例完全相同,两者均不涉及{{1 }}或Foo_Type

Bar_type

答案 2 :(得分:1)

仅当使用字符类型的左值或通过调用memcpy等库函数执行访问时,才能定义访问N1570 p5.6p7的方式,即访问结构或联合的单个成员的代码的行为。 。即使结构或联合具有类型T的成员,标准(故意IMHO)仍不授予使用似乎无关的类型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()的情况下,实现可能是“符合”的,而无法处理v2.xunion U,但仅在某种意义上实现可能是“符合要求的”,而不能正确处理除某些特定的,无用的程序以外的任何东西。

不幸的是,尽管我认为标准的作者可能已经期望质量实现能够处理诸如test2a() / test2b()test1a() / {{1 }},gcc和clang都不可靠地支持它们的模式(*)。别名规则的既定目的是避免在没有证据的情况下,以及在别名可能性“可疑”的情况下,避免强迫编译器允许别名。我没有看到任何证据表明他们打算让质量编译器无法识别使用test1b()地址并使用该地址的代码,而该代码很可能与使用unionArr[i].v1的其他代码(当然明显与unionArr[i]相关联)。但是,gcc和clang的作者似乎认为,无需考虑此类内容就可以将其作为高质量的实现。

  

(*)例如

unionArr[i].v2
     

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