【问题标题】:Why doesn't strict aliasing rule apply to int* and unsigned*?为什么严格的别名规则不适用于 int* 和 unsigned*?
【发布时间】:2019-02-20 07:41:35
【问题描述】:

在 C 语言中,我们不能使用具有与该对象的有效类型不兼容的类型的左值表达式访问对象,因为这会产生未定义的行为。并且基于这一事实,严格的别名规则指出,如果两个指针具有不兼容的类型,则它们不能相互别名(引用内存中的同一个对象)。但在 C11 标准的 p6.2.4 中,允许使用带符号版本左值访问无符号有效类型,反之亦然。

由于最后一段,int *aunsigned *b 两个指针可能互为别名,其中一个指针指向的对象值的变化可能导致另一个指针指向的对象的值变化(因为是同一个对象)。

让我们在编译器级别进行演示:

int f (int *a, unsigned *b)
{
    *a = 1;
    *b = 2;

    return *a;
}

在 GCC 6.3.0 上使用 -O2 生成的上述函数的程序集如下所示:

0000000000000000 <f>:
   0:   movl   $0x1,(%rdi)
   6:   movl   $0x2,(%rsi)
   c:   mov    (%rdi),%eax
   e:   retq  

这是相当预期的,因为GCC没有优化返回值,在写入*b之后仍然会再次读取*a的值(因为*b的变化可能会导致*a的变化) .

但是有了这个其他功能:

int ga;
unsigned gb;

int *g (int **a, unsigned **b)
{
    *a = &ga;
    *b = &gb;

    return *a;
}

生成的程序集相当令人惊讶(GCC -O2):

0000000000000010 <g>:
  10:   lea    0x0(%rip),%rax        # 17 <g+0x7>
  17:   lea    0x0(%rip),%rdx        # 1e <g+0xe>
  1e:   mov    %rax,(%rdi)
  21:   mov    %rdx,(%rsi)
  24:   retq 

返回值经过优化,写入*b后不再读取。我知道 int *aunsigned *b 不是兼容的类型,但是 P6.2.4 段中的规则呢(允许使用带符号版本左值访问无符号有效类型,反之亦然)?为什么它不适用于这种情况?为什么编译器会在这种情况下进行这种优化?

关于兼容类型和严格别名的整个故事,我有些不明白。有人可以启发我们吗? (请解释为什么两个指针有不兼容的类型但可以相互别名,想想int *aunsigned *b)。

【问题讨论】:

  • unsigned * 不是无符号类型。
  • 在第一个示例中,bunsigned 不起作用。同样在第二个更复杂的例子中。 “指针别名”在哪里?
  • @WeatherVane 你什么意思,b 不参与? *b 的赋值是 asm 代码为返回值重新加载 *a 的唯一原因,这是因为别名。
  • @melpomene 什么别名?
  • 请阅读What is the strict aliasing rule? 中的示例,这可能有助于更好地阐明这些概念。

标签: c language-lawyer compiler-optimization undefined-behavior strict-aliasing


【解决方案1】:

给定int **aunsigned **b*a的类型不是*b的有效类型对应的有符号或无符号类型,*b也不是有效类型对应的有符号或无符号类型的*a。因此,这条允许通过相应的有符号或无符号类型进行别名的规则不适用。由于也没有其他允许使用别名的规则,编译器有权假设写入*b 不会修改*a,因此编译器在*a = &amp;ga; 中写入*a 的值仍然存在于*a对于return *a; 声明。

int * 指向有符号的int 的事实并不能使它成为有符号的类型。它是一个指针。 int *unsigned * 是指向不同类型的指针。即使它们被认为是有符号或无符号的,它们也会是指向不同类型的有符号或无符号指针:如果int * 是有符号指针,则它将是指向int 的有符号指针,相应的无符号版本将是无符号指针到int,而不是任何指向unsigned 的指针。

【讨论】:

  • 标准当然可以这样解释。鉴于有符号和无符号类型的历史,以及允许(并且在某些情况下是预期的)实现在超出标准规定的情况下定义行为的事实,这样做是有意义的,我不认为作者打算编译器像 gcc 和 clang 那样狭义地解释权限。
【解决方案2】:

要理解有符号/无符号豁免的预期含义,首先必须了解这些类型的背景。 C 语言最初没有“无符号”整数类型,而是设计用于在溢出时安静环绕的二进制补码机器上使用。虽然有一些操作,最值得注意的是关系运算符,除法,余数和右移,其中有符号和无符号的行为会有所不同,但对有符号类型执行大多数操作会产生与对无符号类型执行相同操作相同的位模式,从而最大限度地减少对后者的需求。

尽管无符号类型即使在安静环绕二进制补码机器上也确实有用,但它们在不支持安静环绕二进制补码语义的平台上是必不可少的。然而,因为 C 最初并不支持这样的平台,所以许多逻辑上“应该”使用的代码都使用了无符号类型,并且如果它们更早存在就会使用它们,因此被编写为使用有符号类型。该标准的作者不希望类型访问规则在使用有符号类型的代码之间产生任何困难,因为在编写时无符号类型不可用,而使用无符号类型的代码因为它们可用并且它们的使用会有道理。

intunsigned 互换处理的历史原因同样适用于允许使用unsigned* 类型的左值访问int* 类型的对象,反之亦然,使用@987654326 访问int** @ 等。虽然标准没有明确规定应允许任何此类用法,但它也忽略了提及显然应允许的一些其他用途,因此不能合理地视为完整和完整地描述了实现应支持的所有内容.

标准未能区分涉及基于指针的类型双关语的两种情况 - 那些涉及别名的情况,以及那些不涉及别名的情况 - 超出了非规范脚注说明规则的目的是表明当事情可能别名。区别如下图所示:

int *x;
unsigned thing;
int *usesAliasingUnlessXandPDisjoint(unsigned **p)
{
  if (x)
    *p = &thing;
  return x;
}

如果x*p 标识相同的存储,*px 之间将存在别名,因为p 的创建和通过*p 的写入将被冲突的访问分开使用左值x 的存储。但是,给定以下内容:

unsigned thing;
unsigned writeUnsignedPtr(unsigned **p)
{ *p = &thing; }

int *x;
int *doesNotUseAliasing(void)
{
  if (x)
    writeUnsignedPtr((unsigned**)&x);
  return x;
}

*p 参数和 x 之间不会有别名,因为在传递的指针 p 的生命周期内,x 和任何其他不是从 p 派生的指针或左值都不是用于访问与*p 相同的存储。我认为很明显标准的作者想要允许后一种模式。我认为他们是否想要允许前者甚至对于signedunsigned [而不是signed*unsigned*] 类型的左值,还是没有意识到将规则的应用限制在案例中,还不太清楚实际上涉及别名就足以允许后者。

gcc 和 clang 解释别名规则的方式并没有将intunsigned 之间的兼容性扩展到int*unsigned*——考虑到标准的措辞,这是一个允许的限制,但是—— - 至少在不涉及别名的情况下,我认为这与标准规定的目的相反。

*a*b 重叠的情况下,您的特定示例确实涉及别名,因为首先创建了a,并且在此类创建和最后一次使用*a 之间发生了通过*b 的冲突访问,或 b 是首先创建的,并且通过 *a 的访问冲突发生在此类创建和最后一次使用 b 之间。我不确定标准的作者是否打算允许这样的使用,但可以证明允许intunsigned 的理由同样适用于int*unsigned*。另一方面,gcc 和 clang 的行为似乎并不取决于标准作者的意思,如已发布的基本原理所示,而是取决于他们没有要求编译器这样做。

【讨论】:

    猜你喜欢
    • 2019-09-05
    • 2015-10-15
    • 2017-02-25
    • 2018-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    相关资源
    最近更新 更多