为什么严格的别名规则不适用于 int* 和 unsigned*?

Why doesn't strict aliasing rule apply to int* and unsigned*?

在 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   [=11=]x1,(%rdi)
   6:   movl   [=11=]x2,(%rsi)
   c:   mov    (%rdi),%eax
   e:   retq  

这是意料之中的,因为 GCC 没有优化 return 值并且在写入 *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 

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

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

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

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

的任何指针

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

虽然无符号类型即使在安静环绕二进制补码机器上也确实有用,但在不支持安静环绕二进制补码语义的平台上它们是必不可少的。但是,由于 C 最初不支持此类平台,因此许多代码逻辑上 "should" 使用了无符号类型,并且如果它们早点存在就会使用它们,但被编写为使用有符号类型。标准的作者不希望类型访问规则在使用有符号类型的代码和使用无符号类型的代码之间造成任何接口困难,因为无符号类型在编写时不可用,因为它们可用并且它们的使用会有道理。

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

标准未能区分涉及基于指针的类型双关的两种情况——涉及别名的情况和不涉及别名的情况——除了非规范性脚注说规则的目的是指示事物何时可能会别名。区别如下图所示:

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

如果 x*p 标识相同的存储,则 *px 之间会出现别名,因为 p 的创建和写入via *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 并且通过 *b 发生冲突访问*a 的创建和最后一次使用,或者 b 是首先创建的,并且在此类创建和 b 的最后一次使用之间发生了通过 *a 的冲突访问。我不确定该标准的作者是否打算允许这种用法,但可以证明允许 intunsigned 的相同理由同样适用于 int*unsigned*。另一方面,gcc 和 clang 的行为似乎并不是由标准的作者按照已发布的基本原理所表达的意思来决定的,而是由他们没有要求编译器做的事情决定的。