这个struct-field-aliasing代码以什么方式调用Undefined Behavior

时间:2015-11-26 23:14:32

标签: gcc language-lawyer strict-aliasing

鉴于代码:

<o> because <o href="url"></o> just doesn't work, even when adding the styling for cursor hover and o:focus {etc..} and o:hover {etc..}

行为取决于gcc优化级别,这意味着gcc认为 此代码调用Undefined Behavior(“foo”的定义折叠为空,但有趣的是“hey”的定义会增加传入的值)。不过,我不太确定它与标准规则有什么关系。

代码非常刻意和邪恶地构造了两个指针 s2a&gt; y和s2b-&gt; x将别名,但指针是故意构造的,两者都识别INTPAIR类型的合法潜在对象。因为代码使用calloc来获取内存,所以所有字段成员都具有合法的初始定义值零。对分配的内存的所有访问都是通过INTPAIR *的int32_t成员完成的。

我可以理解为什么标准以这种方式禁止别名结构字段是有意义的,但我在标准中找不到实际上这样做的任何内容。 gcc是否以符合标准的方式运行,或者是否违反了标准中的某些条款,该条款未被附件J.2引用,并且不使用我搜索的任何条款?

2 个答案:

答案 0 :(得分:0)

<强>更新 我觉得这个答案还可以,但不是仍然有点不精确,并且不会因为UB是什么而切断干燥。经过很多非常有趣的讨论和评论之后,我再次尝试了new answer

C99标准的右边部分引用了answer。为方便起见,我在这里复制它。问题和几个答案都非常彻底。

(C99; ISO / IEC 9899:1999 6.5 / 7:

  

对象的存储值只能由左值访问   表达式具有以下类型之一 73)或88)

     
      
  • 与对象的有效类型兼容的类型
  •   
  • 与有效类型兼容的类型的合格版本   对象,
  •   
  • 一种类型,是与之对应的有符号或无符号类型   有效的对象类型,
  •   
  • 与a对应的有符号或无符号类型的类型   对象的有效类型的合格版本,
  •   
  • 包含上述之一的聚合或联合类型   其成员之间的类型(包括,递归地,成员   subaggregate或contains union),或
  •   
  • 字符类型。
  •   
     

73)或88)此列表的目的是指定对象可能存在或不存在别名的情况。

那么什么是有效类型? (C99; ISO / IEC 9899:1999 6.5 / 6:

  

访问其存储值的对象的有效类型是对象的声明类型(如果有)。 87)如果通过具有非字符类型的左值的值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及后续访问的有效类型修改存储的值。如果使用memcpy或memmove将值复制到没有声明类型的对象中,或者将其复制为字符类型数组,则该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是复制值的对象的有效类型(如果有)。对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

     

87)分配的对象没有声明的类型。

因此,在p2b->x = x行,p + 4处的对象变为有效类型INTPAIR。它是否正确对齐?如果它不是未定义的行为(UB)。但为了保持它的趣味性,假设它在这种情况下必须是因为INTANDPAIR的布局。

通过相同的分析,有两个8字节的对象,@(p + 4)和p2b @p的p2a(s2)。正如你的例子展示p2a的第二个元素,p2b的第一个元素最终被别名化。

foo()中,通过s1->x通过常规方法访问对象p2b @ p + 4。但那么&#34;储值&#34;也可以通过修改不同对象p2a @p的副作用来访问对象p2b。因为这不属于6.5 / 7的子弹,所以它是UB。请注意,6.5 / 7仅表示 ,因此不得以任何其他方式访问

我认为主要区别在于&#34;对象&#34;问题是整个结构p2a / s2和p2b / s1,而不是整数成员。如果你改变函数的参数来取整数和别名它的工作&#34;罚款&#34;因为该函数不能知道s1和s2别名。例如:

void foo2(int *s1, int *s2)
{
  (*s2)++;
  (*s1)^=1;
  (*s2)--;
  (*s1)^=1;
}
  ...
/*foo(p2b,p2a);*/
foo2((int*)p, (int*)p); /* or p+4 or whatever you want */

这或多或少证实了这是GCC选择解释事物的方式:修改成员正在修改整个结构对象,并且因为修改一个对象的副作用不在列出的合法方式上间接修改不同的对象哇!我们可以做任何我们想做的傻事。

因此,GCC是否解释标准中的歧义以决定通过不同类型指针导出s1和s2指针然后访问它们构成间接通过p1和p通过不同的原始类型访问内存或是否它以我建议&#34;对象&#34;的方式解释标准。 s2->y修改不仅仅是整数而是s2对象,它是UB的任何一种方式。或者GCC只是特别讽刺并且指出如果标准没有非常清楚地指定动态分配但重叠的对象的语义,它可以自由地做它想要的任何事情,因为根据定义它是&#34;未定义&# 34。

我不认为在这个微观层面上除了标准组织之外的任何人都可以明确地回答这是否应该是UB,因为在这个级别它需要一些&#34;解释&# 34 ;.海湾合作委员会的实施者意见似乎倾向于非常积极的解释。

我喜欢Linus对这件事的反应。确实,为什么不保守,让程序员告诉编译器何时安全? Very Excellent Linus Rant

答案 1 :(得分:0)

我之前的answer缺乏,也许并非完全错误,但是样本程序是故意设计的,以回避C99标准规定的每个更明显的显式未定义行为(UB),如6.5 / 7。但是对于GCC(和Clang),这个例子展示了严格的混叠失败,如优化下的症状。他们似乎假设s1-> y和s2-x可以是别名。那么,编译器是错误的吗?这是一个严格别名法律术语的漏洞吗?

简短的回答:不会。鉴于标准的复杂性,如果标准中存在某种漏洞,我不会感到惊讶。但是在这个例子中,在堆上创建重叠对象是明确未定义的行为,并且还有其他一些事情正在发生,标准没有定义。

我认为这个例子的重点并不在于它失败了 - 很明显&#34;快速而宽松地玩耍&#34;指针是一个坏主意,依靠角落案件和法律术语来证明编译错误&#34;如果代码不起作用,则没什么用处。关键问题是:GCC错了吗?和标准中的是这样说的。

首先,让我们看一下明显严格的别名规则以及这个例子试图避免这些规则的方法。

C99 6.5 / 7:

  

对象的存储值只能由具有以下类型之一的左值表达式访问: 76)

     
      
  • 与对象的有效类型兼容的类型
  •   
  • 与对象的有效类型兼容的类型的限定版本,
  •   
  • 与对象的有效类型对应的有符号或无符号类型
  •   
  • 与对象的有效类型的限定版本对应的有符号或无符号类型的类型,
  •   
  • 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或
  •   
  • 字符类型。
  •   

这是主要的严格别名部分。这意味着通过两个不同类型的指针访问相同的内存是UB。此示例通过使用foo()中的INTPAIR指针访问它们来回避它。

这个问题的关键在于它正在谈论通过两种不同的有效类型(例如指针)访问存储的值。它没有谈论通过两个不同的对象进行访问。

正在访问什么?是整数成员还是整个对象s1 / s2?通过s1-> y访问s2-&gt; x通过&#34;与对象的有效类型兼容的类型&#34;。我认为可以提出一个论点:a)作为修改不同对象的副作用的访问不属于6.5 / 7中允许的方法,b)修改聚合的一个成员可以传递修改聚合(* s1或* s2)也。

由于没有指定,它是UB,但它有点手工波浪。

我们如何获得指向两个重叠对象的指针?指针投射导致它们好吗?第6.3.2.3节包含了用于转换指针的规则,并且该示例小心地不违反它们中的任何一个。特别是,因为p2b是一个指向INTANDPAIR成员xy的指针,所以对齐保证是正确的,否则它肯定会与6.3.2.3/7冲突。

此外,&amp; p1-&gt; xy不是问题 - 它不可能 - 它是指向INTPAIR的完全合法的指针。简单地转换指针和/或获取地址是安全地超出&#34;访问&#34;的定义。 (3.1 / 1)。

很明显,问题是通过访问两个相互重叠的整数成员作为重叠对象的不同部分来实现的。任何通过不同类型的指针做这件事的尝试都会明显与6.5 / 7相冲突。如果在同一地址使用相同类型的指针访问,则不会有任何问题。因此,他们可以通过这种方式使用别名的唯一方法是,如果不同地址的两个对象以某种方式重叠。

显然,这可能是联盟的一部分,但这个例子并非如此。在C99中,通过工会打字的类型可能不是UB,但是这个例子的变体是否可以通过工会行为不当会是一个不同的问题。

该示例使用动态分配并将结果void指针强制转换为两种不同的类型。从指向对象的指针变为void *并再次返回是有效的(6.3.2.3/1)。获取指向重叠对象的指针的其他几种方法是6.3.2.3的指针转换规则,6.5 / 7的别名规则和/或兼容类型规则6.2.7。显式UB。

那还有什么不对?

  

6.2.4对象的存储持续时间

     

1对象具有确定其生命周期的存储持续时间。存储持续时间有三种:静态,自动和已分配。分配的存储在7.20.3中描述

每个对象的存储空间由calloc()分配,因此我们想要的持续时间是&#34;已分配&#34;。所以我们检查7.20.3 :(强调添加)

  

7.20.3内存管理功能

     

1未指定连续调用calloc,malloc和realloc函数分配的存储的顺序和连续性。如果分配成功,则返回的指针被适当地对齐,以便可以将其指定给指向任何类型对象的指针,然后用于在分配的空间中访问此类对象或此类对象的数组(直到空间被显式释放) 。分配对象的生命周期从分配延伸到解除分配。 每个此类分配都应生成一个指向与任何其他对象不相交的对象的指针

     

...

     

2对象的生命周期是程序执行的一部分,在此期间保证为其保留存储。存在一个对象,具有一个常量地址 25)并在其整个生命周期内保留其最后存储的值。 26) 如果某个对象在其生命周期之外被引用,则该行为未定义

为避免UB,对两个不同对象的访问必须是其生命周期内的有效对象。您可以使用malloc()/ calloc()获得单个有效对象(或数组),但这些可以保证您将收到与所有其他对象不相交的指针。那么从calloc()p返回的对象还是p1?它不可能都是。

通过尝试重用相同的动态分配对象来保存两个不相交的对象来触发UB。虽然calloc()保证它会返回一个指向不相交对象的指针,但如果你开始使用缓冲区的某些部分进行第二个重叠对象,则没有任何内容表明它仍然可以工作。实际上,它甚至明确地说如果你在其生命周期之外访问一个对象,那么它就是UB,而且只有一个分配就是一个生命周期。

另请注意:

  

<强> 4。一致性

     
      
  1. 在本国际标准中,''shall'应被解释为对实施或计划的要求;相反,''不应'被解释为禁止。
  2.   
  3. 如果违反了约束之外的“应该”或“不应该”的要求,则行为未定义。未定义的行为在本国际标准中以“未定义行为”或省略任何明确定义的方式表示   行为。这三者之间的重点没有区别;他们都描述''未定义的行为''。
  4.   

对于这是一个编译器错误,它必须在仅使用显式定义的构造的程序上失败。其他任何东西都在安全港之外并且仍未定义,即使标准没有明确说明它是未定义的行为。