严格的别名规则是什么?

时间:2020-03-06 14:24:17  来源:igfitidea点击:

当问及C语言中常见的未定义行为时,比我提到的严格的别名规则更能启发人。
他们在说什么?

解决方案

严格的别名不允许不同的指针类型指向相同的数据。

本文应详细了解此问题。

通过指针强制类型转换(与使用联合相对)是打破严格别名的一个主要示例。

我发现的最好的解释是Mike Acton的《理解严格的别名》。它只专注于PS3开发,但这基本上只是GCC。

从文章:

"Strict aliasing is an assumption, made by the C (or C++) compiler, that dereferencing pointers to objects of different types will never refer to the same memory location (i.e. alias each other.)"

因此,基本上,如果我们有一个指向包含" int"的内存的" int *",然后将" float *"指向该内存并将其用作" float",则会违反规则。如果代码不遵守此规定,则编译器的优化器很可能会破坏代码。

该规则的例外是char *,它可以指向任何类型。

典型的情况是我们遇到严格的别名问题,就是将结构(如设备/网络消息)覆盖在系统字长的缓冲区(如指向uint32_t或者uint16_t的指针)上。当我们将结构覆盖到这样的缓冲区上,或者通过指针强制转换将缓冲区覆盖到这样的结构上时,我们很容易违反严格的别名规则。

因此,在这种设置中,如果要向某条消息发送消息,则必须有两个不兼容的指针指向相同的内存块。然后,我可能会天真地编写如下代码:

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

严格的别名规则使该设置非法:取消引用对不具有兼容类型或者C 2011 6.5段落71允许的其他类型之一的对象进行别名的指针是未定义的行为。不幸的是,我们仍然可以通过这种方式进行编码,也许会得到一些警告,可以对其进行良好的编译,而在运行代码时只会产生奇怪的意外行为。

(GCC发出锯齿警告的能力似乎有些不一致,有时会给我们友好的警告,有时则不会。)

要了解为什么未定义此行为,我们必须考虑严格的别名规则会给编译器带来什么。基本上,使用此规则,不必考虑在每次循环运行时插入指令来刷新buff的内容。取而代之的是,在进行优化时,可以使用一些令人讨厌的关于别名的非强制性假设,它可以忽略这些指令,在循环运行之前将buff [0]buff [1]加载到CPU寄存器中一次,并加速环形。在引入严格的别名之前,编译器必须处于一种偏执状态,即buff的内容可以随时随地由任何人更改。因此,为了获得额外的性能优势,并假设大多数人不键入双关指针,便引入了严格的别名规则。

请记住,如果我们认为该示例是人为设计的,那么即使我们将缓冲区传递给另一个函数来进行发送(如果我们有),甚至可能会发生这种情况。

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

并重写我们之前的循环以利用此便捷功能

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

编译器可能会或者可能不会或者足够聪明以尝试内联SendMessage,并且它可能会或者可能不会决定再次加载或者不加载buff。如果SendMessage是另一个单独编译的API的一部分,则它可能具有加载buff内容的指令。再说一次,也许我们使用的是C ++,这是编译器认为可以内联的仅模板头实现。或者,也许这只是我们为了自己的方便而在.c文件中编写的内容。无论如何,仍可能会发生未定义的行为。即使我们知道幕后发生的事情,也仍然违反规则,因此无法保证明确定义的行为。因此,仅通过包装一个使用我们的单词分隔缓冲区的函数并不一定会有所帮助。

那么我该如何解决呢?

  • 使用工会。大多数编译器都支持此功能,而不会抱怨严格的别名。这在C99中允许,在C11中明确允许。
union {
    Msg msg;
    unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
  • 我们可以在编译器中禁用严格别名(gcc中的f [no-] strict-aliasing)
  • 我们可以使用char *代替系统的单词来别名。该规则允许char *例外(包括signed charunsigned char)。一直假设char *是其他类型的别名。但是,这将无法以其他方式起作用:没有假设结构别名为char缓冲区。

初学者当心

当两种类型相互叠加时,这只是一个潜在的雷区。我们还应该了解字节序,单词对齐以及如何通过正确打包结构来处理对齐问题。

脚注

1 C 2011 6.5 7允许左值访问的类型为:

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